├── site
├── .nojekyll
├── docs
│ ├── _media
│ │ ├── Luxon_icon_32x32.png
│ │ ├── Luxon_icon_64x64.png
│ │ ├── Luxon_icon_180x180.png
│ │ ├── Luxon_icon_180x180@2x.png
│ │ └── Luxon_icon.svg
│ ├── _coverpage.md
│ └── _sidebar.md
├── demo
│ ├── demo.css
│ ├── global.html
│ ├── requirejs.html
│ └── demo.js
├── plugins
│ ├── dark-theme-toggle.js
│ └── dark-theme-toggle.css
└── index.html
├── .husky
├── .gitignore
└── pre-commit
├── .prettierrc
├── .agignore
├── benchmarks
├── package.json
├── index.js
├── info.js
└── datetime.js
├── docker
├── push
├── build
├── npm
├── Dockerfile
└── readme.md
├── babel.config.js
├── .babelrc
├── src
├── package.json
├── impl
│ ├── invalid.js
│ ├── zoneUtil.js
│ ├── digits.js
│ ├── formats.js
│ ├── diff.js
│ ├── english.js
│ └── conversions.js
├── luxon.js
├── zones
│ ├── invalidZone.js
│ ├── systemZone.js
│ └── fixedOffsetZone.js
├── errors.js
├── zone.js
└── settings.js
├── scripts
├── repl
├── test
├── jest
├── release
├── tag
├── readme.md
├── bootstrap.js
└── version
├── tasks
├── buildAll.js
├── buildNode.js
├── buildGlobal.js
└── build.js
├── .gitattributes
├── codecov.yml
├── .gitignore
├── test
├── zones
│ ├── zoneInterface.test.js
│ ├── invalid.test.js
│ ├── local.test.js
│ ├── fixedOffset.test.js
│ └── IANA.test.js
├── duration
│ ├── proto.test.js
│ ├── info.test.js
│ ├── typecheck.test.js
│ ├── accuracy.test.js
│ ├── set.test.js
│ ├── customMatrix.test.js
│ ├── reconfigure.test.js
│ ├── invalid.test.js
│ ├── getters.test.js
│ ├── equality.test.js
│ ├── parse.test.js
│ ├── create.test.js
│ └── math.test.js
├── datetime
│ ├── proto.test.js
│ ├── degrade.test.js
│ ├── typecheck.test.js
│ ├── info.test.js
│ ├── reconfigure.test.js
│ ├── equality.test.js
│ ├── transform.test.js
│ ├── many.test.js
│ ├── invalid.test.js
│ ├── misc.test.js
│ ├── getters.test.js
│ └── dst.test.js
├── interval
│ ├── proto.test.js
│ ├── setter.test.js
│ ├── getters.test.js
│ ├── typecheck.test.js
│ ├── localeWeek.test.js
│ └── create.test.js
├── info
│ ├── features.test.js
│ ├── localeWeek.test.js
│ └── zones.test.js
├── helpers.js
└── impl
│ └── english.test.js
├── .editorconfig
├── jest.config.js
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── test.yml
├── LICENSE.md
├── docs
├── home.md
├── validity.md
├── calendars.md
├── install.md
├── matrix.md
├── upgrading.md
└── why.md
├── README.md
├── package.json
└── CONTRIBUTING.md
/site/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | { "printWidth": 100 }
2 |
--------------------------------------------------------------------------------
/.agignore:
--------------------------------------------------------------------------------
1 | /build
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/benchmarks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/docker/push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker push icambron/luxon
3 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { presets: ["@babel/preset-env"] };
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/docker/build:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker build docker -t icambron/luxon
3 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "version": "3.7.1"
4 | }
5 |
--------------------------------------------------------------------------------
/scripts/repl:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | LANG=en-US.utf8 node -i -r "./scripts/bootstrap.js"
3 |
--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | TZ="America/New_York" LANG=en_US.utf8 npm run test
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/scripts/jest:
--------------------------------------------------------------------------------
1 | TZ="America/New_York" NODE_ICU_DATA="$(pwd)/node_modules/full-icu" LANG=en_US.utf8 jest $@
--------------------------------------------------------------------------------
/tasks/buildAll.js:
--------------------------------------------------------------------------------
1 | const { buildAll } = require("./build");
2 | buildAll().catch(console.error);
3 |
--------------------------------------------------------------------------------
/tasks/buildNode.js:
--------------------------------------------------------------------------------
1 | const { buildNode } = require("./build");
2 | buildNode().catch(console.error);
3 |
--------------------------------------------------------------------------------
/docker/npm:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker run -it --rm -v $(pwd):/luxon -w /luxon icambron/luxon npm $@
3 |
--------------------------------------------------------------------------------
/tasks/buildGlobal.js:
--------------------------------------------------------------------------------
1 | const { buildGlobal } = require("./build");
2 | buildGlobal().catch(console.error);
3 |
--------------------------------------------------------------------------------
/site/docs/_media/Luxon_icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkGhostHunter/luxon/master/site/docs/_media/Luxon_icon_32x32.png
--------------------------------------------------------------------------------
/site/docs/_media/Luxon_icon_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkGhostHunter/luxon/master/site/docs/_media/Luxon_icon_64x64.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | scripts/* linguist-vendored
2 | docker/* linguist-vendored
3 | site/** linguist-vendored
4 | .husky/* linguist-vendored
5 |
--------------------------------------------------------------------------------
/site/docs/_media/Luxon_icon_180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkGhostHunter/luxon/master/site/docs/_media/Luxon_icon_180x180.png
--------------------------------------------------------------------------------
/site/docs/_media/Luxon_icon_180x180@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkGhostHunter/luxon/master/site/docs/_media/Luxon_icon_180x180@2x.png
--------------------------------------------------------------------------------
/scripts/release:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ./scripts/version || exit $?
3 |
4 | npm run build
5 | npm run site
6 | npm publish
7 | ./scripts/tag
8 | ./scripts/deploy-site
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 | coverage:
3 | status:
4 | project:
5 | default:
6 | target: auto
7 | threshold: null
8 | base: auto
9 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM library/node:15-slim
2 |
3 | ENV HUSKY_SKIP_INSTALL=1
4 |
5 | ENV LANG=en_US.utf8
6 | ENV LIMIT_JEST=yes
7 | ENV CI=yes
8 | ENV TZ=America/New_York
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .tern-port
3 | /build
4 | #*
5 | .#*
6 | coverage/
7 | .DS_Store
8 | .external-ecmascript.js
9 | .idea
10 | .vscode
11 | test/scratch.test.js
12 |
--------------------------------------------------------------------------------
/scripts/tag:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ./scripts/version || exit $?
3 |
4 | PACKAGE_VERSION=$(node -p "require('./package.json').version")
5 |
6 | git tag $PACKAGE_VERSION
7 | git push origin master $PACKAGE_VERSION
8 |
--------------------------------------------------------------------------------
/test/zones/zoneInterface.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Zone } from "../../src/luxon";
3 |
4 | test("You can instantiate Zone directly", () => {
5 | expect(() => new Zone().isValid).toThrow();
6 | });
7 |
--------------------------------------------------------------------------------
/site/demo/demo.css:
--------------------------------------------------------------------------------
1 | .divider {
2 | color: blue;
3 | }
4 |
5 | .code {
6 | display: inline-block;
7 | font-family: monospace;
8 | }
9 |
10 | .result {
11 | color: darkgreen;
12 | font-style: italic;
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 2
6 | end_of_line = lf
7 | indent_style = space
8 | insert_final_newline = false
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/scripts/readme.md:
--------------------------------------------------------------------------------
1 | These are scripts useful for development:
2 |
3 | 1. `test` is a script for more conveniently running the tests if you've installed `full-icu` as an npm module
4 | 1. `release`, `tag`, and `deploy-site` are administrative tasks you won't need
5 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "node",
3 | roots: ["test"],
4 | coverageDirectory: "build/coverage",
5 | collectCoverageFrom: ["src/**/*.js", "!src/zone.js"],
6 | transform: {
7 | "^.+\\.js$": "babel-jest",
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const luxon = require("../build/node/luxon");
2 | global.DateTime = luxon.DateTime;
3 | global.Duration = luxon.Duration;
4 | global.Interval = luxon.Interval;
5 | global.Settings = luxon.Settings;
6 | global.Info = luxon.Info;
7 | global.IANAZone = luxon.IANAZone;
8 |
--------------------------------------------------------------------------------
/docker/readme.md:
--------------------------------------------------------------------------------
1 | Luxon provides a Docker container and some wrapping scripts to make it easier to run the tests.
2 |
3 | 1. The Dockerfile is really just here as an FYI. You shouldn't need to interact with it
4 | 1. `npm` is a bash script that runs `npm run [arg]` inside the Docker container.
5 |
--------------------------------------------------------------------------------
/benchmarks/index.js:
--------------------------------------------------------------------------------
1 | import dateTimeSuites from "./datetime.js";
2 | import infoSuites from "./info.js";
3 |
4 | const allSuites = [...dateTimeSuites, ...infoSuites];
5 |
6 | async function runAllSuites() {
7 | for (const runSuite of allSuites) {
8 | await runSuite();
9 | }
10 | }
11 |
12 | runAllSuites();
13 |
--------------------------------------------------------------------------------
/site/demo/global.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/impl/invalid.js:
--------------------------------------------------------------------------------
1 | export default class Invalid {
2 | constructor(reason, explanation) {
3 | this.reason = reason;
4 | this.explanation = explanation;
5 | }
6 |
7 | toMessage() {
8 | if (this.explanation) {
9 | return `${this.reason}: ${this.explanation}`;
10 | } else {
11 | return this.reason;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/duration/proto.test.js:
--------------------------------------------------------------------------------
1 | import { Duration } from "../../src/luxon";
2 |
3 | test("Duration prototype properties should not throw when addressed", () => {
4 | const d = Duration.fromObject({ hours: 1 });
5 | expect(() =>
6 | Object.getOwnPropertyNames(Object.getPrototypeOf(d)).forEach(
7 | (name) => Object.getPrototypeOf(d)[name]
8 | )
9 | ).not.toThrow();
10 | });
11 |
--------------------------------------------------------------------------------
/test/datetime/proto.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { DateTime } from "../../src/luxon";
3 |
4 | test("DateTime prototype properties should not throw when accessed", () => {
5 | const d = DateTime.now();
6 | expect(() =>
7 | Object.getOwnPropertyNames(Object.getPrototypeOf(d)).forEach(
8 | (name) => Object.getPrototypeOf(d)[name]
9 | )
10 | ).not.toThrow();
11 | });
12 |
--------------------------------------------------------------------------------
/test/datetime/degrade.test.js:
--------------------------------------------------------------------------------
1 | /* global expect */
2 | import { DateTime } from "../../src/luxon";
3 |
4 | const Helpers = require("../helpers");
5 |
6 | Helpers.withoutRTF("calling toRelative falls back to English", () => {
7 | expect(
8 | DateTime.fromISO("2014-08-06", { locale: "fr" }).toRelative({
9 | base: DateTime.fromISO("1982-05-25"),
10 | })
11 | ).toBe("in 32 years");
12 | });
13 |
--------------------------------------------------------------------------------
/site/docs/_coverpage.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Luxon 3.x
4 |
5 | > A powerful, modern, and friendly wrapper for JavaScript dates and times.
6 |
7 | * DateTimes, Durations, and Intervals
8 | * Immutable, chainable, unambiguous API.
9 | * Native time zone and Intl support (no locale or tz files)
10 |
11 | [GitHub](https://github.com/moment/luxon/)
12 | [Get started](#Luxon)
13 |
14 | 
15 |
--------------------------------------------------------------------------------
/test/interval/proto.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { DateTime } from "../../src/luxon";
3 |
4 | test("Interval prototype properties should not throw when addressed", () => {
5 | const i = DateTime.fromISO("2018-01-01").until(DateTime.fromISO("2018-01-02"));
6 | expect(() =>
7 | Object.getOwnPropertyNames(Object.getPrototypeOf(i)).forEach(
8 | (name) => Object.getPrototypeOf(i)[name]
9 | )
10 | ).not.toThrow();
11 | });
12 |
--------------------------------------------------------------------------------
/site/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | * [Home](/)
2 | * [Install guide](install.md)
3 | * [A quick tour](tour.md)
4 | * [Upgrade guide](upgrading.md)
5 | * [Intl](intl.md)
6 | * [Time zones and offsets](zones.md)
7 | * [Calendars](calendars.md)
8 | * [Formatting](formatting.md)
9 | * [Parsing](parsing.md)
10 | * [Math](math.md)
11 | * [Validity](validity.md)
12 | * [API docs](api-docs/index.html ':ignore')
13 | * [Support matrix](matrix.md)
14 | * [For Moment users](moment.md)
15 | * [Why does Luxon exist?](why.md)
16 |
17 |
--------------------------------------------------------------------------------
/site/demo/requirejs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/duration/info.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | const dur = Duration.fromObject({
6 | years: 1,
7 | months: 2,
8 | days: 3.3,
9 | });
10 |
11 | //------
12 | // #toObject
13 | //-------
14 | test("Duration#toObject returns the object", () => {
15 | expect(dur.toObject()).toEqual({
16 | years: 1,
17 | months: 2,
18 | days: 3.3,
19 | });
20 | });
21 |
22 | test("Duration#toObject returns an empty object for invalid durations", () => {
23 | expect(Duration.invalid("because").toObject()).toEqual({});
24 | });
25 |
--------------------------------------------------------------------------------
/test/info/features.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Info } from "../../src/luxon";
3 |
4 | const Helpers = require("../helpers");
5 |
6 | test("Info.features shows this environment supports all the features", () => {
7 | expect(Info.features().relative).toBe(true);
8 | expect(Info.features().localeWeek).toBe(true);
9 | });
10 |
11 | Helpers.withoutRTF("Info.features shows no support", () => {
12 | expect(Info.features().relative).toBe(false);
13 | });
14 |
15 | Helpers.withoutLocaleWeekInfo("Info.features shows no support", () => {
16 | expect(Info.features().localeWeek).toBe(false);
17 | });
18 |
--------------------------------------------------------------------------------
/test/zones/invalid.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { InvalidZone } from "../../src/luxon";
3 |
4 | test("InvalidZone", () => {
5 | const zone = new InvalidZone("foo");
6 |
7 | expect(zone.type).toBe("invalid");
8 | expect(zone.name).toBe("foo");
9 | expect(zone.offsetName()).toBe(null); // the abstract class states this returns a string, yet InvalidZones return null :(
10 | expect(zone.formatOffset(0, "short")).toBe("");
11 | expect(zone.isUniversal).toBe(false);
12 | expect(zone.offset()).toBe(NaN);
13 | expect(zone.isValid).toBe(false);
14 | expect(zone.equals(zone)).toBe(false); // always false even if it has the same name
15 | });
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: enhancement
6 | ---
7 |
8 | **Is your feature request related to a problem? Please describe.**
9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
10 |
11 | **Describe the solution you'd like**
12 | A clear and concise description of what you want to happen.
13 |
14 | **Describe alternatives you've considered**
15 | A clear and concise description of any alternative solutions or features you've considered.
16 |
17 | **Additional context**
18 | Add any other context or screenshots about the feature request here.
19 |
--------------------------------------------------------------------------------
/src/luxon.js:
--------------------------------------------------------------------------------
1 | import DateTime from "./datetime.js";
2 | import Duration from "./duration.js";
3 | import Interval from "./interval.js";
4 | import Info from "./info.js";
5 | import Zone from "./zone.js";
6 | import FixedOffsetZone from "./zones/fixedOffsetZone.js";
7 | import IANAZone from "./zones/IANAZone.js";
8 | import InvalidZone from "./zones/invalidZone.js";
9 | import SystemZone from "./zones/systemZone.js";
10 | import Settings from "./settings.js";
11 |
12 | const VERSION = "3.7.1";
13 |
14 | export {
15 | VERSION,
16 | DateTime,
17 | Duration,
18 | Interval,
19 | Info,
20 | Zone,
21 | FixedOffsetZone,
22 | IANAZone,
23 | InvalidZone,
24 | SystemZone,
25 | Settings,
26 | };
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | ---
7 |
8 | **Describe the bug**
9 | A clear and concise description of what the bug is.
10 |
11 | **To Reproduce**
12 | Please share a minimal code example that triggers the problem:
13 |
14 | **Actual vs Expected behavior**
15 | A clear and concise description of what you expected to happen.
16 |
17 | **Desktop (please complete the following information):**
18 |
19 | - OS: [e.g. iOS]
20 | - Browser [e.g. Chrome 84, safari 14.0]
21 | - Luxon version [e.g. 1.25.0]
22 | - Your timezone [e.g. "America/New_York"]
23 |
24 | **Additional context**
25 | Add any other context about the problem here.
26 |
--------------------------------------------------------------------------------
/test/interval/setter.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Interval } from "../../src/luxon";
3 |
4 | const Helpers = require("../helpers");
5 |
6 | const todayFrom = (h1, h2) => Interval.fromDateTimes(Helpers.atHour(h1), Helpers.atHour(h2));
7 |
8 | test("Interval.set can set the start", () => {
9 | expect(todayFrom(3, 5).set({ start: Helpers.atHour(4) }).start.hour).toBe(4);
10 | });
11 |
12 | test("Interval.set can set the end", () => {
13 | expect(todayFrom(3, 5).set({ end: Helpers.atHour(6) }).end.hour).toBe(6);
14 | });
15 |
16 | test("Interval.set preserves invalidity", () => {
17 | const invalid = Interval.invalid("because");
18 | expect(invalid.set({ start: Helpers.atHour(4) }).isValid).toBe(false);
19 | });
20 |
--------------------------------------------------------------------------------
/test/interval/getters.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Interval } from "../../src/luxon";
3 |
4 | const Helpers = require("../helpers");
5 |
6 | const todayFrom = (h1, h2) => Interval.fromDateTimes(Helpers.atHour(h1), Helpers.atHour(h2)),
7 | invalid = Interval.invalid("because");
8 |
9 | test("Interval.start gets the start", () => {
10 | expect(todayFrom(3, 5).start.hour).toBe(3);
11 | });
12 |
13 | test("Interval.start returns null for invalid intervals", () => {
14 | expect(invalid.start).toBe(null);
15 | });
16 |
17 | test("Interval.end gets the end", () => {
18 | expect(todayFrom(3, 5).end.hour).toBe(5);
19 | });
20 |
21 | test("Interval.end returns null for invalid intervals", () => {
22 | expect(invalid.end).toBe(null);
23 | });
24 |
--------------------------------------------------------------------------------
/test/interval/typecheck.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Interval, DateTime } from "../../src/luxon";
4 |
5 | //------
6 | // #isInterval
7 | //-------
8 | test("Interval.isInterval return true for valid duration", () => {
9 | const int = Interval.fromDateTimes(DateTime.now(), DateTime.now());
10 | expect(Interval.isInterval(int)).toBe(true);
11 | });
12 |
13 | test("Interval.isInterval return true for invalid duration", () => {
14 | const int = Interval.invalid("because");
15 | expect(Interval.isInterval(int)).toBe(true);
16 | });
17 |
18 | test("Interval.isInterval return false for primitives", () => {
19 | expect(Interval.isInterval({})).toBe(false);
20 | expect(Interval.isInterval(1)).toBe(false);
21 | expect(Interval.isInterval("")).toBe(false);
22 | expect(Interval.isInterval(null)).toBe(false);
23 | expect(Interval.isInterval()).toBe(false);
24 | });
25 |
--------------------------------------------------------------------------------
/test/datetime/typecheck.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { DateTime } from "../../src/luxon";
3 |
4 | //------
5 | // #isDateTime()
6 | //------
7 | test("DateTime#isDateTime return true for valid DateTime", () => {
8 | const dt = DateTime.now();
9 | expect(DateTime.isDateTime(dt)).toBe(true);
10 | });
11 |
12 | test("DateTime#isDateTime return true for invalid DateTime", () => {
13 | const dt = DateTime.invalid("because");
14 | expect(DateTime.isDateTime(dt)).toBe(true);
15 | });
16 |
17 | test("DateTime#isDateTime return false for primitives", () => {
18 | expect(DateTime.isDateTime({})).toBe(false);
19 | expect(DateTime.isDateTime({ hours: 60 })).toBe(false);
20 | expect(DateTime.isDateTime(1)).toBe(false);
21 | expect(DateTime.isDateTime("")).toBe(false);
22 | expect(DateTime.isDateTime(null)).toBe(false);
23 | expect(DateTime.isDateTime()).toBe(false);
24 | });
25 |
--------------------------------------------------------------------------------
/test/zones/local.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { SystemZone } from "../../src/luxon";
3 |
4 | test("SystemZone.instance returns a singleton", () => {
5 | expect(SystemZone.instance).toBe(SystemZone.instance);
6 | });
7 |
8 | test("SystemZone.instance provides valid ...", () => {
9 | expect(SystemZone.instance.type).toBe("system");
10 | expect(SystemZone.instance.isUniversal).toBe(false);
11 | expect(SystemZone.instance.isValid).toBe(true);
12 | expect(SystemZone.instance).toBe(SystemZone.instance);
13 |
14 | // todo: figure out how to test these without inadvertently testing IANAZone
15 | expect(SystemZone.instance.name).toBe("America/New_York"); // this is true for the provided Docker container, what's the right way to test it?
16 | // expect(SystemZone.instance.offsetName()).toBe("UTC");
17 | // expect(SystemZone.instance.formatOffset(0, "short")).toBe("+00:00");
18 | // expect(SystemZone.instance.offset()).toBe(0);
19 | });
20 |
--------------------------------------------------------------------------------
/scripts/version:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | abort() {
4 | echo "ERROR: $1"
5 | exit 1
6 | }
7 |
8 | if [[ -x $(which ggrep) ]]
9 | then
10 | grepper='ggrep'
11 | else
12 | grepper='grep'
13 | fi
14 |
15 | PACKAGE_VERSION=$(node -p "require('./package.json').version")
16 | PACKAGE_LOCK_VERSION=$(node -p "require('./package-lock.json').version")
17 | MODULE_VERSION=$(node -p "require('./src/package.json').version")
18 | EXPORTED_VERSION=$($grepper -oP "(?<=const VERSION = \").*(?=\";)" ./src/luxon.js)
19 |
20 | if [ $PACKAGE_VERSION != $PACKAGE_LOCK_VERSION ]; then
21 | abort "package-lock.json's version differs from package.json's"
22 | elif [ $PACKAGE_VERSION != $EXPORTED_VERSION ]; then
23 | abort "exported version differs from package.json's"
24 | elif [ $PACKAGE_VERSION != $MODULE_VERSION ]; then
25 | abort "src/package.json differs from package.json's"
26 | elif [ $(git tag -l "$PACKAGE_VERSION") ]; then
27 | abort "tag already exists"
28 | fi
29 |
--------------------------------------------------------------------------------
/test/duration/typecheck.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | //------
6 | // #isDuration
7 | //-------
8 | test("Duration#isDuration return true for valid duration", () => {
9 | const dur = Duration.fromObject({ hours: 1, minutes: 4.5 });
10 | expect(Duration.isDuration(dur)).toBe(true);
11 | });
12 |
13 | test("Duration#isDuration return true for invalid duration", () => {
14 | const dur = Duration.invalid("because");
15 | expect(Duration.isDuration(dur)).toBe(true);
16 | });
17 |
18 | test("Duration#isDuration return false for primitives", () => {
19 | expect(Duration.isDuration({})).toBe(false);
20 | expect(Duration.isDuration({ hours: 60 })).toBe(false);
21 | expect(Duration.isDuration(1)).toBe(false);
22 | expect(Duration.isDuration(1.1)).toBe(false);
23 | expect(Duration.isDuration("")).toBe(false);
24 | expect(Duration.isDuration(null)).toBe(false);
25 | expect(Duration.isDuration()).toBe(false);
26 | });
27 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build-and-test:
11 | runs-on: ubuntu-latest
12 |
13 | env:
14 | LANG: en_US.utf8
15 | LIMIT_JEST: yes
16 | TZ: America/New_York
17 |
18 | strategy:
19 | matrix:
20 | node-version:
21 | - 18.20.8 # latest 18.x
22 | - 20.19.4 # latest 20.x
23 | - 22.17.1 # latest 22.x
24 | - 24.4.1 # latest 22.x
25 |
26 | steps:
27 | - uses: actions/checkout@v3
28 | - name: Use Node.js ${{ matrix.node-version }}
29 | uses: actions/setup-node@v3
30 | with:
31 | node-version: ${{ matrix.node-version }}
32 | cache: "npm"
33 | - run: npm ci
34 | - run: npm run build
35 | - run: npm run format-check
36 | - run: npm run test
37 | - run: npm run site
38 | - run: bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/test/interval/localeWeek.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime, Interval } from "../../src/luxon";
4 |
5 | //------
6 | // .count() with useLocaleWeeks
7 | //------
8 | test("count(weeks) with useLocaleWeeks adheres to the locale", () => {
9 | const start = DateTime.fromISO("2023-06-04T13:00:00Z", { setZone: true, locale: "en-US" });
10 | const end = DateTime.fromISO("2023-06-23T13:00:00Z", { setZone: true, locale: "en-US" });
11 | const interval = Interval.fromDateTimes(start, end);
12 |
13 | expect(interval.count("weeks", { useLocaleWeeks: true })).toBe(3);
14 | });
15 |
16 | test("count(weeks) with useLocaleWeeks uses the start locale", () => {
17 | const start = DateTime.fromISO("2023-06-04T13:00:00Z", { setZone: true, locale: "de-DE" });
18 | const end = DateTime.fromISO("2023-06-23T13:00:00Z", { setZone: true, locale: "en-US" });
19 | const interval = Interval.fromDateTimes(start, end);
20 |
21 | expect(interval.count("weeks", { useLocaleWeeks: true })).toBe(4);
22 | });
23 |
--------------------------------------------------------------------------------
/src/zones/invalidZone.js:
--------------------------------------------------------------------------------
1 | import Zone from "../zone.js";
2 |
3 | /**
4 | * A zone that failed to parse. You should never need to instantiate this.
5 | * @implements {Zone}
6 | */
7 | export default class InvalidZone extends Zone {
8 | constructor(zoneName) {
9 | super();
10 | /** @private */
11 | this.zoneName = zoneName;
12 | }
13 |
14 | /** @override **/
15 | get type() {
16 | return "invalid";
17 | }
18 |
19 | /** @override **/
20 | get name() {
21 | return this.zoneName;
22 | }
23 |
24 | /** @override **/
25 | get isUniversal() {
26 | return false;
27 | }
28 |
29 | /** @override **/
30 | offsetName() {
31 | return null;
32 | }
33 |
34 | /** @override **/
35 | formatOffset() {
36 | return "";
37 | }
38 |
39 | /** @override **/
40 | offset() {
41 | return NaN;
42 | }
43 |
44 | /** @override **/
45 | equals() {
46 | return false;
47 | }
48 |
49 | /** @override **/
50 | get isValid() {
51 | return false;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/site/plugins/dark-theme-toggle.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const darkThemeTogglePlugin = (hook, vm) => {
3 | const TOGGLE_ID = "docsify-dark-theme-toggle",
4 | dom = Docsify.dom,
5 | darkThemeStyleSheet = dom.find('link[href$="dark.css"]'),
6 | toggleEl = dom.create("div", ""),
7 | applyTheme = (swap = false) => {
8 | const isDark = Boolean(swap ^ (localStorage[TOGGLE_ID] == "true"));
9 | localStorage[TOGGLE_ID] = isDark;
10 | darkThemeStyleSheet.disabled = !isDark;
11 | dom.toggleClass(dom.body, isDark ? "add" : "remove", "dark");
12 | };
13 | localStorage[TOGGLE_ID] ??= matchMedia("(prefers-color-scheme: dark)").matches;
14 | toggleEl.id = TOGGLE_ID;
15 | dom.on(toggleEl, "click", () => applyTheme(true));
16 | hook.init(applyTheme);
17 | hook.doneEach(() => dom.before(dom.find(".cover.show, .sidebar > .app-name"), toggleEl));
18 | };
19 | $docsify ??= {};
20 | $docsify.plugins = [...($docsify.plugins ?? []), darkThemeTogglePlugin];
21 | })();
22 |
--------------------------------------------------------------------------------
/test/datetime/info.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 |
5 | const dateTime = DateTime.fromJSDate(new Date(1982, 4, 25, 9, 23, 54, 123));
6 |
7 | //------
8 | // #toObject
9 | //-------
10 | test("DateTime#toObject returns the object", () => {
11 | expect(dateTime.toObject()).toEqual({
12 | year: 1982,
13 | month: 5,
14 | day: 25,
15 | hour: 9,
16 | minute: 23,
17 | second: 54,
18 | millisecond: 123,
19 | });
20 | });
21 |
22 | test("DateTime#toObject accepts a flag to return config", () => {
23 | expect(dateTime.toObject({ includeConfig: true })).toEqual({
24 | year: 1982,
25 | month: 5,
26 | day: 25,
27 | hour: 9,
28 | minute: 23,
29 | second: 54,
30 | millisecond: 123,
31 | locale: "en-US",
32 | numberingSystem: null,
33 | outputCalendar: null,
34 | });
35 | });
36 |
37 | test("DateTime#toObject returns an empty object for invalid DateTimes", () => {
38 | expect(DateTime.invalid("because").toObject()).toEqual({});
39 | });
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2019 JS Foundation and other contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/test/duration/accuracy.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | const convert = (amt, from, to, accuracy) =>
6 | Duration.fromObject({ [from]: amt }, { conversionAccuracy: accuracy }).as(to);
7 |
8 | test("There are slightly more than 365 days in a year", () => {
9 | expect(convert(1, "years", "days", "casual")).toBeCloseTo(365, 4);
10 | expect(convert(1, "years", "days", "longterm")).toBeCloseTo(365.2425, 4);
11 |
12 | expect(convert(365, "days", "years", "casual")).toBeCloseTo(1, 4);
13 | expect(convert(365.2425, "days", "years", "longterm")).toBeCloseTo(1, 4);
14 | });
15 |
16 | test("There are slightly more than 30 days in a month", () => {
17 | expect(convert(1, "month", "days", "casual")).toBeCloseTo(30, 4);
18 | expect(convert(1, "month", "days", "longterm")).toBeCloseTo(30.4369, 4);
19 | });
20 |
21 | test("There are slightly more than 91 days in a quarter", () => {
22 | expect(convert(1, "quarter", "days", "casual")).toBeCloseTo(91, 4);
23 | expect(convert(1, "quarter", "days", "longterm")).toBeCloseTo(91.3106, 4);
24 | });
25 |
--------------------------------------------------------------------------------
/docs/home.md:
--------------------------------------------------------------------------------
1 | ## Luxon
2 |
3 | Luxon is a library for dealing with dates and times in JavaScript.
4 |
5 | ```js
6 | DateTime.now().setZone('America/New_York').minus({weeks:1}).endOf('day').toISO();
7 | ```
8 |
9 | ### Features
10 |
11 | * A nice API for working with datetimes
12 | * Interval support (from time x to time y)
13 | * Duration support (14 days, 5 minutes, 33 seconds)
14 | * [Parsing](parsing.md) and [Formatting](formatting.md) datetimes, intervals, and durations
15 | * [Internationalization](intl.md) of strings using the Intl API
16 | * Detailed and unambiguous [math](math.md) operations
17 | * Built-in handling of [time zones](zones.md)
18 | * Partial support for multiple [calendar systems](calendars.md)
19 |
20 | For more, see the docs on the left, including the [api docs](api-docs/index.html ':ignore')
21 |
22 | ### Getting started
23 |
24 | * [Demo](https://moment.github.io/luxon/demo/global.html ':ignore')
25 | * Read the [quick tour](tour.md)
26 | * Browse the topic docs on the left
27 | * Read the [api docs](api-docs/index.html ':ignore')
28 |
29 | Logo by [John Dalziel](https://github.com/crashposition)
30 |
--------------------------------------------------------------------------------
/test/duration/set.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Duration } from "../../src/luxon";
3 |
4 | //------
5 | // years/months/days/hours/minutes/seconds/milliseconds
6 | //-------
7 | const dur = () =>
8 | Duration.fromObject({
9 | years: 1,
10 | months: 1,
11 | days: 1,
12 | hours: 1,
13 | minutes: 1,
14 | seconds: 1,
15 | milliseconds: 1,
16 | });
17 |
18 | test("Duration#set() sets the values", () => {
19 | expect(dur().set({ years: 2 }).years).toBe(2);
20 | expect(dur().set({ months: 2 }).months).toBe(2);
21 | expect(dur().set({ days: 2 }).days).toBe(2);
22 | expect(dur().set({ hours: 4 }).hours).toBe(4);
23 | expect(dur().set({ hours: 4.5 }).hours).toBe(4.5);
24 | expect(dur().set({ minutes: 16 }).minutes).toBe(16);
25 | expect(dur().set({ seconds: 45 }).seconds).toBe(45);
26 | expect(dur().set({ milliseconds: 86 }).milliseconds).toBe(86);
27 | });
28 |
29 | test("Duration#set() throws for metadata", () => {
30 | expect(() => dur.set({ locale: "be" })).toThrow();
31 | expect(() => dur.set({ numberingSystem: "thai" })).toThrow();
32 | expect(() => dur.set({ invalid: 42 })).toThrow();
33 | });
34 |
35 | test("Duration#set maintains invalidity", () => {
36 | expect(Duration.invalid("because").set({ hours: 200 }).isValid).toBe(false);
37 | });
38 |
--------------------------------------------------------------------------------
/src/errors.js:
--------------------------------------------------------------------------------
1 | // these aren't really private, but nor are they really useful to document
2 |
3 | /**
4 | * @private
5 | */
6 | class LuxonError extends Error {}
7 |
8 | /**
9 | * @private
10 | */
11 | export class InvalidDateTimeError extends LuxonError {
12 | constructor(reason) {
13 | super(`Invalid DateTime: ${reason.toMessage()}`);
14 | }
15 | }
16 |
17 | /**
18 | * @private
19 | */
20 | export class InvalidIntervalError extends LuxonError {
21 | constructor(reason) {
22 | super(`Invalid Interval: ${reason.toMessage()}`);
23 | }
24 | }
25 |
26 | /**
27 | * @private
28 | */
29 | export class InvalidDurationError extends LuxonError {
30 | constructor(reason) {
31 | super(`Invalid Duration: ${reason.toMessage()}`);
32 | }
33 | }
34 |
35 | /**
36 | * @private
37 | */
38 | export class ConflictingSpecificationError extends LuxonError {}
39 |
40 | /**
41 | * @private
42 | */
43 | export class InvalidUnitError extends LuxonError {
44 | constructor(unit) {
45 | super(`Invalid unit ${unit}`);
46 | }
47 | }
48 |
49 | /**
50 | * @private
51 | */
52 | export class InvalidArgumentError extends LuxonError {}
53 |
54 | /**
55 | * @private
56 | */
57 | export class ZoneIsAbstractError extends LuxonError {
58 | constructor() {
59 | super("Zone is an abstract class");
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/impl/zoneUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @private
3 | */
4 |
5 | import Zone from "../zone.js";
6 | import IANAZone from "../zones/IANAZone.js";
7 | import FixedOffsetZone from "../zones/fixedOffsetZone.js";
8 | import InvalidZone from "../zones/invalidZone.js";
9 |
10 | import { isUndefined, isString, isNumber } from "./util.js";
11 | import SystemZone from "../zones/systemZone.js";
12 |
13 | export function normalizeZone(input, defaultZone) {
14 | let offset;
15 | if (isUndefined(input) || input === null) {
16 | return defaultZone;
17 | } else if (input instanceof Zone) {
18 | return input;
19 | } else if (isString(input)) {
20 | const lowered = input.toLowerCase();
21 | if (lowered === "default") return defaultZone;
22 | else if (lowered === "local" || lowered === "system") return SystemZone.instance;
23 | else if (lowered === "utc" || lowered === "gmt") return FixedOffsetZone.utcInstance;
24 | else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input);
25 | } else if (isNumber(input)) {
26 | return FixedOffsetZone.instance(input);
27 | } else if (typeof input === "object" && "offset" in input && typeof input.offset === "function") {
28 | // This is dumb, but the instanceof check above doesn't seem to really work
29 | // so we're duck checking it
30 | return input;
31 | } else {
32 | return new InvalidZone(input);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/datetime/reconfigure.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 |
5 | const dt = DateTime.fromObject(
6 | {},
7 | {
8 | locale: "fr",
9 | numberingSystem: "beng",
10 | outputCalendar: "coptic",
11 | }
12 | );
13 |
14 | //------
15 | // #reconfigure()
16 | //------
17 | test("DateTime#reconfigure() sets the locale", () => {
18 | const recon = dt.reconfigure({ locale: "it" });
19 | expect(recon.locale).toBe("it");
20 | expect(recon.numberingSystem).toBe("beng");
21 | expect(recon.outputCalendar).toBe("coptic");
22 | });
23 |
24 | test("DateTime#reconfigure() sets the outputCalendar", () => {
25 | const recon = dt.reconfigure({ outputCalendar: "ethioaa" });
26 | expect(recon.locale).toBe("fr");
27 | expect(recon.numberingSystem).toBe("beng");
28 | expect(recon.outputCalendar).toBe("ethioaa");
29 | });
30 |
31 | test("DateTime#reconfigure() sets the numberingSystem", () => {
32 | const recon = dt.reconfigure({ numberingSystem: "thai" });
33 | expect(recon.locale).toBe("fr");
34 | expect(recon.numberingSystem).toBe("thai");
35 | expect(recon.outputCalendar).toBe("coptic");
36 | });
37 |
38 | test("DateTime#reconfigure() with no arguments no opts", () => {
39 | const recon = dt.reconfigure();
40 | expect(recon.locale).toBe("fr");
41 | expect(recon.numberingSystem).toBe("beng");
42 | expect(recon.outputCalendar).toBe("coptic");
43 | });
44 |
--------------------------------------------------------------------------------
/src/zones/systemZone.js:
--------------------------------------------------------------------------------
1 | import { formatOffset, parseZoneInfo } from "../impl/util.js";
2 | import Zone from "../zone.js";
3 |
4 | let singleton = null;
5 |
6 | /**
7 | * Represents the local zone for this JavaScript environment.
8 | * @implements {Zone}
9 | */
10 | export default class SystemZone extends Zone {
11 | /**
12 | * Get a singleton instance of the local zone
13 | * @return {SystemZone}
14 | */
15 | static get instance() {
16 | if (singleton === null) {
17 | singleton = new SystemZone();
18 | }
19 | return singleton;
20 | }
21 |
22 | /** @override **/
23 | get type() {
24 | return "system";
25 | }
26 |
27 | /** @override **/
28 | get name() {
29 | return new Intl.DateTimeFormat().resolvedOptions().timeZone;
30 | }
31 |
32 | /** @override **/
33 | get isUniversal() {
34 | return false;
35 | }
36 |
37 | /** @override **/
38 | offsetName(ts, { format, locale }) {
39 | return parseZoneInfo(ts, format, locale);
40 | }
41 |
42 | /** @override **/
43 | formatOffset(ts, format) {
44 | return formatOffset(this.offset(ts), format);
45 | }
46 |
47 | /** @override **/
48 | offset(ts) {
49 | return -new Date(ts).getTimezoneOffset();
50 | }
51 |
52 | /** @override **/
53 | equals(otherZone) {
54 | return otherZone.type === "system";
55 | }
56 |
57 | /** @override **/
58 | get isValid() {
59 | return true;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/duration/customMatrix.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 | import { casualMatrix } from "../../src/duration";
5 |
6 | const businessMatrix = {
7 | ...casualMatrix,
8 | months: {
9 | weeks: 4,
10 | days: 22,
11 | hours: 22 * 7,
12 | minutes: 22 * 7 * 60,
13 | seconds: 22 * 7 * 60 * 60,
14 | milliseconds: 22 * 7 * 60 * 60 * 1000,
15 | },
16 | weeks: {
17 | days: 5,
18 | hours: 5 * 7,
19 | minutes: 5 * 7 * 60,
20 | seconds: 5 * 7 * 60 * 60,
21 | milliseconds: 5 * 7 * 60 * 60 * 1000,
22 | },
23 | days: {
24 | hours: 7,
25 | minutes: 7 * 60,
26 | seconds: 7 * 60 * 60,
27 | milliseconds: 7 * 60 * 60 * 1000,
28 | },
29 | };
30 |
31 | const convert = (amt, from, to) =>
32 | Duration.fromObject({ [from]: amt }, { matrix: businessMatrix }).as(to);
33 |
34 | test("One day is made of 7 hours", () => {
35 | expect(convert(1, "days", "hours")).toBeCloseTo(7, 4);
36 | expect(convert(7, "hours", "days")).toBeCloseTo(1, 4);
37 | });
38 |
39 | test("One and a half week is made of 7 days 3 hours and 30 minutes", () => {
40 | const dur = Duration.fromObject({ weeks: 1.5 }, { matrix: businessMatrix }).shiftTo(
41 | "days",
42 | "hours",
43 | "minutes"
44 | );
45 |
46 | expect(dur.days).toBeCloseTo(7, 4);
47 | expect(dur.hours).toBeCloseTo(3, 4);
48 | expect(dur.minutes).toBeCloseTo(30, 4);
49 | });
50 |
--------------------------------------------------------------------------------
/test/duration/reconfigure.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | const dur = Duration.fromObject(
6 | {
7 | years: 1,
8 | months: 2,
9 | days: 3,
10 | },
11 | {
12 | locale: "fr",
13 | numberingSystem: "beng",
14 | conversionAccuracy: "longterm",
15 | }
16 | );
17 |
18 | //------
19 | // #reconfigure()
20 | //------
21 |
22 | test("Duration#reconfigure() sets the locale", () => {
23 | const recon = dur.reconfigure({ locale: "it" });
24 | expect(recon.locale).toBe("it");
25 | expect(recon.numberingSystem).toBe("beng");
26 | expect(recon.conversionAccuracy).toBe("longterm");
27 | });
28 |
29 | test("Duration#reconfigure() sets the numberingSystem", () => {
30 | const recon = dur.reconfigure({ numberingSystem: "thai" });
31 | expect(recon.locale).toBe("fr");
32 | expect(recon.numberingSystem).toBe("thai");
33 | expect(recon.conversionAccuracy).toBe("longterm");
34 | });
35 |
36 | test("Duration#reconfigure() sets the conversion accuracy", () => {
37 | const recon = dur.reconfigure({ conversionAccuracy: "casual" });
38 | expect(recon.locale).toBe("fr");
39 | expect(recon.numberingSystem).toBe("beng");
40 | expect(recon.conversionAccuracy).toBe("casual");
41 | });
42 |
43 | test("Duration#reconfigure() with no arguments does nothing", () => {
44 | const recon = dur.reconfigure();
45 | expect(recon.locale).toBe("fr");
46 | expect(recon.numberingSystem).toBe("beng");
47 | expect(recon.conversionAccuracy).toBe("longterm");
48 | });
49 |
--------------------------------------------------------------------------------
/test/datetime/equality.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 |
5 | test("equals self", () => {
6 | const l = DateTime.now();
7 | expect(l.equals(l)).toBe(true);
8 | });
9 |
10 | test("equals identically constructed", () => {
11 | const l1 = DateTime.local(2017, 5, 15),
12 | l2 = DateTime.local(2017, 5, 15);
13 | expect(l1.equals(l2)).toBe(true);
14 | });
15 |
16 | test("does not equal a different zone", () => {
17 | const l1 = DateTime.local(2017, 5, 15).setZone("America/New_York"),
18 | l2 = DateTime.local(2017, 5, 15).setZone("America/Los_Angeles");
19 | expect(l1.equals(l2)).toBe(false);
20 | });
21 |
22 | test("does not equal an invalid DateTime", () => {
23 | const l1 = DateTime.local(2017, 5, 15),
24 | l2 = DateTime.invalid("whatever");
25 | expect(l1.equals(l2)).toBe(false);
26 | });
27 |
28 | test("does not equal a different locale", () => {
29 | const l1 = DateTime.local(2017, 5, 15),
30 | l2 = DateTime.local(2017, 5, 15).setLocale("fr");
31 | expect(l1.equals(l2)).toBe(false);
32 | });
33 |
34 | test("does not equal a different numbering system", () => {
35 | const l1 = DateTime.local(2017, 5, 15),
36 | l2 = DateTime.local(2017, 5, 15).reconfigure({ numberingSystem: "beng" });
37 | expect(l1.equals(l2)).toBe(false);
38 | });
39 |
40 | test("does not equal a different output calendar", () => {
41 | const l1 = DateTime.local(2017, 5, 15),
42 | l2 = DateTime.local(2017, 5, 15).reconfigure({ outputCalendar: "islamic" });
43 | expect(l1.equals(l2)).toBe(false);
44 | });
45 |
--------------------------------------------------------------------------------
/site/plugins/dark-theme-toggle.css:
--------------------------------------------------------------------------------
1 | #docsify-dark-theme-toggle {
2 | position: absolute;
3 | display: inline-block;
4 | width: 52px;
5 | height: 28px;
6 | margin-left: 2rem;
7 | margin-top: 1.5rem;
8 | left: 0;
9 | top: 0;
10 | z-index: 0;
11 | cursor: pointer;
12 | }
13 | .sidebar > .app-name {
14 | position: relative;
15 | }
16 | .app-name > #docsify-dark-theme-toggle {
17 | margin-right: 1rem;
18 | margin-top: 0;
19 | right: 0;
20 | left: unset;
21 | }
22 | #docsify-dark-theme-toggle::before, #docsify-dark-theme-toggle::after {
23 | position: absolute;
24 | top: 0.1em;
25 | font-size: 16px;
26 | transition: opacity 0.3s;
27 | }
28 | #docsify-dark-theme-toggle::before {
29 | content: "🌙";
30 | left: 0.1em;
31 | opacity: 0;
32 | z-index: 1;
33 | }
34 | #docsify-dark-theme-toggle::after {
35 | content: "🌞";
36 | right: 0.1em;
37 | opacity: 1;
38 | }
39 | #docsify-dark-theme-toggle > span {
40 | position: absolute;
41 | top: 0;
42 | left: 0;
43 | right: 0;
44 | bottom: 0;
45 | background-color: var(--theme-color, #42b983);
46 | border-radius: 28px;
47 | transition: background-color 0.3s;
48 | }
49 | #docsify-dark-theme-toggle > span::before {
50 | content: "";
51 | position: absolute;
52 | height: 22px;
53 | width: 22px;
54 | left: 3px;
55 | top: 3px;
56 | z-index: 1;
57 | background-color: white;
58 | border-radius: 50%;
59 | transition: transform 0.3s;
60 | }
61 | .dark #docsify-dark-theme-toggle::after {
62 | opacity: 0;
63 | }
64 | .dark #docsify-dark-theme-toggle::before {
65 | opacity: 1;
66 | }
67 | .dark #docsify-dark-theme-toggle > span {
68 | background-color: var(--theme-color, #ea6f5a);
69 | }
70 | .dark #docsify-dark-theme-toggle > span::before {
71 | transform: translateX(24px);
72 | }
--------------------------------------------------------------------------------
/site/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | luxon - Immutable date wrapper
6 |
7 |
8 |
9 |
10 |
11 |
12 |
24 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/test/duration/invalid.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration, DateTime, Settings } from "../../src/luxon";
4 |
5 | test("Explicitly invalid durations are invalid", () => {
6 | const dur = Duration.invalid("just because", "seriously, just because");
7 | expect(dur.isValid).toBe(false);
8 | expect(dur.invalidReason).toBe("just because");
9 | expect(dur.invalidExplanation).toBe("seriously, just because");
10 | });
11 |
12 | test("throwOnInvalid throws", () => {
13 | try {
14 | Settings.throwOnInvalid = true;
15 | expect(() => Duration.invalid("because")).toThrow();
16 | } finally {
17 | Settings.throwOnInvalid = false;
18 | }
19 | });
20 |
21 | test("Duration.invalid throws if you don't provide a reason", () => {
22 | expect(() => Duration.invalid()).toThrow();
23 | });
24 |
25 | test("Diffing invalid DateTimes creates invalid Durations", () => {
26 | const invalidDT = DateTime.invalid("so?");
27 | expect(invalidDT.diff(DateTime.now()).isValid).toBe(false);
28 | expect(DateTime.now().diff(invalidDT).isValid).toBe(false);
29 | });
30 |
31 | test("Duration.invalid produces invalid Intervals", () => {
32 | expect(Duration.invalid("because").isValid).toBe(false);
33 | });
34 |
35 | test("Duration.toMillis produces NaN on invalid Durations", () => {
36 | expect(Duration.invalid("because").toMillis()).toBe(NaN);
37 | });
38 |
39 | test("Duration.as produces NaN on invalid Durations", () => {
40 | expect(Duration.invalid("because").as("seconds")).toBe(NaN);
41 | });
42 |
43 | test("Duration.toHuman produces null on invalid Durations", () => {
44 | expect(Duration.invalid("because").toHuman()).toBe("Invalid Duration");
45 | });
46 |
47 | test("Duration.toISO produces null on invalid Durations", () => {
48 | expect(Duration.invalid("because").toISO()).toBeNull();
49 | });
50 |
51 | test("Duration.toFormat produces Invalid Duration on invalid Durations", () => {
52 | expect(Duration.invalid("because").toFormat("s")).toBe("Invalid Duration");
53 | });
54 |
--------------------------------------------------------------------------------
/test/datetime/transform.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 |
5 | const dtMaker = () =>
6 | DateTime.fromObject(
7 | {
8 | year: 1982,
9 | month: 5,
10 | day: 25,
11 | hour: 9,
12 | minute: 23,
13 | second: 54,
14 | millisecond: 123,
15 | },
16 | {
17 | zone: "utc",
18 | }
19 | ),
20 | dt = dtMaker();
21 |
22 | //------
23 | // #toMillis()
24 | //------
25 | test("DateTime#toMillis() returns milliseconds for valid DateTimes", () => {
26 | const js = dt.toJSDate();
27 | expect(dt.toMillis()).toBe(js.getTime());
28 | });
29 |
30 | test("DateTime#toMillis() returns NaN for invalid DateTimes", () => {
31 | const invalid = DateTime.invalid("reason");
32 | expect(invalid.toMillis()).toBe(NaN);
33 | });
34 |
35 | //------
36 | // #toSeconds()
37 | //------
38 | test("DateTime#toSeconds() returns seconds for valid DateTimes", () => {
39 | const js = dt.toJSDate();
40 | expect(dt.toSeconds()).toBe(js.getTime() / 1000);
41 | });
42 |
43 | test("DateTime#toSeconds() returns NaN for invalid DateTimes", () => {
44 | const invalid = DateTime.invalid("reason");
45 | expect(invalid.toSeconds()).toBe(NaN);
46 | });
47 |
48 | //------
49 | // #valueOf()
50 | //------
51 | test("DateTime#valueOf() just does toMillis()", () => {
52 | expect(dt.valueOf()).toBe(dt.toMillis());
53 | const invalid = DateTime.invalid("reason");
54 | expect(invalid.valueOf()).toBe(invalid.toMillis());
55 | });
56 |
57 | //------
58 | // #toJSDate()
59 | //------
60 | test("DateTime#toJSDate() returns a native Date equivalent", () => {
61 | const js = dt.toJSDate();
62 | expect(js).toBeInstanceOf(Date);
63 | expect(js.getTime()).toBe(dt.toMillis());
64 | });
65 |
66 | //------
67 | // #toBSON()
68 | //------
69 | test("DateTime#toBSON() return a BSON serializable equivalent", () => {
70 | const js = dt.toBSON();
71 | expect(js).toBeInstanceOf(Date);
72 | expect(js.getTime()).toBe(dt.toMillis());
73 | });
74 |
--------------------------------------------------------------------------------
/test/duration/getters.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | const dur = Duration.fromObject({
6 | years: 1,
7 | quarters: 2,
8 | months: 2,
9 | days: 3,
10 | hours: 4,
11 | minutes: 5,
12 | seconds: 6,
13 | milliseconds: 7,
14 | weeks: 8,
15 | }),
16 | inv = Duration.invalid("because i say so");
17 |
18 | //------
19 | // years/months/days/hours/minutes/seconds/milliseconds
20 | //------
21 |
22 | test("Duration#years returns the years", () => {
23 | expect(dur.years).toBe(1);
24 | expect(inv.years).toBeFalsy();
25 | });
26 |
27 | test("Duration#quarters returns the quarters", () => {
28 | expect(dur.quarters).toBe(2);
29 | expect(inv.quarters).toBeFalsy();
30 | });
31 |
32 | test("Duration#months returns the (1-indexed) months", () => {
33 | expect(dur.months).toBe(2);
34 | expect(inv.months).toBeFalsy();
35 | });
36 |
37 | test("Duration#days returns the days", () => {
38 | expect(dur.days).toBe(3);
39 | expect(inv.days).toBeFalsy();
40 | });
41 |
42 | test("Duration#hours returns the hours", () => {
43 | expect(dur.hours).toBe(4);
44 | expect(inv.hours).toBeFalsy();
45 | });
46 |
47 | test("Duration#hours returns the fractional hours", () => {
48 | const localDur = Duration.fromObject({
49 | years: 1,
50 | quarters: 2,
51 | months: 2,
52 | days: 3,
53 | hours: 4.5,
54 | minutes: 5,
55 | seconds: 6,
56 | milliseconds: 7,
57 | weeks: 8,
58 | }),
59 | localInv = Duration.invalid("because i say so");
60 |
61 | expect(localDur.hours).toBe(4.5);
62 | expect(localInv.hours).toBeFalsy();
63 | });
64 |
65 | test("Duration#minutes returns the minutes", () => {
66 | expect(dur.minutes).toBe(5);
67 | expect(inv.minutes).toBeFalsy();
68 | });
69 |
70 | test("Duration#seconds returns the seconds", () => {
71 | expect(dur.seconds).toBe(6);
72 | expect(inv.seconds).toBeFalsy();
73 | });
74 |
75 | test("Duration#milliseconds returns the milliseconds", () => {
76 | expect(dur.milliseconds).toBe(7);
77 | expect(inv.milliseconds).toBeFalsy();
78 | });
79 |
80 | test("Duration#weeks returns the weeks", () => {
81 | expect(dur.weeks).toBe(8);
82 | expect(inv.weeks).toBeFalsy();
83 | });
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Luxon
2 |
3 | [![MIT License][license-image]][license] [![Build Status][github-action-image]][github-action-url] [![NPM version][npm-version-image]][npm-url] [![Coverage Status][test-coverage-image]][test-coverage-url] [![PRs welcome][contributing-image]][contributing-url]
4 |
5 | Luxon is a library for working with dates and times in JavaScript.
6 |
7 | ```js
8 | DateTime.now().setZone("America/New_York").minus({ weeks: 1 }).endOf("day").toISO();
9 | ```
10 |
11 | ## Upgrading to 3.0
12 |
13 | [Guide](https://moment.github.io/luxon/#upgrading)
14 |
15 | ## Features
16 | * DateTime, Duration, and Interval types.
17 | * Immutable, chainable, unambiguous API.
18 | * Parsing and formatting for common and custom formats.
19 | * Native time zone and Intl support (no locale or tz files).
20 |
21 | ## Download/install
22 |
23 | [Download/install instructions](https://moment.github.io/luxon/#/install)
24 |
25 | ## Documentation
26 |
27 | * [General documentation](https://moment.github.io/luxon/#/?id=luxon)
28 | * [API docs](https://moment.github.io/luxon/api-docs/index.html)
29 | * [Quick tour](https://moment.github.io/luxon/#/tour)
30 | * [For Moment users](https://moment.github.io/luxon/#/moment)
31 | * [Why does Luxon exist?](https://moment.github.io/luxon/#/why)
32 | * [A quick demo](https://moment.github.io/luxon/demo/global.html)
33 |
34 | ## Development
35 |
36 | See [contributing](CONTRIBUTING.md).
37 |
38 | ![Phasers to stun][phasers-image]
39 |
40 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
41 | [license]: LICENSE.md
42 |
43 | [github-action-image]: https://github.com/moment/luxon/actions/workflows/test.yml/badge.svg
44 | [github-action-url]: https://github.com/moment/luxon/actions/workflows/test.yml
45 |
46 | [npm-url]: https://npmjs.org/package/luxon
47 | [npm-version-image]: https://badge.fury.io/js/luxon.svg
48 |
49 | [test-coverage-url]: https://codecov.io/gh/moment/luxon
50 | [test-coverage-image]: https://codecov.io/gh/moment/luxon/branch/master/graph/badge.svg
51 |
52 | [contributing-url]: https://github.com/moment/luxon/blob/master/CONTRIBUTING.md
53 | [contributing-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
54 |
55 | [phasers-image]: https://img.shields.io/badge/phasers-stun-brightgreen.svg
56 |
--------------------------------------------------------------------------------
/test/info/localeWeek.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Info } from "../../src/luxon";
3 | import { supportsMinDaysInFirstWeek } from "../helpers";
4 |
5 | const Helpers = require("../helpers");
6 |
7 | test("Info.getStartOfWeek reports the correct start of the week", () => {
8 | expect(Info.getStartOfWeek({ locale: "en-US" })).toBe(7);
9 | expect(Info.getStartOfWeek({ locale: "de-DE" })).toBe(1);
10 | });
11 |
12 | Helpers.withoutLocaleWeekInfo("Info.getStartOfWeek reports Monday as the start of the week", () => {
13 | expect(Info.getStartOfWeek({ locale: "en-US" })).toBe(1);
14 | expect(Info.getStartOfWeek({ locale: "de-DE" })).toBe(1);
15 | });
16 |
17 | test("Info.getMinimumDaysInFirstWeek reports the correct value", () => {
18 | expect(Info.getMinimumDaysInFirstWeek({ locale: "en-US" })).toBe(
19 | supportsMinDaysInFirstWeek() ? 1 : 4
20 | );
21 | expect(Info.getMinimumDaysInFirstWeek({ locale: "de-DE" })).toBe(4);
22 | });
23 |
24 | Helpers.withoutLocaleWeekInfo("Info.getMinimumDaysInFirstWeek reports 4", () => {
25 | expect(Info.getMinimumDaysInFirstWeek({ locale: "en-US" })).toBe(4);
26 | expect(Info.getMinimumDaysInFirstWeek({ locale: "de-DE" })).toBe(4);
27 | });
28 |
29 | test("Info.getWeekendWeekdays reports the correct value", () => {
30 | expect(Info.getWeekendWeekdays({ locale: "en-US" })).toStrictEqual([6, 7]);
31 | expect(Info.getWeekendWeekdays({ locale: "he" })).toStrictEqual([5, 6]);
32 | });
33 |
34 | Helpers.withoutLocaleWeekInfo("Info.getWeekendWeekdays reports [6, 7]", () => {
35 | expect(Info.getWeekendWeekdays({ locale: "en-US" })).toStrictEqual([6, 7]);
36 | expect(Info.getWeekendWeekdays({ locale: "he" })).toStrictEqual([6, 7]);
37 | });
38 |
39 | test("Info.getStartOfWeek honors the default locale", () => {
40 | Helpers.withDefaultLocale("en-US", () => {
41 | expect(Info.getStartOfWeek()).toBe(7);
42 | expect(Info.getMinimumDaysInFirstWeek()).toBe(supportsMinDaysInFirstWeek() ? 1 : 4);
43 | expect(Info.getWeekendWeekdays()).toStrictEqual([6, 7]);
44 | });
45 |
46 | Helpers.withDefaultLocale("de-DE", () => {
47 | expect(Info.getStartOfWeek()).toBe(1);
48 | });
49 |
50 | Helpers.withDefaultLocale("he", () => {
51 | expect(Info.getWeekendWeekdays()).toStrictEqual([5, 6]);
52 | });
53 |
54 | Helpers.withDefaultLocale("he", () => {
55 | expect(Info.getWeekendWeekdays()).toStrictEqual([5, 6]);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/zone.js:
--------------------------------------------------------------------------------
1 | import { ZoneIsAbstractError } from "./errors.js";
2 |
3 | /**
4 | * @interface
5 | */
6 | export default class Zone {
7 | /**
8 | * The type of zone
9 | * @abstract
10 | * @type {string}
11 | */
12 | get type() {
13 | throw new ZoneIsAbstractError();
14 | }
15 |
16 | /**
17 | * The name of this zone.
18 | * @abstract
19 | * @type {string}
20 | */
21 | get name() {
22 | throw new ZoneIsAbstractError();
23 | }
24 |
25 | /**
26 | * The IANA name of this zone.
27 | * Defaults to `name` if not overwritten by a subclass.
28 | * @abstract
29 | * @type {string}
30 | */
31 | get ianaName() {
32 | return this.name;
33 | }
34 |
35 | /**
36 | * Returns whether the offset is known to be fixed for the whole year.
37 | * @abstract
38 | * @type {boolean}
39 | */
40 | get isUniversal() {
41 | throw new ZoneIsAbstractError();
42 | }
43 |
44 | /**
45 | * Returns the offset's common name (such as EST) at the specified timestamp
46 | * @abstract
47 | * @param {number} ts - Epoch milliseconds for which to get the name
48 | * @param {Object} opts - Options to affect the format
49 | * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.
50 | * @param {string} opts.locale - What locale to return the offset name in.
51 | * @return {string}
52 | */
53 | offsetName(ts, opts) {
54 | throw new ZoneIsAbstractError();
55 | }
56 |
57 | /**
58 | * Returns the offset's value as a string
59 | * @abstract
60 | * @param {number} ts - Epoch milliseconds for which to get the offset
61 | * @param {string} format - What style of offset to return.
62 | * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
63 | * @return {string}
64 | */
65 | formatOffset(ts, format) {
66 | throw new ZoneIsAbstractError();
67 | }
68 |
69 | /**
70 | * Return the offset in minutes for this zone at the specified timestamp.
71 | * @abstract
72 | * @param {number} ts - Epoch milliseconds for which to compute the offset
73 | * @return {number}
74 | */
75 | offset(ts) {
76 | throw new ZoneIsAbstractError();
77 | }
78 |
79 | /**
80 | * Return whether this Zone is equal to another zone
81 | * @abstract
82 | * @param {Zone} otherZone - the zone to compare
83 | * @return {boolean}
84 | */
85 | equals(otherZone) {
86 | throw new ZoneIsAbstractError();
87 | }
88 |
89 | /**
90 | * Return whether this Zone is valid.
91 | * @abstract
92 | * @type {boolean}
93 | */
94 | get isValid() {
95 | throw new ZoneIsAbstractError();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | /* global test */
2 | import { DateTime, Settings } from "../src/luxon";
3 | import { hasLocaleWeekInfo } from "../src/impl/util";
4 |
5 | exports.withoutRTF = function (name, f) {
6 | const fullName = `With no RelativeTimeFormat support, ${name}`;
7 | test(fullName, () => {
8 | const rtf = Intl.RelativeTimeFormat;
9 | try {
10 | Intl.RelativeTimeFormat = undefined;
11 | Settings.resetCaches();
12 | f();
13 | } finally {
14 | Intl.RelativeTimeFormat = rtf;
15 | }
16 | });
17 | };
18 |
19 | exports.withoutLocaleWeekInfo = function (name, f) {
20 | const fullName = `With no Intl.Locale.weekInfo support, ${name}`;
21 | test(fullName, () => {
22 | const l = Intl.Locale;
23 | try {
24 | Intl.Locale = undefined;
25 | Settings.resetCaches();
26 | f();
27 | } finally {
28 | Intl.Locale = l;
29 | }
30 | });
31 | };
32 |
33 | exports.withNow = function (name, dt, f) {
34 | test(name, () => {
35 | const oldNow = Settings.now;
36 |
37 | try {
38 | Settings.now = () => dt.valueOf();
39 | f();
40 | } finally {
41 | Settings.now = oldNow;
42 | }
43 | });
44 | };
45 |
46 | // not a tester!
47 | exports.withDefaultZone = function (zone, f) {
48 | try {
49 | Settings.defaultZone = zone;
50 | f();
51 | } finally {
52 | Settings.defaultZone = null;
53 | }
54 | };
55 |
56 | exports.withDefaultLocale = function (locale, f) {
57 | try {
58 | Settings.defaultLocale = locale;
59 | f();
60 | } finally {
61 | Settings.defaultLocale = null;
62 | }
63 | };
64 |
65 | exports.setUnset = function (prop) {
66 | return (value, f) => {
67 | const existing = Settings[prop];
68 | try {
69 | Settings[prop] = value;
70 | f();
71 | } finally {
72 | Settings[prop] = existing;
73 | }
74 | };
75 | };
76 |
77 | exports.atHour = function (hour) {
78 | return DateTime.fromObject({ year: 2017, month: 5, day: 25 }).startOf("day").set({ hour });
79 | };
80 |
81 | exports.cldrMajorVersion = function () {
82 | try {
83 | const cldr = process?.versions?.cldr;
84 | if (cldr) {
85 | const match = cldr.match(/^(\d+)\./);
86 | if (match) {
87 | return parseInt(match[1]);
88 | }
89 | }
90 | return null;
91 | } catch {
92 | return null;
93 | }
94 | };
95 |
96 | exports.supportsMinDaysInFirstWeek = function () {
97 | if (!hasLocaleWeekInfo()) return false;
98 | const locale = new Intl.Locale("en-US");
99 | const wi = locale.getWeekInfo?.() ?? locale.weekInfo;
100 | return "minimalDays" in wi;
101 | };
102 |
--------------------------------------------------------------------------------
/src/impl/digits.js:
--------------------------------------------------------------------------------
1 | const numberingSystems = {
2 | arab: "[\u0660-\u0669]",
3 | arabext: "[\u06F0-\u06F9]",
4 | bali: "[\u1B50-\u1B59]",
5 | beng: "[\u09E6-\u09EF]",
6 | deva: "[\u0966-\u096F]",
7 | fullwide: "[\uFF10-\uFF19]",
8 | gujr: "[\u0AE6-\u0AEF]",
9 | hanidec: "[〇|一|二|三|四|五|六|七|八|九]",
10 | khmr: "[\u17E0-\u17E9]",
11 | knda: "[\u0CE6-\u0CEF]",
12 | laoo: "[\u0ED0-\u0ED9]",
13 | limb: "[\u1946-\u194F]",
14 | mlym: "[\u0D66-\u0D6F]",
15 | mong: "[\u1810-\u1819]",
16 | mymr: "[\u1040-\u1049]",
17 | orya: "[\u0B66-\u0B6F]",
18 | tamldec: "[\u0BE6-\u0BEF]",
19 | telu: "[\u0C66-\u0C6F]",
20 | thai: "[\u0E50-\u0E59]",
21 | tibt: "[\u0F20-\u0F29]",
22 | latn: "\\d",
23 | };
24 |
25 | const numberingSystemsUTF16 = {
26 | arab: [1632, 1641],
27 | arabext: [1776, 1785],
28 | bali: [6992, 7001],
29 | beng: [2534, 2543],
30 | deva: [2406, 2415],
31 | fullwide: [65296, 65303],
32 | gujr: [2790, 2799],
33 | khmr: [6112, 6121],
34 | knda: [3302, 3311],
35 | laoo: [3792, 3801],
36 | limb: [6470, 6479],
37 | mlym: [3430, 3439],
38 | mong: [6160, 6169],
39 | mymr: [4160, 4169],
40 | orya: [2918, 2927],
41 | tamldec: [3046, 3055],
42 | telu: [3174, 3183],
43 | thai: [3664, 3673],
44 | tibt: [3872, 3881],
45 | };
46 |
47 | const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split("");
48 |
49 | export function parseDigits(str) {
50 | let value = parseInt(str, 10);
51 | if (isNaN(value)) {
52 | value = "";
53 | for (let i = 0; i < str.length; i++) {
54 | const code = str.charCodeAt(i);
55 |
56 | if (str[i].search(numberingSystems.hanidec) !== -1) {
57 | value += hanidecChars.indexOf(str[i]);
58 | } else {
59 | for (const key in numberingSystemsUTF16) {
60 | const [min, max] = numberingSystemsUTF16[key];
61 | if (code >= min && code <= max) {
62 | value += code - min;
63 | }
64 | }
65 | }
66 | }
67 | return parseInt(value, 10);
68 | } else {
69 | return value;
70 | }
71 | }
72 |
73 | // cache of {numberingSystem: {append: regex}}
74 | const digitRegexCache = new Map();
75 | export function resetDigitRegexCache() {
76 | digitRegexCache.clear();
77 | }
78 |
79 | export function digitRegex({ numberingSystem }, append = "") {
80 | const ns = numberingSystem || "latn";
81 |
82 | let appendCache = digitRegexCache.get(ns);
83 | if (appendCache === undefined) {
84 | appendCache = new Map();
85 | digitRegexCache.set(ns, appendCache);
86 | }
87 | let regex = appendCache.get(append);
88 | if (regex === undefined) {
89 | regex = new RegExp(`${numberingSystems[ns]}${append}`);
90 | appendCache.set(append, regex);
91 | }
92 |
93 | return regex;
94 | }
95 |
--------------------------------------------------------------------------------
/test/datetime/many.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 |
5 | //------
6 | // min
7 | //-------
8 | test("DateTime.min returns the only dateTime if solo", () => {
9 | const m = DateTime.min(DateTime.fromJSDate(new Date(1982, 5, 25)));
10 | expect(m).toBeTruthy();
11 | expect(m.valueOf()).toBe(DateTime.fromJSDate(new Date(1982, 5, 25)).valueOf());
12 | });
13 |
14 | test("DateTime.min returns the min dateTime", () => {
15 | const m = DateTime.min(
16 | DateTime.fromJSDate(new Date(1982, 4, 25)),
17 | DateTime.fromJSDate(new Date(1982, 3, 25)),
18 | DateTime.fromJSDate(new Date(1982, 3, 26))
19 | );
20 | expect(m.valueOf()).toBe(DateTime.fromJSDate(new Date(1982, 3, 25)).valueOf());
21 | });
22 |
23 | test("DateTime.min returns undefined if no argument", () => {
24 | const m = DateTime.min();
25 | expect(m).toBeUndefined();
26 | });
27 |
28 | test("DateTime.min is stable", () => {
29 | const m = DateTime.min(
30 | DateTime.fromJSDate(new Date(1982, 4, 25)),
31 | DateTime.fromJSDate(new Date(1982, 3, 25)).reconfigure({ locale: "en-GB" }),
32 | DateTime.fromJSDate(new Date(1982, 3, 25)).reconfigure({ locale: "en-US" })
33 | );
34 | expect(m.locale).toBe("en-GB");
35 | });
36 |
37 | test("DateTime.min throws if you don't pass it DateTimes", () => {
38 | const dt = DateTime.fromJSDate(new Date(1982, 2, 25));
39 | const notADt = "flob";
40 |
41 | expect(() => DateTime.min(dt, notADt)).toThrow();
42 | expect(() => DateTime.min(notADt)).toThrow();
43 | expect(() => DateTime.min(notADt, notADt)).toThrow();
44 | });
45 |
46 | //------
47 | // max
48 | //-------
49 | test("DateTime.max returns the only dateTime if solo", () => {
50 | const m = DateTime.max(DateTime.fromJSDate(new Date(1982, 5, 25)));
51 | expect(m).toBeTruthy();
52 | expect(m.valueOf()).toBe(DateTime.fromJSDate(new Date(1982, 5, 25)).valueOf());
53 | });
54 |
55 | test("DateTime.max returns the max dateTime", () => {
56 | const m = DateTime.max(
57 | DateTime.fromJSDate(new Date(1982, 5, 25)),
58 | DateTime.fromJSDate(new Date(1982, 3, 25)),
59 | DateTime.fromJSDate(new Date(1982, 3, 26))
60 | );
61 | expect(m.valueOf()).toBe(DateTime.fromJSDate(new Date(1982, 5, 25)).valueOf());
62 | });
63 |
64 | test("DateTime.max returns undefined if no argument", () => {
65 | const m = DateTime.max();
66 | expect(m).toBeUndefined();
67 | });
68 |
69 | test("DateTime.max is stable", () => {
70 | const m = DateTime.max(
71 | DateTime.fromJSDate(new Date(1982, 2, 25)),
72 | DateTime.fromJSDate(new Date(1982, 3, 25)).reconfigure({ locale: "en-GB" }),
73 | DateTime.fromJSDate(new Date(1982, 3, 25)).reconfigure({ locale: "en-US" })
74 | );
75 | expect(m.locale).toBe("en-GB");
76 | });
77 |
78 | test("DateTime.max throws if you don't pass it DateTimes", () => {
79 | const dt = DateTime.fromJSDate(new Date(1982, 2, 25));
80 | const notADt = "flob";
81 |
82 | expect(() => DateTime.max(dt, notADt)).toThrow();
83 | expect(() => DateTime.max(notADt)).toThrow();
84 | expect(() => DateTime.max(notADt, notADt)).toThrow();
85 | });
86 |
--------------------------------------------------------------------------------
/test/duration/equality.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { Duration } from "../../src/luxon";
3 |
4 | test("equals self", () => {
5 | const l = Duration.fromObject({ years: 5, days: 6 });
6 | expect(l.equals(l)).toBe(true);
7 | });
8 |
9 | test("equals identically constructed", () => {
10 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
11 | l2 = Duration.fromObject({ years: 5, days: 6 });
12 | expect(l1.equals(l2)).toBe(true);
13 | });
14 |
15 | test("equals identically constructed with fractional values", () => {
16 | const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
17 | l2 = Duration.fromObject({ years: 5.5, days: 6 });
18 | expect(l1.equals(l2)).toBe(true);
19 | });
20 |
21 | test("equals identically constructed but one has string type values", () => {
22 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
23 | l2 = Duration.fromObject({ years: "5", days: "6" });
24 | expect(l1.equals(l2)).toBe(true);
25 | });
26 |
27 | test("equals identically constructed but one has fractional string type values", () => {
28 | const l1 = Duration.fromObject({ years: 5.5, days: 6 }),
29 | l2 = Duration.fromObject({ years: "5.5", days: "6" });
30 | expect(l1.equals(l2)).toBe(true);
31 | });
32 |
33 | // #809
34 | test("equals with extra zero units", () => {
35 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
36 | l2 = Duration.fromObject({ years: 5, days: 6, minutes: 0, seconds: -0 });
37 | expect(l1.equals(l2)).toBe(true);
38 | expect(l2.equals(l1)).toBe(true);
39 | });
40 |
41 | test("does not equal an invalid duration", () => {
42 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
43 | l2 = Duration.invalid("because");
44 | expect(l1.equals(l2)).toBe(false);
45 | });
46 |
47 | test("does not equal a different locale", () => {
48 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
49 | l2 = Duration.fromObject({ years: 5, days: 6 }).reconfigure({ locale: "fr" });
50 | expect(l1.equals(l2)).toBe(false);
51 | });
52 |
53 | test("does not equal a different numbering system", () => {
54 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
55 | l2 = Duration.fromObject({ years: 5, days: 6 }).reconfigure({ numberingSystem: "beng" });
56 | expect(l1.equals(l2)).toBe(false);
57 | });
58 |
59 | test("does not equal a different set of units", () => {
60 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
61 | l2 = Duration.fromObject({ years: 5, months: 6 });
62 | expect(l1.equals(l2)).toBe(false);
63 | });
64 |
65 | test("does not equal a subset of units", () => {
66 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
67 | l2 = Duration.fromObject({ years: 5 });
68 | expect(l1.equals(l2)).toBe(false);
69 | });
70 |
71 | test("does not equal a superset of units", () => {
72 | const l1 = Duration.fromObject({ years: 5 }),
73 | l2 = Duration.fromObject({ years: 5, days: 6 });
74 | expect(l1.equals(l2)).toBe(false);
75 | });
76 |
77 | test("does not equal a different unit values", () => {
78 | const l1 = Duration.fromObject({ years: 5, days: 6 }),
79 | l2 = Duration.fromObject({ years: 5, days: 7 });
80 | expect(l1.equals(l2)).toBe(false);
81 | });
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "luxon",
3 | "version": "3.7.1",
4 | "description": "Immutable date wrapper",
5 | "author": "Isaac Cambron",
6 | "keywords": [
7 | "date",
8 | "immutable"
9 | ],
10 | "repository": "https://github.com/moment/luxon",
11 | "exports": {
12 | ".": {
13 | "import": "./build/es6/luxon.mjs",
14 | "require": "./build/node/luxon.js"
15 | },
16 | "./package.json": "./package.json"
17 | },
18 | "scripts": {
19 | "build": "babel-node tasks/buildAll.js",
20 | "build-node": "babel-node tasks/buildNode.js",
21 | "build-global": "babel-node tasks/buildGlobal.js",
22 | "jest": "jest",
23 | "test": "jest --coverage",
24 | "api-docs": "mkdir -p build && documentation build src/luxon.js -f html -o build/api-docs && sed -i.bak 's/<\\/body>/
16 | ```
17 |
18 | You may wish to alias the classes you use:
19 |
20 | ```js
21 | var DateTime = luxon.DateTime;
22 | ```
23 |
24 | ## Node.js
25 |
26 | Supports Node.js 6+. Install via NPM:
27 |
28 | ```
29 | npm install --save luxon
30 | ```
31 |
32 | ```js
33 | const { DateTime } = require("luxon");
34 | ```
35 |
36 | If you want to work with locales, you need ICU support:
37 |
38 | 1. **For Node.js 13+, it comes built-in, no action necessary**
39 | 2. For older versions of Node.js (only 12 is supported), you need to install it yourself:
40 | 1. Install a build of Node.js with full ICU baked in, such as via nvm: nvm install -s --with-intl=full-icu --download=all or brew: brew install node --with-full-icu
41 | 2. Install the ICU data externally and point Node.js to it. The instructions on how to do that are below.
42 |
43 | The instructions for using full-icu as a package are a little confusing. Node.js can't automatically discover that you've installed it, so you need to tell it where to find the data, like this:
44 |
45 | ```
46 | npm install full-icu
47 | node --icu-data-dir=./node_modules/full-icu
48 | ```
49 |
50 | You can also point to the data with an environment var, like this:
51 |
52 | ```
53 | NODE_ICU_DATA="$(pwd)/node_modules/full-icu" node
54 | ```
55 |
56 | ## AMD (System.js, RequireJS, etc)
57 |
58 | - [Download full](https://moment.github.io/luxon/amd/luxon.js)
59 | - [Download minified](https://moment.github.io/luxon/amd/luxon.min.js)
60 |
61 | ```js
62 | requirejs(["luxon"], function(luxon) {
63 | //...
64 | });
65 | ```
66 |
67 | ## ES6
68 |
69 | - [Download full](https://moment.github.io/luxon/es6/luxon.js)
70 | - [Download minified](https://moment.github.io/luxon/es6/luxon.min.js)
71 |
72 | ```js
73 | import { DateTime } from "luxon";
74 | ```
75 |
76 | ## Webpack
77 |
78 | ```
79 | npm install --save luxon
80 | ```
81 |
82 | ```js
83 | import { DateTime } from "luxon";
84 | ```
85 |
86 | ## Types
87 |
88 | There are third-party typing files for Flow (via [flow-typed](https://github.com/flowtype/flow-typed)) and TypeScript (via [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)).
89 |
90 | For Flow, use:
91 |
92 | ```
93 | flow-typed install luxon
94 | ```
95 |
96 | For TypeScript, use:
97 |
98 | ```
99 | npm install --save-dev @types/luxon
100 | ```
101 |
102 | ## React Native
103 |
104 | React Native >=0.70 works just fine out of the box. Older versions of React Native for Android (or if you disable Hermes) doesn't include Intl support by default, which you need for [a lot of Luxon's functionality](matrix.md).
105 |
106 | For React Native >=0.60, you should configure the build flavor of jsc in `android/app/build.gradle`:
107 |
108 | ```diff
109 | -def jscFlavor = 'org.webkit:android-jsc:+'
110 | +def jscFlavor = 'org.webkit:android-jsc-intl:+'
111 | ```
112 |
113 | For even older versions of React Native you can use [jsc-android-buildscripts](https://github.com/SoftwareMansion/jsc-android-buildscripts) to fix it.
114 |
--------------------------------------------------------------------------------
/test/datetime/invalid.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime, Settings } from "../../src/luxon";
4 |
5 | const organic1 = DateTime.utc(2014, 13, 33),
6 | // not an actual Wednesday
7 | organic2 = DateTime.fromObject({ weekday: 3, year: 1982, month: 5, day: 25 }, { zone: "UTC" }),
8 | organic3 = DateTime.fromObject({ year: 1982, month: 5, day: 25, hour: 27 }),
9 | organic4 = DateTime.fromObject(
10 | { year: 1982, month: 5, day: 25, hour: 2 },
11 | { zone: "America/Lasers" }
12 | );
13 |
14 | test("Explicitly invalid dates are invalid", () => {
15 | const dt = DateTime.invalid("just because", "seriously, just because");
16 | expect(dt.isValid).toBe(false);
17 | expect(dt.invalidReason).toBe("just because");
18 | expect(dt.invalidExplanation).toBe("seriously, just because");
19 | });
20 |
21 | test("Invalid creations are invalid", () => {
22 | expect(organic1.isValid).toBe(false);
23 | expect(organic2.isValid).toBe(false);
24 | expect(organic3.isValid).toBe(false);
25 | });
26 |
27 | test("invalid zones result in invalid dates", () => {
28 | expect(DateTime.now().setZone("America/Lasers").isValid).toBe(false);
29 | expect(DateTime.now().setZone("America/Lasers").invalidReason).toBe("unsupported zone");
30 |
31 | expect(DateTime.local({ zone: "America/Lasers" }).isValid).toBe(false);
32 | expect(DateTime.local({ zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
33 |
34 | expect(DateTime.local(1982, { zone: "America/Lasers" }).isValid).toBe(false);
35 | expect(DateTime.local(1982, { zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
36 |
37 | expect(DateTime.fromJSDate(new Date(), { zone: "America/Lasers" }).isValid).toBe(false);
38 | expect(DateTime.fromJSDate(new Date(), { zone: "America/Lasers" }).invalidReason).toBe(
39 | "unsupported zone"
40 | );
41 |
42 | expect(DateTime.fromMillis(0, { zone: "America/Lasers" }).isValid).toBe(false);
43 | expect(DateTime.fromMillis(0, { zone: "America/Lasers" }).invalidReason).toBe("unsupported zone");
44 | });
45 |
46 | test("Invalid DateTimes tell you why", () => {
47 | expect(organic1.invalidReason).toBe("unit out of range");
48 | expect(organic2.invalidReason).toBe("mismatched weekday");
49 | expect(organic3.invalidReason).toBe("unit out of range");
50 | expect(organic4.invalidReason).toBe("unsupported zone");
51 | });
52 |
53 | test("Invalid DateTimes can provide an extended explanation", () => {
54 | expect(organic1.invalidExplanation).toBe(
55 | "you specified 13 (of type number) as a month, which is invalid"
56 | );
57 | expect(organic2.invalidExplanation).toBe(
58 | "you can't specify both a weekday of 3 and a date of 1982-05-25T00:00:00.000Z"
59 | );
60 | expect(organic3.invalidExplanation).toBe(
61 | "you specified 27 (of type number) as a hour, which is invalid"
62 | );
63 | });
64 |
65 | test("Invalid DateTimes return invalid Dates", () => {
66 | expect(organic1.toJSDate().valueOf()).toBe(NaN);
67 | });
68 |
69 | test("Diffing invalid DateTimes creates invalid Durations", () => {
70 | expect(organic1.diff(DateTime.now()).isValid).toBe(false);
71 | expect(DateTime.now().diff(organic1).isValid).toBe(false);
72 | });
73 |
74 | test("throwOnInvalid throws", () => {
75 | try {
76 | Settings.throwOnInvalid = true;
77 | expect(() =>
78 | DateTime.fromObject({
79 | weekday: 3,
80 | year: 1982,
81 | month: 5,
82 | day: 25,
83 | })
84 | ).toThrow();
85 | } finally {
86 | Settings.throwOnInvalid = false;
87 | }
88 | });
89 |
90 | test("DateTime.invalid throws if you don't provide a reason", () => {
91 | expect(() => DateTime.invalid()).toThrow();
92 | });
93 |
94 | test("throwOnInvalid throws if year is too big", () => {
95 | try {
96 | Settings.throwOnInvalid = true;
97 | expect(() =>
98 | DateTime.fromObject({
99 | year: 9999999,
100 | month: 5,
101 | day: 25,
102 | })
103 | ).toThrow();
104 | } finally {
105 | Settings.throwOnInvalid = false;
106 | }
107 | });
108 |
--------------------------------------------------------------------------------
/test/info/zones.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import {
4 | Info,
5 | FixedOffsetZone,
6 | IANAZone,
7 | InvalidZone,
8 | SystemZone,
9 | Settings,
10 | } from "../../src/luxon";
11 |
12 | const Helpers = require("../helpers");
13 |
14 | //------
15 | // .hasDST()
16 | //------
17 |
18 | test("Info.hasDST returns true for America/New_York", () => {
19 | expect(Info.hasDST("America/New_York")).toBe(true);
20 | });
21 |
22 | test("Info.hasDST returns false for America/Aruba", () => {
23 | expect(Info.hasDST("America/Aruba")).toBe(false);
24 | });
25 |
26 | test("Info.hasDST returns false for America/Cancun", () => {
27 | expect(Info.hasDST("America/Cancun")).toBe(false);
28 | });
29 |
30 | test("Info.hasDST returns true for Europe/Andora", () => {
31 | expect(Info.hasDST("Europe/Andora")).toBe(true);
32 | });
33 |
34 | test("Info.hasDST defaults to the global zone", () => {
35 | Helpers.withDefaultZone("America/Cancun", () => {
36 | expect(Info.hasDST()).toBe(false);
37 | });
38 | });
39 |
40 | //------
41 | // .isValidIANAZone()
42 | //------
43 |
44 | test("Info.isValidIANAZone returns true for valid zones", () => {
45 | expect(Info.isValidIANAZone("America/Cancun")).toBe(true);
46 | });
47 |
48 | test("Info.isValidIANAZone returns true for single-section zones", () => {
49 | expect(Info.isValidIANAZone("UTC")).toBe(true);
50 | });
51 |
52 | test("Info.isValidIANAZone returns false for junk", () => {
53 | expect(Info.isValidIANAZone("blorp")).toBe(false);
54 | });
55 |
56 | test("Info.isValidIANAZone returns false for well-specified but invalid zones", () => {
57 | expect(Info.isValidIANAZone("America/Blork")).toBe(false);
58 | });
59 |
60 | test("Info.isValidIANAZone returns true for valid zones like America/Indiana/Indianapolis", () => {
61 | expect(Info.isValidIANAZone("America/Indiana/Indianapolis")).toBe(true);
62 | });
63 |
64 | test("Info.isValidIANAZone returns false for well-specified but invalid zones like America/Indiana/Blork", () => {
65 | expect(Info.isValidIANAZone("America/Indiana/Blork")).toBe(false);
66 | });
67 |
68 | //------
69 | // .normalizeZone()
70 | //------
71 |
72 | test("Info.normalizeZone returns Zone objects unchanged", () => {
73 | const fixedOffsetZone = FixedOffsetZone.instance(5);
74 | expect(Info.normalizeZone(fixedOffsetZone)).toBe(fixedOffsetZone);
75 |
76 | const ianaZone = new IANAZone("Europe/Paris");
77 | expect(Info.normalizeZone(ianaZone)).toBe(ianaZone);
78 |
79 | const invalidZone = new InvalidZone("bumblebee");
80 | expect(Info.normalizeZone(invalidZone)).toBe(invalidZone);
81 |
82 | const systemZone = SystemZone.instance;
83 | expect(Info.normalizeZone(systemZone)).toBe(systemZone);
84 | });
85 |
86 | test.each([
87 | ["Local", SystemZone.instance],
88 | ["System", SystemZone.instance],
89 | ["UTC", FixedOffsetZone.utcInstance],
90 | ["GMT", FixedOffsetZone.utcInstance],
91 | ["Etc/GMT+5", new IANAZone("Etc/GMT+5")],
92 | ["Etc/GMT-10", new IANAZone("Etc/GMT-10")],
93 | ["Europe/Paris", new IANAZone("Europe/Paris")],
94 | [0, FixedOffsetZone.utcInstance],
95 | [3, FixedOffsetZone.instance(3)],
96 | [-11, FixedOffsetZone.instance(-11)],
97 | ])("Info.normalizeZone converts valid input %p into valid Zone instance", (input, expected) => {
98 | expect(Info.normalizeZone(input)).toEqual(expected);
99 | });
100 |
101 | test("Info.normalizeZone converts unknown name to invalid Zone", () => {
102 | expect(Info.normalizeZone("bumblebee").isValid).toBe(false);
103 | });
104 |
105 | test("Info.normalizeZone converts null and undefined to default Zone", () => {
106 | expect(Info.normalizeZone(null)).toBe(Settings.defaultZone);
107 | expect(Info.normalizeZone(undefined)).toBe(Settings.defaultZone);
108 | });
109 |
110 | // Local zone no longer refers to default one but behaves as system
111 | // As per Docker Container, zone is America/New_York
112 | test("Info.normalizeZone converts local to system Zone", () => {
113 | expect(Info.normalizeZone("local")).toBe(Settings.defaultZone);
114 | Helpers.withDefaultZone("America/New_York", () => {
115 | expect(Info.normalizeZone("local").name).toBe("America/New_York");
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/docs/matrix.md:
--------------------------------------------------------------------------------
1 | # Support matrix
2 |
3 | This page covers what platforms are supported by Luxon and what caveats apply to them.
4 |
5 | ## Official support
6 |
7 | Luxon officially supports the last two versions of the major browsers, with some caveats. The table below shows which of the not-universally-supported features are available in what environments.
8 |
9 | | Browser | Versions | Intl relative time formatting |
10 | | -------------------------------- | -------- | ----------------------------- |
11 | | Chrome | >= 73 | ✓ |
12 | | Firefox | >= 65 | ✓ |
13 | | Edge | >= 79 | ✓ |
14 | | | 18 | ✗ |
15 | | Safari | >= 14 | ✓ |
16 | | | 13 | ✗ |
17 | | iOS Safari (iOS version numbers) | >= 14 | ✓ |
18 | | Node | >= 12 | ✓ |
19 |
20 | - Those capabilities are explained in the next sections, along with possible polyfill options
21 | - "w/ICU" refers to providing Node with ICU data. See the [install](install.md?id=node) for instructions
22 |
23 | ## Effects of missing features
24 |
25 | **If the platforms you're targeting has all its boxes above check off, ignore this section**.
26 |
27 | In the support table above, you can see that some environments are missing capabilities. In the current version of
28 | Luxon, there's only one partially-supported feature, so this is currently pretty simple. (Older versions of Luxon supported
29 | older browsers, so there were nuanced feature caveats. Newer versions will add more caveats as new browser capabilities
30 | become available and Luxon takes advantage of them if they're present.)
31 |
32 | 1. **Relative time formatting**. Luxon's support for relative time formatting (e.g. `DateTime#toRelative` and `DateTime#toRelativeCalendar`) depends on Intl.RelativeTimeFormat. Luxon will fall back to using English if that capability is missing.
33 |
34 | If the browser lacks these capabilities, Luxon tries its best:
35 |
36 | | Feature | Full support | No relative time format |
37 | | -------------------------------------- | ------------ | ----------------------- |
38 | | Most things | OK | OK |
39 | | `DateTime#toRelative` in en-US | OK | OK |
40 | | `DateTime#toRelative` in other locales | Uses English | Uses English |
41 |
42 |
43 | ## Older platforms
44 |
45 | - **Older versions of both Chrome and Firefox** will most likely work. It's just that I only officially support the last two versions. As you get to older versions of these browsers, the missing capabilities listed above begin to apply to them. (e.g. FF started supporting `formatToParts` in 51 and time zones in 52). I haven't broken that out because it's complicated, Luxon doesn't officially support them, and no one runs them anyway.
46 | - **Older versions of IE** probably won't work at all.
47 | - **Older versions of Node** probably won't work without recompiling Luxon with a different Node target. In which case they'll work with some features missing.
48 |
49 | ## Other platforms
50 |
51 | If the platform you're targeting isn't on the list and you're unsure what caveats apply, you can check which pieces are supported:
52 |
53 | ```js
54 | Info.features(); //=> { relative: false }
55 | ```
56 |
57 | Specific notes on other platforms:
58 |
59 | - **React Native <0.70 on (specifically) Android** doesn't include Intl support by default, so all the possible-to-be-missing capabilities above are unavailable. To fix this on React Native >=0.60, you should configure the build flavor of jsc in `android/app/build.gradle`:
60 |
61 | ```diff
62 | -def jscFlavor = 'org.webkit:android-jsc:+'
63 | +def jscFlavor = 'org.webkit:android-jsc-intl:+'
64 | ```
65 |
66 | For even older versions of React Native you can use [jsc-android-buildscripts](https://github.com/SoftwareMansion/jsc-android-buildscripts) to fix it.
67 |
--------------------------------------------------------------------------------
/docs/upgrading.md:
--------------------------------------------------------------------------------
1 | # Upgrading Luxon
2 |
3 | ## 2.x to 3.0
4 |
5 | Version 3.0 has one breaking change: specifying "system" as the zone always results in the system zone, regardless of what you have the default set to. To get the default zone (whatever it is set to), use "default":
6 |
7 | ```js
8 | Settings.defaultZone = "America/Chicago";
9 |
10 | DateTime.now().setZone("default") // results in Chicago time
11 | DateTime.now().setZone("system") // uses the user's system time
12 | ```
13 |
14 | If this seems obvious, just be aware that it didn't work like that before!
15 |
16 | ## 1.x to 2.0
17 |
18 | Version 2.0 of Luxon has a number of breaking changes.
19 |
20 | ### Environment support
21 |
22 | Luxon 2.0 does not support Node < 12, or any version of IE. It also only supports newer versions of major browsers. This change
23 | allows Luxon to make more assumptions about what's supported in the environment and will allow Luxon's code to simplify. See
24 | the [Support Matrix](matrix.md) for more.
25 |
26 | For this same reason, a polyfilled build is no longer provided; everything Luxon needs comes standard on browsers.
27 |
28 | ### Breaking signature changes
29 |
30 | There are many more specific breaking changes. Most are aimed and making Luxon's handling of option parameters more consistent.
31 |
32 | #### fromObject
33 | `DateTime.fromObject()` and `Duration.fromObject()` now accept two parameters: one for the object and one for the options.
34 |
35 | For example:
36 |
37 | ```js
38 | // Luxon 1.x
39 | DateTime.fromObject({ hour: 3, minute: 2, zone: "America/New_York", locale: "ru" });
40 | Duration.fromObject({ hours: 3, minutes: 2, conversionAccuracy: "casual", locale: "ru" });
41 |
42 | // vs Luxon 2.x
43 | DateTime.fromObject({ hour: 3, minute: 2 }, { zone: "America/New_York", locale: "ru" });
44 | Duration.fromObject({ hours: 3, minutes: 2 }, { conversionAccuracy: "casual", locale: "ru" });
45 | ```
46 |
47 | #### toLocaleString
48 |
49 | In Luxon 1.x, you can mix Intl options with overrides of the DateTime configuration into the same options parameter. These are now
50 | two separate parameters:
51 |
52 | ```js
53 |
54 | // Luxon 1.x
55 | DateTime.now().toLocaleString({ hour: "2-digit", locale: "ru" })
56 |
57 | // vs Luxon 2.x
58 |
59 | DateTime.now().toLocaleString({ hour: "2-digit" }, { locale: "ru" })
60 | ```
61 |
62 | #### System zone
63 |
64 | The zone of the executing environment (e.g. the time set on the computer running the browser running Luxon), is now called
65 | "system" instead of "local" to reduce confusion.
66 |
67 | ```js
68 | DateTime.fromObject({}, { zone: "local" }) // still works
69 | DateTime.fromObject({}, { zone: "system" }) // preferred
70 |
71 | DateTime.fromObject({}, { zone: "system" }).zone // => type is SystemZone
72 | DateTime.fromObject({}, { zone: "system" }).zone.type // => "system"
73 | ```
74 |
75 | #### Default zone
76 |
77 | Luxon 2.x cleans up the handling of `Settings.defaultZone`:
78 |
79 | ```js
80 |
81 | // setting
82 | Settings.defaultZone = "America/New_York"; // can take a string
83 | Settings.defaultZone = IANAZone.create("America/New_York"); // or a Zone instance
84 |
85 | // getting
86 | Settings.defaultZone //=> a Zone instance
87 | ```
88 |
89 | The most significant breaking change here is that `Settings.defaultZoneName` no longer exists.
90 |
91 | #### Other breaking changes
92 |
93 | * `DateTime#toObject` no longer accepts an `includeConfig` option
94 | * `resolvedLocaleOpts` is now `resolvedLocaleOptions`
95 | * `Zone#universal` is now `Zone#isUniversal`
96 |
97 | ### Non-breaking changes
98 |
99 | * `DateTime.local()` and `DateTime.utc()` now take an options parameter for setting zone and locale, same as `fromObject()`.
100 |
101 | ### A note
102 |
103 | We originally had more ambitious plans for Luxon 2.0: a port to Typescript, an overhaul of error handling, and lots of other changes.
104 | The problem is that we're very busy, and in the meantime browsers have evolved quickly, the mistakes in our API bothered a lot
105 | of developers, and our need to support old environments made Luxon more difficult to change. So we made a basic set of changes
106 | to give us some operating room. And hopefully someday we'll get back to those more ambitious plans.
107 |
--------------------------------------------------------------------------------
/src/zones/fixedOffsetZone.js:
--------------------------------------------------------------------------------
1 | import { formatOffset, signedOffset } from "../impl/util.js";
2 | import Zone from "../zone.js";
3 |
4 | let singleton = null;
5 |
6 | /**
7 | * A zone with a fixed offset (meaning no DST)
8 | * @implements {Zone}
9 | */
10 | export default class FixedOffsetZone extends Zone {
11 | /**
12 | * Get a singleton instance of UTC
13 | * @return {FixedOffsetZone}
14 | */
15 | static get utcInstance() {
16 | if (singleton === null) {
17 | singleton = new FixedOffsetZone(0);
18 | }
19 | return singleton;
20 | }
21 |
22 | /**
23 | * Get an instance with a specified offset
24 | * @param {number} offset - The offset in minutes
25 | * @return {FixedOffsetZone}
26 | */
27 | static instance(offset) {
28 | return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset);
29 | }
30 |
31 | /**
32 | * Get an instance of FixedOffsetZone from a UTC offset string, like "UTC+6"
33 | * @param {string} s - The offset string to parse
34 | * @example FixedOffsetZone.parseSpecifier("UTC+6")
35 | * @example FixedOffsetZone.parseSpecifier("UTC+06")
36 | * @example FixedOffsetZone.parseSpecifier("UTC-6:00")
37 | * @return {FixedOffsetZone}
38 | */
39 | static parseSpecifier(s) {
40 | if (s) {
41 | const r = s.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);
42 | if (r) {
43 | return new FixedOffsetZone(signedOffset(r[1], r[2]));
44 | }
45 | }
46 | return null;
47 | }
48 |
49 | constructor(offset) {
50 | super();
51 | /** @private **/
52 | this.fixed = offset;
53 | }
54 |
55 | /**
56 | * The type of zone. `fixed` for all instances of `FixedOffsetZone`.
57 | * @override
58 | * @type {string}
59 | */
60 | get type() {
61 | return "fixed";
62 | }
63 |
64 | /**
65 | * The name of this zone.
66 | * All fixed zones' names always start with "UTC" (plus optional offset)
67 | * @override
68 | * @type {string}
69 | */
70 | get name() {
71 | return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`;
72 | }
73 |
74 | /**
75 | * The IANA name of this zone, i.e. `Etc/UTC` or `Etc/GMT+/-nn`
76 | *
77 | * @override
78 | * @type {string}
79 | */
80 | get ianaName() {
81 | if (this.fixed === 0) {
82 | return "Etc/UTC";
83 | } else {
84 | return `Etc/GMT${formatOffset(-this.fixed, "narrow")}`;
85 | }
86 | }
87 |
88 | /**
89 | * Returns the offset's common name at the specified timestamp.
90 | *
91 | * For fixed offset zones this equals to the zone name.
92 | * @override
93 | */
94 | offsetName() {
95 | return this.name;
96 | }
97 |
98 | /**
99 | * Returns the offset's value as a string
100 | * @override
101 | * @param {number} ts - Epoch milliseconds for which to get the offset
102 | * @param {string} format - What style of offset to return.
103 | * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
104 | * @return {string}
105 | */
106 | formatOffset(ts, format) {
107 | return formatOffset(this.fixed, format);
108 | }
109 |
110 | /**
111 | * Returns whether the offset is known to be fixed for the whole year:
112 | * Always returns true for all fixed offset zones.
113 | * @override
114 | * @type {boolean}
115 | */
116 | get isUniversal() {
117 | return true;
118 | }
119 |
120 | /**
121 | * Return the offset in minutes for this zone at the specified timestamp.
122 | *
123 | * For fixed offset zones, this is constant and does not depend on a timestamp.
124 | * @override
125 | * @return {number}
126 | */
127 | offset() {
128 | return this.fixed;
129 | }
130 |
131 | /**
132 | * Return whether this Zone is equal to another zone (i.e. also fixed and same offset)
133 | * @override
134 | * @param {Zone} otherZone - the zone to compare
135 | * @return {boolean}
136 | */
137 | equals(otherZone) {
138 | return otherZone.type === "fixed" && otherZone.fixed === this.fixed;
139 | }
140 |
141 | /**
142 | * Return whether this Zone is valid:
143 | * All fixed offset zones are valid.
144 | * @override
145 | * @type {boolean}
146 | */
147 | get isValid() {
148 | return true;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/test/interval/create.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { DateTime, Interval, Duration, Settings } from "../../src/luxon";
3 | import Helpers from "../helpers";
4 |
5 | const withThrowOnInvalid = Helpers.setUnset("throwOnInvalid");
6 |
7 | //------
8 | // .fromObject()
9 | //-------
10 | test("Interval.fromDateTimes creates an interval from datetimes", () => {
11 | const start = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
12 | end = DateTime.fromObject({ year: 2016, month: 5, day: 27 }),
13 | int = Interval.fromDateTimes(start, end);
14 |
15 | expect(int.start).toBe(start);
16 | expect(int.end).toBe(end);
17 | });
18 |
19 | test("Interval.fromDateTimes creates an interval from objects", () => {
20 | const start = { year: 2016, month: 5, day: 25 },
21 | end = { year: 2016, month: 5, day: 27 },
22 | int = Interval.fromDateTimes(start, end);
23 |
24 | expect(int.start).toEqual(DateTime.fromObject(start));
25 | expect(int.end).toEqual(DateTime.fromObject(end));
26 | });
27 |
28 | test("Interval.fromDateTimes creates an interval from Dates", () => {
29 | const start = DateTime.fromObject({
30 | year: 2016,
31 | month: 5,
32 | day: 25,
33 | }).toJSDate(),
34 | end = DateTime.fromObject({ year: 2016, month: 5, day: 27 }).toJSDate(),
35 | int = Interval.fromDateTimes(start, end);
36 |
37 | expect(int.start.toJSDate()).toEqual(start);
38 | expect(int.end.toJSDate()).toEqual(end);
39 | });
40 |
41 | test("Interval.fromDateTimes results in an invalid Interval if the endpoints are invalid", () => {
42 | const validDate = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
43 | invalidDate = DateTime.invalid("because");
44 |
45 | expect(Interval.fromDateTimes(validDate, invalidDate).invalidReason).toBe(
46 | "missing or invalid end"
47 | );
48 | expect(Interval.fromDateTimes(invalidDate, validDate).invalidReason).toBe(
49 | "missing or invalid start"
50 | );
51 |
52 | expect(Interval.fromDateTimes(validDate.plus({ days: 1 }), validDate).invalidReason).toBe(
53 | "end before start"
54 | );
55 | });
56 |
57 | test("Interval.fromDateTimes throws with invalid input", () => {
58 | expect(() => Interval.fromDateTimes(DateTime.now(), true)).toThrow();
59 | });
60 |
61 | test("Interval.fromDateTimes throws with start date coming after end date", () => {
62 | const start = DateTime.fromObject({
63 | year: 2016,
64 | month: 5,
65 | day: 25,
66 | }).toJSDate(),
67 | end = DateTime.fromObject({ year: 2016, month: 5, day: 27 }).toJSDate();
68 |
69 | withThrowOnInvalid(true, () => {
70 | expect(() => Interval.fromDateTimes(end, start)).toThrow();
71 | });
72 | });
73 |
74 | //------
75 | // .after()
76 | //-------
77 | test("Interval.after takes a duration", () => {
78 | const start = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
79 | int = Interval.after(start, Duration.fromObject({ days: 3 }));
80 |
81 | expect(int.start).toBe(start);
82 | expect(int.end.day).toBe(28);
83 | });
84 |
85 | test("Interval.after an object", () => {
86 | const start = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
87 | int = Interval.after(start, { days: 3 });
88 |
89 | expect(int.start).toBe(start);
90 | expect(int.end.day).toBe(28);
91 | });
92 |
93 | //------
94 | // .before()
95 | //-------
96 | test("Interval.before takes a duration", () => {
97 | const end = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
98 | int = Interval.before(end, Duration.fromObject({ days: 3 }));
99 |
100 | expect(int.start.day).toBe(22);
101 | expect(int.end).toBe(end);
102 | });
103 |
104 | test("Interval.before takes a number and unit", () => {
105 | const end = DateTime.fromObject({ year: 2016, month: 5, day: 25 }),
106 | int = Interval.before(end, { days: 3 });
107 |
108 | expect(int.start.day).toBe(22);
109 | expect(int.end).toBe(end);
110 | });
111 |
112 | //------
113 | // .invalid()
114 | //-------
115 | test("Interval.invalid produces invalid Intervals", () => {
116 | expect(Interval.invalid("because").isValid).toBe(false);
117 | });
118 |
119 | test("Interval.invalid throws if throwOnInvalid is set", () => {
120 | try {
121 | Settings.throwOnInvalid = true;
122 | expect(() => Interval.invalid("because")).toThrow();
123 | } finally {
124 | Settings.throwOnInvalid = false;
125 | }
126 | });
127 |
128 | test("Interval.invalid throws if no reason is specified", () => {
129 | expect(() => Interval.invalid()).toThrow();
130 | });
131 |
--------------------------------------------------------------------------------
/site/demo/demo.js:
--------------------------------------------------------------------------------
1 | function demo(luxon) {
2 | const DateTime = luxon.DateTime;
3 | const Duration = luxon.Duration;
4 | const Info = luxon.Info;
5 |
6 | const run = function (code) {
7 | let result;
8 | try {
9 | result = eval(code);
10 | } catch (e) {
11 | console.error(e);
12 | return "[error]";
13 | }
14 |
15 | switch (true) {
16 | case result.isValid === false:
17 | return "Invalid";
18 | case result instanceof DateTime:
19 | return "[ DateTime " + result.toISO() + " ]";
20 | case result instanceof Duration:
21 | return "[ Duration " + JSON.stringify(result.toObject()) + " ]";
22 | case result instanceof Date:
23 | return "[ Date " + result.toString() + " ]";
24 | default:
25 | return JSON.stringify(result);
26 | }
27 | };
28 |
29 | const examples = [];
30 | const example = function (code) {
31 | examples.push(
32 | "| " +
33 | code +
34 | " | //=> | " +
35 | run(code) +
36 | " |
"
37 | );
38 | };
39 |
40 | example("Info.features()");
41 | example("DateTime.now()");
42 | example("DateTime.now().toUnixInteger()");
43 | example("DateTime.now().toJSDate()");
44 | example("DateTime.utc().toISO()");
45 | example("DateTime.utc(2017, 5, 15, 17, 36)");
46 | example("DateTime.utc(2017, 5, 15, 17, 36).toLocal()");
47 | example("DateTime.local(2017, 5, 15, 17, 36)");
48 | example("DateTime.local(2017, 5, 15, 17, 36).toUTC()");
49 | example("DateTime.now().toObject()");
50 | example("DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 })");
51 | example(
52 | "DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'America/New_York' })"
53 | );
54 | example(
55 | "DateTime.fromObject({ year: 2017, month: 5, day: 15, hour: 17, minute: 36 }, { zone: 'Asia/Singapore' })"
56 | );
57 | example("DateTime.now().setZone('America/New_York')");
58 | example("DateTime.now().setZone('America/New_York').startOf('day')");
59 | example("DateTime.now().plus({minutes: 15, seconds: 8})");
60 | example("DateTime.now().plus({days: 6})");
61 | example("DateTime.now().minus({days: 6})");
62 | example("DateTime.now().diff(DateTime.local(1982, 5, 25)).milliseconds");
63 | example("DateTime.now().diff(DateTime.local(1982, 5, 25), 'days').days");
64 | example("DateTime.now().diff(DateTime.local(1982, 5, 25), ['days', 'hours'])");
65 | example("DateTime.now().toLocaleString()");
66 | example("DateTime.now().setLocale('zh').toLocaleString()");
67 | example("DateTime.now().toLocaleString(DateTime.DATE_MED)");
68 | example("DateTime.now().setLocale('zh').toLocaleString(DateTime.DATE_MED)");
69 | example("DateTime.now().setLocale('fr').toLocaleString(DateTime.DATE_FULL)");
70 | example("DateTime.fromISO('2017-05-15')");
71 | example("DateTime.fromISO('2017-05-15T17:36')");
72 | example("DateTime.fromISO('2017-W33-4')");
73 | example("DateTime.fromISO('2017-W33-4T04:45:32.343')");
74 | example("DateTime.fromFormat('12-16-2017', 'MM-dd-yyyy')");
75 | example("DateTime.now().toFormat('MM-dd-yyyy')");
76 | example("DateTime.now().toFormat('MMMM dd, yyyy')");
77 | example("DateTime.now().setLocale('fr').toFormat('MMMM dd, yyyy')");
78 | example("DateTime.fromFormat('May 25, 1982', 'MMMM dd, yyyy')");
79 | example("DateTime.fromFormat('mai 25, 1982', 'MMMM dd, yyyy', { locale: 'fr' })");
80 | example("DateTime.now().plus({ days: 1 }).toRelativeCalendar()");
81 | example("DateTime.now().plus({ days: -1 }).toRelativeCalendar()");
82 | example("DateTime.now().plus({ months: 1 }).toRelativeCalendar()");
83 | example("DateTime.now().setLocale('fr').plus({ days: 1 }).toRelativeCalendar()");
84 | example("DateTime.now().setLocale('fr').plus({ days: -1 }).toRelativeCalendar()");
85 | example("DateTime.now().setLocale('fr').plus({ months: 1 }).toRelativeCalendar()");
86 |
87 | let all = "Some Luxon examples
";
88 | all +=
89 | "This is not meant to be a comprehensive showcase of Luxon's capabilities, just a quick flavoring.
";
90 | all += "";
91 | all += examples.join("");
92 | all += "
";
93 |
94 | document.body.innerHTML = all;
95 | }
96 |
97 | if (typeof define !== "undefined") {
98 | define(["luxon"], function (luxon) {
99 | return function () {
100 | demo(luxon);
101 | };
102 | });
103 | } else {
104 | window.demo = demo;
105 | }
106 |
--------------------------------------------------------------------------------
/test/duration/parse.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | //------
6 | // #fromISO()
7 | //------
8 |
9 | const check = (s, ob) => {
10 | expect(Duration.fromISO(s).toObject()).toEqual(ob);
11 | };
12 |
13 | test("Duration.fromISO can parse a variety of ISO formats", () => {
14 | check("P5Y3M", { years: 5, months: 3 });
15 | check("PT54M32S", { minutes: 54, seconds: 32 });
16 | check("P3DT54M32S", { days: 3, minutes: 54, seconds: 32 });
17 | check("P1YT34000S", { years: 1, seconds: 34000 });
18 | check("P1W1DT13H23M34S", { weeks: 1, days: 1, hours: 13, minutes: 23, seconds: 34 });
19 | check("P2W", { weeks: 2 });
20 | check("PT10000000000000000000.999S", { seconds: 10000000000000000000, milliseconds: 999 });
21 | });
22 |
23 | test("Duration.fromISO can parse mixed or negative durations", () => {
24 | check("P-5Y-3M", { years: -5, months: -3 });
25 | check("PT-54M32S", { minutes: -54, seconds: 32 });
26 | check("P-3DT54M-32S", { days: -3, minutes: 54, seconds: -32 });
27 | check("P1YT-34000S", { years: 1, seconds: -34000 });
28 | check("P-1W1DT13H23M34S", { weeks: -1, days: 1, hours: 13, minutes: 23, seconds: 34 });
29 | check("P-2W", { weeks: -2 });
30 | check("-P1D", { days: -1 });
31 | check("-P5Y3M", { years: -5, months: -3 });
32 | check("-P-5Y-3M", { years: 5, months: 3 });
33 | check("-P-1W1DT13H-23M34S", { weeks: 1, days: -1, hours: -13, minutes: 23, seconds: -34 });
34 | check("PT-1.5S", { seconds: -1, milliseconds: -500 });
35 | check("PT-0.5S", { seconds: 0, milliseconds: -500 });
36 | check("PT1.5S", { seconds: 1, milliseconds: 500 });
37 | check("PT0.5S", { seconds: 0, milliseconds: 500 });
38 | });
39 |
40 | test("Duration.fromISO can parse fractions of seconds", () => {
41 | expect(Duration.fromISO("PT54M32.5S").toObject()).toEqual({
42 | minutes: 54,
43 | seconds: 32,
44 | milliseconds: 500,
45 | });
46 | expect(Duration.fromISO("PT54M32.53S").toObject()).toEqual({
47 | minutes: 54,
48 | seconds: 32,
49 | milliseconds: 530,
50 | });
51 | expect(Duration.fromISO("PT54M32.534S").toObject()).toEqual({
52 | minutes: 54,
53 | seconds: 32,
54 | milliseconds: 534,
55 | });
56 | expect(Duration.fromISO("PT54M32.5348S").toObject()).toEqual({
57 | minutes: 54,
58 | seconds: 32,
59 | milliseconds: 534,
60 | });
61 | expect(Duration.fromISO("PT54M32.034S").toObject()).toEqual({
62 | minutes: 54,
63 | seconds: 32,
64 | milliseconds: 34,
65 | });
66 | });
67 |
68 | test("Duration.fromISO can parse fractions", () => {
69 | expect(Duration.fromISO("P1.5Y").toObject()).toEqual({
70 | years: 1.5,
71 | });
72 | expect(Duration.fromISO("P1.5M").toObject()).toEqual({
73 | months: 1.5,
74 | });
75 | expect(Duration.fromISO("P1.5W").toObject()).toEqual({
76 | weeks: 1.5,
77 | });
78 | expect(Duration.fromISO("P1.5D").toObject()).toEqual({
79 | days: 1.5,
80 | });
81 | expect(Duration.fromISO("PT9.5H").toObject()).toEqual({
82 | hours: 9.5,
83 | });
84 | });
85 |
86 | const rejects = (s) => {
87 | expect(Duration.fromISO(s).isValid).toBe(false);
88 | };
89 |
90 | test("Duration.fromISO rejects junk", () => {
91 | rejects("poop");
92 | rejects("PTglorb");
93 | rejects("P5Y34S");
94 | rejects("5Y");
95 | rejects("P34S");
96 | rejects("P34K");
97 | rejects("P5D2W");
98 | });
99 |
100 | //------
101 | // #fromISOTime()
102 | //------
103 |
104 | const checkTime = (s, ob) => {
105 | expect(Duration.fromISOTime(s).toObject()).toEqual(ob);
106 | };
107 |
108 | test("Duration.fromISOTime can parse a variety of extended ISO time formats", () => {
109 | checkTime("11:22:33.444", { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 });
110 | checkTime("11:22:33", { hours: 11, minutes: 22, seconds: 33 });
111 | checkTime("11:22", { hours: 11, minutes: 22, seconds: 0 });
112 | checkTime("T11:22", { hours: 11, minutes: 22, seconds: 0 });
113 | });
114 |
115 | test("Duration.fromISOTime can parse a variety of basic ISO time formats", () => {
116 | checkTime("112233.444", { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 });
117 | checkTime("112233", { hours: 11, minutes: 22, seconds: 33 });
118 | checkTime("1122", { hours: 11, minutes: 22, seconds: 0 });
119 | checkTime("11", { hours: 11, minutes: 0, seconds: 0 });
120 | checkTime("T1122", { hours: 11, minutes: 22, seconds: 0 });
121 | });
122 |
123 | const rejectsTime = (s) => {
124 | expect(Duration.fromISOTime(s).isValid).toBe(false);
125 | };
126 |
127 | test("Duration.fromISOTime rejects junk", () => {
128 | rejectsTime("poop");
129 | rejectsTime("Tglorb");
130 | rejectsTime("-00:00");
131 | });
132 |
--------------------------------------------------------------------------------
/docs/why.md:
--------------------------------------------------------------------------------
1 | # Why does Luxon exist?
2 |
3 | What's the deal with this whole Luxon thing anyway? Why did I write it? How is it related to the Moment project? What's different about it? This page tries to hash all that out.
4 |
5 | ## A disclaimer
6 |
7 | I should clarify here that I'm just one of Moment's maintainers; I'm not in charge and I'm not Moment's creator. The opinions here are solely mine. Finally, none of this is meant to bash Moment, a project I've spent a lot of time on and whose other developers I respect.
8 |
9 | ## Origin
10 |
11 | Luxon started because I had a bunch of ideas on how to improve Moment but kept finding Moment wasn't a good codebase to explore them with. Namely:
12 |
13 | - I wanted to try out some ideas that I thought would provide a better, more explicit API but didn't want to break everything in Moment.
14 | - I had an idea on how to provide out-of-the-box, no-data-files-required support for time zones, but Moment's design made that difficult.
15 | - I wanted to completely rethink how internationalization worked by using the Intl API that comes packaged in browsers.
16 | - I wanted to use a modern JS toolchain, which would require a major retrofit to Moment.
17 |
18 | So I decided to write something from scratch, a sort of modernized Moment. It's a combination of all the things I learned maintaining Moment and Twix, plus a bunch of fresh ideas. I worked on it in little slivers of spare time for about two years. But now it's ready to actually use, and the Moment team likes it enough that we pulled it under the organization's umbrella.
19 |
20 | ## Ideas in Luxon
21 |
22 | Luxon is built around a few core ideas:
23 |
24 | 1. Keep the basic chainable date wrapper idea from Moment.
25 | 1. Make all the types immutable.
26 | 1. Make the API explicit; different methods do different things and have well-defined options.
27 | 1. Use the Intl API to provide internationalization, including token parsing. Fall back to English if the browser doesn't support those APIs.
28 | 1. Abuse the Intl API horribly to provide time zone support. Only possible for modern browsers.
29 | 1. Provide more comprehensive duration support.
30 | 1. Directly provide interval support.
31 | 1. Write inline docs for everything.
32 |
33 | These ideas have some big advantages:
34 |
35 | 1. It's much easier to understand and debug code that uses Luxon.
36 | 1. Using native browser capabilities for internationalization leads to a much better behavior and is dramatically easier to maintain.
37 | 1. Luxon has the best time zone support of any JS date library.
38 | 1. Luxon's durations are both flexible and easy to use.
39 | 1. The documentation is very good.
40 |
41 | They also have some disadvantages:
42 |
43 | 1. Using modern browser capabilities means that the fallback behavior introduces complexity for the programmer.
44 | 1. Never keeping internationalized strings in the code base means that some capabilities have to wait until the browsers provide it.
45 | 1. Some aspects of the Intl API are browser-dependent, which means Luxon's behavior is too.
46 |
47 | ## Place in the Moment project
48 |
49 | Luxon lives in the Moment project because, basically, we all really like it, and it represents a huge improvement.
50 |
51 | But Luxon doesn't quite fulfill Moment's mandate. Since it sometimes relies on browsers' implementations of the `Intl` specifications, it doesn't provide some of Moment's most commonly-used features on all browsers. Relative date formatting is for instance not supported in IE11 and [other older browsers](https://caniuse.com/?search=Intl%20RelativeTimeFormat). Luxon's Intl features do not work as expected on sufficiently outdated browsers, whereas Moment's all work everywhere. That represents a good tradeoff, IMO, but it's clearly a different one than Moment makes.
52 |
53 | Luxon makes a major break in API conventions. Part of Moment's charm is that you just call `moment()` on basically anything and you get date, whereas Luxon forces you to decide that you want to call `fromISO` or whatever. The upshot of all that is that Luxon feels like a different library; that's why it's not Moment 3.0.
54 |
55 | So what is it then? We're not really sure. We're calling it a Moment labs project. Will its ideas get backported into Moment 3? Will it gradually siphon users away from Moment and become the focus of the Moment project? Will the march of modern browsers retire the arguments above and cause us to revisit branding Luxon as Moment? We don't know.
56 |
57 | There, now you know as much as I do.
58 |
59 | ## Future plans
60 |
61 | Luxon is fully usable and I plan to support it indefinitely. It's also largely complete. Luxon will eventually strip out its fallbacks for missing platform features. But overall I expect the core functionality to stay basically as it is, adding mostly minor tweaks and bugfixes.
62 |
--------------------------------------------------------------------------------
/tasks/build.js:
--------------------------------------------------------------------------------
1 | const rollup = require("rollup"),
2 | { babel } = require("@rollup/plugin-babel"),
3 | { terser } = require("rollup-plugin-terser"),
4 | { nodeResolve } = require("@rollup/plugin-node-resolve"),
5 | rollupCommonJS = require("@rollup/plugin-commonjs"),
6 | UglifyJS = require("uglify-js"),
7 | fs = require("fs");
8 |
9 | // For some reason, the minifier is currently producing total giberrish, at least for the global build.
10 | // I've disabled it for now, and will simply uglify externally.
11 | const TRUST_MINIFY = false;
12 |
13 | function rollupInputOpts(opts) {
14 | const presetOpts = {
15 | modules: false,
16 | loose: true,
17 | };
18 |
19 | if (opts.target) {
20 | presetOpts.targets = opts.target;
21 | }
22 |
23 | const inputOpts = {
24 | input: opts.src || "./src/luxon.js",
25 | onwarn: (warning) => {
26 | // I don't care about these for now
27 | if (warning.code !== "CIRCULAR_DEPENDENCY") {
28 | console.warn(`(!) ${warning.message}`);
29 | }
30 | },
31 |
32 | plugins: [
33 | nodeResolve(),
34 | rollupCommonJS({
35 | include: "node_modules/**",
36 | }),
37 | ],
38 | };
39 |
40 | if (opts.compile || typeof opts.compile === "undefined") {
41 | inputOpts.plugins.push(
42 | babel({
43 | babelrc: false,
44 | presets: [["@babel/preset-env", presetOpts]],
45 | babelHelpers: "bundled",
46 | })
47 | );
48 | }
49 |
50 | if (opts.minify && TRUST_MINIFY) {
51 | inputOpts.plugins.push(
52 | terser({
53 | comments: false,
54 | mangle: {
55 | topLevel: !opts.global,
56 | },
57 | })
58 | );
59 | }
60 |
61 | return inputOpts;
62 | }
63 |
64 | function rollupOutputOpts(dest, opts) {
65 | const outputOpts = {
66 | file: `build/${dest}/${opts.filename || "luxon.js"}`,
67 | format: opts.format,
68 | sourcemap: true,
69 | };
70 |
71 | if (opts.name) {
72 | outputOpts.name = opts.name;
73 | }
74 |
75 | return outputOpts;
76 | }
77 |
78 | async function babelAndRollup(dest, opts) {
79 | const inputOpts = rollupInputOpts(opts),
80 | outputOpts = rollupOutputOpts(dest, opts),
81 | bundle = await rollup.rollup(inputOpts);
82 | await bundle.write(outputOpts);
83 | }
84 |
85 | async function buildLibrary(dest, opts) {
86 | console.log("Building", dest);
87 | const promises = [babelAndRollup(dest, opts)];
88 |
89 | const filename = opts.filename || "luxon.js";
90 | const minFilename = filename.replace(/\.(m?js)$/, ".min.$1");
91 |
92 | if (opts.minify && TRUST_MINIFY) {
93 | promises.push(
94 | babelAndRollup(dest, {
95 | ...opts,
96 | minify: true,
97 | filename: minFilename,
98 | })
99 | );
100 | }
101 |
102 | await Promise.all(promises);
103 |
104 | if (opts.minify && !TRUST_MINIFY) {
105 | const code = fs.readFileSync(`build/${dest}/${filename}`, "utf8"),
106 | ugly = UglifyJS.minify(code, {
107 | toplevel: !opts.global,
108 | output: {
109 | comments: false,
110 | },
111 | sourceMap: {
112 | filename: `build/${dest}/${filename}`,
113 | },
114 | });
115 | if (ugly.error) {
116 | console.error("Error uglifying", ugly.error);
117 | } else {
118 | fs.writeFileSync(`build/${dest}/${minFilename}`, ugly.code);
119 | fs.writeFileSync(`build/${dest}/${minFilename}.map`, ugly.map);
120 | }
121 | }
122 | console.log("Built", dest);
123 | }
124 |
125 | const browsersOld = "last 2 major versions";
126 |
127 | async function global() {
128 | await buildLibrary("global", {
129 | format: "iife",
130 | global: true,
131 | name: "luxon",
132 | target: browsersOld,
133 | minify: true,
134 | });
135 | }
136 |
137 | async function amd() {
138 | await buildLibrary("amd", {
139 | format: "amd",
140 | name: "luxon",
141 | target: browsersOld,
142 | minify: true,
143 | });
144 | }
145 |
146 | async function node() {
147 | await buildLibrary("node", { format: "cjs", target: "node 12" });
148 | }
149 |
150 | async function cjsBrowser() {
151 | await buildLibrary("cjs-browser", { format: "cjs", target: browsersOld });
152 | }
153 |
154 | async function es6() {
155 | await buildLibrary("es6", {
156 | format: "es",
157 | minify: true,
158 | compile: false,
159 | filename: "luxon.mjs",
160 | });
161 | }
162 |
163 | async function globalEs6() {
164 | await buildLibrary("global-es6", {
165 | format: "iife",
166 | name: "luxon",
167 | compile: false,
168 | global: true,
169 | });
170 | }
171 |
172 | async function buildAll() {
173 | await Promise.all([node(), cjsBrowser(), es6(), amd(), global(), globalEs6()]);
174 | }
175 |
176 | module.exports = {
177 | buildAll,
178 | buildNode: node,
179 | buildGlobal: global,
180 | };
181 |
--------------------------------------------------------------------------------
/test/duration/create.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | //------
6 | // .fromObject()
7 | //-------
8 | test("Duration.fromObject sets all the values", () => {
9 | const dur = Duration.fromObject({
10 | years: 1,
11 | months: 2,
12 | days: 3,
13 | hours: 4,
14 | minutes: 5,
15 | seconds: 6,
16 | milliseconds: 7,
17 | });
18 | expect(dur.years).toBe(1);
19 | expect(dur.months).toBe(2);
20 | expect(dur.days).toBe(3);
21 | expect(dur.hours).toBe(4);
22 | expect(dur.minutes).toBe(5);
23 | expect(dur.seconds).toBe(6);
24 | expect(dur.milliseconds).toBe(7);
25 | });
26 |
27 | test("Duration.fromObject sets all the fractional values", () => {
28 | const dur = Duration.fromObject({
29 | years: 1,
30 | months: 2,
31 | days: 3,
32 | hours: 4.5,
33 | });
34 | expect(dur.years).toBe(1);
35 | expect(dur.months).toBe(2);
36 | expect(dur.days).toBe(3);
37 | expect(dur.hours).toBe(4.5);
38 | expect(dur.minutes).toBe(0);
39 | expect(dur.seconds).toBe(0);
40 | expect(dur.milliseconds).toBe(0);
41 | });
42 |
43 | test("Duration.fromObject sets all the values from the object having string type values", () => {
44 | const dur = Duration.fromObject({
45 | years: "1",
46 | months: "2",
47 | days: "3",
48 | hours: "4",
49 | minutes: "5",
50 | seconds: "6",
51 | milliseconds: "7",
52 | });
53 | expect(dur.years).toBe(1);
54 | expect(dur.months).toBe(2);
55 | expect(dur.days).toBe(3);
56 | expect(dur.hours).toBe(4);
57 | expect(dur.minutes).toBe(5);
58 | expect(dur.seconds).toBe(6);
59 | expect(dur.milliseconds).toBe(7);
60 | });
61 |
62 | test("Duration.fromObject accepts a conversionAccuracy", () => {
63 | const dur = Duration.fromObject({ days: 1 }, { conversionAccuracy: "longterm" });
64 | expect(dur.conversionAccuracy).toBe("longterm");
65 | });
66 |
67 | test("Duration.fromObject throws if the argument is not an object", () => {
68 | expect(() => Duration.fromObject()).toThrow();
69 | expect(() => Duration.fromObject(null)).toThrow();
70 | expect(() => Duration.fromObject("foo")).toThrow();
71 | });
72 |
73 | test("Duration.fromObject({}) constructs zero duration", () => {
74 | const dur = Duration.fromObject({});
75 | expect(dur.years).toBe(0);
76 | expect(dur.months).toBe(0);
77 | expect(dur.days).toBe(0);
78 | expect(dur.hours).toBe(0);
79 | expect(dur.minutes).toBe(0);
80 | expect(dur.seconds).toBe(0);
81 | expect(dur.milliseconds).toBe(0);
82 | });
83 |
84 | test("Duration.fromObject throws if the initial object has invalid keys", () => {
85 | expect(() => Duration.fromObject({ foo: 0 })).toThrow();
86 | expect(() => Duration.fromObject({ years: 1, foo: 0 })).toThrow();
87 | });
88 |
89 | test("Duration.fromObject throws if the initial object has invalid values", () => {
90 | expect(() => Duration.fromObject({ years: {} })).toThrow();
91 | expect(() => Duration.fromObject({ months: "some" })).toThrow();
92 | expect(() => Duration.fromObject({ days: NaN })).toThrow();
93 | expect(() => Duration.fromObject({ hours: true })).toThrow();
94 | expect(() => Duration.fromObject({ minutes: false })).toThrow();
95 | expect(() => Duration.fromObject({ seconds: "" })).toThrow();
96 | });
97 |
98 | test("Duration.fromObject is valid if providing options only", () => {
99 | const dur = Duration.fromObject({}, { conversionAccuracy: "longterm" });
100 | expect(dur.years).toBe(0);
101 | expect(dur.months).toBe(0);
102 | expect(dur.days).toBe(0);
103 | expect(dur.hours).toBe(0);
104 | expect(dur.minutes).toBe(0);
105 | expect(dur.seconds).toBe(0);
106 | expect(dur.milliseconds).toBe(0);
107 | expect(dur.isValid).toBe(true);
108 | });
109 |
110 | //------
111 | // .fromDurationLike()
112 | //-------
113 |
114 | it("Duration.fromDurationLike returns a Duration from millis", () => {
115 | const dur = Duration.fromDurationLike(1000);
116 | expect(dur).toBeInstanceOf(Duration);
117 | expect(dur).toMatchInlineSnapshot(`"PT1S"`);
118 | });
119 |
120 | it("Duration.fromDurationLike returns a Duration from object", () => {
121 | const dur = Duration.fromDurationLike({ hours: 1 });
122 | expect(dur).toBeInstanceOf(Duration);
123 | expect(dur.toObject()).toStrictEqual({ hours: 1 });
124 | });
125 |
126 | it("Duration.fromDurationLike returns passed Duration", () => {
127 | const durFromObject = Duration.fromObject({ hours: 1 });
128 | const dur = Duration.fromDurationLike(durFromObject);
129 | expect(dur).toStrictEqual(durFromObject);
130 | });
131 |
132 | it("Duration.fromDurationLike throws for invalid inputs", () => {
133 | expect(() => Duration.fromDurationLike("foo")).toThrow();
134 | expect(() => Duration.fromDurationLike(null)).toThrow();
135 | expect(() => Duration.fromDurationLike(Infinity)).toThrow();
136 | expect(() => Duration.fromDurationLike(NaN)).toThrow();
137 | });
138 |
--------------------------------------------------------------------------------
/test/datetime/misc.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { DateTime } from "../../src/luxon";
3 |
4 | // you hate to see a class like this, but here we are
5 |
6 | //------
7 | // #hasSame()
8 | //------
9 |
10 | test("DateTime#hasSame() can use milliseconds for exact comparisons", () => {
11 | const dt = DateTime.now();
12 | expect(dt.hasSame(dt, "millisecond")).toBe(true);
13 | expect(dt.hasSame(dt.reconfigure({ locale: "fr" }), "millisecond")).toBe(true);
14 | expect(dt.hasSame(dt.plus({ milliseconds: 1 }), "millisecond")).toBe(false);
15 | });
16 |
17 | test("DateTime#hasSame() checks the unit", () => {
18 | const dt = DateTime.now();
19 | expect(dt.hasSame(dt, "day")).toBe(true);
20 | expect(dt.hasSame(dt.startOf("day"), "day")).toBe(true);
21 | expect(dt.hasSame(dt.plus({ days: 1 }), "days")).toBe(false);
22 | });
23 |
24 | test("DateTime#hasSame() checks high-order units", () => {
25 | const dt1 = DateTime.fromISO("2001-02-03");
26 | const dt2 = DateTime.fromISO("2001-05-03");
27 | expect(dt1.hasSame(dt2, "year")).toBe(true);
28 | expect(dt1.hasSame(dt2, "month")).toBe(false);
29 | // Even when days are equal, return false when a higher-order unit differs.
30 | expect(dt1.hasSame(dt2, "day")).toBe(false);
31 | });
32 |
33 | // #584
34 | test("DateTime#hasSame() ignores time offsets and is symmetric", () => {
35 | const d1 = DateTime.fromISO("2019-10-02T01:02:03.045+03:00", {
36 | zone: "Europe/Helsinki",
37 | });
38 | const d2 = DateTime.fromISO("2019-10-02T01:02:03.045-05:00", {
39 | zone: "America/Chicago",
40 | });
41 |
42 | expect(d1.hasSame(d2, "day")).toBe(true);
43 | expect(d2.hasSame(d1, "day")).toBe(true);
44 | expect(d1.hasSame(d2, "hour")).toBe(true);
45 | expect(d2.hasSame(d1, "hour")).toBe(true);
46 | expect(d1.hasSame(d2, "second")).toBe(true);
47 | expect(d2.hasSame(d1, "second")).toBe(true);
48 | expect(d1.hasSame(d2, "millisecond")).toBe(true);
49 | expect(d2.hasSame(d1, "millisecond")).toBe(true);
50 | });
51 |
52 | test("DateTime#hasSame() returns false for invalid DateTimes", () => {
53 | const dt = DateTime.now(),
54 | invalid = DateTime.invalid("because");
55 | expect(dt.hasSame(invalid, "day")).toBe(false);
56 | expect(invalid.hasSame(invalid, "day")).toBe(false);
57 | expect(invalid.hasSame(dt, "day")).toBe(false);
58 | });
59 |
60 | //------
61 | // #until()
62 | //------
63 |
64 | test("DateTime#until() creates an Interval", () => {
65 | const dt = DateTime.now(),
66 | other = dt.plus({ days: 1 }),
67 | i = dt.until(other);
68 |
69 | expect(i.start).toBe(dt);
70 | expect(i.end).toBe(other);
71 | });
72 |
73 | test("DateTime#until() creates an invalid Interval out of an invalid DateTime", () => {
74 | const dt = DateTime.now(),
75 | invalid = DateTime.invalid("because");
76 |
77 | expect(invalid.until(invalid).isValid).toBe(false);
78 | expect(invalid.until(dt).isValid).toBe(false);
79 | expect(dt.until(invalid).isValid).toBe(false);
80 | });
81 |
82 | //------
83 | // #isInLeapYear
84 | //------
85 | test("DateTime#isInLeapYear returns the whether the DateTime's year is in a leap year", () => {
86 | expect(DateTime.local(2017, 5, 25).isInLeapYear).toBe(false);
87 | expect(DateTime.local(2020, 5, 25).isInLeapYear).toBe(true);
88 | });
89 |
90 | test("DateTime#isInLeapYear returns false for invalid DateTimes", () => {
91 | expect(DateTime.invalid("because").isInLeapYear).toBe(false);
92 | });
93 |
94 | //------
95 | // #daysInYear
96 | //------
97 | test("DateTime#daysInYear returns the number of days in the DateTime's year", () => {
98 | expect(DateTime.local(2017, 5, 25).daysInYear).toBe(365);
99 | expect(DateTime.local(2020, 5, 25).daysInYear).toBe(366);
100 | });
101 |
102 | test("DateTime#daysInYear returns NaN for invalid DateTimes", () => {
103 | expect(DateTime.invalid("because").daysInYear).toBeFalsy();
104 | });
105 |
106 | //------
107 | // #daysInMonth
108 | //------
109 | test("DateTime#daysInMonth returns the number of days in the DateTime's month", () => {
110 | expect(DateTime.local(2017, 3, 10).daysInMonth).toBe(31);
111 | expect(DateTime.local(2017, 6, 10).daysInMonth).toBe(30);
112 | expect(DateTime.local(2017, 2, 10).daysInMonth).toBe(28);
113 | expect(DateTime.local(2020, 2, 10).daysInMonth).toBe(29);
114 | });
115 |
116 | test("DateTime#daysInMonth returns NaN for invalid DateTimes", () => {
117 | expect(DateTime.invalid("because").daysInMonth).toBeFalsy();
118 | });
119 |
120 | //------
121 | // #weeksInWeekYear
122 | //------
123 | test("DateTime#weeksInWeekYear returns the number of days in the DateTime's year", () => {
124 | expect(DateTime.local(2004, 5, 25).weeksInWeekYear).toBe(53);
125 | expect(DateTime.local(2017, 5, 25).weeksInWeekYear).toBe(52);
126 | expect(DateTime.local(2020, 5, 25).weeksInWeekYear).toBe(53);
127 | });
128 |
129 | test("DateTime#weeksInWeekYear returns NaN for invalid DateTimes", () => {
130 | expect(DateTime.invalid("because").weeksInWeekYear).toBeFalsy();
131 | });
132 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Luxon
2 |
3 | ## General guidelines
4 |
5 | Patches are welcome. Luxon is at this point just a baby and it could use lots of help. But before you dive in...Luxon is one of those tightly-scoped libraries where the default answer to "should this library do X?" is likely "no". **So ask first!** It might save you some time and energy.
6 |
7 | Here are some vague notes on Luxon's design philosophy:
8 |
9 | 1. We won't accept patches that can't be internationalized using the JS environment's (e.g. the browser's) native capabilities. This means that most convenient humanization features are out of scope.
10 | 1. We try hard to have a clear definition of what Luxon does and doesn't do. With few exceptions, this is not a "do what I mean" library.
11 | 1. Luxon shouldn't contain simple conveniences that bloat the library to save callers a couple lines of code. Write those lines in your own code.
12 | 1. Most of the complexity of JS module loading compatibility is left to the build. If you have a "this can't be loaded in my bespoke JS module loader" problems, this isn't something you should be solving with changes to the `src` directory. If it's a common use case and is possible to generate with Rollup, it can get its own build command.
13 | 1. We prefer documentation clarifications and gotchas to go in the docstrings, not in the guides on the docs page. Obviously, if the guides are wrong, they should be fixed, but we don't want them to turn into troubleshooting pages. On the other hand, making sure the method-level documentation has ample examples and notes is great.
14 | 1. You'll need to sign a CLA as part of your first pull request to Luxon.
15 |
16 | ## Building and testing
17 |
18 | Building and testing is done through npm scripts. The tests run in Node and require Node 18 with full-icu support. This is because some of the features available in Luxon (like internationalization and time zones) need that stuff and we test it all. On any platform, if you have Node 18 installed with full-icu, you're good to go; just run `scripts/test`. But you probably don't have that, so read on.
19 |
20 | ### OSX
21 |
22 | Mac is easy:
23 | Open the terminal.
24 |
25 | ```
26 | brew install node --with-full-icu
27 | npm install
28 | ./scripts/test
29 | ```
30 |
31 | If that's for whatever reason a pain, the Linux instructions should also work, as well as the Docker ones.
32 |
33 | ### Linux
34 |
35 | There are two ways to get full-icu support in Linux: build it with that support, or provide it as a module. We'll cover the latter. Assuming you've installed Node 10:
36 |
37 | ```
38 | npm install
39 | npm install full-icu
40 | ./scripts/test
41 | ```
42 |
43 | Where `scripts/test` is just `NODE_ICU_DATA="$(pwd)/node_modules/full-icu" npm run test`, which is required for making Node load the full-icu module you just installed. You can run all the other npm scripts (e.g. `npm run docs`) directly; they don't require Intl support.
44 |
45 | ### Windows
46 |
47 | If you have [Bash](https://git-scm.com/downloads) or [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10), the Linux instructions seem to work fine.
48 |
49 | I would love to add instructions for a non-WSL install of the dev env!
50 |
51 | ### Docker
52 |
53 | In case messing with your Node environment just to run Luxon's tests is too much to ask, we've provided a Docker container. You'll need a functioning Docker environment, but the rest is easy:
54 |
55 | ```
56 | ./docker/npm install --ignore-scripts
57 | ./docker/npm test
58 | ```
59 |
60 | ## Patch basics
61 |
62 | Once you're sure your bugfix or feature makes sense for Luxon, make sure you take these steps:
63 |
64 | 1. Be sure to add tests and run them with `scripts/test`
65 | 1. Be sure you run `npm run format` before you commit. Note this will modify your source files to line up with the style guidelines.
66 | 1. Make sure you add or ESDoc annotations appropriately. You can run `npm run docs` to generate the HTML for them. They land in the `build/docs` directory. This also builds the markdown files in `/docs` into the guide on the Luxon website.
67 | 1. To test Luxon in your browser, run `npm run site` and then open `build/demo/global.html`. You can access Luxon classes in the console like `window.luxon.DateTime`.
68 | 1. To test in Node, run `npm run build` and then run something like `var DateTime = require('./build/cjs-browser/luxon').DateTime`.
69 |
70 | Luxon uses [Husky](https://github.com/typicode/husky) to run the formatter on your code as a pre-commit hook. You should still run `npm run format` yourself to catch other issues, but this hook will help prevent you from failing the build with a trivial formatting error.
71 |
72 | ## npm script reference
73 |
74 | | Command | Function |
75 | | ---------------------------- | --------------------------------------- |
76 | | `npm run build` | Build all the distributable files |
77 | | `npm run build-node` | Build just for Node |
78 | | `npm run test` | Run the test suite, but see notes above |
79 | | `npm run format` | Run the Prettier formatter |
80 | | `npm run site` | Build the Luxon website, including docs |
81 | | `npm run check-doc-coverage` | Check whether there's full doc coverage |
82 | | `npm run benchmark` | Run performance benchmarks |
83 |
--------------------------------------------------------------------------------
/site/docs/_media/Luxon_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/zones/IANA.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import { FixedOffsetZone, IANAZone } from "../../src/luxon";
3 |
4 | test("IANAZone.create returns a singleton per zone name", () => {
5 | expect(IANAZone.create("UTC")).toBe(IANAZone.create("UTC"));
6 | expect(IANAZone.create("America/New_York")).toBe(IANAZone.create("America/New_York"));
7 |
8 | expect(IANAZone.create("UTC")).not.toBe(IANAZone.create("America/New_York"));
9 |
10 | // hold true even for invalid zone names
11 | expect(IANAZone.create("blorp")).toBe(IANAZone.create("blorp"));
12 | });
13 |
14 | test("IANAZone.create should return IANAZone instance", () => {
15 | const result = IANAZone.create("America/Cancun");
16 | expect(result).toBeInstanceOf(IANAZone);
17 | });
18 |
19 | test("IANAZone.isValidSpecifier", () => {
20 | expect(IANAZone.isValidSpecifier("America/New_York")).toBe(true);
21 | // this used to return true but now returns false, because we just defer to isValidZone
22 | expect(IANAZone.isValidSpecifier("Fantasia/Castle")).toBe(false);
23 | expect(IANAZone.isValidSpecifier("Sport~~blorp")).toBe(false);
24 | expect(IANAZone.isValidSpecifier("Etc/GMT+8")).toBe(true);
25 | expect(IANAZone.isValidSpecifier("Etc/GMT-8")).toBe(true);
26 | expect(IANAZone.isValidSpecifier("Etc/GMT-0")).toBe(true);
27 | expect(IANAZone.isValidSpecifier("Etc/GMT-1")).toBe(true);
28 | expect(IANAZone.isValidSpecifier(null)).toBe(false);
29 | });
30 |
31 | test("IANAZone.isValidZone", () => {
32 | expect(IANAZone.isValidZone("America/New_York")).toBe(true);
33 | expect(IANAZone.isValidZone("Fantasia/Castle")).toBe(false);
34 | expect(IANAZone.isValidZone("Sport~~blorp")).toBe(false);
35 | expect(IANAZone.isValidZone("")).toBe(false);
36 | expect(IANAZone.isValidZone(undefined)).toBe(false);
37 | expect(IANAZone.isValidZone(null)).toBe(false);
38 | expect(IANAZone.isValidZone(4)).toBe(false);
39 | });
40 |
41 | test("IANAZone.type returns a static string", () => {
42 | expect(new IANAZone("America/Santiago").type).toBe("iana");
43 | expect(new IANAZone("America/Blorp").type).toBe("iana");
44 | });
45 |
46 | test("IANAZone.name returns the zone name passed to the constructor", () => {
47 | expect(new IANAZone("America/Santiago").name).toBe("America/Santiago");
48 | expect(new IANAZone("America/Blorp").name).toBe("America/Blorp");
49 | expect(new IANAZone("foo").name).toBe("foo");
50 | });
51 |
52 | test("IANAZone is not universal", () => {
53 | expect(new IANAZone("America/Santiago").isUniversal).toBe(false);
54 | });
55 |
56 | test("IANAZone.offsetName with a long format", () => {
57 | const zone = new IANAZone("America/Santiago");
58 | const offsetName = zone.offsetName(1552089600, { format: "long", locale: "en-US" });
59 | expect(offsetName).toBe("Chile Summer Time");
60 | });
61 |
62 | test("IANAZone.offsetName with a short format", () => {
63 | const zone = new IANAZone("America/Santiago");
64 | const offsetName = zone.offsetName(1552089600, { format: "short", locale: "en-US" });
65 | expect(offsetName).toBe("GMT-3");
66 | });
67 |
68 | test("IANAZone.formatOffset with a short format", () => {
69 | const zone = new IANAZone("America/Santiago");
70 | const offsetName = zone.formatOffset(1552089600, "short");
71 | expect(offsetName).toBe("-03:00");
72 | });
73 |
74 | test("IANAZone.formatOffset with a narrow format", () => {
75 | const zone = new IANAZone("America/Santiago");
76 | const offsetName = zone.formatOffset(1552089600, "narrow");
77 | expect(offsetName).toBe("-3");
78 | });
79 |
80 | test("IANAZone.formatOffset with a techie format", () => {
81 | const zone = new IANAZone("America/Santiago");
82 | const offsetName = zone.formatOffset(1552089600, "techie");
83 | expect(offsetName).toBe("-0300");
84 | });
85 |
86 | test("IANAZone.formatOffset throws for an invalid format", () => {
87 | const zone = new IANAZone("America/Santiago");
88 | expect(() => zone.formatOffset(1552089600, "blorp")).toThrow();
89 | });
90 |
91 | test("IANAZone.equals requires both zones to be iana", () => {
92 | expect(IANAZone.create("UTC").equals(FixedOffsetZone.utcInstance)).toBe(false);
93 | });
94 |
95 | test("IANAZone.equals returns false even if the two share offsets", () => {
96 | const luxembourg = IANAZone.create("Europe/Luxembourg");
97 | const rome = IANAZone.create("Europe/Rome");
98 | expect(luxembourg.equals(rome)).toBe(false);
99 | });
100 |
101 | test("IANAZone.isValid returns true for valid zone names", () => {
102 | expect(new IANAZone("UTC").isValid).toBe(true);
103 | expect(new IANAZone("America/Santiago").isValid).toBe(true);
104 | expect(new IANAZone("Europe/Paris").isValid).toBe(true);
105 | });
106 |
107 | test("IANAZone.isValid returns false for invalid zone names", () => {
108 | expect(new IANAZone("").isValid).toBe(false);
109 | expect(new IANAZone("foo").isValid).toBe(false);
110 | expect(new IANAZone("CEDT").isValid).toBe(false);
111 | expect(new IANAZone("GMT+2").isValid).toBe(false);
112 | expect(new IANAZone("America/Blorp").isValid).toBe(false);
113 | expect(new IANAZone(null).isValid).toBe(false);
114 | });
115 |
116 | test("IANAZone.normalize normalizes the zone name", () => {
117 | expect(IANAZone.normalizeZone("america/nEw_york")).toBe("America/New_York");
118 | expect(IANAZone.normalizeZone("AMERICA/NEW_YORK")).toBe("America/New_York");
119 | expect(IANAZone.normalizeZone("America/New_York")).toBe("America/New_York");
120 | expect(IANAZone.normalizeZone("europe/paris")).toBe("Europe/Paris");
121 | expect(IANAZone.normalizeZone("EUROPE/PARIS")).toBe("Europe/Paris");
122 | expect(IANAZone.normalizeZone("Asia/Tokyo")).toBe("Asia/Tokyo");
123 | expect(IANAZone.normalizeZone("Etc/GMT")).toBe("UTC");
124 | });
125 |
126 | test("IANAZone returns canonical zone name regardless of input casing", () => {
127 | expect(new IANAZone("america/nEw_york").name).toBe("America/New_York");
128 | expect(new IANAZone("AMERICA/NEW_YORK").name).toBe("America/New_York");
129 | expect(new IANAZone("America/New_York").name).toBe("America/New_York");
130 | expect(new IANAZone("europe/paris").name).toBe("Europe/Paris");
131 | expect(new IANAZone("EUROPE/PARIS").name).toBe("Europe/Paris");
132 | expect(new IANAZone("Asia/Tokyo").name).toBe("Asia/Tokyo");
133 | });
134 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | import SystemZone from "./zones/systemZone.js";
2 | import IANAZone from "./zones/IANAZone.js";
3 | import Locale from "./impl/locale.js";
4 | import DateTime from "./datetime.js";
5 |
6 | import { normalizeZone } from "./impl/zoneUtil.js";
7 | import { validateWeekSettings } from "./impl/util.js";
8 | import { resetDigitRegexCache } from "./impl/digits.js";
9 |
10 | let now = () => Date.now(),
11 | defaultZone = "system",
12 | defaultLocale = null,
13 | defaultNumberingSystem = null,
14 | defaultOutputCalendar = null,
15 | twoDigitCutoffYear = 60,
16 | throwOnInvalid,
17 | defaultWeekSettings = null;
18 |
19 | /**
20 | * Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here.
21 | */
22 | export default class Settings {
23 | /**
24 | * Get the callback for returning the current timestamp.
25 | * @type {function}
26 | */
27 | static get now() {
28 | return now;
29 | }
30 |
31 | /**
32 | * Set the callback for returning the current timestamp.
33 | * The function should return a number, which will be interpreted as an Epoch millisecond count
34 | * @type {function}
35 | * @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future
36 | * @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time
37 | */
38 | static set now(n) {
39 | now = n;
40 | }
41 |
42 | /**
43 | * Set the default time zone to create DateTimes in. Does not affect existing instances.
44 | * Use the value "system" to reset this value to the system's time zone.
45 | * @type {string}
46 | */
47 | static set defaultZone(zone) {
48 | defaultZone = zone;
49 | }
50 |
51 | /**
52 | * Get the default time zone object currently used to create DateTimes. Does not affect existing instances.
53 | * The default value is the system's time zone (the one set on the machine that runs this code).
54 | * @type {Zone}
55 | */
56 | static get defaultZone() {
57 | return normalizeZone(defaultZone, SystemZone.instance);
58 | }
59 |
60 | /**
61 | * Get the default locale to create DateTimes with. Does not affect existing instances.
62 | * @type {string}
63 | */
64 | static get defaultLocale() {
65 | return defaultLocale;
66 | }
67 |
68 | /**
69 | * Set the default locale to create DateTimes with. Does not affect existing instances.
70 | * @type {string}
71 | */
72 | static set defaultLocale(locale) {
73 | defaultLocale = locale;
74 | }
75 |
76 | /**
77 | * Get the default numbering system to create DateTimes with. Does not affect existing instances.
78 | * @type {string}
79 | */
80 | static get defaultNumberingSystem() {
81 | return defaultNumberingSystem;
82 | }
83 |
84 | /**
85 | * Set the default numbering system to create DateTimes with. Does not affect existing instances.
86 | * @type {string}
87 | */
88 | static set defaultNumberingSystem(numberingSystem) {
89 | defaultNumberingSystem = numberingSystem;
90 | }
91 |
92 | /**
93 | * Get the default output calendar to create DateTimes with. Does not affect existing instances.
94 | * @type {string}
95 | */
96 | static get defaultOutputCalendar() {
97 | return defaultOutputCalendar;
98 | }
99 |
100 | /**
101 | * Set the default output calendar to create DateTimes with. Does not affect existing instances.
102 | * @type {string}
103 | */
104 | static set defaultOutputCalendar(outputCalendar) {
105 | defaultOutputCalendar = outputCalendar;
106 | }
107 |
108 | /**
109 | * @typedef {Object} WeekSettings
110 | * @property {number} firstDay
111 | * @property {number} minimalDays
112 | * @property {number[]} weekend
113 | */
114 |
115 | /**
116 | * @return {WeekSettings|null}
117 | */
118 | static get defaultWeekSettings() {
119 | return defaultWeekSettings;
120 | }
121 |
122 | /**
123 | * Allows overriding the default locale week settings, i.e. the start of the week, the weekend and
124 | * how many days are required in the first week of a year.
125 | * Does not affect existing instances.
126 | *
127 | * @param {WeekSettings|null} weekSettings
128 | */
129 | static set defaultWeekSettings(weekSettings) {
130 | defaultWeekSettings = validateWeekSettings(weekSettings);
131 | }
132 |
133 | /**
134 | * Get the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.
135 | * @type {number}
136 | */
137 | static get twoDigitCutoffYear() {
138 | return twoDigitCutoffYear;
139 | }
140 |
141 | /**
142 | * Set the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.
143 | * @type {number}
144 | * @example Settings.twoDigitCutoffYear = 0 // all 'yy' are interpreted as 20th century
145 | * @example Settings.twoDigitCutoffYear = 99 // all 'yy' are interpreted as 21st century
146 | * @example Settings.twoDigitCutoffYear = 50 // '49' -> 2049; '50' -> 1950
147 | * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50
148 | * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50
149 | */
150 | static set twoDigitCutoffYear(cutoffYear) {
151 | twoDigitCutoffYear = cutoffYear % 100;
152 | }
153 |
154 | /**
155 | * Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
156 | * @type {boolean}
157 | */
158 | static get throwOnInvalid() {
159 | return throwOnInvalid;
160 | }
161 |
162 | /**
163 | * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
164 | * @type {boolean}
165 | */
166 | static set throwOnInvalid(t) {
167 | throwOnInvalid = t;
168 | }
169 |
170 | /**
171 | * Reset Luxon's global caches. Should only be necessary in testing scenarios.
172 | * @return {void}
173 | */
174 | static resetCaches() {
175 | Locale.resetCache();
176 | IANAZone.resetCache();
177 | DateTime.resetCache();
178 | resetDigitRegexCache();
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/test/impl/english.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 | import * as Formats from "../../src/impl/formats";
3 | import { formatRelativeTime, formatString, weekdays, eras } from "../../src/impl/english";
4 |
5 | test("today", () => {
6 | expect(formatRelativeTime("days", 0, "auto")).toBe("today");
7 | expect(formatRelativeTime("days", 0, "always")).toBe("in 0 days");
8 | });
9 |
10 | test("tomorrow", () => {
11 | expect(formatRelativeTime("days", 1, "auto")).toBe("tomorrow");
12 | expect(formatRelativeTime("days", 1, "always")).toBe("in 1 day");
13 | });
14 |
15 | test("yesterday", () => {
16 | expect(formatRelativeTime("days", -1, "auto")).toBe("yesterday");
17 | expect(formatRelativeTime("days", -1, "always")).toBe("1 day ago");
18 | });
19 |
20 | test("in 0.5 days", () => {
21 | expect(formatRelativeTime("days", 1.5, "auto")).toBe("in 1.5 days");
22 | expect(formatRelativeTime("days", 1.5, "always")).toBe("in 1.5 days");
23 | });
24 |
25 | test("0.5 days ago", () => {
26 | expect(formatRelativeTime("days", -1.5, "auto")).toBe("1.5 days ago");
27 | expect(formatRelativeTime("days", -1.5, "always")).toBe("1.5 days ago");
28 | });
29 |
30 | test("2 days ago", () => {
31 | expect(formatRelativeTime("days", -2, "auto")).toBe("2 days ago");
32 | expect(formatRelativeTime("days", -2, "always")).toBe("2 days ago");
33 | });
34 |
35 | test("this month", () => {
36 | expect(formatRelativeTime("months", 0, "auto")).toBe("this month");
37 | expect(formatRelativeTime("months", 0, "always")).toBe("in 0 months");
38 | expect(formatRelativeTime("months", -0, "always")).toBe("0 months ago");
39 | expect(formatRelativeTime("months", 0, "always", true)).toBe("in 0 mo.");
40 | expect(formatRelativeTime("months", -0, "always", true)).toBe("0 mo. ago");
41 | });
42 |
43 | test("next month", () => {
44 | expect(formatRelativeTime("months", 1, "auto")).toBe("next month");
45 | expect(formatRelativeTime("months", 1, "auto", true)).toBe("next month");
46 | expect(formatRelativeTime("months", 1, "always")).toBe("in 1 month");
47 | expect(formatRelativeTime("months", 1, "always", true)).toBe("in 1 mo.");
48 | });
49 |
50 | test("last month", () => {
51 | expect(formatRelativeTime("months", -1, "auto")).toBe("last month");
52 | expect(formatRelativeTime("months", -1, "auto", true)).toBe("last month");
53 | expect(formatRelativeTime("months", -1, "always")).toBe("1 month ago");
54 | expect(formatRelativeTime("months", -1, "always", true)).toBe("1 mo. ago");
55 | });
56 |
57 | test("in 3 months", () => {
58 | expect(formatRelativeTime("months", 3, "auto")).toBe("in 3 months");
59 | expect(formatRelativeTime("months", 3, "auto", true)).toBe("in 3 mo.");
60 | expect(formatRelativeTime("months", 3, "always")).toBe("in 3 months");
61 | expect(formatRelativeTime("months", 3, "always", true)).toBe("in 3 mo.");
62 | });
63 |
64 | test("in 1 hour", () => {
65 | expect(formatRelativeTime("hours", 1, "auto")).toBe("in 1 hour");
66 | expect(formatRelativeTime("hours", 1, "always")).toBe("in 1 hour");
67 | });
68 |
69 | test("in 1 hour", () => {
70 | expect(formatRelativeTime("hours", 1, "auto")).toBe("in 1 hour");
71 | expect(formatRelativeTime("hours", 1, "auto", true)).toBe("in 1 hr.");
72 | expect(formatRelativeTime("hours", 1, "always")).toBe("in 1 hour");
73 | expect(formatRelativeTime("hours", 1, "always", true)).toBe("in 1 hr.");
74 | });
75 |
76 | test("1 hour ago", () => {
77 | expect(formatRelativeTime("hours", -1, "auto")).toBe("1 hour ago");
78 | expect(formatRelativeTime("hours", -1, "auto", true)).toBe("1 hr. ago");
79 | expect(formatRelativeTime("hours", -1, "always")).toBe("1 hour ago");
80 | expect(formatRelativeTime("hours", -1, "always", true)).toBe("1 hr. ago");
81 | });
82 |
83 | test("formatString", () => {
84 | expect(formatString(Formats.DATE_SHORT)).toBe("M/d/yyyy");
85 | expect(formatString(Formats.DATE_MED)).toBe("LLL d, yyyy");
86 | expect(formatString(Formats.DATE_MED_WITH_WEEKDAY)).toBe("EEE, LLL d, yyyy");
87 | expect(formatString(Formats.DATE_FULL)).toBe("LLLL d, yyyy");
88 | expect(formatString(Formats.DATE_HUGE)).toBe("EEEE, LLLL d, yyyy");
89 | expect(formatString(Formats.TIME_SIMPLE)).toBe("h:mm a");
90 | expect(formatString(Formats.TIME_WITH_SECONDS)).toBe("h:mm:ss a");
91 | expect(formatString(Formats.TIME_WITH_SHORT_OFFSET)).toBe("h:mm a");
92 | expect(formatString(Formats.TIME_WITH_LONG_OFFSET)).toBe("h:mm a");
93 | expect(formatString(Formats.TIME_24_SIMPLE)).toBe("HH:mm");
94 | expect(formatString(Formats.TIME_24_WITH_SECONDS)).toBe("HH:mm:ss");
95 | expect(formatString(Formats.TIME_24_WITH_SHORT_OFFSET)).toBe("HH:mm");
96 | expect(formatString(Formats.TIME_24_WITH_LONG_OFFSET)).toBe("HH:mm");
97 | expect(formatString(Formats.DATETIME_SHORT)).toBe("M/d/yyyy, h:mm a");
98 | expect(formatString(Formats.DATETIME_MED)).toBe("LLL d, yyyy, h:mm a");
99 | expect(formatString(Formats.DATETIME_FULL)).toBe("LLLL d, yyyy, h:mm a");
100 | expect(formatString(Formats.DATETIME_HUGE)).toBe("EEEE, LLLL d, yyyy, h:mm a");
101 | expect(formatString(Formats.DATETIME_SHORT_WITH_SECONDS)).toBe("M/d/yyyy, h:mm:ss a");
102 | expect(formatString(Formats.DATETIME_MED_WITH_SECONDS)).toBe("LLL d, yyyy, h:mm:ss a");
103 | expect(formatString(Formats.DATETIME_MED_WITH_WEEKDAY)).toBe("EEE, d LLL yyyy, h:mm a");
104 | expect(formatString(Formats.DATETIME_FULL_WITH_SECONDS)).toBe("LLLL d, yyyy, h:mm:ss a");
105 | expect(formatString(Formats.DATETIME_HUGE_WITH_SECONDS)).toBe("EEEE, LLLL d, yyyy, h:mm:ss a");
106 | expect(formatString("Default")).toBe("EEEE, LLLL d, yyyy, h:mm a");
107 | });
108 |
109 | test("weekdays", () => {
110 | expect(weekdays("narrow")).toStrictEqual(["M", "T", "W", "T", "F", "S", "S"]);
111 | expect(weekdays("short")).toStrictEqual(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]);
112 | expect(weekdays("long")).toStrictEqual([
113 | "Monday",
114 | "Tuesday",
115 | "Wednesday",
116 | "Thursday",
117 | "Friday",
118 | "Saturday",
119 | "Sunday",
120 | ]);
121 | expect(weekdays("numeric")).toStrictEqual(["1", "2", "3", "4", "5", "6", "7"]);
122 | expect(weekdays(null)).toStrictEqual(null);
123 | });
124 |
125 | test("eras", () => {
126 | expect(eras("narrow")).toStrictEqual(["B", "A"]);
127 | expect(eras("short")).toStrictEqual(["BC", "AD"]);
128 | expect(eras("long")).toStrictEqual(["Before Christ", "Anno Domini"]);
129 | expect(eras("default")).toStrictEqual(null);
130 | });
131 |
--------------------------------------------------------------------------------
/test/datetime/getters.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime } from "../../src/luxon";
4 | import Settings from "../../src/settings";
5 |
6 | const dateTime = DateTime.fromJSDate(new Date(1982, 4, 25, 9, 23, 54, 123)),
7 | utc = DateTime.fromMillis(Date.UTC(1982, 4, 25, 9, 23, 54, 123)).toUTC(),
8 | inv = DateTime.invalid("I said so");
9 |
10 | //------
11 | // year/month/day/hour/minute/second/millisecond
12 | //------
13 | test("DateTime#year returns the year", () => {
14 | expect(dateTime.year).toBe(1982);
15 | expect(utc.year).toBe(1982);
16 | expect(inv.year).toBeFalsy();
17 | });
18 |
19 | test("DateTime#month returns the (1-indexed) month", () => {
20 | expect(dateTime.month).toBe(5);
21 | expect(utc.month).toBe(5);
22 | expect(inv.month).toBeFalsy();
23 | });
24 |
25 | test("DateTime#day returns the day", () => {
26 | expect(dateTime.day).toBe(25);
27 | expect(utc.day).toBe(25);
28 | expect(inv.day).toBeFalsy();
29 | });
30 |
31 | test("DateTime#hour returns the hour", () => {
32 | expect(dateTime.hour).toBe(9);
33 | expect(utc.hour).toBe(9);
34 | expect(inv.hour).toBeFalsy();
35 | });
36 |
37 | test("DateTime#minute returns the minute", () => {
38 | expect(dateTime.minute).toBe(23);
39 | expect(utc.minute).toBe(23);
40 | expect(inv.minute).toBeFalsy();
41 | });
42 |
43 | test("DateTime#second returns the second", () => {
44 | expect(dateTime.second).toBe(54);
45 | expect(utc.second).toBe(54);
46 | expect(inv.second).toBeFalsy();
47 | });
48 |
49 | test("DateTime#millisecond returns the millisecond", () => {
50 | expect(dateTime.millisecond).toBe(123);
51 | expect(utc.millisecond).toBe(123);
52 | expect(inv.millisecond).toBeFalsy();
53 | });
54 |
55 | //------
56 | // weekYear/weekNumber/weekday
57 | //------
58 | test("DateTime#weekYear returns the weekYear", () => {
59 | expect(dateTime.weekYear).toBe(1982);
60 | // test again bc caching
61 | expect(dateTime.weekYear).toBe(1982);
62 | });
63 |
64 | test("DateTime#weekNumber returns the weekNumber", () => {
65 | expect(dateTime.weekNumber).toBe(21);
66 | // test again bc caching
67 | expect(dateTime.weekNumber).toBe(21);
68 | });
69 |
70 | test("DateTime#weekday returns the weekday", () => {
71 | expect(dateTime.weekday).toBe(2);
72 | // test again bc caching
73 | expect(dateTime.weekday).toBe(2);
74 | });
75 |
76 | test("DateTime#weekday returns the weekday for older dates", () => {
77 | const dt = DateTime.fromObject({ year: 43, month: 4, day: 4 });
78 | expect(dt.weekday).toBe(6);
79 | });
80 |
81 | //------
82 | // weekdayShort/weekdayLong
83 | //------
84 | test("DateTime#weekdayShort returns the short human readable weekday for en-US locale", () => {
85 | expect(dateTime.setLocale("en-US").weekdayShort).toBe("Tue");
86 | });
87 |
88 | test("DateTime#weekdayLong returns the human readable weekday for en-US locale", () => {
89 | expect(dateTime.setLocale("en-US").weekdayLong).toBe("Tuesday");
90 | });
91 |
92 | test("DateTime#weekdayShort returns the short human readable weekday for fr locale", () => {
93 | expect(dateTime.setLocale("fr").weekdayShort).toBe("mar.");
94 | });
95 |
96 | test("DateTime#weekdayLong returns the human readable weekday for fr locale", () => {
97 | expect(dateTime.setLocale("fr").weekdayLong).toBe("mardi");
98 | });
99 |
100 | test("DateTime#weekdayShort returns null for invalid DateTimes", () => {
101 | expect(inv.weekdayShort).toBe(null);
102 | });
103 |
104 | test("DateTime#weekdayLong returns null for invalid DateTimes", () => {
105 | expect(inv.weekdayLong).toBe(null);
106 | });
107 |
108 | //------
109 | // monthShort/monthLong
110 | //------
111 | test("DateTime#monthShort returns the short human readable month", () => {
112 | expect(dateTime.setLocale("en-US").monthShort).toBe("May");
113 | });
114 |
115 | test("DateTime#monthLong returns the human readable month", () => {
116 | expect(dateTime.setLocale("en-US").monthLong).toBe("May");
117 | });
118 |
119 | test("DateTime#monthShort returns the short human readable month", () => {
120 | expect(dateTime.minus({ months: 1 }).setLocale("en-US").monthShort).toBe("Apr");
121 | });
122 |
123 | test("DateTime#monthLong returns the human readable month", () => {
124 | expect(dateTime.minus({ months: 1 }).setLocale("en-US").monthLong).toBe("April");
125 | });
126 |
127 | test("DateTime#monthShort returns the short human readable month for fr locale", () => {
128 | expect(dateTime.minus({ months: 1 }).setLocale("fr").monthShort).toBe("avr.");
129 | });
130 |
131 | test("DateTime#monthLong returns the human readable month for fr locale", () => {
132 | expect(dateTime.minus({ months: 1 }).setLocale("fr").monthLong).toBe("avril");
133 | });
134 |
135 | test("DateTime#monthLong returns null for invalid DateTimes", () => {
136 | expect(inv.monthLong).toBe(null);
137 | });
138 |
139 | test("DateTime#monthShort returns null for invalid DateTimes", () => {
140 | expect(inv.monthShort).toBe(null);
141 | });
142 |
143 | //------
144 | // ordinal
145 | //------
146 |
147 | test("DateTime#ordinal returns the ordinal", () => {
148 | expect(dateTime.ordinal).toBe(145);
149 | });
150 |
151 | test("DateTime#ordinal returns NaN for invalid DateTimes", () => {
152 | expect(inv.ordinal).toBeFalsy();
153 | });
154 |
155 | //------
156 | // get
157 | //------
158 | test("DateTime#get can retrieve any unit", () => {
159 | expect(dateTime.get("ordinal")).toBe(145);
160 | expect(dateTime.get("year")).toBe(1982);
161 | expect(dateTime.get("weekNumber")).toBe(21);
162 | });
163 |
164 | test("DateTime#get returns undefined for invalid units", () => {
165 | expect(dateTime.get("plurp")).toBeUndefined();
166 | });
167 |
168 | //------
169 | // locale
170 | //------
171 | test("DateTime#locale returns the locale", () => {
172 | const dt = DateTime.now().reconfigure({ locale: "be" });
173 | expect(dt.locale).toBe("be");
174 | });
175 |
176 | //------
177 | // zone/zoneName
178 | //------
179 | test("DateTime#zone returns the time zone", () => {
180 | expect(dateTime.zone).toBe(Settings.defaultZone);
181 | });
182 |
183 | test("DateTime#zoneName returns the name of the time zone", () => {
184 | expect(dateTime.zoneName).toBe(Settings.defaultZone.name);
185 | });
186 |
187 | //------
188 | // Misc
189 | //------
190 | test("Invalid DateTimes have unhelpful getters", () => {
191 | const i = DateTime.invalid("because");
192 | expect(i.year).toBeFalsy();
193 | expect(i.month).toBeFalsy();
194 | expect(i.day).toBeFalsy();
195 | expect(i.hour).toBeFalsy();
196 | expect(i.minute).toBeFalsy();
197 | expect(i.second).toBeFalsy();
198 | expect(i.millisecond).toBeFalsy();
199 | expect(i.weekYear).toBeFalsy();
200 | expect(i.weekNumber).toBeFalsy();
201 | expect(i.weekday).toBeFalsy();
202 | });
203 |
--------------------------------------------------------------------------------
/src/impl/english.js:
--------------------------------------------------------------------------------
1 | import * as Formats from "./formats.js";
2 | import { pick } from "./util.js";
3 |
4 | function stringify(obj) {
5 | return JSON.stringify(obj, Object.keys(obj).sort());
6 | }
7 |
8 | /**
9 | * @private
10 | */
11 |
12 | export const monthsLong = [
13 | "January",
14 | "February",
15 | "March",
16 | "April",
17 | "May",
18 | "June",
19 | "July",
20 | "August",
21 | "September",
22 | "October",
23 | "November",
24 | "December",
25 | ];
26 |
27 | export const monthsShort = [
28 | "Jan",
29 | "Feb",
30 | "Mar",
31 | "Apr",
32 | "May",
33 | "Jun",
34 | "Jul",
35 | "Aug",
36 | "Sep",
37 | "Oct",
38 | "Nov",
39 | "Dec",
40 | ];
41 |
42 | export const monthsNarrow = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
43 |
44 | export function months(length) {
45 | switch (length) {
46 | case "narrow":
47 | return [...monthsNarrow];
48 | case "short":
49 | return [...monthsShort];
50 | case "long":
51 | return [...monthsLong];
52 | case "numeric":
53 | return ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
54 | case "2-digit":
55 | return ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"];
56 | default:
57 | return null;
58 | }
59 | }
60 |
61 | export const weekdaysLong = [
62 | "Monday",
63 | "Tuesday",
64 | "Wednesday",
65 | "Thursday",
66 | "Friday",
67 | "Saturday",
68 | "Sunday",
69 | ];
70 |
71 | export const weekdaysShort = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
72 |
73 | export const weekdaysNarrow = ["M", "T", "W", "T", "F", "S", "S"];
74 |
75 | export function weekdays(length) {
76 | switch (length) {
77 | case "narrow":
78 | return [...weekdaysNarrow];
79 | case "short":
80 | return [...weekdaysShort];
81 | case "long":
82 | return [...weekdaysLong];
83 | case "numeric":
84 | return ["1", "2", "3", "4", "5", "6", "7"];
85 | default:
86 | return null;
87 | }
88 | }
89 |
90 | export const meridiems = ["AM", "PM"];
91 |
92 | export const erasLong = ["Before Christ", "Anno Domini"];
93 |
94 | export const erasShort = ["BC", "AD"];
95 |
96 | export const erasNarrow = ["B", "A"];
97 |
98 | export function eras(length) {
99 | switch (length) {
100 | case "narrow":
101 | return [...erasNarrow];
102 | case "short":
103 | return [...erasShort];
104 | case "long":
105 | return [...erasLong];
106 | default:
107 | return null;
108 | }
109 | }
110 |
111 | export function meridiemForDateTime(dt) {
112 | return meridiems[dt.hour < 12 ? 0 : 1];
113 | }
114 |
115 | export function weekdayForDateTime(dt, length) {
116 | return weekdays(length)[dt.weekday - 1];
117 | }
118 |
119 | export function monthForDateTime(dt, length) {
120 | return months(length)[dt.month - 1];
121 | }
122 |
123 | export function eraForDateTime(dt, length) {
124 | return eras(length)[dt.year < 0 ? 0 : 1];
125 | }
126 |
127 | export function formatRelativeTime(unit, count, numeric = "always", narrow = false) {
128 | const units = {
129 | years: ["year", "yr."],
130 | quarters: ["quarter", "qtr."],
131 | months: ["month", "mo."],
132 | weeks: ["week", "wk."],
133 | days: ["day", "day", "days"],
134 | hours: ["hour", "hr."],
135 | minutes: ["minute", "min."],
136 | seconds: ["second", "sec."],
137 | };
138 |
139 | const lastable = ["hours", "minutes", "seconds"].indexOf(unit) === -1;
140 |
141 | if (numeric === "auto" && lastable) {
142 | const isDay = unit === "days";
143 | switch (count) {
144 | case 1:
145 | return isDay ? "tomorrow" : `next ${units[unit][0]}`;
146 | case -1:
147 | return isDay ? "yesterday" : `last ${units[unit][0]}`;
148 | case 0:
149 | return isDay ? "today" : `this ${units[unit][0]}`;
150 | default: // fall through
151 | }
152 | }
153 |
154 | const isInPast = Object.is(count, -0) || count < 0,
155 | fmtValue = Math.abs(count),
156 | singular = fmtValue === 1,
157 | lilUnits = units[unit],
158 | fmtUnit = narrow
159 | ? singular
160 | ? lilUnits[1]
161 | : lilUnits[2] || lilUnits[1]
162 | : singular
163 | ? units[unit][0]
164 | : unit;
165 | return isInPast ? `${fmtValue} ${fmtUnit} ago` : `in ${fmtValue} ${fmtUnit}`;
166 | }
167 |
168 | export function formatString(knownFormat) {
169 | // these all have the offsets removed because we don't have access to them
170 | // without all the intl stuff this is backfilling
171 | const filtered = pick(knownFormat, [
172 | "weekday",
173 | "era",
174 | "year",
175 | "month",
176 | "day",
177 | "hour",
178 | "minute",
179 | "second",
180 | "timeZoneName",
181 | "hourCycle",
182 | ]),
183 | key = stringify(filtered),
184 | dateTimeHuge = "EEEE, LLLL d, yyyy, h:mm a";
185 | switch (key) {
186 | case stringify(Formats.DATE_SHORT):
187 | return "M/d/yyyy";
188 | case stringify(Formats.DATE_MED):
189 | return "LLL d, yyyy";
190 | case stringify(Formats.DATE_MED_WITH_WEEKDAY):
191 | return "EEE, LLL d, yyyy";
192 | case stringify(Formats.DATE_FULL):
193 | return "LLLL d, yyyy";
194 | case stringify(Formats.DATE_HUGE):
195 | return "EEEE, LLLL d, yyyy";
196 | case stringify(Formats.TIME_SIMPLE):
197 | return "h:mm a";
198 | case stringify(Formats.TIME_WITH_SECONDS):
199 | return "h:mm:ss a";
200 | case stringify(Formats.TIME_WITH_SHORT_OFFSET):
201 | return "h:mm a";
202 | case stringify(Formats.TIME_WITH_LONG_OFFSET):
203 | return "h:mm a";
204 | case stringify(Formats.TIME_24_SIMPLE):
205 | return "HH:mm";
206 | case stringify(Formats.TIME_24_WITH_SECONDS):
207 | return "HH:mm:ss";
208 | case stringify(Formats.TIME_24_WITH_SHORT_OFFSET):
209 | return "HH:mm";
210 | case stringify(Formats.TIME_24_WITH_LONG_OFFSET):
211 | return "HH:mm";
212 | case stringify(Formats.DATETIME_SHORT):
213 | return "M/d/yyyy, h:mm a";
214 | case stringify(Formats.DATETIME_MED):
215 | return "LLL d, yyyy, h:mm a";
216 | case stringify(Formats.DATETIME_FULL):
217 | return "LLLL d, yyyy, h:mm a";
218 | case stringify(Formats.DATETIME_HUGE):
219 | return dateTimeHuge;
220 | case stringify(Formats.DATETIME_SHORT_WITH_SECONDS):
221 | return "M/d/yyyy, h:mm:ss a";
222 | case stringify(Formats.DATETIME_MED_WITH_SECONDS):
223 | return "LLL d, yyyy, h:mm:ss a";
224 | case stringify(Formats.DATETIME_MED_WITH_WEEKDAY):
225 | return "EEE, d LLL yyyy, h:mm a";
226 | case stringify(Formats.DATETIME_FULL_WITH_SECONDS):
227 | return "LLLL d, yyyy, h:mm:ss a";
228 | case stringify(Formats.DATETIME_HUGE_WITH_SECONDS):
229 | return "EEEE, LLLL d, yyyy, h:mm:ss a";
230 | default:
231 | return dateTimeHuge;
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/test/datetime/dst.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { DateTime, Settings } from "../../src/luxon";
4 |
5 | const dateTimeConstructors = {
6 | fromObject: (year, month, day, hour) =>
7 | DateTime.fromObject({ year, month, day, hour }, { zone: "America/New_York" }),
8 | local: (year, month, day, hour) =>
9 | DateTime.local(year, month, day, hour, { zone: "America/New_York" }),
10 | };
11 |
12 | for (const [name, local] of Object.entries(dateTimeConstructors)) {
13 | describe(`DateTime.${name}`, () => {
14 | test("Hole dates are bumped forward", () => {
15 | const d = local(2017, 3, 12, 2);
16 | expect(d.hour).toBe(3);
17 | expect(d.offset).toBe(-4 * 60);
18 | });
19 |
20 | if (name == "fromObject") {
21 | // this is questionable behavior, but I wanted to document it
22 | test("Ambiguous dates pick the one with the current offset", () => {
23 | const oldSettings = Settings.now;
24 | try {
25 | Settings.now = () => 1495653314595; // May 24, 2017
26 | let d = local(2017, 11, 5, 1);
27 | expect(d.hour).toBe(1);
28 | expect(d.offset).toBe(-4 * 60);
29 |
30 | Settings.now = () => 1484456400000; // Jan 15, 2017
31 | d = local(2017, 11, 5, 1);
32 | expect(d.hour).toBe(1);
33 | expect(d.offset).toBe(-5 * 60);
34 | } finally {
35 | Settings.now = oldSettings;
36 | }
37 | });
38 | } else {
39 | test("Ambiguous dates pick the one with the cached offset", () => {
40 | const oldSettings = Settings.now;
41 | try {
42 | Settings.resetCaches();
43 | Settings.now = () => 1495653314595; // May 24, 2017
44 | let d = local(2017, 11, 5, 1);
45 | expect(d.hour).toBe(1);
46 | expect(d.offset).toBe(-4 * 60);
47 |
48 | Settings.now = () => 1484456400000; // Jan 15, 2017
49 | d = local(2017, 11, 5, 1);
50 | expect(d.hour).toBe(1);
51 | expect(d.offset).toBe(-4 * 60);
52 |
53 | Settings.resetCaches();
54 |
55 | Settings.now = () => 1484456400000; // Jan 15, 2017
56 | d = local(2017, 11, 5, 1);
57 | expect(d.hour).toBe(1);
58 | expect(d.offset).toBe(-5 * 60);
59 |
60 | Settings.now = () => 1495653314595; // May 24, 2017
61 | d = local(2017, 11, 5, 1);
62 | expect(d.hour).toBe(1);
63 | expect(d.offset).toBe(-5 * 60);
64 | } finally {
65 | Settings.now = oldSettings;
66 | }
67 | });
68 | }
69 |
70 | test("Adding an hour to land on the Spring Forward springs forward", () => {
71 | const d = local(2017, 3, 12, 1).plus({ hour: 1 });
72 | expect(d.hour).toBe(3);
73 | expect(d.offset).toBe(-4 * 60);
74 | });
75 |
76 | test("Subtracting an hour to land on the Spring Forward springs forward", () => {
77 | const d = local(2017, 3, 12, 3).minus({ hour: 1 });
78 | expect(d.hour).toBe(1);
79 | expect(d.offset).toBe(-5 * 60);
80 | });
81 |
82 | test("Adding an hour to land on the Fall Back falls back", () => {
83 | const d = local(2017, 11, 5, 0).plus({ hour: 2 });
84 | expect(d.hour).toBe(1);
85 | expect(d.offset).toBe(-5 * 60);
86 | });
87 |
88 | test("Subtracting an hour to land on the Fall Back falls back", () => {
89 | let d = local(2017, 11, 5, 3).minus({ hour: 2 });
90 | expect(d.hour).toBe(1);
91 | expect(d.offset).toBe(-5 * 60);
92 |
93 | d = d.minus({ hour: 1 });
94 | expect(d.hour).toBe(1);
95 | expect(d.offset).toBe(-4 * 60);
96 | });
97 |
98 | test("Changing a calendar date to land on a hole bumps forward", () => {
99 | let d = local(2017, 3, 11, 2).plus({ day: 1 });
100 | expect(d.hour).toBe(3);
101 | expect(d.offset).toBe(-4 * 60);
102 |
103 | d = local(2017, 3, 13, 2).minus({ day: 1 });
104 | expect(d.hour).toBe(3);
105 | expect(d.offset).toBe(-4 * 60);
106 | });
107 |
108 | test("Changing a calendar date to land on an ambiguous time chooses the closest one", () => {
109 | let d = local(2017, 11, 4, 1).plus({ day: 1 });
110 | expect(d.hour).toBe(1);
111 | expect(d.offset).toBe(-4 * 60);
112 |
113 | d = local(2017, 11, 6, 1).minus({ day: 1 });
114 | expect(d.hour).toBe(1);
115 | expect(d.offset).toBe(-5 * 60);
116 | });
117 |
118 | test("Start of a 0:00->1:00 DST day is 1:00", () => {
119 | const d = DateTime.fromObject(
120 | {
121 | year: 2017,
122 | month: 10,
123 | day: 15,
124 | },
125 | {
126 | zone: "America/Sao_Paulo",
127 | }
128 | ).startOf("day");
129 | expect(d.day).toBe(15);
130 | expect(d.hour).toBe(1);
131 | expect(d.minute).toBe(0);
132 | expect(d.second).toBe(0);
133 | });
134 |
135 | test("End of a 0:00->1:00 DST day is 23:59", () => {
136 | const d = DateTime.fromObject(
137 | {
138 | year: 2017,
139 | month: 10,
140 | day: 15,
141 | },
142 | {
143 | zone: "America/Sao_Paulo",
144 | }
145 | ).endOf("day");
146 | expect(d.day).toBe(15);
147 | expect(d.hour).toBe(23);
148 | expect(d.minute).toBe(59);
149 | expect(d.second).toBe(59);
150 | });
151 | });
152 | }
153 |
154 | describe("DateTime.local() with offset caching", () => {
155 | const edtTs = 1495653314000; // May 24, 2017 15:15:14 -0400
156 | const estTs = 1484456400000; // Jan 15, 2017 00:00 -0500
157 |
158 | const edtDate = [2017, 5, 24, 15, 15, 14, 0];
159 | const estDate = [2017, 1, 15, 0, 0, 0, 0];
160 |
161 | const timestamps = { EDT: edtTs, EST: estTs };
162 | const dates = { EDT: edtDate, EST: estDate };
163 | const zoneObj = { zone: "America/New_York" };
164 |
165 | for (const [cacheName, cacheTs] of Object.entries(timestamps)) {
166 | for (const [nowName, nowTs] of Object.entries(timestamps)) {
167 | for (const [dateName, date] of Object.entries(dates)) {
168 | test(`cache = ${cacheName}, now = ${nowName}, date = ${dateName}`, () => {
169 | const oldSettings = Settings.now;
170 | try {
171 | Settings.now = () => cacheTs;
172 | Settings.resetCaches();
173 | // load cache
174 | DateTime.local(2020, 1, 1, 0, zoneObj);
175 |
176 | Settings.now = () => nowTs;
177 | const dt = DateTime.local(...date, zoneObj);
178 | expect(dt.toMillis()).toBe(timestamps[dateName]);
179 | expect(dt.year).toBe(date[0]);
180 | expect(dt.month).toBe(date[1]);
181 | expect(dt.day).toBe(date[2]);
182 | expect(dt.hour).toBe(date[3]);
183 | expect(dt.minute).toBe(date[4]);
184 | expect(dt.second).toBe(date[5]);
185 | } finally {
186 | Settings.now = oldSettings;
187 | }
188 | });
189 | }
190 | }
191 | }
192 | });
193 |
--------------------------------------------------------------------------------
/test/duration/math.test.js:
--------------------------------------------------------------------------------
1 | /* global test expect */
2 |
3 | import { Duration } from "../../src/luxon";
4 |
5 | //------
6 | // #plus()
7 | //------
8 | test("Duration#plus add straightforward durations", () => {
9 | const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
10 | second = Duration.fromObject({ hours: 1, seconds: 6, milliseconds: 14 }),
11 | result = first.plus(second);
12 |
13 | expect(result.hours).toBe(5);
14 | expect(result.minutes).toBe(12);
15 | expect(result.seconds).toBe(8);
16 | expect(result.milliseconds).toBe(14);
17 | });
18 |
19 | test("Duration#plus add fractional durations", () => {
20 | const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
21 | second = Duration.fromObject({ hours: 1, seconds: 6.8, milliseconds: 14 }),
22 | result = first.plus(second);
23 |
24 | expect(result.hours).toBeCloseTo(5.2, 8);
25 | expect(result.minutes).toBe(12);
26 | expect(result.seconds).toBeCloseTo(8.8, 8);
27 | expect(result.milliseconds).toBe(14);
28 | });
29 |
30 | test("Duration#plus noops empty druations", () => {
31 | const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
32 | second = Duration.fromObject({}),
33 | result = first.plus(second);
34 |
35 | expect(result.hours).toBe(4);
36 | expect(result.minutes).toBe(12);
37 | expect(result.seconds).toBe(2);
38 | });
39 |
40 | test("Duration#plus adds negatives", () => {
41 | const first = Duration.fromObject({ hours: 4, minutes: -12, seconds: -2 }),
42 | second = Duration.fromObject({ hours: -5, seconds: 6, milliseconds: 14 }),
43 | result = first.plus(second);
44 |
45 | expect(result.hours).toBe(-1);
46 | expect(result.minutes).toBe(-12);
47 | expect(result.seconds).toBe(4);
48 | expect(result.milliseconds).toBe(14);
49 | });
50 |
51 | test("Duration#plus adds single values", () => {
52 | const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
53 | result = first.plus({ minutes: 5 });
54 |
55 | expect(result.hours).toBe(4);
56 | expect(result.minutes).toBe(17);
57 | expect(result.seconds).toBe(2);
58 | });
59 |
60 | test("Duration#plus adds number as milliseconds", () => {
61 | const first = Duration.fromObject({ minutes: 11, seconds: 22 }),
62 | result = first.plus(333);
63 |
64 | expect(result.minutes).toBe(11);
65 | expect(result.seconds).toBe(22);
66 | expect(result.milliseconds).toBe(333);
67 | });
68 |
69 | test("Duration#plus maintains invalidity", () => {
70 | const dur = Duration.invalid("because").plus({ minutes: 5 });
71 | expect(dur.isValid).toBe(false);
72 | expect(dur.invalidReason).toBe("because");
73 | });
74 |
75 | test("Duration#plus results in the superset of units", () => {
76 | let dur = Duration.fromObject({ hours: 1, minutes: 0 }).plus({ seconds: 3, milliseconds: 0 });
77 | expect(dur.toObject()).toEqual({ hours: 1, minutes: 0, seconds: 3, milliseconds: 0 });
78 |
79 | dur = Duration.fromObject({ hours: 1, minutes: 0 }).plus({});
80 | expect(dur.toObject()).toEqual({ hours: 1, minutes: 0 });
81 | });
82 |
83 | test("Duration#plus throws with invalid parameter", () => {
84 | expect(() => Duration.fromObject({}).plus("123")).toThrow();
85 | });
86 |
87 | //------
88 | // #minus()
89 | //------
90 | test("Duration#minus subtracts durations", () => {
91 | const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
92 | second = Duration.fromObject({ hours: 1, seconds: 6, milliseconds: 14 }),
93 | result = first.minus(second);
94 |
95 | expect(result.hours).toBe(3);
96 | expect(result.minutes).toBe(12);
97 | expect(result.seconds).toBe(-4);
98 | expect(result.milliseconds).toBe(-14);
99 | });
100 |
101 | test("Duration#minus subtracts fractional durations", () => {
102 | const first = Duration.fromObject({ hours: 4.2, minutes: 12, seconds: 2 }),
103 | second = Duration.fromObject({ hours: 1, seconds: 6, milliseconds: 14 }),
104 | result = first.minus(second);
105 |
106 | expect(result.hours).toBeCloseTo(3.2, 8);
107 | expect(result.minutes).toBe(12);
108 | expect(result.seconds).toBe(-4);
109 | expect(result.milliseconds).toBe(-14);
110 | });
111 |
112 | test("Duration#minus subtracts single values", () => {
113 | const first = Duration.fromObject({ hours: 4, minutes: 12, seconds: 2 }),
114 | result = first.minus({ minutes: 5 });
115 |
116 | expect(result.hours).toBe(4);
117 | expect(result.minutes).toBe(7);
118 | expect(result.seconds).toBe(2);
119 | });
120 |
121 | test("Duration#minus maintains invalidity", () => {
122 | const dur = Duration.invalid("because").minus({ minutes: 5 });
123 | expect(dur.isValid).toBe(false);
124 | expect(dur.invalidReason).toBe("because");
125 | });
126 |
127 | //------
128 | // #negate()
129 | //------
130 |
131 | test("Duration#negate flips all the signs", () => {
132 | const dur = Duration.fromObject({ hours: 4, minutes: -12, seconds: 2 }),
133 | result = dur.negate();
134 | expect(result.hours).toBe(-4);
135 | expect(result.minutes).toBe(12);
136 | expect(result.seconds).toBe(-2);
137 | });
138 |
139 | test("Duration#negate preserves invalidity", () => {
140 | const dur = Duration.invalid("because"),
141 | result = dur.negate();
142 | expect(result.isValid).toBe(false);
143 | expect(result.invalidReason).toBe("because");
144 | });
145 |
146 | test("Duration#negate doesn't mutate", () => {
147 | const orig = Duration.fromObject({ hours: 8 });
148 | orig.negate();
149 | expect(orig.hours).toBe(8);
150 | });
151 |
152 | test("Duration#negate preserves conversionAccuracy", () => {
153 | const dur = Duration.fromObject(
154 | {
155 | hours: 4,
156 | minutes: -12,
157 | seconds: 2,
158 | },
159 | {
160 | conversionAccuracy: "longterm",
161 | }
162 | ),
163 | result = dur.negate();
164 | expect(result.conversionAccuracy).toBe("longterm");
165 | });
166 |
167 | //------
168 | // #mapUnits
169 | //------
170 |
171 | test("Duration#units can multiply durations", () => {
172 | const dur = Duration.fromObject({ hours: 1, minutes: 2, seconds: -3, milliseconds: -4 }),
173 | result = dur.mapUnits((x) => x * 5);
174 |
175 | expect(result.hours).toBe(5);
176 | expect(result.minutes).toBe(10);
177 | expect(result.seconds).toBe(-15);
178 | expect(result.milliseconds).toBe(-20);
179 | });
180 |
181 | test("Duration#units can take the unit into account", () => {
182 | const dur = Duration.fromObject({ hours: 1, minutes: 2, seconds: -3, milliseconds: -4 }),
183 | result = dur.mapUnits((x, u) => x * (u === "milliseconds" ? 2 : 5));
184 |
185 | expect(result.hours).toBe(5);
186 | expect(result.minutes).toBe(10);
187 | expect(result.seconds).toBe(-15);
188 | expect(result.milliseconds).toBe(-8);
189 | });
190 |
191 | test("Duration#mapUnits maintains invalidity", () => {
192 | const dur = Duration.invalid("because").mapUnits((x) => x * 5);
193 | expect(dur.isValid).toBe(false);
194 | expect(dur.invalidReason).toBe("because");
195 | });
196 |
197 | test("Duration#mapUnits requires that fn return a number", () => {
198 | const dur = Duration.fromObject({ hours: 1, minutes: 2, seconds: -3, milliseconds: -4 });
199 | expect(() => dur.mapUnits(() => "hello?")).toThrow();
200 | });
201 |
--------------------------------------------------------------------------------
/src/impl/conversions.js:
--------------------------------------------------------------------------------
1 | import {
2 | integerBetween,
3 | isLeapYear,
4 | timeObject,
5 | daysInYear,
6 | daysInMonth,
7 | weeksInWeekYear,
8 | isInteger,
9 | isUndefined,
10 | } from "./util.js";
11 | import Invalid from "./invalid.js";
12 | import { ConflictingSpecificationError } from "../errors.js";
13 |
14 | const nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
15 | leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
16 |
17 | function unitOutOfRange(unit, value) {
18 | return new Invalid(
19 | "unit out of range",
20 | `you specified ${value} (of type ${typeof value}) as a ${unit}, which is invalid`
21 | );
22 | }
23 |
24 | export function dayOfWeek(year, month, day) {
25 | const d = new Date(Date.UTC(year, month - 1, day));
26 |
27 | if (year < 100 && year >= 0) {
28 | d.setUTCFullYear(d.getUTCFullYear() - 1900);
29 | }
30 |
31 | const js = d.getUTCDay();
32 |
33 | return js === 0 ? 7 : js;
34 | }
35 |
36 | function computeOrdinal(year, month, day) {
37 | return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1];
38 | }
39 |
40 | function uncomputeOrdinal(year, ordinal) {
41 | const table = isLeapYear(year) ? leapLadder : nonLeapLadder,
42 | month0 = table.findIndex((i) => i < ordinal),
43 | day = ordinal - table[month0];
44 | return { month: month0 + 1, day };
45 | }
46 |
47 | export function isoWeekdayToLocal(isoWeekday, startOfWeek) {
48 | return ((isoWeekday - startOfWeek + 7) % 7) + 1;
49 | }
50 |
51 | /**
52 | * @private
53 | */
54 |
55 | export function gregorianToWeek(gregObj, minDaysInFirstWeek = 4, startOfWeek = 1) {
56 | const { year, month, day } = gregObj,
57 | ordinal = computeOrdinal(year, month, day),
58 | weekday = isoWeekdayToLocal(dayOfWeek(year, month, day), startOfWeek);
59 |
60 | let weekNumber = Math.floor((ordinal - weekday + 14 - minDaysInFirstWeek) / 7),
61 | weekYear;
62 |
63 | if (weekNumber < 1) {
64 | weekYear = year - 1;
65 | weekNumber = weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek);
66 | } else if (weekNumber > weeksInWeekYear(year, minDaysInFirstWeek, startOfWeek)) {
67 | weekYear = year + 1;
68 | weekNumber = 1;
69 | } else {
70 | weekYear = year;
71 | }
72 |
73 | return { weekYear, weekNumber, weekday, ...timeObject(gregObj) };
74 | }
75 |
76 | export function weekToGregorian(weekData, minDaysInFirstWeek = 4, startOfWeek = 1) {
77 | const { weekYear, weekNumber, weekday } = weekData,
78 | weekdayOfJan4 = isoWeekdayToLocal(dayOfWeek(weekYear, 1, minDaysInFirstWeek), startOfWeek),
79 | yearInDays = daysInYear(weekYear);
80 |
81 | let ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 7 + minDaysInFirstWeek,
82 | year;
83 |
84 | if (ordinal < 1) {
85 | year = weekYear - 1;
86 | ordinal += daysInYear(year);
87 | } else if (ordinal > yearInDays) {
88 | year = weekYear + 1;
89 | ordinal -= daysInYear(weekYear);
90 | } else {
91 | year = weekYear;
92 | }
93 |
94 | const { month, day } = uncomputeOrdinal(year, ordinal);
95 | return { year, month, day, ...timeObject(weekData) };
96 | }
97 |
98 | export function gregorianToOrdinal(gregData) {
99 | const { year, month, day } = gregData;
100 | const ordinal = computeOrdinal(year, month, day);
101 | return { year, ordinal, ...timeObject(gregData) };
102 | }
103 |
104 | export function ordinalToGregorian(ordinalData) {
105 | const { year, ordinal } = ordinalData;
106 | const { month, day } = uncomputeOrdinal(year, ordinal);
107 | return { year, month, day, ...timeObject(ordinalData) };
108 | }
109 |
110 | /**
111 | * Check if local week units like localWeekday are used in obj.
112 | * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties.
113 | * Modifies obj in-place!
114 | * @param obj the object values
115 | */
116 | export function usesLocalWeekValues(obj, loc) {
117 | const hasLocaleWeekData =
118 | !isUndefined(obj.localWeekday) ||
119 | !isUndefined(obj.localWeekNumber) ||
120 | !isUndefined(obj.localWeekYear);
121 | if (hasLocaleWeekData) {
122 | const hasIsoWeekData =
123 | !isUndefined(obj.weekday) || !isUndefined(obj.weekNumber) || !isUndefined(obj.weekYear);
124 |
125 | if (hasIsoWeekData) {
126 | throw new ConflictingSpecificationError(
127 | "Cannot mix locale-based week fields with ISO-based week fields"
128 | );
129 | }
130 | if (!isUndefined(obj.localWeekday)) obj.weekday = obj.localWeekday;
131 | if (!isUndefined(obj.localWeekNumber)) obj.weekNumber = obj.localWeekNumber;
132 | if (!isUndefined(obj.localWeekYear)) obj.weekYear = obj.localWeekYear;
133 | delete obj.localWeekday;
134 | delete obj.localWeekNumber;
135 | delete obj.localWeekYear;
136 | return {
137 | minDaysInFirstWeek: loc.getMinDaysInFirstWeek(),
138 | startOfWeek: loc.getStartOfWeek(),
139 | };
140 | } else {
141 | return { minDaysInFirstWeek: 4, startOfWeek: 1 };
142 | }
143 | }
144 |
145 | export function hasInvalidWeekData(obj, minDaysInFirstWeek = 4, startOfWeek = 1) {
146 | const validYear = isInteger(obj.weekYear),
147 | validWeek = integerBetween(
148 | obj.weekNumber,
149 | 1,
150 | weeksInWeekYear(obj.weekYear, minDaysInFirstWeek, startOfWeek)
151 | ),
152 | validWeekday = integerBetween(obj.weekday, 1, 7);
153 |
154 | if (!validYear) {
155 | return unitOutOfRange("weekYear", obj.weekYear);
156 | } else if (!validWeek) {
157 | return unitOutOfRange("week", obj.weekNumber);
158 | } else if (!validWeekday) {
159 | return unitOutOfRange("weekday", obj.weekday);
160 | } else return false;
161 | }
162 |
163 | export function hasInvalidOrdinalData(obj) {
164 | const validYear = isInteger(obj.year),
165 | validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year));
166 |
167 | if (!validYear) {
168 | return unitOutOfRange("year", obj.year);
169 | } else if (!validOrdinal) {
170 | return unitOutOfRange("ordinal", obj.ordinal);
171 | } else return false;
172 | }
173 |
174 | export function hasInvalidGregorianData(obj) {
175 | const validYear = isInteger(obj.year),
176 | validMonth = integerBetween(obj.month, 1, 12),
177 | validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month));
178 |
179 | if (!validYear) {
180 | return unitOutOfRange("year", obj.year);
181 | } else if (!validMonth) {
182 | return unitOutOfRange("month", obj.month);
183 | } else if (!validDay) {
184 | return unitOutOfRange("day", obj.day);
185 | } else return false;
186 | }
187 |
188 | export function hasInvalidTimeData(obj) {
189 | const { hour, minute, second, millisecond } = obj;
190 | const validHour =
191 | integerBetween(hour, 0, 23) ||
192 | (hour === 24 && minute === 0 && second === 0 && millisecond === 0),
193 | validMinute = integerBetween(minute, 0, 59),
194 | validSecond = integerBetween(second, 0, 59),
195 | validMillisecond = integerBetween(millisecond, 0, 999);
196 |
197 | if (!validHour) {
198 | return unitOutOfRange("hour", hour);
199 | } else if (!validMinute) {
200 | return unitOutOfRange("minute", minute);
201 | } else if (!validSecond) {
202 | return unitOutOfRange("second", second);
203 | } else if (!validMillisecond) {
204 | return unitOutOfRange("millisecond", millisecond);
205 | } else return false;
206 | }
207 |
--------------------------------------------------------------------------------