@js-joda/core 6.3.0 → 6.3.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-joda/core",
3
- "version": "6.3.0",
3
+ "version": "6.3.2",
4
4
  "description": "a date and time library for javascript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "test": "NODE_ENV=test npx mocha --timeout 5000 --require @babel/register ./test/*Test_mochaOnly.js ./test/*Test.js ./test/**/*Test.js ./test/**/**/*Test.js",
32
32
  "test-coverage": "NODE_ENV=test COVERAGE=1 npx nyc --report-dir=build/coverage --reporter=lcov --reporter html npx mocha --timeout 5000 --require @babel/register --reporter progress ./test/*Test.js ./test/**/*Test.js ./test/**/**/*Test.js",
33
33
  "test-browser": "npx karma start --reporters=dots --single-run",
34
- "test-ci": "npm run build-dist && npm run test && npm run test-browser && npm run test-ts-definitions && npm run test-coverage",
34
+ "test-ci": "npm run build-dist && npm run lint && npm run test && npm run test-browser && npm run test-ts-definitions && npm run test-coverage",
35
35
  "test-ts-definitions": "TS_NODE_PROJECT='../../shared/ts-test-config.json' NODE_ENV=test mocha --timeout 5000 --require ts-node/register ./test/typescript_definitions/*.ts",
36
36
  "build-dist": "npx rollup -c rollup.config.js",
37
37
  "build-md-toc": "npx markdown-toc -i CheatSheet.md",
@@ -47,5 +47,5 @@
47
47
  "sourceMap": false,
48
48
  "instrument": false
49
49
  },
50
- "gitHead": "8c0a3b1b555bc09fced243dac13fdf5ef9746dc7"
50
+ "gitHead": "2364adefa7985123ce6d17d4181bf93d0e179351"
51
51
  }
package/src/Duration.js CHANGED
@@ -315,7 +315,7 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
315
315
  /**
316
316
  * The pattern for parsing.
317
317
  */
318
- const PATTERN = new RegExp('([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?', 'i');
318
+ const PATTERN = new RegExp('^([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?$', 'i');
319
319
  const matches = PATTERN.exec(text);
