Skip to content

Commit d4b1b01

Browse files
authored
Add leap seconds, TAI, etc. (#8)
* Added leap second handling and TAI/TDT support. * Now supports Universal Time with smooth, integrated transition between calculated UT1, proleptic UTC, and official UTC. * Uses timezone data generated by @tubular/time-tzdb. * Added jde, mjde, jdu, mjdu, and deltaTai fields to DateAndTime. * Case-insensitive timezone lookup. * Added ability to add/subtract TAI time. * Added negative length option to toIsoString(). * Deprecated utcTimeMillis and utcTimeSeconds in favor of utcMillis and utcSeconds. * Added Timezone.version.
1 parent d6c2bbe commit d4b1b01

23 files changed

Lines changed: 2072 additions & 477 deletions

.coveralls.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
service_name: travis-ci

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
*.tgz
1616
/test.html
1717
.travis.yml
18+
.coveralls.yml

.nycrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"exclude": ["src/zone-*.ts"]
3+
}

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
language: node_js
22
node_js:
33
- "14"
4+
after_success: npm run coverage

README.md

Lines changed: 238 additions & 16 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 614 additions & 258 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tubular/time",
3-
"version": "2.5.0",
3+
"version": "2.6.0",
44
"description": "Date/time, IANA timezones, calendar with settable Julian/Gregorian switchover",
55
"browser": "dist/web/index.js",
66
"browser-es5": "dist/web5/index.js",
@@ -12,7 +12,8 @@
1212
"sideEffects": false,
1313
"scripts": {
1414
"build": "rimraf dist/ && tsc && tsc --build tsconfig.es6.json && tsc --build tsconfig.es5.json && webpack && webpack --env esver=5 && ts-node make-remote-zone-data.ts",
15-
"prepublishOnly": "npm run build",
15+
"coverage": "nyc report --reporter=text-lcov | coveralls",
16+
"prepack": "npm run build",
1617
"lint": "eslint 'src/**/*.ts' '**/*.cjs'",
1718
"test": "TS_NODE_FILES=true nyc --reporter=html mocha --require ts-node/register src/**/*.spec.ts",
1819
"document": "typedoc --name \"ks-date-time-zone\" --module commonjs --exclude \"**/*+(e2e|spec|index).ts\" --excludePrivate --excludeProtected --readme README.md --target ES5 --out docs src"
@@ -23,8 +24,10 @@
2324
"gregorian",
2425
"iana",
2526
"julian",
27+
"leap second",
2628
"moment",
2729
"olson",
30+
"tai",
2831
"time",
2932
"time zone",
3033
"timezone",
@@ -35,21 +38,22 @@
3538
"license": "MIT",
3639
"dependencies": {
3740
"@tubular/math": "^2.2.1",
38-
"@tubular/util": "^3.5.0"
41+
"@tubular/util": "^3.6.0"
3942
},
4043
"devDependencies": {
41-
"@babel/core": "^7.13.8",
42-
"@babel/preset-env": "^7.13.9",
43-
"@babel/register": "^7.13.8",
44-
"@types/chai": "^4.2.14",
45-
"@types/mocha": "^8.0.4",
46-
"@types/node": "^14.14.22",
47-
"@typescript-eslint/eslint-plugin": "^4.16.1",
48-
"@typescript-eslint/parser": "^4.16.1",
44+
"@babel/core": "^7.13.15",
45+
"@babel/preset-env": "^7.13.15",
46+
"@babel/register": "^7.13.14",
47+
"@types/chai": "^4.2.16",
48+
"@types/mocha": "^8.2.2",
49+
"@types/node": "^14.14.41",
50+
"@typescript-eslint/eslint-plugin": "^4.22.0",
51+
"@typescript-eslint/parser": "^4.22.0",
4952
"babel-loader": "^8.2.2",
5053
"by-request": "^1.2.0",
5154
"chai": "^4.2.0",
52-
"eslint": "^7.21.0",
55+
"coveralls": "^3.1.0",
56+
"eslint": "^7.24.0",
5357
"eslint-plugin-chai-friendly": "^0.6.0",
5458
"eslint-plugin-import": "^2.22.1",
5559
"eslint-plugin-node": "^11.1.0",
@@ -60,9 +64,9 @@
6064
"rimraf": "^3.0.2",
6165
"terser-webpack-plugin": "^4.2.3",
6266
"ts-node": "^9.1.1",
63-
"typescript": "^4.2.2",
64-
"webpack": "^5.24.2",
65-
"webpack-cli": "^4.5.0",
67+
"typescript": "^4.2.4",
68+
"webpack": "^5.34.0",
69+
"webpack-cli": "^4.6.0",
6670
"webpack-node-externals": "^2.5.2"
6771
},
6872
"repository": "github:kshetline/tubular_time.git"

src/calendar.spec.ts

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { assert, expect } from 'chai';
2-
import { getISOFormatDate, Calendar, LAST, getDayNumber_SGC, getDayNumberGregorian, getDayNumberJulian, isValidDate_SGC } from './calendar';
2+
import {
3+
getISOFormatDate, Calendar, LAST, getDayNumber_SGC, getDayNumberGregorian, getDayNumberJulian, isValidDate_SGC,
4+
getLastDateInMonth_SGC, getDaysInMonth_SGC, getDayOfWeek_SGC, getDayOfWeekInMonthCount_SGC, getDayOnOrBefore_SGC,
5+
WEDNESDAY, MONDAY, getDayOnOrAfter_SGC
6+
} from './calendar';
37
import { YMDDate } from './common';
48
import { DateTime } from './date-time';
59

610
describe('Calendar', () => {
7-
const calendar = new Calendar();
11+
let calendar: Calendar;
812

913
beforeEach(() => {
14+
calendar = new Calendar();
1015
DateTime.setDefaultLocale('en-us');
1116
DateTime.setDefaultTimezone('America/New_York');
1217
});
@@ -26,6 +31,8 @@ describe('Calendar', () => {
2631
expect(getDayNumberGregorian({ y: 2021, dy: 40 })).to.equal(18667);
2732
expect(getDayNumberGregorian({ n: 34567 })).to.equal(34567);
2833
expect(getDayNumberGregorian({ n: -234567 })).to.equal(-234567);
34+
expect(getDayNumberJulian({ y: 2021, m: 0, d: 31 })).to.equal(18640);
35+
expect(getDayNumberJulian({ y: 2020, m: 13, d: 1 })).to.equal(18641);
2936
expect(getDayNumberJulian({ y: 2021 })).to.equal(18641);
3037
expect(getDayNumberJulian({ y: 2021, m: 3 })).to.equal(18700);
3138
expect(getDayNumberJulian({ y: 2021, m: 3, d: 2 })).to.equal(18701);
@@ -35,8 +42,11 @@ describe('Calendar', () => {
3542
expect(() => getDayNumber_SGC({})).to.throw('Calendar: Invalid date arguments');
3643
expect(getDayNumber_SGC({ y: 2021, j: false })).to.equal(18628);
3744
expect(getDayNumber_SGC({ y: 2021, j: true })).to.equal(18641);
45+
expect(getDayNumber_SGC({ y: 2021, m: 0, d: 31 })).to.equal(18627);
46+
expect(getDayNumber_SGC({ y: 2020, m: 13, d: 1 })).to.equal(18628);
3847
expect(isValidDate_SGC({ y: 1582, m: 10, d: 7 })).to.be.false;
3948
expect(isValidDate_SGC({ y: 1582, m: 10, d: 7, j: true })).to.be.true;
49+
expect(calendar.isValidDate([1582, 10, 7])).to.be.false;
4050
});
4151

4252
it('should consistently convert the date for a day number back to the same day number.', () => {
@@ -62,6 +72,7 @@ describe('Calendar', () => {
6272

6373
it('should return Saturday (6) for 1962-10-13.', () => {
6474
expect(calendar.getDayOfWeek(1962, 10, 13)).to.equal(6);
75+
expect(getDayOfWeek_SGC(1962, 10, 13)).to.equal(6);
6576
});
6677

6778
it('should return Friday (5) for 2016-12-16.', () => {
@@ -72,10 +83,35 @@ describe('Calendar', () => {
7283
expect(calendar.getDateOfNthWeekdayOfMonth(2016, 11, 4, 4)).to.equal(24);
7384
});
7485

86+
it('should get proper last day for February.', () => {
87+
expect(calendar.getLastDateInMonth(1900, 2)).to.equal(28);
88+
expect(calendar.getLastDateInMonth(1904, 2)).to.equal(29);
89+
expect(calendar.getLastDateInMonth(1905, 2)).to.equal(28);
90+
expect(calendar.getLastDateInMonth(2000, 2)).to.equal(29);
91+
expect(calendar.getDaysInMonth(1900, 2)).to.equal(28);
92+
expect(calendar.getDaysInMonth(1904, 2)).to.equal(29);
93+
expect(calendar.getDaysInMonth(1905, 2)).to.equal(28);
94+
expect(calendar.getDaysInMonth(2000, 2)).to.equal(29);
95+
calendar.setPureJulian(true);
96+
expect(calendar.isPureJulian()).to.be.true;
97+
expect(calendar.getLastDateInMonth(1900, 2)).to.equal(29);
98+
99+
expect(getLastDateInMonth_SGC(1900, 2)).to.equal(28);
100+
expect(getLastDateInMonth_SGC(1904, 2)).to.equal(29);
101+
expect(getLastDateInMonth_SGC(1905, 2)).to.equal(28);
102+
expect(getLastDateInMonth_SGC(2000, 2)).to.equal(29);
103+
expect(getDaysInMonth_SGC(1900, 2)).to.equal(28);
104+
expect(getDaysInMonth_SGC(1904, 2)).to.equal(29);
105+
expect(getDaysInMonth_SGC(1905, 2)).to.equal(28);
106+
expect(getDaysInMonth_SGC(2000, 2)).to.equal(29);
107+
});
108+
75109
it('should return a series of Tuesdays at the correct index for each month.', () => {
76110
let match = true;
77111
let countMatch = true;
112+
let countMatch2 = true;
78113
let count = 0;
114+
let count2 = 0;
79115
let expectedCount = 0;
80116
let month = 1;
81117
let day: number;
@@ -89,8 +125,10 @@ describe('Calendar', () => {
89125
++index;
90126
else {
91127
count = calendar.getDayOfWeekInMonthCount(lastYmd?.y, lastYmd?.m, 2);
128+
count2 = getDayOfWeekInMonthCount_SGC(lastYmd?.y, lastYmd?.m, 2);
92129
expectedCount = index;
93130
countMatch = (count === expectedCount);
131+
countMatch2 = (count2 === expectedCount);
94132
index = 1;
95133
month = ymd.m;
96134
}
@@ -102,20 +140,21 @@ describe('Calendar', () => {
102140

103141
assert(match, getISOFormatDate(ymd) + ' -> ' + index + ': ' + day);
104142
assert(countMatch, getISOFormatDate(lastYmd) + ' -> ' + count + ' counted, ' + expectedCount + ' expected.');
143+
assert(countMatch2, getISOFormatDate(lastYmd) + ' -> ' + count2 + ' counted, ' + expectedCount + ' expected.');
105144
});
106145

107146
it('should have only 19 days in September 1752 when most of North America switched to the Gregorian calendar.', () => {
108147
calendar.setGregorianChange(1752, 9, 14);
109148
expect(calendar.getDaysInMonth(1752, 9)).to.equal(19);
110149
});
111150

112-
// Proceeding with modified Gregorian Calendar change...
113-
114151
it('should return 30 as the third Saturday of 1752/09.', () => {
152+
calendar.setGregorianChange(1752, 9, 14);
115153
expect(calendar.getDateOfNthWeekdayOfMonth(1752, 9, 6, 3)).to.equal(30);
116154
});
117155

118156
it('should return 30 as the last Saturday of 1752/09.', () => {
157+
calendar.setGregorianChange(1752, 9, 14);
119158
expect(calendar.getDateOfNthWeekdayOfMonth(1752, 9, 6, LAST)).to.equal(30);
120159
});
121160

@@ -152,4 +191,36 @@ describe('Calendar', () => {
152191
expect(calendar.getYearWeekAndWeekday([2021, 1, 3])).to.eql([2020, 53, 7]);
153192
expect(calendar.getYearWeekAndWeekday([2021, 1, 4])).to.eql([2021, 1, 1]);
154193
});
194+
195+
it('should properly handle various Julian/Gregorian crossovers.', () => {
196+
calendar = new Calendar([1752, 9, 14]);
197+
expect(calendar.getDaysInMonth(1752, 9)).to.equal(19);
198+
expect(calendar.getDaysInMonth(1752, 10)).to.equal(31);
199+
expect(calendar.getMissingDateRange(1752, 9)).to.eql([3, 13]);
200+
201+
const dayNum = calendar.getDayNumber(1752, 9, 2);
202+
203+
calendar.setPureGregorian(true);
204+
expect(calendar.isPureGregorian()).to.be.true;
205+
expect(calendar.getDateFromDayNumber(dayNum)).to.include({ y: 1752, m: 9, d: 13 });
206+
expect(calendar.getMissingDateRange(1752, 9)).to.equal(null);
207+
calendar.setPureGregorian(false);
208+
expect(calendar.getDateFromDayNumber(dayNum)).to.include({ y: 1752, m: 9, d: 13 });
209+
calendar.setGregorianChange('j');
210+
expect(calendar.getDateFromDayNumber(dayNum)).to.include({ y: 1752, m: 9, d: 2 });
211+
});
212+
213+
it('should properly handle getDayOnOrAfter.', () => {
214+
expect(getDayOnOrAfter_SGC(2022, 8, MONDAY, 1)).to.equal(1);
215+
expect(calendar.getDayOnOrAfter(2022, 8, MONDAY, 1)).to.equal(1);
216+
expect(getDayOnOrAfter_SGC(2022, 8, MONDAY, 2)).to.equal(8);
217+
expect(calendar.getDayOnOrAfter(2022, 8, MONDAY, 2)).to.equal(8);
218+
});
219+
220+
it('should properly handle getDayOnOrBefore.', () => {
221+
expect(getDayOnOrBefore_SGC(2021, 4, WEDNESDAY, 21)).to.equal(21);
222+
expect(calendar.getDayOnOrBefore(2021, 4, WEDNESDAY, 21)).to.equal(21);
223+
expect(getDayOnOrBefore_SGC(2021, 4, WEDNESDAY, 20)).to.equal(14);
224+
expect(calendar.getDayOnOrBefore(2021, 4, WEDNESDAY, 20)).to.equal(14);
225+
});
155226
});

src/calendar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export function getDayOfWeekInMonthCount_SGC(year: number, month: number, dayOfT
348348
const firstDay = getDayNumber_SGC(year, month, getDateOfNthWeekdayOfMonth_SGC(year, month, dayOfTheWeek, 1));
349349
const nextMonth = getDayNumber_SGC(year, month + 1, 1);
350350

351-
return (nextMonth - firstDay - 1) / 7 + 1;
351+
return div_tt0(nextMonth - firstDay - 1, 7) + 1;
352352
}
353353

354354
export function getDayOnOrAfter_SGC(year: number, month: number, dayOfTheWeek: number, minDate: number): number {
@@ -921,13 +921,17 @@ export class Calendar {
921921
return dates;
922922
}
923923

924+
isValidDate(year: number, month: number, day: number): boolean;
925+
isValidDate(yearOrDate: YMDDate | number[]): boolean;
924926
isValidDate(yearOrDate: YearOrDate, month?: number, day?: number): boolean {
925927
let year: number; [year, month, day] = handleVariableDateArgs(yearOrDate, month, day, this, true);
926928
const ymd = this.getDateFromDayNumber(this.getDayNumber(year, month, day));
927929

928930
return (year === ymd.y && month === ymd.m && day === ymd.d);
929931
}
930932

933+
normalizeDate(year: number, month: number, day: number): YMDDate;
934+
normalizeDate(yearOrDate: YMDDate | number[]): YMDDate;
931935
normalizeDate(yearOrDate: YearOrDate, month?: number, day?: number): YMDDate {
932936
let year: number; [year, month, day] = handleVariableDateArgs(yearOrDate, month, day, this, true);
933937

src/common.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@ import { getDateFromDayNumber_SGC, getDayNumber_SGC } from './calendar';
33
import { isNumber, toNumber } from '@tubular/util';
44

55
export const MIN_YEAR = -271820;
6-
export const MAX_YEAR = 275759;
6+
export const MAX_YEAR = 275759;
7+
8+
export const MINUTE_MSEC = 60_000;
9+
export const HOUR_MSEC = 3_600_000;
10+
export const DAY_MSEC = 86_400_000;
11+
export const DAY_SEC = 86_400;
12+
export const DAY_MINUTES = 1440;
13+
14+
export const UNIX_TIME_ZERO_AS_JULIAN_DAY = 2440587.5;
15+
export const JD_J2000 = 2451545.0; // Julian date for the J2000.0 epoch.
16+
export const DELTA_TDT_SEC = 32.184;
17+
export const DELTA_TDT_MSEC = 32184;
18+
export const DELTA_TDT_DAYS = DELTA_TDT_SEC / DAY_SEC;
19+
export const DELTA_MJD = 2400000.5;
720

821
/**
922
* Specifies a calendar date by year, month, and day. Optionally provides day number and boolean flag indicating Julian
@@ -70,6 +83,16 @@ export interface DateAndTime extends YMDDate {
7083
utcOffset?: number;
7184
dstOffset?: number;
7285
occurrence?: number;
86+
deltaTai?: number;
87+
88+
/** Julian days, ephemeris. */
89+
jde?: number;
90+
/** Modified Julian days, ephemeris. */
91+
mjde?: number;
92+
/** Julian days, UT. */
93+
jdu?: number;
94+
/** Modified Julian days, UT. */
95+
mjdu?: number;
7396
}
7497

7598
const altFields = [
@@ -88,9 +111,9 @@ const fieldOrder = [
88111
'ywl', 'wl', 'dwl',
89112
'yearByWeekLocale', 'weekLocale', 'dayByWeekLocale',
90113
'hrs', 'min', 'sec',
91-
'hour', 'minute', 'second',
92-
'millis',
93-
'utcOffset', 'dstOffset', 'occurrence',
114+
'hour', 'minute', 'second', 'millis',
115+
'utcOffset', 'dstOffset', 'occurrence', 'deltaTai',
116+
'jde', 'mjde', 'jdu', 'mjdu',
94117
'error'
95118
];
96119

@@ -128,27 +151,29 @@ export function orderFields<T extends YMDDate | DateAndTime>(obj: T): T {
128151
}
129152

130153
export function validateDateAndTime(obj: YMDDate | DateAndTime): void {
154+
const dt = obj as DateAndTime;
155+
131156
Object.keys(obj).forEach(key => {
132157
if (key !== 'j' && key !== 'isJulian') {
133158
const value = obj[key];
134159

135-
if (value != null && !isNumber(value) || value !== floor(value))
136-
throw new Error(`${key} must be an integer value (${value})`);
160+
if (value != null) {
161+
if (/^(m?(jde|jdu))$/.test(key)) {
162+
if (!isNumber(value))
163+
throw new Error(`${key} must be a numeric value (${value})`);
164+
}
165+
else if (!isNumber(value) || value !== floor(value))
166+
throw new Error(`${key} must be an integer value (${value})`);
167+
}
137168
}
138169
});
139170

140171
if (obj.y == null && obj.year == null && obj.yw == null && obj.yearByWeek == null &&
141172
obj.ywl == null && obj.yearByWeekLocale == null && obj.n == null && obj.epochDay == null &&
142-
(obj as DateAndTime).hrs == null && (obj as DateAndTime).hour == null)
143-
throw new Error('A year value, an epoch day, or an hour value must be specified');
173+
dt.hrs == null && dt.hour == null && dt.jde == null && dt.mjde == null && dt.jdu == null && dt.mjdu == null)
174+
throw new Error('A year value, an epoch day, an hour value, or a Julian date value must be specified');
144175
}
145176

146-
export const MINUTE_MSEC = 60000;
147-
export const HOUR_MSEC = 3600000;
148-
export const DAY_MSEC = 86400000;
149-
150-
export const DAY_MINUTES = 1440;
151-
152177
export function millisFromDateTime_SGC(year: number, month: number, day: number, hour: number, minute: number, second?: number, millis?: number): number {
153178
millis = millis || 0;
154179
second = second || 0;
@@ -161,7 +186,7 @@ export function millisFromDateTime_SGC(year: number, month: number, day: number,
161186
}
162187

163188
export function dateAndTimeFromMillis_SGC(ticks: number): DateAndTime {
164-
const wallTime = getDateFromDayNumber_SGC(div_rd(ticks, 86400000)) as DateAndTime;
189+
const wallTime = getDateFromDayNumber_SGC(div_rd(ticks, DAY_MSEC)) as DateAndTime;
165190

166191
wallTime.millis = mod(ticks, 1000);
167192
ticks = div_rd(ticks, 1000);

0 commit comments

Comments
 (0)