Skip to content

Commit 781d962

Browse files
authored
fix: avoid exponential notation in Duration#toISO (#1784)
1 parent d249e0e commit 781d962

2 files changed

Lines changed: 36 additions & 7 deletions

File tree

src/duration.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ function removeZeroes(vals) {
209209
return newVals;
210210
}
211211

212+
// Render a number for toISO(). JS prints very small magnitudes in exponential
213+
// notation (e.g. `1e-7`), which is not valid ISO 8601 and which fromISO() cannot
214+
// parse, so expand those to a plain decimal. (toFixed keeps exponential notation
215+
// for magnitudes >= 1e21, so very large durations are left untouched here.)
216+
function toISONumber(value) {
217+
const str = `${value}`;
218+
return str.includes("e") ? value.toFixed(20).replace(/\.?0+$/, "") : str;
219+
}
220+
212221
/**
213222
* A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime.
214223
*
@@ -566,18 +575,19 @@ export default class Duration {
566575
if (!this.isValid) return null;
567576

568577
let s = "P";
569-
if (this.years !== 0) s += this.years + "Y";
570-
if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + "M";
571-
if (this.weeks !== 0) s += this.weeks + "W";
572-
if (this.days !== 0) s += this.days + "D";
578+
if (this.years !== 0) s += toISONumber(this.years) + "Y";
579+
if (this.months !== 0 || this.quarters !== 0)
580+
s += toISONumber(this.months + this.quarters * 3) + "M";
581+
if (this.weeks !== 0) s += toISONumber(this.weeks) + "W";
582+
if (this.days !== 0) s += toISONumber(this.days) + "D";
573583
if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0)
574584
s += "T";
575-
if (this.hours !== 0) s += this.hours + "H";
576-
if (this.minutes !== 0) s += this.minutes + "M";
585+
if (this.hours !== 0) s += toISONumber(this.hours) + "H";
586+
if (this.minutes !== 0) s += toISONumber(this.minutes) + "M";
577587
if (this.seconds !== 0 || this.milliseconds !== 0)
578588
// this will handle "floating point madness" by removing extra decimal places
579589
// https://stackoverflow.com/questions/588004/is-floating-point-math-broken
580-
s += roundTo(this.seconds + this.milliseconds / 1000, 3) + "S";
590+
s += toISONumber(roundTo(this.seconds + this.milliseconds / 1000, 3)) + "S";
581591
if (s === "P") s += "T0S";
582592
return s;
583593
}

test/duration/format.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ test("Duration#toISO handles mixed negative/positive numbers in seconds/millisec
7777
expect(Duration.fromObject({ seconds: -17, milliseconds: 548 }).toISO()).toBe("PT-16.452S");
7878
});
7979

80+
test("Duration#toISO does not use exponential notation for very small values", () => {
81+
// JS renders very small magnitudes in exponential notation (e.g. "1e-7"),
82+
// which is not valid ISO 8601 and which Duration.fromISO cannot parse.
83+
expect(Duration.fromObject({ years: 1e-7 }).toISO()).toBe("P0.0000001Y");
84+
expect(Duration.fromObject({ days: 1e-7 }).toISO()).toBe("P0.0000001D");
85+
expect(Duration.fromObject({ hours: 1e-7 }).toISO()).toBe("PT0.0000001H");
86+
expect(Duration.fromObject({ minutes: 1e-7 }).toISO()).toBe("PT0.0000001M");
87+
});
88+
89+
test("Duration#toISO output round-trips small fractional values through fromISO", () => {
90+
// Converting a small duration to a coarse unit yields a tiny fractional value.
91+
const dur = Duration.fromObject({ milliseconds: 1 }).shiftTo("hours");
92+
expect(dur.toISO()).not.toMatch(/e/i);
93+
expect(Duration.fromISO(dur.toISO()).isValid).toBe(true);
94+
95+
const exact = Duration.fromObject({ hours: 1e-7 });
96+
expect(Duration.fromISO(exact.toISO()).hours).toBe(1e-7);
97+
});
98+
8099
//------
81100
// #toISOTime()
82101
//------

0 commit comments

Comments
 (0)