@fr0st/datetime 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@fr0st/datetime",
3
+ "version": "5.0.0",
4
+ "description": "FrostDateTime is a free, open-source date manipulation library for JavaScript.",
5
+ "keywords": [
6
+ "date",
7
+ "time",
8
+ "datetime",
9
+ "timezone",
10
+ "locale",
11
+ "localization",
12
+ "icu",
13
+ "parse",
14
+ "format"
15
+ ],
16
+ "homepage": "https://github.com/elusivecodes/FrostDateTime",
17
+ "bugs": {
18
+ "url": "https://github.com/elusivecodes/FrostDateTime/issues",
19
+ "email": "elusivecodes@gmail.com"
20
+ },
21
+ "main": "src/index.js",
22
+ "type": "module",
23
+ "files": [
24
+ "/LICENSE",
25
+ "/README.md",
26
+ "src"
27
+ ],
28
+ "scripts": {
29
+ "build": "npm run js-compile && npm run js-minify",
30
+ "js-compile": "rollup src/index.js --file dist/frost-datetime.js --format iife --sourcemap --name DateTime",
31
+ "js-lint": "eslint --ext .js .",
32
+ "js-minify": "terser --compress passes=2 --mangle --source-map \"content=dist/frost-datetime.js.map\" --output dist/frost-datetime.min.js dist/frost-datetime.js",
33
+ "test": "mocha --recursive",
34
+ "locales": "php generate-locales.php > src/formatter/locales.js"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/elusivecodes/FrostDateTime.git"
39
+ },
40
+ "author": "Elusive <elusivecodes@gmail.com>",
41
+ "license": "MIT",
42
+ "private": false,
43
+ "devDependencies": {
44
+ "eslint": "^8.30.0",
45
+ "eslint-config-google": "^0.14.0",
46
+ "filepath": "^1.1.0",
47
+ "mocha": "^10.2.0",
48
+ "rollup": "^3.7.5",
49
+ "terser": "^5.16.1"
50
+ }
51
+ }
@@ -0,0 +1,213 @@
1
+ import { getOffset } from './helpers.js';
2
+ import { config, dateStringTimeZoneRegExp, offsetRegExp } from './vars.js';
3
+ import { formatOffset } from './formatter/format.js';
4
+
5
+ /**
6
+ * DateTime class
7
+ * @class
8
+ */
9
+ export default class DateTime {
10
+ #date;
11
+ #timeZone;
12
+ #locale;
13
+ #offset = 0;
14
+ #dynamicTz;
15
+
16
+ /**
17
+ * New DateTime constructor.
18
+ * @param {string|number|null} [date] The date or timestamp to parse.
19
+ * @param {object} [options] Options for the new DateTime.
20
+ * @param {string} [options.timeZone] The timeZone to use.
21
+ * @param {string} [options.locale] The locale to use.
22
+ */
23
+ constructor(date = null, options = {}) {
24
+ let timestamp;
25
+ let adjustOffset = false;
26
+
27
+ if (date === null) {
28
+ timestamp = Date.now();
29
+ } else if (!isNaN(parseInt(date)) && isFinite(date)) {
30
+ timestamp = date;
31
+ } else if (date === `${date}`) {
32
+ timestamp = Date.parse(date);
33
+
34
+ if (isNaN(timestamp)) {
35
+ throw new Error('Invalid date string supplied');
36
+ }
37
+
38
+ if (!date.match(dateStringTimeZoneRegExp)) {
39
+ timestamp -= new Date()
40
+ .getTimezoneOffset() *
41
+ 60000;
42
+ }
43
+
44
+ adjustOffset = true;
45
+ } else {
46
+ throw new Error('Invalid date supplied');
47
+ }
48
+
49
+ this.#date = new Date(timestamp);
50
+ this.#dynamicTz = false;
51
+ this.isValid = true;
52
+
53
+ let timeZone = options.timeZone;
54
+
55
+ if (!timeZone) {
56
+ timeZone = config.defaultTimeZone;
57
+ }
58
+
59
+ if (['Z', 'GMT'].includes(timeZone)) {
60
+ timeZone = 'UTC';
61
+ }
62
+
63
+ const match = timeZone.match(offsetRegExp);
64
+ if (match) {
65
+ this.#offset = match[2] * 60 + parseInt(match[4] || 0);
66
+ if (this.#offset && match[1] === '+') {
67
+ this.#offset *= -1;
68
+ }
69
+
70
+ if (this.#offset) {
71
+ this.#timeZone = formatOffset(this.#offset);
72
+ } else {
73
+ this.#dynamicTz = true;
74
+ this.#timeZone = 'UTC';
75
+ }
76
+ } else {
77
+ this.#dynamicTz = true;
78
+ this.#timeZone = timeZone;
79
+ }
80
+
81
+ if (this.#dynamicTz) {
82
+ this.#offset = getOffset(this);
83
+ }
84
+
85
+ if (adjustOffset && this.#offset) {
86
+ const oldOffset = this.#offset;
87
+
88
+ this.#date.setTime(this.getTime() + this.#offset * 60000);
89
+
90
+ if (this.#dynamicTz) {
91
+ this.#offset = getOffset(this);
92
+
93
+ // compensate for DST transitions
94
+ if (oldOffset !== this.#offset) {
95
+ this.#date.setTime(this.getTime() - ((oldOffset - offset) * 60000));
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!('locale' in options)) {
101
+ options.locale = config.defaultLocale;
102
+ }
103
+
104
+ this.#locale = options.locale;
105
+ }
106
+
107
+ /**
108
+ * Get the name of the current locale.
109
+ * @return {string} The name of the current locale.
110
+ */
111
+ getLocale() {
112
+ return this.#locale;
113
+ }
114
+
115
+ /**
116
+ * Get the number of milliseconds since the UNIX epoch.
117
+ * @return {number} The number of milliseconds since the UNIX epoch.
118
+ */
119
+ getTime() {
120
+ return this.#date.getTime();
121
+ }
122
+
123
+ /**
124
+ * Get the name of the current timeZone.
125
+ * @return {string} The name of the current timeZone.
126
+ */
127
+ getTimeZone() {
128
+ return this.#timeZone;
129
+ }
130
+
131
+ /**
132
+ * Get the UTC offset (in minutes) of the current timeZone.
133
+ * @return {number} The UTC offset (in minutes) of the current timeZone.
134
+ */
135
+ getTimeZoneOffset() {
136
+ return this.#offset;
137
+ }
138
+
139
+ /**
140
+ * Determine if the timeZone is dynamic.
141
+ * @return {Boolean} TRUE if the timeZone is dynamic, otherwise FALSE.
142
+ */
143
+ isDynamicTimeZone() {
144
+ return this.#dynamicTz;
145
+ }
146
+
147
+ /**
148
+ * Set the current locale.
149
+ * @param {string} locale The name of the timeZone.
150
+ * @return {DateTime} The DateTime object.
151
+ */
152
+ setLocale(locale) {
153
+ return new DateTime(this.getTime(), {
154
+ locale,
155
+ timeZone: this.#timeZone,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Set the number of milliseconds since the UNIX epoch.
161
+ * @param {number} time The number of milliseconds since the UNIX epoch.
162
+ * @return {DateTime} The DateTime object.
163
+ */
164
+ setTime(time) {
165
+ return new DateTime(time, {
166
+ locale: this.#locale,
167
+ timeZone: this.#timeZone,
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Set the current timeZone.
173
+ * @param {string} timeZone The name of the timeZone.
174
+ * @return {DateTime} The DateTime object.
175
+ */
176
+ setTimeZone(timeZone) {
177
+ return new DateTime(this.getTime(), {
178
+ locale: this.#locale,
179
+ timeZone,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Set the current UTC offset.
185
+ * @param {number} offset The UTC offset (in minutes).
186
+ * @return {DateTime} The DateTime object.
187
+ */
188
+ setTimeZoneOffset(offset) {
189
+ return new DateTime(this.getTime(), {
190
+ locale: this.#locale,
191
+ timeZone: formatOffset(offset),
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Get the number of milliseconds since the UNIX epoch.
197
+ * @return {number} The number of milliseconds since the UNIX epoch.
198
+ */
199
+ valueOf() {
200
+ return this.getTime();
201
+ }
202
+
203
+ /**
204
+ * Return a primitive value of the DateTime.
205
+ * @param {string} hint The type hint.
206
+ * @return {string|number}
207
+ */
208
+ [Symbol.toPrimitive](hint) {
209
+ return hint === 'number' ?
210
+ this.valueOf() :
211
+ this.toString();
212
+ }
213
+ }
package/src/factory.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * DateTime Factory
3
+ */
4
+
5
+ const data = {};
6
+
7
+ /**
8
+ * Get values from cache (or generate if they don't exist).
9
+ * @param {string} key The key for the values.
10
+ * @param {function} callback The callback to generate the values.
11
+ * @return {array} The cached values.
12
+ */
13
+ export function getData(key, callback) {
14
+ if (!(key in data)) {
15
+ data[key] = callback();
16
+ }
17
+
18
+ return data[key];
19
+ };
20
+
21
+ /**
22
+ * Create a new date formatter for a timeZone.
23
+ * @param {string} timeZone The timeZone.
24
+ * @param {object} options The options for the formatter.
25
+ * @return {Intl.DateTimeFormat} A new DateTimeFormat object.
26
+ */
27
+ export function getDateFormatter(timeZone) {
28
+ return getData(
29
+ `dateFormatter.${timeZone}`,
30
+ (_) => makeFormatter('en', {
31
+ timeZone,
32
+ hourCycle: 'h23',
33
+ year: 'numeric',
34
+ month: 'numeric',
35
+ day: 'numeric',
36
+ hour: 'numeric',
37
+ minute: 'numeric',
38
+ }),
39
+ );
40
+ };
41
+
42
+ /**
43
+ * Create a new relative formatter for a locale.
44
+ * @param {string} locale The locale.
45
+ * @param {object} options The options for the formatter.
46
+ * @return {Intl.RelativeTimeFormat} A new RelativeTimeFormat object.
47
+ */
48
+ export function getRelativeFormatter(locale) {
49
+ if (!('RelativeTimeFormat' in Intl)) {
50
+ return null;
51
+ }
52
+
53
+ return getData(
54
+ `relativeFormatter.${locale}`,
55
+ (_) => new Intl.RelativeTimeFormat(locale, {
56
+ numeric: 'auto',
57
+ style: 'long',
58
+ }),
59
+ );
60
+ };
61
+
62
+ /**
63
+ * Create a new formatter for a locale.
64
+ * @param {string} locale The locale.
65
+ * @param {object} options The options for the formatter.
66
+ * @return {Intl.DateTimeFormat} A new DateTimeFormat object.
67
+ */
68
+ export function makeFormatter(locale, options) {
69
+ return new Intl.DateTimeFormat(locale, {
70
+ timeZone: 'UTC',
71
+ ...options,
72
+ });
73
+ };
@@ -0,0 +1,104 @@
1
+ import { makeFormatter } from './../factory.js';
2
+ import { getDayPeriods, getDays, getEras, getMonths, getNumbers } from './values.js';
3
+
4
+ /**
5
+ * Format a day as a locale string.
6
+ * @param {string} locale The locale.
7
+ * @param {number} day The day to format (0-6).
8
+ * @param {string} [type=long] The formatting type.
9
+ * @param {Boolean} [standalone=true] Whether the value is standalone.
10
+ * @return {string} The formatted string.
11
+ */
12
+ export function formatDay(locale, day, type = 'long', standalone = true) {
13
+ return getDays(locale, type, standalone)[day];
14
+ };
15
+
16
+ /**
17
+ * Format a day period as a locale string.
18
+ * @param {string} locale The locale.
19
+ * @param {number} period The period to format (0-1).
20
+ * @param {string} [type=long] The formatting type.
21
+ * @return {string} The formatted string.
22
+ */
23
+ export function formatDayPeriod(locale, period, type = 'long') {
24
+ return getDayPeriods(locale, type)[period];
25
+ };
26
+
27
+ /**
28
+ * Format an era as a locale string.
29
+ * @param {string} locale The locale.
30
+ * @param {number} era The period to format (0-1).
31
+ * @param {string} [type=long] The formatting type.
32
+ * @return {string} The formatted string.
33
+ */
34
+ export function formatEra(locale, era, type = 'long') {
35
+ return getEras(locale, type)[era];
36
+ };
37
+
38
+ /**
39
+ * Format a month as a locale string.
40
+ * @param {string} locale The locale.
41
+ * @param {number} month The month to format (1-12).
42
+ * @param {string} [type=long] The formatting type.
43
+ * @param {Boolean} [standalone=true] Whether the value is standalone.
44
+ * @return {string} The formatted string.
45
+ */
46
+ export function formatMonth(locale, month, type = 'long', standalone = true) {
47
+ return getMonths(locale, type, standalone)[month - 1];
48
+ };
49
+
50
+ /**
51
+ * Format a number as a locale number string.
52
+ * @param {string} locale The locale.
53
+ * @param {number} number The number to format.
54
+ * @param {number} [padding=0] The amount of padding to use.
55
+ * @return {string} The formatted string.
56
+ */
57
+ export function formatNumber(locale, number, padding = 0) {
58
+ const numbers = getNumbers(locale);
59
+ return `${number}`
60
+ .padStart(padding, 0)
61
+ .replace(/\d/g, (match) => numbers[match]);
62
+ };
63
+
64
+ /**
65
+ * Format a number to an offset string.
66
+ * @param {number} offset The offset to format.
67
+ * @param {Boolean} [useColon=true] Whether to use a colon seperator.
68
+ * @param {Boolean} [optionalMinutes=false] Whether minutes are optional.
69
+ * @return {string} The formatted offset string.
70
+ */
71
+ export function formatOffset(offset, useColon = true, optionalMinutes = false) {
72
+ const hours = Math.abs(
73
+ (offset / 60) | 0,
74
+ );
75
+ const minutes = Math.abs(offset % 60);
76
+
77
+ const sign = offset > 0 ?
78
+ '-' :
79
+ '+';
80
+ const hourString = `${hours}`.padStart(2, 0);
81
+ const minuteString = minutes || !optionalMinutes ?
82
+ `${minutes}`.padStart(2, 0) :
83
+ '';
84
+ const colon = useColon && minuteString ?
85
+ ':' :
86
+ '';
87
+
88
+ return `${sign}${hourString}${colon}${minuteString}`;
89
+ };
90
+
91
+ /**
92
+ * Format a time zone as a locale string.
93
+ * @param {string} locale The locale.
94
+ * @param {number} timestamp The timestamp to use.
95
+ * @param {string} timeZone The time zone to format.
96
+ * @param {string} [type=long] The formatting type.
97
+ * @return {string} The formatted string.
98
+ */
99
+ export function formatTimeZoneName(locale, timestamp, timeZone, type = 'long') {
100
+ return makeFormatter(locale, { second: 'numeric', timeZone, timeZoneName: type })
101
+ .formatToParts(timestamp)
102
+ .find((part) => part.type === 'timeZoneName')
103
+ .value;
104
+ };
@@ -0,0 +1,2 @@
1
+ export const weekStart = { '1': ['af', 'am', 'ar-il', 'ar-sa', 'ar-ye', 'as', 'bn', 'bo', 'brx', 'ccp', 'ceb', 'chr', 'dav', 'dz', 'ebu', 'en', 'fil', 'gu', 'guz', 'haw', 'he', 'hi', 'id', 'ii', 'ja', 'jv', 'kam', 'ki', 'kln', 'km', 'kn', 'ko', 'kok', 'ks', 'lkt', 'lo', 'luo', 'luy', 'mas', 'mer', 'mgh', 'ml', 'mr', 'mt', 'my', 'nd', 'ne', 'om', 'or', 'pa', 'ps-pk', 'pt', 'qu', 'saq', 'sd', 'seh', 'sn', 'ta', 'te', 'th', 'ti', 'ug', 'ur', 'xh', 'yue', 'zh', 'zu'], '7': ['ar', 'ckb', 'en-ae', 'en-sd', 'fa', 'kab', 'lrc', 'mzn', 'ps'] };
2
+ export const minDaysInFirstWeek = { '4': ['ast', 'bg', 'br', 'ca', 'ce', 'cs', 'cy', 'da', 'de', 'dsb', 'el', 'en-at', 'en-be', 'en-ch', 'en-de', 'en-dk', 'en-fi', 'en-fj', 'en-gb', 'en-gg', 'en-gi', 'en-ie', 'en-im', 'en-je', 'en-nl', 'en-se', 'es', 'et', 'eu', 'fi', 'fo', 'fr', 'fur', 'fy', 'ga', 'gd', 'gl', 'gsw', 'gv', 'hsb', 'hu', 'is', 'it', 'ksh', 'kw', 'lb', 'lt', 'nb', 'nds', 'nl', 'nn', 'os-ru', 'pl', 'pt-ch', 'pt-lu', 'pt-pt', 'rm', 'ru', 'sah', 'se', 'sk', 'smn', 'sv', 'tt', 'wae'] };
@@ -0,0 +1,62 @@
1
+ import { getDayPeriods, getDays, getEras, getMonths, getNumbers } from './values.js';
2
+ import { weekDay } from './utility.js';
3
+
4
+ /**
5
+ * Parse a day from a locale string.
6
+ * @param {string} locale The locale.
7
+ * @param {string} value The value to parse.
8
+ * @param {string} [type=long] The formatting type.
9
+ * @param {Boolean} [standalone=true] Whether the value is standalone.
10
+ * @return {number} The day number (0-6).
11
+ */
12
+ export function parseDay(locale, value, type = 'long', standalone = true) {
13
+ const day = getDays(locale, type, standalone).indexOf(value) || 7;
14
+ return weekDay(locale, day);
15
+ };
16
+
17
+ /**
18
+ * Parse a day period from a locale string.
19
+ * @param {string} locale The locale.
20
+ * @param {string} value The value to parse.
21
+ * @param {string} [type=long] The formatting type.
22
+ * @return {number} The day period (0-1).
23
+ */
24
+ export function parseDayPeriod(locale, value, type = 'long') {
25
+ return getDayPeriods(locale, type).indexOf(value);
26
+ };
27
+
28
+ /**
29
+ * Parse an era from a locale string.
30
+ * @param {string} locale The locale.
31
+ * @param {string} value The value to parse.
32
+ * @param {string} [type=long] The formatting type.
33
+ * @return {number} The era (0-1).
34
+ */
35
+ export function parseEra(locale, value, type = 'long') {
36
+ return getEras(locale, type).indexOf(value);
37
+ };
38
+
39
+ /**
40
+ * Parse a month from a locale string.
41
+ * @param {string} locale The locale.
42
+ * @param {string} value The value to parse.
43
+ * @param {string} [type=long] The formatting type.
44
+ * @param {Boolean} [standalone=true] Whether the value is standalone.
45
+ * @return {number} The month number (1-12).
46
+ */
47
+ export function parseMonth(locale, value, type = 'long', standalone = true) {
48
+ return getMonths(locale, type, standalone).indexOf(value) + 1;
49
+ };
50
+
51
+ /**
52
+ * Parse a number from a locale number string.
53
+ * @param {string} locale The locale.
54
+ * @param {string} value The value to parse.
55
+ * @return {number} The parsed number.
56
+ */
57
+ export function parseNumber(locale, value) {
58
+ const numbers = getNumbers(locale);
59
+ return parseInt(
60
+ `${value}`.replace(/./g, (match) => numbers.indexOf(match)),
61
+ );
62
+ };