Skip to content

Commit 581397a

Browse files
authored
feat: add picosecond support (#302)
1 parent 22a7cdf commit 581397a

2 files changed

Lines changed: 171 additions & 8 deletions

File tree

core/precise-date/src/index.ts

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
const FULL_ISO_REG = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d{4,9}Z/;
17+
const FULL_ISO_REG = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d{4,12}Z/;
1818
const NO_BIG_INT =
1919
'BigInt only available in Node >= v10.7. Consider using getFullTimeString instead.';
2020

@@ -101,6 +101,7 @@ enum Sign {
101101
export class PreciseDate extends Date {
102102
private _micros = 0;
103103
private _nanos = 0;
104+
private _picos = 0;
104105
constructor(time?: number | Date);
105106
constructor(preciseTime: string | bigint | DateTuple | ProtobufDate);
106107
constructor(
@@ -113,13 +114,20 @@ export class PreciseDate extends Date {
113114
milliseconds?: number,
114115
microseconds?: number,
115116
nanoseconds?: number,
117+
picoseconds?: number,
116118
);
117119
constructor(
118120
time?: number | string | bigint | Date | DateTuple | ProtobufDate,
119121
) {
120122
super();
121123

122124
if (time && typeof time !== 'number' && !(time instanceof Date)) {
125+
if (typeof time === 'string' && isFullISOString(time)) {
126+
const pd = parseFullISO(time as string);
127+
this.setFullTime(pd.getFullTimeString());
128+
this.setPicoseconds(pd.getPicoseconds());
129+
return;
130+
}
123131
this.setFullTime(PreciseDate.parseFull(time));
124132
return;
125133
}
@@ -128,12 +136,17 @@ export class PreciseDate extends Date {
128136
const args: number[] = Array.from(arguments);
129137
const dateFields = args.slice(0, 7) as DateFields;
130138
const date = new Date(...dateFields);
139+
const picos = args.length === 10 ? args.pop()! : 0;
131140
const nanos = args.length === 9 ? args.pop()! : 0;
132141
const micros = args.length === 8 ? args.pop()! : 0;
133142

134143
this.setTime(date.getTime());
135144
this.setMicroseconds(micros);
136145
this.setNanoseconds(nanos);
146+
147+
if (picos !== 0) {
148+
this.setPicoseconds(picos);
149+
}
137150
}
138151
/**
139152
* Returns the specified date represented in nanoseconds according to
@@ -210,6 +223,20 @@ export class PreciseDate extends Date {
210223
getNanoseconds(): number {
211224
return this._nanos;
212225
}
226+
/**
227+
* Returns the picoseconds in the specified date according to universal time.
228+
*
229+
* @returns {number}
230+
*
231+
* @example
232+
* const date = new PreciseDate('2019-02-08T10:34:29.481145231123Z');
233+
*
234+
* console.log(date.getPicoseconds());
235+
* // expected output: 123
236+
*/
237+
getPicoseconds(): number {
238+
return this._picos;
239+
}
213240
/**
214241
* Sets the microseconds for a specified date according to universal time.
215242
*
@@ -277,6 +304,39 @@ export class PreciseDate extends Date {
277304

278305
return this.setMicroseconds(micros);
279306
}
307+
/**
308+
* Sets the picoseconds for a specified date according to universal time.
309+
*
310+
* @param {number} picoseconds A number representing the picoseconds.
311+
* @returns {string} Returns a string representing the nanoseconds in the
312+
* specified date according to universal time.
313+
*
314+
* @example
315+
* const date = new PreciseDate();
316+
*
317+
* date.setPicoseconds(123);
318+
*
319+
* console.log(date.getPicoseconds());
320+
* // expected output: 123
321+
*/
322+
setPicoseconds(picos: number): string {
323+
const abs = Math.abs(picos);
324+
let nanos = this._nanos;
325+
326+
if (abs >= 1000) {
327+
nanos += Math.floor(abs / 1000) * Math.sign(picos);
328+
picos %= 1000;
329+
}
330+
331+
if (Math.sign(picos) === Sign.NEGATIVE) {
332+
nanos -= 1;
333+
picos += 1000;
334+
}
335+
336+
this._picos = picos;
337+
338+
return this.setNanoseconds(nanos);
339+
}
280340
/**
281341
* Sets the PreciseDate object to the time represented by a number of
282342
* nanoseconds since January 1, 1970, 00:00:00 UTC.
@@ -327,6 +387,7 @@ export class PreciseDate extends Date {
327387
setTime(time: number): number {
328388
this._micros = 0;
329389
this._nanos = 0;
390+
this._picos = 0;
330391
return super.setTime(time);
331392
}
332393
/**
@@ -346,7 +407,15 @@ export class PreciseDate extends Date {
346407
toISOString(): string {
347408
const micros = padLeft(this._micros, 3);
348409
const nanos = padLeft(this._nanos, 3);
349-
return super.toISOString().replace(/z$/i, `${micros}${nanos}Z`);
410+
let picos = '';
411+
412+
if (this._picos > 0) {
413+
// only include picoseconds if they are non-zero to avoid
414+
// breaking existing consumers of this method.
415+
picos = padLeft(this._picos, 3);
416+
}
417+
418+
return super.toISOString().replace(/z$/i, `${micros}${nanos}${picos}Z`);
350419
}
351420
/**
352421
* Returns an object representing the specified date according to universal
@@ -464,7 +533,9 @@ export class PreciseDate extends Date {
464533
date.setTime(seconds * 1000);
465534
date.setNanoseconds(nanos);
466535
} else if (isFullISOString(time)) {
467-
date.setFullTime(parseFullISO(time as string));
536+
const pd = parseFullISO(time as string);
537+
date.setFullTime(pd.getFullTimeString());
538+
date.setPicoseconds(pd.getPicoseconds());
468539
} else {
469540
date.setTime(new Date(time as string).getTime());
470541
}
@@ -516,6 +587,10 @@ export class PreciseDate extends Date {
516587
const milliseconds = Date.UTC(...(args.slice(0, 7) as DateFields));
517588
const date = new PreciseDate(milliseconds);
518589

590+
if (args.length === 10) {
591+
date.setPicoseconds(args.pop()!);
592+
}
593+
519594
if (args.length === 9) {
520595
date.setNanoseconds(args.pop()!);
521596
}
@@ -530,25 +605,27 @@ export class PreciseDate extends Date {
530605

531606
/**
532607
* Parses a RFC 3339 formatted string representation of the date, and returns
533-
* a string representing the nanoseconds since January 1, 1970, 00:00:00.
608+
* a {@link PreciseDate} object.
534609
*
535610
* @private
536611
*
537612
* @param {string} time The RFC 3339 formatted string.
538-
* @returns {string}
613+
* @returns {PreciseDate}
539614
*/
540-
function parseFullISO(time: string): string {
615+
function parseFullISO(time: string): PreciseDate {
541616
let digits = '0';
542617

543618
time = time.replace(/\.(\d+)/, ($0, $1) => {
544619
digits = $1;
545620
return '.000';
546621
});
547622

548-
const nanos = Number(padRight(digits, 9));
623+
const picos = Number(padRight(digits, 12));
549624
const date = new PreciseDate(time);
550625

551-
return date.setNanoseconds(nanos);
626+
date.setPicoseconds(picos);
627+
628+
return date;
552629
}
553630

554631
/**

core/precise-date/test/index.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ describe('PreciseDate', () => {
7676
const MILLISECONDS = 381;
7777
const MICROSECONDS = 101;
7878
const NANOSECONDS = 32;
79+
const PICOSECONDS = 123;
7980

8081
before(() => {
8182
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -136,6 +137,26 @@ describe('PreciseDate', () => {
136137
assert.strictEqual(micros, MICROSECONDS);
137138
assert.strictEqual(nanos, NANOSECONDS);
138139
});
140+
141+
it('should accept picoseconds in constructor', () => {
142+
// year, month, date, hours, minutes, seconds, millis, micros, nanos, picos
143+
const date = new PreciseDate(
144+
YEAR,
145+
MONTH,
146+
DAY,
147+
HOURS,
148+
MINUTES,
149+
SECONDS,
150+
MILLISECONDS,
151+
MICROSECONDS,
152+
NANOSECONDS,
153+
PICOSECONDS,
154+
);
155+
assert.strictEqual(date.getPicoseconds(), PICOSECONDS);
156+
assert.strictEqual(date.getNanoseconds(), NANOSECONDS);
157+
assert.strictEqual(date.getMicroseconds(), MICROSECONDS);
158+
assert.strictEqual(date.getUTCMilliseconds(), MILLISECONDS);
159+
});
139160
});
140161

141162
describe('#getFullTime()', () => {
@@ -203,6 +224,14 @@ describe('PreciseDate', () => {
203224
});
204225
});
205226

227+
describe('#getPicoseconds()', () => {
228+
it('should return the picoseconds', () => {
229+
const date = new PreciseDate(TIME_STRING);
230+
date.setPicoseconds(PICOSECONDS);
231+
assert.strictEqual(date.getPicoseconds(), PICOSECONDS);
232+
});
233+
});
234+
206235
describe('#setMicroseconds()', () => {
207236
it('should set the microseconds', () => {
208237
const micros = 912;
@@ -275,6 +304,41 @@ describe('PreciseDate', () => {
275304
});
276305
});
277306

307+
describe('#setPicoseconds()', () => {
308+
it('should set picoseconds correctly', () => {
309+
const date = new PreciseDate(TIME_STRING);
310+
date.setPicoseconds(999);
311+
assert.strictEqual(date.getPicoseconds(), 999);
312+
});
313+
314+
it('should return the precise time string', () => {
315+
const fakeTimestamp = '123456789';
316+
sandbox.stub(date, 'setNanoseconds').returns(fakeTimestamp);
317+
318+
const timestamp = date.setPicoseconds(0);
319+
assert.strictEqual(timestamp, fakeTimestamp);
320+
});
321+
322+
it('should cascade overflow from setPicoseconds', () => {
323+
const date = new PreciseDate({seconds: SECS, nanos: 789});
324+
// 1000 picos = 1 nano
325+
// Current nanos: 789
326+
// New nanos should be 790
327+
date.setPicoseconds(1001);
328+
assert.strictEqual(date.getPicoseconds(), 1);
329+
assert.strictEqual(date.getNanoseconds(), 790);
330+
});
331+
332+
it('should handle negative picoseconds', () => {
333+
const date = new PreciseDate({seconds: SECS, nanos: 789});
334+
// Current: ...789 nanos, 0 picos
335+
// set -1 pico -> ...788 nanos, 999 picos
336+
date.setPicoseconds(-1);
337+
assert.strictEqual(date.getPicoseconds(), 999);
338+
assert.strictEqual(date.getNanoseconds(), 788);
339+
});
340+
});
341+
278342
describe('#setFullTime()', () => {
279343
let timeStub: sinon.SinonStub<[number], number>;
280344
let nanosStub: sinon.SinonStub<[number], string>;
@@ -362,6 +426,19 @@ describe('PreciseDate', () => {
362426
const isoString = date.toISOString();
363427
assert.strictEqual(isoString, FULL_ISO_STRING);
364428
});
429+
430+
it('should format to 12-digit fractional ISO string when picos > 0', () => {
431+
const PICO_ISO_STRING = '2019-01-12T00:30:35.123456789123Z';
432+
const date = new PreciseDate(PICO_ISO_STRING);
433+
assert.strictEqual(date.toISOString(), PICO_ISO_STRING);
434+
});
435+
436+
it('should format to 9-digit fractional ISO string when picos == 0', () => {
437+
const NANO_ISO_STRING = '2019-01-12T00:30:35.123456789Z';
438+
const date = new PreciseDate(NANO_ISO_STRING);
439+
assert.strictEqual(date.getPicoseconds(), 0);
440+
assert.strictEqual(date.toISOString(), NANO_ISO_STRING);
441+
});
365442
});
366443

367444
describe('#toStruct()', () => {
@@ -452,6 +529,15 @@ describe('PreciseDate', () => {
452529
const timestamp = PreciseDate.parseFull(date.toISOString());
453530
assert.strictEqual(timestamp, expectedTimestamp);
454531
});
532+
533+
it('should parse 12-digit fractional ISO string', () => {
534+
const PICO_ISO_STRING = '2019-01-12T00:30:35.123456789123Z';
535+
const date = new PreciseDate(PICO_ISO_STRING);
536+
assert.strictEqual(date.getPicoseconds(), 123);
537+
assert.strictEqual(date.getNanoseconds(), 789);
538+
assert.strictEqual(date.getMicroseconds(), 456);
539+
assert.strictEqual(date.getUTCMilliseconds(), 123);
540+
});
455541
});
456542

457543
describe('.fullUTC()', () => {

0 commit comments

Comments
 (0)