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