From ed529ea6fbcc70315c0c3f6d7c7cb70eadf56b03 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 16 May 2020 10:39:08 -0400 Subject: [PATCH] Yet another PR adding TypeScript declarations (#530) * Adds TypeScript declarations for moment-timezone * Remove unnecessary `moment.` qualification in typings * Move exported types to top-level so that they can all be imported as named imports - Zone class must exist at `tz.Zone` because that's where it's exposed at runtime, but the `Zone` interface is still re-exported as a named export * tsc invocation always uses locally installed tsc Co-authored-by: Elena Sharovar --- Gruntfile.js | 14 +- index.d.ts | 127 +++++++++++++++++ moment-timezone-utils.d.ts | 41 ++++++ package-lock.json | 6 + package.json | 5 +- typing-tests/moment-tests.ts | 259 +++++++++++++++++++++++++++++++++++ typing-tests/tsconfig.json | 14 ++ 7 files changed, 461 insertions(+), 5 deletions(-) create mode 100644 index.d.ts create mode 100644 moment-timezone-utils.d.ts create mode 100644 typing-tests/moment-tests.ts create mode 100644 typing-tests/tsconfig.json diff --git a/Gruntfile.js b/Gruntfile.js index 31543dc6..1bf92df5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -45,6 +45,10 @@ module.exports = function(grunt) { clean: { data: ['temp'] + }, + + exec: { + 'typing-tests': './node_modules/.bin/tsc --project ./typing-tests' } }); @@ -54,11 +58,13 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-exec'); + + grunt.registerTask('release', ['jshint', 'data', 'nodeunit', 'typing-tests', 'build', 'uglify']); - /** creates a release */ - grunt.registerTask('release', ['jshint', 'data', 'nodeunit', 'build', 'uglify']); + grunt.registerTask('releaseNoData', ['jshint', 'nodeunit', 'typing-tests', 'build', 'uglify']); - grunt.registerTask('releaseNoData', ['jshint', 'nodeunit', 'build', 'uglify']); + grunt.registerTask('typing-tests', ['exec:typing-tests']); - grunt.registerTask('default', ['jshint', 'nodeunit']); + grunt.registerTask('default', ['jshint', 'nodeunit', 'typing-tests']); }; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..8cb684de --- /dev/null +++ b/index.d.ts @@ -0,0 +1,127 @@ +import * as moment from 'moment'; + +// require('moment-timezone') === require('moment') +export = moment; + +declare module 'moment' { + interface Moment { + /** Set the timezone and update the offset */ + tz(zone: string): Moment; + /** Return the timezone name or undefined if a zone has not yet been set */ + tz(): string | undefined; + + /** Get the zone abbreviation. This is what moment.js uses when formatting the z token. */ + zoneAbbr(): string; + + /** Get the zone long form name. This is what moment.js uses when formatting the zz token. */ + zoneName(): string; + } + + // Match normal moment constructor but with an extra timezone argument + // Here's a copy-paste of the normal moment constructor's signature, from https://github.com/moment/moment/blob/develop/moment.d.ts#L1-L2 + // declare function moment(inp?: moment.MomentInput, format?: moment.MomentFormatSpecification, strict?: boolean): moment.Moment; + // declare function moment(inp?: moment.MomentInput, format?: moment.MomentFormatSpecification, language?: string, strict?: boolean): moment.Moment; + + // Should be sorted from tightest to loosest. TypeScript picks the first signature that matches, going top to bottom. + + /** create a moment with a time zone */ + function tz(inp: MomentInput, format: MomentFormatSpecification, language: string, strict: boolean, zone: string): Moment; + /** create a moment with a time zone */ + function tz(inp: MomentInput, format: MomentFormatSpecification, language: string, zone: string): Moment; + /** create a moment with a time zone */ + function tz(inp: MomentInput, format: MomentFormatSpecification, strict: boolean, zone: string): Moment; + /** create a moment with a time zone */ + function tz(inp: MomentInput, format: MomentFormatSpecification, zone: string): Moment; + /** create a moment with a time zone */ + function tz(inp: MomentInput, zone: string): Moment; + /** create a moment with a time zone */ + function tz(zone?: string): Moment; + + namespace tz { + /** Version of moment-timezone */ + const version: string; + + /** + * Change the default timezone of newly created Moment instances. + * By default new instances are created in the local timezone. + */ + function setDefault(zone: string): typeof moment; + + /** Reset the default timezone to local. */ + function setDefault(): typeof moment; + + /** + * Retrieve or guess the user's timezone. Uses the browser's Internationalization API if available. + * Otherwise, guesses by sampling offsets from different points in time and comparing them to available zone data. + */ + function guess(): string; + + interface Zone extends UnpackedZone {} + class Zone { + /** Get the abbreviation for a given timestamp from a Zone. */ + abbr(timestamp: number): string; + + /** Get the offset for a given timestamp from a Zone. */ + offset(timestamp: number): number; + + /** Parse an offset for a timestamp constructed from Date.UTC in that zone. */ + parse(timestamp: number): number; + } + + /** Return a timezone by name or null if timezone by that name is not loaded. */ + function zone(name: string): Zone | null; + + /** Add zone data for a timezone. */ + function add(packedZone: string): void; + /** Add zone data for multiple timezones. */ + function add(packedZones: Array): void; + + /** Link two zone names to the same data */ + function link(packedLink: string): void; + /** Add multiple links at once */ + function link(packedLinks: Array): void; + + /** load a bundle of zone data and links */ + function load(bundle: PackedZoneBundle): void; + + /** get a list of all available time zone names */ + function names(): Array; + + /** Convert a packed string to an unpacked zone data object */ + function unpack(packedZone: string): UnpackedZone; + /** Convert a base 60 string to a base 10 number. */ + function unpackBase60(base60String: string): number; + } + + type Zone = tz.Zone; + + /** Parsed / unpacked zone data. */ + interface UnpackedZone { + /** The uniquely identifying name of the time zone. */ + name: string; + + /** zone abbreviations */ + abbrs: Array; + + /** (measured in milliseconds) */ + untils: Array; + + /** (measured in minutes) */ + offsets: Array; + } + + /** Bundle of zone data and links for multiple timezones */ + interface PackedZoneBundle { + version: string; + zones: Array; + links: Array; + } + + /** Bundle of zone data and links for multiple timezones */ + interface UnpackedZoneBundle { + version: string; + zones: Array; + links: Array; + } + +} diff --git a/moment-timezone-utils.d.ts b/moment-timezone-utils.d.ts new file mode 100644 index 00000000..61163f56 --- /dev/null +++ b/moment-timezone-utils.d.ts @@ -0,0 +1,41 @@ +import * as moment from 'moment'; + +// require('moment-timezone') === require('moment') +export = moment; + +declare module 'moment' { + namespace tz { + /** Converts zone data in the unpacked format to the packed format. */ + function pack(unpackedObject: UnpackedZone): string; + + /** Convert a base 10 number to a base 60 string. */ + function packBase60(input: number, precision?: number): string; + + /** Create links out of two zones that share data. + * @returns A new ZoneBundle with duplicate zone data replaced by links + */ + function createLinks(unlinked: UnpackedZoneBundle): PackedZoneBundle; + + /** + * Filter out data for years outside a certain range. + * @return a new, filtered UnPackedZone object + */ + function filterYears(unpackedZone: UnpackedZone, startYear: number, endYear: number): UnpackedZone; + /** + * Filter out data for years outside a certain range. + * @return a new, filtered UnPackedZone object + */ + function filterYears(unpackedZone: UnpackedZone, startAndEndYear: number): UnpackedZone; + + /** + * Combines packing, link creation, and subsetting of years into one simple interface. + * Pass in an unpacked bundle, start year, and end year and get a filtered, linked, packed bundle back. + */ + function filterLinkPack(unpackedBundle: UnpackedZoneBundle, startYear: number, endYear: number): PackedZoneBundle; + /** + * Combines packing, link creation, and subsetting of years into one simple interface. + * Pass in an unpacked bundle, start year, and end year and get a filtered, linked, packed bundle back. + */ + function filterLinkPack(unpackedBundle: UnpackedZoneBundle, startAndEndYear: number): PackedZoneBundle; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2d7fee85..01897c30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1058,6 +1058,12 @@ "uri-path": "1.0.0" } }, + "grunt-exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-exec/-/grunt-exec-3.0.0.tgz", + "integrity": "sha512-cgAlreXf3muSYS5LzW0Cc4xHK03BjFOYk0MqCQ/MZ3k1Xz2GU7D+IAJg4UKicxpO+XdONJdx/NJ6kpy2wI+uHg==", + "dev": true + }, "grunt-known-options": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", diff --git a/package.json b/package.json index 8c1a119b..28f11ea4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "tz" ], "main": "./index.js", + "typings": "./index.d.ts", "engines": { "node": "*" }, @@ -34,7 +35,9 @@ "grunt-contrib-clean": "^2.0.0", "grunt-contrib-jshint": "^2.1.0", "grunt-contrib-nodeunit": "^2.0.0", - "grunt-contrib-uglify": "^4.0.1" + "grunt-contrib-uglify": "^4.0.1", + "grunt-exec": "^3.0.0", + "typescript": "^3.5.1" }, "jspm": { "main": "builds/moment-timezone-with-data", diff --git a/typing-tests/moment-tests.ts b/typing-tests/moment-tests.ts new file mode 100644 index 00000000..74c518d5 --- /dev/null +++ b/typing-tests/moment-tests.ts @@ -0,0 +1,259 @@ +import moment = require('../'); + +// Tests are copied from all the code examples on https://momentjs.com/timezone/docs/ and then lightly tweaked to be valid code. +// Run this in Chrome's DevTools: +// const exclude = Array.from($('.docs-method-signature>pre>code')); +// const include = Array.from($('code.language-js')); +// copy(include.filter(v => !exclude.some(v2 => v2 === v)).map(v => v.innerText).join('\n\n')); + +moment().tz("America/Los_Angeles").format(); + +var a = moment.tz("2013-11-18 11:55", "America/Toronto"); +var b = moment.tz("May 12th 2014 8PM", "MMM Do YYYY hA", "America/Toronto"); +var c = moment.tz(1403454068850, "America/Toronto"); +a.format(); // 2013-11-18T11:55:00-05:00 +b.format(); // 2014-05-12T20:00:00-04:00 +c.format(); // 2014-06-22T12:21:08-04:00 + +moment.tz("2013-12-01", "America/Los_Angeles").format(); // 2013-12-01T00:00:00-08:00 +moment.tz("2013-06-01", "America/Los_Angeles").format(); // 2013-06-01T00:00:00-07:00 + +var arr = [2013, 5, 1], + str = "2013-12-01", + obj = { year : 2013, month : 5, day : 1 }; + +moment.tz(arr, "America/Los_Angeles").format(); // 2013-06-01T00:00:00-07:00 +moment.tz(str, "America/Los_Angeles").format(); // 2013-12-01T00:00:00-08:00 +moment.tz(obj, "America/Los_Angeles").format(); // 2013-06-01T00:00:00-07:00 + +moment.tz(arr, "America/New_York").format(); // 2013-06-01T00:00:00-04:00 +moment.tz(str, "America/New_York").format(); // 2013-12-01T00:00:00-05:00 +moment.tz(obj, "America/New_York").format(); // 2013-06-01T00:00:00-04:00 + +var zoneName = "America/Los_Angeles"; +moment.tz('2013-06-01T00:00:00', zoneName).format(); // 2013-06-01T00:00:00-07:00 +moment.tz('2013-06-01T00:00:00-04:00', zoneName).format(); // 2013-05-31T21:00:00-07:00 +moment.tz('2013-06-01T00:00:00+00:00', zoneName).format(); // 2013-05-31T17:00:00-07:00 + +var timestamp = 1403454068850, + date = new Date(timestamp); + +moment.tz(timestamp, "America/Los_Angeles").format(); // 2014-06-22T09:21:08-07:00 +moment(timestamp).tz("America/Los_Angeles").format(); // 2014-06-22T09:21:08-07:00 + +moment.tz(date, "America/Los_Angeles").format(); // 2014-06-22T09:21:08-07:00 +moment(date).tz("America/Los_Angeles").format(); // 2014-06-22T09:21:08-07:00 + +moment.tz("2012-03-11 01:59:59", "America/New_York").format() // 2012-03-11T01:59:59-05:00 +moment.tz("2012-03-11 02:00:00", "America/New_York").format() // 2012-03-11T03:00:00-04:00 +moment.tz("2012-03-11 02:59:59", "America/New_York").format() // 2012-03-11T03:59:59-04:00 +moment.tz("2012-03-11 03:00:00", "America/New_York").format() // 2012-03-11T03:00:00-04:00 + +moment.tz("2012-11-04 00:59:59", "America/New_York"); // 2012-11-04T00:59:59-04:00 +moment.tz("2012-11-04 01:00:00", "America/New_York"); // 2012-11-04T01:00:00-04:00 +moment.tz("2012-11-04 01:59:59", "America/New_York"); // 2012-11-04T01:59:59-04:00 +moment.tz("2012-11-04 02:00:00", "America/New_York"); // 2012-11-04T02:00:00-05:00 + +moment.tz("2012-11-04 01:00:00-04:00", "America/New_York"); // 2012-11-04T01:00:00-04:00 +moment.tz("2012-11-04 01:00:00-05:00", "America/New_York"); // 2012-11-04T01:00:00-05:00 + +moment.tz([2012, 0], 'America/New_York').format('z'); // EST +moment.tz([2012, 5], 'America/New_York').format('z'); // EDT +moment.tz([2012, 0], 'America/Los_Angeles').format('z'); // PST +moment.tz([2012, 5], 'America/Los_Angeles').format('z'); // PDT + +// Denver observes DST +moment.tz([2012, 0], 'America/Denver').format('Z z'); // -07:00 MST +moment.tz([2012, 5], 'America/Denver').format('Z z'); // -06:00 MDT +// Phoenix does not observe DST +moment.tz([2012, 0], 'America/Phoenix').format('Z z'); // -07:00 MST +moment.tz([2012, 5], 'America/Phoenix').format('Z z'); // -07:00 MST + +moment.tz('2016-01-01', 'America/Chicago').format('z'); // CST +moment.tz('2016-01-01', 'Asia/Shanghai').format('z'); // CST + +moment.tz([2012, 0], 'America/New_York').zoneAbbr(); // EST +moment.tz([2012, 5], 'America/New_York').zoneAbbr(); // EDT + +var abbrs: {[prop: string]: string} = { + EST : 'Eastern Standard Time', + EDT : 'Eastern Daylight Time', + CST : 'Central Standard Time', + CDT : 'Central Daylight Time', + MST : 'Mountain Standard Time', + MDT : 'Mountain Daylight Time', + PST : 'Pacific Standard Time', + PDT : 'Pacific Daylight Time', +}; + +moment.fn.zoneName = function () { + var abbr = this.zoneAbbr(); + return abbrs[abbr] || abbr; +}; + +moment.tz([2012, 0], 'America/New_York').format('zz'); // Eastern Standard Time +moment.tz([2012, 5], 'America/New_York').format('zz'); // Eastern Daylight Time +moment.tz([2012, 0], 'America/Los_Angeles').format('zz'); // Pacific Standard Time +moment.tz([2012, 5], 'America/Los_Angeles').format('zz'); // Pacific Daylight Time + +moment.tz('America/Los_Angeles').format('z') // "PDT" (abbreviation) +moment.tz('Asia/Magadan').format('z') // "+11" (3-char offset) +moment.tz('Asia/Colombo').format('z') // "+0530" (5-char offset) + +moment.tz.setDefault("America/New_York"); + +moment.tz.setDefault(); + +moment.tz.guess(); // America/Chicago + +const unpackedZone: moment.UnpackedZone = { + name : 'America/Los_Angeles', // the unique identifier + abbrs : ['PDT', 'PST'], // the abbreviations + untils : [1414918800000, 1425808800000], // the timestamps in milliseconds + offsets : [420, 480] // the offsets in minutes +} + +moment.tz.zone('America/Los_Angeles').abbr(1403465838805); // PDT +moment.tz.zone('America/Los_Angeles').abbr(1388563200000); // PST + +moment.tz.zone('America/Los_Angeles').offset(1403465838805); // 420 +moment.tz.zone('America/Los_Angeles').offset(1388563200000); // 480 + +var zone = moment.tz.zone('America/New_York'); +zone.parse(Date.UTC(2012, 2, 19, 8, 30)); // 240 + +var zone = moment.tz.zone('America/New_York'); +zone.parse(Date.UTC(2012, 2, 11, 1, 59)); // 300 +zone.parse(Date.UTC(2012, 2, 11, 2, 0)); // 240 + +const unpackedZone2: moment.UnpackedZone = { + name : 'America/Los_Angeles', + abbrs : ['PST', 'PDT','PST', 'PDT', 'PST', 'PDT', 'PST', 'PDT', 'PST', 'PDT', 'PST'], + untils : [1394359200000, 1414918800000, 1425808800000, 1446368400000, 1457863200000, 1478422800000, 1489312800000, 1509872400000, 1520762400000, 1541322000000, null], + offsets : [480, 420, 480, 420, 480, 420, 480, 420, 480, 420, 480] +} + +'America/Los_Angeles|PST PDT|80 70|01010101010|1Lzm0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0' + +moment.tz.add('America/Los_Angeles|PST PDT|80 70|01010101010|1Lzm0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0'); +moment.tz.link('America/Los_Angeles|US/Pacific'); +moment.tz("2013-12-01", "America/Los_Angeles").format(); // 2013-12-01T00:00:00-08:00 +moment.tz("2013-12-01", "US/Pacific").format(); // 2013-12-01T00:00:00-08:00 + +moment.tz.add('America/Los_Angeles|PST PDT|80 70|0101|1Lzm0 1zb0 Op0'); + +moment.tz.add([ + 'America/Los_Angeles|PST PDT|80 70|0101|1Lzm0 1zb0 Op0', + 'America/New_York|EST EDT|50 40|0101|1Lz50 1zb0 Op0' +]); + +moment.tz.link('America/Los_Angeles|US/Pacific'); + +moment.tz.link([ + 'America/Los_Angeles|US/Pacific', + 'America/New_York|US/Eastern' +]); + +const packedZoneBundle: moment.PackedZoneBundle = { + version : '2014e', + zones : [ + 'America/Los_Angeles|PST PDT|80 70|0101|1Lzm0 1zb0 Op0', + 'America/New_York|EST EDT|50 40|0101|1Lz50 1zb0 Op0' + ], + links : [ + 'America/Los_Angeles|US/Pacific', + 'America/New_York|US/Eastern' + ] +} + +moment.tz.load({ + version : '2014e', + zones : ['packed', 'packed'], + links : ['packedlink', 'packedlink'] +}) + +moment.tz.zone("UnloadedZone"); // null +moment.tz.add("UnloadedZone|UZ|0|0|"); +moment.tz.zone("UnloadedZone"); // Zone { name : "UnloadedZone", ...} + +moment.tz.names(); // ["Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", ...] + +// in moment-timezone.js +moment.tz.unpack +moment.tz.unpackBase60 +// in moment-timezone-utils.js +moment.tz.pack +moment.tz.packBase60 +moment.tz.createLinks +moment.tz.filterYears +moment.tz.filterLinkPack + +var unpacked = { + name : 'Indian/Mauritius', + abbrs : ['LMT', 'MUT', 'MUST', 'MUT', 'MUST', 'MUT'], + offsets : [-230, -240, -300, -240, -300, -240], + untils : [-1988164200000, 403041600000, 417034800000, 1224972000000, 1238274000000, null] +}; +moment.tz.pack(unpacked); // "Indian/Mauritius|LMT MUT MUST|-3O -40 -50|012121|-2xorO 34unO 14L0 12kr0 11z0" + +var packed = "Indian/Mauritius|LMT MUT MUST|-3O -40 -50|012121|-2xorO 34unO 14L0 12kr0 11z0"; + +moment.tz.unpack(packed); +// { +// name : 'Indian/Mauritius', +// abbrs : ['LMT', 'MUT', 'MUST', 'MUT', 'MUST', 'MUT'], +// offsets : [-230, -240, -300, -240, -300, -240], +// untils : [-1988164200000, 403041600000, 417034800000, 1224972000000, 1238274000000, null] +// }; + +moment.tz.packBase60(9); // 9 +moment.tz.packBase60(10); // a +moment.tz.packBase60(59); // X +moment.tz.packBase60(1337); // mh + +moment.tz.packBase60(1.1667, 1); // 1.a +moment.tz.packBase60(20.12345, 3); // k.7op +moment.tz.packBase60(59, 1); // X + +moment.tz.packBase60(1.1667, 1); // 1.a +moment.tz.packBase60(0.1667, 1); // .a + +moment.tz.packBase60(1/6, 1); // .a +moment.tz.packBase60(1/6, 5); // .a +moment.tz.packBase60(59, 5); // X + +moment.tz.unpackBase60('9'); // 9 +moment.tz.unpackBase60('a'); // 10 +moment.tz.unpackBase60('X'); // 59 +moment.tz.unpackBase60('mh'); // 1337 +moment.tz.unpackBase60('1.9'); // 1.15 +moment.tz.unpackBase60('k.7op'); // 20.123449074074074 + +var unlinked: moment.UnpackedZoneBundle = { + zones : [ + {name:"Zone/One",abbrs:["OST","ODT"],offsets:[60,120],untils:[403041600000,417034800000]}, + {name:"Zone/Two",abbrs:["OST","ODT"],offsets:[60,120],untils:[403041600000,417034800000]} + ], + links: [], + version : "2014x-doc-example" +}; + +moment.tz.createLinks(unlinked); + +// { +// zones : [ +// {name:"Zone/One",abbrs:["OST","ODT"],offsets:[60,120],untils:[403041600000,417034800000]} +// ], +// links : ["Zone/One|Zone/Two"], +// version : "2014x-doc-example" +// } + +var all = { name : "America/Los_Angeles", abbrs : ['EST', 'EDT'], offsets : [1, 2], untils : [1, 2]}; +var subset = moment.tz.filterYears(all, 2012, 2016); +all.untils.length; // 186 +subset.untils.length; // 11 + +var all = { name : "America/Los_Angeles", abbrs : ['EST', 'EDT'], offsets : [1, 2], untils : [1, 2]}; +var subset = moment.tz.filterYears(all, 2012); +all.untils.length; // 186 +subset.untils.length; // 3 \ No newline at end of file diff --git a/typing-tests/tsconfig.json b/typing-tests/tsconfig.json new file mode 100644 index 00000000..f96dd31c --- /dev/null +++ b/typing-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "module": "commonjs", + "noEmit": true, + "noImplicitAny": true, + "types": [] + }, + "include": [ + "../index.d.ts", + "../moment-timezone-utils.d.ts", + "./moment-tests.ts" + ] +}