@js-joda/core 6.0.1 → 6.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-joda/core",
3
- "version": "6.0.1",
3
+ "version": "6.2.0",
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": "a7f1bec40afb1e84ffc0ce2d88d72d690fe72f6a"
50
+ "gitHead": "bb36ea96b26ee93408068c8ad8ebe8b094e96a10"
51
51
  }
package/src/Duration.js CHANGED
@@ -893,10 +893,18 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
893
893
  if (divisor === 1) {
894
894
  return this;
895
895
  }
896
- const secs = MathUtil.intDiv(this._seconds, divisor);
897
- const secsMod = MathUtil.roundDown(((this._seconds/ divisor) - secs) * LocalTime.NANOS_PER_SECOND);
898
- let nos = MathUtil.intDiv(this._nanos, divisor);
899
- nos = secsMod + nos;
896
+ // Give both components the same sign before truncating the quotient.
897
+ // Otherwise the positive nano adjustment of a negative duration is
898
+ // truncated separately and can move the result away from zero.
899
+ let seconds = this._seconds;
900
+ let nanos = this._nanos;
901
+ if (seconds < 0 && nanos > 0) {
902
+ seconds += 1;
903
+ nanos -= LocalTime.NANOS_PER_SECOND;
904
+ }
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);
900
908
  return Duration.ofSeconds(secs, nos);
901
909
  }
902
910
 
@@ -1209,9 +1217,22 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1209
1217
  if (this === Duration.ZERO) {
1210
1218
  return 'PT0S';
1211
1219
  }
1212
- const hours = MathUtil.intDiv(this._seconds, LocalTime.SECONDS_PER_HOUR);
1213
- const minutes = MathUtil.intDiv(MathUtil.intMod(this._seconds, LocalTime.SECONDS_PER_HOUR), LocalTime.SECONDS_PER_MINUTE);
1214
- const secs = MathUtil.intMod(this._seconds, LocalTime.SECONDS_PER_MINUTE);
1220
+ // A negative duration is stored as a negative second count plus a
1221
+ // non-negative nano offset, e.g. -0.5s is held as seconds = -1,
1222
+ // nanos = 500000000. To render ISO-8601 components that all carry the
1223
+ // duration's sign, recover the signed whole-second part and the
1224
+ // magnitude of the sub-second fraction separately: when the duration is
1225
+ // negative and has a fractional offset, one second of that offset
1226
+ // belongs to the whole part, and the remaining fraction is its
1227
+ // complement against a full second.
1228
+ const negativeFraction = this._seconds < 0 && this._nanos > 0;
1229
+ const wholeSeconds = negativeFraction ? this._seconds + 1 : this._seconds;
1230
+ const fractionNanos = negativeFraction ? LocalTime.NANOS_PER_SECOND - this._nanos : this._nanos;
1231
+
1232
+ const hours = MathUtil.intDiv(wholeSeconds, LocalTime.SECONDS_PER_HOUR);
1233
+ const minutes = MathUtil.intDiv(MathUtil.intMod(wholeSeconds, LocalTime.SECONDS_PER_HOUR), LocalTime.SECONDS_PER_MINUTE);
1234
+ const secs = MathUtil.intMod(wholeSeconds, LocalTime.SECONDS_PER_MINUTE);
1235
+
1215
1236
  let rval = 'PT';
