From 2719386de095f43dbbf8b9cc64e0556c02a07605 Mon Sep 17 00:00:00 2001 From: Justin Schroeder Date: Wed, 17 Apr 2024 10:39:26 -0400 Subject: [PATCH] chore: release 0.1.0 (#53) * feat: diff functions * differenceInMilliseconds * created the other constant difference functions, but will need to add the option 'ignoreTime' for day/week version * created the other constant difference functions * completing tests * adding differenceInX exports in index * make sure that rounding doesn't give -0 * added difference In Months & years * shortened the all difference functions to diffX moved using monthDays at diffMonths after `if (ld < rd)` * raname type DifferenceRoundingMethod -> DiffRoundingMethod * docs: adds diffs to docs * chore: bumps tests to latest actions * feat: existing "Z" token is now "ZZ" adds "Z" token style (#52) * Support for the timezone token "ZZ" and changing the timezone format style. * Fix timezone offset parsing and applyOffset function * chore: adjusts time format < full to be ZZ --------- Co-authored-by: WilcoSp <17604138+WilcoSp@users.noreply.github.com> Co-authored-by: Kouta Motegi --- .github/workflows/release.yml | 4 +- .github/workflows/tests.yml | 6 +- docs/components/content/Format.vue | 208 ++++++++++++++--------- docs/components/content/Helpers.vue | 225 +++++++++++++++++++++---- docs/examples/diffDays.ts | 5 + src/__tests__/applyOffset.spec.ts | 6 + src/__tests__/common.spec.ts | 25 +++ src/__tests__/diffDays.spec.ts | 20 +++ src/__tests__/diffHours.spec.ts | 10 ++ src/__tests__/diffMilliseconds.spec.ts | 10 ++ src/__tests__/diffMinutes.spec.ts | 19 +++ src/__tests__/diffMonths.spec.ts | 32 ++++ src/__tests__/diffSeconds.spec.ts | 10 ++ src/__tests__/diffWeeks.spec.ts | 8 + src/__tests__/diffYears.spec.ts | 48 ++++++ src/__tests__/format.spec.ts | 51 +++++- src/__tests__/formatStr.spec.ts | 8 +- src/__tests__/offset.spec.ts | 23 ++- src/__tests__/parse.spec.ts | 44 ++++- src/__tests__/parts.spec.ts | 12 ++ src/__tests__/removeOffset.spec.ts | 6 + src/__tests__/validOffset.spec.ts | 10 +- src/applyOffset.ts | 16 +- src/common.ts | 64 +++++-- src/diffDays.ts | 20 +++ src/diffHours.ts | 20 +++ src/diffMilliseconds.ts | 13 ++ src/diffMinutes.ts | 16 ++ src/diffMonths.ts | 36 ++++ src/diffRound.ts | 11 ++ src/diffSeconds.ts | 17 ++ src/diffWeeks.ts | 20 +++ src/diffYears.ts | 13 ++ src/format.ts | 6 +- src/index.ts | 8 + src/offset.ts | 7 +- src/parse.ts | 4 +- src/parts.ts | 11 +- src/removeOffset.ts | 4 +- 39 files changed, 898 insertions(+), 178 deletions(-) create mode 100644 docs/examples/diffDays.ts create mode 100644 src/__tests__/common.spec.ts create mode 100644 src/__tests__/diffDays.spec.ts create mode 100644 src/__tests__/diffHours.spec.ts create mode 100644 src/__tests__/diffMilliseconds.spec.ts create mode 100644 src/__tests__/diffMinutes.spec.ts create mode 100644 src/__tests__/diffMonths.spec.ts create mode 100644 src/__tests__/diffSeconds.spec.ts create mode 100644 src/__tests__/diffWeeks.spec.ts create mode 100644 src/__tests__/diffYears.spec.ts create mode 100644 src/diffDays.ts create mode 100644 src/diffHours.ts create mode 100644 src/diffMilliseconds.ts create mode 100644 src/diffMinutes.ts create mode 100644 src/diffMonths.ts create mode 100644 src/diffRound.ts create mode 100644 src/diffSeconds.ts create mode 100644 src/diffWeeks.ts create mode 100644 src/diffYears.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1f0760..f784f06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,11 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: lts/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 49f24f9..42c22c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,9 +4,7 @@ jobs: vitest-run: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8 + - uses: actions/checkout@v4 + - run: corepack enable - run: pnpm install - run: pnpm test diff --git a/docs/components/content/Format.vue b/docs/components/content/Format.vue index 6e396c9..a4c0816 100644 --- a/docs/components/content/Format.vue +++ b/docs/components/content/Format.vue @@ -13,20 +13,25 @@ import sizes from "../../assets/func-sizes.json" /> - +

Tempo’s format() function outputs dates in two ways:

  • @@ -83,11 +88,14 @@ import sizes from "../../assets/func-sizes.json" en - {{ format(new Date(), "full", "en") }}
    + {{ format(new Date(), "full", "en") }}
    de - {{ format(new Date(), "full", "de") }}
    + {{ format(new Date(), "full", "de") }}
    zh - {{ format(new Date(), "full", "zh") }}
    + {{ format(new Date(), "full", "zh") }}
    @@ -96,11 +104,14 @@ import sizes from "../../assets/func-sizes.json" en - {{ format(new Date(), "long", "en") }}
    + {{ format(new Date(), "long", "en") }}
    de - {{ format(new Date(), "long", "de") }}
    + {{ format(new Date(), "long", "de") }}
    zh - {{ format(new Date(), "long", "zh") }}
    + {{ format(new Date(), "long", "zh") }}
    @@ -109,11 +120,14 @@ import sizes from "../../assets/func-sizes.json" en - {{ format(new Date(), "medium", "en") }}
    + {{ format(new Date(), "medium", "en") }}
    de - {{ format(new Date(), "medium", "de") }}
    + {{ format(new Date(), "medium", "de") }}
    zh - {{ format(new Date(), "medium", "zh") }}
    + {{ format(new Date(), "medium", "zh") }}
    @@ -122,11 +136,14 @@ import sizes from "../../assets/func-sizes.json" en - {{ format(new Date(), "short", "en") }}
    + {{ format(new Date(), "short", "en") }}
    de - {{ format(new Date(), "short", "de") }}
    + {{ format(new Date(), "short", "de") }}
    zh - {{ format(new Date(), "short", "zh") }}
    + {{ format(new Date(), "short", "zh") }}
    @@ -155,15 +172,18 @@ import sizes from "../../assets/func-sizes.json" en {{ format(new Date(), { time: "full" }, "en") - }}
    + }}
    de {{ format(new Date(), { time: "full" }, "de") - }}
    + }}
    zh {{ format(new Date(), { time: "full" }, "zh") - }}
    + }}
    @@ -174,15 +194,18 @@ import sizes from "../../assets/func-sizes.json" en {{ format(new Date(), { time: "long" }, "en") - }}
    + }}
    de {{ format(new Date(), { time: "long" }, "de") - }}
    + }}
    zh {{ format(new Date(), { time: "long" }, "zh") - }}
    + }}
    @@ -193,15 +216,18 @@ import sizes from "../../assets/func-sizes.json" en {{ format(new Date(), { time: "medium" }, "en") - }}
    + }}
    de {{ format(new Date(), { time: "medium" }, "de") - }}
    + }}
    zh {{ format(new Date(), { time: "medium" }, "zh") - }}
    + }}
    @@ -212,15 +238,18 @@ import sizes from "../../assets/func-sizes.json" en {{ format(new Date(), { time: "short" }, "en") - }}
    + }}
    de {{ format(new Date(), { time: "short" }, "de") - }}
    + }}
    zh {{ format(new Date(), { time: "short" }, "zh") - }}
    + }}
    @@ -351,8 +380,13 @@ import sizes from "../../assets/func-sizes.json" Z + +08:00, +05:30, -13:45 + The timezone offset from GMT ([+-]HH:mm) + + + ZZ +0800, +0530, -1345 - The timezone offset from GMT + The timezone offset from GMT ([+-]HHmm) @@ -362,50 +396,53 @@ import sizes from "../../assets/func-sizes.json" The format() function can accept an object of options as its argument to provide more control over the output.

    - +

    Timezone

    The tz option allows you to format the provided date from the @@ -415,7 +452,10 @@ import sizes from "../../assets/func-sizes.json"

    Part filter

    The partFilter option allows you to filter out - parts + parts of the formatted date. The function is called with each "part" of the formatted date and should return a boolean indicating whether or not to include that part in final formatted string. diff --git a/docs/components/content/Helpers.vue b/docs/components/content/Helpers.vue index 7918a83..9814fbb 100644 --- a/docs/components/content/Helpers.vue +++ b/docs/components/content/Helpers.vue @@ -12,96 +12,154 @@ const fns: Record< tip?: string } > = { - sameSecond: { + diffMilliseconds: { description: - "Checks if two dates are the same second. This function is useful for comparing dates but ignoring the milliseconds.", + "Returns the number of milliseconds difference between two date objects.", + return: "number", arguments: [ { name: "dateA", - type: "Date", + type: "string | Date", }, { name: "dateB", - type: "Date", + type: "string | Date", }, ], - return: "boolean", }, - sameMinute: { + diffSeconds: { description: - "Checks if two dates are the same minute. This function is useful for comparing dates but ignoring the seconds and milliseconds.", + "Returns the number of seconds difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial seconds.", + return: "number", arguments: [ { name: "dateA", - type: "Date", + type: "string | Date", }, { name: "dateB", - type: "Date", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', }, ], - return: "boolean", }, - sameHour: { + diffMinutes: { description: - "Checks if two dates are the same hour. This function is useful for comparing dates but ignoring the minutes, seconds, and milliseconds.", + "Returns the number of minutes difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial minutes.", + return: "number", arguments: [ { name: "dateA", - type: "Date", + type: "string | Date", }, { name: "dateB", - type: "Date", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', }, ], - return: "boolean", }, - sameDay: { + diffHours: { description: - "Checks if two dates are the same day. This function is useful for comparing dates but ignoring the time.", + "Returns the number of hours difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial hours.", + return: "number", arguments: [ { name: "dateA", - type: "Date", + type: "string | Date", }, { name: "dateB", - type: "Date", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', }, ], - return: "boolean", }, - sameYear: { + diffDays: { description: - "Checks if two dates are the same year. This function is useful for comparing dates but ignoring the month, day, and time.", + "Returns the number of days difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial days.", + return: "number", arguments: [ { name: "dateA", - type: "Date", + type: "string | Date", }, { name: "dateB", - type: "Date", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', }, ], - return: "boolean", + example: "diffDays", }, - isBefore: { + diffWeeks: { description: - "Returns true if the first date is before the second date, otherwise false.", - return: "boolean", + "Returns the number of weeks difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial weeks.", + return: "number", arguments: [ { - name: "inputDate", + name: "dateA", type: "string | Date", }, { - name: "dateToCompare", + name: "dateB", type: "string | Date", }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', + }, + ], + }, + diffMonths: { + description: + "Returns the number of months difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial months.", + return: "number", + arguments: [ + { + name: "dateA", + type: "string | Date", + }, + { + name: "dateB", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', + }, + ], + }, + diffYears: { + description: + "Returns the number of years difference between two date objects. An optional third argument controls what kind of “rounding” should be used for partial years.", + return: "number", + arguments: [ + { + name: "dateA", + type: "string | Date", + }, + { + name: "dateB", + type: "string | Date", + }, + { + name: "roundingMethod", + type: '"trunc" | "round" | "floor" | "ceil"', + }, ], - example: "isBefore", }, isAfter: { description: @@ -119,6 +177,22 @@ const fns: Record< ], example: "isAfter", }, + isBefore: { + description: + "Returns true if the first date is before the second date, otherwise false.", + return: "boolean", + arguments: [ + { + name: "inputDate", + type: "string | Date", + }, + { + name: "dateToCompare", + type: "string | Date", + }, + ], + example: "isBefore", + }, isEqual: { description: "Returns true if the first date is equal to the second date, otherwise false.", @@ -135,6 +209,81 @@ const fns: Record< ], example: "isEqual", }, + sameSecond: { + description: + "Checks if two dates are the same second. This function is useful for comparing dates but ignoring the milliseconds.", + arguments: [ + { + name: "dateA", + type: "Date", + }, + { + name: "dateB", + type: "Date", + }, + ], + return: "boolean", + }, + sameMinute: { + description: + "Checks if two dates are the same minute. This function is useful for comparing dates but ignoring the seconds and milliseconds.", + arguments: [ + { + name: "dateA", + type: "Date", + }, + { + name: "dateB", + type: "Date", + }, + ], + return: "boolean", + }, + sameHour: { + description: + "Checks if two dates are the same hour. This function is useful for comparing dates but ignoring the minutes, seconds, and milliseconds.", + arguments: [ + { + name: "dateA", + type: "Date", + }, + { + name: "dateB", + type: "Date", + }, + ], + return: "boolean", + }, + sameDay: { + description: + "Checks if two dates are the same day. This function is useful for comparing dates but ignoring the time.", + arguments: [ + { + name: "dateA", + type: "Date", + }, + { + name: "dateB", + type: "Date", + }, + ], + return: "boolean", + }, + sameYear: { + description: + "Checks if two dates are the same year. This function is useful for comparing dates but ignoring the month, day, and time.", + arguments: [ + { + name: "dateA", + type: "Date", + }, + { + name: "dateB", + type: "Date", + }, + ], + return: "boolean", + }, } @@ -143,19 +292,25 @@ const fns: Record<

    Tempo includes a number of (tree-shakable) helper functions to assist you - in your date workarounds. These functions all accept either an ISO - 8601 string or a Date object and return a boolean. + in your date workarounds. These functions all accept either an ISO 8601 + string or a Date object and return a boolean.

    {{ fn }}

    - +

    diff --git a/docs/examples/diffDays.ts b/docs/examples/diffDays.ts new file mode 100644 index 0000000..a52ab6f --- /dev/null +++ b/docs/examples/diffDays.ts @@ -0,0 +1,5 @@ +import { diffDays } from "@formkit/tempo" + +diffDays("2021-07-03", "2021-01-01") +// lets round the difference +diffDays("2025-02-07T18:31:00Z", "2025-02-05T05:31:00Z", "round") diff --git a/src/__tests__/applyOffset.spec.ts b/src/__tests__/applyOffset.spec.ts index cfbd6df..853ebd5 100644 --- a/src/__tests__/applyOffset.spec.ts +++ b/src/__tests__/applyOffset.spec.ts @@ -4,12 +4,18 @@ process.env.TZ = "America/New_York" describe("applyOffset", () => { it("can apply a negative offset to a date", () => { + expect(applyOffset("2023-02-22T00:00:00Z", "-05:00").toISOString()).toBe( + "2023-02-21T19:00:00.000Z" + ) expect(applyOffset("2023-02-22T00:00:00Z", "-0500").toISOString()).toBe( "2023-02-21T19:00:00.000Z" ) }) it("can apply a positive offset to a date", () => { + expect(applyOffset("2023-04-13T10:15:00", "+02:00").toISOString()).toBe( + "2023-04-13T16:15:00.000Z" + ) expect(applyOffset("2023-04-13T10:15:00", "+0200").toISOString()).toBe( "2023-04-13T16:15:00.000Z" ) diff --git a/src/__tests__/common.spec.ts b/src/__tests__/common.spec.ts new file mode 100644 index 0000000..d8053e4 --- /dev/null +++ b/src/__tests__/common.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest" +import { getOffsetFormat } from "../common" + +describe("getOffsetFormat", () => { + it("should return 'Z' for format 'YYYY-MM-DDTHH:mm:ssZ'", () => { + expect(getOffsetFormat("YYYY-MM-DDTHH:mm:ssZ")).toBe("Z") + }) + + it("should return 'ZZ' for format 'YYYY-MM-DDTHH:mm:ssZZ'", () => { + expect(getOffsetFormat("YYYY-MM-DDTHH:mm:ssZZ")).toBe("ZZ") + }) + + it("should return 'Z' for formats 'full', 'long', 'medium', and 'short'", () => { + expect(getOffsetFormat("full")).toBe("Z") + expect(getOffsetFormat("long")).toBe("Z") + expect(getOffsetFormat("medium")).toBe("Z") + expect(getOffsetFormat("short")).toBe("Z") + }) + + it("should return 'Z' for formats { date: 'full', time: 'full' }, and { time: 'full' }", () => { + expect(getOffsetFormat({ date: "full", time: "full" })).toBe("Z") + expect(getOffsetFormat({ date: "full" })).toBe("ZZ") + expect(getOffsetFormat({ time: "full" })).toBe("Z") + }) +}) diff --git a/src/__tests__/diffDays.spec.ts b/src/__tests__/diffDays.spec.ts new file mode 100644 index 0000000..cd4dd91 --- /dev/null +++ b/src/__tests__/diffDays.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest" +import { diffDays } from "../diffDays" + +describe("differenceInDays", () => { + it("difference is 3 days", () => { + expect(diffDays("2024-04-10", "2024-04-07")).toBe(3) + }) + + it("difference is 2 days", () => { + expect( + diffDays("2024-04-10T09:50:00.000Z", "2024-04-07T15:28:00.000Z") + ).toBe(2) + }) + + it("difference is 3 days by using round", () => { + expect( + diffDays("2024-04-10T09:50:00.000Z", "2024-04-07T15:28:00.000Z", "round") + ).toBe(3) + }) +}) diff --git a/src/__tests__/diffHours.spec.ts b/src/__tests__/diffHours.spec.ts new file mode 100644 index 0000000..06788cb --- /dev/null +++ b/src/__tests__/diffHours.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest" +import { diffHours } from "../diffHours" + +describe("differenceInHours", () => { + it("difference is 5 hours", () => { + expect( + diffHours("2024-04-07T15:28:00.000Z", "2024-04-07T09:50:00.000Z") + ).toBe(5) + }) +}) diff --git a/src/__tests__/diffMilliseconds.spec.ts b/src/__tests__/diffMilliseconds.spec.ts new file mode 100644 index 0000000..da8b4d5 --- /dev/null +++ b/src/__tests__/diffMilliseconds.spec.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from "vitest" +import { diffMilliseconds } from "../diffMilliseconds" + +describe("differenceInMilliseconds", () => { + it("difference is 257 milliseconds", () => { + expect( + diffMilliseconds("2024-04-07T09:10:48.257Z", "2024-04-07T09:10:48.000Z") + ).toBe(257) + }) +}) diff --git a/src/__tests__/diffMinutes.spec.ts b/src/__tests__/diffMinutes.spec.ts new file mode 100644 index 0000000..1d8dddf --- /dev/null +++ b/src/__tests__/diffMinutes.spec.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest" +import { diffMinutes } from "../diffMinutes" + +describe("differenceInMinutes", () => { + it("difference is 18 minutes", () => { + expect( + diffMinutes("2024-04-07T09:28:30.050Z", "2024-04-07T09:10:00.000Z") + ).toBe(18) + }) + it("difference is 19 minutes by using ceil", () => { + expect( + diffMinutes( + "2024-04-07T09:28:01.050Z", + "2024-04-07T09:10:00.000Z", + "ceil" + ) + ).toBe(19) + }) +}) diff --git a/src/__tests__/diffMonths.spec.ts b/src/__tests__/diffMonths.spec.ts new file mode 100644 index 0000000..4372c16 --- /dev/null +++ b/src/__tests__/diffMonths.spec.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest" +import { diffMonths } from "../diffMonths" + +describe("differenceInMonths", () => { + it("should give 11 months", () => { + expect(diffMonths("2025-04-13", "2024-04-14")).toBe(11) + }) + + it("should give 12 months", () => { + expect(diffMonths("2025-04-14", "2024-04-14")).toBe(12) + }) + + it("should give a negative amount when the right side is in the future", () => { + expect(diffMonths("2024-04-14", "2025-04-15")).toBe(-12) + }) + + it("should give 8 months" /* till it's xmas */, () => { + expect(diffMonths("2024-12-25", "2024-04-13")).toBe(8) + }) + + it("should give 3 full months", () => { + expect(diffMonths("2024-04-13", "2023-12-25")).toBe(3) // not yet full 4 months + }) + + it("should still be a full month even if the left one is shorter", () => { + expect(diffMonths("2024-02-29", "2024-01-31")).toBe(1) + }) + + it("should also be a negative full month when swapped", () => { + expect(diffMonths("2024-01-31", "2024-02-29")).toBe(-1) + }) +}) diff --git a/src/__tests__/diffSeconds.spec.ts b/src/__tests__/diffSeconds.spec.ts new file mode 100644 index 0000000..ab35cfe --- /dev/null +++ b/src/__tests__/diffSeconds.spec.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from "vitest" +import { diffSeconds } from "../diffSeconds" + +describe("differenceInSeconds", () => { + it("difference is 28 seconds", () => { + expect( + diffSeconds("2024-04-07T09:10:28.900Z", "2024-04-07T09:10:00.000Z") + ).toBe(28) + }) +}) diff --git a/src/__tests__/diffWeeks.spec.ts b/src/__tests__/diffWeeks.spec.ts new file mode 100644 index 0000000..8637261 --- /dev/null +++ b/src/__tests__/diffWeeks.spec.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "vitest" +import { diffWeeks } from "../diffWeeks" + +describe("differenceInWeeks", () => { + it("difference is 5 hours", () => { + expect(diffWeeks("2025-06-30", "2024-04-07")).toBe(64) + }) +}) diff --git a/src/__tests__/diffYears.spec.ts b/src/__tests__/diffYears.spec.ts new file mode 100644 index 0000000..22404aa --- /dev/null +++ b/src/__tests__/diffYears.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, suite } from "vitest" +import { diffYears } from "../diffYears" + +describe("differenceInYears", () => { + it("returns the amount of full years between dates", () => { + expect(diffYears("2025-04-20", "2024-03-31")).toBe(1) + }) + + it("returns a negative number when swapped around as the first is now smaller", () => { + expect(diffYears("2024-03-31", "2025-04-20")).toBe(-1) + }) + + // date-fns had a lot of tests for the leap day + // although because differenceInMonth/year doesn't depend on time it shouldn't be an issue + suite("leap days", () => { + it("supports right side dates that are after a leap day", () => { + expect(diffYears("2024-02-29", "2022-03-01")).toBe(1) + }) + + it("And also supports if right side date is before the leap date", () => { + expect(diffYears("2024-02-29", "2022-02-28")).toBe(2) + }) + + it("supports future dates", () => { + expect(diffYears("2024-02-29", "2026-03-10")).toBe(-2) + }) + + it("equal (leap) day give 0", () => { + expect(diffYears("2024-02-28", "2024-02-28")).toBe(0) + }) + + it("Futute (leap) dates supported", () => { + expect(diffYears("2028-02-29", "2024-02-29")).toBe(4) + }) + }) + + // some of the edge cases are also tested with leap days + suite("edge cases", () => { + it("difference is less than a year because of 1 day difference", () => { + // NL kings day + expect(diffYears("2025-04-26", "2024-04-27")).toBe(0) + }) + + it("same but swapped", () => { + expect(diffYears("2024-04-27", "2025-04-26")).toBe(0) + }) + }) +}) diff --git a/src/__tests__/format.spec.ts b/src/__tests__/format.spec.ts index 76cd1d2..89c972e 100644 --- a/src/__tests__/format.spec.ts +++ b/src/__tests__/format.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest" +import { describe, expect, it } from "vitest" import { format } from "../format" import { tzDate } from "../tzDate" process.env.TZ = "America/New_York" @@ -136,9 +136,9 @@ describe("format", () => { ).toBe("2100年5月3日月曜日 4:04") }) it("can render a long time in Japanese", () => { - expect( - format("2010-06-09T04:32:00Z", { time: "full" }, "ja") - ).toBe("0時32分00秒 -0400") + expect(format("2010-06-09T04:32:00Z", { time: "full" }, "ja")).toBe( + "0時32分00秒 -04:00" + ) }) it("can format the russian month of february", () => { expect(format("2023-03-14", { date: "medium" }, "ru")).toBe( @@ -147,13 +147,16 @@ describe("format", () => { }) it("can include the timezone of a date", () => { expect(format("2023-05-05T05:30:10Z", "HH:mm:ss Z", "en")).toBe( + "01:30:10 -04:00" + ) + expect(format("2023-05-05T05:30:10Z", "HH:mm:ss ZZ", "en")).toBe( "01:30:10 -0400" ) }) it("uses offsets in full date formatting", () => { expect( format("2023-05-05T05:30:10Z", { date: "full", time: "full" }, "en") - ).toBe("Friday, May 5, 2023 at 1:30:10 AM -0400") + ).toBe("Friday, May 5, 2023 at 1:30:10 AM -04:00") }) it("can filter out the month part", () => { expect( @@ -201,6 +204,13 @@ describe("format with a timezone", () => { format: "Z", tz: "Asia/Kolkata", }) + ).toBe("+05:30") + expect( + format({ + date: "2022-10-29T11:30:50Z", + format: "ZZ", + tz: "Asia/Kolkata", + }) ).toBe("+0530") expect( format({ @@ -208,6 +218,13 @@ describe("format with a timezone", () => { format: "D hh:mm a Z", tz: "America/New_York", }) + ).toBe("20 10:15 am -05:00") + expect( + format({ + date: "2023-02-20T10:15:00", + format: "D hh:mm a ZZ", + tz: "America/New_York", + }) ).toBe("20 10:15 am -0500") expect( format({ @@ -215,6 +232,13 @@ describe("format with a timezone", () => { format: "YYYY-MM-DDTHH:mm:ssZ", tz: "Europe/Stockholm", }) + ).toBe("2024-02-16T12:00:00+01:00") + expect( + format({ + date: new Date("2024-02-16T11:00:00Z"), + format: "YYYY-MM-DDTHH:mm:ssZZ", + tz: "Europe/Stockholm", + }) ).toBe("2024-02-16T12:00:00+0100") }) @@ -225,9 +249,26 @@ describe("format with a timezone", () => { format: "HH:mm:ssZ", tz: "UTC", }) + ).toBe("02:30:00+00:00") + expect( + format({ + date: new Date("2024-03-10T02:30:00Z"), + format: "HH:mm:ssZZ", + tz: "UTC", + }) ).toBe("02:30:00+0000") }) it("can render a double character zero with leading zeros in zh (#41)", () => { expect(format("2022-04-10", "YYYY-MM", "zh")).toBe("2022-04") }) + it('can render "long" time format as the ZZ token', () => { + expect( + format({ + date: "1989-12-19T07:30:10.000Z", + format: { date: "short", time: "long" }, + locale: "en", + tz: "America/Chicago", + }) + ).toBe("12/19/89, 1:30:10 AM -0600") + }) }) diff --git a/src/__tests__/formatStr.spec.ts b/src/__tests__/formatStr.spec.ts index dd1df50..3c0eca4 100644 --- a/src/__tests__/formatStr.spec.ts +++ b/src/__tests__/formatStr.spec.ts @@ -37,7 +37,7 @@ describe("formatStr", () => { }) it("can parse en locale with long time in object format", () => { - expect(formatStr({ time: "long" }, "en")).toEqual("h:mm:ss A Z") + expect(formatStr({ time: "long" }, "en")).toEqual("h:mm:ss A ZZ") }) it("can parse en locale with medium time in object format", () => { @@ -57,4 +57,10 @@ describe("formatStr", () => { "Jan 1 at 08:14 AM" ) }) + it("uses the ZZ token for long time", () => { + expect(formatStr({ time: "long" }, "en")).toBe("h:mm:ss A ZZ") + }) + it("uses the Z token for full time", () => { + expect(formatStr({ time: "full" }, "en")).toBe("h:mm:ss A Z") + }) }) diff --git a/src/__tests__/offset.spec.ts b/src/__tests__/offset.spec.ts index 4bb187b..494200e 100644 --- a/src/__tests__/offset.spec.ts +++ b/src/__tests__/offset.spec.ts @@ -4,29 +4,38 @@ process.env.TZ = "America/New_York" describe("offset", () => { it("can determine the offset of a winter month to UTC", () => { - expect(offset("2023-02-22")).toBe("-0500") + expect(offset("2023-02-22")).toBe("-05:00") }) it("changes the offset after daylight savings", () => { - expect(offset("2023-03-12T06:59:00Z")).toBe("-0500") - expect(offset("2023-03-12T07:00:00Z")).toBe("-0400") + expect(offset("2023-03-12T06:59:00Z")).toBe("-05:00") + expect(offset("2023-03-12T07:00:00Z")).toBe("-04:00") }) it("can determine the offset to another base timezone", () => { - expect(offset("2023-02-22", "Europe/Amsterdam")).toBe("-0600") + expect(offset("2023-02-22", "Europe/Amsterdam")).toBe("-06:00") }) it("can determine the offset to another base timezone with daylight savings", () => { - expect(offset("2023-03-26T00:59Z", "Europe/Amsterdam")).toBe("-0500") - expect(offset("2023-03-26T01:00Z", "Europe/Amsterdam")).toBe("-0600") + expect(offset("2023-03-26T00:59Z", "Europe/Amsterdam")).toBe("-05:00") + expect(offset("2023-03-26T01:00Z", "Europe/Amsterdam")).toBe("-06:00") }) it("can determine the offset between two arbitrary timezones", () => { expect(offset("2023-02-22", "Europe/Moscow", "America/Los_Angeles")).toBe( - "-1100" + "-11:00" ) expect(offset("2023-02-22", "America/Los_Angeles", "Europe/Moscow")).toBe( + "+11:00" + ) + expect(offset("2023-02-22", "Europe/Moscow", "America/Los_Angeles", "ZZ")).toBe( + "-1100" + ) + expect(offset("2023-02-22", "America/Los_Angeles", "Europe/Moscow", "ZZ")).toBe( "+1100" ) }) it("can determine the offset to a non full-hour offset timezone", () => { expect(offset("2023-02-22", "Europe/London", "Pacific/Chatham")).toBe( + "+13:45" + ) + expect(offset("2023-02-22", "Europe/London", "Pacific/Chatham", "ZZ")).toBe( "+1345" ) }) diff --git a/src/__tests__/parse.spec.ts b/src/__tests__/parse.spec.ts index e2fd53e..48c8e88 100644 --- a/src/__tests__/parse.spec.ts +++ b/src/__tests__/parse.spec.ts @@ -88,9 +88,9 @@ describe("parse", () => { expect( parse("1994-06-22T04:22:32+09:00", "YYYY-MM-DDTHH:mm:ssZ").toISOString() ).toBe("1994-06-21T19:22:32.000Z") - expect( - parse("1994-06-22T04:22:32+09:00").toISOString() - ).toBe("1994-06-21T19:22:32.000Z") + expect(parse("1994-06-22T04:22:32+09:00").toISOString()).toBe( + "1994-06-21T19:22:32.000Z" + ) }) it("can parse the string month in en", () => { let h: number | string = new Date("2019-01-01").getTimezoneOffset() / 60 @@ -184,7 +184,7 @@ describe("parse", () => { }) it("can parse a full date with a timezone offset", () => { expect( - parse("Friday, May 5, 2023 at 1:30:10 AM -0600", { + parse("Friday, May 5, 2023 at 1:30:10 AM -06:00", { date: "full", time: "full", }).toISOString() @@ -192,16 +192,22 @@ describe("parse", () => { }) it("can parse a custom format with a timezone offset", () => { expect( - parse("2023-02-24T13:44-0500", "YYYY-MM-DDTHH:mmZ", "en").toISOString() + parse("2023-02-24T13:44-05:00", "YYYY-MM-DDTHH:mmZ", "en").toISOString() + ).toBe("2023-02-24T18:44:00.000Z") + expect( + parse("2023--05:00-02-24T13:44", "YYYY-Z-MM-DDTHH:mm", "en").toISOString() + ).toBe("2023-02-24T18:44:00.000Z") + expect( + parse("2023-02-24T13:44-0500", "YYYY-MM-DDTHH:mmZZ", "en").toISOString() ).toBe("2023-02-24T18:44:00.000Z") expect( - parse("2023--0500-02-24T13:44", "YYYY-Z-MM-DDTHH:mm", "en").toISOString() + parse("2023--0500-02-24T13:44", "YYYY-ZZ-MM-DDTHH:mm", "en").toISOString() ).toBe("2023-02-24T18:44:00.000Z") }) it("can filter out the timezone offset", () => { expect( parse({ - date: "Friday, May 7, 2023 at 1:30:10 AM -1000", + date: "Friday, May 7, 2023 at 1:30:10 AM -10:00", format: { date: "full", time: "full", @@ -214,7 +220,7 @@ describe("parse", () => { it("can filter out the timezone offset", () => { expect( parse({ - date: ", May 7, 2023 at 1:30:10 AM -1000", + date: ", May 7, 2023 at 1:30:10 AM -10:00", format: { date: "full", time: "full", @@ -250,4 +256,26 @@ describe("parse", () => { }).toISOString() ).toThrow() }) + it("should throws an error if the Z token is specified and [+-]HHmm", () => { + expect(() => + parse("1994-06-22T04:22:32-0900", "YYYY-MM-DDTHH:mm:ssZ") + ).toThrow("Invalid offset: -0900") + }) + it("should throws an error when a FormatStyle is specified for [+-]HHmm", () => { + expect(() => + parse("Friday, May 5, 2023 at 1:30:10 AM -0600", { + date: "full", + time: "full", + }) + ).toThrow("Invalid offset: -0600") + }) + it("parses a long time format by using the ZZ token", () => { + expect( + parse( + "12/19/89, 1:30:10 AM -0600", + { date: "short", time: "long" }, + "en" + ).toISOString() + ).toBe("1989-12-19T07:30:10.000Z") + }) }) diff --git a/src/__tests__/parts.spec.ts b/src/__tests__/parts.spec.ts index 1a80910..d49ef09 100644 --- a/src/__tests__/parts.spec.ts +++ b/src/__tests__/parts.spec.ts @@ -8,4 +8,16 @@ describe("parts", () => { "MMMM" ) }) + it("uses a Z format when the time style is full", () => { + expect( + parts({ time: "full" }, "en").find((p) => p.partName === "timeZoneName") + ?.token + ).toBe("Z") + }) + it("uses a ZZ format when the time style is long", () => { + expect( + parts({ time: "long" }, "en").find((p) => p.partName === "timeZoneName") + ?.token + ).toBe("ZZ") + }) }) diff --git a/src/__tests__/removeOffset.spec.ts b/src/__tests__/removeOffset.spec.ts index 63369bf..da79e26 100644 --- a/src/__tests__/removeOffset.spec.ts +++ b/src/__tests__/removeOffset.spec.ts @@ -4,12 +4,18 @@ process.env.TZ = "America/New_York" describe("removeOffset", () => { it("can apply a negative offset to a date", () => { + expect( + removeOffset("2023-02-21T19:00:00.000Z", "-05:00").toISOString() + ).toBe("2023-02-22T00:00:00.000Z") expect( removeOffset("2023-02-21T19:00:00.000Z", "-0500").toISOString() ).toBe("2023-02-22T00:00:00.000Z") }) it("can apply a positive offset to a date", () => { + expect( + removeOffset("2023-04-13T16:15:00.000Z", "+02:00").toISOString() + ).toBe("2023-04-13T14:15:00.000Z") expect( removeOffset("2023-04-13T16:15:00.000Z", "+0200").toISOString() ).toBe("2023-04-13T14:15:00.000Z") diff --git a/src/__tests__/validOffset.spec.ts b/src/__tests__/validOffset.spec.ts index 21d4d46..2e203f3 100644 --- a/src/__tests__/validOffset.spec.ts +++ b/src/__tests__/validOffset.spec.ts @@ -4,9 +4,15 @@ process.env.TZ = "America/New_York" describe("validOffset", () => { it("returns its own value when valid", () => { - expect(validOffset("+0000")).toBe("+0000") - expect(validOffset("+0100")).toBe("+0100") + expect(validOffset("+0000", "ZZ")).toBe("+0000") + expect(validOffset("+0100", "ZZ")).toBe("+0100") + expect(validOffset("+00:00", "Z")).toBe("+00:00") + expect(validOffset("+01:00", "Z")).toBe("+01:00") expect(validOffset("+00:00")).toBe("+00:00") expect(validOffset("+01:00")).toBe("+01:00") }) + it("should throw an error when the timezone token does not match the format", () => { + expect(() => validOffset("+0000", "Z")).toThrow() + expect(() => validOffset("+00:00", "ZZ")).toThrow() + }) }) diff --git a/src/applyOffset.ts b/src/applyOffset.ts index 27b18e7..6c5377d 100644 --- a/src/applyOffset.ts +++ b/src/applyOffset.ts @@ -1,15 +1,23 @@ import { date } from "./date" -import { offsetToMins } from "./common" +import { TimezoneToken, fixedLengthByOffset, offsetToMins } from "./common" import type { DateInput } from "./types" /** * Apply a given offset to a date, returning a new date with the offset * applied by adding or subtracting the given number of minutes. * @param dateInput - The date to apply the offset to. - * @param offset - The offset to apply in the +-HHmm format. + * @param offset - The offset to apply in the +-HHmm or +-HH:mm format. */ -export function applyOffset(dateInput: DateInput, offset = "+0000"): Date { +export function applyOffset(dateInput: DateInput, offset = "+00:00"): Date { const d = date(dateInput) - const timeDiffInMins = offsetToMins(offset) + const token = ((): TimezoneToken => { + switch (fixedLengthByOffset(offset)) { + case 5: + return "ZZ" + case 6: + return "Z" + } + })() + const timeDiffInMins = offsetToMins(offset, token) return new Date(d.getTime() + timeDiffInMins * 1000 * 60) } diff --git a/src/common.ts b/src/common.ts index 7958081..27117f9 100644 --- a/src/common.ts +++ b/src/common.ts @@ -7,6 +7,7 @@ import type { FormatStyle, Part, FilledPart, + Format, } from "./types" /** @@ -38,9 +39,20 @@ export const clockAgnostic: FormatPattern[] = [ ["m", { minute: "numeric" }], ["ss", { second: "2-digit" }], ["s", { second: "numeric" }], + ["ZZ", { timeZoneName: "long" }], ["Z", { timeZoneName: "short" }], ] +/** + * Timezone tokens. + */ +const timeZoneTokens = ["Z", "ZZ"] as const + +/** + * Timezone token type. + */ +export type TimezoneToken = (typeof timeZoneTokens)[number] + /** * 24 hour click format patterns. */ @@ -76,7 +88,7 @@ export const fixedLength = { /** * token Z can have variable length depending on the actual value, so it's */ -export function fixedLengthByOffset(offsetString: string): number { +export function fixedLengthByOffset(offsetString: string): 6 | 5 { // starts with [+-]xx:xx if (/^[+-]\d{2}:\d{2}/.test(offsetString)) { return 6 @@ -187,7 +199,7 @@ export function fill( return token === "A" ? p.toUpperCase() : p.toLowerCase() } if (partName === "timeZoneName") { - return offset ?? minsToOffset(-1 * d.getTimezoneOffset()) + return offset ?? minsToOffset(-1 * d.getTimezoneOffset(), token) } return value } @@ -281,27 +293,38 @@ function createPartMap( } /** - * Converts minutes (300) to an ISO8601 compatible offset (+0400). + * Converts minutes (300) to an ISO8601 compatible offset (+0400 or +04:00). * @param timeDiffInMins - The difference in minutes between two timezones. * @returns */ -export function minsToOffset(timeDiffInMins: number): string { +export function minsToOffset( + timeDiffInMins: number, + token: string = "Z" +): string { const hours = String(Math.floor(Math.abs(timeDiffInMins / 60))).padStart( 2, "0" ) const mins = String(Math.abs(timeDiffInMins % 60)).padStart(2, "0") const sign = timeDiffInMins < 0 ? "-" : "+" - return `${sign}${hours}${mins}` + + if (token === "ZZ") { + return `${sign}${hours}${mins}` + } + + return `${sign}${hours}:${mins}` } /** * Converts an offset (-0500) to minutes (-300). * @param offset - The offset to convert to minutes. + * @param token - The timezone token format. */ -export function offsetToMins(offset: string): number { - validOffset(offset) - const [_, sign, hours, mins] = offset.match(/([+-])([0-3][0-9])([0-6][0-9])/)! +export function offsetToMins(offset: string, token: TimezoneToken): number { + validOffset(offset, token) + const [_, sign, hours, mins] = offset.match( + /([+-])([0-3][0-9]):?([0-6][0-9])/ + )! const offsetInMins = Number(hours) * 60 + Number(mins) return sign === "+" ? offsetInMins : -offsetInMins } @@ -310,9 +333,18 @@ export function offsetToMins(offset: string): number { * Validates that an offset is valid according to the format: * [+-]HHmm or [+-]HH:mm * @param offset - The offset to validate. + * @param token - The timezone token format. */ -export function validOffset(offset: string) { - const valid = /^([+-])[0-3][0-9]:?[0-6][0-9]$/.test(offset) +export function validOffset(offset: string, token: TimezoneToken = "Z") { + const valid = ((token: TimezoneToken): boolean => { + switch (token) { + case "Z": + return /^([+-])[0-3][0-9]:[0-6][0-9]$/.test(offset) + case "ZZ": + return /^([+-])[0-3][0-9][0-6][0-9]$/.test(offset) + } + })(token) + if (!valid) throw new Error(`Invalid offset: ${offset}`) return offset } @@ -369,3 +401,15 @@ export function validate(parts: Part[]): Part[] | never { } return parts } + +/** + * Returns the timezone token format from a given format. + * @param format - The format to check. + * @returns The timezone token format ("Z" or "ZZ"). + */ +export function getOffsetFormat(format: Format): TimezoneToken { + if (typeof format === "string") { + return format.includes("ZZ") ? "ZZ" : "Z" + } + return "time" in format && format.time === "full" ? "Z" : "ZZ" +} diff --git a/src/diffDays.ts b/src/diffDays.ts new file mode 100644 index 0000000..deb11de --- /dev/null +++ b/src/diffDays.ts @@ -0,0 +1,20 @@ +import { diffMilliseconds } from "./diffMilliseconds" +import { DateInput } from "./types" +import { diffRound, type DiffRoundingMethod } from "./diffRound" + +/** + * Returns the difference between 2 dates in days. + * @param dateA A date to compare with the right date + * @param dateB A date to compare with the left date + * @param roundingMethod the rounding method to use, default: trunc + */ +export function diffDays( + dateA: DateInput, + dateB: DateInput, + roundingMethod?: DiffRoundingMethod +) { + return diffRound( + diffMilliseconds(dateA, dateB) / 86_400_000, // hour * 24 + roundingMethod + ) +} diff --git a/src/diffHours.ts b/src/diffHours.ts new file mode 100644 index 0000000..d935224 --- /dev/null +++ b/src/diffHours.ts @@ -0,0 +1,20 @@ +import { diffMilliseconds } from "./diffMilliseconds" +import { diffRound, type DiffRoundingMethod } from "./diffRound" +import { DateInput } from "./types" + +/** + * Returns the difference between 2 dates in hours. + * @param dateA A date to compare with the right date + * @param dateB A date to compare with the left date + * @param roundingMethod the rounding method to use, default: trunc + */ +export function diffHours( + dateA: DateInput, + dateB: DateInput, + roundingMethod?: DiffRoundingMethod +) { + return diffRound( + diffMilliseconds(dateA, dateB) / 3_600_000, // 1000 * 60 * 60 + roundingMethod + ) +} diff --git a/src/diffMilliseconds.ts b/src/diffMilliseconds.ts new file mode 100644 index 0000000..2e1e780 --- /dev/null +++ b/src/diffMilliseconds.ts @@ -0,0 +1,13 @@ +import { date } from "./date" +import { DateInput } from "./types" + +/** + * Returns the difference between 2 dates in milliseconds. + * @param dateA A date to compare with the right date + * @param dateB A date to compare with the left date + */ +export function diffMilliseconds(dateA: DateInput, dateB: DateInput) { + const left = date(dateA) + const right = date(dateB) + return +left - +right +} diff --git a/src/diffMinutes.ts b/src/diffMinutes.ts new file mode 100644 index 0000000..da0de7d --- /dev/null +++ b/src/diffMinutes.ts @@ -0,0 +1,16 @@ +import { DateInput } from "./types" +import { diffMilliseconds } from "./diffMilliseconds" +import { diffRound, type DiffRoundingMethod } from "./diffRound" + +/** + * Returns the difference between 2 dates in minutes. + * @param dateA A date to compare with the right date + * @param roundingMethod the rounding method to use, default: trunc + */ +export function diffMinutes( + dateA: DateInput, + dateB: DateInput, + roundingMethod?: DiffRoundingMethod +) { + return diffRound(diffMilliseconds(dateA, dateB) / 60_000, roundingMethod) +} diff --git a/src/diffMonths.ts b/src/diffMonths.ts new file mode 100644 index 0000000..960752e --- /dev/null +++ b/src/diffMonths.ts @@ -0,0 +1,36 @@ +import { date } from "./date" +import { DateInput } from "./types" +import { monthDays } from "./monthDays" + +/** + * Returns the difference between 2 dates in months. + * @param dateA A date to compare with the dateB date + * @param dateB A date to compare with the dateA date + */ +export function diffMonths(dateA: DateInput, dateB: DateInput): number { + const l = date(dateA) + const r = date(dateB) + // if the dateB one is bigger, we switch them around as it's easier to do + if (l < r) { + const rs = diffMonths(r, l) + return rs == 0 ? 0 : -rs + } + + // we first get the amount of calendar months + let months = + (l.getFullYear() - r.getFullYear()) * 12 + (l.getMonth() - r.getMonth()) + + const ld = l.getDate() + const rd = r.getDate() + + // if no full month has passed we may subtract a month from the calendar months so we get the amount of full months + if (ld < rd) { + // in case dateA date is the last day of the month & the dateB date is higher, we don't subtract as a full month did actually pass + const lm = monthDays(l) + if (!(lm == ld && lm < rd)) { + months-- + } + } + //ensures we don't give back -0 + return months == 0 ? 0 : months +} diff --git a/src/diffRound.ts b/src/diffRound.ts new file mode 100644 index 0000000..8839c33 --- /dev/null +++ b/src/diffRound.ts @@ -0,0 +1,11 @@ +export type DiffRoundingMethod = "trunc" | "round" | "floor" | "ceil" + +/** + * Return a rounded value with the given rounding method + * @param value the value to round + * @param method the rounding method + */ +export function diffRound(value: number, method: DiffRoundingMethod = "trunc") { + const r = Math[method](value) + return r == 0 ? 0 : r +} diff --git a/src/diffSeconds.ts b/src/diffSeconds.ts new file mode 100644 index 0000000..8ad6e56 --- /dev/null +++ b/src/diffSeconds.ts @@ -0,0 +1,17 @@ +import { diffMilliseconds } from "./diffMilliseconds" +import { DiffRoundingMethod, diffRound } from "./diffRound" +import { DateInput } from "./types" + +/** + * Returns the difference between 2 dates in seconds. + * @param dateA A date to compare with the right date + * @param dateB A date to compare with the left date + * @param roundingMethod the rounding method to use, default: trunc + */ +export function diffSeconds( + dateA: DateInput, + dateB: DateInput, + roundingMethod?: DiffRoundingMethod +) { + return diffRound(diffMilliseconds(dateA, dateB) / 1000, roundingMethod) +} diff --git a/src/diffWeeks.ts b/src/diffWeeks.ts new file mode 100644 index 0000000..0eb9ea7 --- /dev/null +++ b/src/diffWeeks.ts @@ -0,0 +1,20 @@ +import { diffMilliseconds } from "./diffMilliseconds" +import { DateInput } from "./types" +import { diffRound, type DiffRoundingMethod } from "./diffRound" + +/** + * Returns the difference between 2 dates in days. + * @param dateA A date to compare with the right date + * @param dateB A date to compare with the left date + * @param roundingMethod the rounding method to use, default: trunc + */ +export function diffWeeks( + dateA: DateInput, + dateB: DateInput, + roundingMethod?: DiffRoundingMethod +) { + return diffRound( + diffMilliseconds(dateA, dateB) / 604800000, // day * 7 + roundingMethod + ) +} diff --git a/src/diffYears.ts b/src/diffYears.ts new file mode 100644 index 0000000..836b830 --- /dev/null +++ b/src/diffYears.ts @@ -0,0 +1,13 @@ +import { diffMonths } from "./diffMonths" +import { DateInput } from "./types" + +/** + * Returns the difference between 2 dates in years. + * @param dateA A date to compare with the dateB date + * @param dateB A date to compare with the dateA date + */ +export function diffYears(dateA: DateInput, dateB: DateInput): number { + const r = Math.trunc(diffMonths(dateA, dateB) / 12) + //ensures we don't give back -0 + return r == 0 ? 0 : r +} diff --git a/src/format.ts b/src/format.ts index 78edb20..c2a8ddf 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,7 +1,7 @@ import { date } from "./date" import { parts } from "./parts" -import { fill } from "./common" -import type { DateInput, Format, FormatOptions, Part } from "./types" +import { fill, getOffsetFormat } from "./common" +import type { DateInput, Format, FormatOptions, FormatStyle, Part } from "./types" import { offset } from "./offset" import { removeOffset } from "./removeOffset" import { deviceLocale } from "./deviceLocale" @@ -72,7 +72,7 @@ export function format( if (format === "ISO8601") return date(inputDateOrOptions).toISOString() if (tz) { - forceOffset = offset(inputDateOrOptions, "utc", tz) + forceOffset = offset(inputDateOrOptions, "utc", tz, getOffsetFormat(format)) } // We need to apply an offset to the date so that it can be formatted as UTC. diff --git a/src/index.ts b/src/index.ts index 59eec7a..eaa33e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,3 +43,11 @@ export { isBefore } from "./isBefore" export { isAfter } from "./isAfter" export { isEqual } from "./isEqual" export * from "./types" +export { diffMilliseconds } from "./diffMilliseconds" +export { diffSeconds } from "./diffSeconds" +export { diffMinutes } from "./diffMinutes" +export { diffHours } from "./diffHours" +export { diffDays } from "./diffDays" +export { diffWeeks } from "./diffWeeks" +export { diffMonths } from "./diffMonths" +export { diffYears } from "./diffYears" diff --git a/src/offset.ts b/src/offset.ts index deb2b07..939d394 100644 --- a/src/offset.ts +++ b/src/offset.ts @@ -1,5 +1,5 @@ import { date } from "./date" -import { normStr, minsToOffset } from "./common" +import { normStr, minsToOffset, TimezoneToken } from "./common" import { deviceTZ } from "./deviceTZ" import type { DateInput } from "./types" @@ -49,12 +49,13 @@ function relativeTime(d: Date, timeZone: string): Date { export function offset( utcTime: DateInput, tzA = "UTC", - tzB = "device" + tzB = "device", + timeZoneToken: TimezoneToken = "Z" , ): string { tzB = tzB === "device" ? deviceTZ() ?? "utc" : tzB const d = date(utcTime) const timeA = relativeTime(d, tzA) const timeB = relativeTime(d, tzB) const timeDiffInMins = (timeB.getTime() - timeA.getTime()) / 1000 / 60 - return minsToOffset(timeDiffInMins) + return minsToOffset(timeDiffInMins, timeZoneToken) } diff --git a/src/parse.ts b/src/parse.ts index f862d49..1296a1e 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -107,8 +107,8 @@ export function parse( parsed.set("MM", v) } else if (t === "a" || t === "A") { a = part.value.toLowerCase() === ap("am", locale).toLowerCase() - } else if (t === "Z") { - offset = validOffset(part.value) + } else if (t === "Z" || t === "ZZ") { + offset = validOffset(part.value, t) } else { const values = range(t as FormatToken, locale, genitive) const index = values.indexOf(part.value) diff --git a/src/parts.ts b/src/parts.ts index 0cd8633..9ac53f3 100644 --- a/src/parts.ts +++ b/src/parts.ts @@ -135,7 +135,8 @@ function styleParts( part.type, part.value, locale, - part.type === "hour" ? hourType : undefined + part.type === "hour" ? hourType : undefined, + options ) if (formatPattern === undefined) return const partValue = formatPattern[1][partName] @@ -165,7 +166,8 @@ function guessPattern( partName: T, partValue: string, locale: string, - hour: T extends "hour" ? 12 | 24 : undefined + hour: T extends "hour" ? 12 | 24 : undefined, + options: Intl.DateTimeFormatOptions ): FormatPattern | undefined { const l = partValue.length const n = !isNaN(Number(partValue)) @@ -208,10 +210,7 @@ function guessPattern( case "literal": return [partValue, { literal: partValue }, new RegExp("")] case "timeZoneName": - const offset = partValue.split("-") - return offset.length === 2 && offset[1].length === 4 - ? tokens.get("ZZ") - : tokens.get("Z") + return options.timeStyle === "full" ? tokens.get("Z") : tokens.get("ZZ") default: return undefined } diff --git a/src/removeOffset.ts b/src/removeOffset.ts index aacaa3d..bf221ba 100644 --- a/src/removeOffset.ts +++ b/src/removeOffset.ts @@ -4,9 +4,9 @@ import type { DateInput } from "./types" /** * Inverts the offset and applies it to the given date, returning a new date. * @param dateInput - The date to remove the offset from. - * @param offset - The offset to remove in the +-HHmm format. + * @param offset - The offset to remove in the +-HHmm or +-HH:mm format. */ -export function removeOffset(dateInput: DateInput, offset = "+0000"): Date { +export function removeOffset(dateInput: DateInput, offset = "+00:00"): Date { const positive = offset.slice(0, 1) === "+" return applyOffset( dateInput,