diff --git a/CHANGES.md b/CHANGES.md index 698829a9cbd..f430332fe68 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +### 1.125 - 2025-01-02 + +##### Fixes :wrench: + +- Fixed JulianDate to always generate valid ISO strings for fractional milliseconds [#12345](https://github.com/CesiumGS/cesium/pull/12345) + ### 1.124 - 2024-12-02 #### @cesium/engine diff --git a/packages/engine/Source/Core/JulianDate.js b/packages/engine/Source/Core/JulianDate.js index 092e4683e5a..ec9bfe09da8 100644 --- a/packages/engine/Source/Core/JulianDate.js +++ b/packages/engine/Source/Core/JulianDate.js @@ -10,7 +10,7 @@ import TimeStandard from "./TimeStandard.js"; const gregorianDateScratch = new GregorianDate(); const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; -const daysInLeapFeburary = 29; +const daysInLeapFebruary = 29; function compareLeapSecondDates(leapSecond, dateToFind) { return JulianDate.compare(leapSecond.julianDate, dateToFind.julianDate); @@ -430,7 +430,7 @@ JulianDate.fromIso8601 = function (iso8601String, result) { month > 12 || day < 1 || ((month !== 2 || !inLeapYear) && day > daysInMonth[month - 1]) || - (inLeapYear && month === 2 && day > daysInLeapFeburary) + (inLeapYear && month === 2 && day > daysInLeapFebruary) ) { throw new DeveloperError(iso8601ErrorMessage); } @@ -542,7 +542,7 @@ JulianDate.fromIso8601 = function (iso8601String, result) { day++; } - tmp = inLeapYear && month === 2 ? daysInLeapFeburary : daysInMonth[month - 1]; + tmp = inLeapYear && month === 2 ? daysInLeapFebruary : daysInMonth[month - 1]; while (day > tmp) { day -= tmp; month++; @@ -553,7 +553,7 @@ JulianDate.fromIso8601 = function (iso8601String, result) { } tmp = - inLeapYear && month === 2 ? daysInLeapFeburary : daysInMonth[month - 1]; + inLeapYear && month === 2 ? daysInLeapFebruary : daysInMonth[month - 1]; } //If UTC offset is at the beginning/end of the day, minutes can be negative. @@ -575,7 +575,7 @@ JulianDate.fromIso8601 = function (iso8601String, result) { } tmp = - inLeapYear && month === 2 ? daysInLeapFeburary : daysInMonth[month - 1]; + inLeapYear && month === 2 ? daysInLeapFebruary : daysInMonth[month - 1]; day += tmp; } @@ -784,8 +784,15 @@ JulianDate.toIso8601 = function (julianDate, precision) { let millisecondStr; if (!defined(precision) && millisecond !== 0) { - //Forces milliseconds into a number with at least 3 digits to whatever the default toString() precision is. - millisecondStr = (millisecond * 0.01).toString().replace(".", ""); + // Forces milliseconds into a number with at least 3 digits. + const millisecondHundreds = millisecond * 0.01; + // Below 1e-6, toString returns scientific notation, so it should be replaced by toFixed with appropriate number of digits. + // 20 digits is a trade-off choice guided by JavaScript's Number representation accuracy (15-17 decimal digits for most numbers). + // Using toFixed(20) ensures capturing enough precision while avoiding inaccuracies due to floating-point limitations. + millisecondStr = + millisecondHundreds < 1e-6 + ? millisecondHundreds.toFixed(20).replace(".", "").replace(/0+$/, "") + : millisecondHundreds.toString().replace(".", ""); return `${year.toString().padStart(4, "0")}-${month .toString() .padStart(2, "0")}-${day.toString().padStart(2, "0")}T${hour diff --git a/packages/engine/Specs/Core/JulianDateSpec.js b/packages/engine/Specs/Core/JulianDateSpec.js index 9bc22b32d88..03842a7f335 100644 --- a/packages/engine/Specs/Core/JulianDateSpec.js +++ b/packages/engine/Specs/Core/JulianDateSpec.js @@ -594,13 +594,13 @@ describe("Core/JulianDate", function () { }).toThrowDeveloperError(); }); - it("Fails to construct an ISO8601 Febuary date with more than 28 days", function () { + it("Fails to construct an ISO8601 February date with more than 28 days", function () { expect(function () { return JulianDate.fromIso8601("2009-02-29"); }).toThrowDeveloperError(); }); - it("Fails to construct an ISO8601 Febuary leap year date with more than 29 days", function () { + it("Fails to construct an ISO8601 February leap year date with more than 29 days", function () { expect(function () { return JulianDate.fromIso8601("2000-02-30"); }).toThrowDeveloperError(); @@ -877,6 +877,38 @@ describe("Core/JulianDate", function () { expect(date).toEqual("0950-01-02T03:04:05.0123450Z"); }); + it("toIso8601 works with very small milliseconds", function () { + let expectedDate, date; + + expectedDate = new JulianDate(2450630, 1e-3); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-4); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-6); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-7); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-8); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-10); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-15); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-18); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + expectedDate = new JulianDate(2450630, 1e-21); + date = JulianDate.fromIso8601(JulianDate.toIso8601(expectedDate)); + expect(date).toEqual(expectedDate); + }); + it("can format Iso8601.MINIMUM_VALUE and MAXIMUM_VALUE to ISO strings", function () { const minString = Iso8601.MINIMUM_VALUE.toString(); expect(minString).toEqual("0000-01-01T00:00:00Z");