@js-joda/core 6.2.0 → 6.3.1

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.2.0",
3
+ "version": "6.3.1",
4
4
  "description": "a date and time library for javascript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,5 +47,5 @@
47
47
  "sourceMap": false,
48
48
  "instrument": false
49
49
  },
50
- "gitHead": "bb36ea96b26ee93408068c8ad8ebe8b094e96a10"
50
+ "gitHead": "109fb8287af88f6cbc00cf7982201e1b999aa04e"
51
51
  }
package/src/Duration.js CHANGED
@@ -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
  //-----------------------------------------------------------------------
@@ -1089,6 +1112,21 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1089
1112
  }
1090
1113
 
1091
1114
  //-----------------------------------------------------------------------
1115
+ /**
1116
+ * Extracts the number of days in the duration.
1117
+ *
1118
+ * This returns the total number of days in the duration by dividing the
1119
+ * number of seconds by 86400.
1120
+ * This is based on the standard definition of a day as 24 hours.
1121
+ *
1122
+ * This instance is immutable and unaffected by this method call.
1123
+ *
1124
+ * @return {number} the number of days in the duration, may be negative
1125
+ */
1126
+ toDaysPart() {
1127
+ return MathUtil.intDiv(this._seconds, LocalTime.SECONDS_PER_DAY);
1128
+ }
1129
+
1092
1130
  /**
1093
1131
  * Extracts the number of hours part in the duration.
1094
1132
  *
@@ -1151,6 +1189,22 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1151
1189
  return MathUtil.intDiv(this._nanos, 1000000);
1152
1190
  }
1153
1191
 
1192
+ /**
1193
+ * Get the nanoseconds part within seconds of the duration.
1194
+ *
1195
+ * The length of the duration is stored using two fields - seconds and nanoseconds.
1196
+ * The nanoseconds part is a value from 0 to 999,999,999 that is an adjustment to
1197
+ * the length in seconds.
1198
+ * The total duration is defined by calling {@link Duration.nano} and {@link Duration.seconds}.
1199
+ *
1200
+ * This instance is immutable and unaffected by this method call.
1201
+ *
1202
+ * @return {number} the nanoseconds within the second part of the length of the duration, from 0 to 999,999,999
1203
+ */
1204
+ toNanosPart() {
1205
+ return this._nanos;
1206
+ }
1207
+
1154
1208
  //-----------------------------------------------------------------------
1155
1209
  /**
1156
1210
  * Compares this duration to the specified {@link Duration}.
@@ -1277,6 +1331,74 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1277
1331
 
1278
1332
  }
1279
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
+
1280
1402
  export function _init() {
1281
1403
  /**
1282
1404
  * Constant for a duration of zero.
package/src/Instant.js CHANGED
@@ -916,6 +916,12 @@ export class Instant extends Temporal {
916
916
  * @throws ArithmeticException if numeric overflow occurs
917
917
  */
918
918
  toEpochMilli() {
919
+ if (this._seconds < 0 && this._nanos > 0) {
920
+ // avoid an overflow of seconds * 1000 for instants close to the minimum epoch milli
921
+ const millis = MathUtil.safeMultiply(this._seconds + 1, 1000);
922
+ const adjustment = MathUtil.intDiv(this._nanos, NANOS_PER_MILLI) - 1000;
923
+ return MathUtil.safeAdd(millis, adjustment);
924
+ }
919
925
  const millis = MathUtil.safeMultiply(this._seconds, 1000);
920
926
  return millis + MathUtil.intDiv(this._nanos, NANOS_PER_MILLI);
921
927
  }
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/MonthDay.js CHANGED
@@ -25,7 +25,7 @@ import { ZoneId } from './ZoneId';
25
25
  * A month-day in the ISO-8601 calendar system, such as `--12-03`.
26
26
  *
27
27
  * {@link MonthDay} is an immutable date-time object that represents the combination
28
- * of a year and month. Any field that can be derived from a month and day, such as
28
+ * of a month and day. Any field that can be derived from a month and day, such as
29
29
  * quarter-of-year, can be obtained.
30
30
  *
31
31
  * This class does not store or represent a year, time or time-zone.
