Closed XDIJK closed 3 years ago
Interesting. I'd need to dig in pretty far to see why that is. Before I do: what time zone is your computer in (this may or may not matter, and I haven't tried to repro yet, but you are doing the minus before converting to UTC, so it could plausibly a factor)
Interesting. I'd need to dig in pretty far to see why that is. Before I do: what time zone is your computer in (this may or may not matter, and I haven't tried to repro yet, but you are doing the minus before converting UTC, so it could plausibly a factor)
Hi thanks for responding so quickly, My computer is in the Europe/Berlin time zone.
I've also just tried converting to UTC before subtracting or adding and the result is the same. running the same test case.
//1
let end = reference.toUTC().plus(dur);
//2
let end = reference.toUTC().minus(dur);
I took a look. First, note that in your update you're not really doing it all in UTC because you're still passing the local time into the fromDateTimes
.
The real problem is that you're diffing a local time and a UTC time. Luxon's day diffing logic doesn't quite handle that, which is no surprise: it's hard to even define the behavior to be expected then, since days start and end at different times in different zones.
Here's a simplified version of what's happening:
const { DateTime, Duration, Interval } = require("luxon");
let dur = Duration.fromMillis(172800000); // 2 days in millis
let local = DateTime.fromMillis(1583019045*1000); //29th feb 2020 23:30:45
let units = ["days","hours", "minutes", "second", "millisecond"]
console.log("base\t\t", local.toISO());
let localPlus = local.plus(dur);
let localPlusRezoned = localPlus.toUTC();
console.log("plus\t\t", localPlus.toISO());
console.log("plus rezoned\t", localPlusRezoned.toISO());
console.log("local-local\t", localPlus.diff(local, units).toISO());
console.log("utc-utc\t\t", localPlusRezoned.diff(local.toUTC(), units).toISO());
console.log("local-utc\t", localPlusRezoned.diff(local, units).toISO());
❯ TZ=Europe/Berlin node luxon_654.js
base 2020-03-01T00:30:45.000+01:00
plus 2020-03-03T00:30:45.000+01:00
plus rezoned 2020-03-02T23:30:45.000Z
local-local P2D
utc-utc P2D
local-utc P1DT24H
I think probably Luxon should rezone the second argument of the diff to match the zone of the first one. I'd accept a PR doing that.
Thanks for getting back to me, the resulting date object being in my Local timezone makes sense now you've mentioned it. I'll have a look at Luxons code and see if I can contribute in a meaningful amount of time.
I now have run into issue where a period of 1 month (P1M) is returned over a span of multiple durations. Where I am really expecting P(X)D
formatFuture(): reference date of 2020-01-29 duration of 31 days in the future (2020-02-29) Expected: P1M1D Result: P1M1D
reference date of 2020-01-29 duration of 31 days in the future (2020-02-29) Expected: P1M Result: P1M
reference date of 2020-01-30 duration of 30 days in the future (2020-02-29) Expected: P30D Result: P1M
reference date of 2020-01-31 duration of 29 days in the future (2020-02-29) Expected: P29D Result: P1M
reference date of 2020-02-01 duration of 28 days in the future (2020-02-29) Expected: P28D Result: P28D
formatPast(): Reference date of 2020-02-29 and a duration of 32 days (2020-01-28) Expected:P1M1D Result: P1M1D
Reference date of 2020-02-29 and a duration of 31 days (2020-01-29) Expected: P1M Result: P1M
Reference date of 2020-02-29 and a duration of 30 days (2020-01-30) Expected: P30D Result: P1M
Reference date of 2020-02-29 and a duration of 29 days (2020-01-31) Expected: P29D Result: P1M
Reference date of 2020-02-29 and a duration of 28 days (2020-02-01) Expected: P28D Result: P28D
code:
function formatFuture(durationMillis:number, referenceDateMillis:number, units?:any[]): string {
let duration = Duration.fromMillis(durationMillis);
let referenceLocal = DateTime.fromMillis(referenceDateMillis);
let referenceUTC = referenceLocal.toUTC();
let referencePlusUTC = referenceUTC.plus(duration).toUTC();
let durationBetween = referencePlusUTC.diff(referenceUTC, units).toISO();
return durationBetween.toString();
}
function formatPast(durationMillis:number, referenceDateMillis:number, units?:any[]): string {
let duration = Duration.fromMillis(durationMillis);
let referenceLocal = DateTime.fromMillis(referenceDateMillis);
let referenceUTC = referenceLocal.toUTC();
let referenceMinusUTC = referenceUTC.minus(duration).toUTC();
let durationBetween = referenceUTC.diff(referenceMinusUTC, units).toISO();
return durationBetween.toString();
}
let millisOfP1D= 86400000;
let millisOfP27D = 2332800000;
let millisOfP28D = 2419200000;
let millisOfP29D= 2505600000;
let millisOfP30D= 2592000000;
let millisOfP31D= 2678400000;
let millisOfP32D= 2764800000;
let UNITS_UP_TO_YEAR = ["years","months","days", "hours", "minutes", "second", "millisecond"];
let feb29ReferenceDate = 1583019045000;
let jan29ReferenceDate= 1580340645000;
formatFuture(millisOfP32D, jan29ReferenceDate-millisOfP1D, UNITS_UP_TO_YEAR);
formatFuture(millisOfP31D, jan29ReferenceDate, UNITS_UP_TO_YEAR);
formatFuture(millisOfP30D, jan29ReferenceDate+millisOfP1D, UNITS_UP_TO_YEAR);
formatFuture(millisOfP29D, jan29ReferenceDate+(millisOfP1D * 2), UNITS_UP_TO_YEAR);
formatFuture(millisOfP28D, jan29ReferenceDate+(millisOfP1D * 3), UNITS_UP_TO_YEAR);
formatPast(millisOfP32D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
formatPast(millisOfP31D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
formatPast(millisOfP30D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
formatPast(millisOfP29D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
formatPast(millisOfP28D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
formatPast(millisOfP27D, feb29ReferenceDate, UNITS_UP_TO_YEAR);
Yes, that's expected. Month/day math works in calendar terms; milliseconds work in time terms. See the docs here. You are providing milliseconds and then diffing explicitly in variable-length units.
If you don't want Luxon to use variable length units (or to compare the ends of months), you can just diff in milliseconds and then convert to months, iow, intentionally do this
I believe we are misunderstanding each other. (Probably me misunderstanding you mostly 😅 )
I am attempting to do calendar math in this case. I am creating a DateTime object from a millisecond timestamp, which results in a calendar date. Then I add a duration in milliseconds to this DateTime object which results in another calendar date.
Then I want to get the period between these two calendar dates.
2020-02-29 to 2020-01-29 is in calendar terms is 1 Month in the past. 2020-02-29 to 2020-01-28 is 1 month and 1 day in the past. 2020-02-29 to 2020-01-27 is 1 month and 2 days in the past.
I understand that calculating how many milliseconds are in months or days is subject to lossy conversion (Time math). I don't see where I am doing time math except for the creation of the two initial dateTime objects
For some context this is my practical use case.
I am polling a system for uptime this system sends me its timestamp in milliseconds (Reference date) when it started. And it sends me in milliseconds how long it has been online(duration). I want to know what that duration is in the ISO8601 standard.
Taking the plain millisecond duration and converting it into months or days will give me for example: 0.9xxxx years/days/months which is what I am trying to avoid. I am solely interested in the human understandable and readable format.
OK, got it.
Those specific expectations are matched by Luxon:
DateTime.fromISO("2020-02-29").diff(DateTime.fromISO("2020-01-29"), ["months", "days"]).toObject()
//=> {months: 1, days: 0}
DateTime.fromISO("2020-02-29").diff(DateTime.fromISO("2020-01-28"), ["months", "days"]).toObject()
//=> {months: 1, days: 1}
DateTime.fromISO("2020-02-29").diff(DateTime.fromISO("2020-01-27"), ["months", "days"]).toObject()
//=> {months: 1, days: 2}
What isn't matched is this one:
DateTime.fromISO("2020-02-29").diff(DateTime.fromISO("2020-01-30"), ["months", "days"]).toObject()
//=> {months: 1, days: 0}
Month math is funny for the part of the month that doesn't exist in the target month. Obviously there's no truly right answer to how to handle that. You could certainly argue that it should be 29 days.
But the way Luxon thinks about it, adding and subtracting months follows the convention that if the month creates a date that is outside the bounds of a month (e.g. Feb 30), then it moves it to the last day of the month. Then diff
asks, "how much would I need to add to get to the target date". IOW, diff
gives 1 month in this example because of this:
DateTime.fromISO("2020-01-30").plus({ months: 1 }).toISO() //=> "2020-02-29T00:00:00.000-05:00"
That kind of has to be the result of that addition; any other answer would put it in a different month. That Luxon makes diff
match that is just a choice; we could have equally made it match minus
:
DateTime.fromISO("2020-02-29").minus({ months: 1 }).toISO()
"2020-01-29T00:00:00.000-05:00"
which is I think roughly what you were expecting in that case since that would resulted in 29 days for the diff, since at Jan 30, it wouldn't be a full month away. Addition and subtraction here are just inherently asymmetrical because the months are different lengths, so the operations aren't reversable. If Luxon were coded the other way, my guess is that you'd just have a different set of expectations violated.
I hope that helps clear it up. I can't reasonably change this behavior and while it's a roughly arbitrary choice, I do prefer it.
This library would be great as an extension for Luxon https://moment.github.io/luxon/
This library has awesome formatting and Luxon has 100% compatibility for real calendar based durations.
Hi,
I've written two functions which calculates gets the period between a duration in milliseconds of how long the duration was and a reference date in milliseconds.
1.When the duration is added to the reference day to a new variable(B) and the interval between reference and B is calculated and converted to a duration.toISO().
I get a different result from when:
The result as in total time between the two is correct however the formatting is off. 1 returns P2D & 2 returns P1DT24H
TestCase