This repository has been archived by the owner on Sep 15, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pull timestamp definition out of yaml itself
Little bit hacky, but at least don't have to manage a copy of it. See: eemeli/yaml#475
- Loading branch information
Showing
1 changed file
with
7 additions
and
126 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,126 +1,7 @@ | ||
// this is identical to the !!timestamp tag that ships with yaml, but | ||
// we mark it as not default, so that it will not stringify as a plain | ||
// old string. | ||
// Ignore the file for coverage, because it's just a copypasta from | ||
// the yaml package. If https://github.com/eemeli/yaml/issues/475 is | ||
// resolved, then this file can go poof. | ||
/* c8 ignore start */ | ||
import type { Scalar, ScalarTag } from 'yaml' | ||
import { stringifyNumber } from 'yaml/util' | ||
|
||
/** Internal types handle bigint as number, because TS can't figure it out. */ | ||
function parseSexagesimal<B extends boolean>(str: string, asBigInt?: B) { | ||
const sign = str[0] | ||
const parts = sign === '-' || sign === '+' ? str.substring(1) : str | ||
const num = (n: number | string) => | ||
asBigInt ? (BigInt(n) as unknown as number) : Number(n) | ||
const res = parts | ||
.replace(/_/g, '') | ||
.split(':') | ||
.reduce((res, p) => res * num(60) + num(p), num(0)) | ||
return (sign === '-' ? num(-1) * res : res) as B extends true | ||
? number | bigint | ||
: number | ||
} | ||
|
||
/** | ||
* hhhh:mm:ss.sss | ||
* | ||
* Internal types handle bigint as number, because TS can't figure it out. | ||
*/ | ||
function stringifySexagesimal(node: Scalar) { | ||
let { value } = node as Scalar<number> | ||
let num = (n: number) => n | ||
if (typeof value === 'bigint') num = n => BigInt(n) as unknown as number | ||
else if (isNaN(value) || !isFinite(value)) return stringifyNumber(node) | ||
let sign = '' | ||
if (value < 0) { | ||
sign = '-' | ||
value *= num(-1) | ||
} | ||
const _60 = num(60) | ||
const parts = [value % _60] // seconds, including ms | ||
if (value < 60) { | ||
parts.unshift(0) // at least one : is required | ||
} else { | ||
value = (value - parts[0]) / _60 | ||
parts.unshift(value % _60) // minutes | ||
if (value >= 60) { | ||
value = (value - parts[0]) / _60 | ||
parts.unshift(value) // hours | ||
} | ||
} | ||
return ( | ||
sign + | ||
parts | ||
.map(n => String(n).padStart(2, '0')) | ||
.join(':') | ||
.replace(/000000\d*$/, '') // % 60 may introduce error | ||
) | ||
} | ||
|
||
export const intTime: ScalarTag = { | ||
identify: value => typeof value === 'bigint' || Number.isInteger(value), | ||
tag: 'tag:yaml.org,2002:int', | ||
format: 'TIME', | ||
test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/, | ||
resolve: (str, _onError, { intAsBigInt }) => | ||
parseSexagesimal(str, intAsBigInt), | ||
stringify: stringifySexagesimal, | ||
} | ||
|
||
export const floatTime: ScalarTag = { | ||
identify: value => typeof value === 'number', | ||
default: false, | ||
tag: 'tag:yaml.org,2002:float', | ||
format: 'TIME', | ||
test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/, | ||
resolve: str => parseSexagesimal(str, false), | ||
stringify: stringifySexagesimal, | ||
} | ||
|
||
export const timestamp: ScalarTag & { test: RegExp } = { | ||
identify: value => value instanceof Date, | ||
default: false, | ||
tag: 'tag:yaml.org,2002:timestamp', | ||
|
||
// If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part | ||
// may be omitted altogether, resulting in a date format. In such a case, the time part is | ||
// assumed to be 00:00:00Z (start of day, UTC). | ||
test: RegExp( | ||
'^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})' + // YYYY-Mm-Dd | ||
'(?:' + // time is optional | ||
'(?:t|T|[ \\t]+)' + // t | T | whitespace | ||
'([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)' + // Hh:Mm:Ss(.ss)? | ||
'(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?' + // Z | +5 | -03:30 | ||
')?$' | ||
), | ||
|
||
resolve(str) { | ||
const match = str.match(timestamp.test) | ||
if (!match) | ||
throw new Error('!!timestamp expects a date, starting with yyyy-mm-dd') | ||
const [, year, month, day, hour, minute, second] = match.map(Number) | ||
const millisec = match[7] ? Number((match[7] + '00').substring(1, 4)) : 0 | ||
let date = Date.UTC( | ||
year, | ||
month - 1, | ||
day, | ||
hour || 0, | ||
minute || 0, | ||
second || 0, | ||
millisec | ||
) | ||
const tz = match[8] | ||
if (tz && tz !== 'Z') { | ||
let d = parseSexagesimal(tz, false) | ||
if (Math.abs(d) < 30) d *= 60 | ||
date -= 60000 * d | ||
} | ||
return new Date(date) | ||
}, | ||
|
||
stringify: ({ value }) => | ||
(value as Date).toISOString().replace(/((T00:00)?:00)?\.000Z$/, ''), | ||
} | ||
/* c8 ignore stop */ | ||
// this just sets the !!timestamp tag to be not considered a default, | ||
// so that we don't confuse date strings and actual dates. | ||
// See: https://github.com/eemeli/yaml/issues/475 | ||
import { Schema } from 'yaml' | ||
const schema = new Schema({ resolveKnownTags: true }) | ||
export const timestamp = schema.knownTags['tag:yaml.org,2002:timestamp'] | ||
timestamp.default = false |