@@ -319,7 +319,7 @@ export class ZonedDateTime extends ChronoZonedDateTime {
319
319
  *
320
320
  * Converting an instant to a zoned date-time is simple as there is only one valid
321
321
  * offset for each instant. If the valid offset is different to the offset specified,
322
- * the the date-time and offset of the zoned date-time will differ from those specified.
322
+ * the date-time and offset of the zoned date-time will differ from those specified.
323
323
  *
324
324
  * If the {@link ZoneId} to be used is a {@link ZoneOffset}, this method is equivalent
325
325
  * to {@link of}.
@@ -13,15 +13,17 @@ 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
 
21
+ import { Instant } from '../Instant';
21
22
  import { LocalTime } from '../LocalTime';
22
23
  import { LocalDate } from '../LocalDate';
23
24
  import { Period } from '../Period';
24
25
 
26
+ import { ZonedDateTime } from '../ZonedDateTime';
25
27
  import { ZoneOffset } from '../ZoneOffset';
26
28
 
27
29
  /**
@@ -146,46 +148,92 @@ export class DateTimeBuilder extends TemporalAccessor {
146
148
  this.fieldValues.retainAll(resolverFields);
147
149
  }
148
150
  // handle standard fields
149
- // this._mergeInstantFields();
150
- if (this._resolveFields(resolverStyle)) {
151
- // this._mergeInstantFields();
152
- }
151
+ this._mergeInstantFields();
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
- //this._crossCheck();
161
+ this._crossCheck();
157
162
  if (this.excessDays != null && this.excessDays.isZero() === false && this.date != null && this.time != null) {
158
163
  this.date = this.date.plus(this.excessDays);
159
164
  this.excessDays = Period.ZERO;
160
165
  }
161
- //resolveFractional();
166
+ this._resolveFractional();
162
167
  this._resolveInstant();
163
168
  return this;
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
  }
@@ -470,6 +516,105 @@ export class DateTimeBuilder extends TemporalAccessor {
470
516
  }
471
517
  }
472
518
 
519
+ /**
520
+ * Converts a parsed instant-seconds into a date and a second-of-day, using the parsed zone
521
+ * or, if there is none, the parsed offset. Without a zone or offset, the instant-seconds
522
+ * are left unchanged.
523
+ *
524
+ * @private
525
+ */
526
+ _mergeInstantFields() {
527
+ if (this.fieldValues.containsKey(ChronoField.INSTANT_SECONDS)) {
528
+ if (this.zone != null) {
529
+ this._mergeInstantFields0(this.zone);
530
+ } else {
531
+ const offsetSecs = this.fieldValues.get(ChronoField.OFFSET_SECONDS);
532
+ if (offsetSecs != null) {
533
+ this._mergeInstantFields0(ZoneOffset.ofTotalSeconds(offsetSecs));
534
+ }
535
+ }
536
+ }
537
+ }
538
+
539
+ /**
540
+ * @param {ZoneId} selectedZone
541
+ * @private
542
+ */
543
+ _mergeInstantFields0(selectedZone) {
544
+ const instant = Instant.ofEpochSecond(this.fieldValues.remove(ChronoField.INSTANT_SECONDS));
545
+ const zdt = ZonedDateTime.ofInstant(instant, selectedZone);
546
+ if (this.date == null) {
547
+ this._addObject(zdt.toLocalDate());
548
+ } else {
549
+ this._resolveMakeChanges(ChronoField.INSTANT_SECONDS, zdt.toLocalDate());
550
+ }
551
+ this._addFieldValue(ChronoField.SECOND_OF_DAY, zdt.toLocalTime().toSecondOfDay());
552
+ }
553
+
554
+ /**
555
+ * Checks the remaining field values against the resolved date and time, and removes the
556
+ * ones that match.
557
+ *
558
+ * @private
559
+ */
560
+ _crossCheck() {
561
+ if (this.date != null && this.time != null) {
562
+ this._crossCheck0(this.date.atTime(this.time));
563
+ } else if (this.date != null) {
564
+ this._crossCheck0(this.date);
565
+ } else if (this.time != null) {
566
+ this._crossCheck0(this.time);
567
+ }
568
+ }
569
+
570
+ /**
571
+ * @param {TemporalAccessor} temporal
572
+ * @private
573
+ */
574
+ _crossCheck0(temporal) {
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;
583
+ }
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}`);
589
+ }
590
+ this.fieldValues.remove(field);
591
+ }
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Without a time, adds the fraction-of-second fields to a parsed instant-seconds,
597
+ * second-of-day or second-of-minute, with zero if no nano-of-second was parsed.
598
+ *
599
+ * @private
600
+ */
601
+ _resolveFractional() {
602
+ if (this.time == null &&
603
+ (this.fieldValues.containsKey(ChronoField.INSTANT_SECONDS) ||
604
+ this.fieldValues.containsKey(ChronoField.SECOND_OF_DAY) ||
605
+ this.fieldValues.containsKey(ChronoField.SECOND_OF_MINUTE))) {
606
+ if (this.fieldValues.containsKey(ChronoField.NANO_OF_SECOND)) {
607
+ const nos = this.fieldValues.get(ChronoField.NANO_OF_SECOND);
608
+ this.fieldValues.put(ChronoField.MICRO_OF_SECOND, MathUtil.intDiv(nos, 1000));
609
+ this.fieldValues.put(ChronoField.MILLI_OF_SECOND, MathUtil.intDiv(nos, 1000000));
610
+ } else {
611
+ this.fieldValues.put(ChronoField.NANO_OF_SECOND, 0);
612
+ this.fieldValues.put(ChronoField.MICRO_OF_SECOND, 0);
613
+ this.fieldValues.put(ChronoField.MILLI_OF_SECOND, 0);
614
+ }
615
+ }
616
+ }
617
+
473
618
  _resolveInstant() {
474
619
  if (this.date != null && this.time != null) {
475
620
  const offsetSecs = this.fieldValues.get(ChronoField.OFFSET_SECONDS);
@@ -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
  }