iamkun / iamkun/dayjs

is Diff calculation wrong?

Open
#2,322 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
48.7k
Forks
2.5k
PR merge metrics
No merged PRs in 30d

Description

**Describe the bug**

I am using moment and I found a very weird behaviour (created issue also here https://github.com/moment/moment/issues/6135). When trying dayjs (to potentially replace moment by), the exact same numbers come out. Is the Diff implementation exactly the same as moment's?

Diff returning different values from really edge scenario:
- only seems to happen every other month when the 'current' date is the next day, and 'createdAt' is a month with 30 days or less.

**To Reproduce**
```
import moment from 'moment';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);

describe('Moment issue', () => {
afterEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
});

beforeEach(() => {
jest.useFakeTimers();
});

test('success - should return 13', () => {
expect.assertions(4);

const createdAt = moment.utc(new Date('2022-04-30T23:58:05.489Z'));
const createdAt2 = dayjs.utc(createdAt.toDate());
expect(createdAt.toDate()).toStrictEqual(createdAt2.toDate());

jest.setSystemTime(new Date('2023-05-30T23:59:45.489Z'));

const current = moment.utc();
const current2 = dayjs.utc(current.toDate());

expect(current.toDate()).toStrictEqual(current2.toDate());

const days = current.diff(createdAt, 'month');
const days2 = current2.diff(createdAt2, 'month');
expect(days).toBe(days2);

// outputs 13.00003733572282 precisely
expect(days).toBe(13);
});

test('failure - should return 13 but returns 12', () => {
expect.assertions(4);

const createdAt = moment.utc(new Date('2022-04-30T23:58:05.489Z'));
const createdAt2 = dayjs.utc(createdAt.toDate());

expect(createdAt.toDate()).toStrictEqual(createdAt2.toDate());

jest.setSystemTime(new Date('2023-05-31T00:01:45.489Z'));

const current = moment.utc();
const current2 = dayjs.utc(current.toDate());
expect(current.toDate()).toStrictEqual(current2.toDate());

const days = current.diff(createdAt, 'month');
const days2 = current2.diff(createdAt2, 'month');

// should not match; meaning it has the exact same issue as moment
expect(days).not.toBe(days2);

// outputs 12 (12.967824074074073 precisely) rather than 13
expect(days).toBe(13);
});

test('success (showing it doesnt happen with months with more than 30 days) - should return 10', () => {
expect.assertions(4);

const createdAt = moment.utc(new Date('2022-05-31T23:58:05.489Z'));
const createdAt2 = dayjs.utc(createdAt.toDate());
expect(createdAt.toDate()).toStrictEqual(createdAt2.toDate());

jest.setSystemTime(new Date('2023-03-31T23:59:45.489Z'));

const current = moment.utc();
const current2 = dayjs.utc(current.toDate());
expect(current.toDate()).toStrictEqual(current2.toDate());

const days = current.diff(createdAt, 'month');
const days2 = current2.diff(createdAt2, 'month');
expect(days).toBe(days2);

// outputs 10.00003733572282 precisely
expect(days).toBe(10);
});

test('success (showing it doesnt happen with months with more than 30 days) - should return 10', () => {
expect.assertions(4);

const createdAt = moment.utc(new Date('2022-05-31T23:58:05.489Z'));
const createdAt2 = dayjs.utc(createdAt.toDate());
expect(createdAt.toDate()).toStrictEqual(createdAt2.toDate());

// changed to the next day (1 min later)
jest.setSystemTime(new Date('2023-04-01T00:01:45.489Z'));

const current = moment.utc();
const current2 = dayjs.utc(current.toDate());
expect(current.toDate()).toStrictEqual(current2.toDate());

const days = current.diff(createdAt, 'month');
const days2 = current2.diff(createdAt2, 'month');
expect(days).toBe(days2);

// outputs 10.00008487654321 precisely
expect(days).toBe(10);
});
});
```

**Expected behavior**
The first 2 tests should return 13 months on both occasions. The behaviour shouldn't change every other month.
The 3rd and 4th tests show that it doesn't happen when the 'createdAt' is a month with 31 days.

**Moment-specific environment**

* moment@2.29.4
* typescript@4.9.5
* jest@29.5.0
* dayjs@1.11.7

Please run the following code in your environment and include the output:
1st (success) test:
```
console.log((new Date()).toString())
> Wed May 31 2023 01:59:45 GMT+0200 (Central European Summer Time)
console.log((new Date()).toLocaleString())
> 5/31/2023, 1:59:45 AM
console.log((new Date()).getTimezoneOffset())
> -120
console.log(navigator.userAgent)
> not applicable
console.log(moment.version)
> 2.29.4
```

2nd (failure) test:
```
console.log((new Date()).toString())
> Wed May 31 2023 02:01:45 GMT+0200 (Central European Summer Time)
console.log((new Date()).toLocaleString())
> 5/31/2023, 2:01:45 AM
console.log((new Date()).getTimezoneOffset())
> -120
console.log(navigator.userAgent)
> not applicable
console.log(moment.version)
> 2.29.4
```

**Additional context**
And some extra tests to show it only happens every other month (with moment only, but can be adapted like the ones above to also use dayjs):
```
// extra tests showing this behaviour only happens every other month

test('failure (september) - should return 6 but returns 5', () => {
expect.assertions(1);

// september (30 days month)
const createdAt = moment.utc(new Date('2022-09-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');

// outputs 5 instead of 6 (5.967801672640382 with precision)
expect(days).toBe(6);
});

test('success (August) - should return 7', () => {
expect.assertions(1);

// august (30 days month)
const createdAt = moment.utc(new Date('2022-08-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');
// outputs 7.000059737156511 with precision
expect(days).toBe(7);
});

test('success (July) - should return 8', () => {
expect.assertions(1);

// july (31 days month)
const createdAt = moment.utc(new Date('2022-07-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');
// outputs 8.000059737156512 with precision
expect(days).toBe(8);
});

test('failure (June) - should return 9 but returns 8', () => {
expect.assertions(1);

// june (30 days month)
const createdAt = moment.utc(new Date('2022-06-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');

// outputs 8 instead of 9 (8.967801672640382 with precision)
expect(days).toBe(9);
});

test('success (May) - should return 10', () => {
expect.assertions(1);

// may (31 days month)
const createdAt = moment.utc(new Date('2022-05-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');
// outputs 0.000059737156512 with precision
expect(days).toBe(10);
});

test('failure (April) - should return 11 but outputs 10', () => {
expect.assertions(1);

// april (30 days month)
const createdAt = moment.utc(new Date('2022-04-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');

// outputs 10 instead of 11 (10.967801672640382 with precision)
expect(days).toBe(11);
});

test('success (March) - should return 12', () => {
expect.assertions(1);

// march (31 days month)
const createdAt = moment.utc(new Date('2022-03-30T23:59:05.489Z'));

jest.setSystemTime(new Date('2023-03-31T00:01:45.489Z'));

const current = moment.utc();

const days = current.diff(createdAt, 'month');
// outputs 12.000059737156512 with precision
expect(days).toBe(12);
});
```

**Screenshots**
![image](https://github.com/moment/moment/assets/3720818/a8a27934-664f-42d0-a93e-23cfcdd59e3f)

Contributor guide

Open the contributing guide

Research direction

Start by running the provided Jest reproduction for month diff, including the April, June, September, and May cases. Trace the date-difference entry point used by diff(..., 'month') and compare the failing results with the expected month counts. Done means the supplied edge cases pass without regressing the successful cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.