320
320
  if (matches !== null) {
321
321
  // check for letter T but no time sections
@@ -879,8 +879,11 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
879
879
  /**
880
880
  * Returns a copy of this duration divided by the specified value.
881
881
  *
882
- * In opposite to the threeten implementation the division is realized by floating point not by
883
- * fixed point arithmetic. Expect floating point rounding errors for {@link Duration.dividedBy}.
882
+ * The result is truncated toward zero to the nanosecond, as in threeten.
883
+ * For an integer divisor up to `MAX_SAFE_INTEGER` (positive or negative) the division is exact.
884
+ * Other divisors, with a fraction or beyond `MAX_SAFE_INTEGER`, are not exactly representable
885
+ * or not supported by threeten; they are divided by floating point arithmetic, expect floating
886
+ * point rounding errors.
884
887
  *
885
888
  * @param {Number} divisor - the value to divide the duration by, positive or negative, not zero
886
889
  * @return {Duration} based on this duration divided by the specified divisor, not null
@@ -902,10 +905,30 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
902
905
  seconds += 1;
903
906
  nanos -= LocalTime.NANOS_PER_SECOND;
904
907
  }
905
- const secs = MathUtil.intDiv(seconds, divisor);
906
- const remainder = MathUtil.intMod(seconds, divisor);
907
- const nos = MathUtil.intDiv(remainder * LocalTime.NANOS_PER_SECOND + nanos, divisor);
908
- return Duration.ofSeconds(secs, nos);
908
+ if (!Number.isInteger(divisor) || Math.abs(divisor) > MAX_SAFE_INTEGER) {
909
+ const secs = MathUtil.intDiv(seconds, divisor);
910
+ const remainder = MathUtil.intMod(seconds, divisor);
911
+ const nos = MathUtil.intDiv(remainder * LocalTime.NANOS_PER_SECOND + nanos, divisor);
912
+ return Duration.ofSeconds(secs, nos);
913
+ }
914
+ // the remainder operator is exact for integers, so are secs and the remainder
915
+ const remainder = seconds % divisor;
916
+ const secs = MathUtil.safeZero((seconds - remainder) / divisor);
917
+ const sign = ((seconds < 0 || nanos < 0) ? -1 : 1) * (divisor < 0 ? -1 : 1);
918
+ const absRemainder = Math.abs(remainder);
919
+ const absNanos = Math.abs(nanos);
920
+ const absDivisor = Math.abs(divisor);
921
+ // the largest remainder for which remainder * NANOS_PER_SECOND + nanos + divisor <= MAX_SAFE_INTEGER
922
+ const headroom = MAX_SAFE_INTEGER - absDivisor - absNanos;
923
+ let quotientNanos;
924
+ if (headroom >= 0 && absRemainder <= MathUtil.intDiv(headroom, LocalTime.NANOS_PER_SECOND)) {
925
+ // the usual case, e.g. dividedBy(60): remainder * NANOS_PER_SECOND + nanos is exact
926
+ quotientNanos = exactFloorDiv(absRemainder * LocalTime.NANOS_PER_SECOND + absNanos, absDivisor);
927
+ } else {
928
+ // remainder * NANOS_PER_SECOND exceeds MAX_SAFE_INTEGER, divide it without computing it
929
+ quotientNanos = divideScaledByNanos(absRemainder, absNanos, absDivisor);
930
+ }
931
+ return Duration.ofSeconds(secs, MathUtil.safeZero(sign * quotientNanos));
909
932
  }
910
933
 
911
934
  //-----------------------------------------------------------------------
@@ -1308,6 +1331,74 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1308
1331
 
1309
1332
  }
1310
1333
 
1334
+ /**
1335
+ * `floor(value / divisor)` by floating point division, exact if `value + divisor <= MAX_SAFE_INTEGER`:
1336
+ * with `value = q * divisor + r` and `0 <= r < divisor`, the quotient `value / divisor` is at least
1337
+ * `1 / divisor` below `q + 1`. Rounding it up to `q + 1` would need that distance to be at most half
1338
+ * an ulp of `q + 1`, at most `(q + 1) / 2^53`, so `divisor * (q + 1) >= 2^53`; but
1339
+ * `divisor * (q + 1) <= value + divisor < 2^53`.
1340
+ *
1341
+ * @param {number} value - integer, `0 <= value`
1342
+ * @param {number} divisor - positive integer, `value + divisor <= MAX_SAFE_INTEGER`
1343
+ * @return {number}
1344
+ * @private
1345
+ */
1346
+ function exactFloorDiv(value, divisor) {
1347
+ return Math.floor(value / divisor);
1348
+ }
1349
+
1350
+ /**
1351
+ * Computes `floor((remainder * NANOS_PER_SECOND + nanos) / divisor)` exactly, by long division over
1352
+ * the nine decimal digits of `nanos`, without the intermediate values exceeding `MAX_SAFE_INTEGER`.
1353
+ * Each step divides `rem * 10 + digit`, with `rem < divisor`, by `divisor`:
1354
+ *
1355
+ * * up to `MAX_SAFE_INTEGER / 11`, `rem * 10 + digit + divisor < 11 * divisor` is small enough
1356
+ * for {@link exactFloorDiv}
1357
+ * * above, `rem * 10` itself may exceed `MAX_SAFE_INTEGER`; it is computed as ten additions of `rem`
1358
+ * modulo `divisor`, counting the wrap-arounds, so that all values stay below `divisor`
1359
+ *
1360
+ * @param {number} remainder - integer, `0 <= remainder < divisor`
1361
+ * @param {number} nanos - integer, `0 <= nanos < NANOS_PER_SECOND`
1362
+ * @param {number} divisor - positive integer, at most `MAX_SAFE_INTEGER`
1363
+ * @return {number} the quotient, `0 <= quotient < NANOS_PER_SECOND`
1364
+ * @private
1365
+ */
1366
+ function divideScaledByNanos(remainder, nanos, divisor) {
1367
+ const directLimit = MathUtil.intDiv(MAX_SAFE_INTEGER, 11);
1368
+ let rem = remainder;
1369
+ let quotient = 0;
1370
+ for (let scale = 100000000; scale >= 1; scale /= 10) {
1371
+ const digit = MathUtil.intDiv(nanos, scale) % 10;
1372
+ let digitQuotient;
1373
+ if (divisor <= directLimit) {
1374
+ const value = rem * 10 + digit;
1375
+ digitQuotient = exactFloorDiv(value, divisor);
1376
+ rem = value - digitQuotient * divisor;
1377
+ } else {
1378
+ // digit < 10 < divisor, so divisor - digit and divisor - rem are positive and exact
1379
+ let acc = 0;
1380
+ digitQuotient = 0;
1381
+ for (let i = 0; i < 10; i++) {
1382
+ if (acc >= divisor - rem) {
1383
+ acc -= divisor - rem;
1384
+ digitQuotient++;
1385
+ } else {
1386
+ acc += rem;
1387
+ }
1388
+ }
1389
+ if (acc >= divisor - digit) {
1390
+ acc -= divisor - digit;
1391
+ digitQuotient++;
1392
+ } else {
1393
+ acc += digit;
1394
+ }
1395
+ rem = acc;
1396
+ }
1397
+ quotient = quotient * 10 + digitQuotient;
1398
+ }
1399
+ return quotient;
1400
+ }
1401
+
1311
1402
  export function _init() {
1312
1403
  /**
1313
1404
  * Constant for a duration of zero.
package/src/LocalDate.js CHANGED
@@ -178,9 +178,10 @@ export class LocalDate extends ChronoLocalDate{
178
178
  *
179
179
  * @param {number} [epochDay=0] - the Epoch Day to convert, based on the epoch 1970-01-01
180
180
  * @return {LocalDate} the local date, not null
181
- * @throws {AssertionError} if the epoch days exceeds the supported date range
181
+ * @throws {DateTimeException} if the epoch days exceeds the supported date range
182
182
  */
183
183
  static ofEpochDay(epochDay=0) {
184
+ ChronoField.EPOCH_DAY.checkValidValue(epochDay);
184
185
  let adjust, adjustCycles, doyEst, yearEst, zeroDay;
185
186
  zeroDay = epochDay + DAYS_0000_TO_1970;
186
187
  zeroDay -= 60;
package/src/Period.js CHANGED
@@ -18,7 +18,7 @@ import { LocalDate } from './LocalDate';
18
18
  /**
19
19
  * The pattern for parsing.
20
20
  */
21
- const PATTERN = /([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?/;
21
+ const PATTERN = /^([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?$/i;
22
22
 
23
23
  /**
24
24
  * A date-based amount of time, such as '2 years, 3 months and 4 days'.
@@ -13,8 +13,8 @@ import { ResolverStyle } from './ResolverStyle';
13
13
 
14
14
  import { IsoChronology } from '../chrono/IsoChronology';
15
15
  import { ChronoLocalDate } from '../chrono/ChronoLocalDate';
16
+ import { ChronoLocalDateTime } from '../chrono/ChronoLocalDateTime';
16
17
  import { ChronoField } from '../temporal/ChronoField';
17
- import { IsoFields } from '../temporal/IsoFields';
18
18
  import { TemporalAccessor } from '../temporal/TemporalAccessor';
19
19
  import { TemporalQueries } from '../temporal/TemporalQueries';
20
20
 
@@ -149,9 +149,14 @@ export class DateTimeBuilder extends TemporalAccessor {
149
149
  }
150
150
  // handle standard fields
151
151
  this._mergeInstantFields();
152
- this._resolveFields(resolverStyle);
153
152
  this._mergeDate(resolverStyle);
154
153
  this._mergeTime(resolverStyle);
154
+ // the other fields may need fields produced by merging, e.g. YEAR from YEAR_OF_ERA
155
+ if (this._resolveFields(resolverStyle)) {
156
+ this._mergeInstantFields();
157
+ this._mergeDate(resolverStyle);
158
+ this._mergeTime(resolverStyle);
159
+ }
155
160
  this._resolveTimeInferZeroes(resolverStyle);
156
161
  this._crossCheck();
157
162
  if (this.excessDays != null && this.excessDays.isZero() === false && this.date != null && this.time != null) {
@@ -164,28 +169,71 @@ export class DateTimeBuilder extends TemporalAccessor {
164
169
  }
165
170
 
166
171
  /**
167
- * Resolves the date fields that are not {@link ChronoField}s by delegating to their own
168
- * `resolve()`, the step java.time performs generically. As parsed values are held in a
169
- * name-keyed map, the ISO fields that define a `resolve()` (week-of-week-based-year and
170
- * day-of-quarter) are handled here. They do not depend on one another, so one pass is
171
- * sufficient. Returns whether a date was produced.
172
+ * Resolves the fields that are not {@link ChronoField}s, such as {@link IsoFields} or the
173
+ * fields of `@js-joda/locale` WeekFields, by delegating to their own `resolve()`, as
174
+ * java.time does. A field may resolve to a date, a time or a date-time, or replace itself
175
+ * by other fields (for example a localized day-of-week by {@link ChronoField.DAY_OF_WEEK}),
176
+ * so the fields are visited again after each change. Returns whether anything changed.
172
177
  *
173
178
  * @param {ResolverStyle} resolverStyle
174
179
  * @return {boolean}
175
180
  * @private
176
181
  */
177
182
  _resolveFields(resolverStyle) {
178
- let resolved = false;
179
- for (const field of [IsoFields.WEEK_OF_WEEK_BASED_YEAR, IsoFields.DAY_OF_QUARTER]) {
180
- if (this.fieldValues.containsKey(field)) {
181
- const date = field.resolve(this.fieldValues, this, resolverStyle);
182
- if (date != null) {
183
- this._checkDate(date);
184
- resolved = true;
183
+ let changes = 0;
184
+ let changed = true;
185
+ while (changed && changes < 100) {
186
+ changed = false;
187
+ const fields = this.fieldValues.keys();
188
+ for (let i = 0; i < fields.length; i++) {
189
+ const field = fields[i];
190
+ if (field instanceof ChronoField || typeof field.resolve !== 'function') {
191
+ continue;
192
+ }
193
+ const resolved = field.resolve(this.fieldValues, this, resolverStyle);
194
+ if (resolved != null) {
195
+ if (resolved instanceof ChronoLocalDateTime) {
196
+ this._resolveMakeChanges(field, resolved.toLocalDate());
197
+ this._resolveMakeChanges(field, resolved.toLocalTime());
198
+ } else if (resolved instanceof ChronoLocalDate || resolved instanceof LocalTime) {
199
+ this._resolveMakeChanges(field, resolved);
200
+ } else {
201
+ throw new DateTimeException(`Unknown type: ${resolved}`);
202
+ }
203
+ changed = true;
204
+ } else if (this.fieldValues.containsKey(field) === false) {
205
+ changed = true;
206
+ }
207
+ if (changed) {
208
+ changes++;
209
+ break; // start again, the fields have changed
185
210
  }
186
211
  }
187
212
  }
188
- return resolved;
213
+ if (changes === 100) {
214
+ throw new DateTimeException('Badly written field');
215
+ }
216
+ return changes > 0;
217
+ }
218
+
219
+ /**
220
+ * Adds a date or time resolved by a field as {@link ChronoField.EPOCH_DAY} or
221
+ * {@link ChronoField.NANO_OF_DAY}, to be merged with the other fields.
222
+ *
223
+ * @param {TemporalField} targetField
224
+ * @param {ChronoLocalDate|LocalTime} dateOrTime
225
+ * @private
226
+ */
227
+ _resolveMakeChanges(targetField, dateOrTime) {
228
+ const isDate = dateOrTime instanceof ChronoLocalDate;
229
+ const field = isDate ? ChronoField.EPOCH_DAY : ChronoField.NANO_OF_DAY;
230
+ const value = isDate ? dateOrTime.toEpochDay() : dateOrTime.toNanoOfDay();
231
+ const old = this.fieldValues.get(field);
232
+ if (old != null && old !== value) {
233
+ const oldDateOrTime = isDate ? LocalDate.ofEpochDay(old) : LocalTime.ofNanoOfDay(old);
234
+ throw new DateTimeException(`Conflict found: ${oldDateOrTime} differs from ${dateOrTime} while resolving ${targetField}`);
235
+ }
236
+ this.fieldValues.put(field, value);
189
237
  }
190
238
 
191
239
  /**
@@ -211,28 +259,26 @@ export class DateTimeBuilder extends TemporalAccessor {
211
259
  */
212
260
  _checkDate(date) {
213
261
  if (date != null) {
262
+ if (this.date != null && this.date.equals(date) === false) {
263
+ throw new DateTimeException(`Conflict found: Fields resolved to two different dates: ${this.date} ${date}`);
264
+ }
214
265
  this._addObject(date);
215
- for (const fieldName in this.fieldValues.keySet()) {
216
- const field = ChronoField.byName(fieldName);
217
- if (field) {
218
- if (this.fieldValues.get(field) !== undefined) { // undefined if "removed" in EnumMap
219
- if (field.isDateBased()) {
220
- let val1;
221
- try {
222
- val1 = date.getLong(field);
223
- } catch (ex) {
224
- if (ex instanceof DateTimeException) {
225
- continue;
226
- } else {
227
- throw ex;
228
- }
229
- }
230
- const val2 = this.fieldValues.get(field);
231
- if (val1 !== val2) {
232
- throw new DateTimeException(`Conflict found: Field ${field} ${val1} differs from ${field} ${val2} derived from ${date}`);
233
- }
266
+ for (const field of this.fieldValues.keys()) {
267
+ if (field instanceof ChronoField && field.isDateBased()) {
268
+ let val1;
269
+ try {
270
+ val1 = date.getLong(field);
271
+ } catch (ex) {
272
+ if (ex instanceof DateTimeException) {
273
+ continue;
274
+ } else {
275
+ throw ex;
234
276
  }
235
277
  }
278
+ const val2 = this.fieldValues.get(field);
279
+ if (val1 !== val2) {
280
+ throw new DateTimeException(`Conflict found: Field ${field} ${val1} differs from ${field} ${val2} derived from ${date}`);
281
+ }
236
282
  }
237
283
  }
238
284
  }
@@ -505,20 +551,6 @@ export class DateTimeBuilder extends TemporalAccessor {
505
551
  this._addFieldValue(ChronoField.SECOND_OF_DAY, zdt.toLocalTime().toSecondOfDay());
506
552
  }
507
553
 
508
- /**
509
- * @param {TemporalField} targetField
510
- * @param {LocalDate} date
511
- * @private
512
- */
513
- _resolveMakeChanges(targetField, date) {
514
- const epochDay = date.toEpochDay();
515
- const old = this.fieldValues.get(ChronoField.EPOCH_DAY);
516
- this.fieldValues.put(ChronoField.EPOCH_DAY, epochDay);
517
- if (old != null && old !== epochDay) {
518
- throw new DateTimeException(`Conflict found: ${LocalDate.ofEpochDay(old)} differs from ${LocalDate.ofEpochDay(epochDay)} while resolving ${targetField}`);
519
- }
520
- }
521
-
522
554
  /**
523
555
  * Checks the remaining field values against the resolved date and time, and removes the
524
556
  * ones that match.
@@ -540,25 +572,22 @@ export class DateTimeBuilder extends TemporalAccessor {
540
572
  * @private
541
573
  */
542
574
  _crossCheck0(temporal) {
543
- for (const fieldName in this.fieldValues.keySet()) {
544
- const field = ChronoField.byName(fieldName);
545
- if (field && this.fieldValues.get(field) !== undefined) { // undefined if "removed" in EnumMap
546
- if (temporal.isSupported(field)) {
547
- let temporalValue;
548
- try {
549
- temporalValue = temporal.getLong(field);
550
- } catch (ex) {
551
- if (ex instanceof DateTimeException) {
552
- continue;
553
- }
554
- throw ex;
575
+ for (const field of this.fieldValues.keys()) {
576
+ if (temporal.isSupported(field)) {
577
+ let temporalValue;
578
+ try {
579
+ temporalValue = temporal.getLong(field);
580
+ } catch (ex) {
581
+ if (ex instanceof DateTimeException) {
582
+ continue;
555
583
  }
556
- const value = this.fieldValues.get(field);
557
- if (temporalValue !== value) {
558
- throw new DateTimeException(`Cross check failed: ${field} ${temporalValue} vs ${field} ${value}`);
559
- }
560
- this.fieldValues.remove(field);
584
+ throw ex;
585
+ }
586
+ const value = this.fieldValues.get(field);
587
+ if (temporalValue !== value) {
588
+ throw new DateTimeException(`Cross check failed: ${field} ${temporalValue} vs ${field} ${value}`);
561
589
  }
590
+ this.fieldValues.remove(field);
562
591
  }
563
592
  }
564
593
  }
@@ -4,6 +4,12 @@
4
4
  */
5
5
 
6
6
  /**
7
+ * A map from {@link TemporalField} to value.
8
+ *
9
+ * Fields are matched by identity, like the `HashMap` used by java.time. Entries are bucketed
10
+ * by `field.name()`, but fields with the same name are distinct keys: for example the
11
+ * `WeekBasedYear` field of `@js-joda/locale` WeekFields is not `IsoFields.WEEK_BASED_YEAR`.
12
+ *
7
13
  * @private
8
14
  */
9
15
  export class EnumMap {
@@ -11,20 +17,49 @@ export class EnumMap {
11
17
  this._map = {};
12
18
  }
13
19
 
20
+ /**
21
+ * @param {TemporalField} key
22
+ * @return {?Array<{key: TemporalField, value: *}>} the entries with the name of key, or undefined
23
+ * @private
24
+ */
25
+ _bucket(key){
26
+ const name = key.name();
27
+ return Object.prototype.hasOwnProperty.call(this._map, name) ? this._map[name] : undefined;
28
+ }
29
+
30
+ /**
31
+ * @param {TemporalField} key
32
+ * @return {?{key: TemporalField, value: *}} the entry of key, or undefined
33
+ * @private
34
+ */
35
+ _entry(key){
36
+ const bucket = this._bucket(key);
37
+ if (bucket != null) {
38
+ for (let i = 0; i < bucket.length; i++) {
39
+ if (bucket[i].key === key) {
40
+ return bucket[i];
41
+ }
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+
14
47
  putAll(otherMap){
15
- for(const key in otherMap._map){
16
- this._map[key] = otherMap._map[key];
48
+ const keys = otherMap.keys();
49
+ for (let i = 0; i < keys.length; i++) {
50
+ this.set(keys[i], otherMap.get(keys[i]));
17
51
  }
18
52
  return this;
19
53
  }
20
54
 
21
55
  containsKey(key){
22
- // eslint-disable-next-line no-prototype-builtins
23
- return (this._map.hasOwnProperty(key.name())) && (this.get(key) !== undefined);
56
+ const entry = this._entry(key);
57
+ return entry != null && entry.value !== undefined;
24
58
  }
25
59
 
26
60
  get(key) {
27
- return this._map[key.name()];
61
+ const entry = this._entry(key);
62
+ return entry != null ? entry.value : undefined;
28
63
  }
29
64
 
30
65
  put(key, val) {
@@ -32,39 +67,74 @@ export class EnumMap {
32
67
  }
33
68
 
34
69
  set(key, val) {
35
- this._map[key.name()] = val;
70
+ const entry = this._entry(key);
71
+ if (entry != null) {
72
+ entry.value = val;
73
+ } else {
74
+ const name = key.name();
75
+ // eslint-disable-next-line no-prototype-builtins
76
+ if (this._map.hasOwnProperty(name) === false) {
77
+ this._map[name] = [];
78
+ }
79
+ this._map[name].push({ key, value: val });
80
+ }
36
81
  return this;
37
82
  }
38
83
 
39
84
  retainAll(keyList){
40
- const map = {};
41
- for(let i=0; i<keyList.length; i++){
42
- const key = keyList[i].name();
43
- map[key] = this._map[key];
85
+ const retained = new EnumMap();
86
+ for (let i = 0; i < keyList.length; i++) {
87
+ if (this.containsKey(keyList[i])) {
88
+ retained.set(keyList[i], this.get(keyList[i]));
89
+ }
44
90
  }
45
- this._map = map;
91
+ this._map = retained._map;
46
92
  return this;
47
93
  }
48
94
 
49
- /**
50
- * due to the bad performance of delete we just set the key entry to undefined.
51
- *
52
- * this might lead to issues with "null" entries. Calling clear in the end might solve the issue
53
- * @param key
54
- * @returns {*}
55
- */
56
95
  remove(key){
57
- const keyName = key.name();
58
- const val = this._map[keyName];
59
- this._map[keyName] = undefined;
60
- return val;
96
+ const bucket = this._bucket(key);
97
+ if (bucket != null) {
98
+ for (let i = 0; i < bucket.length; i++) {
99
+ if (bucket[i].key === key) {
100
+ const val = bucket[i].value;
101
+ bucket.splice(i, 1);
102
+ if (bucket.length === 0) {
103
+ delete this._map[key.name()];
104
+ }
105
+ return val;
106
+ }
107
+ }
108
+ }
109
+ return undefined;
61
110
  }
62
111
 
63
- keySet(){
64
- return this._map;
112
+ /**
113
+ * Returns the fields that have a value, a snapshot that is not changed by later updates.
114
+ *
115
+ * @return {TemporalField[]}
116
+ */
117
+ keys(){
118
+ const keys = [];
119
+ for (const name in this._map) {
120
+ // eslint-disable-next-line no-prototype-builtins
121
+ if (this._map.hasOwnProperty(name)) {
122
+ const bucket = this._map[name];
123
+ for (let i = 0; i < bucket.length; i++) {
124
+ if (bucket[i].value !== undefined) {
125
+ keys.push(bucket[i].key);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return keys;
65
131
  }
66
132
 
67
133
  clear(){
68
134
  this._map = {};
69
135
  }
136
+
137
+ toString(){
138
+ return `{${this.keys().map((key) => `${key}=${this.get(key)}`).join(', ')}}`;
139
+ }
70
140
  }
@@ -7,6 +7,29 @@ import { ZoneRules } from './ZoneRules';
7
7
  import { ZoneOffset } from '../ZoneOffset';
8
8
  import { DateTimeException } from '../errors';
9
9
 
10
+ // the range of a Date in epoch millis (ECMA-262), a Date outside of it is invalid
11
+ const MAX_DATE_EPOCH_MILLI = 8640000000000000;
12
+
13
+ // the timezone offset of a Date in minutes, as Date.getTimezoneOffset(); an epoch milli outside
14
+ // the range of a Date is clamped to it, so that far instants get the offset at the end of the range
15
+ function timezoneOffsetInMinutes(epochMilli) {
16
+ const clamped = Math.max(-MAX_DATE_EPOCH_MILLI, Math.min(MAX_DATE_EPOCH_MILLI, epochMilli));
17
+ return new Date(clamped).getTimezoneOffset();
18
+ }
19
+
20
+ // the epoch milli of the instant, clamped to the range of a Date; Instant.toEpochMilli() would
21
+ // overflow for instants like Instant.MAX
22
+ function clampedEpochMilli(instant) {
23
+ const epochSecond = instant.epochSecond();
24
+ if (epochSecond > MAX_DATE_EPOCH_MILLI / 1000) {
25
+ return MAX_DATE_EPOCH_MILLI;
26
+ }
27
+ if (epochSecond < -MAX_DATE_EPOCH_MILLI / 1000) {
28
+ return -MAX_DATE_EPOCH_MILLI;
29
+ }
30
+ return instant.toEpochMilli();
31
+ }
32
+
10
33
  export class SystemDefaultZoneRules extends ZoneRules {
11
34
 
12
35
  isFixedOffset(){
@@ -19,7 +42,7 @@ export class SystemDefaultZoneRules extends ZoneRules {
19
42
  * @returns {ZoneOffset}
20
43
  */
21
44
  offsetOfInstant(instant){
22
- const offsetInMinutes = new Date(instant.toEpochMilli()).getTimezoneOffset();
45
+ const offsetInMinutes = timezoneOffsetInMinutes(clampedEpochMilli(instant));
23
46
  return ZoneOffset.ofTotalMinutes(offsetInMinutes * -1);
24
47
  }
25
48
 
@@ -29,7 +52,7 @@ export class SystemDefaultZoneRules extends ZoneRules {
29
52
  * @returns {ZoneOffset}
30
53
  */
31
54
  offsetOfEpochMilli(epochMilli){
32
- const offsetInMinutes = new Date(epochMilli).getTimezoneOffset();
55
+ const offsetInMinutes = timezoneOffsetInMinutes(epochMilli);
33
56
  return ZoneOffset.ofTotalMinutes(offsetInMinutes * -1);
34
57
  }
35
58
 
@@ -48,9 +71,9 @@ export class SystemDefaultZoneRules extends ZoneRules {
48
71
  */
49
72
  offsetOfLocalDateTime(localDateTime){
50
73
  const epochMilli = localDateTime.toEpochSecond(ZoneOffset.UTC) * 1000;
51
- const offsetInMinutesBeforePossibleTransition = new Date(epochMilli).getTimezoneOffset();
74
+ const offsetInMinutesBeforePossibleTransition = timezoneOffsetInMinutes(epochMilli);
52
75
  const epochMilliSystemZone = epochMilli + offsetInMinutesBeforePossibleTransition * 60000;
53
- const offsetInMinutesAfterPossibleTransition = new Date(epochMilliSystemZone).getTimezoneOffset();
76
+ const offsetInMinutesAfterPossibleTransition = timezoneOffsetInMinutes(epochMilliSystemZone);
54
77
  return ZoneOffset.ofTotalMinutes(offsetInMinutesAfterPossibleTransition * -1);
55
78
  }
56
79