├── .agignore ├── .babelrc ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── benchmarks ├── datetime.js ├── index.js ├── info.js └── package.json ├── codecov.yml ├── docker ├── Dockerfile ├── build ├── npm ├── push └── readme.md ├── docs ├── calendars.md ├── formatting.md ├── home.md ├── install.md ├── intl.md ├── math.md ├── matrix.md ├── moment.md ├── parsing.md ├── tour.md ├── upgrading.md ├── validity.md ├── why.md └── zones.md ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts ├── bootstrap.js ├── deploy-site ├── jest ├── readme.md ├── release ├── repl ├── tag ├── test └── version ├── site ├── .nojekyll ├── demo │ ├── demo.css │ ├── demo.js │ ├── global.html │ └── requirejs.html ├── docs │ ├── _coverpage.md │ ├── _media │ │ ├── Luxon_icon.svg │ │ ├── Luxon_icon_180x180.png │ │ ├── Luxon_icon_180x180@2x.png │ │ ├── Luxon_icon_32x32.png │ │ └── Luxon_icon_64x64.png │ └── _sidebar.md └── index.html ├── src ├── datetime.js ├── duration.js ├── errors.js ├── impl │ ├── conversions.js │ ├── diff.js │ ├── digits.js │ ├── english.js │ ├── formats.js │ ├── formatter.js │ ├── invalid.js │ ├── locale.js │ ├── regexParser.js │ ├── tokenParser.js │ ├── util.js │ └── zoneUtil.js ├── info.js ├── interval.js ├── luxon.js ├── package.json ├── settings.js ├── zone.js └── zones │ ├── IANAZone.js │ ├── fixedOffsetZone.js │ ├── invalidZone.js │ └── systemZone.js ├── tasks ├── build.js ├── buildAll.js ├── buildGlobal.js └── buildNode.js └── test ├── datetime ├── create.test.js ├── degrade.test.js ├── diff.test.js ├── dst.test.js ├── equality.test.js ├── format.test.js ├── getters.test.js ├── info.test.js ├── invalid.test.js ├── localeWeek.test.js ├── many.test.js ├── math.test.js ├── misc.test.js ├── proto.test.js ├── reconfigure.test.js ├── regexParse.test.js ├── relative.test.js ├── set.test.js ├── toFormat.test.js ├── tokenParse.test.js ├── transform.test.js ├── typecheck.test.js └── zone.test.js ├── duration ├── accuracy.test.js ├── create.test.js ├── customMatrix.test.js ├── equality.test.js ├── format.test.js ├── getters.test.js ├── info.test.js ├── invalid.test.js ├── math.test.js ├── parse.test.js ├── proto.test.js ├── reconfigure.test.js ├── set.test.js ├── typecheck.test.js └── units.test.js ├── helpers.js ├── impl └── english.test.js ├── info ├── features.test.js ├── listers.test.js ├── localeWeek.test.js └── zones.test.js ├── interval ├── create.test.js ├── format.test.js ├── getters.test.js ├── info.test.js ├── localeWeek.test.js ├── many.test.js ├── parse.test.js ├── proto.test.js ├── setter.test.js └── typecheck.test.js └── zones ├── IANA.test.js ├── fixedOffset.test.js ├── invalid.test.js ├── local.test.js └── zoneInterface.test.js /.agignore: -------------------------------------------------------------------------------- 1 | /build 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | scripts/* linguist-vendored 2 | docker/* linguist-vendored 3 | site/** linguist-vendored 4 | .husky/* linguist-vendored 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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.6 # latest 18.x 22 | - 20.18.3 # latest 20.x 23 | - 22.14.0 # latest 22.x 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: "npm" 32 | - run: npm ci 33 | - run: npm run build 34 | - run: npm run format-check 35 | - run: npm run test 36 | - run: npm run site 37 | - run: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "printWidth": 100 } 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ["@babel/preset-env"] }; 2 | -------------------------------------------------------------------------------- /benchmarks/datetime.js: -------------------------------------------------------------------------------- 1 | import Benchmark from "benchmark"; 2 | import DateTime from "../src/datetime.js"; 3 | import Settings from "../src/settings.js"; 4 | 5 | function runDateTimeSuite() { 6 | return new Promise((resolve, reject) => { 7 | const suite = new Benchmark.Suite(); 8 | 9 | const dt = DateTime.now(); 10 | 11 | const formatParser = DateTime.buildFormatParser("yyyy/MM/dd HH:mm:ss.SSS"); 12 | 13 | suite 14 | .add("DateTime.now", () => { 15 | DateTime.now(); 16 | }) 17 | .add("DateTime.fromObject with locale", () => { 18 | DateTime.fromObject({}, { locale: "fr" }); 19 | }) 20 | .add("DateTime.local with numbers", () => { 21 | DateTime.local(2017, 5, 15); 22 | }) 23 | .add("DateTime.local with numbers and zone", () => { 24 | DateTime.local(2017, 5, 15, 11, 7, 35, { zone: "America/New_York" }); 25 | }) 26 | .add("DateTime.fromISO", () => { 27 | DateTime.fromISO("1982-05-25T09:10:11.445Z"); 28 | }) 29 | .add("DateTime.fromSQL", () => { 30 | DateTime.fromSQL("2016-05-14 10:23:54.2346"); 31 | }) 32 | .add("DateTime.fromFormat", () => { 33 | DateTime.fromFormat("1982/05/25 09:10:11.445", "yyyy/MM/dd HH:mm:ss.SSS"); 34 | }) 35 | .add("DateTime.fromFormat with zone", () => { 36 | DateTime.fromFormat("1982/05/25 09:10:11.445", "yyyy/MM/dd HH:mm:ss.SSS", { 37 | zone: "America/Los_Angeles", 38 | }); 39 | }) 40 | .add("DateTime.fromFormatParser", () => { 41 | DateTime.fromFormatParser("1982/05/25 09:10:11.445", formatParser); 42 | }) 43 | .add("DateTime.fromFormatParser with zone", () => { 44 | DateTime.fromFormatParser("1982/05/25 09:10:11.445", formatParser, { 45 | zone: "America/Los_Angeles", 46 | }); 47 | }) 48 | .add("DateTime#setZone", () => { 49 | dt.setZone("America/Los_Angeles"); 50 | }) 51 | .add("DateTime#toFormat", () => { 52 | dt.toFormat("yyyy-MM-dd"); 53 | }) 54 | .add("DateTime#toFormat with macro", () => { 55 | dt.toFormat("T"); 56 | }) 57 | .add("DateTime#toFormat with macro no cache", () => { 58 | dt.toFormat("T"); 59 | Settings.resetCaches(); 60 | }) 61 | .add("DateTime#format in german", () => { 62 | dt.setLocale("de-De").toFormat("d. LLL. HH:mm"); 63 | }) 64 | .add("DateTime#format in german and no-cache", () => { 65 | dt.setLocale("de-De").toFormat("d. LLL. HH:mm"); 66 | Settings.resetCaches(); 67 | }) 68 | .add("DateTime#add", () => { 69 | dt.plus({ milliseconds: 3434 }); 70 | }) 71 | .add("DateTime#toISO", () => { 72 | dt.toISO(); 73 | }) 74 | .add("DateTime#toLocaleString", () => { 75 | dt.toLocaleString(); 76 | }) 77 | .add("DateTime#toLocaleString in utc", () => { 78 | dt.toUTC().toLocaleString(); 79 | }) 80 | .add("DateTime#toRelativeCalendar", () => { 81 | dt.toRelativeCalendar({ base: DateTime.now(), locale: "fi" }); 82 | }) 83 | .on("cycle", (event) => { 84 | console.log(String(event.target)); 85 | }) 86 | .on("complete", function () { 87 | console.log("Fastest is " + this.filter("fastest").map("name")); 88 | resolve(); 89 | }) 90 | .on("error", function () { 91 | reject(this.error); 92 | }) 93 | .run(); 94 | }); 95 | } 96 | 97 | const allSuites = [runDateTimeSuite]; 98 | 99 | export default allSuites; 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /benchmarks/info.js: -------------------------------------------------------------------------------- 1 | import Benchmark from "benchmark"; 2 | import Info from "../src/info.js"; 3 | import Locale from "../src/impl/locale.js"; 4 | 5 | function runWeekdaysSuite() { 6 | return new Promise((resolve, reject) => { 7 | const locale = Locale.create(null, null, null); 8 | 9 | new Benchmark.Suite() 10 | .add("Info.weekdays with existing locale", () => { 11 | Info.weekdays("long", { locObj: locale }); 12 | }) 13 | .add("Info.weekdays", () => { 14 | Info.weekdays("long"); 15 | }) 16 | .on("cycle", (event) => { 17 | console.log(String(event.target)); 18 | }) 19 | .on("complete", function () { 20 | console.log("Fastest is " + this.filter("fastest").map("name")); 21 | resolve(); 22 | }) 23 | .on("error", function () { 24 | reject(this.error); 25 | }) 26 | .run(); 27 | }); 28 | } 29 | 30 | function runWeekdaysFormatSuite() { 31 | return new Promise((resolve, reject) => { 32 | const locale = Locale.create(null, null, null); 33 | 34 | new Benchmark.Suite() 35 | .add("Info.weekdaysFormat with existing locale", () => { 36 | Info.weekdaysFormat("long", { locObj: locale }); 37 | }) 38 | .add("Info.weekdaysFormat", () => { 39 | Info.weekdaysFormat("long"); 40 | }) 41 | .on("cycle", (event) => { 42 | console.log(String(event.target)); 43 | }) 44 | .on("complete", function () { 45 | console.log("Fastest is " + this.filter("fastest").map("name")); 46 | resolve(); 47 | }) 48 | .on("error", function () { 49 | reject(this.error); 50 | }) 51 | .run(); 52 | }); 53 | } 54 | 55 | function runMonthsSuite() { 56 | return new Promise((resolve, reject) => { 57 | const locale = Locale.create(null, null, null); 58 | new Benchmark.Suite() 59 | .add("Info.months with existing locale", () => { 60 | Info.months("long", { locObj: locale }); 61 | }) 62 | .add("Info.months", () => { 63 | Info.months("long"); 64 | }) 65 | .on("cycle", (event) => { 66 | console.log(String(event.target)); 67 | }) 68 | .on("complete", function () { 69 | console.log("Fastest is " + this.filter("fastest").map("name")); 70 | resolve(); 71 | }) 72 | .on("error", function () { 73 | reject(this.error); 74 | }) 75 | .run(); 76 | }); 77 | } 78 | 79 | function runMonthsFormatSuite() { 80 | return new Promise((resolve, reject) => { 81 | const locale = Locale.create(null, null, null); 82 | 83 | new Benchmark.Suite() 84 | .add("Info.monthsFormat with existing locale", () => { 85 | Info.monthsFormat("long", { locObj: locale }); 86 | }) 87 | .add("Info.monthsFormat", () => { 88 | Info.monthsFormat("long"); 89 | }) 90 | .on("cycle", (event) => { 91 | console.log(String(event.target)); 92 | }) 93 | .on("complete", function () { 94 | console.log("Fastest is " + this.filter("fastest").map("name")); 95 | resolve(); 96 | }) 97 | .on("error", function () { 98 | reject(this.error); 99 | }) 100 | .run(); 101 | }); 102 | } 103 | 104 | const allSuites = [runMonthsSuite, runMonthsFormatSuite, runWeekdaysSuite, runWeekdaysFormatSuite]; 105 | 106 | export default allSuites; 107 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker build docker -t icambron/luxon 3 | -------------------------------------------------------------------------------- /docker/npm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker run -it --rm -v $(pwd):/luxon -w /luxon icambron/luxon npm $@ 3 | -------------------------------------------------------------------------------- /docker/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker push icambron/luxon 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/calendars.md: -------------------------------------------------------------------------------- 1 | # Calendars 2 | 3 | This covers Luxon's support for various calendar systems. If you don't need to use non-standard calendars, you don't need to read any of this. 4 | 5 | ## Fully supported calendars 6 | 7 | Luxon has full support for Gregorian and ISO Week calendars. What I mean by that is that Luxon can parse dates specified in those calendars, format dates into strings using those calendars, and transform dates using the units of those calendars. For example, here is Luxon working directly with an ISO calendar: 8 | 9 | ```js 10 | DateTime.fromISO('2017-W23-3').plus({ weeks: 1, days: 2 }).toISOWeekDate(); //=> '2017-W24-5' 11 | ``` 12 | 13 | The main reason I bring all this is up is to contrast it with the capabilities for other calendars described below. 14 | 15 | ## Output calendars 16 | 17 | Luxon has limited support for other calendaring systems. Which calendars are supported at all is a platform-dependent, but can generally be expected to be these: Buddhist, Chinese, Coptic, Ethioaa, Ethiopic, Hebrew, Indian, Islamic, Islamicc, Japanese, Persian, and ROC. **Support is limited to formatting strings with them**, hence the qualified name "output calendar". 18 | 19 | In practice this is pretty useful; you can show users the date in their preferred calendaring system while the software works with dates using Gregorian units or Epoch milliseconds. But the limitations are real enough; Luxon doesn't know how to do things like "add one Islamic month". 20 | 21 | The output calendar is a property of the DateTime itself. For example: 22 | 23 | ```js 24 | var dtHebrew = DateTime.now().reconfigure({ outputCalendar: "hebrew" }); 25 | dtHebrew.outputCalendar; //=> 'hebrew' 26 | dtHebrew.toLocaleString() //=> '4 Tishri 5778' 27 | ``` 28 | 29 | You can modulate the structure of that string with arguments to `toLocaleString` (see [the docs on that](formatting.md?id=tolocalestring-strings-for-humans)), but the point here is just that you got the alternative calendar. 30 | 31 | ### Generally supported calendars 32 | 33 | Here's a table of the different calendars with examples generated formatting the same date generated like this: 34 | 35 | ```js 36 | DateTime.fromObject({ outputCalendar: c }).toLocaleString(DateTime.DATE_FULL); 37 | ``` 38 | 39 | Since Luxon uses the browser's **Intl API**, you can use all the supported calendars. 40 | (See [Intl.Locale.prototype.getCalendars()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getCalendars) for a full list) 41 | 42 | | Calendar | Example | 43 | | ------------------ | ------------------------ | 44 | | buddhist | September 24, 2560 BE | 45 | | chinese | Eighth Month 5, 2017 | 46 | | coptic | Tout 14, 1734 ERA1 | 47 | | ethioaa | Meskerem 14, 7510 ERA0 | 48 | | ethiopic | Meskerem 14, 2010 ERA1 | 49 | | hebrew | 4 Tishri 5778 | 50 | | indian | Asvina 2, 1939 Saka | 51 | | islamic | Muharram 4, 1439 AH | 52 | | islamic-civil | Muharram 3, 1439 AH | 53 | | islamic-umalqura | Muharram 3, 1439 AH | 54 | | iso8601 | September 24, 2017 | 55 | | japanese | September 24, 29 Heisei | 56 | | persian | Mehr 2, 1396 AP | 57 | | roc | September 24, 106 Minguo | 58 | 59 | 60 | 61 | ### Default output calendar 62 | 63 | You can set the default output calendar for new DateTime instances like this: 64 | 65 | ```js 66 | Settings.defaultOutputCalendar = 'persian'; 67 | ``` 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install guide 2 | 3 | Luxon provides different builds for different JS environments. See below for a link to the right one and instructions on how to use it. Luxon supports all modern platforms, but see [the support matrix](matrix.md) for additional details. 4 | 5 | ## Basic browser setup 6 | 7 | - [Download full](https://moment.github.io/luxon/global/luxon.js) 8 | - [Download minified](https://moment.github.io/luxon/global/luxon.min.js) 9 | 10 | You can also load the files from a [CDN](https://www.jsdelivr.com/package/npm/luxon). 11 | 12 | Just include Luxon in a script tag. You can access its various classes through the `luxon` global. 13 | 14 | ```html 15 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/validity.md: -------------------------------------------------------------------------------- 1 | # Validity 2 | 3 | ## Invalid DateTimes 4 | 5 | One of the most irritating aspects of programming with time is that it's possible to end up with invalid dates. This is a bit subtle: barring integer overflows, there's no count of milliseconds that don't correspond to a valid DateTime, but when working with calendar units, it's pretty easy to say something like "June 400th". Luxon considers that invalid and will mark it accordingly. 6 | 7 | Unless you've asked Luxon to throw an exception when it creates an invalid DateTime (see more on that below), it will fail silently, creating an instance that doesn't know how to do anything. You can check validity with `isValid`: 8 | 9 | ```js 10 | > var dt = DateTime.fromObject({ month: 6, day: 400 }); 11 | dt.isValid //=> false 12 | ``` 13 | 14 | All of the methods or getters that return primitives return degenerate ones: 15 | 16 | ```js 17 | dt.year; //=> NaN 18 | dt.toString(); //=> 'Invalid DateTime' 19 | dt.toObject(); //=> {} 20 | ``` 21 | 22 | Methods that return other Luxon objects will return invalid ones: 23 | 24 | ```js 25 | dt.plus({ days: 4 }).isValid; //=> false 26 | ``` 27 | 28 | ## Reasons a DateTimes can be invalid 29 | 30 | The most common way to do that is to over- or underflow some unit: 31 | 32 | - February 40th 33 | - 28:00 34 | - -4 pm 35 | - etc 36 | 37 | But there are other ways to do it: 38 | 39 | ```js 40 | // specify a time zone that doesn't exist 41 | DateTime.now().setZone("America/Blorp").isValid; //=> false 42 | 43 | // provide contradictory information (here, this date is not a Wednesday) 44 | DateTime.fromObject({ year: 2017, month: 5, day: 25, weekday: 3 }).isValid; //=> false 45 | ``` 46 | 47 | Note that some other kinds of mistakes throw, based on our judgment that they are more likely programmer errors than data issues: 48 | 49 | ```js 50 | DateTime.now().set({ blorp: 7 }); //=> kerplosion 51 | ``` 52 | 53 | ## Debugging invalid DateTimes 54 | 55 | Because DateTimes fail silently, they can be a pain to debug. Luxon has some features that can help. 56 | 57 | ### invalidReason and invalidExplanation 58 | 59 | Invalid DateTime objects are happy to tell you why they're invalid. `invalidReason` will give you a consistent error code you can use, whereas `invalidExplanation` will spell it out 60 | 61 | ```js 62 | var dt = DateTime.now().setZone("America/Blorp"); 63 | dt.invalidReason; //=> 'unsupported zone' 64 | dt.invalidExplanation; //=> 'the zone "America/Blorp" is not supported' 65 | ``` 66 | 67 | ### throwOnInvalid 68 | 69 | You can make Luxon throw whenever it creates an invalid DateTime. The message will combine `invalidReason` and `invalidExplanation`: 70 | 71 | ```js 72 | Settings.throwOnInvalid = true; 73 | DateTime.now().setZone("America/Blorp"); //=> Error: Invalid DateTime: unsupported zone: the zone "America/Blorp" is not supported 74 | ``` 75 | 76 | You can of course leave this on in production too, but be sure to try/catch it appropriately. 77 | 78 | ## Invalid Durations 79 | 80 | Durations can be invalid too. The easiest way to get one is to diff an invalid DateTime. 81 | 82 | ```js 83 | DateTime.local(2017, 28).diffNow().isValid; //=> false 84 | ``` 85 | 86 | ## Invalid Intervals 87 | 88 | Intervals can be invalid. This can happen a few different ways: 89 | 90 | - The end time is before the start time 91 | - It was created from invalid DateTime or Duration 92 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luxon", 3 | "version": "3.6.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": "./src/luxon.js", 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>/ 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /site/demo/requirejs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /site/docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](_media/Luxon_icon_64x64.png) 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 | ![color](#e8dffc) 15 | -------------------------------------------------------------------------------- /site/docs/_media/Luxon_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Luxon_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /site/docs/_media/Luxon_icon_180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moment/luxon/e1d9886e478167593217d6621f9a79bc958e89d6/site/docs/_media/Luxon_icon_180x180.png -------------------------------------------------------------------------------- /site/docs/_media/Luxon_icon_180x180@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moment/luxon/e1d9886e478167593217d6621f9a79bc958e89d6/site/docs/_media/Luxon_icon_180x180@2x.png -------------------------------------------------------------------------------- /site/docs/_media/Luxon_icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moment/luxon/e1d9886e478167593217d6621f9a79bc958e89d6/site/docs/_media/Luxon_icon_32x32.png -------------------------------------------------------------------------------- /site/docs/_media/Luxon_icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moment/luxon/e1d9886e478167593217d6621f9a79bc958e89d6/site/docs/_media/Luxon_icon_64x64.png -------------------------------------------------------------------------------- /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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | luxon - Immutable date wrapper 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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/diff.js: -------------------------------------------------------------------------------- 1 | import Duration from "../duration.js"; 2 | 3 | function dayDiff(earlier, later) { 4 | const utcDayStart = (dt) => dt.toUTC(0, { keepLocalTime: true }).startOf("day").valueOf(), 5 | ms = utcDayStart(later) - utcDayStart(earlier); 6 | return Math.floor(Duration.fromMillis(ms).as("days")); 7 | } 8 | 9 | function highOrderDiffs(cursor, later, units) { 10 | const differs = [ 11 | ["years", (a, b) => b.year - a.year], 12 | ["quarters", (a, b) => b.quarter - a.quarter + (b.year - a.year) * 4], 13 | ["months", (a, b) => b.month - a.month + (b.year - a.year) * 12], 14 | [ 15 | "weeks", 16 | (a, b) => { 17 | const days = dayDiff(a, b); 18 | return (days - (days % 7)) / 7; 19 | }, 20 | ], 21 | ["days", dayDiff], 22 | ]; 23 | 24 | const results = {}; 25 | const earlier = cursor; 26 | let lowestOrder, highWater; 27 | 28 | /* This loop tries to diff using larger units first. 29 | If we overshoot, we backtrack and try the next smaller unit. 30 | "cursor" starts out at the earlier timestamp and moves closer and closer to "later" 31 | as we use smaller and smaller units. 32 | highWater keeps track of where we would be if we added one more of the smallest unit, 33 | this is used later to potentially convert any difference smaller than the smallest higher order unit 34 | into a fraction of that smallest higher order unit 35 | */ 36 | for (const [unit, differ] of differs) { 37 | if (units.indexOf(unit) >= 0) { 38 | lowestOrder = unit; 39 | 40 | results[unit] = differ(cursor, later); 41 | highWater = earlier.plus(results); 42 | 43 | if (highWater > later) { 44 | // we overshot the end point, backtrack cursor by 1 45 | results[unit]--; 46 | cursor = earlier.plus(results); 47 | 48 | // if we are still overshooting now, we need to backtrack again 49 | // this happens in certain situations when diffing times in different zones, 50 | // because this calculation ignores time zones 51 | if (cursor > later) { 52 | // keep the "overshot by 1" around as highWater 53 | highWater = cursor; 54 | // backtrack cursor by 1 55 | results[unit]--; 56 | cursor = earlier.plus(results); 57 | } 58 | } else { 59 | cursor = highWater; 60 | } 61 | } 62 | } 63 | 64 | return [cursor, results, highWater, lowestOrder]; 65 | } 66 | 67 | export default function (earlier, later, units, opts) { 68 | let [cursor, results, highWater, lowestOrder] = highOrderDiffs(earlier, later, units); 69 | 70 | const remainingMillis = later - cursor; 71 | 72 | const lowerOrderUnits = units.filter( 73 | (u) => ["hours", "minutes", "seconds", "milliseconds"].indexOf(u) >= 0 74 | ); 75 | 76 | if (lowerOrderUnits.length === 0) { 77 | if (highWater < later) { 78 | highWater = cursor.plus({ [lowestOrder]: 1 }); 79 | } 80 | 81 | if (highWater !== cursor) { 82 | results[lowestOrder] = (results[lowestOrder] || 0) + remainingMillis / (highWater - cursor); 83 | } 84 | } 85 | 86 | const duration = Duration.fromObject(results, opts); 87 | 88 | if (lowerOrderUnits.length > 0) { 89 | return Duration.fromMillis(remainingMillis, opts) 90 | .shiftTo(...lowerOrderUnits) 91 | .plus(duration); 92 | } else { 93 | return duration; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/impl/formats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | */ 4 | 5 | const n = "numeric", 6 | s = "short", 7 | l = "long"; 8 | 9 | export const DATE_SHORT = { 10 | year: n, 11 | month: n, 12 | day: n, 13 | }; 14 | 15 | export const DATE_MED = { 16 | year: n, 17 | month: s, 18 | day: n, 19 | }; 20 | 21 | export const DATE_MED_WITH_WEEKDAY = { 22 | year: n, 23 | month: s, 24 | day: n, 25 | weekday: s, 26 | }; 27 | 28 | export const DATE_FULL = { 29 | year: n, 30 | month: l, 31 | day: n, 32 | }; 33 | 34 | export const DATE_HUGE = { 35 | year: n, 36 | month: l, 37 | day: n, 38 | weekday: l, 39 | }; 40 | 41 | export const TIME_SIMPLE = { 42 | hour: n, 43 | minute: n, 44 | }; 45 | 46 | export const TIME_WITH_SECONDS = { 47 | hour: n, 48 | minute: n, 49 | second: n, 50 | }; 51 | 52 | export const TIME_WITH_SHORT_OFFSET = { 53 | hour: n, 54 | minute: n, 55 | second: n, 56 | timeZoneName: s, 57 | }; 58 | 59 | export const TIME_WITH_LONG_OFFSET = { 60 | hour: n, 61 | minute: n, 62 | second: n, 63 | timeZoneName: l, 64 | }; 65 | 66 | export const TIME_24_SIMPLE = { 67 | hour: n, 68 | minute: n, 69 | hourCycle: "h23", 70 | }; 71 | 72 | export const TIME_24_WITH_SECONDS = { 73 | hour: n, 74 | minute: n, 75 | second: n, 76 | hourCycle: "h23", 77 | }; 78 | 79 | export const TIME_24_WITH_SHORT_OFFSET = { 80 | hour: n, 81 | minute: n, 82 | second: n, 83 | hourCycle: "h23", 84 | timeZoneName: s, 85 | }; 86 | 87 | export const TIME_24_WITH_LONG_OFFSET = { 88 | hour: n, 89 | minute: n, 90 | second: n, 91 | hourCycle: "h23", 92 | timeZoneName: l, 93 | }; 94 | 95 | export const DATETIME_SHORT = { 96 | year: n, 97 | month: n, 98 | day: n, 99 | hour: n, 100 | minute: n, 101 | }; 102 | 103 | export const DATETIME_SHORT_WITH_SECONDS = { 104 | year: n, 105 | month: n, 106 | day: n, 107 | hour: n, 108 | minute: n, 109 | second: n, 110 | }; 111 | 112 | export const DATETIME_MED = { 113 | year: n, 114 | month: s, 115 | day: n, 116 | hour: n, 117 | minute: n, 118 | }; 119 | 120 | export const DATETIME_MED_WITH_SECONDS = { 121 | year: n, 122 | month: s, 123 | day: n, 124 | hour: n, 125 | minute: n, 126 | second: n, 127 | }; 128 | 129 | export const DATETIME_MED_WITH_WEEKDAY = { 130 | year: n, 131 | month: s, 132 | day: n, 133 | weekday: s, 134 | hour: n, 135 | minute: n, 136 | }; 137 | 138 | export const DATETIME_FULL = { 139 | year: n, 140 | month: l, 141 | day: n, 142 | hour: n, 143 | minute: n, 144 | timeZoneName: s, 145 | }; 146 | 147 | export const DATETIME_FULL_WITH_SECONDS = { 148 | year: n, 149 | month: l, 150 | day: n, 151 | hour: n, 152 | minute: n, 153 | second: n, 154 | timeZoneName: s, 155 | }; 156 | 157 | export const DATETIME_HUGE = { 158 | year: n, 159 | month: l, 160 | day: n, 161 | weekday: l, 162 | hour: n, 163 | minute: n, 164 | timeZoneName: l, 165 | }; 166 | 167 | export const DATETIME_HUGE_WITH_SECONDS = { 168 | year: n, 169 | month: l, 170 | day: n, 171 | weekday: l, 172 | hour: n, 173 | minute: n, 174 | second: n, 175 | timeZoneName: l, 176 | }; 177 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.6.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 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "3.6.1" 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/zones/IANAZone.js: -------------------------------------------------------------------------------- 1 | import { formatOffset, parseZoneInfo, isUndefined, objToLocalTS } from "../impl/util.js"; 2 | import Zone from "../zone.js"; 3 | 4 | const dtfCache = new Map(); 5 | function makeDTF(zoneName) { 6 | let dtf = dtfCache.get(zoneName); 7 | if (dtf === undefined) { 8 | dtf = new Intl.DateTimeFormat("en-US", { 9 | hour12: false, 10 | timeZone: zoneName, 11 | year: "numeric", 12 | month: "2-digit", 13 | day: "2-digit", 14 | hour: "2-digit", 15 | minute: "2-digit", 16 | second: "2-digit", 17 | era: "short", 18 | }); 19 | dtfCache.set(zoneName, dtf); 20 | } 21 | return dtf; 22 | } 23 | 24 | const typeToPos = { 25 | year: 0, 26 | month: 1, 27 | day: 2, 28 | era: 3, 29 | hour: 4, 30 | minute: 5, 31 | second: 6, 32 | }; 33 | 34 | function hackyOffset(dtf, date) { 35 | const formatted = dtf.format(date).replace(/\u200E/g, ""), 36 | parsed = /(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(formatted), 37 | [, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed; 38 | return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond]; 39 | } 40 | 41 | function partsOffset(dtf, date) { 42 | const formatted = dtf.formatToParts(date); 43 | const filled = []; 44 | for (let i = 0; i < formatted.length; i++) { 45 | const { type, value } = formatted[i]; 46 | const pos = typeToPos[type]; 47 | 48 | if (type === "era") { 49 | filled[pos] = value; 50 | } else if (!isUndefined(pos)) { 51 | filled[pos] = parseInt(value, 10); 52 | } 53 | } 54 | return filled; 55 | } 56 | 57 | const ianaZoneCache = new Map(); 58 | /** 59 | * A zone identified by an IANA identifier, like America/New_York 60 | * @implements {Zone} 61 | */ 62 | export default class IANAZone extends Zone { 63 | /** 64 | * @param {string} name - Zone name 65 | * @return {IANAZone} 66 | */ 67 | static create(name) { 68 | let zone = ianaZoneCache.get(name); 69 | if (zone === undefined) { 70 | ianaZoneCache.set(name, (zone = new IANAZone(name))); 71 | } 72 | return zone; 73 | } 74 | 75 | /** 76 | * Reset local caches. Should only be necessary in testing scenarios. 77 | * @return {void} 78 | */ 79 | static resetCache() { 80 | ianaZoneCache.clear(); 81 | dtfCache.clear(); 82 | } 83 | 84 | /** 85 | * Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that. 86 | * @param {string} s - The string to check validity on 87 | * @example IANAZone.isValidSpecifier("America/New_York") //=> true 88 | * @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false 89 | * @deprecated For backward compatibility, this forwards to isValidZone, better use `isValidZone()` directly instead. 90 | * @return {boolean} 91 | */ 92 | static isValidSpecifier(s) { 93 | return this.isValidZone(s); 94 | } 95 | 96 | /** 97 | * Returns whether the provided string identifies a real zone 98 | * @param {string} zone - The string to check 99 | * @example IANAZone.isValidZone("America/New_York") //=> true 100 | * @example IANAZone.isValidZone("Fantasia/Castle") //=> false 101 | * @example IANAZone.isValidZone("Sport~~blorp") //=> false 102 | * @return {boolean} 103 | */ 104 | static isValidZone(zone) { 105 | if (!zone) { 106 | return false; 107 | } 108 | try { 109 | new Intl.DateTimeFormat("en-US", { timeZone: zone }).format(); 110 | return true; 111 | } catch (e) { 112 | return false; 113 | } 114 | } 115 | 116 | constructor(name) { 117 | super(); 118 | /** @private **/ 119 | this.zoneName = name; 120 | /** @private **/ 121 | this.valid = IANAZone.isValidZone(name); 122 | } 123 | 124 | /** 125 | * The type of zone. `iana` for all instances of `IANAZone`. 126 | * @override 127 | * @type {string} 128 | */ 129 | get type() { 130 | return "iana"; 131 | } 132 | 133 | /** 134 | * The name of this zone (i.e. the IANA zone name). 135 | * @override 136 | * @type {string} 137 | */ 138 | get name() { 139 | return this.zoneName; 140 | } 141 | 142 | /** 143 | * Returns whether the offset is known to be fixed for the whole year: 144 | * Always returns false for all IANA zones. 145 | * @override 146 | * @type {boolean} 147 | */ 148 | get isUniversal() { 149 | return false; 150 | } 151 | 152 | /** 153 | * Returns the offset's common name (such as EST) at the specified timestamp 154 | * @override 155 | * @param {number} ts - Epoch milliseconds for which to get the name 156 | * @param {Object} opts - Options to affect the format 157 | * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'. 158 | * @param {string} opts.locale - What locale to return the offset name in. 159 | * @return {string} 160 | */ 161 | offsetName(ts, { format, locale }) { 162 | return parseZoneInfo(ts, format, locale, this.name); 163 | } 164 | 165 | /** 166 | * Returns the offset's value as a string 167 | * @override 168 | * @param {number} ts - Epoch milliseconds for which to get the offset 169 | * @param {string} format - What style of offset to return. 170 | * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively 171 | * @return {string} 172 | */ 173 | formatOffset(ts, format) { 174 | return formatOffset(this.offset(ts), format); 175 | } 176 | 177 | /** 178 | * Return the offset in minutes for this zone at the specified timestamp. 179 | * @override 180 | * @param {number} ts - Epoch milliseconds for which to compute the offset 181 | * @return {number} 182 | */ 183 | offset(ts) { 184 | if (!this.valid) return NaN; 185 | const date = new Date(ts); 186 | 187 | if (isNaN(date)) return NaN; 188 | 189 | const dtf = makeDTF(this.name); 190 | let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts 191 | ? partsOffset(dtf, date) 192 | : hackyOffset(dtf, date); 193 | 194 | if (adOrBc === "BC") { 195 | year = -Math.abs(year) + 1; 196 | } 197 | 198 | // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat 199 | const adjustedHour = hour === 24 ? 0 : hour; 200 | 201 | const asUTC = objToLocalTS({ 202 | year, 203 | month, 204 | day, 205 | hour: adjustedHour, 206 | minute, 207 | second, 208 | millisecond: 0, 209 | }); 210 | 211 | let asTS = +date; 212 | const over = asTS % 1000; 213 | asTS -= over >= 0 ? over : 1000 + over; 214 | return (asUTC - asTS) / (60 * 1000); 215 | } 216 | 217 | /** 218 | * Return whether this Zone is equal to another zone 219 | * @override 220 | * @param {Zone} otherZone - the zone to compare 221 | * @return {boolean} 222 | */ 223 | equals(otherZone) { 224 | return otherZone.type === "iana" && otherZone.name === this.name; 225 | } 226 | 227 | /** 228 | * Return whether this Zone is valid. 229 | * @override 230 | * @type {boolean} 231 | */ 232 | get isValid() { 233 | return this.valid; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | if (opts.minify && TRUST_MINIFY) { 90 | promises.push( 91 | babelAndRollup(dest, { 92 | ...opts, 93 | minify: true, 94 | filename: "luxon.min.js", 95 | }) 96 | ); 97 | } 98 | 99 | await Promise.all(promises); 100 | 101 | if (opts.minify && !TRUST_MINIFY) { 102 | const code = fs.readFileSync(`build/${dest}/luxon.js`, "utf8"), 103 | ugly = UglifyJS.minify(code, { 104 | toplevel: !opts.global, 105 | output: { 106 | comments: false, 107 | }, 108 | sourceMap: { 109 | filename: `build/${dest}/luxon.js`, 110 | }, 111 | }); 112 | if (ugly.error) { 113 | console.error("Error uglifying", ugly.error); 114 | } else { 115 | fs.writeFileSync(`build/${dest}/luxon.min.js`, ugly.code); 116 | fs.writeFileSync(`build/${dest}/luxon.min.js.map`, ugly.map); 117 | } 118 | } 119 | console.log("Built", dest); 120 | } 121 | 122 | const browsersOld = "last 2 major versions"; 123 | 124 | async function global() { 125 | await buildLibrary("global", { 126 | format: "iife", 127 | global: true, 128 | name: "luxon", 129 | target: browsersOld, 130 | minify: true, 131 | }); 132 | } 133 | 134 | async function amd() { 135 | await buildLibrary("amd", { 136 | format: "amd", 137 | name: "luxon", 138 | target: browsersOld, 139 | minify: true, 140 | }); 141 | } 142 | 143 | async function node() { 144 | await buildLibrary("node", { format: "cjs", target: "node 12" }); 145 | } 146 | 147 | async function cjsBrowser() { 148 | await buildLibrary("cjs-browser", { format: "cjs", target: browsersOld }); 149 | } 150 | 151 | async function es6() { 152 | await buildLibrary("es6", { 153 | format: "es", 154 | minify: true, 155 | compile: false, 156 | }); 157 | } 158 | 159 | async function globalEs6() { 160 | await buildLibrary("global-es6", { 161 | format: "iife", 162 | name: "luxon", 163 | compile: false, 164 | global: true, 165 | }); 166 | } 167 | 168 | async function buildAll() { 169 | await Promise.all([node(), cjsBrowser(), es6(), amd(), global(), globalEs6()]); 170 | } 171 | 172 | module.exports = { 173 | buildAll, 174 | buildNode: node, 175 | buildGlobal: global, 176 | }; 177 | -------------------------------------------------------------------------------- /tasks/buildAll.js: -------------------------------------------------------------------------------- 1 | const { buildAll } = require("./build"); 2 | buildAll().catch(console.error); 3 | -------------------------------------------------------------------------------- /tasks/buildGlobal.js: -------------------------------------------------------------------------------- 1 | const { buildGlobal } = require("./build"); 2 | buildGlobal().catch(console.error); 3 | -------------------------------------------------------------------------------- /tasks/buildNode.js: -------------------------------------------------------------------------------- 1 | const { buildNode } = require("./build"); 2 | buildNode().catch(console.error); 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 returns passed Duration", () => { 133 | expect(() => Duration.fromDurationLike("foo")).toThrow(); 134 | expect(() => Duration.fromDurationLike(null)).toThrow(); 135 | }); 136 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* global test */ 2 | import { DateTime, Settings } from "../src/luxon"; 3 | 4 | exports.withoutRTF = function (name, f) { 5 | const fullName = `With no RelativeTimeFormat support, ${name}`; 6 | test(fullName, () => { 7 | const rtf = Intl.RelativeTimeFormat; 8 | try { 9 | Intl.RelativeTimeFormat = undefined; 10 | Settings.resetCaches(); 11 | f(); 12 | } finally { 13 | Intl.RelativeTimeFormat = rtf; 14 | } 15 | }); 16 | }; 17 | 18 | exports.withoutLocaleWeekInfo = function (name, f) { 19 | const fullName = `With no Intl.Locale.weekInfo support, ${name}`; 20 | test(fullName, () => { 21 | const l = Intl.Locale; 22 | try { 23 | Intl.Locale = undefined; 24 | Settings.resetCaches(); 25 | f(); 26 | } finally { 27 | Intl.Locale = l; 28 | } 29 | }); 30 | }; 31 | 32 | exports.withNow = function (name, dt, f) { 33 | test(name, () => { 34 | const oldNow = Settings.now; 35 | 36 | try { 37 | Settings.now = () => dt.valueOf(); 38 | f(); 39 | } finally { 40 | Settings.now = oldNow; 41 | } 42 | }); 43 | }; 44 | 45 | // not a tester! 46 | exports.withDefaultZone = function (zone, f) { 47 | try { 48 | Settings.defaultZone = zone; 49 | f(); 50 | } finally { 51 | Settings.defaultZone = null; 52 | } 53 | }; 54 | 55 | exports.withDefaultLocale = function (locale, f) { 56 | try { 57 | Settings.defaultLocale = locale; 58 | f(); 59 | } finally { 60 | Settings.defaultLocale = null; 61 | } 62 | }; 63 | 64 | exports.setUnset = function (prop) { 65 | return (value, f) => { 66 | const existing = Settings[prop]; 67 | try { 68 | Settings[prop] = value; 69 | f(); 70 | } finally { 71 | Settings[prop] = existing; 72 | } 73 | }; 74 | }; 75 | 76 | exports.atHour = function (hour) { 77 | return DateTime.fromObject({ year: 2017, month: 5, day: 25 }).startOf("day").set({ hour }); 78 | }; 79 | 80 | exports.cldrMajorVersion = function () { 81 | try { 82 | const cldr = process?.versions?.cldr; 83 | if (cldr) { 84 | const match = cldr.match(/^(\d+)\./); 85 | if (match) { 86 | return parseInt(match[1]); 87 | } 88 | } 89 | return null; 90 | } catch { 91 | return null; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /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/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/info/localeWeek.test.js: -------------------------------------------------------------------------------- 1 | /* global test expect */ 2 | import { Info } from "../../src/luxon"; 3 | 4 | const Helpers = require("../helpers"); 5 | 6 | test("Info.getStartOfWeek reports the correct start of the week", () => { 7 | expect(Info.getStartOfWeek({ locale: "en-US" })).toBe(7); 8 | expect(Info.getStartOfWeek({ locale: "de-DE" })).toBe(1); 9 | }); 10 | 11 | Helpers.withoutLocaleWeekInfo("Info.getStartOfWeek reports Monday as the start of the week", () => { 12 | expect(Info.getStartOfWeek({ locale: "en-US" })).toBe(1); 13 | expect(Info.getStartOfWeek({ locale: "de-DE" })).toBe(1); 14 | }); 15 | 16 | test("Info.getMinimumDaysInFirstWeek reports the correct value", () => { 17 | expect(Info.getMinimumDaysInFirstWeek({ locale: "en-US" })).toBe(1); 18 | expect(Info.getMinimumDaysInFirstWeek({ locale: "de-DE" })).toBe(4); 19 | }); 20 | 21 | Helpers.withoutLocaleWeekInfo("Info.getMinimumDaysInFirstWeek reports 4", () => { 22 | expect(Info.getMinimumDaysInFirstWeek({ locale: "en-US" })).toBe(4); 23 | expect(Info.getMinimumDaysInFirstWeek({ locale: "de-DE" })).toBe(4); 24 | }); 25 | 26 | test("Info.getWeekendWeekdays reports the correct value", () => { 27 | expect(Info.getWeekendWeekdays({ locale: "en-US" })).toStrictEqual([6, 7]); 28 | expect(Info.getWeekendWeekdays({ locale: "he" })).toStrictEqual([5, 6]); 29 | }); 30 | 31 | Helpers.withoutLocaleWeekInfo("Info.getWeekendWeekdays reports [6, 7]", () => { 32 | expect(Info.getWeekendWeekdays({ locale: "en-US" })).toStrictEqual([6, 7]); 33 | expect(Info.getWeekendWeekdays({ locale: "he" })).toStrictEqual([6, 7]); 34 | }); 35 | 36 | test("Info.getStartOfWeek honors the default locale", () => { 37 | Helpers.withDefaultLocale("en-US", () => { 38 | expect(Info.getStartOfWeek()).toBe(7); 39 | expect(Info.getMinimumDaysInFirstWeek()).toBe(1); 40 | expect(Info.getWeekendWeekdays()).toStrictEqual([6, 7]); 41 | }); 42 | 43 | Helpers.withDefaultLocale("de-DE", () => { 44 | expect(Info.getStartOfWeek()).toBe(1); 45 | }); 46 | 47 | Helpers.withDefaultLocale("he", () => { 48 | expect(Info.getWeekendWeekdays()).toStrictEqual([5, 6]); 49 | }); 50 | 51 | Helpers.withDefaultLocale("he", () => { 52 | expect(Info.getWeekendWeekdays()).toStrictEqual([5, 6]); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/interval/parse.test.js: -------------------------------------------------------------------------------- 1 | /* global test expect */ 2 | import { Interval } from "../../src/luxon"; 3 | import Helpers from "../helpers"; 4 | 5 | const withThrowOnInvalid = Helpers.setUnset("throwOnInvalid"); 6 | 7 | //------ 8 | // .fromISO() 9 | //------ 10 | 11 | test("Interval.fromISO can parse a variety of ISO formats", () => { 12 | const check = (s, ob1, ob2) => { 13 | const i = Interval.fromISO(s); 14 | expect(i.start.toObject()).toEqual(ob1); 15 | expect(i.end.toObject()).toEqual(ob2); 16 | }; 17 | 18 | // keeping these brief because I don't want to rehash the existing DT ISO tests 19 | 20 | check( 21 | "2007-03-01T13:00:00/2008-05-11T15:30:00", 22 | { 23 | year: 2007, 24 | month: 3, 25 | day: 1, 26 | hour: 13, 27 | minute: 0, 28 | second: 0, 29 | millisecond: 0, 30 | }, 31 | { 32 | year: 2008, 33 | month: 5, 34 | day: 11, 35 | hour: 15, 36 | minute: 30, 37 | second: 0, 38 | millisecond: 0, 39 | } 40 | ); 41 | 42 | check( 43 | "2007-03-01T13:00:00/2016-W21-3", 44 | { 45 | year: 2007, 46 | month: 3, 47 | day: 1, 48 | hour: 13, 49 | minute: 0, 50 | second: 0, 51 | millisecond: 0, 52 | }, 53 | { 54 | year: 2016, 55 | month: 5, 56 | day: 25, 57 | hour: 0, 58 | minute: 0, 59 | second: 0, 60 | millisecond: 0, 61 | } 62 | ); 63 | 64 | check( 65 | "2007-03-01T13:00:00/P1Y2M10DT2H30M", 66 | { 67 | year: 2007, 68 | month: 3, 69 | day: 1, 70 | hour: 13, 71 | minute: 0, 72 | second: 0, 73 | millisecond: 0, 74 | }, 75 | { 76 | year: 2008, 77 | month: 5, 78 | day: 11, 79 | hour: 15, 80 | minute: 30, 81 | second: 0, 82 | millisecond: 0, 83 | } 84 | ); 85 | 86 | check( 87 | "P1Y2M10DT2H30M/2008-05-11T15:30:00", 88 | { 89 | year: 2007, 90 | month: 3, 91 | day: 1, 92 | hour: 13, 93 | minute: 0, 94 | second: 0, 95 | millisecond: 0, 96 | }, 97 | { 98 | year: 2008, 99 | month: 5, 100 | day: 11, 101 | hour: 15, 102 | minute: 30, 103 | second: 0, 104 | millisecond: 0, 105 | } 106 | ); 107 | }); 108 | 109 | test("Interval.fromISO accepts a zone argument", () => { 110 | const dateDate = Interval.fromISO("2016-01-01/2016-12-31", { zone: "Europe/Paris" }); 111 | expect(dateDate.isValid).toBe(true); 112 | expect(dateDate.start.zoneName).toBe("Europe/Paris"); 113 | 114 | const dateDur = Interval.fromISO("2016-01-01/P1Y", { zone: "Europe/Paris" }); 115 | expect(dateDur.isValid).toBe(true); 116 | expect(dateDur.start.zoneName).toBe("Europe/Paris"); 117 | 118 | const durDate = Interval.fromISO("P1Y/2016-01-01", { zone: "Europe/Paris" }); 119 | expect(durDate.isValid).toBe(true); 120 | expect(durDate.start.zoneName).toBe("Europe/Paris"); 121 | }); 122 | 123 | // #728 124 | test("Interval.fromISO works with Settings.throwOnInvalid", () => { 125 | withThrowOnInvalid(true, () => { 126 | const dateDur = Interval.fromISO("2020-06-22T17:30:00.000+02:00/PT5H30M"); 127 | expect(dateDur.isValid).toBe(true); 128 | 129 | const durDate = Interval.fromISO("PT5H30M/2020-06-22T17:30:00.000+02:00"); 130 | expect(durDate.isValid).toBe(true); 131 | }); 132 | }); 133 | 134 | const badInputs = [ 135 | null, 136 | "", 137 | "hello", 138 | "foo/bar", 139 | "R5/2008-03-01T13:00:00Z/P1Y2M10DT2H30M", // valid ISO 8601 interval with a repeat, but not supported here 140 | ]; 141 | 142 | test.each(badInputs)("Interval.fromISO will return invalid for [%s]", (s) => { 143 | const i = Interval.fromISO(s); 144 | expect(i.isValid).toBe(false); 145 | expect(i.invalidReason).toBe("unparsable"); 146 | }); 147 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /test/zones/fixedOffset.test.js: -------------------------------------------------------------------------------- 1 | /* global test expect */ 2 | import { FixedOffsetZone, IANAZone } from "../../src/luxon"; 3 | 4 | test("FixedOffsetZone.utcInstance returns a singleton", () => { 5 | expect(FixedOffsetZone.utcInstance).toBe(FixedOffsetZone.utcInstance); 6 | }); 7 | 8 | test("FixedOffsetZone.utcInstance provides valid UTC data", () => { 9 | expect(FixedOffsetZone.utcInstance.type).toBe("fixed"); 10 | expect(FixedOffsetZone.utcInstance.name).toBe("UTC"); 11 | expect(FixedOffsetZone.utcInstance.offsetName()).toBe("UTC"); 12 | expect(FixedOffsetZone.utcInstance.formatOffset(0, "short")).toBe("+00:00"); 13 | expect(FixedOffsetZone.utcInstance.isUniversal).toBe(true); 14 | expect(FixedOffsetZone.utcInstance.offset()).toBe(0); 15 | expect(FixedOffsetZone.utcInstance.isValid).toBe(true); 16 | }); 17 | 18 | test("FixedOffsetZone.parseSpecifier returns a valid instance from a UTC offset string", () => { 19 | let zone = FixedOffsetZone.parseSpecifier("UTC+6"); 20 | expect(zone.isValid).toBe(true); 21 | expect(zone.offset()).toBe(360); 22 | expect(zone.name).toBe("UTC+6"); 23 | 24 | zone = FixedOffsetZone.parseSpecifier("UTC+06"); 25 | expect(zone.isValid).toBe(true); 26 | expect(zone.offset()).toBe(360); 27 | expect(zone.name).toBe("UTC+6"); 28 | 29 | zone = FixedOffsetZone.parseSpecifier("UTC-6:00"); 30 | expect(zone.isValid).toBe(true); 31 | expect(zone.offset()).toBe(-360); 32 | expect(zone.name).toBe("UTC-6"); 33 | }); 34 | 35 | test("FixedOffsetZone.parseSpecifier returns null for invalid data", () => { 36 | expect(FixedOffsetZone.parseSpecifier()).toBe(null); 37 | expect(FixedOffsetZone.parseSpecifier(null)).toBe(null); 38 | expect(FixedOffsetZone.parseSpecifier("")).toBe(null); 39 | expect(FixedOffsetZone.parseSpecifier("foo")).toBe(null); 40 | expect(FixedOffsetZone.parseSpecifier("UTC+blorp")).toBe(null); 41 | }); 42 | 43 | test("FixedOffsetZone.formatOffset is consistent despite the provided timestamp", () => { 44 | // formatOffset accepts a timestamp to maintain the call signature of the abstract Zone class, 45 | // but because of the nature of a fixed offset zone instance, the TS is ignored. 46 | const zone = FixedOffsetZone.instance(-300); 47 | expect(zone.formatOffset(0, "techie")).toBe("-0500"); 48 | 49 | // March 9th 2019. A day before DST started 50 | expect(zone.formatOffset(1552089600, "techie")).toBe("-0500"); 51 | 52 | // March 11th 2019. A day after DST started 53 | expect(zone.formatOffset(1552280400, "techie")).toBe("-0500"); 54 | }); 55 | 56 | test("FixedOffsetZone.formatOffset prints the correct sign before the offset", () => { 57 | expect(FixedOffsetZone.instance(-300).formatOffset(0, "short")).toBe("-05:00"); 58 | expect(FixedOffsetZone.instance(-30).formatOffset(0, "short")).toBe("-00:30"); 59 | // Note the negative zero results in a + sign 60 | expect(FixedOffsetZone.instance(-0).formatOffset(0, "short")).toBe("+00:00"); 61 | expect(FixedOffsetZone.instance(0).formatOffset(0, "short")).toBe("+00:00"); 62 | expect(FixedOffsetZone.instance(30).formatOffset(0, "short")).toBe("+00:30"); 63 | expect(FixedOffsetZone.instance(300).formatOffset(0, "short")).toBe("+05:00"); 64 | }); 65 | 66 | test("FixedOffsetZone.equals requires both zones to be fixed", () => { 67 | expect(FixedOffsetZone.utcInstance.equals(IANAZone.create("UTC"))).toBe(false); 68 | }); 69 | 70 | test("FixedOffsetZone.equals compares fixed offset values", () => { 71 | expect(FixedOffsetZone.utcInstance.equals(FixedOffsetZone.instance(0))).toBe(true); 72 | expect(FixedOffsetZone.instance(60).equals(FixedOffsetZone.instance(-60))).toBe(false); 73 | }); 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------