Skip to content

Commit e434a47

Browse files
authored
Add support for parsing partial end dates in ISO intervals (#1720)
1 parent 24fdb22 commit e434a47

File tree

4 files changed

+236
-5
lines changed

4 files changed

+236
-5
lines changed

src/datetime.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ function adjustTime(inst, dur) {
186186

187187
// helper useful in turning the results of parsing into real dates
188188
// by handling the zone options
189-
function parseDataToDateTime(parsed, parsedZone, opts, format, text, specificOffset) {
189+
export function parseDataToDateTime(parsed, parsedZone, opts, format, text, specificOffset) {
190190
const { setZone, zone } = opts;
191191
if ((parsed && Object.keys(parsed).length !== 0) || parsedZone) {
192192
const interpretationZone = parsedZone || zone,
@@ -801,7 +801,7 @@ export default class DateTime {
801801
const normalized = normalizeObject(obj, normalizeUnitWithLocalWeeks);
802802
const { minDaysInFirstWeek, startOfWeek } = usesLocalWeekValues(normalized, loc);
803803

804-
const tsNow = Settings.now(),
804+
const tsNow = opts.overrideNow ?? Settings.now(),
805805
offsetProvis = !isUndefined(opts.specificOffset)
806806
? opts.specificOffset
807807
: zoneToUse.offset(tsNow),

src/impl/regexParser.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,39 @@ export function parseISODate(s) {
294294
);
295295
}
296296

297+
// ISO Interval parsing
298+
299+
// Note: Do not optimize the outer non-capturing group, it is necessary, because the
300+
// regex is combined with other regexes and contains |
301+
const partialIsoIntervalEndDate = /(?:(?:(\d\d)-)?(\d\d)?|(?:W(\d\d)-)?(\d)|(\d{3}))/;
302+
const isoIntervalEndDateTime = combineRegexes(partialIsoIntervalEndDate, isoTimeExtensionRegex);
303+
304+
const extractPartialIsoIntervalEndDate = simpleParse(
305+
"month",
306+
"day",
307+
"weekNumber",
308+
"weekDay",
309+
"ordinal"
310+
);
311+
312+
const extractISOIntervalPartialDateAndTime = combineExtractors(
313+
extractPartialIsoIntervalEndDate,
314+
extractISOTime,
315+
extractISOOffset,
316+
extractIANAZone
317+
);
318+
319+
export function parseISOIntervalEnd(s) {
320+
return parse(
321+
s,
322+
[isoIntervalEndDateTime, extractISOIntervalPartialDateAndTime],
323+
[isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset],
324+
[isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset],
325+
[isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime],
326+
[isoTimeCombinedRegex, extractISOTimeAndOffset]
327+
);
328+
}
329+
297330
export function parseRFC2822Date(s) {
298331
return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]);
299332
}

src/interval.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import DateTime, { friendlyDateTime } from "./datetime.js";
1+
import DateTime, { friendlyDateTime, parseDataToDateTime } from "./datetime.js";
22
import Duration from "./duration.js";
33
import Settings from "./settings.js";
44
import { InvalidArgumentError, InvalidIntervalError } from "./errors.js";
55
import Invalid from "./impl/invalid.js";
66
import Formatter from "./impl/formatter.js";
77
import * as Formats from "./impl/formats.js";
8+
import { parseISOIntervalEnd } from "./impl/regexParser.js";
89

910
const INVALID = "Invalid Interval";
1011

@@ -134,24 +135,42 @@ export default class Interval {
134135
* @return {Interval}
135136
*/
136137
static fromISO(text, opts) {
138+
const { zone, setZone, ...restOpts } = opts || {};
137139
const [s, e] = (text || "").split("/", 2);
138140
if (s && e) {
139141
let start, startIsValid;
140142
try {
141-
start = DateTime.fromISO(s, opts);
143+
// we need to know the zone that was used in the string, so that we can
144+
// default to it when parsing end, therefor use setZone: true
145+
start = DateTime.fromISO(s, { ...restOpts, zone, setZone: true });
142146
startIsValid = start.isValid;
143147
} catch (e) {
144148
startIsValid = false;
145149
}
146150

147151
let end, endIsValid;
148152
try {
149-
end = DateTime.fromISO(e, opts);
153+
const [vals, parsedZone] = parseISOIntervalEnd(e);
154+
const endParseOpts = {
155+
...restOpts,
156+
overrideNow: startIsValid ? start.valueOf() : null,
157+
zone: startIsValid ? start.zone : zone,
158+
setZone: true,
159+
};
160+
end = parseDataToDateTime(vals, parsedZone, endParseOpts, "ISO 8601 Interval end", e);
150161
endIsValid = end.isValid;
151162
} catch (e) {
152163
endIsValid = false;
153164
}
154165

166+
// if we overrode the user's choice for setZone earlier, make up for it now
167+
if (startIsValid && !setZone) {
168+
start = start.setZone(zone);
169+
}
170+
if (endIsValid && !setZone) {
171+
end = end.setZone(zone);
172+
}
173+
155174
if (startIsValid && endIsValid) {
156175
return Interval.fromDateTimes(start, end);
157176
}

test/interval/parse.test.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,182 @@ test.each(badInputs)("Interval.fromISO will return invalid for [%s]", (s) => {
144144
expect(i.isValid).toBe(false);
145145
expect(i.invalidReason).toBe("unparsable");
146146
});
147+
148+
describe("Interval.fromISO defaults missing values in end to start", () => {
149+
test("Gregorian, end just time", () => {
150+
const i = Interval.fromISO("1988-04-15T09/15:30");
151+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
152+
expect(i.end.toISO()).toBe("1988-04-15T15:30:00.000-04:00");
153+
});
154+
test("Gregorian, end just time and zone", () => {
155+
const i = Interval.fromISO("1988-04-15T09/15:30-07:00");
156+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
157+
expect(i.end.toISO()).toBe("1988-04-15T18:30:00.000-04:00");
158+
});
159+
test("Gregorian, end just day", () => {
160+
const i = Interval.fromISO("1988-04-15T09/17");
161+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
162+
expect(i.end.toISO()).toBe("1988-04-17T00:00:00.000-04:00");
163+
});
164+
test("Gregorian, end day and time", () => {
165+
const i = Interval.fromISO("1988-04-15T09/17T15:30");
166+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
167+
expect(i.end.toISO()).toBe("1988-04-17T15:30:00.000-04:00");
168+
});
169+
test("Gregorian, end month and day", () => {
170+
const i = Interval.fromISO("1988-04-15T09/05-17");
171+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
172+
expect(i.end.toISO()).toBe("1988-05-17T00:00:00.000-04:00");
173+
});
174+
test("Gregorian, end month, day and time", () => {
175+
const i = Interval.fromISO("1988-04-15T09/05-17T15:30");
176+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-04:00");
177+
expect(i.end.toISO()).toBe("1988-05-17T15:30:00.000-04:00");
178+
});
179+
test("Gregorian with zone in options and partial date", () => {
180+
const i = Interval.fromISO("1988-04-15T09/19", { zone: "UTC-06:00" });
181+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-06:00");
182+
expect(i.end.toISO()).toBe("1988-04-19T00:00:00.000-06:00");
183+
});
184+
test("Gregorian with zone in options and partial date and time", () => {
185+
const i = Interval.fromISO("1988-04-15T09/19T13:00", { zone: "UTC-06:00" });
186+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-06:00");
187+
expect(i.end.toISO()).toBe("1988-04-19T13:00:00.000-06:00");
188+
});
189+
test("Gregorian with zone in options and full date and time", () => {
190+
const i = Interval.fromISO("1988-04-15T09/1989-03-01T13:00", { zone: "UTC-06:00" });
191+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-06:00");
192+
expect(i.end.toISO()).toBe("1989-03-01T13:00:00.000-06:00");
193+
});
194+
test("Gregorian with zone in options and end zone", () => {
195+
const i = Interval.fromISO("1988-04-15T09/16T15:00-07:00", { zone: "UTC-06:00" });
196+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-06:00");
197+
expect(i.end.toISO()).toBe("1988-04-16T16:00:00.000-06:00");
198+
});
199+
test("Gregorian with zone in options, setZone and end zone", () => {
200+
const i = Interval.fromISO("1988-04-15T09/16T15:00-07:00", {
201+
zone: "UTC-06:00",
202+
setZone: true,
203+
});
204+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000-06:00");
205+
expect(i.end.toISO()).toBe("1988-04-16T15:00:00.000-07:00");
206+
});
207+
test("Gregorian with start zone", () => {
208+
const i = Interval.fromISO("1988-04-15T09:00:00+01:00/17T15:30");
209+
expect(i.start.toISO()).toBe("1988-04-15T04:00:00.000-04:00");
210+
expect(i.end.toISO()).toBe("1988-04-17T10:30:00.000-04:00");
211+
});
212+
test("Gregorian with start zone and zone in options", () => {
213+
const i = Interval.fromISO("1988-04-15T09:00:00+01:00/15T15:00", { zone: "UTC-06:00" });
214+
expect(i.start.toISO()).toBe("1988-04-15T02:00:00.000-06:00");
215+
expect(i.end.toISO()).toBe("1988-04-15T08:00:00.000-06:00");
216+
});
217+
test("Gregorian with start zone and setZone", () => {
218+
const i = Interval.fromISO("1988-04-15T09:00:00+01:00/15T15:00", { setZone: true });
219+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000+01:00");
220+
expect(i.end.toISO()).toBe("1988-04-15T15:00:00.000+01:00");
221+
});
222+
test("Gregorian with two zones", () => {
223+
const i = Interval.fromISO("1988-04-15T09:00:00+01:00/15T16:00+02:00");
224+
expect(i.start.toISO()).toBe("1988-04-15T04:00:00.000-04:00");
225+
expect(i.end.toISO()).toBe("1988-04-15T10:00:00.000-04:00");
226+
});
227+
test("Gregorian with two zones and setZone", () => {
228+
const i = Interval.fromISO("1988-04-15T09:00:00+01:00/15T16:00+02:00", { setZone: true });
229+
expect(i.start.toISO()).toBe("1988-04-15T09:00:00.000+01:00");
230+
expect(i.end.toISO()).toBe("1988-04-15T16:00:00.000+02:00");
231+
});
232+
233+
// Week dates
234+
test("Week, end just time", () => {
235+
const i = Interval.fromISO("2025-W20-1T09/15:30");
236+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
237+
expect(i.end.toISO()).toBe("2025-05-12T15:30:00.000-04:00");
238+
});
239+
test("Week, end just time and zone", () => {
240+
const i = Interval.fromISO("2025-W20-1T09/15:30-07:00");
241+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
242+
expect(i.end.toISO()).toBe("2025-05-12T18:30:00.000-04:00");
243+
});
244+
test("Week, end week day", () => {
245+
const i = Interval.fromISO("2025-W20-1T09/3T15:30");
246+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
247+
expect(i.end.toISO()).toBe("2025-05-14T15:30:00.000-04:00");
248+
});
249+
test("Week, end week number and week day", () => {
250+
const i = Interval.fromISO("2025-W20-1T09/W21-3T15:30");
251+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
252+
expect(i.end.toISO()).toBe("2025-05-21T15:30:00.000-04:00");
253+
});
254+
255+
// Ordinal dates
256+
test("Ordinal, end just time", () => {
257+
const i = Interval.fromISO("2025-132T09/15:30");
258+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
259+
expect(i.end.toISO()).toBe("2025-05-12T15:30:00.000-04:00");
260+
});
261+
test("Ordinal, end just time and zone", () => {
262+
const i = Interval.fromISO("2025-132T09/15:30-07:00");
263+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
264+
expect(i.end.toISO()).toBe("2025-05-12T18:30:00.000-04:00");
265+
});
266+
test("Ordinal, end with ordinal", () => {
267+
const i = Interval.fromISO("2025-132T09/135T15:30");
268+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
269+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
270+
});
271+
272+
// Mixed
273+
test("Gregorian, end just weekday", () => {
274+
const i = Interval.fromISO("2025-05-12T09/4T15:30");
275+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
276+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
277+
});
278+
test("Gregorian, end weekNumber and weekday", () => {
279+
const i = Interval.fromISO("2025-05-12T09/W21-1T15:30");
280+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
281+
expect(i.end.toISO()).toBe("2025-05-19T15:30:00.000-04:00");
282+
});
283+
test("Gregorian, end just ordinal", () => {
284+
const i = Interval.fromISO("2025-05-12T09/135T15:30");
285+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
286+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
287+
});
288+
289+
test("Week date, end just day", () => {
290+
const i = Interval.fromISO("2025-W20-1T09/15T15:30");
291+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
292+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
293+
});
294+
test("Week date, end month and day", () => {
295+
const i = Interval.fromISO("2025-W20-1T09/06-15T15:30");
296+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
297+
expect(i.end.toISO()).toBe("2025-06-15T15:30:00.000-04:00");
298+
});
299+
test("Week date, end just ordinal", () => {
300+
const i = Interval.fromISO("2025-W20-1T09/135T15:30");
301+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
302+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
303+
});
304+
305+
test("Ordinal, end just day", () => {
306+
const i = Interval.fromISO("2025-132T09/15T15:30");
307+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
308+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
309+
});
310+
test("Ordinal, end month and day", () => {
311+
const i = Interval.fromISO("2025-132T09/06-15T15:30");
312+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
313+
expect(i.end.toISO()).toBe("2025-06-15T15:30:00.000-04:00");
314+
});
315+
test("Ordinal, end just weekday", () => {
316+
const i = Interval.fromISO("2025-132T09/4T15:30");
317+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
318+
expect(i.end.toISO()).toBe("2025-05-15T15:30:00.000-04:00");
319+
});
320+
test("Ordinal, end weekNumber and weekday", () => {
321+
const i = Interval.fromISO("2025-132T09/W21-1T15:30");
322+
expect(i.start.toISO()).toBe("2025-05-12T09:00:00.000-04:00");
323+
expect(i.end.toISO()).toBe("2025-05-19T15:30:00.000-04:00");
324+
});
325+
});

0 commit comments

Comments
 (0)