1216
1237
  if (hours !== 0) {
1217
1238
  rval += `${hours}H`;
@@ -1219,32 +1240,28 @@ export class Duration extends TemporalAmount /*implements TemporalAmount, Compar
1219
1240
  if (minutes !== 0) {
1220
1241
  rval += `${minutes}M`;
1221
1242
  }
1222
- if (secs === 0 && this._nanos === 0 && rval.length > 2) {
1243
+ if (secs === 0 && fractionNanos === 0 && rval.length > 2) {
1223
1244
  return rval;
1224
1245
  }
1225
- if (secs < 0 && this._nanos > 0) {
1226
- if (secs === -1) {
1227
- rval += '-0';
1228
- } else {
1229
- rval += secs + 1;
1230
- }
1246
+ // The seconds field needs an explicit minus sign when the whole-second
1247
+ // part is zero but the duration is a negative fraction (e.g. -0.5s
1248
+ // prints as "-0.5S", not "0.5S").
1249
+ if (secs === 0 && negativeFraction) {
1250
+ rval += '-0';
1231
1251
  } else {
1232
- rval += secs;
1252
+ rval += `${secs}`;
1233
1253
  }
1234
- if (this._nanos > 0) {
1235
- rval += '.';
1236
- let nanoString;
1237
- if (secs < 0) {
1238
- nanoString = `${2 * LocalTime.NANOS_PER_SECOND - this._nanos}`;
1239
- } else {
1240
- nanoString = `${LocalTime.NANOS_PER_SECOND + this._nanos}`;
1254
+ if (fractionNanos > 0) {
1255
+ // fractionNanos is a magnitude in [1, 1e9); print it as a fixed
1256
+ // nine-digit fraction with any trailing zeros removed.
1257
+ let fraction = `${fractionNanos}`;
1258
+ while (fraction.length < 9) {
1259
+ fraction = `0${fraction}`;
1241
1260
  }
1242
- // remove the leading '1'
1243
- nanoString = nanoString.slice(1, nanoString.length);
1244
- rval += nanoString;
1245
- while (rval.charAt(rval.length - 1) === '0') {
1246
- rval = rval.slice(0, rval.length - 1);
1261
+ while (fraction.charAt(fraction.length - 1) === '0') {
1262
+ fraction = fraction.slice(0, fraction.length - 1);
1247
1263
  }
1264
+ rval += `.${fraction}`;
1248
1265
  }
1249
1266
  rval += 'S';
1250
1267
  return rval;
package/src/Instant.js CHANGED
@@ -520,7 +520,7 @@ export class Instant extends Temporal {
520
520
  throw new DateTimeException('Unit must divide into a standard day without remainder');
521
521
  }
522
522
  const nod = MathUtil.intMod(this._seconds, LocalTime.SECONDS_PER_DAY) * LocalTime.NANOS_PER_SECOND + this._nanos;
523
- const result = MathUtil.intDiv(nod, dur) * dur;
523
+ const result = MathUtil.floorDiv(nod, dur) * dur;
524
524
  return this.plusNanos(result - nod);
525
525
  }
526
526
 
@@ -823,9 +823,17 @@ export class Instant extends Temporal {
823
823
  * @private
824
824
  */
825
825
  _microsUntil(end) {
826
- const secsDiff = MathUtil.safeSubtract(end.epochSecond(), this.epochSecond());
826
+ let secsDiff = MathUtil.safeSubtract(end.epochSecond(), this.epochSecond());
827
+ let nanosDiff = end.nano() - this.nano();
828
+ if (secsDiff > 0 && nanosDiff < 0) {
829
+ secsDiff--;
830
+ nanosDiff += LocalTime.NANOS_PER_SECOND;
831
+ } else if (secsDiff < 0 && nanosDiff > 0) {
832
+ secsDiff++;
833
+ nanosDiff -= LocalTime.NANOS_PER_SECOND;
834
+ }
827
835
  const totalMicros = MathUtil.safeMultiply(secsDiff, 1000000);
828
- return MathUtil.safeAdd(totalMicros, MathUtil.intDiv(end.nano() - this.nano(), 1000));
836
+ return MathUtil.safeAdd(totalMicros, MathUtil.intDiv(nanosDiff, 1000));
829
837
  }
830
838
 
831
839
  /**
@@ -14,6 +14,7 @@ import { ResolverStyle } from './ResolverStyle';
14
14
  import { IsoChronology } from '../chrono/IsoChronology';
15
15
  import { ChronoLocalDate } from '../chrono/ChronoLocalDate';
16
16
  import { ChronoField } from '../temporal/ChronoField';
17
+ import { IsoFields } from '../temporal/IsoFields';
17
18
  import { TemporalAccessor } from '../temporal/TemporalAccessor';
18
19
  import { TemporalQueries } from '../temporal/TemporalQueries';
19
20
 
@@ -146,13 +147,11 @@ export class DateTimeBuilder extends TemporalAccessor {
146
147
  }
147
148
  // handle standard fields
148
149
  // this._mergeInstantFields();
150
+ if (this._resolveFields(resolverStyle)) {
151
+ // this._mergeInstantFields();
152
+ }
149
153
  this._mergeDate(resolverStyle);
150
154
  this._mergeTime(resolverStyle);
151
- //if (resolveFields(resolverStyle)) {
152
- // mergeInstantFields();
153
- // mergeDate(resolverStyle);
154
- // mergeTime(resolverStyle);
155
- //}
156
155
  this._resolveTimeInferZeroes(resolverStyle);
157
156
  //this._crossCheck();
158
157
  if (this.excessDays != null && this.excessDays.isZero() === false && this.date != null && this.time != null) {
@@ -164,6 +163,31 @@ export class DateTimeBuilder extends TemporalAccessor {
164
163
  return this;
165
164
  }
166
165
 
166
+ /**
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
+ *
173
+ * @param {ResolverStyle} resolverStyle
174
+ * @return {boolean}
175
+ * @private
176
+ */
177
+ _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;
185
+ }
186
+ }
187
+ }
188
+ return resolved;
189
+ }
190
+
167
191
  /**
168
192
  *
169
193
  * @param {ResolverStyle} resolverStyle
@@ -21,6 +21,7 @@ import { ResolverStyle } from './ResolverStyle';
21
21
 
22
22
  import { IsoChronology } from '../chrono/IsoChronology';
23
23
  import { ChronoField } from '../temporal/ChronoField';
24
+ import { IsoFields } from '../temporal/IsoFields';
24
25
  import { createTemporalQuery } from '../temporal/TemporalQuery';
25
26
 
26
27
  /**
@@ -696,12 +697,13 @@ export function _init() {
696
697
  .toFormatter(ResolverStyle.STRICT);
697
698
 
698
699
  DateTimeFormatter.ISO_WEEK_DATE = new DateTimeFormatterBuilder()
699
- .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
700
+ .parseCaseInsensitive()
701
+ .appendValue(IsoFields.WEEK_BASED_YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
700
702
  .appendLiteral('-W')
701
- .appendValue(ChronoField.ALIGNED_WEEK_OF_YEAR)
703
+ .appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
702
704
  .appendLiteral('-')
703
- .appendValue(ChronoField.DAY_OF_WEEK)
704
- .toFormatter(ResolverStyle.STRICT);
705
+ .appendValue(ChronoField.DAY_OF_WEEK, 1)
706
+ .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);
705
707
 
706
708
  DateTimeFormatter.ISO_DATE = new DateTimeFormatterBuilder()
707
709
  .parseCaseInsensitive()
@@ -1516,7 +1516,7 @@ class InstantPrinterParser {
1516
1516
  const parser = new DateTimeFormatterBuilder()
1517
1517
  .append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral('T')
1518
1518
  .appendValue(ChronoField.HOUR_OF_DAY, 2).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).appendLiteral(':')
1519
- .appendValue(ChronoField.SECOND_OF_MINUTE, 2).appendFraction(ChronoField.NANO_OF_SECOND, minDigits, maxDigits, true).appendLiteral('Z')
1519
+ .appendValue(ChronoField.SECOND_OF_MINUTE, 2).appendFraction(ChronoField.NANO_OF_SECOND, minDigits, maxDigits, true).appendOffsetId()
1520
1520
  .toFormatter()._toPrinterParser(false);
1521
1521
  const pos = parser.parse(newContext, text, position);
1522
1522
  if (pos < 0) {
@@ -1531,6 +1531,10 @@ class InstantPrinterParser {
1531
1531
  const min = newContext.getParsed(ChronoField.MINUTE_OF_HOUR);
1532
1532
  const secVal = newContext.getParsed(ChronoField.SECOND_OF_MINUTE);
1533
1533
  const nanoVal = newContext.getParsed(ChronoField.NANO_OF_SECOND);
1534
+ // The instant is parsed with an offset id (which also accepts 'Z' as
1535
+ // zero), so resolve against the parsed offset rather than assuming UTC.
1536
+ const offsetSecs = newContext.getParsed(ChronoField.OFFSET_SECONDS);
1537
+ const offset = (offsetSecs != null ? offsetSecs : 0);
1534
1538
  let sec = (secVal != null ? secVal : 0);
1535
1539
  const nano = (nanoVal != null ? nanoVal : 0);
1536
1540
  const year = MathUtil.intMod(yearParsed, 10000);
@@ -1545,7 +1549,7 @@ class InstantPrinterParser {
1545
1549
  let instantSecs;
1546
1550
  try {
1547
1551
  const ldt = LocalDateTime.of(year, month, day, hour, min, sec, 0).plusDays(days);
1548
- instantSecs = ldt.toEpochSecond(ZoneOffset.UTC);
1552
+ instantSecs = ldt.toEpochSecond(ZoneOffset.ofTotalSeconds(offset));
1549
1553
  instantSecs += MathUtil.safeMultiply(MathUtil.intDiv(yearParsed, 10000), SECONDS_PER_10000_YEARS);
1550
1554
  } catch (ex) {
1551
1555
  return ~position;
@@ -9,6 +9,7 @@ import { assert, requireNonNull } from '../assert';
9
9
  import { DateTimeBuilder } from './DateTimeBuilder';
10
10
  import { EnumMap } from './EnumMap';
11
11
 
12
+ import { Period } from '../Period';
12
13
  import { IsoChronology } from '../chrono/IsoChronology';
13
14
  import { Temporal } from '../temporal/Temporal';
14
15
  import { TemporalQueries } from '../temporal/TemporalQueries';
@@ -244,6 +245,7 @@ class Parsed extends Temporal {
244
245
  this.zone = null;
245
246
  this.fieldValues = new EnumMap();
246
247
  this.leapSecond = false;
248
+ this.excessDays = Period.ZERO;
247
249
  this.dateTimeParseContext = dateTimeParseContext;
248
250
  }
249
251
 
@@ -71,7 +71,67 @@ export class ZoneIdPrinterParser {
71
71
  return ~position;
72
72
  }
73
73
 
74
- // handle fixed time-zone IDs
74
+ // A fixed ID (Z, UT, UTC, GMT, an offset) can also be the start of a
75
+ // longer region id such as GMT0 or Zulu, so parse both and keep the
76
+ // longest match (as the class doc above promises).
77
+ const region = this._parseRegion(text, position);
78
+ const fixedEndPos = this._parseFixedId(context, text, position);
79
+
80
+ const fixedLength = fixedEndPos >= 0 ? fixedEndPos - position : 0;
81
+ if (region != null && region.parseLength > fixedLength) {
82
+ context.setParsedZone(ZoneRegion.ofId(region.parsedZoneId));
83
+ return position + region.parseLength;
84
+ }
85
+ return fixedEndPos;
86
+ }
87
+
88
+ /**
89
+ * Finds the longest available zone region id matching text at position,
90
+ * without touching the parse context.
91
+ *
92
+ * @param {String} text
93
+ * @param {number} position
94
+ * @return {?{parsedZoneId: string, parseLength: number}}
95
+ */
96
+ _parseRegion(text, position) {
97
+ const availableZoneIds = ZoneRulesProvider.getAvailableZoneIds();
98
+ if (availableZoneIds.length === 0) {
99
+ return null;
100
+ }
101
+ if (zoneIdTree.size !== availableZoneIds.length) {
102
+ zoneIdTree = ZoneIdTree.createTreeMap(availableZoneIds);
103
+ }
104
+
105
+ const maxParseLength = text.length - position;
106
+ let treeMap = zoneIdTree.treeMap;
107
+ let parsedZoneId = null;
108
+ let parseLength = 0;
109
+ while(treeMap != null) {
110
+ const parsedSubZoneId = text.substr(position, Math.min(treeMap.length, maxParseLength));
111
+ treeMap = treeMap.get(parsedSubZoneId);
112
+ if (treeMap != null && treeMap.isLeaf) {
113
+ parsedZoneId = parsedSubZoneId;
114
+ parseLength = treeMap.length;
115
+ }
116
+ }
117
+ if (parsedZoneId == null) {
118
+ return null;
119
+ }
120
+ return { parsedZoneId, parseLength };
121
+ }
122
+
123
+ /**
124
+ * Parses a fixed zone id (an offset, a UT/UTC/GMT prefixed offset, SYSTEM
125
+ * or Z) and sets it on the context. Returns the end position, or ~position
126
+ * if no fixed id matches at position.
127
+ *
128
+ * @param {DateTimeParseContext} context
129
+ * @param {String} text
130
+ * @param {number} position
131
+ * @return {number}
132
+ */
133
+ _parseFixedId(context, text, position) {
134
+ const length = text.length;
75
135
  const nextChar = text.charAt(position);
76
136
  if (nextChar === '+' || nextChar === '-') {
77
137
  const newContext = context.copy();
@@ -80,8 +140,7 @@ export class ZoneIdPrinterParser {
80
140
  return endPos;
81
141
  }
82
142
  const offset = newContext.getParsed(ChronoField.OFFSET_SECONDS);
83
- const zone = ZoneOffset.ofTotalSeconds(offset);
84
- context.setParsedZone(zone);
143
+ context.setParsedZone(ZoneOffset.ofTotalSeconds(offset));
85
144
  return endPos;
86
145
  } else if (length >= position + 2) {
87
146
  const nextNextChar = text.charAt(position + 1);
@@ -104,35 +163,10 @@ export class ZoneIdPrinterParser {
104
163
  context.setParsedZone(ZoneId.systemDefault());
105
164
  return position + 6;
106
165
  }
107
-
108
- // ...
109
166
  if (context.charEquals(nextChar, 'Z')) {
110
167
  context.setParsedZone(ZoneOffset.UTC);
111
168
  return position + 1;
112
169
  }
113
-
114
- const availableZoneIds = ZoneRulesProvider.getAvailableZoneIds();
115
- if (zoneIdTree.size !== availableZoneIds.length) {
116
- zoneIdTree = ZoneIdTree.createTreeMap(availableZoneIds);
117
- }
118
-
119
- const maxParseLength = length - position;
120
- let treeMap = zoneIdTree.treeMap;
121
- let parsedZoneId = null;
122
- let parseLength = 0;
123
- while(treeMap != null) {
124
- const parsedSubZoneId = text.substr(position, Math.min(treeMap.length, maxParseLength));
125
- treeMap = treeMap.get(parsedSubZoneId);
126
- if (treeMap != null && treeMap.isLeaf) {
127
- parsedZoneId = parsedSubZoneId;
128
- parseLength = treeMap.length;
129
- }
130
- }
131
- if (parsedZoneId != null) {
132
- context.setParsedZone(ZoneRegion.ofId(parsedZoneId));
133
- return position + parseLength;
134
- }
135
-
136
170
  return ~position;
137
171
  }
138
172
 
@@ -110,7 +110,7 @@ declare module "js-joda" {
110
110
  toNanos(): number;
111
111
  toHoursPart(): number;
112
112
  toMinutesPart(): number;
113
- toSecondspart(): number;
113
+ toSecondsPart(): number;
114
114
  toMillisPart(): number;
115
115
  toString(): string;
116
116
  units(): any;