Skip to content
This repository has been archived by the owner on Sep 15, 2023. It is now read-only.

Commit

Permalink
pull timestamp definition out of yaml itself
Browse files Browse the repository at this point in the history
Little bit hacky, but at least don't have to manage a copy of it.

See: eemeli/yaml#475
  • Loading branch information
isaacs committed May 25, 2023
1 parent ffc70c6 commit fd6b3e6
Showing 1 changed file with 7 additions and 126 deletions.
133 changes: 7 additions & 126 deletions src/types/timestamp.ts
@@ -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

0 comments on commit fd6b3e6

Please sign in to comment.