Skip to content

Commit

Permalink
Fix bugs caused by flooring negative numbers
Browse files Browse the repository at this point in the history
Fixed bugs when working with negative numbers caused by using `Math.floor` (`-1.1` → `-2`) instead of `Math.trunc` (`-1.1` → `-1`). Most of the conversion functions (i.e. `hoursToMinutes`) got affected when passing some negative fractional input. Also, some other functions that could be possibly affected by unfortunate timezone/date combinations were fixed. The functions that were affected: `format`, `parse`, `getUnixTime`, `daysToWeeks`, `hoursToMilliseconds`, `hoursToMinutes`, `hoursToSeconds`, `milliseconds`, `minutesToMilliseconds`, `millisecondsToMinutes`, `monthsToYears`, `millisecondsToHours`, `millisecondsToSeconds`, `minutesToHours`, `minutesToSeconds`, `yearsToQuarters`, `yearsToMonths`, `yearsToDays`, `weeksToDays`, `secondsToMinutes`, `secondsToHours`, `quartersToYears`, `quartersToMonths` and `monthsToQuarters`.
  • Loading branch information
kossnocorp committed Jan 20, 2024
1 parent 19ed910 commit c0aa54c
Show file tree
Hide file tree
Showing 71 changed files with 263 additions and 95 deletions.
6 changes: 3 additions & 3 deletions src/_lib/format/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ export const formatters: { [token: string]: Formatter } = {

// Seconds timestamp
t: function (date, token, _localize) {
const timestamp = Math.floor(date.getTime() / 1000);
const timestamp = Math.trunc(date.getTime() / 1000);
return addLeadingZeros(timestamp, token.length);
},

Expand All @@ -770,7 +770,7 @@ export const formatters: { [token: string]: Formatter } = {
function formatTimezoneShort(offset: number, delimiter: string = ""): string {
const sign = offset > 0 ? "-" : "+";
const absOffset = Math.abs(offset);
const hours = Math.floor(absOffset / 60);
const hours = Math.trunc(absOffset / 60);
const minutes = absOffset % 60;
if (minutes === 0) {
return sign + String(hours);
Expand All @@ -792,7 +792,7 @@ function formatTimezoneWithOptionalMinutes(
function formatTimezone(offset: number, delimiter: string = ""): string {
const sign = offset > 0 ? "-" : "+";
const absOffset = Math.abs(offset);
const hours = addLeadingZeros(Math.floor(absOffset / 60), 2);
const hours = addLeadingZeros(Math.trunc(absOffset / 60), 2);
const minutes = addLeadingZeros(absOffset % 60, 2);
return sign + hours + delimiter + minutes;
}
2 changes: 1 addition & 1 deletion src/_lib/format/lightFormatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const lightFormatters = {
S(date: Date, token: string): string {
const numberOfDigits = token.length;
const milliseconds = date.getMilliseconds();
const fractionalSeconds = Math.floor(
const fractionalSeconds = Math.trunc(
milliseconds * Math.pow(10, numberOfDigits - 3),
);
return addLeadingZeros(fractionalSeconds, token.length);
Expand Down
2 changes: 1 addition & 1 deletion src/_lib/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function generateOffset(originalDate: Date) {

if (tzOffset !== 0) {
const absoluteOffset = Math.abs(tzOffset);
const hourOffset = addLeadingZeros(Math.floor(absoluteOffset / 60), 2);
const hourOffset = addLeadingZeros(Math.trunc(absoluteOffset / 60), 2);
const minuteOffset = addLeadingZeros(absoluteOffset % 60, 2);
// If less than 0, the sign is +, because it is ahead of time.
const sign = tzOffset < 0 ? "+" : "-";
Expand Down
4 changes: 2 additions & 2 deletions src/daysToWeeks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { daysInWeek } from "../constants/index.js";
* //=> 2
*
* @example
* // It uses floor rounding:
* // It uses trunc rounding:
* const result = daysToWeeks(13)
* //=> 1
*/
export function daysToWeeks(days: number): number {
const weeks = days / daysInWeek;
return Math.floor(weeks);
return Math.trunc(weeks);
}
19 changes: 18 additions & 1 deletion src/daysToWeeks/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe("daysToWeeks", () => {
assert(daysToWeeks(14) === 2);
});

it("uses floor rounding", () => {
it("uses trunc rounding", () => {
assert(daysToWeeks(8) === 1);
assert(daysToWeeks(6) === 0);
});
Expand All @@ -19,4 +19,21 @@ describe("daysToWeeks", () => {
assert(daysToWeeks(7.5) === 1);
assert(daysToWeeks(0) === 0);
});

it("properly works with negative numbers", () => {
assert(daysToWeeks(7) === 1);
assert(daysToWeeks(-7) === -1);

assert(daysToWeeks(14) === 2);
assert(daysToWeeks(-14) === -2);

assert(daysToWeeks(8) === 1);
assert(daysToWeeks(-8) === -1);

assert(daysToWeeks(6) === 0);
assert(daysToWeeks(-6) === 0);

assert(daysToWeeks(7.5) === 1);
assert(daysToWeeks(-7.5) === -1);
});
});
7 changes: 3 additions & 4 deletions src/differenceInCalendarDays/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ export function differenceInCalendarDays<DateType extends Date>(
const startOfDayRight = startOfDay(dateRight);

const timestampLeft =
startOfDayLeft.getTime() - getTimezoneOffsetInMilliseconds(startOfDayLeft);
+startOfDayLeft - getTimezoneOffsetInMilliseconds(startOfDayLeft);
const timestampRight =
startOfDayRight.getTime() -
getTimezoneOffsetInMilliseconds(startOfDayRight);
+startOfDayRight - getTimezoneOffsetInMilliseconds(startOfDayRight);

// Round the number of days to the nearest integer
// because the number of milliseconds in a day is not constant
// (e.g. it's different in the day of the daylight saving time clock shift)
return Math.round((timestampLeft - timestampRight) / millisecondsInDay);
return Math.trunc((timestampLeft - timestampRight) / millisecondsInDay);
}
10 changes: 9 additions & 1 deletion src/differenceInCalendarDays/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env mocha */

import assert from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { differenceInCalendarDays } from "./index.js";
import { getDstTransitions } from "../../test/dst/tzOffsetTransitions.js";

Expand Down Expand Up @@ -76,6 +76,14 @@ describe("differenceInCalendarDays", () => {
const resultIsNegative = isNegativeZero(result);
assert(resultIsNegative === false);
});

it("properly works with negative numbers", () => {
const a = new Date(2014, 6 /* Jul */, 1);
const b = new Date(2014, 6 /* Jul */, 2, 1);

expect(differenceInCalendarDays(b, a)).toBe(1);
expect(differenceInCalendarDays(a, b)).toBe(-1);
});
});

it("returns NaN if the first date is `Invalid Date`", () => {
Expand Down
8 changes: 3 additions & 5 deletions src/differenceInCalendarISOWeeks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,12 @@ export function differenceInCalendarISOWeeks<DateType extends Date>(
const startOfISOWeekRight = startOfISOWeek(dateRight);

const timestampLeft =
startOfISOWeekLeft.getTime() -
getTimezoneOffsetInMilliseconds(startOfISOWeekLeft);
+startOfISOWeekLeft - getTimezoneOffsetInMilliseconds(startOfISOWeekLeft);
const timestampRight =
startOfISOWeekRight.getTime() -
getTimezoneOffsetInMilliseconds(startOfISOWeekRight);
+startOfISOWeekRight - getTimezoneOffsetInMilliseconds(startOfISOWeekRight);

// Round the number of days to the nearest integer
// because the number of milliseconds in a week is not constant
// (e.g. it's different in the week of the daylight saving time clock shift)
return Math.round((timestampLeft - timestampRight) / millisecondsInWeek);
return Math.trunc((timestampLeft - timestampRight) / millisecondsInWeek);
}
9 changes: 8 additions & 1 deletion src/differenceInCalendarISOWeeks/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env mocha */

import assert from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { differenceInCalendarISOWeeks } from "./index.js";

describe("differenceInCalendarISOWeeks", () => {
Expand Down Expand Up @@ -75,6 +75,13 @@ describe("differenceInCalendarISOWeeks", () => {
const resultIsNegative = isNegativeZero(result);
assert(resultIsNegative === false);
});

it("properly works with negative numbers", () => {
const a = new Date(2014, 6 /* Jul */, 9);
const b = new Date(2014, 6 /* Jul */, 19);
expect(differenceInCalendarISOWeeks(b, a)).toBe(1);
expect(differenceInCalendarISOWeeks(a, b)).toBe(-1);
});
});

it("returns NaN if the first date is `Invalid Date`", () => {
Expand Down
8 changes: 3 additions & 5 deletions src/differenceInCalendarWeeks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ export function differenceInCalendarWeeks<DateType extends Date>(
const startOfWeekRight = startOfWeek(dateRight, options);

const timestampLeft =
startOfWeekLeft.getTime() -
getTimezoneOffsetInMilliseconds(startOfWeekLeft);
+startOfWeekLeft - getTimezoneOffsetInMilliseconds(startOfWeekLeft);
const timestampRight =
startOfWeekRight.getTime() -
getTimezoneOffsetInMilliseconds(startOfWeekRight);
+startOfWeekRight - getTimezoneOffsetInMilliseconds(startOfWeekRight);

// Round the number of days to the nearest integer
// because the number of milliseconds in a week is not constant
// (e.g. it's different in the week of the daylight saving time clock shift)
return Math.round((timestampLeft - timestampRight) / millisecondsInWeek);
return Math.trunc((timestampLeft - timestampRight) / millisecondsInWeek);
}
9 changes: 8 additions & 1 deletion src/differenceInCalendarWeeks/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env mocha */

import assert from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { differenceInCalendarWeeks } from "./index.js";

describe("differenceInCalendarWeeks", () => {
Expand Down Expand Up @@ -122,6 +122,13 @@ describe("differenceInCalendarWeeks", () => {
const resultIsNegative = isNegativeZero(result);
assert(resultIsNegative === false);
});

it("properly works with negative numbers", () => {
const a = new Date(2014, 6 /* Jul */, 9);
const b = new Date(2014, 6 /* Jul */, 19);
expect(differenceInCalendarWeeks(b, a)).toBe(1);
expect(differenceInCalendarWeeks(a, b)).toBe(-1);
});
});

it("returns NaN if the first date is `Invalid Date`", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/differenceInDays/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { toDate } from "../toDate/index.js";
* or more than 24 hours if a daylight savings change happens between two dates.
*
* To ignore DST and only measure exact 24-hour periods, use this instead:
* `Math.floor(differenceInHours(dateLeft, dateRight)/24)|0`.
* `Math.trunc(differenceInHours(dateLeft, dateRight)/24)|0`.
*
* @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
*
Expand Down
2 changes: 1 addition & 1 deletion src/differenceInWeeks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface DifferenceInWeeksOptions extends RoundingOptions {}
* or more than 7*24 hours if a daylight savings change happens between two dates.
*
* To ignore DST and only measure exact 7*24-hour periods, use this instead:
* `Math.floor(differenceInHours(dateLeft, dateRight)/(7*24))|0`.
* `Math.trunc(differenceInHours(dateLeft, dateRight)/(7*24))|0`.
*
* @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
*
Expand Down
9 changes: 7 additions & 2 deletions src/format/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("format", () => {

const offset = date.getTimezoneOffset();
const absoluteOffset = Math.abs(offset);
const hours = Math.floor(absoluteOffset / 60);
const hours = Math.trunc(absoluteOffset / 60);
const hoursLeadingZero = hours < 10 ? "0" : "";
const minutes = absoluteOffset % 60;
const minutesLeadingZero = minutes < 10 ? "0" : "";
Expand All @@ -43,7 +43,7 @@ describe("format", () => {
const timezoneGMT = "GMT" + timezone;

const timestamp = date.getTime().toString();
const secondsTimestamp = Math.floor(date.getTime() / 1000).toString();
const secondsTimestamp = Math.trunc(date.getTime() / 1000).toString();

it("accepts a timestamp", () => {
const date = new Date(2014, 3, 4).getTime();
Expand Down Expand Up @@ -666,6 +666,11 @@ describe("format", () => {
const result = format(date, "T");
assert(result === timestamp);
});

it("seconds timestamp handles negative numbers", () => {
expect(format(new Date(1001), "t")).toBe("1");
expect(format(new Date(-1001), "t")).toBe("-1");
});
});

describe("long format", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/formatDistance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function formatDistance<DateType extends Date>(
// 1 year up to max Date
} else {
const monthsSinceStartOfYear = months % 12;
const years = Math.floor(months / 12);
const years = Math.trunc(months / 12);

// N years up to 1 years 3 months
if (monthsSinceStartOfYear < 3) {
Expand Down
2 changes: 1 addition & 1 deletion src/formatISO/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function formatISO<DateType extends Date>(

if (offset !== 0) {
const absoluteOffset = Math.abs(offset);
const hourOffset = addLeadingZeros(Math.floor(absoluteOffset / 60), 2);
const hourOffset = addLeadingZeros(Math.trunc(absoluteOffset / 60), 2);
const minuteOffset = addLeadingZeros(absoluteOffset % 60, 2);
// If less than 0, the sign is +, because it is ahead of time.
const sign = offset < 0 ? "+" : "-";
Expand Down
2 changes: 1 addition & 1 deletion src/formatRFC3339/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function formatRFC3339<DateType extends Date>(
let fractionalSecond = "";
if (fractionDigits > 0) {
const milliseconds = _date.getMilliseconds();
const fractionalSeconds = Math.floor(
const fractionalSeconds = Math.trunc(
milliseconds * Math.pow(10, fractionDigits - 3),
);
fractionalSecond = "." + addLeadingZeros(fractionalSeconds, fractionDigits);
Expand Down
5 changes: 2 additions & 3 deletions src/getISOWeek/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ export function getISOWeek<DateType extends Date>(
date: DateType | number | string,
): number {
const _date = toDate(date);
const diff =
startOfISOWeek(_date).getTime() - startOfISOWeekYear(_date).getTime();
const diff = +startOfISOWeek(_date) - +startOfISOWeekYear(_date);

// Round the number of days to the nearest integer
// because the number of milliseconds in a week is not constant
// (e.g. it's different in the week of the daylight saving time clock shift)
return Math.round(diff / millisecondsInWeek) + 1;
return Math.trunc(diff / millisecondsInWeek) + 1;
}
7 changes: 6 additions & 1 deletion src/getISOWeek/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env mocha */

import assert from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { getISOWeek } from "./index.js";

describe("getISOWeek", () => {
Expand Down Expand Up @@ -35,6 +35,11 @@ describe("getISOWeek", () => {
const result = getISOWeek(new Date(2016, 4 /* May */, 31));
assert(result === 22);
});

it("properly works with negative numbers", () => {
expect(getISOWeek(new Date(2014, 6 /* Jul */, 14))).toBe(29);
expect(getISOWeek(new Date(-2014, 6 /* Jul */, 14))).toBe(29);
});
});

it("handles dates before 100 AD", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/getISOWeeksInYear/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export function getISOWeeksInYear<DateType extends Date>(
): number {
const thisYear = startOfISOWeekYear(date);
const nextYear = startOfISOWeekYear(addWeeks(thisYear, 60));
const diff = nextYear.valueOf() - thisYear.valueOf();
const diff = +nextYear - +thisYear;
// Round the number of weeks to the nearest integer
// because the number of milliseconds in a week is not constant
// (e.g. it's different in the week of the daylight saving time clock shift)
return Math.round(diff / millisecondsInWeek);
return Math.trunc(diff / millisecondsInWeek);
}
11 changes: 10 additions & 1 deletion src/getISOWeeksInYear/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env mocha */

import assert from "assert";
import { describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import { getISOWeeksInYear } from "./index.js";

describe("getISOWeeksInYear", () => {
Expand All @@ -28,4 +28,13 @@ describe("getISOWeeksInYear", () => {
const result = getISOWeeksInYear(new Date(NaN));
assert(isNaN(result));
});

it("properly works with negative numbers", () => {
expect(getISOWeeksInYear(new Date(2015, 1 /* Feb */, 11))).toBe(53);
// The number must differ. The Gregorian calendar repeats every 400 years,
// so the number of ISO weeks as well. -2015 corresponds to 385 AD where
// there was 52 ISO weeks.
expect(getISOWeeksInYear(new Date(-2015, 1 /* Feb */, 11))).toBe(52);
expect(getISOWeeksInYear(new Date(385, 1 /* Feb */, 11))).toBe(52);
});
});
2 changes: 1 addition & 1 deletion src/getQuarter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export function getQuarter<DateType extends Date>(
date: DateType | number | string,
): number {
const _date = toDate(date);
const quarter = Math.floor(_date.getMonth() / 3) + 1;
const quarter = Math.trunc(_date.getMonth() / 3) + 1;
return quarter;
}
4 changes: 2 additions & 2 deletions src/getUnixTime/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getTime } from "../getTime/index.js";
import { toDate } from "../index.js";

/**
* @name getUnixTime
Expand All @@ -22,5 +22,5 @@ import { getTime } from "../getTime/index.js";
export function getUnixTime<DateType extends Date>(
date: DateType | number | string,
): number {
return Math.floor(getTime(date) / 1000);
return Math.trunc(+toDate(date) / 1000);
}
Loading

0 comments on commit c0aa54c

Please sign in to comment.