├── .github └── workflows │ ├── build-and-test.yml │ ├── coverage.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── api └── index.js ├── builds ├── spacetime.cjs ├── spacetime.min.js └── spacetime.mjs ├── changelog.md ├── codecov.yml ├── contributing.md ├── eslint.config.js ├── package-lock.json ├── package.json ├── plugins ├── age │ ├── README.md │ ├── cli.js │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ └── src │ │ └── index.js ├── daylight │ ├── README.md │ ├── builds │ │ ├── spacetime-daylight.cjs │ │ ├── spacetime-daylight.min.js │ │ └── spacetime-daylight.mjs │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── scratch.js │ ├── src │ │ ├── index.js │ │ ├── solstices.js │ │ └── sunPosition.js │ └── tests │ │ ├── daytime.test.js │ │ ├── hemisphere.test.js │ │ ├── solstice.test.js │ │ └── sunlight.test.js ├── dst │ ├── README.md │ ├── anytime │ │ ├── lux.js │ │ └── parse.js │ ├── old │ │ ├── scripts │ │ │ ├── findPatterns.js │ │ │ ├── getzones.js │ │ │ ├── split-2.js │ │ │ ├── split-to-files.js │ │ │ └── tznames-lib.js │ │ ├── zonefile │ │ │ ├── aliases.js │ │ │ ├── dst.js │ │ │ ├── missing.js │ │ │ ├── north.js │ │ │ └── south.js │ │ └── zones │ │ │ ├── dst-patterns.js │ │ │ ├── index.js │ │ │ └── patterns.js │ ├── script │ │ └── list-changes.js │ ├── src │ │ ├── calc.js │ │ ├── index.js │ │ ├── patterns.js │ │ ├── test.js │ │ └── zonefile.2022.js │ ├── test │ │ ├── eastern.test.js │ │ └── europe.test.js │ ├── tzdb │ │ ├── aliases.js │ │ ├── download.js │ │ ├── parse.js │ │ ├── test.js │ │ ├── time_zone.csv │ │ └── titlecase.js │ └── zonefile │ │ ├── dst-patterns.js │ │ ├── index.js │ │ ├── metas.js │ │ └── zones.js ├── dst2 │ ├── package.json │ ├── reporter.js │ ├── scratch.js │ ├── src │ │ ├── add.js │ │ ├── index.js │ │ ├── lib │ │ │ ├── format.js │ │ │ ├── native.js │ │ │ └── parse.js │ │ └── units.js │ └── tests │ │ ├── _lib.js │ │ ├── auto.test.js │ │ ├── manual.test.js │ │ └── tmp.test.js ├── geo │ ├── README.md │ ├── builds │ │ ├── spacetime-geo.cjs │ │ ├── spacetime-geo.min.js │ │ └── spacetime-geo.mjs │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── scratch.js │ ├── scripts │ │ └── getPoints │ │ │ ├── iana.json │ │ │ ├── index.js │ │ │ └── qa-test.js │ ├── src │ │ ├── findTz │ │ │ └── index.js │ │ ├── index.js │ │ └── point │ │ │ ├── IANA-points.js │ │ │ └── index.js │ └── tests │ │ ├── in.test.js │ │ └── point.test.js ├── holiday │ ├── README.md │ ├── builds │ │ ├── spacetime-holiday.cjs │ │ ├── spacetime-holiday.min.js │ │ └── spacetime-holiday.mjs │ ├── changelog.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── scratch.js │ ├── src │ │ ├── 01-fixedDates.js │ │ ├── 02-nthWeekday.js │ │ ├── 03-easterDates.js │ │ ├── 04-astronomical.js │ │ ├── 05-lunarDates.js │ │ ├── holidays │ │ │ ├── astro-holidays.js │ │ │ ├── calendar-holidays.js │ │ │ ├── easter-holidays.js │ │ │ ├── fixed-holidays.js │ │ │ ├── lunar-holidays.js │ │ │ └── misc-holidays.js │ │ ├── index.js │ │ └── lib │ │ │ ├── calcEaster.js │ │ │ └── seasons.js │ └── tests │ │ ├── _lib.js │ │ └── misc.test.js ├── play │ ├── scratch.js │ └── src │ │ ├── Ticker.js │ │ └── index.js ├── ticks │ ├── README.md │ ├── _version.js │ ├── assets │ │ ├── bundle.js │ │ └── spencer.min.css │ ├── builds │ │ ├── spacetime-ticks.cjs │ │ ├── spacetime-ticks.min.js │ │ └── spacetime-ticks.mjs │ ├── demo │ │ ├── _drawGraph.js │ │ ├── custom.js │ │ └── duration.js │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── scratch.js │ ├── scripts │ │ ├── filesize.js │ │ └── version.js │ └── src │ │ ├── _reduce.js │ │ ├── index.js │ │ └── methods.js ├── tz │ ├── README.md │ ├── cli.js │ ├── package-lock.json │ └── package.json ├── week-of-month │ ├── README.md │ ├── builds │ │ ├── spacetime-week-of-month.cjs │ │ ├── spacetime-week-of-month.min.js │ │ └── spacetime-week-of-month.mjs │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── index.js │ └── week.test.js └── week-start │ ├── README.md │ ├── _version.js │ ├── builds │ ├── spacetime-week-start.cjs │ ├── spacetime-week-start.min.js │ └── spacetime-week-start.mjs │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── scratch.js │ ├── src │ ├── data │ │ └── countries.js │ ├── index.js │ └── input │ │ └── weekStart.js │ ├── test │ └── basic.test.js │ ├── types │ └── types.d.ts │ ├── weekStart-demo.js │ └── zonefile │ └── iana.js ├── rollup.config.js ├── scratch.js ├── scripts ├── tz │ ├── build.js │ └── iana.js ├── updateZonefile.js └── version.js ├── src ├── _version.js ├── data │ ├── ampm.js │ ├── caseFormat.js │ ├── days.js │ ├── distance.js │ ├── milliseconds.js │ ├── monthLengths.js │ ├── months.js │ ├── quarters.js │ ├── seasons.js │ └── units.js ├── fns.js ├── index.js ├── input │ ├── formats │ │ ├── 01-ymd.js │ │ ├── 02-mdy.js │ │ ├── 03-dmy.js │ │ ├── 04-misc.js │ │ ├── _parsers.js │ │ ├── index.js │ │ ├── parseOffset.js │ │ └── parseTime.js │ ├── helpers.js │ ├── index.js │ ├── named-dates.js │ ├── normalize.js │ └── parse.js ├── methods.js ├── methods │ ├── add.js │ ├── compare.js │ ├── diff │ │ ├── index.js │ │ ├── one.js │ │ └── waterfall.js │ ├── every.js │ ├── format │ │ ├── _offset.js │ │ ├── index.js │ │ └── unixFmt.js │ ├── i18n.js │ ├── nearest.js │ ├── progress.js │ ├── query │ │ ├── 01-time.js │ │ ├── 02-date.js │ │ ├── 03-year.js │ │ └── index.js │ ├── same.js │ ├── set │ │ ├── _model.js │ │ ├── set.js │ │ └── walk.js │ ├── since │ │ ├── _iso.js │ │ ├── getDiff.js │ │ ├── index.js │ │ └── soften.js │ └── startOf.js ├── spacetime.js ├── timezone │ ├── find.js │ ├── guessTz.js │ ├── index.js │ ├── parseOffset.js │ ├── quick.js │ └── summerTime.js └── whereIts.js ├── test ├── add.test.js ├── api.test.js ├── clone.test.js ├── compare.test.js ├── day.test.js ├── dayTime.test.js ├── daysThisMonth.test.js ├── diff.test.js ├── dst-add.ignore.js ├── dst-diff.ignore.js ├── dst-fns.ignore.js ├── dst-misc.ignore.js ├── dst-north.test.js ├── dst-sneak.test.js ├── dst-south.test.js ├── epoch.test.js ├── every.test.js ├── findTz.test.js ├── format.test.js ├── fullDay.test.js ├── goforward.test.js ├── goto.test.js ├── hemisphere.test.js ├── i18n.test.js ├── immutable.test.js ├── informal-tzs.test.js ├── inputs.test.js ├── integration.test.js ├── intl.test.js ├── iso-full.test.js ├── json.test.js ├── leapYear.test.js ├── lib │ ├── dstParse.js │ ├── index.js │ └── useOldTz.js ├── long-years.test.js ├── misc.test.js ├── nearest.test.js ├── now.test.js ├── progress.test.js ├── quarter.test.js ├── query.test.js ├── same.test.js ├── season.test.js ├── semi-destructive.test.js ├── set.test.js ├── since.test.js ├── smoke.test.js ├── startOf.test.js ├── str-parse.test.js ├── subtract.test.js ├── swapTz.test.js ├── timezone-name.test.js ├── toNativeDate.test.js ├── today.test.js ├── types │ ├── constructor.test.ts │ ├── index.ts │ ├── spacetime-static.ts │ ├── tsconfig.json │ └── types.test.ts ├── utcOffset.test.js ├── validation.test.js ├── week.test.js ├── weekStart.test.js └── world.test.js ├── types ├── constraints.d.ts ├── constructors.d.ts ├── index.d.cts ├── index.d.ts └── types.d.ts └── zonefile ├── _build.js ├── _prefixes.js ├── aliases.js ├── iana.js ├── pack.js └── unpack.js /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 22.x, 24.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: use node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: cache dependencies 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-npm-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-npm-${{ matrix.node-version }}- 28 | ${{ runner.os }}-npm- 29 | 30 | - name: npm install, build, and test 31 | run: | 32 | npm ci 33 | npm i eslint ts-node typescript 34 | npm run lint 35 | npm run pack 36 | npm run build 37 | npm run testb 38 | # npm run test:types 39 | env: 40 | CI: true 41 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | # sends test-coverage data to codecov.io 2 | # https://codecov.io/gh/spencermountain/spacetime 3 | name: Coverage 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | getCoverage: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '20' 18 | 19 | - uses: actions/cache@v4 20 | with: 21 | path: ~/.npm 22 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | ${{ runner.os }}-node- 25 | 26 | - run: npm ci 27 | - run: npm run codecov 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | env: 14 | CI: true 15 | 16 | # Note that these steps are *identical* to build-and-test (with the caveat 17 | # that build-and-test uses several versions of Node, and Release only uses 18 | # 10.x) at least until the actual publishing happens. Ideally, we could 19 | # delegate to the build- and-test workflow, but I haven't found a way to do 20 | # that yet. 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20.x 28 | 29 | - name: cache dependencies 30 | uses: actions/cache@v3 31 | with: 32 | path: ~/.npm 33 | key: ${{ runner.os }}-npm-10.x-${{ hashFiles('package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-npm-10.x- 36 | ${{ runner.os }}-npm- 37 | 38 | - name: install 39 | run: | 40 | npm ci 41 | npm i --no-save eslint ts-node typescript 42 | 43 | - name: static checks 44 | run: | 45 | npm run lint 46 | 47 | - name: build 48 | run: | 49 | npm run build 50 | 51 | - name: test 52 | run: | 53 | npm run testb 54 | npm run test:types 55 | 56 | # And finally... publish it! Note that we create the .npmrc file 57 | # "just in time" so that `npm publish` can get the auth token from the 58 | # environment 59 | - name: publish 60 | run: | 61 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 62 | npm publish --access public --provenance 63 | env: 64 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .jshintrc 3 | .env 4 | node_modules/ 5 | .nyc_output 6 | npm-debug.log 7 | viz/ 8 | coverage/ 9 | coverage.lcov 10 | .vscode 11 | .gitignore 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | demo/ 3 | scripts/ 4 | examples/ 5 | .babelrc 6 | .esformatter 7 | .eslintrc 8 | scratch.js 9 | *.tsv 10 | changelog.md 11 | contributing.md 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Spencer Kelly 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: false 3 | comment: false 4 | branches: 5 | - 'master' 6 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | Hi! Pull requests are welcome and always respected. 2 | 3 | If the PR is a big one, or adds a new feature, please open an issue to ask questions first. 4 | 5 | We promise to avoid pedantic or cosmetic disputes! 6 | 7 | ### To get it running: 8 | 9 | - [fork](https://help.github.com/articles/fork-a-repo/) the repository 10 | - [clone](https://help.github.com/articles/cloning-a-repository/) it to your computer 11 | - set environment variable `TESTENV=dev` or `TESTENV=prod` in .env file 12 | 13 | ```bash 14 | cd spacetime 15 | npm install 16 | npm test 17 | ``` 18 | 19 | ### Making changes 20 | 21 | - make your changes in `./src` 22 | - make sure the tests still pass `npm test` 23 | - for bonus points - add a few tests in `./tests` (doesn't matter where) for the new behaviour 24 | 25 | - create a [Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) 26 | 27 | don't worry about incrementing package numbers, or kicking-off builds. Releases will be handled by the maintainers. 28 | 29 | Lastly, thank you! A usable timezone library in the browser is serious and important project. 30 | 31 | It's done collaboratively or badly! 32 | 33 | ### Agreement to Code of Conduct 34 | 35 | By participating and contributing to Spacetime -- you agree to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct) Code of Conduct. Lack of familiarity with this Code of Conduct is not an excuse for not adhering to it. 36 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import * as regexpPlugin from "eslint-plugin-regexp" 2 | 3 | export default [ 4 | regexpPlugin.configs["flat/recommended"], 5 | { 6 | "ignores": ["**/builds/*"], 7 | "rules": { 8 | "regexp/no-misleading-capturing-group": 0, //todo remove this 9 | "regexp/no-super-linear-backtracking": 0, //todo remove this, too 10 | "comma-dangle": [1, "only-multiline"], 11 | "quotes": [0, "single", "avoid-escape"], 12 | "max-nested-callbacks": [1, 4], 13 | "max-params": [1, 5], 14 | "consistent-return": 1, 15 | "no-bitwise": 1, 16 | "no-empty": 1, 17 | "no-console": 1, 18 | "no-duplicate-imports": 1, 19 | "no-eval": 2, 20 | "no-implied-eval": 2, 21 | "no-mixed-operators": 2, 22 | "no-multi-assign": 2, 23 | "no-nested-ternary": 1, 24 | "no-prototype-builtins": 0, 25 | "no-self-compare": 1, 26 | "no-sequences": 1, 27 | "no-shadow": 2, 28 | "no-unmodified-loop-condition": 1, 29 | "no-use-before-define": 1, 30 | "prefer-const": 1, 31 | "radix": 1, 32 | "no-unused-vars": 1, 33 | "regexp/prefer-d": 0, 34 | "regexp/prefer-w": 0, 35 | "regexp/prefer-range": 0, 36 | "regexp/no-unused-capturing-group": 0, 37 | "regexp/optimal-quantifier-concatenation": 0 38 | } 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /plugins/age/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
calculate human age
4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | npm i space-age 16 |
17 | 18 | by 19 | Spencer Kelly 20 | 21 |
22 |

23 | 24 | a spacetime plugin to reckon a person's age, in any unit, given their birthday. 25 | 26 | ### javascript api: 27 | ```js 28 | import spacetime from 'spacetime' 29 | import plugin from 'space-age' 30 | spacetime.extend(plugin) 31 | 32 | // set a birthday 33 | let s = spacetime('march 28 1986') 34 | s.age() 35 | // 35 36 | 37 | // get your age in months, weeks 38 | s.age('days') 39 | // 12,770 40 | 41 | s.age('months') 42 | // 441 43 | ``` 44 | 45 | ### command-line api: 46 | ```bash 47 | npx space-age may 18 1984 48 | 49 | npx space-age may 1st 1984 --months 50 | ``` 51 | or you can install it locally with `npm i -g space-age` 52 | 53 | 54 | MIT -------------------------------------------------------------------------------- /plugins/age/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import spacetime from 'spacetime' 3 | import minimist from 'minimist' 4 | import plg from './src/index.js' 5 | spacetime.extend(plg) 6 | 7 | const defaults = { 8 | unit: 'years' 9 | } 10 | const help = function () { 11 | console.log(`\n\n space-age - calculate human age from a birthdate`) 12 | console.log(`\n Usage: \`npx space-age march 24th 1982\``) 13 | console.log(`\n Usage: \`npx space-age march 24th 1982 --months\``) 14 | console.log('\n\n') 15 | } 16 | 17 | const alias = { 18 | h: 'help', 19 | y: 'years', 20 | d: 'days', 21 | day: 'days', 22 | hour: 'hours', 23 | year: 'years', 24 | month: 'months', 25 | m: 'months', 26 | q: 'quarters' 27 | } 28 | 29 | let opts = minimist(process.argv.slice(2), { alias: alias }) 30 | const str = opts._.join(' ').trim() 31 | 32 | if (!str || opts.help) { 33 | help() 34 | process.exit() 35 | } 36 | 37 | opts = Object.assign({}, defaults, opts) 38 | 39 | let unit = 'years' 40 | if (opts.months) { 41 | unit = 'months' 42 | } 43 | if (opts.quarters) { 44 | unit = 'quarters' 45 | } 46 | if (opts.days) { 47 | unit = 'days' 48 | } 49 | if (opts.hours) { 50 | unit = 'hours' 51 | } 52 | if (opts.weeks) { 53 | unit = 'weeks' 54 | } 55 | 56 | const num = spacetime(str).age(unit) 57 | let output = `${num.toLocaleString()}` 58 | if (unit !== 'years') { 59 | output += ` (${unit})` 60 | } 61 | console.log(output) 62 | -------------------------------------------------------------------------------- /plugins/age/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space-age", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "space-age", 9 | "version": "0.0.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "minimist": "^1.2.5", 13 | "spacetime": "6.16.3" 14 | }, 15 | "bin": { 16 | "space-age": "cli.js" 17 | }, 18 | "engines": { 19 | "node": ">=8" 20 | } 21 | }, 22 | "node_modules/minimist": { 23 | "version": "1.2.5", 24 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 25 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 26 | }, 27 | "node_modules/spacetime": { 28 | "version": "6.16.3", 29 | "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz", 30 | "integrity": "sha512-JQEfj3VHT1gU1IMV5NvhgAP8P+2mDFd84ZCiHN//dp6hRKmuW0IizHissy62lO0nilfFjVhnoSaMC7te+Y5f4A==" 31 | } 32 | }, 33 | "dependencies": { 34 | "minimist": { 35 | "version": "1.2.5", 36 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 37 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 38 | }, 39 | "spacetime": { 40 | "version": "6.16.3", 41 | "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz", 42 | "integrity": "sha512-JQEfj3VHT1gU1IMV5NvhgAP8P+2mDFd84ZCiHN//dp6hRKmuW0IizHissy62lO0nilfFjVhnoSaMC7te+Y5f4A==" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugins/age/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space-age", 3 | "version": "0.1.0", 4 | "description": "a CLI calendar", 5 | "main": "cli.js", 6 | "bin": { 7 | "space-age": "cli.js" 8 | }, 9 | "type": "module", 10 | "sideEffects": false, 11 | "exports": { 12 | ".": { 13 | "import": "./cli.js", 14 | "default": "./cli.js" 15 | } 16 | }, 17 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/space-age", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/spencermountain/spacetime.git" 21 | }, 22 | "engines": { 23 | "node": ">=8" 24 | }, 25 | "scripts": { 26 | "test": "" 27 | }, 28 | "prettier": { 29 | "trailingComma": "none", 30 | "tabWidth": 2, 31 | "semi": false, 32 | "singleQuote": true, 33 | "printWidth": 100 34 | }, 35 | "dependencies": { 36 | "minimist": "^1.2.5", 37 | "spacetime": ">=6.3.0" 38 | }, 39 | "license": "MIT" 40 | } -------------------------------------------------------------------------------- /plugins/age/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import babel from 'rollup-plugin-babel' 6 | import sizeCheck from 'rollup-plugin-filesize-check' 7 | import { version } from './package.json' 8 | 9 | console.log('\n 📦 - running rollup..\n') 10 | 11 | const banner = '/* spencermountain/space-age ' + version + ' MIT */' 12 | 13 | export default [ 14 | { 15 | input: 'src/index.js', 16 | output: [{ banner: banner, file: 'builds/space-age.mjs', format: 'esm' }], 17 | plugins: [ 18 | resolve(), 19 | json(), 20 | commonjs(), 21 | babel({ 22 | babelrc: false, 23 | presets: ['@babel/preset-env'] 24 | }), 25 | sizeCheck({ expect: 2, warn: 10 }) 26 | ] 27 | }, 28 | { 29 | input: 'src/index.js', 30 | output: [ 31 | { 32 | banner: banner, 33 | file: 'builds/space-age.js', 34 | format: 'umd', 35 | sourcemap: false, 36 | name: 'spaceAge' 37 | } 38 | ], 39 | plugins: [ 40 | resolve(), 41 | json(), 42 | commonjs(), 43 | babel({ 44 | babelrc: false, 45 | presets: ['@babel/preset-env'] 46 | }), 47 | sizeCheck({ expect: 2, warn: 10 }) 48 | ] 49 | }, 50 | { 51 | input: 'src/index.js', 52 | output: [ 53 | { 54 | banner: banner, 55 | file: 'builds/space-age.min.js', 56 | format: 'umd', 57 | name: 'spaceAge' 58 | } 59 | ], 60 | plugins: [ 61 | resolve(), 62 | json(), 63 | commonjs(), 64 | babel({ 65 | babelrc: false, 66 | presets: ['@babel/preset-env'] 67 | }), 68 | terser(), 69 | sizeCheck({ expect: 2, warn: 10 }) 70 | ] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /plugins/age/src/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | age: function (unit = 'years') { 3 | const now = this.set() 4 | const diff = this.diff(now, unit) 5 | return diff 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /plugins/daylight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-daylight", 3 | "version": "1.5.2", 4 | "description": "calculate approximate sunlight times for a given timezone", 5 | "main": "builds/spacetime-daylight.mjs", 6 | "unpkg": "builds/spacetime-daylight.min.js", 7 | "module": "builds/spacetime-daylight.mjs", 8 | "type": "module", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "require": "./builds/spacetime-daylight.cjs", 13 | "import": "./builds/spacetime-daylight.mjs", 14 | "default": "./builds/spacetime-daylight.mjs" 15 | } 16 | }, 17 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/spacetime-daylight", 18 | "scripts": { 19 | "watch": "node --watch ./scratch.js", 20 | "test": "\"node_modules/.bin/tape\" \"./tests/*.test.js\" | \"node_modules/.bin/tap-dancer\" --color", 21 | "build": "rollup -c --silent" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/spencermountain/spacetime.git" 26 | }, 27 | "keywords": [ 28 | "sunlight", 29 | "timezone" 30 | ], 31 | "author": "spencermountain@gmail.com", 32 | "dependencies": { 33 | "spacetime-geo": "1.4.1", 34 | "suncalc": "1.9.0" 35 | }, 36 | "devDependencies": { 37 | "spacetime": ">=7.4.0", 38 | "tap-dancer": "0.3.4", 39 | "tape": "5.9.0" 40 | }, 41 | "license": "MIT" 42 | } -------------------------------------------------------------------------------- /plugins/daylight/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import sizeCheck from 'rollup-plugin-filesize-check' 6 | import pkg from './package.json' assert { type: "json" }; 7 | console.log('\n 📦 - running rollup..\n') 8 | 9 | const banner = '/* spencermountain/spacetime-daylight ' + pkg.version + ' MIT */' 10 | 11 | export default [ 12 | { 13 | input: 'src/index.js', 14 | output: [{ banner: banner, file: 'builds/spacetime-daylight.mjs', format: 'esm' }], 15 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 132, warn: 10 })] 16 | }, 17 | { 18 | input: 'src/index.js', 19 | output: [{ banner: banner, file: 'builds/spacetime-daylight.cjs', format: 'umd', sourcemap: false, name: 'spacetimeDaylight' }], 20 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 134, warn: 10 })] 21 | }, 22 | { 23 | input: 'src/index.js', 24 | output: [{ banner: banner, file: 'builds/spacetime-daylight.min.js', format: 'umd', name: 'spacetimeDaylight' }], 25 | plugins: [resolve(), json(), commonjs(), terser(), sizeCheck({ expect: 95, warn: 10 })] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /plugins/daylight/scratch.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import sunlight from './src/index.js' 3 | // console.log(tzlookup(42.7235, -73.6931)); // "America/New_York" 4 | // console.log(tzlookup(48.7235, 1.9931)); // paris 5 | // console.log(tzlookup(50.4050, -31.8971)); // atlantic ocean 6 | 7 | spacetime.extend(sunlight) 8 | 9 | // let s = spacetime.today('America/Iqaluit').time('3:00am') 10 | const s = spacetime.today('america/argentina/comodrivadavia') 11 | // let s = spacetime.today('America/Havana').time('3:00am') 12 | 13 | // ---day--- 14 | // let hours = s.every('hour', s.add(1, 'day').time('3:00am')) 15 | console.log(s.sunPosition()) 16 | // }) 17 | // ---year-- 18 | // let hours = s.every('hour', s.add(1, 'day')) 19 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/daylight/src/solstices.js: -------------------------------------------------------------------------------- 1 | // the average time between solstices, on timeanddate.com 2 | // approx 88 days, 23 hours, 30 mins 3 | const oneYear = 31557060000 4 | 5 | const halfYear = 15855660000 6 | // strangely, this does not seem to be exactly half. 7 | // const halfYear = oneYear / 2 8 | 9 | // the 2015 winter solstice 10 | const oneWinter = 1450759620000 11 | 12 | const goForward = function (epoch) { 13 | let num = oneWinter + oneYear 14 | while (num < epoch) { 15 | num += oneYear 16 | } 17 | return num 18 | } 19 | const goBackward = function (epoch) { 20 | let num = oneWinter - oneYear 21 | while (num > epoch) { 22 | num -= oneYear 23 | } 24 | return num 25 | } 26 | 27 | const solstice = function (s) { 28 | let found = null 29 | if (s.epoch > oneWinter) { 30 | found = goForward(s.epoch) 31 | } else { 32 | found = goBackward(s.epoch) 33 | } 34 | let winter = s.set(found) 35 | // ensure it's the right year 36 | if (winter.year() < s.year()) { 37 | winter = winter.set(winter.epoch + oneYear) 38 | } 39 | if (winter.year() > s.year()) { 40 | winter = winter.set(winter.epoch - oneYear) 41 | } 42 | const summer = winter.set(winter.epoch - halfYear) 43 | return { 44 | winter: winter, 45 | summer: summer, 46 | } 47 | } 48 | // const equinox = function (s) { 49 | // return { 50 | // summer: null, 51 | // winter: null, 52 | // } 53 | // } 54 | export default solstice 55 | -------------------------------------------------------------------------------- /plugins/daylight/src/sunPosition.js: -------------------------------------------------------------------------------- 1 | import sunCalc from 'suncalc' 2 | import spacetimeGeo from 'spacetime-geo' 3 | 4 | function toDegree(radians) { 5 | const pi = Math.PI 6 | return radians * (180 / pi) 7 | } 8 | 9 | const sunPosition = function (s, lat, lng) { 10 | if (lat === undefined || lng === undefined) { 11 | const guess = s.point() 12 | lat = guess.lat 13 | lng = guess.lng 14 | } 15 | if (!lat || !lng) { 16 | return {} 17 | } 18 | s.in = s.in || spacetimeGeo.in //bolt-on the plugin 19 | s = s.in(lat, lng) 20 | const d = new Date(s.epoch) 21 | const res = sunCalc.getPosition(d, lat, lng) 22 | // return res 23 | return { 24 | azimuth: toDegree(res.azimuth), 25 | altitude: toDegree(res.altitude), 26 | } 27 | } 28 | export default sunPosition 29 | -------------------------------------------------------------------------------- /plugins/daylight/tests/daytime.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from 'spacetime' 3 | import daylight from '../src/index.js' 4 | // import daylight from '../builds/spacetime-daylight.mjs' 5 | spacetime.extend(daylight) 6 | 7 | test('day-status-summer', function (t) { 8 | const s = spacetime('June 26 2018', 'Canada/Eastern') 9 | let o = {} 10 | o = s.time('3:30am').daylight() 11 | t.equal(o.current.status, 'night', '3:30am') 12 | o = s.time('5:30am').daylight() 13 | t.equal(o.current.status, 'dawn', '5:30am') 14 | o = s.time('11:30am').daylight() 15 | t.equal(o.current.status, 'day', '11:30am') 16 | o = s.time('5:30pm').daylight() 17 | t.equal(o.current.status, 'day', '5:30pm') 18 | o = s.time('9:30pm').daylight() 19 | t.equal(o.current.status, 'dusk', '9:30pm') 20 | o = s.time('11:30pm').daylight() 21 | t.equal(o.current.status, 'night', '11:30pm') 22 | t.equal(o.current.progress, 0, 'no-sun-11:30pm') 23 | t.end() 24 | }) 25 | 26 | test('day-status-winter', function (t) { 27 | const s = spacetime('November 26 2018', 'Canada/Eastern') 28 | let o = {} 29 | o = s.time('3:30am').daylight() 30 | t.equal(o.current.status, 'night', '3:30am') 31 | t.equal(o.current.progress, 0, 'no-sun-3am') 32 | o = s.time('7:00am').daylight() 33 | t.equal(o.current.status, 'dawn', '7:00am') 34 | o = s.time('11:30am').daylight() 35 | t.equal(o.current.status, 'day', '11:30am') 36 | o = s.time('4:30pm').daylight() 37 | t.equal(o.current.status, 'day', '4:30pm') 38 | o = s.time('5:00pm').daylight() 39 | t.equal(o.current.status, 'dusk', '5:00pm') 40 | o = s.time('11:30pm').daylight() 41 | t.equal(o.current.status, 'night', '11:30pm') 42 | t.end() 43 | }) 44 | -------------------------------------------------------------------------------- /plugins/daylight/tests/hemisphere.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from 'spacetime' 3 | import daylight from '../src/index.js' 4 | // import daylight from '../builds/spacetime-daylight.mjs' 5 | spacetime.extend(daylight) 6 | 7 | test('southern-hemisphere-opposite', function (t) { 8 | let s = spacetime('December 16 2018', 'Australia/Canberra') 9 | t.equal(s.daylight().duration.human.hours, 14, 'long-days in Australia') 10 | 11 | s = spacetime('June 12 2018', 'Australia/Canberra') 12 | t.equal(s.daylight().duration.human.hours, 9, 'short-days in Australia') 13 | 14 | s = spacetime('December 12 2018', 'America/Sao_Paulo') 15 | t.equal(s.daylight().duration.human.hours, 13, 'long-days in brazil') 16 | 17 | s = spacetime('June 12 2018', 'America/Sao_Paulo') 18 | t.equal(s.daylight().duration.human.hours, 10, 'short-days in brazil') 19 | t.end() 20 | }) 21 | -------------------------------------------------------------------------------- /plugins/daylight/tests/sunlight.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from 'spacetime' 3 | import daylight from '../src/index.js' 4 | // import daylight from '../builds/spacetime-daylight.mjs' 5 | spacetime.extend(daylight) 6 | 7 | test('day-length-winter', function (t) { 8 | const s = spacetime('December 16 2018') 9 | const newYork = s.daylight(42.7235, -73.6931) 10 | t.equal(newYork.duration.human.hours, 9, 'short-days in NY') 11 | const equator = s.daylight(0, 0) 12 | t.equal(equator.duration.human.hours, 12, 'medium-days in equator') 13 | t.end() 14 | }) 15 | 16 | test('day-length-summer', function (t) { 17 | const s = spacetime('June 21 2018') 18 | const newYork = s.daylight(42.7235, -73.6931) 19 | t.equal(newYork.duration.human.hours, 15, 'long-days in NY') 20 | const equator = s.daylight(0, 0) 21 | t.equal(equator.duration.human.hours, 12, 'medium-days in equator') 22 | t.end() 23 | }) 24 | 25 | test('using-point()-winter', function (t) { 26 | let s = spacetime('December 16 2018', 'Canada/Eastern') 27 | const newYork = s.daylight() 28 | t.equal(newYork.duration.human.hours, 8, 'short-days in Toronto') 29 | s = s.goto('Africa/Freetown') 30 | const equator = s.daylight() 31 | t.equal(equator.duration.human.hours, 11, 'medium-days in sierra-leone') 32 | t.end() 33 | }) 34 | 35 | test('using-point()-summer', function (t) { 36 | let s = spacetime('June 21 2018', 'Canada/Eastern') 37 | const newYork = s.daylight() 38 | t.equal(newYork.duration.human.hours, 15, 'long-days in Toronto') 39 | s = s.goto('Africa/Freetown') 40 | const equator = s.daylight() 41 | t.equal(equator.duration.human.hours, 12, 'medium-days in sierra-leone') 42 | t.end() 43 | }) 44 | -------------------------------------------------------------------------------- /plugins/dst/anytime/lux.js: -------------------------------------------------------------------------------- 1 | import spacetime from '../../../src/index.js' 2 | import { DateTime } from "luxon"; 3 | const test = function (epoch, tz) { 4 | // DateTime.now().setZone('America/New_York').minus({ weeks: 1 }).endOf('day').toISO(); 5 | const d = DateTime.fromMillis(epoch).setZone() 6 | console.log('lux: ', d.year, d.month, d.day, d.hour) 7 | 8 | const s = spacetime(epoch, tz) 9 | console.log('spc:', s.year(), s.month(), s.day(), s.hour()) 10 | } 11 | test(1636264800000, 'Europe/London') -------------------------------------------------------------------------------- /plugins/dst/anytime/parse.js: -------------------------------------------------------------------------------- 1 | import spacetime from '../../../src/index.js' 2 | import changes from '../tzdb/parse.js' 3 | 4 | import { DateTime } from "luxon"; 5 | 6 | const fromEpoch = function (epoch, tz) { 7 | return DateTime.fromMillis(epoch).setZone(tz) 8 | } 9 | 10 | 11 | const hour = 1000 * 60 * 60 12 | const start = 2020 13 | const doTZ = function (tz) { 14 | const arr = [] 15 | for (let y = start; y < start + 5; y += 1) { 16 | // add start of year 17 | // let s = spacetime.now(tz) 18 | // s = s.year(y).startOf('year') 19 | // arr.push([s.epoch, y, 1, 1]) 20 | // add dst 21 | if (changes[tz][y]) { 22 | // add dst start 23 | let epoch = changes[tz][y][0] 24 | const on = fromEpoch(epoch, tz) 25 | arr.push([epoch, on.year, on.month, on.day, on.hour]) 26 | 27 | // add dst end 28 | epoch = changes[tz][y][1] 29 | // epoch -= hour 30 | // epoch += hour 31 | const off = fromEpoch(epoch, tz) 32 | arr.push([epoch, off.year, off.month, off.day, off.hour]) 33 | } 34 | } 35 | return arr 36 | } 37 | 38 | const arr = ['America/Toronto', 'America/Vancouver', 'europe/london'] 39 | const out = arr.reduce((h, id) => { 40 | h[id] = doTZ('America/Toronto') 41 | return h 42 | }, {}) 43 | console.log(out) -------------------------------------------------------------------------------- /plugins/dst/old/scripts/findPatterns.js: -------------------------------------------------------------------------------- 1 | // eu - 03/28:02->10/31:03 2 | const zones = require('/Users/spencer/mountain/spacetime/zonefile/iana.js') 3 | let keys = Object.keys(zones) 4 | console.log(keys.length) 5 | keys = keys.filter((k) => { 6 | return zones[k].dst //&& !zones[k].dst.includes('03/28') 7 | }) 8 | // usa 9 | keys = keys.filter((k) => zones[k].dst !== '03/14:02->11/07:02') 10 | // australia 11 | // keys = keys.filter((k) => zones[k].dst !== '04/04:03->10/03:02') 12 | // // mexico 13 | // keys = keys.filter((k) => zones[k].dst !== '04/04:02->10/31:02') 14 | console.log(keys.length) 15 | console.log(keys) 16 | -------------------------------------------------------------------------------- /plugins/dst/old/scripts/getzones.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs') 2 | // const path = require('path') 3 | // const sh = require('shelljs') 4 | // let year = 2021 5 | // let tz = 'australia/melbourne' 6 | 7 | const tzs = require('/Users/spencer/mountain/spacetime/zonefile/iana.js') 8 | const list = Object.keys(tzs) 9 | 10 | // const titleCase = (str) => { 11 | // str = str[0].toUpperCase() + str.substr(1) 12 | // str = str.replace(/\/gmt/, '/GMT') 13 | // str = str.replace(/[\/_]([a-z])/gi, (s) => { 14 | // return s.toUpperCase() 15 | // }) 16 | // return str 17 | // } 18 | 19 | // console.log(list) 20 | // list.forEach((tz) => { 21 | // tz = titleCase(tz) 22 | // console.log(tz) 23 | // let lines = sh.exec(`zdump -v ${tz} | grep ${year}`).toString().split('\n') 24 | // console.log(lines) 25 | // }) 26 | 27 | // const zones = require('/Users/spencer/Desktop/zones.json') 28 | // console.log(Object.keys(zones.zoneData)) 29 | // console.log(Object.keys(zones.zoneData.Canada)) 30 | // console.log(Object.keys(zones.zoneData.EST)) 31 | // console.log(zones.zoneData.Canada.Pacific) 32 | 33 | const parse = require('parse-zoneinfo') 34 | 35 | list.slice(9, 10).forEach((tz) => { 36 | parse(tz, function (err, tzdata) { 37 | if (err) { 38 | console.log(tz) 39 | // console.log(err) 40 | } else { 41 | console.log(tzdata) 42 | } 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /plugins/dst/old/scripts/split-2.js: -------------------------------------------------------------------------------- 1 | const zones = require('/Users/spencer/mountain/spacetime/zonefile/iana.js') 2 | let keys = Object.keys(zones) 3 | 4 | const titleCase = (str) => { 5 | str = str[0].toUpperCase() + str.substr(1) 6 | str = str.replace(/\/gmt/, '/GMT') 7 | str = str.replace(/[\/_]([a-z])/gi, (s) => { 8 | return s.toUpperCase() 9 | }) 10 | return str 11 | } 12 | 13 | keys = keys.filter((k) => zones[k].dst) 14 | // keys = keys.filter((k) => zones[k].hem === 's') 15 | // keys = keys.map((k) => titleCase(k)) 16 | 17 | const byDate = {} 18 | keys.forEach((k) => { 19 | const off = '' + zones[k].offset + 'h ' + zones[k].dst 20 | const init = { 21 | name: '', 22 | abbrev: '', 23 | abbrev_dst: '', 24 | offset: zones[k].offset, 25 | pattern: {}, 26 | dst: zones[k].dst, 27 | ids: [] 28 | } 29 | byDate[off] = byDate[off] || init 30 | const id = titleCase(k) 31 | byDate[off].ids.push(id) 32 | }) 33 | let result = Object.values(byDate) 34 | result = result 35 | .sort((a, b) => { 36 | if (a.offset > b.offset) { 37 | return -1 38 | } else if (a.offset < b.offset) { 39 | return 1 40 | } 41 | return 0 42 | }) 43 | .reverse() 44 | console.log(result.length) 45 | // console.log(JSON.stringify(result, null, 2)) 46 | -------------------------------------------------------------------------------- /plugins/dst/old/scripts/split-to-files.js: -------------------------------------------------------------------------------- 1 | const zones = require('/Users/spencer/mountain/spacetime/zonefile/iana.js') 2 | let keys = Object.keys(zones) 3 | 4 | const titleCase = (str) => { 5 | str = str[0].toUpperCase() + str.substr(1) 6 | str = str.replace(/\/gmt/, '/GMT') 7 | str = str.replace(/[\/_]([a-z])/gi, (s) => { 8 | return s.toUpperCase() 9 | }) 10 | return str 11 | } 12 | 13 | keys = keys.filter((k) => !zones[k].dst) 14 | keys = keys.filter((k) => zones[k].hem === 's') 15 | // keys = keys.map((k) => titleCase(k)) 16 | 17 | const byOffset = {} 18 | keys.forEach((k) => { 19 | const off = '' + zones[k].offset 20 | byOffset[off] = byOffset[off] || [] 21 | const id = titleCase(k) 22 | byOffset[off].push(id) 23 | }) 24 | 25 | const res = {} 26 | let nums = Object.keys(byOffset) 27 | nums = nums.sort((a, b) => { 28 | if (a > b) { 29 | return -1 30 | } else if (a < b) { 31 | return 1 32 | } 33 | return 0 34 | }) 35 | nums.forEach((num) => { 36 | res[num] = byOffset[num] 37 | byOffset[num] = byOffset[num].sort((a, b) => { 38 | a = Number(a) 39 | b = Number(b) 40 | if (a > b) { 41 | return -1 42 | } else if (a < b) { 43 | return 1 44 | } 45 | return 0 46 | }) 47 | }) 48 | // let byRegion = {} 49 | // keys.forEach((k) => { 50 | // let split = k.split(/\//) 51 | // byRegion[split[0]] = byRegion[split[0]] || [] 52 | // byRegion[split[0]].push(split[1]) 53 | // }) 54 | 55 | console.log(JSON.stringify(res, null, 2)) 56 | -------------------------------------------------------------------------------- /plugins/dst/old/scripts/tznames-lib.js: -------------------------------------------------------------------------------- 1 | const data = require('/Users/spencer/Downloads/data.json') 2 | 3 | // let key = 'DisplayNames' // 140 metazones 4 | // let key = 'TzdbZoneCountries' // which country iana is in (424 of them) 5 | // let key = 'CldrZoneCountries' 6 | // let key = 'CldrMetazones' // * 7 | // let key = 'CldrPrimaryZones' 8 | // let key = 'CldrAliases' 9 | const key = 'CldrLanguageData' //i18n place-names 10 | // let key = 'SelectionZones' 11 | // console.log(Object.keys(data)) 12 | console.log(Object.keys(data[key])) 13 | // console.log(data[key].en) 14 | 15 | // displaynames[en] = 140 16 | // "'North Asia East Standard Time'" 17 | // console.log(data['DisplayNames']['en_US']) 18 | 19 | let names = {} 20 | console.log(JSON.stringify(Object.keys(data[key]), null, 2)) 21 | Object.keys(data[key]).forEach((lang) => { 22 | // console.log(data[key][lang].ShortNames) 23 | names = Object.assign(names, data[key][lang].ShortNames) 24 | }) 25 | // console.log(JSON.stringify(names, null, 2)) 26 | 27 | // 'Africa_Eastern' 28 | // console.log(data['DisplayNames']) 29 | // const byMeta = {} 30 | // Object.keys(data.CldrMetazones).forEach((k) => { 31 | // let code = data.CldrMetazones[k] 32 | // byMeta[code] = byMeta[code] || [] 33 | // byMeta[code].push(k) 34 | // }) 35 | // console.log(byMeta) 36 | // const byMeta = {} 37 | // Object.keys(data.CldrMetazones).forEach((k) => { 38 | // let code = data.CldrMetazones[k] 39 | // byMeta[code] = byMeta[code] || [] 40 | // byMeta[code].push(k) 41 | // }) 42 | // console.log(byMeta) 43 | 44 | // const byCountry = {} 45 | // Object.keys(data.TzdbZoneCountries).forEach((k) => { 46 | // let code = data.TzdbZoneCountries[k][0] 47 | // byCountry[code] = byCountry[code] || [] 48 | // byCountry[code].push(k) 49 | // }) 50 | // console.log(byCountry) 51 | -------------------------------------------------------------------------------- /plugins/dst/old/zonefile/missing.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'America/Argentina/La_Rioja', 3 | 'America/Argentina/Rio_Gallegos', 4 | 'America/Argentina/Salta', 5 | 'America/Argentina/San_Juan', 6 | 'America/Argentina/San_Luis', 7 | 'America/Argentina/Tucuman', 8 | 'America/Argentina/Ushuaia', 9 | 'America/Indiana/Knox', 10 | 'America/Indiana/Tell_City', 11 | 'America/North_Dakota/Beulah', 12 | 'America/North_Dakota/Center', 13 | 'America/North_Dakota/New_Salem', 14 | 'America/Indiana/Marengo', 15 | 'America/Indiana/Petersburg', 16 | 'America/Indiana/Vevay', 17 | 'America/Indiana/Vincennes', 18 | 'America/Indiana/Winamac', 19 | 'America/Kentucky/Monticello' 20 | ] 21 | -------------------------------------------------------------------------------- /plugins/dst/old/zonefile/south.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 1: ['Africa/Kinshasa', 'Africa/Luanda'], 3 | 2: [ 4 | 'Africa/Gaborone', 5 | 'Africa/Harare', 6 | 'Africa/Johannesburg', 7 | 'Africa/Lubumbashi', 8 | 'Africa/Lusaka', 9 | 'Africa/Maputo', 10 | 'Africa/Maseru', 11 | 'Africa/Mbabane' 12 | ], 13 | 3: ['Antarctica/Syowa', 'Indian/Antananarivo'], 14 | 4: ['Indian/Reunion'], 15 | 5: ['Antarctica/Mawson', 'Indian/Kerguelen'], 16 | 6: ['Antarctica/Vostok'], 17 | 7: ['Antarctica/Davis', 'Asia/Jakarta', 'Indian/Christmas'], 18 | 8: ['Asia/Kuala_Lumpur', 'Asia/Makassar', 'Asia/Singapore', 'Australia/Perth', 'Australia/West'], 19 | 9: ['Asia/Dili', 'Asia/Jayapura'], 20 | 9.5: ['Australia/Darwin', 'Australia/North'], 21 | 8.75: ['Australia/Eucla'], 22 | 10: [ 23 | 'Antarctica/Dumontdurville', 24 | 'Australia/Brisbane', 25 | 'Australia/Lindeman', 26 | 'Australia/Queensland' 27 | ], 28 | 11: ['Pacific/Bougainville'], 29 | 30 | // eastern hemisphere 31 | '-5': ['America/Lima', 'America/Rio_Branco', 'Brazil/Acre'], 32 | '-4': ['America/La_Paz', 'America/Manaus', 'Brazil/West'], 33 | '-3': [ 34 | 'America/Argentina', 35 | 'America/Buenos_Aires', 36 | 'America/Cordoba', 37 | 'America/Fortaleza', 38 | 'America/Montevideo', 39 | 'America/Punta_Arenas', 40 | 'America/Sao_Paulo', 41 | 'Antarctica/Rothera', 42 | 'Atlantic/Stanley', 43 | 'Brazil/East' 44 | ], 45 | '-2': ['Brazil/Denoronha'] 46 | } 47 | -------------------------------------------------------------------------------- /plugins/dst/old/zones/patterns.js: -------------------------------------------------------------------------------- 1 | // (From 1987 to 2006, DST began on the first Sunday in April and ended on the last Sunday of October) 2 | const northAmerica = { 3 | // the second Sunday of March 4 | start: { 5 | day: 'sunday', 6 | num: 2, 7 | month: 'march', 8 | hour: 2 9 | }, 10 | // first Sunday of November 11 | end: { 12 | day: 'sunday', 13 | num: 1, 14 | month: 'november', 15 | hour: 2 16 | } 17 | } 18 | 19 | const eu = { 20 | // last Sunday in March 21 | start: { 22 | day: 'sunday', 23 | num: 'last', 24 | month: 'march' 25 | // hour: ()=>{} 26 | }, 27 | // the last Sunday in October. 28 | end: { 29 | day: 'sunday', 30 | num: 'last', 31 | month: 'october' 32 | // hour: ()=>{} 33 | } 34 | } 35 | 36 | export default { 37 | northAmerica, 38 | eu 39 | } 40 | -------------------------------------------------------------------------------- /plugins/dst/script/list-changes.js: -------------------------------------------------------------------------------- 1 | import spacetime from '../../../src/index.js' 2 | import zones from '../src/zonefile.2022.js' 3 | import { fromSpace } from '../src/calc.js' 4 | import parsedDst from '../src/patterns.js' 5 | const start = 2020 6 | 7 | const byTz = function (tz) { 8 | let s = spacetime.now(tz) 9 | let dst = zones[tz].pattern 10 | dst = parsedDst[dst] 11 | console.log(fromSpace(dst, tz, start)) 12 | const arr = [] 13 | for (let y = start; y < start + 5; y += 1) { 14 | s = s.year(y).startOf('year') 15 | arr.push([s.epoch, y, 1, 1]) 16 | 17 | // if (dst) { 18 | 19 | // } 20 | } 21 | // console.log(s.timezone()) 22 | // console.log(spacetime.timezones()) 23 | 24 | return arr 25 | } 26 | console.log(byTz('america/toronto')) -------------------------------------------------------------------------------- /plugins/dst/src/calc.js: -------------------------------------------------------------------------------- 1 | import spacetime from '../../../src/index.js' 2 | 3 | const fromJSDate = function (obj, year) { 4 | const d = new Date([year, obj.month, 1]) 5 | const currentDay = d.getDay() 6 | // set to the right day eg 'monday' 7 | if (currentDay !== obj.day) { 8 | const distance = (obj.day + 7 - currentDay) % 7; 9 | d.setDate(1 + distance) 10 | } 11 | if (obj.num === 1) { 12 | return d 13 | } 14 | if (obj.num === 2) { 15 | d.setDate(d.getDate() + 7) 16 | return d 17 | } 18 | if (obj.num === 3) { 19 | d.setDate(d.getDate() + 14) 20 | return d 21 | } 22 | if (obj.num === 'last') { 23 | // get the last sunday in the month 24 | const m = d.getMonth() 25 | while (d.getMonth() === m) { 26 | d.setDate(d.getDate() + 7) 27 | } 28 | d.setDate(d.getDate() - 7) 29 | } 30 | return d 31 | } 32 | 33 | const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] 34 | const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] 35 | 36 | const fromSpace = function (obj, tz, year) { 37 | let s = spacetime.now(tz).year(year).startOf('year') 38 | s = s.month(months[obj.month - 1]) 39 | s = s.startOf('month') 40 | s = s.day(days[obj.day], true) 41 | if (obj.num === 2) { 42 | s = s.add(1, 'week') 43 | } else if (obj.num === 3) { 44 | s = s.add(2, 'week') 45 | } else if (obj.num === 'last') { 46 | s = s.endOf('month') 47 | s = s.day(obj.day, false)//roll backward 48 | } 49 | s = s.hour(obj.hour) 50 | return s 51 | } 52 | export { fromJSDate, fromSpace } -------------------------------------------------------------------------------- /plugins/dst/src/index.js: -------------------------------------------------------------------------------- 1 | import zones from './zonefile.2022.js' 2 | import patterns from './patterns.js' 3 | 4 | 5 | const topk = function (arr) { 6 | const obj = {} 7 | arr.forEach(a => { 8 | obj[a] = obj[a] || 0 9 | obj[a] += 1 10 | }) 11 | const res = Object.keys(obj).map(k => [k, obj[k]]) 12 | return res.sort((a, b) => (a[1] > b[1] ? -1 : 0)) 13 | } 14 | 15 | let all = Object.keys(zones).map(k => { 16 | return (zones[k].dst || '') + '|' + (zones[k].pattern || '') 17 | }) 18 | all = topk(all) 19 | all = all.map(a => { 20 | return a 21 | }) 22 | console.log(all) -------------------------------------------------------------------------------- /plugins/dst/src/test.js: -------------------------------------------------------------------------------- 1 | import zones from './zonefile.2022.js' 2 | import patterns from './patterns.js' 3 | import { fromJSDate, fromSpace } from './calc.js' 4 | // import spacetime from '../../../src/index.js' 5 | // let zones = { 6 | // } 7 | 8 | 9 | const pad = (num) => { 10 | return String(num).padStart(2, '0') 11 | } 12 | 13 | // test spacetime version 14 | Object.keys(zones).forEach(k => { 15 | if (zones[k].dst) { 16 | const pattern = patterns[zones[k].pattern] 17 | const start = fromSpace(pattern.start, 2022) 18 | const end = fromSpace(pattern.end, 2022) 19 | let dst = `${pad(start.month() + 1)}/${pad(start.date())}:${pad(pattern.start.hour)}->` 20 | dst += `${pad(end.month() + 1)}/${pad(end.date())}:${pad(pattern.end.hour)}` 21 | // console.log(k, pattern) 22 | if (dst !== zones[k].dst) { 23 | console.log('\n', k) 24 | console.log(pattern) 25 | console.log('made', dst) 26 | console.log('want', zones[k].dst) 27 | } 28 | console.log(dst === zones[k].dst) 29 | } 30 | }) 31 | 32 | // test js date version 33 | // Object.keys(zones).forEach(k => { 34 | // if (zones[k].dst) { 35 | // let pattern = patterns[zones[k].pattern] 36 | // let start = fromJSDate(pattern.start, 2022) 37 | // let end = fromJSDate(pattern.end, 2022) 38 | // let dst = `${pad(start.getMonth() + 1)}/${pad(start.getDate())}:${pad(pattern.start.hour)}->` 39 | // dst += `${pad(end.getMonth() + 1)}/${pad(end.getDate())}:${pad(pattern.end.hour)}` 40 | // // console.log(k, pattern) 41 | // if (dst !== zones[k].dst) { 42 | // console.log('\n', k) 43 | // console.log(pattern) 44 | // console.log('made', dst) 45 | // console.log('want', zones[k].dst) 46 | // } 47 | // console.log(dst === zones[k].dst) 48 | // } 49 | // }) 50 | 51 | // console.log(calculate({ num: 1, day: 0, month: 3, hour: 0 }, 2022)) -------------------------------------------------------------------------------- /plugins/dst/tzdb/aliases.js: -------------------------------------------------------------------------------- 1 | import aliases from '/Users/spencer/mountain/spacetime/zonefile/aliases.js' 2 | import zone from '../src/zonefile.2022.js' 3 | Object.entries(aliases).forEach(a => { 4 | const [k, v] = a 5 | if (zone[k]) { 6 | console.log(k) 7 | } 8 | if (!zone[v]) { 9 | console.log(v) 10 | } 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /plugins/dst/tzdb/download.js: -------------------------------------------------------------------------------- 1 | import sh from 'shelljs' 2 | 3 | sh.exec(`wget -qO- https://timezonedb.com/files/TimeZoneDB.csv.zip | bsdtar -xvf-`) 4 | sh.exec(`rm database.sql`) 5 | sh.rm(`readme.txt`) 6 | sh.rm(`country.csv`) 7 | sh.mv(`time_zone.csv`, './plugins/dst/tzdb') -------------------------------------------------------------------------------- /plugins/dst/tzdb/parse.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | const dir = path.dirname(fileURLToPath(import.meta.url)) // eslint-disable-line 5 | 6 | // https://timezonedb.com/files/TimeZoneDB.csv.zip 7 | const rows = fs.readFileSync(dir + '/time_zone.csv').toString().split(/\n/g) 8 | const data = {} 9 | const want = new Set([2021, 2022, 2023, 2024]) 10 | rows.forEach(str => { 11 | let [id, _, _co, epoch, offset, on] = str.split(/,/g) 12 | epoch = Number(epoch) * 1000 13 | const d = new Date(epoch) 14 | const year = d.getFullYear() 15 | if (!want.has(year)) { 16 | return 17 | } 18 | data[id] = data[id] || {} 19 | data[id][year] = data[id][year] || [] 20 | data[id][year].push(epoch) 21 | }) 22 | // delete data['Pacific/Apia'] 23 | // delete data['Asia/Tehran'] 24 | // delete data['Pacific/Fiji'] 25 | // delete data['Africa/Juba'] //no longer dst 26 | // //aliases 27 | // delete data['America/Kentucky/Louisville'] 28 | // delete data['America/Indiana/Indianapolis'] 29 | 30 | export default data 31 | // console.log(data) -------------------------------------------------------------------------------- /plugins/dst/tzdb/test.js: -------------------------------------------------------------------------------- 1 | import data from './parse.js' 2 | import spacetime from '../../../src/index.js' 3 | import zone from '../src/zonefile.2022.js' 4 | 5 | 6 | import patterns from '../src/patterns.js' 7 | import { fromSpace } from '../src/calc.js' 8 | 9 | const year = 2021 10 | // let k = 'America/Toronto' 11 | Object.keys(data).forEach(k => { 12 | let [start, end] = data[k][year] 13 | const hour = 1000 * 60 * 60 14 | start = spacetime(start, k) 15 | end = spacetime(end + hour, k) 16 | 17 | const name = zone[k.toLowerCase()].pattern 18 | const pattern = patterns[name] 19 | const pStart = fromSpace(pattern.start, k, year).add(1, 'hour') 20 | const pEnd = fromSpace(pattern.end, k, year) 21 | 22 | if (Math.abs(start.diff(pStart, 'hour')) > 2 || Math.abs(end.diff(pEnd, 'hour')) > 2) { 23 | console.log(k, Math.abs(start.diff(pStart, 'hour'))) 24 | console.log(start.format('{nice-day} {time}'), ' ', end.format('{nice-day} {time}')) 25 | console.log(pStart.format('{nice-day} {time}'), ' ', pEnd.format('{nice-day} {time}')) 26 | } else { 27 | console.log(true) 28 | } 29 | }) -------------------------------------------------------------------------------- /plugins/dst/tzdb/titlecase.js: -------------------------------------------------------------------------------- 1 | import spacetime from '../../../src/index.js' 2 | import zone from '../src/zonefile.2022.js' 3 | 4 | import fs from 'fs' 5 | import path from 'path' 6 | import { fileURLToPath } from 'url' 7 | const dir = path.dirname(fileURLToPath(import.meta.url)) // eslint-disable-line 8 | 9 | // https://timezonedb.com/files/TimeZoneDB.csv.zip 10 | const rows = fs.readFileSync(dir + '/time_zone.csv').toString().split(/\n/g) 11 | const data = {} 12 | rows.forEach(str => { 13 | const [id] = str.split(/,/g) 14 | data[id] = true 15 | }) 16 | Object.keys(zone).forEach(k => { 17 | const s = spacetime.now(k) 18 | const tz = s.timezone().name 19 | if (!data.hasOwnProperty(tz)) { 20 | console.log(tz) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /plugins/dst/zonefile/dst-patterns.js: -------------------------------------------------------------------------------- 1 | // these are the folk heuristics that timezones use to set their dst change dates 2 | // for example, the US changes: 3 | // the second Sunday of March -> first Sunday of November 4 | // http://www.webexhibits.org/daylightsaving/g.html 5 | const patterns = { 6 | usa: '2nd-sun-mar-2h|1st-sun-nov-2h',// (From 1987 to 2006) 7 | // mexico 8 | mex: '1st-sun-apr-2h|last-sun-oct-2h', 9 | 10 | // European Union zone 11 | eu0: 'last-sun-mar-0h|last-sun-oct-1h', 12 | eu1: 'last-sun-mar-1h|last-sun-oct-2h', 13 | eu2: 'last-sun-mar-2h|last-sun-oct-3h', 14 | eu3: 'last-sun-mar-3h|last-sun-oct-4h', 15 | //greenland 16 | green: 'last-sat-mar-22h|last-sat-oct-23h', 17 | 18 | // australia 19 | aus: '1st-sun-apr-3h|1st-sun-oct-2h', 20 | //lord howe australia 21 | lhow: '1st-sun-apr-2h|1st-sun-oct-2h', 22 | // new zealand 23 | chat: '1st-sun-apr-3h|last-sun-sep-2h', //technically 3:45h -> 2:45h 24 | // new Zealand, antarctica 25 | nz: '1st-sun-apr-3h|last-sun-sep-2h', 26 | // casey - antarctica 27 | ant: '2nd-sun-mar-0h|1st-sun-oct-0h', 28 | // troll - antarctica 29 | troll: '3rd-sun-mar-1h|last-sun-oct-3h', 30 | 31 | //jordan 32 | jord: 'last-fri-feb-0h|last-fri-oct-1h', 33 | // lebanon 34 | leb: 'last-sun-mar-0h|last-sun-oct-0h', 35 | // syria 36 | syr: 'last-fri-mar-0h|last-fri-oct-0h', 37 | //israel 38 | // Start: Last Friday before April 2 -> The Sunday between Rosh Hashana and Yom Kippur 39 | isr: 'last-fri-mar-2h|last-sun-oct-2h', 40 | //palestine 41 | pal: 'last-sun-mar-0h|last-fri-oct-1h', 42 | 43 | // el aaiun 44 | //this one seems to be on arabic calendar? 45 | saha: 'last-sun-mar-3h|1st-sun-may-2h', 46 | 47 | // paraguay 48 | par: 'last-sun-mar-0h|1st-sun-oct-0h', 49 | //cuba 50 | cuba: '2nd-sun-mar-0h|1st-sun-nov-1h', 51 | //chile 52 | chile: '1st-sun-apr-0h|1st-sun-sep-0h', 53 | //easter island 54 | east: '1st-sat-apr-22h|1st-sat-sep-22h', 55 | //fiji 56 | fiji: '3rd-sun-jan-3h|2nd-sun-nov-2h', 57 | } 58 | 59 | export default patterns -------------------------------------------------------------------------------- /plugins/dst/zonefile/index.js: -------------------------------------------------------------------------------- 1 | import metas from './metas.js' 2 | import patterns from './dst-patterns.js' 3 | import zones from './zones.js' 4 | 5 | const parsePattern = function (str) { 6 | const a = str.split(/-/) 7 | return { 8 | nth: a[0], 9 | day: a[1], 10 | month: a[2], 11 | hour: a[3], 12 | } 13 | } 14 | 15 | Object.keys(zones).forEach(k => { 16 | const obj = zones[k] 17 | const meta = metas[obj.meta] 18 | zones[k].std = { 19 | abbr: meta.std[0], 20 | offset: meta.std[1] 21 | } 22 | if (obj.dst) { 23 | const pattern = patterns[obj.dst].split(/\|/) 24 | zones[k].dst = { 25 | abbr: meta.dst[0], 26 | offset: meta.dst[1], 27 | start: parsePattern(pattern[0]), 28 | end: parsePattern(pattern[1]), 29 | } 30 | } 31 | obj.name = obj.meta + ' Time' 32 | delete obj.meta 33 | }) 34 | console.dir(zones, { depth: 5 }) 35 | -------------------------------------------------------------------------------- /plugins/dst2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dst2", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "datetime-math.js", 6 | "exports": { 7 | "./units": "./datetime-units.js", 8 | "./math": "./datetime-math.js" 9 | }, 10 | "scripts": { 11 | "test": "node --test tests/", 12 | "watch": "node --watch ./scratch.js" 13 | } 14 | } -------------------------------------------------------------------------------- /plugins/dst2/scratch.js: -------------------------------------------------------------------------------- 1 | import { addUnits } from './src/index.js' 2 | import { parseDuration, parseDateTime } from './src/lib/parse.js' 3 | import { formatDate } from './src/lib/format.js' 4 | import { applyDurationToJsDate, compareWithJsDate } from './tests/_lib.js' 5 | 6 | let a 7 | a = ['2025-04-29T00:00:00', 'P1D'] 8 | // a = ['2024-03-15T23:45:00', 'PT2H30M'] 9 | // a = ['2024-02-28T00:00:00', 'P1D'] 10 | // a = ['2023-02-28T00:00:00', 'P1D'] 11 | // a = ['2024-12-31T23:59:59', 'PT1S'] 12 | // a = ['2024-01-31T00:00:00', 'P6M'] 13 | // a = ['2024-01-31T00:00:00', 'P14M'] 14 | 15 | // Our implementation 16 | const state = parseDateTime(a[0]) 17 | const changes = parseDuration(a[1]) 18 | const result = addUnits(state, changes) 19 | 20 | console.log(result) 21 | console.log(formatDate(result)) 22 | // Compare with JS Date 23 | // const jsDate = applyDurationToJsDate( 24 | // new Date(dateStr), 25 | // parseDuration(durationStr) 26 | // ) 27 | 28 | // // console.log('\nTest:', dateStr, '+', durationStr) 29 | // console.log(formatDate(result)) 30 | // console.log(formatDate(jsDate)) 31 | 32 | // // Compare results 33 | // const matches = compareWithJsDate(result, jsDate) 34 | // console.log('Match?', matches ? '✅' : '❌') 35 | -------------------------------------------------------------------------------- /plugins/dst2/src/add.js: -------------------------------------------------------------------------------- 1 | import { units, baseUnit } from './units.js' 2 | 3 | const processUnit = (unitName, state, changes) => { 4 | // Base case - no more units to process 5 | if (!unitName) return state 6 | 7 | const unit = units[unitName] 8 | 9 | // Add any changes for this unit 10 | if (changes[unitName]) { 11 | state[unitName] += changes[unitName] 12 | } 13 | 14 | // Handle overflow if needed 15 | if (state[unitName] > unit.max(state)) { 16 | const overflow = Math.floor(state[unitName] - unit.max(state)) 17 | state[unitName] -= overflow 18 | 19 | // Add overflow to next unit if it exists 20 | if (unit.next) { 21 | changes[unit.next] = (changes[unit.next] || 0) + overflow 22 | } 23 | } 24 | 25 | // Normalize current unit 26 | state[unitName] = unit.normalize(state[unitName], state) 27 | 28 | // Process next unit 29 | return processUnit(unit.next, state, changes) 30 | } 31 | 32 | export const addUnits = (state, additions) => { 33 | return processUnit(baseUnit, { ...state }, additions) 34 | } -------------------------------------------------------------------------------- /plugins/dst2/src/index.js: -------------------------------------------------------------------------------- 1 | import { addUnits } from './add.js' 2 | 3 | export { addUnits } 4 | -------------------------------------------------------------------------------- /plugins/dst2/src/lib/format.js: -------------------------------------------------------------------------------- 1 | // Helper to print dates nicely 2 | export const formatDate = (state) => { 3 | const pad = (n) => String(n).padStart(2, '0') 4 | return `${state.year}-${pad(state.month)}-${pad(state.day)}T` + 5 | `${pad(state.hour)}:${pad(state.minute)}:${pad(state.second)}` 6 | } 7 | -------------------------------------------------------------------------------- /plugins/dst2/src/lib/native.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencermountain/spacetime/93e8fc821ea1c2a124bb3a1e46668857bfac4bf3/plugins/dst2/src/lib/native.js -------------------------------------------------------------------------------- /plugins/dst2/src/lib/parse.js: -------------------------------------------------------------------------------- 1 | // Parse ISO duration string (e.g. P1Y2M3DT4H5M6S) 2 | export const parseDuration = (iso) => { 3 | const matches = iso.match(/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/) 4 | if (!matches) return null 5 | 6 | return { 7 | year: parseInt(matches[1]) || 0, 8 | month: parseInt(matches[2]) || 0, 9 | day: parseInt(matches[3]) || 0, 10 | hour: parseInt(matches[4]) || 0, 11 | minute: parseInt(matches[5]) || 0, 12 | second: parseInt(matches[6]) || 0 13 | } 14 | } 15 | 16 | // Parse ISO date string (e.g. 2024-03-15T14:30:45) 17 | export const parseDateTime = (iso) => { 18 | const [date, time] = iso.split('T') 19 | const [year, month, day] = date.split('-') 20 | const [hour, minute, second] = (time || '00:00:00').split(':') 21 | 22 | return { 23 | year: parseInt(year), 24 | month: parseInt(month), 25 | day: parseInt(day), 26 | hour: parseInt(hour), 27 | minute: parseInt(minute), 28 | second: parseInt(second) 29 | } 30 | } -------------------------------------------------------------------------------- /plugins/dst2/tests/_lib.js: -------------------------------------------------------------------------------- 1 | import { formatDate } from '../src/lib/format.js' 2 | // Convert our state object to JS Date 3 | export const stateToDate = (state) => { 4 | return new Date( 5 | state.year, 6 | state.month - 1, // JS months are 0-based 7 | state.day, 8 | state.hour, 9 | state.minute, 10 | state.second 11 | ) 12 | } 13 | 14 | // Convert JS Date to our state format 15 | export const dateToState = (date) => ({ 16 | year: date.getFullYear(), 17 | month: date.getMonth() + 1, // Convert back to 1-based 18 | day: date.getDate(), 19 | hour: date.getHours(), 20 | minute: date.getMinutes(), 21 | second: date.getSeconds() 22 | }) 23 | 24 | // Apply duration changes to JS Date object 25 | export const applyDurationToJsDate = (jsDate, duration) => { 26 | if (duration.year) jsDate.setFullYear(jsDate.getFullYear() + duration.year) 27 | if (duration.month) jsDate.setMonth(jsDate.getMonth() + duration.month) 28 | if (duration.day) jsDate.setDate(jsDate.getDate() + duration.day) 29 | if (duration.hour) jsDate.setHours(jsDate.getHours() + duration.hour) 30 | if (duration.minute) jsDate.setMinutes(jsDate.getMinutes() + duration.minute) 31 | if (duration.second) jsDate.setSeconds(jsDate.getSeconds() + duration.second) 32 | return jsDate 33 | } 34 | 35 | // Compare our result with JS Date behavior 36 | export const compareWithJsDate = (result, jsDate) => { 37 | const jsResult = dateToState(jsDate) 38 | const matches = formatDate(result) === formatDate(jsResult) 39 | 40 | if (!matches) { 41 | console.log('Difference found:') 42 | Object.keys(result).forEach(key => { 43 | if (result[key] !== jsResult[key]) { 44 | console.log(` ${key}: ${result[key]} vs ${jsResult[key]}`) 45 | } 46 | }) 47 | } 48 | 49 | return matches 50 | } -------------------------------------------------------------------------------- /plugins/dst2/tests/auto.test.js: -------------------------------------------------------------------------------- 1 | import test from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { addUnits } from '../src/index.js' 4 | import { parseDuration, parseDateTime } from '../src/lib/parse.js' 5 | import { applyDurationToJsDate, compareWithJsDate } from './_lib.js' 6 | 7 | // [starting datetime, duration to add] 8 | const tests = [ 9 | // Basic additions 10 | ['2024-03-15T23:45:00', 'PT2H30M'], 11 | ['2024-01-01T00:00:00', 'PT24H'], 12 | 13 | // // Month end cases 14 | // ['2024-01-31T00:00:00', 'P1M'], // Jan 31 -> Feb 29 (leap year) 15 | // ['2024-01-31T00:00:00', 'P2M'], // Jan 31 -> Mar 31 16 | // ['2024-01-31T00:00:00', 'P3M'], // Jan 31 -> Apr 30 17 | 18 | // // Leap year cases 19 | // ['2024-02-28T00:00:00', 'P1D'], // Feb 28 -> Feb 29 20 | // ['2023-02-28T00:00:00', 'P1D'], // Feb 28 -> Mar 1 (non-leap) 21 | // ['2024-02-29T00:00:00', 'P1Y'], // Feb 29 -> Feb 28 (next year) 22 | 23 | // // Multiple unit overflow 24 | // ['2024-12-31T23:59:59', 'PT1S'], // Full year rollover 25 | // ['2024-01-31T00:00:00', 'P6M'], // Multi-month 26 | // ['2024-01-31T00:00:00', 'P14M'], // Month overflow to year 27 | 28 | // // Complex cases 29 | // ['2024-02-29T23:59:59', 'P1Y1D'], // Leap day plus overflow 30 | // ['2024-01-31T23:59:59', 'P1M1D'] // Month end plus day 31 | ] 32 | 33 | test('datetime additions', async (t) => { 34 | for (const [dateStr, durationStr] of tests) { 35 | t.test(`${dateStr} + ${durationStr}`, () => { 36 | // Our implementation 37 | const state = parseDateTime(dateStr) 38 | const changes = parseDuration(durationStr) 39 | const result = addUnits(state, changes) 40 | 41 | // Compare with JS Date 42 | const jsDate = applyDurationToJsDate( 43 | new Date(dateStr), 44 | parseDuration(durationStr) 45 | ) 46 | 47 | assert.ok(compareWithJsDate(result, jsDate)) 48 | }) 49 | } 50 | }) -------------------------------------------------------------------------------- /plugins/dst2/tests/manual.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import { addUnits } from '../src/index.js' 4 | import { parseDuration, parseDateTime } from '../src/lib/parse.js' 5 | import { formatDate } from '../src/lib/format.js' 6 | 7 | describe('manual tests', () => { 8 | const tests = [ 9 | ['2024-03-15T00:00:00', 'P1D', '2024-03-16T00:00:00'], 10 | ['2024-03-15T00:00:00', 'P2D', '2024-03-17T00:00:00'], 11 | ['2024-03-15T00:00:00', 'P3D', '2024-03-18T00:00:00'], 12 | ] 13 | tests.forEach(([date, duration, expected], i) => { 14 | test(`${date} + ${duration}`, () => { 15 | let res = addUnits(parseDateTime(date), parseDuration(duration)) 16 | res = formatDate(res) 17 | assert.strictEqual(res, expected) 18 | if (i == 1) { 19 | console.log('=====HERE=====') 20 | } 21 | }) 22 | }) 23 | }); 24 | -------------------------------------------------------------------------------- /plugins/dst2/tests/tmp.test.js: -------------------------------------------------------------------------------- 1 | import { test, describe } from 'node:test' 2 | import assert from 'node:assert/strict' 3 | 4 | describe('Basic test suite', () => { 5 | test('should pass this test', () => { 6 | assert.strictEqual(1 + 1, 2); 7 | }); 8 | 9 | test('should pass with async', async () => { 10 | // Simulate async operation 11 | await new Promise(resolve => setTimeout(resolve, 100)); 12 | assert.strictEqual(2 + 2, 4); 13 | console.log('hello inside') 14 | }); 15 | console.log('hello outside') 16 | process.stderr.write('This log should appear even with dot reporter\n'); 17 | 18 | // test('should fail this test', () => { 19 | // assert.strictEqual(1 + 1, 3, 'Expected 1+1 to equal 3'); 20 | // }); 21 | 22 | test.skip('this test is skipped', () => { 23 | assert.strictEqual(1, 1); 24 | }); 25 | }); 26 | 27 | describe('Another test group', () => { 28 | test('should pass with object comparison', () => { 29 | const obj1 = { a: 1, b: 2 }; 30 | const obj2 = { a: 1, b: 2 }; 31 | assert.deepStrictEqual(obj1, obj2); 32 | }); 33 | 34 | // test('should fail with object comparison', () => { 35 | // const obj1 = { a: 1, b: 2 }; 36 | // const obj2 = { a: 1, b: 3 }; 37 | // assert.deepStrictEqual(obj1, obj2); 38 | // }); 39 | }); -------------------------------------------------------------------------------- /plugins/geo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-geo", 3 | "version": "1.4.2", 4 | "description": "determine a timezone from a lat/lng", 5 | "main": "./builds/spacetime-geo.js", 6 | "unpkg": "./builds/spacetime-geo.js", 7 | "type": "module", 8 | "sideEffects": false, 9 | "exports": { 10 | ".": { 11 | "require": "./builds/spacetime-geo.cjs", 12 | "import": "./builds/spacetime-geo.mjs", 13 | "default": "./builds/spacetime-geo.mjs" 14 | } 15 | }, 16 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/spacetime-geo", 17 | "scripts": { 18 | "watch": "node --watch ./scratch.js", 19 | "test": "\"node_modules/.bin/tape\" \"./tests/*.test.js\" | \"node_modules/.bin/tap-dancer\" --color", 20 | "build": "rollup -c --silent" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/spencermountain/spacetime.git" 25 | }, 26 | "keywords": [ 27 | "timezone" 28 | ], 29 | "author": "spencermountain@gmail.com", 30 | "dependencies": { 31 | "tz-lookup": "6.1.25" 32 | }, 33 | "devDependencies": { 34 | "shelljs": "0.8.5", 35 | "spacetime": ">=7.4.0", 36 | "tap-dancer": "0.3.4", 37 | "tape": "5.6.3" 38 | }, 39 | "license": "MIT" 40 | } -------------------------------------------------------------------------------- /plugins/geo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import sizeCheck from 'rollup-plugin-filesize-check' 6 | import pkg from './package.json' assert { type: "json" }; 7 | const version = pkg.version 8 | console.log('\n 📦 - running rollup..\n') 9 | 10 | const banner = '/* spencermountain/spacetime-geo ' + version + ' MIT */' 11 | 12 | export default [ 13 | { 14 | input: 'src/index.js', 15 | output: [{ banner: banner, file: 'builds/spacetime-geo.mjs', format: 'esm' }], 16 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 109, warn: 10 })] 17 | }, 18 | { 19 | input: 'src/index.js', 20 | output: [{ banner: banner, file: 'builds/spacetime-geo.cjs', format: 'umd', sourcemap: false, name: 'spacetimeGeo' }], 21 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 109, warn: 10 })] 22 | }, 23 | { 24 | input: 'src/index.js', 25 | output: [{ banner: banner, file: 'builds/spacetime-geo.min.js', format: 'umd', name: 'spacetimeGeo' }], 26 | plugins: [resolve(), json(), commonjs(), terser(), sizeCheck({ expect: 109, warn: 10 })] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /plugins/geo/scratch.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import geo from './src/index.js' 3 | spacetime.extend(geo) 4 | 5 | 6 | // let s = spacetime('june 4 2018', 'Canada/Eastern') //.time('3:37pm') 7 | // s = s.in([48.7235, 1.9931]) //near paris 8 | // console.log(s.timezone().name) 9 | 10 | let s = spacetime.today('America/Havana') 11 | console.log(s.point()) 12 | 13 | s = spacetime.today('Asia/Famagusta') 14 | console.log(s.point()) 15 | -------------------------------------------------------------------------------- /plugins/geo/scripts/getPoints/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | let iana = require('./iana') 3 | // const fs = require('fs'); 4 | const key = 'get-your-own' 5 | 6 | const done = {} 7 | 8 | iana = iana.map((arr) => { 9 | const cities = arr[3].split(',') 10 | let tz = arr[1] 11 | tz = tz.split('/') 12 | const city = cities[0] || tz[tz.length - 1] 13 | const str = `${city}, ${arr[2]}` 14 | return { 15 | str: str, 16 | tz: arr[1] 17 | } 18 | }) 19 | 20 | const roundIt = function(num) { 21 | return Math.round(num * 100) / 100 22 | } 23 | 24 | function doit(i) { 25 | const str = iana[i].str 26 | const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(str)}&key=${key}` 27 | fetch(url).then(res => res.json()).then(res => { 28 | if (res.results && res.results[0]) { 29 | let point = res.results[0].geometry.location 30 | const place = res.results[0].formatted_address 31 | point = [roundIt(point.lat), roundIt(point.lng)] 32 | done[iana[i].tz] = { 33 | point: point, 34 | place: place 35 | } 36 | console.log(place) 37 | } else { 38 | console.log('\nmissing ' + str) 39 | } 40 | i += 1 41 | if (iana[i]) { 42 | doit(i) 43 | } else { 44 | // fs.writeFileSync('./src/data.json', JSON.stringify(done, null, 2)); 45 | console.log('{') 46 | Object.keys(done).forEach((k) => { 47 | console.log(` "${k}" : '${done[k].point[0]},${done[k].point[1]}', //${done[k].place}`) 48 | }) 49 | console.log('}') 50 | } 51 | }).catch(console.log) 52 | } 53 | 54 | doit(0) 55 | -------------------------------------------------------------------------------- /plugins/geo/scripts/getPoints/qa-test.js: -------------------------------------------------------------------------------- 1 | const tzlookup = require("tz-lookup"); 2 | const points = require('../../src/IANA-points.js') 3 | 4 | Object.keys(points).forEach((k) => { 5 | const geo = points[k].split(',') 6 | const tz = tzlookup(geo[0], geo[1]) 7 | if (k !== tz) { 8 | console.log(`want: ${k}, have: ${tz}`) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /plugins/geo/src/findTz/index.js: -------------------------------------------------------------------------------- 1 | import tzlookup from 'tz-lookup' 2 | 3 | //.trim() pollyfill 4 | if (!String.prototype.trim) { 5 | const rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g 6 | String.prototype.trim = function () { 7 | return this.replace(rtrim, '') 8 | } 9 | } 10 | const isArray = function (hmm) { 11 | return Object.prototype.toString.call(hmm) === '[object Array]' 12 | } 13 | const isString = function (hmm) { 14 | return typeof hmm === 'string' 15 | } 16 | function isObject(hmm) { 17 | return hmm instanceof Object && hmm.constructor === Object 18 | } 19 | 20 | const findTz = function (geo, b) { 21 | let lat = null 22 | let lng = null 23 | //accept weird formats 24 | if (typeof b === 'number' && typeof geo === 'number') { 25 | lat = geo 26 | lng = b 27 | } else if (isArray(geo) === true) { 28 | lat = geo[0] 29 | lng = geo[1] 30 | } else if (isString(geo) === true) { 31 | const arr = geo.split(/[,/]/) 32 | lat = arr[0].trim() 33 | lng = arr[1].trim() 34 | } else if (isObject(geo) === true) { 35 | lat = geo.lat || geo.latitude 36 | lng = geo.lng || geo.lon || geo.long || geo.longitude 37 | } else { 38 | return this 39 | } 40 | //validate lat/lng 41 | if (lat < -90 || lat > 90) { 42 | console.warn('Invalid latitude: ' + lat) 43 | return this 44 | } 45 | if (lng < -180 || lng > 180) { 46 | console.warn('Invalid longitude: ' + lng) 47 | return this 48 | } 49 | const tz = tzlookup(lat, lng) 50 | if (!tz) { 51 | console.warn('Found no timezone for ' + lat + ', ' + lng) 52 | return this 53 | } 54 | return this.goto(tz) 55 | } 56 | export default findTz 57 | -------------------------------------------------------------------------------- /plugins/geo/src/index.js: -------------------------------------------------------------------------------- 1 | import find from './findTz/index.js' 2 | import point from './point/index.js' 3 | 4 | export default { 5 | in: find, 6 | point: point, 7 | } 8 | -------------------------------------------------------------------------------- /plugins/geo/src/point/index.js: -------------------------------------------------------------------------------- 1 | import points from './IANA-points.js' 2 | // 3 | const point = function () { 4 | const tz = this.timezone().name 5 | if (points.hasOwnProperty(tz) === false) { 6 | console.warn('Unable to find location for timezone ' + tz) 7 | return {} 8 | } 9 | const arr = points[tz].split(',') 10 | return { 11 | lat: parseFloat(arr[0]), 12 | lng: parseFloat(arr[1]) 13 | } 14 | } 15 | export default point 16 | -------------------------------------------------------------------------------- /plugins/geo/tests/in.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from '../../../src/index.js' 3 | // import spacetime from 'spacetime' 4 | import geo from '../src/index.js' 5 | // import geo from '../builds/spacetime-geo.mjs' 6 | 7 | test('test some lat/lngs', function (t) { 8 | spacetime.extend(geo) 9 | 10 | let s = spacetime('june 4 2018', 'Canada/Eastern').time('3:37pm') 11 | s = s.in([48.7235, 1.9931]) //near paris 12 | t.equal(s.timezone().name, 'Europe/Paris', 'found-paris') 13 | t.equal(s.time(), '9:37pm', 'time has moved') 14 | 15 | s = s.in([42.7235, -73.6931]) //new york 16 | t.equal(s.timezone().name, 'America/New_York', 'found-ny') 17 | t.equal(s.time(), '3:37pm', 'time has back to eastern') 18 | 19 | s = s.in([50.405, -31.8971]) // atlantic ocean 20 | t.equal(s.timezone().name, 'Etc/GMT+2', 'found-ocean') 21 | t.equal(s.time(), '5:37pm', 'time has moved to ocean') 22 | 23 | s = s.in([50.405, -18.8971]) //bit further atlantic ocean 24 | t.equal(s.timezone().name, 'Etc/GMT+1', 'futher-into-ocean') 25 | t.equal(s.time(), '6:37pm', 'almost europe') 26 | 27 | s = s.in([50.405, -40.8971]) //bit closer to canada 28 | t.equal(s.timezone().name, 'Etc/GMT+3', 'closer-to-halifax') 29 | t.equal(s.time(), '4:37pm', 'almost halifax') 30 | 31 | s = s.in([-20, -40.8971]) //down to brazil 32 | t.equal(s.timezone().name, 'America/Sao_Paulo', 'closer-to-halifax') 33 | t.equal(s.time(), '4:37pm', 'almost halifax') 34 | 35 | t.end() 36 | }) 37 | -------------------------------------------------------------------------------- /plugins/geo/tests/point.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from 'spacetime' 3 | import geo from '../src/index.js' 4 | // import geo from '../builds/spacetime-geo.mjs' 5 | 6 | test('test some lat/lngs', function (t) { 7 | spacetime.extend(geo) 8 | 9 | let s = spacetime('june 4 2018', 'Canada/Eastern') 10 | let point = s.point() 11 | t.equal(parseInt(point.lat, 10), 43, 'toronto-lat') 12 | t.equal(parseInt(point.lng, 10), -79, 'toronto-lng') 13 | 14 | s = spacetime('june 14 2018', 'Canada/Pacific') 15 | point = s.point() 16 | t.equal(parseInt(point.lat, 10), 49, 'vancouver-lat') 17 | t.equal(parseInt(point.lng, 10), -123, 'vancouver-lng') 18 | 19 | s = spacetime.now('Europe/Paris') 20 | point = s.point() 21 | t.equal(parseInt(point.lat, 10), 48, 'paris-lat') 22 | t.equal(parseInt(point.lng, 10), 2, 'paris-lng') 23 | 24 | t.end() 25 | }) 26 | -------------------------------------------------------------------------------- /plugins/holiday/changelog.md: -------------------------------------------------------------------------------- 1 | ### 0.1.0 2 | - **[change]** add timezone as third parameter 3 | - update deps -------------------------------------------------------------------------------- /plugins/holiday/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-holiday", 3 | "description": "reckoning of common holiday dates ", 4 | "version": "0.3.0", 5 | "main": "builds/spacetime-holiday.cjs", 6 | "unpkg": "builds/spacetime-holiday.min.js", 7 | "module": "./builds/spacetime-holiday.mjs", 8 | "type": "module", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "require": "./builds/spacetime-holiday.cjs", 13 | "import": "./builds/spacetime-holiday.mjs", 14 | "default": "./builds/spacetime-holiday.mjs" 15 | } 16 | }, 17 | "author": "Spencer Kelly", 18 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/holiday", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/spencermountain/spacetime.git" 22 | }, 23 | "scripts": { 24 | "watch": "node --watch ./scratch.js", 25 | "build": "rollup -c --silent", 26 | "test": "TESTENV=dev tape ./tests/**/*.test.js | tap-dancer", 27 | "testb": "TESTENV=prod tape ./tests/**/*.test.js | tap-dancer" 28 | }, 29 | "files": [ 30 | "builds/" 31 | ], 32 | "prettier": { 33 | "trailingComma": "none", 34 | "tabWidth": 2, 35 | "semi": false, 36 | "singleQuote": true, 37 | "printWidth": 100 38 | }, 39 | "peerDependencies": { 40 | "spacetime": ">=6.3.0" 41 | }, 42 | "devDependencies": { 43 | "tap-dancer": "0.3.4", 44 | "tape": "5.5.3" 45 | }, 46 | "licence": "MIT" 47 | } -------------------------------------------------------------------------------- /plugins/holiday/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import sizeCheck from 'rollup-plugin-filesize-check' 6 | 7 | export default [ 8 | { 9 | input: 'src/index.js', 10 | output: [{ file: 'builds/spacetime-holiday.mjs', format: 'esm' }], 11 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 13, warn: 10 })], 12 | external: ['spacetime'] 13 | }, 14 | { 15 | input: 'src/index.js', 16 | output: [{ 17 | file: 'builds/spacetime-holiday.cjs', format: 'umd', name: 'spacetimeHoliday', 18 | globals: { 19 | spacetime: 'spacetime' 20 | } 21 | } 22 | ], 23 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 6, warn: 10 })], 24 | external: ['spacetime'] 25 | }, 26 | { 27 | input: 'src/index.js', 28 | output: [{ 29 | file: 'builds/spacetime-holiday.min.js', format: 'umd', name: 'spacetimeHoliday', 30 | globals: { 31 | spacetime: 'spacetime' 32 | } 33 | }], 34 | plugins: [resolve(), json(), commonjs(), terser(), sizeCheck({ expect: 12, warn: 10 })], 35 | external: ['spacetime'] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /plugins/holiday/scratch.js: -------------------------------------------------------------------------------- 1 | // const spacetimeHoliday = require('./builds/spacetime-holiday.js') 2 | import spacetimeHoliday from './src' 3 | 4 | const s = spacetimeHoliday('ramadan', 2019, 'Canada/Pacific') 5 | console.log(s.format('iso')) 6 | -------------------------------------------------------------------------------- /plugins/holiday/src/01-fixedDates.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import fixed from './holidays/fixed-holidays.js' 3 | 4 | // holidays that are the same date every year 5 | const fixedDates = function (str, normal, year, tz) { 6 | if (fixed.hasOwnProperty(str) || fixed.hasOwnProperty(normal)) { 7 | const arr = fixed[str] || fixed[normal] || [] 8 | let s = spacetime.now(tz) 9 | s = s.year(year) 10 | s = s.startOf('year') 11 | s = s.month(arr[0]) 12 | s = s.date(arr[1]) 13 | if (s.isValid()) { 14 | return s 15 | } 16 | } 17 | return null 18 | } 19 | export default fixedDates 20 | -------------------------------------------------------------------------------- /plugins/holiday/src/02-nthWeekday.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import calendar from './holidays/calendar-holidays.js' 3 | 4 | // holidays that are the same date every year 5 | const fixedDates = function (str, normal, year, tz) { 6 | if (calendar.hasOwnProperty(str) || calendar.hasOwnProperty(normal)) { 7 | const arr = calendar[str] || calendar[normal] || [] 8 | let s = spacetime.now(tz) 9 | s = s.year(year) 10 | 11 | // [3rd, 'monday', 'january'] 12 | s = s.month(arr[2]) 13 | s = s.startOf('month') 14 | // make it january 15 | const month = s.month() 16 | 17 | // make it the 1st monday 18 | s = s.day(arr[1]) 19 | if (s.month() !== month) { 20 | s = s.add(1, 'week') 21 | } 22 | // make it nth monday 23 | if (arr[0] > 1) { 24 | s = s.add(arr[0] - 1, 'week') 25 | } 26 | if (s.isValid()) { 27 | return s 28 | } 29 | } 30 | 31 | return null 32 | } 33 | export default fixedDates 34 | -------------------------------------------------------------------------------- /plugins/holiday/src/03-easterDates.js: -------------------------------------------------------------------------------- 1 | import holidays from './holidays/easter-holidays.js' 2 | import spacetime from 'spacetime' 3 | import calcEaster from './lib/calcEaster.js' 4 | 5 | //calculate any holidays based on easter 6 | const easterDates = function (str, normal, year, tz) { 7 | if (holidays.hasOwnProperty(str) || holidays.hasOwnProperty(normal)) { 8 | const days = holidays[str] || holidays[normal] || [] 9 | 10 | const date = calcEaster(year) 11 | if (!date) { 12 | return null //no easter for this year 13 | } 14 | let e = spacetime(date, tz) 15 | e = e.year(year) 16 | 17 | const s = e.add(days, 'day') 18 | if (s.isValid()) { 19 | return s 20 | } 21 | } 22 | return null 23 | } 24 | export default easterDates 25 | -------------------------------------------------------------------------------- /plugins/holiday/src/04-astronomical.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import calcSeasons from './lib/seasons.js' 3 | import holidays from './holidays/astro-holidays.js' 4 | 5 | const astroDates = function (str, normal, year, tz) { 6 | if (holidays.hasOwnProperty(str) || holidays.hasOwnProperty(normal)) { 7 | const season = holidays[str] || holidays[normal] 8 | const seasons = calcSeasons(year) 9 | if (!season || !seasons || !seasons[season]) { 10 | return null // couldn't figure it out 11 | } 12 | const s = spacetime(seasons[season], tz) 13 | if (s.isValid()) { 14 | return s 15 | } 16 | } 17 | 18 | return null 19 | } 20 | export default astroDates 21 | -------------------------------------------------------------------------------- /plugins/holiday/src/05-lunarDates.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import holidays from './holidays/lunar-holidays.js' 3 | // (lunar year is 354.36 days) 4 | const dayDiff = -10.64 5 | 6 | const lunarDates = function (str, normal, year, tz) { 7 | if (holidays.hasOwnProperty(str) || holidays.hasOwnProperty(normal)) { 8 | const date = holidays[str] || holidays[normal] || [] 9 | if (!date) { 10 | return null 11 | } 12 | // start at 2018 13 | let s = spacetime(date + ' 2018', tz) 14 | const diff = year - 2018 15 | const toAdd = diff * dayDiff 16 | s = s.add(toAdd, 'day') 17 | s = s.startOf('day') 18 | 19 | // now set the correct year 20 | s = s.year(year) 21 | 22 | if (s.isValid()) { 23 | return s 24 | } 25 | } 26 | return null 27 | } 28 | export default lunarDates 29 | -------------------------------------------------------------------------------- /plugins/holiday/src/holidays/astro-holidays.js: -------------------------------------------------------------------------------- 1 | // these are properly calculated in ./lib/seasons 2 | const dates = { 3 | 'spring equinox': 'spring', 4 | 'summer solistice': 'summer', 5 | 'fall equinox': 'fall', 6 | 'winter solstice': 'winter' 7 | } 8 | 9 | // aliases 10 | dates['march equinox'] = dates['spring equinox'] 11 | dates['vernal equinox'] = dates['spring equinox'] 12 | dates['ostara'] = dates['spring equinox'] 13 | 14 | dates['june solstice'] = dates['summer solistice'] 15 | dates['litha'] = dates['summer solistice'] 16 | 17 | dates['autumn equinox'] = dates['fall equinox'] 18 | dates['autumnal equinox'] = dates['fall equinox'] 19 | dates['september equinox'] = dates['fall equinox'] 20 | dates['sept equinox'] = dates['fall equinox'] 21 | dates['mabon'] = dates['fall equinox'] 22 | 23 | dates['december solstice'] = dates['winter solistice'] 24 | dates['dec solstice'] = dates['winter solistice'] 25 | dates['yule'] = dates['winter solistice'] 26 | 27 | export default dates 28 | -------------------------------------------------------------------------------- /plugins/holiday/src/holidays/calendar-holidays.js: -------------------------------------------------------------------------------- 1 | //these are holidays on the 'nth weekday of month' 2 | const jan = 'january' 3 | const feb = 'february' 4 | const mar = 'march' 5 | // const apr = 'april' 6 | const may = 'may' 7 | const jun = 'june' 8 | // const jul = 'july' 9 | // const aug = 'august' 10 | const sep = 'september' 11 | const oct = 'october' 12 | const nov = 'november' 13 | // const dec = 'december' 14 | 15 | const mon = 'monday' 16 | // const tues = 'tuesday' 17 | // const wed = 'wednesday' 18 | const thurs = 'thursday' 19 | const fri = 'friday' 20 | // const sat = 'saturday' 21 | const sun = 'sunday' 22 | 23 | const holidays = { 24 | 'martin luther king day': [3, mon, jan], //[third monday in january], 25 | 'presidents day': [3, mon, feb], //[third monday in february], 26 | 27 | 'commonwealth day': [2, mon, mar], //[second monday in march], 28 | 'mothers day': [2, sun, may], //[second Sunday in May], 29 | 'fathers day': [3, sun, jun], //[third Sunday in June], 30 | 'labor day': [1, mon, sep], //[first monday in september], 31 | 'columbus day': [2, mon, oct], //[second monday in october], 32 | 'canadian thanksgiving': [2, mon, oct], //[second monday in october], 33 | thanksgiving: [4, thurs, nov], // [fourth Thursday in November], 34 | 'black friday': [4, fri, nov] //[fourth friday in november], 35 | 36 | // 'memorial day': [may], //[last monday in may], 37 | // 'us election': [nov], // [Tuesday following the first Monday in November], 38 | // 'cyber monday': [nov] 39 | // 'advent': [] // fourth Sunday before Christmas 40 | } 41 | 42 | // add aliases 43 | holidays['turday day'] = holidays.thanksgiving 44 | holidays['indigenous peoples day'] = holidays['columbus day'] 45 | holidays['mlk day'] = holidays['martin luther king day'] 46 | export default holidays 47 | -------------------------------------------------------------------------------- /plugins/holiday/src/holidays/easter-holidays.js: -------------------------------------------------------------------------------- 1 | // https://www.timeanddate.com/calendar/determining-easter-date.html 2 | 3 | const dates = { 4 | easter: 0, 5 | 'ash wednesday': -46, // (46 days before easter) 6 | 'palm sunday': 7, // (1 week before easter) 7 | 'maundy thursday': -3, // (3 days before easter) 8 | 'good friday': -2, // (2 days before easter) 9 | 'holy saturday': -1, // (1 days before easter) 10 | 'easter saturday': -1, // (1 day before easter) 11 | 'easter monday': 1, // (1 day after easter) 12 | 'ascension day': 39, // (39 days after easter) 13 | 'whit sunday': 49, // / pentecost (49 days after easter) 14 | 'whit monday': 50, // (50 days after easter) 15 | 'trinity sunday': 65, // (56 days after easter) 16 | 'corpus christi': 60, // (60 days after easter) 17 | 18 | 'mardi gras': -47 //(47 days before easter) 19 | } 20 | dates['easter sunday'] = dates.easter 21 | dates.pentecost = dates['whit sunday'] 22 | dates.whitsun = dates['whit sunday'] 23 | 24 | export default dates 25 | -------------------------------------------------------------------------------- /plugins/holiday/src/holidays/lunar-holidays.js: -------------------------------------------------------------------------------- 1 | const dates = { 2 | // Muslim holidays 3 | 'isra and miraj': 'april 13', 4 | 'lailat al-qadr': 'june 10', 5 | 'eid al-fitr': 'june 15', 6 | 'id al-Fitr': 'june 15', 7 | 'eid ul-Fitr': 'june 15', 8 | ramadan: 'may 16', // Range holiday 9 | 'eid al-adha': 'sep 22', 10 | muharram: 'sep 12', 11 | 'prophets birthday': 'nov 21' 12 | } 13 | export default dates 14 | -------------------------------------------------------------------------------- /plugins/holiday/src/holidays/misc-holidays.js: -------------------------------------------------------------------------------- 1 | //yep, 2 | const jan = 0 3 | const feb = 1 4 | const march = 2 5 | const april = 3 6 | const may = 4 7 | // const june = 5 8 | const july = 6 9 | // const august = 7 10 | const sep = 8 11 | const oct = 9 12 | const nov = 10 13 | const dec = 11 14 | 15 | // hardcoded dates for astronomical holidays 16 | // ----please change, every few years(!)--- 17 | const dates = { 18 | // Jewish 19 | 'tu bishvat': [jan, 31], 20 | 'tu bshevat': [jan, 31], 21 | purim: [march, 1], 22 | passover: [march, 31], // Ranged holiday [april, 7], 23 | 'yom hashoah': [april, 11], 24 | 'lag baomer': [may, 3], 25 | shavuot: [may, 20], 26 | 'tisha bav': [july, 22], 27 | 'rosh hashana': [sep, 10], 28 | 'yom kippur': [sep, 19], 29 | sukkot: [sep, 24], // Ranged holiday [sep, 30], 30 | 'shmini atzeret': [oct, 1], 31 | 'simchat torah': [oct, 2], 32 | chanukah: [dec, 3], // Ranged holiday [dec, 30], 33 | hanukkah: [dec, 3], // Ranged holiday [dec, 30], 34 | 35 | // Additional important holidays 36 | 'chinese new year': [feb, 16], 37 | diwali: [nov, 7] 38 | } 39 | export default dates 40 | -------------------------------------------------------------------------------- /plugins/holiday/src/index.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import fixedDates from './01-fixedDates.js' 3 | import nthWeekday from './02-nthWeekday.js' 4 | import easterDates from './03-easterDates.js' 5 | import astroDates from './04-astronomical.js' 6 | import lunarDates from './05-lunarDates.js' 7 | const nowYear = spacetime.now().year() 8 | 9 | const spacetimeHoliday = function (str, year, tz) { 10 | year = year || nowYear 11 | str = str || '' 12 | str = String(str) 13 | str = str.trim().toLowerCase() 14 | str = str.replace(/'s/, 's') // 'mother's day' 15 | 16 | let normal = str.replace(/ day$/, '') 17 | normal = normal.replace(/^the /, '') 18 | normal = normal.replace(/^orthodox /, '') //orthodox good friday 19 | 20 | // try easier, unmoving holidays 21 | let s = fixedDates(str, normal, year, tz) 22 | if (s !== null) { 23 | return s 24 | } 25 | // try 'nth monday' holidays 26 | s = nthWeekday(str, normal, year, tz) 27 | if (s !== null) { 28 | return s 29 | } 30 | // easter-based holidays 31 | s = easterDates(str, normal, year, tz) 32 | if (s !== null) { 33 | return s 34 | } 35 | // solar-based holidays 36 | s = astroDates(str, normal, year, tz) 37 | if (s !== null) { 38 | return s 39 | } 40 | // mostly muslim holidays 41 | s = lunarDates(str, normal, year, tz) 42 | if (s !== null) { 43 | return s 44 | } 45 | 46 | return null 47 | } 48 | export default spacetimeHoliday 49 | -------------------------------------------------------------------------------- /plugins/holiday/src/lib/calcEaster.js: -------------------------------------------------------------------------------- 1 | // by John Dyer 2 | // based on the algorithm by Oudin (1940) from http://www.tondering.dk/claus/cal/easter.php 3 | const calcEaster = function (year) { 4 | let f = Math.floor, 5 | // Golden Number - 1 6 | G = year % 19, 7 | C = f(year / 100), 8 | // related to Epact 9 | H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30, 10 | // number of days from 21 March to the Paschal full moon 11 | I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)), 12 | // weekday for the Paschal full moon 13 | J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7, 14 | // number of days from 21 March to the Sunday on or before the Paschal full moon 15 | L = I - J, 16 | month = 3 + f((L + 40) / 44), 17 | date = L + 28 - 31 * f(month / 4) 18 | 19 | month = month === 4 ? 'April' : 'March' 20 | return month + ' ' + date 21 | } 22 | 23 | export default calcEaster 24 | -------------------------------------------------------------------------------- /plugins/holiday/tests/_lib.js: -------------------------------------------------------------------------------- 1 | import src from '../src/index.js' 2 | import build from '../builds/spacetime-holiday.mjs' 3 | let lib = src 4 | 5 | if (typeof process !== undefined && typeof module !== undefined) { 6 | if (process.env.TESTENV === 'prod') { 7 | console.warn('== production build test 🚀 ==') 8 | lib = build 9 | } 10 | 11 | } 12 | export default lib 13 | -------------------------------------------------------------------------------- /plugins/play/scratch.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import plugin from './src/index.js' 3 | 4 | spacetime.extend(plugin) 5 | 6 | const s = spacetime.now() 7 | s.play() 8 | -------------------------------------------------------------------------------- /plugins/play/src/Ticker.js: -------------------------------------------------------------------------------- 1 | /* global performance */ 2 | 3 | // recursive setTimeOut - not perfect, but does not drift 4 | // https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript 5 | // see benchmarks at https://github.com/dbkaplun/driftless 6 | class Ticker { 7 | constructor(hertz, callback) { 8 | this.target = performance.now() // target time for the next frame 9 | this.interval = (1 / hertz) * 1000 // the milliseconds between ticks 10 | this.callback = callback 11 | this.stopped = false 12 | this.frame = 0 13 | this.tick(this) 14 | } 15 | 16 | tick(self) { 17 | if (self.stopped) { 18 | return 19 | } 20 | const currentTime = performance.now() 21 | const currentTarget = self.target 22 | const currentInterval = (self.target += self.interval) - currentTime 23 | 24 | setTimeout(self.tick, currentInterval, self) 25 | self.callback(self.frame++, currentTime, currentTarget, self) 26 | } 27 | 28 | stop() { 29 | this.stopped = true 30 | return this.frame 31 | } 32 | } 33 | 34 | export default Ticker 35 | 36 | // let c = new Ticker(2, () => { console.log('tick') }) 37 | -------------------------------------------------------------------------------- /plugins/play/src/index.js: -------------------------------------------------------------------------------- 1 | const methods = { 2 | start: function () { 3 | this.startEpoch = this.epoch 4 | return this 5 | }, 6 | stop: function () { 7 | this.startEpoch = null 8 | this.isRunning = false 9 | return this 10 | }, 11 | pause: function () { 12 | this.isRunning = false 13 | return this 14 | }, 15 | elapsed: async function () { 16 | const start = this._from(this.startEpoch, this.tz) 17 | return this.diff(start) 18 | } 19 | } 20 | methods.play = methods.start 21 | export default methods 22 | -------------------------------------------------------------------------------- /plugins/ticks/README.md: -------------------------------------------------------------------------------- 1 |
2 |
spacetime-ticks
3 | 4 |
demo
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | calculate some sensible break-points between two dates, using the [spacetime](https://github.com/spencermountain/spacetime) date library. 14 | 15 | `npm i spacetime-ticks` 16 | 17 | 18 | 19 | 20 | 21 | ```js 22 | import spacetimeTicks from 'spacetime-ticks' 23 | 24 | let ticks = spacetimeTicks('June 5th 1992', 'Oct 4 2002', 5) 25 | // [ 26 | // { label: "1993", epoch: 725864400000, value: 0.055 } 27 | // { label: "1995", epoch: 788936400000, value: 0.248 } 28 | // { label: "1997", epoch: 852094800000, value: 0.442 } 29 | // { label: "1999", epoch: 915166800000, value: 0.636 } 30 | // { label: "2001", epoch: 978325200000, value: 0.829 } 31 | // ] 32 | ``` 33 | 34 | This library has some opinions: 35 | * ticks should always be `spaced evenly`, even if this means less ticks 36 | * a tick should appear **at the start** of months, years, days 37 | * they don't need to begin or end at the start andend. 38 | * *less ticks* are better than too-many ticks 39 | 40 | it was built for labelling an x-axis in a space-limited way, but you can use it for whatever weird stuff. 41 | 42 | ## See also: 43 | * [d3-time](https://github.com/d3/d3-time) 44 | * [sometime](https://github.com/spencermountain/sometime) - spacetime-calendar 45 | 46 | MIT 47 | -------------------------------------------------------------------------------- /plugins/ticks/_version.js: -------------------------------------------------------------------------------- 1 | export default '0.3.0' -------------------------------------------------------------------------------- /plugins/ticks/demo/_drawGraph.js: -------------------------------------------------------------------------------- 1 | const somehow = require('somehow') 2 | 3 | const drawGraph = function (ticks, id) { 4 | const el = document.querySelector(id) 5 | const w = somehow({ 6 | width: 500, 7 | height: 20, 8 | }) 9 | ticks.map((tick) => { 10 | w.dot().at(tick.value, 1) 11 | }) 12 | w.y.fit() 13 | w.x.fit(0, 1) 14 | w.yAxis.remove() 15 | // w.xAxis.remove() 16 | el.innerHTML = w.build() 17 | } 18 | export default drawGraph 19 | -------------------------------------------------------------------------------- /plugins/ticks/demo/custom.js: -------------------------------------------------------------------------------- 1 | const htm = require('htm') 2 | const vhtml = require('vhtml'); 3 | const h = htm.bind(vhtml); 4 | const inputs = require('somehow-input'); 5 | const drawGraph = require('./_drawGraph') 6 | const spacetimeTicks = require('../src') 7 | 8 | const printTicks = function() { 9 | const start = document.querySelector('#start').querySelector('input').value 10 | const end = document.querySelector('#end').querySelector('input').value 11 | const n = document.querySelector('#ticks').querySelector('select').value 12 | let ticks = spacetimeTicks(start, end, n) 13 | drawGraph(ticks, '#graph-two') 14 | ticks = ticks.map((o) => { 15 | return h` 16 | ${o.label} 17 | ${o.value} 18 | ` 19 | }) 20 | document.querySelector('#results-two').innerHTML = h`${ticks}
` 21 | } 22 | 23 | const start = inputs.input({ 24 | label: 'start', 25 | value: 'June 5th 1998', 26 | width: 130, 27 | cb: () => printTicks() 28 | }) 29 | const end = inputs.input({ 30 | label: 'end', 31 | value: 'Oct 4 2002', 32 | width: 130, 33 | cb: () => printTicks() 34 | }) 35 | const select = inputs.select({ 36 | label: 'max-ticks', 37 | value: '6', 38 | width: 50, 39 | options: ['4', '5', '6', '7', '8', '9', '10', '11'], 40 | cb: () => printTicks() 41 | }) 42 | document.querySelector('#start').innerHTML = start.build() 43 | document.querySelector('#ticks').innerHTML = select.build() 44 | document.querySelector('#end').innerHTML = end.build() 45 | 46 | printTicks() 47 | -------------------------------------------------------------------------------- /plugins/ticks/demo/duration.js: -------------------------------------------------------------------------------- 1 | const spacetime = require('spacetime') 2 | const htm = require('htm') 3 | const vhtml = require('vhtml'); 4 | const h = htm.bind(vhtml); 5 | const inputs = require('somehow-input'); 6 | const drawGraph = require('./_drawGraph') 7 | const spacetimeTicks = require('../src') 8 | 9 | const printTicks = function() { 10 | const start = document.querySelector('#origin').querySelector('input').value 11 | const duration = document.querySelector('#duration').querySelector('input').value 12 | const n = document.querySelector('#ticks-two').querySelector('select').value 13 | const end = spacetime(start).epoch + Number(duration) 14 | let ticks = spacetimeTicks(start, end, n) 15 | drawGraph(ticks, '#graph') 16 | ticks = ticks.map((o) => { 17 | return h` 18 | ${o.label} 19 | ${o.value} 20 | ` 21 | }) 22 | document.querySelector('#results').innerHTML = h`${ticks}
` 23 | } 24 | 25 | const start = inputs.input({ 26 | label: 'start', 27 | value: 'June 5th 1998', 28 | width: 130, 29 | cb: () => printTicks() 30 | }) 31 | const select = inputs.select({ 32 | label: 'max-ticks', 33 | value: '6', 34 | width: 50, 35 | options: ['4', '5', '6', '7', '8', '9', '10', '11'], 36 | cb: () => printTicks() 37 | }) 38 | const end = inputs.duration({ 39 | label: '', 40 | value: { 41 | month: 3 42 | }, 43 | max: { 44 | year: 4 45 | }, 46 | min: { 47 | hour: 3 48 | }, 49 | cb: () => printTicks() 50 | }) 51 | document.querySelector('#origin').innerHTML = start.build() 52 | document.querySelector('#ticks-two').innerHTML = select.build() 53 | document.querySelector('#duration').innerHTML = end.build() 54 | 55 | printTicks() 56 | -------------------------------------------------------------------------------- /plugins/ticks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | •. 8 | 9 | 10 | 12 | 13 | 14 | 15 |
16 |

17 | spacetime-ticks: 18 |

19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 | 30 |
custom times:
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /plugins/ticks/index.js: -------------------------------------------------------------------------------- 1 | import lib from './src/index.js' 2 | export default lib -------------------------------------------------------------------------------- /plugins/ticks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-ticks", 3 | "version": "0.4.0", 4 | "description": "calculate the best breakpoints between two dates", 5 | "main": "src/index.js", 6 | "unpkg": "builds/spacetime-ticks.min.js", 7 | "module": "builds/spacetime-ticks.mjs", 8 | "type": "module", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "require": "./builds/spacetime-ticks.cjs", 13 | "import": "./builds/spacetime-ticks.mjs", 14 | "default": "./builds/spacetime-ticks.mjs" 15 | } 16 | }, 17 | "author": "Spencer Kelly (spencermountain)", 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "watch": "node --watch ./scratch.js", 21 | "start": "budo index.js:assets/bundle.js --live", 22 | "build:demo": "browserify index.js -t [ babelify --presets [ @babel/preset-env ] ] | derequire > ./assets/bundle.js", 23 | "version": "node ./scripts/version.js", 24 | "filesize": "node ./scripts/filesize.js", 25 | "build": "npm run version && rollup -c && npm run filesize" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/spencermountain/spacetime.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/spencermountain/spacetime/issues" 33 | }, 34 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/spacetime-ticks", 35 | "peerDependencies": { 36 | "spacetime": ">=6.1.0" 37 | }, 38 | "devDependencies": { 39 | "spencer-color": "0.1.0", 40 | "spencer-css": "1.1.3" 41 | }, 42 | "license": "MIT" 43 | } -------------------------------------------------------------------------------- /plugins/ticks/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | 6 | export default [ 7 | { 8 | input: 'src/index.js', 9 | output: [{ file: 'builds/spacetime-ticks.mjs', format: 'esm' }], 10 | plugins: [resolve(), json(), commonjs()] 11 | }, 12 | { 13 | input: 'src/index.js', 14 | output: [{ file: 'builds/spacetime-ticks.cjs', format: 'umd', name: 'spacetime-ticks' }], 15 | plugins: [resolve(), json(), commonjs()] 16 | }, 17 | { 18 | input: 'src/index.js', 19 | output: [{ file: 'builds/spacetime-ticks.min.js', format: 'umd', name: 'spacetime-ticks' }], 20 | plugins: [resolve(), json(), commonjs(), terser()] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /plugins/ticks/scratch.js: -------------------------------------------------------------------------------- 1 | import spacetimeTicks from './src/index.js' 2 | 3 | console.time('time') 4 | const ticks = spacetimeTicks('jan 1 2019', 'jan 1 2020', 12) 5 | console.log(ticks) 6 | console.timeEnd('time') 7 | -------------------------------------------------------------------------------- /plugins/ticks/scripts/filesize.js: -------------------------------------------------------------------------------- 1 | import { statSync } from 'fs' 2 | //log the size of our builds 3 | const stats = statSync('./builds/spacetime-ticks.min.js') 4 | const fileSize = (stats['size'] / 1000.0).toFixed(2) 5 | console.log('\n\n min: ' + fileSize + 'kb') 6 | -------------------------------------------------------------------------------- /plugins/ticks/scripts/version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | // avoid requiring our whole package.json file 3 | // make a small file for our version number 4 | const pkg = JSON.parse(fs.readFileSync('./package.json').toString()) 5 | 6 | fs.writeFileSync('./_version.js', `export default '${pkg.version}'`) 7 | -------------------------------------------------------------------------------- /plugins/ticks/src/_reduce.js: -------------------------------------------------------------------------------- 1 | const reduceTo = function (arr, n) { 2 | if (arr.length <= n || arr.length <= 5) { 3 | return arr 4 | } 5 | while (arr.length > n) { 6 | //remove every other one 7 | arr = arr.filter((o, i) => { 8 | return i % 2 === 0 9 | }) 10 | if (arr.length <= n || arr.length <= 5) { 11 | return arr 12 | } 13 | } 14 | return arr 15 | } 16 | export default reduceTo 17 | -------------------------------------------------------------------------------- /plugins/ticks/src/index.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import methods from './methods.js' 3 | import version from '../_version.js' 4 | 5 | const chooseMethod = function (start, end, n = 6) { 6 | const diff = start.diff(end) 7 | if (diff.years > 300) { 8 | return methods.centuries(start, end, n) 9 | } 10 | if (diff.years > 30) { 11 | return methods.decades(start, end, n) 12 | } 13 | if (diff.years > 3) { 14 | return methods.years(start, end, n) 15 | } 16 | if (diff.months > 3) { 17 | return methods.months(start, end, n) 18 | } 19 | if (diff.days > 3) { 20 | return methods.days(start, end, n) 21 | } 22 | if (diff.hours > 3) { 23 | return methods.hours(start, end, n) 24 | } 25 | if (diff.minutes > 3) { 26 | return methods.minutes(start, end, n) 27 | } 28 | return methods.months(start, end, n) 29 | } 30 | 31 | //flip it around backwards 32 | const reverseTicks = function (ticks) { 33 | ticks = ticks.map(o => { 34 | o.value = 1 - o.value 35 | return o 36 | }) 37 | return ticks.reverse() 38 | } 39 | 40 | const spacetimeTicks = function (start, end, n = 6) { 41 | let reverse = false 42 | start = spacetime(start) 43 | end = spacetime(end) 44 | //reverse them, if necessary 45 | if (start.epoch > end.epoch) { 46 | reverse = true 47 | const tmp = start.epoch 48 | start.epoch = end.epoch 49 | end.epoch = tmp 50 | } 51 | // nudge first one back 1 minute 52 | if (start.time() === '12:00am') { 53 | start = start.minus(1, 'minute') 54 | } 55 | let ticks = chooseMethod(start, end, n) 56 | //support backwards ticks 57 | if (reverse === true) { 58 | ticks = reverseTicks(ticks) 59 | } 60 | return ticks 61 | } 62 | spacetimeTicks.version = version 63 | 64 | export default spacetimeTicks 65 | -------------------------------------------------------------------------------- /plugins/tz/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
a CLI app to reckon a timezone
4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | npm i -g stz 16 |
17 | 18 | by 19 | Spencer Kelly 20 | 21 |
22 |

23 | an easy way to check what time it is, somewhere else, from the command-line: 24 | ```bash 25 | $ npx stz milwaukee 26 | # 5:20pm 27 | $ npx stz pacific time 28 | # 4:20pm 29 | ``` 30 | or you can install it locally with `npm i -g stz` 31 | 32 | this library uses [spacetime]() and [timezone-soft](https://github.com/spencermountain/timezone-soft) to loosely match a given timezone. 33 | 34 | MIT -------------------------------------------------------------------------------- /plugins/tz/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import spacetime from 'spacetime' 3 | import soft from 'timezone-soft' 4 | const help = function () { 5 | console.log(`\n\n stz - calculate current time in a given location`) 6 | console.log(`\n Usage: \`npx stz boston\``) 7 | console.log(`\n Usage: \`npx stz ACST\``) 8 | console.log('\n\n') 9 | } 10 | 11 | const str = process.argv.slice(2).join(' ').trim() 12 | if (!str) { 13 | help() 14 | process.exit() 15 | } 16 | 17 | const res = soft(str) 18 | if (res.length === 0) { 19 | console.log(`\n\nCould not find timezone for \'${str}\'`) 20 | help() 21 | process.exit() 22 | } 23 | const tz = res[0] 24 | // are we in standard time, or daylight time? 25 | const s = spacetime.now(tz.iana) 26 | let out = `${s.time()}` 27 | 28 | if (tz.daylight && s.isDST()) { 29 | out += ' ' + tz.daylight.abbr 30 | } else { 31 | out += ' ' + tz.standard.abbr 32 | } 33 | 34 | const here = spacetime.now() 35 | if (!s.isSame('day', here)) { 36 | out += ' ' + s.format('nice') 37 | } else { 38 | out += ' (today)' 39 | } 40 | 41 | console.log('\n' + out + '\n') 42 | -------------------------------------------------------------------------------- /plugins/tz/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stz", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "stz", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "minimist": "^1.2.5", 13 | "spacetime": ">=6.16.3", 14 | "timezone-soft": "1.5.2" 15 | }, 16 | "bin": { 17 | "stz": "cli.js" 18 | }, 19 | "engines": { 20 | "node": ">=8" 21 | } 22 | }, 23 | "node_modules/minimist": { 24 | "version": "1.2.5", 25 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 26 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 27 | }, 28 | "node_modules/spacetime": { 29 | "version": "6.16.3", 30 | "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz", 31 | "integrity": "sha512-JQEfj3VHT1gU1IMV5NvhgAP8P+2mDFd84ZCiHN//dp6hRKmuW0IizHissy62lO0nilfFjVhnoSaMC7te+Y5f4A==" 32 | }, 33 | "node_modules/timezone-soft": { 34 | "version": "1.5.2", 35 | "resolved": "https://registry.npmjs.org/timezone-soft/-/timezone-soft-1.5.2.tgz", 36 | "integrity": "sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==", 37 | "license": "MIT" 38 | } 39 | }, 40 | "dependencies": { 41 | "minimist": { 42 | "version": "1.2.5", 43 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 44 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 45 | }, 46 | "spacetime": { 47 | "version": "6.16.3", 48 | "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz", 49 | "integrity": "sha512-JQEfj3VHT1gU1IMV5NvhgAP8P+2mDFd84ZCiHN//dp6hRKmuW0IizHissy62lO0nilfFjVhnoSaMC7te+Y5f4A==" 50 | }, 51 | "timezone-soft": { 52 | "version": "1.5.2", 53 | "resolved": "https://registry.npmjs.org/timezone-soft/-/timezone-soft-1.5.2.tgz", 54 | "integrity": "sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plugins/tz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stz", 3 | "version": "0.2.0", 4 | "description": "a CLI timezone calculator", 5 | "main": "cli.js", 6 | "type": "module", 7 | "sideEffects": false, 8 | "exports": { 9 | ".": { 10 | "import": "./cli.js", 11 | "default": "./cli.js" 12 | } 13 | }, 14 | "bin": { 15 | "stz": "cli.js" 16 | }, 17 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/tz", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/spencermountain/spacetime.git" 21 | }, 22 | "engines": { 23 | "node": ">=8" 24 | }, 25 | "scripts": { 26 | "test": "" 27 | }, 28 | "prettier": { 29 | "trailingComma": "none", 30 | "tabWidth": 2, 31 | "semi": false, 32 | "singleQuote": true, 33 | "printWidth": 100 34 | }, 35 | "dependencies": { 36 | "minimist": "^1.2.5", 37 | "spacetime": ">=6.16.3", 38 | "timezone-soft": "1.5.2" 39 | }, 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /plugins/week-of-month/builds/spacetime-week-of-month.cjs: -------------------------------------------------------------------------------- 1 | /* spencermountain/spacetime-week-of-month 0.1.0 Apache 2.0 */ 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 4 | typeof define === 'function' && define.amd ? define(factory) : 5 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.weekOfMonth = factory()); 6 | })(this, (function () { 'use strict'; 7 | 8 | // the first week of a month includes a thursday, in that month 9 | // (leap days do not effect week-ordering!) 10 | const getFirstWeek = function (s) { 11 | let month = s.month(); 12 | let start = s.date(1); 13 | start = start.startOf('week'); 14 | let thu = start.add(3, 'days'); 15 | if (thu.month() !== month) { 16 | start = start.add(1, 'week'); 17 | } 18 | return start 19 | }; 20 | 21 | var index = { 22 | weekOfMonth: function (n) { 23 | let start = getFirstWeek(this.clone()); 24 | // week-setter 25 | if (n !== undefined) { 26 | return start.add(n, 'weeks') 27 | } 28 | // week-getter 29 | let num = 0; 30 | let end = start.endOf('week'); 31 | for (let i = 0; i < 5; i += 1) { 32 | if (end.isAfter(this)) { 33 | return num + 1 34 | } 35 | end = end.add(1, 'week'); 36 | num += 1; 37 | } 38 | return num + 1 39 | }, 40 | whichWeek: function () { 41 | let s = this.startOf('week'); 42 | // it's always in the same month that it's thursday is... 43 | let thurs = s.add(3, 'days'); 44 | let month = thurs.monthName(); 45 | let num = thurs.weekOfMonth(); 46 | 47 | return { num, month } 48 | }, 49 | firstWeek: function () { 50 | return getFirstWeek(this.clone()) 51 | }, 52 | lastSunday: function () { 53 | let s = this.endOf('month'); //last day 54 | // if it's after thursday 55 | if (s.day() > 4) { 56 | return s.endOf('week') 57 | } 58 | // else, the previous sunday 59 | s = s.minus(1, 'week'); 60 | return s.endOf('week') 61 | } 62 | }; 63 | 64 | return index; 65 | 66 | })); 67 | -------------------------------------------------------------------------------- /plugins/week-of-month/builds/spacetime-week-of-month.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).weekOfMonth=t()}(this,(function(){"use strict";const e=function(e){let t=e.month(),n=e.date(1);return n=n.startOf("week"),n.add(3,"days").month()!==t&&(n=n.add(1,"week")),n};return{weekOfMonth:function(t){let n=e(this.clone());if(void 0!==t)return n.add(t,"weeks");let o=0,f=n.endOf("week");for(let e=0;e<5;e+=1){if(f.isAfter(this))return o+1;f=f.add(1,"week"),o+=1}return o+1},whichWeek:function(){let e=this.startOf("week").add(3,"days"),t=e.monthName();return{num:e.weekOfMonth(),month:t}},firstWeek:function(){return e(this.clone())},lastSunday:function(){let e=this.endOf("month");return e.day()>4||(e=e.minus(1,"week")),e.endOf("week")}}})); 2 | -------------------------------------------------------------------------------- /plugins/week-of-month/builds/spacetime-week-of-month.mjs: -------------------------------------------------------------------------------- 1 | /* spencermountain/spacetime-week-of-month 0.1.0 Apache 2.0 */ 2 | // the first week of a month includes a thursday, in that month 3 | // (leap days do not effect week-ordering!) 4 | const getFirstWeek = function (s) { 5 | let month = s.month(); 6 | let start = s.date(1); 7 | start = start.startOf('week'); 8 | let thu = start.add(3, 'days'); 9 | if (thu.month() !== month) { 10 | start = start.add(1, 'week'); 11 | } 12 | return start 13 | }; 14 | 15 | var index = { 16 | weekOfMonth: function (n) { 17 | let start = getFirstWeek(this.clone()); 18 | // week-setter 19 | if (n !== undefined) { 20 | return start.add(n, 'weeks') 21 | } 22 | // week-getter 23 | let num = 0; 24 | let end = start.endOf('week'); 25 | for (let i = 0; i < 5; i += 1) { 26 | if (end.isAfter(this)) { 27 | return num + 1 28 | } 29 | end = end.add(1, 'week'); 30 | num += 1; 31 | } 32 | return num + 1 33 | }, 34 | whichWeek: function () { 35 | let s = this.startOf('week'); 36 | // it's always in the same month that it's thursday is... 37 | let thurs = s.add(3, 'days'); 38 | let month = thurs.monthName(); 39 | let num = thurs.weekOfMonth(); 40 | 41 | return { num, month } 42 | }, 43 | firstWeek: function () { 44 | return getFirstWeek(this.clone()) 45 | }, 46 | lastSunday: function () { 47 | let s = this.endOf('month'); //last day 48 | // if it's after thursday 49 | if (s.day() > 4) { 50 | return s.endOf('week') 51 | } 52 | // else, the previous sunday 53 | s = s.minus(1, 'week'); 54 | return s.endOf('week') 55 | } 56 | }; 57 | 58 | export { index as default }; 59 | -------------------------------------------------------------------------------- /plugins/week-of-month/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-week-of-month", 3 | "version": "0.2.0", 4 | "description": "calculate nth week of the month", 5 | "main": "src/index.js", 6 | "unpkg": "builds/spacetime-week-of-month.min.js", 7 | "module": "builds/spacetime-week-of-month.mjs", 8 | "type": "module", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "require": "./builds/spacetime-week-of-month.cjs", 13 | "import": "./builds/spacetime-week-of-month.mjs", 14 | "default": "./builds/spacetime-week-of-month.mjs" 15 | } 16 | }, 17 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/spacetime-week-of-month", 18 | "scripts": { 19 | "test": "TESTENV=dev tape ./week.test.js | tap-dancer --color always", 20 | "build": "rollup -c --silent" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/spencermountain/spacetime.git" 25 | }, 26 | "prettier": { 27 | "trailingComma": "none", 28 | "tabWidth": 2, 29 | "semi": false, 30 | "singleQuote": true, 31 | "printWidth": 100 32 | }, 33 | "devDependencies": { 34 | "tap-dancer": "0.3.4", 35 | "tape": "5.5.3" 36 | }, 37 | "license": "MIT" 38 | } -------------------------------------------------------------------------------- /plugins/week-of-month/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import sizeCheck from 'rollup-plugin-filesize-check' 6 | import { version } from './package.json' 7 | 8 | console.log('\n 📦 - running rollup..\n') 9 | 10 | const name = 'spacetime-week-of-month' 11 | const banner = `/* spencermountain/${name} ` + version + ' Apache 2.0 */' 12 | 13 | export default [ 14 | { 15 | input: 'src/index.js', 16 | output: [{ banner: banner, file: `builds/${name}.mjs`, format: 'esm' }], 17 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 1, warn: 10 })] 18 | }, 19 | { 20 | input: 'src/index.js', 21 | output: [{ banner: banner, file: `builds/${name}.cjs`, format: 'umd', sourcemap: false, name: 'weekOfMonth' }], 22 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 1, warn: 10 })] 23 | }, 24 | { 25 | input: 'src/index.js', 26 | output: [{ banner: banner, file: `builds/${name}.min.js`, format: 'umd', name: 'weekOfMonth' }], 27 | plugins: [resolve(), json(), commonjs(), terser(), sizeCheck({ expect: 1, warn: 10 })] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /plugins/week-of-month/src/index.js: -------------------------------------------------------------------------------- 1 | // the first week of a month includes a thursday, in that month 2 | // (leap days do not effect week-ordering!) 3 | const getFirstWeek = function (s) { 4 | const month = s.month() 5 | let start = s.date(1) 6 | start = start.startOf('week') 7 | const thu = start.add(3, 'days') 8 | if (thu.month() !== month) { 9 | start = start.add(1, 'week') 10 | } 11 | return start 12 | } 13 | 14 | export default { 15 | weekOfMonth: function (n) { 16 | const start = getFirstWeek(this.clone()) 17 | // week-setter 18 | if (n !== undefined) { 19 | return start.add(n, 'weeks') 20 | } 21 | // week-getter 22 | let num = 0 23 | let end = start.endOf('week') 24 | for (let i = 0; i < 5; i += 1) { 25 | if (end.isAfter(this)) { 26 | return num + 1 27 | } 28 | end = end.add(1, 'week') 29 | num += 1 30 | } 31 | return num + 1 32 | }, 33 | whichWeek: function () { 34 | const s = this.startOf('week') 35 | // it's always in the same month that it's thursday is... 36 | const thurs = s.add(3, 'days') 37 | const month = thurs.monthName() 38 | const num = thurs.weekOfMonth() 39 | 40 | return { num, month } 41 | }, 42 | firstWeek: function () { 43 | return getFirstWeek(this.clone()) 44 | }, 45 | lastSunday: function () { 46 | let s = this.endOf('month') //last day 47 | // if it's after thursday 48 | if (s.day() > 4) { 49 | return s.endOf('week') 50 | } 51 | // else, the previous sunday 52 | s = s.minus(1, 'week') 53 | return s.endOf('week') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugins/week-of-month/week.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from '../../src/index.js' 3 | import plugin from './src/index.js' 4 | spacetime.plugin(plugin) 5 | 6 | test('weekOfMonth-getter', (t) => { 7 | // october 1st starts on a thursday 8 | const arr = [ 9 | // ['sep 28 2020', 1], //mon 10 | // ['sep 29 2020', 1], //tues 11 | // ['sep 30 2020', 1], //wed 12 | ['oct 1 2020', 1], //thurs 13 | ['oct 2 2020', 1], //fri 14 | ['oct 3 2020', 1], //sat 15 | ['oct 4 2020', 1], //sun 16 | ['oct 5 2020', 2], //mon 17 | ['oct 6 2020', 2], //tues 18 | ['oct 7 2020', 2], //wed 19 | ['oct 8 2020', 2] //thurs 20 | ] 21 | arr.forEach((a) => { 22 | const s = spacetime(a[0]) 23 | t.equal(s.weekOfMonth(), a[1], a[0] + ' ' + a[1]) 24 | }) 25 | t.end() 26 | }) 27 | 28 | test('weekOfMonth-setter', (t) => { 29 | let s = spacetime('oct 8 2020') 30 | s = s.weekOfMonth(0) 31 | t.equal(s.format('iso-short'), '2020-09-28', '0') 32 | 33 | s = spacetime('oct 8 2020') 34 | s = s.weekOfMonth(1) 35 | t.equal(s.format('iso-short'), '2020-10-05', '1') 36 | 37 | s = spacetime('oct 8 2020') 38 | s = s.weekOfMonth(2) 39 | t.equal(s.format('iso-short'), '2020-10-12', '2') 40 | s = spacetime('oct 8 2020') 41 | 42 | s = spacetime('oct 8 2020') 43 | s = s.weekOfMonth(3) 44 | t.equal(s.format('iso-short'), '2020-10-19', '3') 45 | 46 | s = spacetime('oct 8 2020') 47 | s = s.weekOfMonth(4) 48 | t.equal(s.format('iso-short'), '2020-10-26', '4') 49 | 50 | s = spacetime('oct 8 2020') 51 | s = s.weekOfMonth(5) 52 | t.equal(s.format('iso-short'), '2020-11-02', '5') 53 | t.end() 54 | }) 55 | -------------------------------------------------------------------------------- /plugins/week-start/_version.js: -------------------------------------------------------------------------------- 1 | export default '0.0.1' -------------------------------------------------------------------------------- /plugins/week-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacetime-week-start", 3 | "description": "calculate start-of-week", 4 | "version": "0.3.0", 5 | "main": "src/index.js", 6 | "unpkg": "builds/spacetime-week-start.min.js", 7 | "type": "module", 8 | "sideEffects": false, 9 | "exports": { 10 | ".": { 11 | "require": "./builds/spacetime-week-start.cjs", 12 | "import": "./builds/spacetime-week-start.mjs", 13 | "default": "./builds/spacetime-week-start.mjs" 14 | } 15 | }, 16 | "author": "Martin Spodniak & Spencer Kelly", 17 | "homepage": "https://github.com/spencermountain/spacetime/tree/master/plugins/week-start", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/spencermountain/spacetime.git" 21 | }, 22 | "scripts": { 23 | "watch": "node --watch ./scratch.js", 24 | "test": "TESTENV=dev tape ./test/**/*.test.js | tap-dancer", 25 | "build": "rollup -c --silent" 26 | }, 27 | "prettier": { 28 | "trailingComma": "none", 29 | "tabWidth": 2, 30 | "semi": false, 31 | "singleQuote": true, 32 | "printWidth": 100 33 | }, 34 | "devDependencies": { 35 | "shelljs": "0.8.5", 36 | "tap-dancer": "0.3.4", 37 | "tape": "5.5.3", 38 | "terser": "5.39.0" 39 | }, 40 | "peerDependencies": { 41 | "spacetime": ">=5.8.2" 42 | }, 43 | "licence": "MIT" 44 | } -------------------------------------------------------------------------------- /plugins/week-start/rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import json from 'rollup-plugin-json' 4 | import { terser } from 'rollup-plugin-terser' 5 | import resolve from 'rollup-plugin-node-resolve' 6 | import sizeCheck from 'rollup-plugin-filesize-check' 7 | const pkg = JSON.parse(fs.readFileSync('./package.json').toString()) 8 | const version = pkg.version 9 | 10 | console.log('\n 📦 - running rollup..\n') 11 | 12 | const name = 'spacetime-week-start' 13 | const banner = `/* spencermountain/${name} ` + version + ' Apache 2.0 */' 14 | 15 | export default [ 16 | { 17 | input: 'src/index.js', 18 | output: [{ banner: banner, file: `builds/${name}.mjs`, format: 'esm' }], 19 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 147, warn: 10 })] 20 | }, 21 | { 22 | input: 'src/index.js', 23 | output: [{ banner: banner, file: `builds/${name}.cjs`, format: 'umd', sourcemap: false, name: 'weekStart' }], 24 | plugins: [resolve(), json(), commonjs(), sizeCheck({ expect: 159, warn: 10 })] 25 | }, 26 | { 27 | input: 'src/index.js', 28 | output: [{ banner: banner, file: `builds/${name}.min.js`, format: 'umd', name: 'weekStart' }], 29 | plugins: [resolve(), json(), commonjs(), terser(), sizeCheck({ expect: 79, warn: 10 })] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /plugins/week-start/scratch.js: -------------------------------------------------------------------------------- 1 | import spacetime from 'spacetime' 2 | import spacetimeWeek from './src/index.js' 3 | 4 | spacetime.extend(spacetimeWeek) 5 | const d = spacetime.now('Europe/Berlin') 6 | console.log(d.weekStart('iran')) 7 | -------------------------------------------------------------------------------- /plugins/week-start/src/index.js: -------------------------------------------------------------------------------- 1 | import { getWeekStart } from './input/weekStart.js' 2 | 3 | export default { 4 | weekStart: function (input) { 5 | input = input || this.timezone().name 6 | return getWeekStart(input) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugins/week-start/test/basic.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { getWeekStart as weekStart } from '../src/input/weekStart.js' 3 | 4 | test('week by country returns values, when no or falsy argument is supplied', t => { 5 | t.notEqual(weekStart(), null) 6 | t.notEqual(weekStart(), undefined) 7 | t.notEqual(weekStart(12), null) 8 | t.notEqual(weekStart(12), undefined) 9 | 10 | t.notEqual(weekStart(null), null) 11 | t.notEqual(weekStart(null), undefined) 12 | t.notEqual(weekStart(''), null) 13 | t.notEqual(weekStart(''), undefined) 14 | 15 | t.notEqual(weekStart(undefined), null) 16 | t.notEqual(weekStart(undefined), undefined) 17 | t.notEqual(weekStart('abc'), null) 18 | t.notEqual(weekStart('abc'), undefined) 19 | t.end() 20 | }) 21 | 22 | test('JSON values are string and not empty', t => { 23 | t.equal(typeof weekStart().day, 'string') 24 | t.notEqual(weekStart().day, '') 25 | t.equal(typeof weekStart().country, 'string') 26 | t.notEqual(weekStart().country, '') 27 | t.end() 28 | }) 29 | 30 | test('when supplied full coutry name returns day and country', t => { 31 | t.equal(weekStart('canada').day, 'sunday') 32 | t.equal(weekStart('canada').country, 'canada') 33 | t.end() 34 | }) 35 | 36 | test('when supplied partial coutry name returns day and country', t => { 37 | t.equal(weekStart('united arab emirates').day, 'sunday') 38 | t.equal(weekStart('emirates').country, 'united arab emirates') 39 | t.end() 40 | }) 41 | 42 | test('country name can be in lower case, upper case or camel case', t => { 43 | t.equal(weekStart('cana').day, 'sunday') 44 | t.equal(weekStart('nada').country, 'grenada') 45 | t.notEqual(weekStart('nada').country, 'canada') 46 | // finds first occourance of string 'nada' in JSON 47 | t.equal(weekStart('CANADA').day, 'sunday') 48 | t.equal(weekStart('Canada').country, 'canada') 49 | // it's located in array under key "monday" for 50 | // grenada and after that appears located in 51 | // canada under key "sunday" 52 | t.equal(weekStart('CaNa').day, 'sunday') 53 | t.equal(weekStart('nAdA').country, 'grenada') 54 | t.notEqual(weekStart('nAdA').country, 'canada') 55 | t.end() 56 | }) 57 | -------------------------------------------------------------------------------- /plugins/week-start/weekStart-demo.js: -------------------------------------------------------------------------------- 1 | import weekStart from './src/input/weekStart.js'; 2 | 3 | console.log('#1: ', weekStart()); 4 | console.log('#2: ', weekStart(12)); 5 | console.log('#3: ', weekStart(null)); 6 | console.log('#4: ', weekStart('')); 7 | console.log('#5: ', weekStart(undefined)); 8 | console.log('#6: ', weekStart('abc')); 9 | // all returns results for current tz, f.e. 10 | // { day: 'sunday', country: 'canada' } 11 | 12 | console.log('#7: ', weekStart('slovakia')); 13 | // tz: europe/bratislava 14 | // { day: 'monday', country: 'slovakia' } 15 | 16 | console.log('#8: ', weekStart('iran')); 17 | //tz: asia/tehran 18 | // { day: 'saturday', country: 'iran' } 19 | 20 | console.log('#9: ', weekStart('canAda')); 21 | //tz: f.e. america/montreal 22 | // { day: 'sunday', country: 'canada' } 23 | 24 | console.log('#10: ', weekStart('lize')); 25 | // tz: america/belize 26 | // { day: 'monday', country: 'belize' } 27 | 28 | console.log('#11: ', weekStart('el salvador')); 29 | // tz: america/el_salvador 30 | // { day: 'monday', country: 'el salvador' } 31 | 32 | console.log('#12: ', weekStart('zulu')); 33 | // tz: etc/zulu 34 | // { day: 'monday', location: 'zulu' } 35 | 36 | console.log('#13: ', weekStart('gmt')); 37 | // tz: f.e. etc/gmt 38 | // { day: 'monday', location: 'gmt' } 39 | 40 | console.log('#14: ', weekStart('antarctica')); 41 | // tz: f.e. antarctica/south_pole 42 | // { day: 'monday', location: 'antarctica' } 43 | 44 | console.log('#15: ', weekStart('arctic')); 45 | // tz: f.e. arctic/longyearbyen 46 | // { day: 'monday', location: 'arctic' } 47 | 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import json from 'rollup-plugin-json' 3 | import { terser } from 'rollup-plugin-terser' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import sizeCheck from 'rollup-plugin-filesize-check' 6 | import fs from 'fs' 7 | 8 | const pkg = JSON.parse(fs.readFileSync('./package.json').toString()) 9 | const version = pkg.version 10 | console.log('\n 📦 - running rollup..\n') 11 | 12 | const banner = '/* spencermountain/spacetime ' + version + ' Apache 2.0 */' 13 | 14 | export default [ 15 | { 16 | input: 'src/index.js', 17 | output: [{ banner: banner, file: 'builds/spacetime.mjs', format: 'esm' }], 18 | plugins: [resolve(), json(), terser(), sizeCheck({ expect: 48, warn: 10 })] 19 | }, 20 | { 21 | input: 'src/index.js', 22 | output: [ 23 | { 24 | banner: banner, 25 | file: 'builds/spacetime.cjs', 26 | format: 'umd', 27 | sourcemap: false, 28 | name: 'spacetime' 29 | } 30 | ], 31 | plugins: [ 32 | resolve(), 33 | json(), 34 | commonjs(), 35 | sizeCheck({ expect: 110, warn: 10 }) 36 | ] 37 | }, 38 | { 39 | input: 'src/index.js', 40 | output: [{ banner: banner, file: 'builds/spacetime.min.js', format: 'umd', name: 'spacetime' }], 41 | plugins: [ 42 | resolve(), 43 | json(), 44 | commonjs(), 45 | terser(), 46 | sizeCheck({ expect: 48, warn: 10 }) 47 | ] 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /scratch.js: -------------------------------------------------------------------------------- 1 | import spacetime from './src/index.js' 2 | 3 | // console.log(spacetime('Feb 29 2001').iso()) 4 | 5 | // 6 | // let s = spacetime('1995-12-07T03:24:30', 'Africa/Cairo') 7 | // s = s.timezone('America/Toronto') 8 | // console.log(s.iso()) 9 | 10 | 11 | 12 | let mils = 1744200453183 13 | let secs = 1744200453 14 | 15 | let s = spacetime('jan 5 2028 4:30pm') 16 | console.log(s.epochSeconds()); 17 | console.log(s.iso()); 18 | 19 | 20 | 21 | // let s = spacetime("foobar", 'UTC') 22 | // console.log(s.time()) 23 | // console.log(s.epoch) 24 | // console.log(s.year()) 25 | const arr = [ 26 | 'millisecond', 27 | 'second', 28 | 'minute', 29 | 'hour', 30 | 'hourFloat', 31 | 'hour12', 32 | 'time', 33 | 'ampm', 34 | 'dayTime', 35 | 'iso', 36 | 'epochsecs', 37 | 'date', 38 | 'day', 39 | 'dayName', 40 | 'dayOfYear', 41 | 'week', 42 | 'month', 43 | 'monthName', 44 | 'quarter', 45 | 'season', 46 | 'year', 47 | 'era', 48 | 'decade', 49 | 'century', 50 | 'millenium', 51 | ] 52 | // arr.forEach(fn => { 53 | // console.log(s[fn](), fn) 54 | // }) 55 | 56 | // console.log(s.epochsecs(), 1735689600) 57 | // console.log(s.epochsecs() == 1735689600) -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | // avoid requiring our whole package.json file 3 | // make a small file for our version number 4 | const pkg = JSON.parse(fs.readFileSync('./package.json').toString()) 5 | 6 | fs.writeFileSync('./src/_version.js', `export default '${pkg.version}'`) 7 | -------------------------------------------------------------------------------- /src/_version.js: -------------------------------------------------------------------------------- 1 | export default '7.10.0' -------------------------------------------------------------------------------- /src/data/ampm.js: -------------------------------------------------------------------------------- 1 | let morning = 'am' 2 | let evening = 'pm' 3 | 4 | export function am() { return morning } 5 | export function pm() { return evening } 6 | export function set(i18n) { 7 | morning = i18n.am || morning 8 | evening = i18n.pm || evening 9 | } -------------------------------------------------------------------------------- /src/data/caseFormat.js: -------------------------------------------------------------------------------- 1 | let titleCaseEnabled = true 2 | 3 | export function useTitleCase() { 4 | return titleCaseEnabled 5 | } 6 | 7 | export function set(val) { 8 | titleCaseEnabled = val 9 | } 10 | -------------------------------------------------------------------------------- /src/data/days.js: -------------------------------------------------------------------------------- 1 | let shortDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] 2 | let longDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] 3 | 4 | export function short() { return shortDays } 5 | export function long() { return longDays } 6 | export function set(i18n) { 7 | shortDays = i18n.short || shortDays 8 | longDays = i18n.long || longDays 9 | } 10 | export const aliases = { 11 | mo: 1, 12 | tu: 2, 13 | we: 3, 14 | th: 4, 15 | fr: 5, 16 | sa: 6, 17 | su: 7, 18 | tues: 2, 19 | weds: 3, 20 | wedn: 3, 21 | thur: 4, 22 | thurs: 4 23 | } 24 | -------------------------------------------------------------------------------- /src/data/distance.js: -------------------------------------------------------------------------------- 1 | let past = 'past' 2 | let future = 'future' 3 | let present = 'present' 4 | let now = 'now' 5 | let almost = 'almost' 6 | let over = 'over' 7 | let pastDistance = (value) => `${value} ago` 8 | let futureDistance = (value) => `in ${value}` 9 | 10 | export function pastDistanceString(value) { return pastDistance(value) } 11 | export function futureDistanceString(value) { return futureDistance(value) } 12 | export function pastString() { return past } 13 | export function futureString() { return future } 14 | export function presentString() { return present } 15 | export function nowString() { return now } 16 | export function almostString() { return almost } 17 | export function overString() { return over } 18 | 19 | export function set(i18n) { 20 | pastDistance = i18n.pastDistance || pastDistance 21 | futureDistance = i18n.futureDistance || futureDistance 22 | past = i18n.past || past 23 | future = i18n.future || future 24 | present = i18n.present || present 25 | now = i18n.now || now 26 | almost = i18n.almost || almost 27 | over = i18n.over || over 28 | } -------------------------------------------------------------------------------- /src/data/milliseconds.js: -------------------------------------------------------------------------------- 1 | const o = { 2 | millisecond: 1 3 | } 4 | o.second = 1000 5 | o.minute = 60000 6 | o.hour = 3.6e6 // dst is supported post-hoc 7 | o.day = 8.64e7 // 8 | o.date = o.day 9 | o.month = 8.64e7 * 29.5 //(average) 10 | o.week = 6.048e8 11 | o.year = 3.154e10 // leap-years are supported post-hoc 12 | //add plurals 13 | Object.keys(o).forEach(k => { 14 | o[k + 's'] = o[k] 15 | }) 16 | export default o 17 | -------------------------------------------------------------------------------- /src/data/monthLengths.js: -------------------------------------------------------------------------------- 1 | const monthLengths = [ 2 | 31, // January - 31 days 3 | 28, // February - 28 days in a common year and 29 days in leap years 4 | 31, // March - 31 days 5 | 30, // April - 30 days 6 | 31, // May - 31 days 7 | 30, // June - 30 days 8 | 31, // July - 31 days 9 | 31, // August - 31 days 10 | 30, // September - 30 days 11 | 31, // October - 31 days 12 | 30, // November - 30 days 13 | 31 // December - 31 days 14 | ] 15 | export default monthLengths 16 | 17 | // 28 - feb 18 | // 30 - april, june, sept, nov 19 | // 31 - jan, march, may, july, aug, oct, dec 20 | -------------------------------------------------------------------------------- /src/data/months.js: -------------------------------------------------------------------------------- 1 | let shortMonths = [ 2 | 'jan', 3 | 'feb', 4 | 'mar', 5 | 'apr', 6 | 'may', 7 | 'jun', 8 | 'jul', 9 | 'aug', 10 | 'sep', 11 | 'oct', 12 | 'nov', 13 | 'dec' 14 | ] 15 | let longMonths = [ 16 | 'january', 17 | 'february', 18 | 'march', 19 | 'april', 20 | 'may', 21 | 'june', 22 | 'july', 23 | 'august', 24 | 'september', 25 | 'october', 26 | 'november', 27 | 'december' 28 | ] 29 | 30 | function buildMapping() { 31 | const obj = { 32 | sep: 8 //support this format 33 | } 34 | for (let i = 0; i < shortMonths.length; i++) { 35 | obj[shortMonths[i]] = i 36 | } 37 | for (let i = 0; i < longMonths.length; i++) { 38 | obj[longMonths[i]] = i 39 | } 40 | return obj 41 | } 42 | 43 | export function short() { return shortMonths } 44 | export function long() { return longMonths } 45 | export function mapping() { return buildMapping() } 46 | export function set(i18n) { 47 | shortMonths = i18n.short || shortMonths 48 | longMonths = i18n.long || longMonths 49 | } 50 | -------------------------------------------------------------------------------- /src/data/quarters.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | null, 3 | [0, 1], //jan 1 4 | [3, 1], //apr 1 5 | [6, 1], //july 1 6 | [9, 1] //oct 1 7 | ] 8 | -------------------------------------------------------------------------------- /src/data/seasons.js: -------------------------------------------------------------------------------- 1 | //https://www.timeanddate.com/calendar/aboutseasons.html 2 | // Spring - from March 1 to May 31; 3 | // Summer - from June 1 to August 31; 4 | // Fall (autumn) - from September 1 to November 30; and, 5 | // Winter - from December 1 to February 28 (February 29 in a leap year). 6 | const north = [ 7 | ['spring', 2, 1], 8 | ['summer', 5, 1], 9 | ['fall', 8, 1], 10 | ['autumn', 8, 1], 11 | ['winter', 11, 1] //dec 1 12 | ]; 13 | const south = [ 14 | ['fall', 2, 1], 15 | ['autumn', 2, 1], 16 | ['winter', 5, 1], 17 | ['spring', 8, 1], 18 | ['summer', 11, 1] //dec 1 19 | ]; 20 | 21 | export default { north, south } -------------------------------------------------------------------------------- /src/data/units.js: -------------------------------------------------------------------------------- 1 | let units = { 2 | second: 'second', 3 | seconds: 'seconds', 4 | minute: 'minute', 5 | minutes: 'minutes', 6 | hour: 'hour', 7 | hours: 'hours', 8 | day: 'day', 9 | days: 'days', 10 | month: 'month', 11 | months: 'months', 12 | year: 'year', 13 | years: 'years', 14 | }; 15 | 16 | export function unitsString(unit) { 17 | return units[unit] || ''; 18 | } 19 | 20 | export function set(i18n = {}) { 21 | units = { 22 | second: i18n.second || units.second, 23 | seconds: i18n.seconds || units.seconds, 24 | minute: i18n.minute || units.minute, 25 | minutes: i18n.minutes || units.minutes, 26 | hour: i18n.hour || units.hour, 27 | hours: i18n.hours || units.hours, 28 | day: i18n.day || units.day, 29 | days: i18n.days || units.days, 30 | month: i18n.month || units.month, 31 | months: i18n.months || units.months, 32 | year: i18n.year || units.year, 33 | years: i18n.years || units.years, 34 | }; 35 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Spacetime from './spacetime.js' 2 | import whereIts from './whereIts.js' 3 | import version from './_version.js' 4 | 5 | const main = (input, tz, options) => new Spacetime(input, tz, options) 6 | 7 | // set all properties of a given 'today' object 8 | const setToday = function (s) { 9 | const today = s._today || {} 10 | Object.keys(today).forEach((k) => { 11 | s = s[k](today[k]) 12 | }) 13 | return s 14 | } 15 | 16 | //some helper functions on the main method 17 | main.now = (tz, options) => { 18 | let s = new Spacetime(new Date().getTime(), tz, options) 19 | s = setToday(s) 20 | return s 21 | } 22 | main.today = (tz, options) => { 23 | let s = new Spacetime(new Date().getTime(), tz, options) 24 | s = setToday(s) 25 | return s.startOf('day') 26 | } 27 | main.tomorrow = (tz, options) => { 28 | let s = new Spacetime(new Date().getTime(), tz, options) 29 | s = setToday(s) 30 | return s.add(1, 'day').startOf('day') 31 | } 32 | main.yesterday = (tz, options) => { 33 | let s = new Spacetime(new Date().getTime(), tz, options) 34 | s = setToday(s) 35 | return s.subtract(1, 'day').startOf('day') 36 | } 37 | main.fromUnixSeconds = (secs, tz, options) => { 38 | return new Spacetime(secs * 1000, tz, options) 39 | } 40 | 41 | main.extend = function (obj = {}) { 42 | Object.keys(obj).forEach((k) => { 43 | Spacetime.prototype[k] = obj[k] 44 | }) 45 | return this 46 | } 47 | main.timezones = function () { 48 | const s = new Spacetime() 49 | return s.timezones 50 | } 51 | main.max = function (tz, options) { 52 | const s = new Spacetime(null, tz, options) 53 | s.epoch = 8640000000000000 54 | return s 55 | } 56 | main.min = function (tz, options) { 57 | const s = new Spacetime(null, tz, options) 58 | s.epoch = -8640000000000000 59 | return s 60 | } 61 | 62 | //find tz by time 63 | main.whereIts = whereIts 64 | main.version = version 65 | 66 | //aliases: 67 | main.plugin = main.extend 68 | export default main 69 | -------------------------------------------------------------------------------- /src/input/formats/03-dmy.js: -------------------------------------------------------------------------------- 1 | import walkTo from '../../methods/set/walk.js' 2 | import { toCardinal } from '../../fns.js' 3 | import { validate, parseTime, parseYear, parseMonth } from './_parsers.js' 4 | 5 | export default [ 6 | // ===== 7 | // d-m-y 8 | // ===== 9 | //common british format - "25-feb-2015" 10 | { 11 | reg: /^([0-9]{1,2})[-/]([a-z]+)[\-/]?([0-9]{4})?$/i, 12 | parse: (s, m) => { 13 | const obj = { 14 | year: parseYear(m[3], s._today), 15 | month: parseMonth(m[2]), 16 | date: toCardinal(m[1] || '') 17 | } 18 | if (validate(obj) === false) { 19 | s.epoch = null 20 | return s 21 | } 22 | walkTo(s, obj) 23 | s = parseTime(s, m[4]) 24 | return s 25 | } 26 | }, 27 | // "25 Mar 2015" 28 | { 29 | reg: /^([0-9]{1,2})( [a-z]+)( [0-9]{4}| '[0-9]{2})? ?([0-9]{1,2}:[0-9]{2}:?[0-9]{0,2} ?(am|pm|gmt))?$/i, 30 | parse: (s, m) => { 31 | const obj = { 32 | year: parseYear(m[3], s._today), 33 | month: parseMonth(m[2]), 34 | date: toCardinal(m[1]) 35 | } 36 | if (!obj.month || validate(obj) === false) { 37 | s.epoch = null 38 | return s 39 | } 40 | walkTo(s, obj) 41 | s = parseTime(s, m[4]) 42 | return s 43 | } 44 | }, 45 | // 01-jan-2020 46 | { 47 | reg: /^([0-9]{1,2})[. \-/]([a-z]+)[. \-/]([0-9]{4})?( [0-9]{1,2}(:[0-9]{0,2})?(:[0-9]{0,3})? ?(am|pm)?)?$/i, 48 | parse: (s, m) => { 49 | const obj = { 50 | date: Number(m[1]), 51 | month: parseMonth(m[2]), 52 | year: Number(m[3]) 53 | } 54 | if (validate(obj) === false) { 55 | s.epoch = null 56 | return s 57 | } 58 | walkTo(s, obj) 59 | s = s.startOf('day') 60 | s = parseTime(s, m[4]) 61 | return s 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /src/input/formats/_parsers.js: -------------------------------------------------------------------------------- 1 | import monthLengths from '../../data/monthLengths.js' 2 | import { isLeapYear } from '../../fns.js' 3 | import { mapping } from '../../data/months.js' 4 | const months = mapping() 5 | 6 | import parseOffset from './parseOffset.js' 7 | import parseTime from './parseTime.js' 8 | 9 | //given a month, return whether day number exists in it 10 | const validate = (obj) => { 11 | //invalid values 12 | if (monthLengths.hasOwnProperty(obj.month) !== true) { 13 | return false 14 | } 15 | //support leap-year in february 16 | if (obj.month === 1) { 17 | if (isLeapYear(obj.year) && obj.date <= 29) { 18 | return true 19 | } else { 20 | return obj.date <= 28 21 | } 22 | } 23 | //is this date too-big for this month? 24 | const max = monthLengths[obj.month] || 0 25 | if (obj.date <= max) { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | const parseYear = (str = '', today) => { 32 | str = str.trim() 33 | // parse '86 shorthand 34 | if (/^'[0-9][0-9]$/.test(str) === true) { 35 | const num = Number(str.replace(/'/, '')) 36 | if (num > 50) { 37 | return 1900 + num 38 | } 39 | return 2000 + num 40 | } 41 | let year = parseInt(str, 10) 42 | // use a given year from options.today 43 | if (!year && today) { 44 | year = today.year 45 | } 46 | // fallback to this year 47 | year = year || new Date().getFullYear() 48 | return year 49 | } 50 | 51 | const parseMonth = function (str) { 52 | str = str.toLowerCase().trim() 53 | if (str === 'sept') { 54 | return months.sep 55 | } 56 | return months[str] 57 | } 58 | 59 | const parseTz = function (str) { 60 | str = str.trim() 61 | str = str.replace(/[[\]]/g, '') 62 | return str 63 | } 64 | 65 | export { 66 | parseOffset, 67 | parseTime, 68 | parseYear, 69 | parseMonth, 70 | validate, 71 | parseTz 72 | } -------------------------------------------------------------------------------- /src/input/formats/index.js: -------------------------------------------------------------------------------- 1 | import ymd from './01-ymd.js' 2 | import mdy from './02-mdy.js' 3 | import dmy from './03-dmy.js' 4 | import misc from './04-misc.js' 5 | 6 | export default [].concat(ymd, mdy, dmy, misc) 7 | -------------------------------------------------------------------------------- /src/input/formats/parseOffset.js: -------------------------------------------------------------------------------- 1 | //pull-apart ISO offsets, like "+0100" 2 | const parseOffset = (s, offset) => { 3 | if (!offset) { 4 | return s 5 | } 6 | offset = offset.trim().toLowerCase() 7 | // according to ISO8601, tz could be hh:mm, hhmm or hh 8 | // so need few more steps before the calculation. 9 | let num = 0 10 | 11 | // for (+-)hh:mm 12 | if (/^[+-]?[0-9]{2}:[0-9]{2}$/.test(offset)) { 13 | //support "+01:00" 14 | if (/:00/.test(offset) === true) { 15 | offset = offset.replace(/:00/, '') 16 | } 17 | //support "+01:30" 18 | if (/:30/.test(offset) === true) { 19 | offset = offset.replace(/:30/, '.5') 20 | } 21 | } 22 | 23 | // for (+-)hhmm 24 | if (/^[+-]?[0-9]{4}$/.test(offset)) { 25 | offset = offset.replace(/30$/, '.5') 26 | } 27 | num = parseFloat(offset) 28 | 29 | //divide by 100 or 10 - , "+0100", "+01" 30 | if (Math.abs(num) > 100) { 31 | num = num / 100 32 | } 33 | //this is a fancy-move 34 | if (num === 0 || offset === 'Z' || offset === 'z') { 35 | s.tz = 'etc/gmt' 36 | return s 37 | } 38 | //okay, try to match it to a utc timezone 39 | //remember - this is opposite! a -5 offset maps to Etc/GMT+5 ¯\_(:/)_/¯ 40 | //https://askubuntu.com/questions/519550/why-is-the-8-timezone-called-gmt-8-in-the-filesystem 41 | num *= -1 42 | 43 | if (num >= 0) { 44 | num = '+' + num 45 | } 46 | const tz = 'etc/gmt' + num 47 | const zones = s.timezones 48 | 49 | if (zones[tz]) { 50 | // log a warning if we're over-writing a given timezone? 51 | // console.log('changing timezone to: ' + tz) 52 | s.tz = tz 53 | } 54 | return s 55 | } 56 | export default parseOffset 57 | -------------------------------------------------------------------------------- /src/input/formats/parseTime.js: -------------------------------------------------------------------------------- 1 | // truncate any sub-millisecond values 2 | const parseMs = function (str = '') { 3 | str = String(str) 4 | //js does not support sub-millisecond values 5 | // so truncate these - 2021-11-02T19:55:30.087772 6 | if (str.length > 3) { 7 | str = str.substring(0, 3) 8 | } else if (str.length === 1) { 9 | // assume ms are zero-padded on the left 10 | // but maybe not on the right. 11 | // turn '.10' into '.100' 12 | str = str + '00' 13 | } else if (str.length === 2) { 14 | str = str + '0' 15 | } 16 | return Number(str) || 0 17 | } 18 | 19 | const parseTime = (s, str = '') => { 20 | // remove all whitespace 21 | str = str.replace(/^\s+/, '').toLowerCase() 22 | //formal time format - 04:30.23 23 | let arr = str.match(/([0-9]{1,2}):([0-9]{1,2}):?([0-9]{1,2})?[:.]?([0-9]{1,4})?/) 24 | if (arr !== null) { 25 | // eslint-disable-next-line prefer-const 26 | let [, h, m, sec, ms] = arr 27 | //validate it a little 28 | h = Number(h) 29 | if (h < 0 || h > 24) { 30 | return s.startOf('day') 31 | } 32 | m = Number(m) //don't accept '5:3pm' 33 | if (arr[2].length < 2 || m < 0 || m > 59) { 34 | return s.startOf('day') 35 | } 36 | s = s.hour(h) 37 | s = s.minute(m) 38 | s = s.seconds(sec || 0) 39 | s = s.millisecond(parseMs(ms)) 40 | //parse-out am/pm 41 | const ampm = str.match(/[0-9] ?(am|pm)\b/) 42 | if (ampm !== null && ampm[1]) { 43 | s = s.ampm(ampm[1]) 44 | } 45 | return s 46 | } 47 | 48 | //try an informal form - 5pm (no minutes) 49 | arr = str.match(/([0-9]+) ?(am|pm)/) 50 | if (arr !== null && arr[1]) { 51 | const h = Number(arr[1]) 52 | //validate it a little.. 53 | if (h > 12 || h < 1) { 54 | return s.startOf('day') 55 | } 56 | s = s.hour(arr[1] || 0) 57 | s = s.ampm(arr[2]) 58 | s = s.startOf('hour') 59 | return s 60 | } 61 | 62 | //no time info found, use start-of-day 63 | s = s.startOf('day') 64 | return s 65 | } 66 | export default parseTime 67 | -------------------------------------------------------------------------------- /src/input/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const defaults = { 3 | year: new Date().getFullYear(), 4 | month: 0, 5 | date: 1 6 | } 7 | const units = ['year', 'month', 'date', 'hour', 'minute', 'second', 'millisecond'] 8 | 9 | //support [2016, 03, 01] format 10 | const parseArray = (s, arr, today) => { 11 | if (arr.length === 0) { 12 | return s 13 | } 14 | for (let i = 0; i < units.length; i++) { 15 | const num = arr[i] || today[units[i]] || defaults[units[i]] || 0 16 | s = s[units[i]](num) 17 | } 18 | return s 19 | } 20 | 21 | //support {year:2016, month:3} format 22 | const parseObject = (s, obj) => { 23 | if (Object.keys(obj).length === 0) { 24 | return s 25 | } 26 | obj = Object.assign({}, defaults, obj) 27 | if (obj.timezone) { 28 | s.tz = obj.timezone 29 | } 30 | for (let i = 0; i < units.length; i++) { 31 | const unit = units[i] 32 | if (obj[unit] !== undefined) { 33 | s = s[unit](obj[unit]) 34 | } 35 | } 36 | return s 37 | } 38 | 39 | // this may seem like an arbitrary number, but it's 'within jan 1970' 40 | // this is only really ambiguous until 2054 or so 41 | const parseNumber = function (s, input) { 42 | const minimumEpoch = 2500000000 43 | // if the given epoch is really small, they've probably given seconds and not milliseconds 44 | // anything below this number is likely (but not necessarily) a mistaken input. 45 | if (input > 0 && input < minimumEpoch && s.silent === false) { 46 | console.warn(' - Warning: You are setting the date to January 1970.') 47 | console.warn(' - did input seconds instead of milliseconds?') 48 | } 49 | s.epoch = input 50 | return s 51 | } 52 | 53 | export default { 54 | parseArray, 55 | parseObject, 56 | parseNumber 57 | } 58 | -------------------------------------------------------------------------------- /src/input/named-dates.js: -------------------------------------------------------------------------------- 1 | // pull in 'today' data for the baseline moment 2 | const getNow = function (s) { 3 | s.epoch = Date.now() 4 | Object.keys(s._today || {}).forEach((k) => { 5 | if (typeof s[k] === 'function') { 6 | s = s[k](s._today[k]) 7 | } 8 | }) 9 | return s 10 | } 11 | 12 | const dates = { 13 | now: (s) => { 14 | return getNow(s) 15 | }, 16 | today: (s) => { 17 | return getNow(s) 18 | }, 19 | tonight: (s) => { 20 | s = getNow(s) 21 | s = s.hour(18) //6pm 22 | return s 23 | }, 24 | tomorrow: (s) => { 25 | s = getNow(s) 26 | s = s.add(1, 'day') 27 | s = s.startOf('day') 28 | return s 29 | }, 30 | yesterday: (s) => { 31 | s = getNow(s) 32 | s = s.subtract(1, 'day') 33 | s = s.startOf('day') 34 | return s 35 | }, 36 | christmas: (s) => { 37 | const year = getNow(s).year() 38 | s = s.set([year, 11, 25, 18, 0, 0]) // Dec 25 39 | return s 40 | }, 41 | 'new years': (s) => { 42 | const year = getNow(s).year() 43 | s = s.set([year, 11, 31, 18, 0, 0]) // Dec 31 44 | return s 45 | } 46 | } 47 | dates['new years eve'] = dates['new years'] 48 | export default dates 49 | -------------------------------------------------------------------------------- /src/input/normalize.js: -------------------------------------------------------------------------------- 1 | //little cleanup.. 2 | const normalize = function (str) { 3 | // remove all day-names 4 | str = str.replace(/\b(mon|tues?|wed|wednes|thur?s?|fri|sat|satur|sun)(day)?\b/i, '') 5 | //remove ordinal ending 6 | str = str.replace(/([0-9])(th|rd|st|nd)/, '$1') 7 | str = str.replace(/,/g, '') 8 | str = str.replace(/ +/g, ' ').trim() 9 | return str 10 | } 11 | 12 | export default normalize 13 | -------------------------------------------------------------------------------- /src/input/parse.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import parsers from './formats/index.js' 3 | 4 | const parseString = function (s, input, givenTz) { 5 | // let parsers = s.parsers || [] 6 | //try each text-parse template, use the first good result 7 | for (let i = 0; i < parsers.length; i++) { 8 | const m = input.match(parsers[i].reg) 9 | if (m) { 10 | const res = parsers[i].parse(s, m, givenTz) 11 | if (res !== null && res.isValid()) { 12 | return res 13 | } 14 | } 15 | } 16 | if (s.silent === false) { 17 | console.warn("Warning: couldn't parse date-string: '" + input + "'") 18 | } 19 | s.epoch = null 20 | return s 21 | } 22 | export default parseString 23 | -------------------------------------------------------------------------------- /src/methods/compare.js: -------------------------------------------------------------------------------- 1 | import { beADate, getEpoch } from '../fns.js' 2 | 3 | const addMethods = SpaceTime => { 4 | const methods = { 5 | isAfter: function (d) { 6 | d = beADate(d, this) 7 | const epoch = getEpoch(d) 8 | if (epoch === null) { 9 | return null 10 | } 11 | return this.epoch > epoch 12 | }, 13 | isBefore: function (d) { 14 | d = beADate(d, this) 15 | const epoch = getEpoch(d) 16 | if (epoch === null) { 17 | return null 18 | } 19 | return this.epoch < epoch 20 | }, 21 | isEqual: function (d) { 22 | d = beADate(d, this) 23 | const epoch = getEpoch(d) 24 | if (epoch === null) { 25 | return null 26 | } 27 | return this.epoch === epoch 28 | }, 29 | isBetween: function (start, end, isInclusive = false) { 30 | start = beADate(start, this) 31 | end = beADate(end, this) 32 | const startEpoch = getEpoch(start) 33 | if (startEpoch === null) { 34 | return null 35 | } 36 | const endEpoch = getEpoch(end) 37 | if (endEpoch === null) { 38 | return null 39 | } 40 | if (isInclusive) { 41 | return this.isBetween(start, end) || this.isEqual(start) || this.isEqual(end); 42 | } 43 | return startEpoch < this.epoch && this.epoch < endEpoch 44 | } 45 | } 46 | 47 | //hook them into proto 48 | Object.keys(methods).forEach(k => { 49 | SpaceTime.prototype[k] = methods[k] 50 | }) 51 | } 52 | 53 | export default addMethods 54 | -------------------------------------------------------------------------------- /src/methods/diff/index.js: -------------------------------------------------------------------------------- 1 | import { beADate, normalize } from '../../fns.js' 2 | import waterfall from './waterfall.js' 3 | 4 | const reverseDiff = function (obj) { 5 | Object.keys(obj).forEach((k) => { 6 | obj[k] *= -1 7 | }) 8 | return obj 9 | } 10 | 11 | // this method counts a total # of each unit, between a, b. 12 | // '1 month' means 28 days in february 13 | // '1 year' means 366 days in a leap year 14 | const main = function (a, b, unit) { 15 | b = beADate(b, a) 16 | //reverse values, if necessary 17 | let reversed = false 18 | if (a.isAfter(b)) { 19 | const tmp = a 20 | a = b 21 | b = tmp 22 | reversed = true 23 | } 24 | //compute them all (i know!) 25 | let obj = waterfall(a, b) 26 | if (reversed) { 27 | obj = reverseDiff(obj) 28 | } 29 | //return just the requested unit 30 | if (unit) { 31 | //make sure it's plural-form 32 | unit = normalize(unit) 33 | if (/s$/.test(unit) !== true) { 34 | unit += 's' 35 | } 36 | if (unit === 'dates') { 37 | unit = 'days' 38 | } 39 | return obj[unit] 40 | } 41 | return obj 42 | } 43 | 44 | export default main 45 | -------------------------------------------------------------------------------- /src/methods/diff/one.js: -------------------------------------------------------------------------------- 1 | //increment until dates are the same 2 | const climb = (a, b, unit) => { 3 | let i = 0 4 | a = a.clone() 5 | while (a.isBefore(b)) { 6 | //do proper, expensive increment to catch all-the-tricks 7 | a = a.add(1, unit) 8 | i += 1 9 | } 10 | //oops, we went too-far.. 11 | if (a.isAfter(b, unit)) { 12 | i -= 1 13 | } 14 | return i 15 | } 16 | 17 | // do a thurough +=1 on the unit, until they match 18 | // for speed-reasons, only used on day, month, week. 19 | const diffOne = (a, b, unit) => { 20 | if (a.isBefore(b)) { 21 | return climb(a, b, unit) 22 | } else { 23 | return climb(b, a, unit) * -1 //reverse it 24 | } 25 | } 26 | 27 | export default diffOne 28 | -------------------------------------------------------------------------------- /src/methods/diff/waterfall.js: -------------------------------------------------------------------------------- 1 | import diffOne from './one.js' 2 | 3 | // don't do anything too fancy here. 4 | // 2020 - 2019 may be 1 year, or 0 years 5 | // - '1 year difference' means 366 days during a leap year 6 | const fastYear = (a, b) => { 7 | let years = b.year() - a.year() 8 | // should we decrement it by 1? 9 | a = a.year(b.year()) 10 | if (a.isAfter(b)) { 11 | years -= 1 12 | } 13 | return years 14 | } 15 | 16 | // use a waterfall-method for computing a diff of any 'pre-knowable' units 17 | // compute years, then compute months, etc.. 18 | // ... then ms-math for any very-small units 19 | const diff = function (a, b) { 20 | // an hour is always the same # of milliseconds 21 | // so these units can be 'pre-calculated' 22 | const msDiff = b.epoch - a.epoch 23 | const obj = { 24 | milliseconds: msDiff, 25 | seconds: parseInt(msDiff / 1000, 10) 26 | } 27 | obj.minutes = parseInt(obj.seconds / 60, 10) 28 | obj.hours = parseInt(obj.minutes / 60, 10) 29 | 30 | //do the year 31 | let tmp = a.clone() 32 | obj.years = fastYear(tmp, b) 33 | tmp = a.add(obj.years, 'year') 34 | 35 | //there's always 12 months in a year... 36 | obj.months = obj.years * 12 37 | tmp = a.add(obj.months, 'month') 38 | obj.months += diffOne(tmp, b, 'month') 39 | 40 | // there's always 4 quarters in a year... 41 | obj.quarters = obj.years * 4 42 | obj.quarters += parseInt((obj.months % 12) / 3, 10) 43 | 44 | // there's always atleast 52 weeks in a year.. 45 | // (month * 4) isn't as close 46 | obj.weeks = obj.years * 52 47 | tmp = a.add(obj.weeks, 'week') 48 | obj.weeks += diffOne(tmp, b, 'week') 49 | 50 | // there's always atleast 7 days in a week 51 | obj.days = obj.weeks * 7 52 | tmp = a.add(obj.days, 'day') 53 | obj.days += diffOne(tmp, b, 'day') 54 | 55 | return obj 56 | } 57 | export default diff 58 | -------------------------------------------------------------------------------- /src/methods/every.js: -------------------------------------------------------------------------------- 1 | import { normalize } from '../fns.js' 2 | import { short, long } from '../data/days.js' 3 | 4 | //is it 'wednesday'? 5 | const isDay = function (unit) { 6 | if (short().find((s) => s === unit)) { 7 | return true 8 | } 9 | if (long().find((s) => s === unit)) { 10 | return true 11 | } 12 | return false 13 | } 14 | 15 | // return a list of the weeks/months/days between a -> b 16 | // returns spacetime objects in the timezone of the input 17 | const every = function (start, unit, end, stepCount = 1) { 18 | if (!unit || !end) { 19 | return [] 20 | } 21 | //cleanup unit param 22 | unit = normalize(unit) 23 | //cleanup to param 24 | end = start.clone().set(end) 25 | //swap them, if they're backwards 26 | if (start.isAfter(end)) { 27 | const tmp = start 28 | start = end 29 | end = tmp 30 | } 31 | //prevent going beyond end if unit/stepCount > than the range 32 | if (start.diff(end, unit) < stepCount) { 33 | return [] 34 | } 35 | //support 'every wednesday' 36 | let d = start.clone() 37 | if (isDay(unit)) { 38 | d = d.next(unit) 39 | unit = 'week' 40 | } else { 41 | const first = d.startOf(unit) 42 | if (first.isBefore(start)) { 43 | d = d.next(unit) 44 | } 45 | } 46 | //okay, actually start doing it 47 | const result = [] 48 | while (d.isBefore(end)) { 49 | result.push(d) 50 | d = d.add(stepCount, unit) 51 | } 52 | return result 53 | } 54 | export default every 55 | -------------------------------------------------------------------------------- /src/methods/format/_offset.js: -------------------------------------------------------------------------------- 1 | import { formatTimezone } from '../../fns.js' 2 | 3 | // create the timezone offset part of an iso timestamp 4 | // it's kind of nuts how involved this is 5 | // "+01:00", "+0100", or simply "+01" 6 | const isoOffset = s => { 7 | const offset = s.timezone().current.offset 8 | return !offset ? 'Z' : formatTimezone(offset, ':') 9 | } 10 | 11 | export default isoOffset 12 | -------------------------------------------------------------------------------- /src/methods/i18n.js: -------------------------------------------------------------------------------- 1 | import { isObject, isBoolean } from '../fns.js' 2 | import { set as setD } from '../data/days.js' 3 | import { set as setM } from '../data/months.js' 4 | import { set as setTcf } from '../data/caseFormat.js' 5 | import { set as setAmpm } from '../data/ampm.js' 6 | import { set as setDistance } from '../data/distance.js' 7 | import { set as setUnits } from '../data/units.js' 8 | 9 | const addMethods = SpaceTime => { 10 | const methods = { 11 | i18n: function (data) { 12 | //change the day names 13 | if (isObject(data.days)) { 14 | setD(data.days) 15 | } 16 | //change the month names 17 | if (isObject(data.months)) { 18 | setM(data.months) 19 | } 20 | 21 | //change the display style of the month / day names 22 | if (isBoolean(data.useTitleCase)) { 23 | setTcf(data.useTitleCase) 24 | } 25 | 26 | //change am and pm strings 27 | if (isObject(data.ampm)) { 28 | setAmpm(data.ampm) 29 | } 30 | 31 | //change distance strings 32 | if(isObject(data.distance)){ 33 | setDistance(data.distance) 34 | } 35 | 36 | //change units strings 37 | if(isObject(data.units)){ 38 | setUnits(data.units) 39 | } 40 | 41 | return this 42 | } 43 | } 44 | 45 | //hook them into proto 46 | Object.keys(methods).forEach(k => { 47 | SpaceTime.prototype[k] = methods[k] 48 | }) 49 | } 50 | 51 | export default addMethods 52 | -------------------------------------------------------------------------------- /src/methods/nearest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { normalize } from '../fns.js' 3 | 4 | //round to either current, or +1 of this unit 5 | const nearest = (s, unit) => { 6 | //how far have we gone? 7 | const prog = s.progress() 8 | unit = normalize(unit) 9 | //fix camel-case for this one 10 | if (unit === 'quarterhour') { 11 | unit = 'quarterHour' 12 | } 13 | if (prog[unit] !== undefined) { 14 | // go forward one? 15 | if (prog[unit] > 0.5) { 16 | s = s.add(1, unit) 17 | } 18 | // go to start 19 | s = s.startOf(unit) 20 | } else if (s.silent === false) { 21 | console.warn("no known unit '" + unit + "'") 22 | } 23 | return s 24 | } 25 | export default nearest 26 | -------------------------------------------------------------------------------- /src/methods/progress.js: -------------------------------------------------------------------------------- 1 | import { normalize } from '../fns.js' 2 | const units = ['year', 'season', 'quarter', 'month', 'week', 'day', 'quarterHour', 'hour', 'minute'] 3 | 4 | const doUnit = function (s, k) { 5 | const start = s.clone().startOf(k) 6 | const end = s.clone().endOf(k) 7 | const duration = end.epoch - start.epoch 8 | const percent = (s.epoch - start.epoch) / duration 9 | return parseFloat(percent.toFixed(2)) 10 | } 11 | 12 | //how far it is along, from 0-1 13 | const progress = (s, unit) => { 14 | if (unit) { 15 | unit = normalize(unit) 16 | return doUnit(s, unit) 17 | } 18 | const obj = {} 19 | units.forEach(k => { 20 | obj[k] = doUnit(s, k) 21 | }) 22 | return obj 23 | } 24 | 25 | export default progress 26 | -------------------------------------------------------------------------------- /src/methods/query/02-date.js: -------------------------------------------------------------------------------- 1 | import { date as _date } from '../set/set.js' 2 | import { aliases, short, long } from '../../data/days.js' 3 | import walkTo from '../set/walk.js' 4 | 5 | const methods = { 6 | // # day in the month 7 | date: function (num, goFwd) { 8 | if (num !== undefined) { 9 | const s = this.clone() 10 | num = parseInt(num, 10) 11 | if (num) { 12 | s.epoch = _date(s, num, goFwd) 13 | } 14 | return s 15 | } 16 | return this.d.getDate() 17 | }, 18 | 19 | //like 'wednesday' (hard!) 20 | day: function (input, goFwd) { 21 | if (input === undefined) { 22 | return this.d.getDay() 23 | } 24 | const original = this.clone() 25 | let want = input 26 | // accept 'wednesday' 27 | if (typeof input === 'string') { 28 | input = input.toLowerCase() 29 | if (aliases.hasOwnProperty(input)) { 30 | want = aliases[input] 31 | } else { 32 | want = short().indexOf(input) 33 | if (want === -1) { 34 | want = long().indexOf(input) 35 | } 36 | } 37 | } 38 | //move approx 39 | const day = this.d.getDay() 40 | let diff = day - want 41 | if (goFwd === true && diff > 0) { 42 | diff = diff - 7 43 | } 44 | if (goFwd === false && diff < 0) { 45 | diff = diff + 7 46 | } 47 | const s = this.subtract(diff, 'days') 48 | //tighten it back up 49 | walkTo(s, { 50 | hour: original.hour(), 51 | minute: original.minute(), 52 | second: original.second() 53 | }) 54 | return s 55 | }, 56 | 57 | //these are helpful name-wrappers 58 | dayName: function (input, goFwd) { 59 | if (input === undefined) { 60 | return long()[this.day()] 61 | } 62 | let s = this.clone() 63 | s = s.day(input, goFwd) 64 | return s 65 | } 66 | } 67 | export default methods 68 | -------------------------------------------------------------------------------- /src/methods/query/index.js: -------------------------------------------------------------------------------- 1 | import timeFns from './01-time.js' 2 | import dateFns from './02-date.js' 3 | import yearFns from './03-year.js' 4 | 5 | const methods = Object.assign({}, timeFns, dateFns, yearFns) 6 | 7 | //aliases 8 | methods.milliseconds = methods.millisecond 9 | methods.seconds = methods.second 10 | methods.minutes = methods.minute 11 | methods.hours = methods.hour 12 | methods.hour24 = methods.hour 13 | methods.h12 = methods.hour12 14 | methods.h24 = methods.hour24 15 | methods.days = methods.day 16 | 17 | const addMethods = Space => { 18 | //hook the methods into prototype 19 | Object.keys(methods).forEach(k => { 20 | Space.prototype[k] = methods[k] 21 | }) 22 | } 23 | 24 | export default addMethods 25 | -------------------------------------------------------------------------------- /src/methods/same.js: -------------------------------------------------------------------------------- 1 | //make a string, for easy comparison between dates 2 | const print = { 3 | millisecond: (s) => { 4 | return s.epoch 5 | }, 6 | second: (s) => { 7 | return [s.year(), s.month(), s.date(), s.hour(), s.minute(), s.second()].join('-') 8 | }, 9 | minute: (s) => { 10 | return [s.year(), s.month(), s.date(), s.hour(), s.minute()].join('-') 11 | }, 12 | hour: (s) => { 13 | return [s.year(), s.month(), s.date(), s.hour()].join('-') 14 | }, 15 | day: (s) => { 16 | return [s.year(), s.month(), s.date()].join('-') 17 | }, 18 | week: (s) => { 19 | return [s.year(), s.week()].join('-') 20 | }, 21 | month: (s) => { 22 | return [s.year(), s.month()].join('-') 23 | }, 24 | quarter: (s) => { 25 | return [s.year(), s.quarter()].join('-') 26 | }, 27 | year: (s) => { 28 | return s.year() 29 | } 30 | } 31 | print.date = print.day 32 | 33 | const addMethods = (SpaceTime) => { 34 | SpaceTime.prototype.isSame = function (b, unit, tzAware = true) { 35 | const a = this 36 | if (!unit) { 37 | return null 38 | } 39 | // support swapped params 40 | if (typeof b === 'string' && typeof unit === 'object') { 41 | const tmp = b 42 | b = unit 43 | unit = tmp 44 | } 45 | if (typeof b === 'string' || typeof b === 'number') { 46 | b = new SpaceTime(b, this.timezone.name) 47 | } 48 | //support 'seconds' aswell as 'second' 49 | unit = unit.replace(/s$/, '') 50 | 51 | // make them the same timezone for proper comparison 52 | if (tzAware === true && a.tz !== b.tz) { 53 | b = b.clone() 54 | b.tz = a.tz 55 | } 56 | if (print[unit]) { 57 | return print[unit](a) === print[unit](b) 58 | } 59 | return null 60 | } 61 | } 62 | 63 | export default addMethods 64 | -------------------------------------------------------------------------------- /src/methods/set/_model.js: -------------------------------------------------------------------------------- 1 | import monthLength from '../../data/monthLengths.js' 2 | import { isLeapYear } from '../../fns.js' 3 | 4 | const getMonthLength = function (month, year) { 5 | if (month === 1 && isLeapYear(year)) { 6 | return 29 7 | } 8 | return monthLength[month] 9 | } 10 | 11 | //month is the one thing we 'model/compute' 12 | //- because ms-shifting can be off by enough 13 | const rollMonth = (want, old) => { 14 | //increment year 15 | if (want.month > 0) { 16 | const years = parseInt(want.month / 12, 10) 17 | want.year = old.year() + years 18 | want.month = want.month % 12 19 | } else if (want.month < 0) { 20 | const m = Math.abs(want.month) 21 | let years = parseInt(m / 12, 10) 22 | if (m % 12 !== 0) { 23 | years += 1 24 | } 25 | want.year = old.year() - years 26 | //ignore extras 27 | want.month = want.month % 12 28 | want.month = want.month + 12 29 | if (want.month === 12) { 30 | want.month = 0 31 | } 32 | } 33 | return want 34 | } 35 | 36 | // briefly support day=-2 (this does not need to be perfect.) 37 | const rollDaysDown = (want, old, sum) => { 38 | want.year = old.year() 39 | want.month = old.month() 40 | const date = old.date() 41 | want.date = date - Math.abs(sum) 42 | while (want.date < 1) { 43 | want.month -= 1 44 | if (want.month < 0) { 45 | want.month = 11 46 | want.year -= 1 47 | } 48 | const max = getMonthLength(want.month, want.year) 49 | want.date += max 50 | } 51 | return want 52 | } 53 | 54 | // briefly support day=33 (this does not need to be perfect.) 55 | const rollDaysUp = (want, old, sum) => { 56 | let year = old.year() 57 | let month = old.month() 58 | let max = getMonthLength(month, year) 59 | while (sum > max) { 60 | sum -= max 61 | month += 1 62 | if (month >= 12) { 63 | month -= 12 64 | year += 1 65 | } 66 | max = getMonthLength(month, year) 67 | } 68 | want.month = month 69 | want.date = sum 70 | return want 71 | } 72 | 73 | export const months = rollMonth 74 | export const days = rollDaysUp 75 | export const daysBack = rollDaysDown 76 | -------------------------------------------------------------------------------- /src/methods/since/_iso.js: -------------------------------------------------------------------------------- 1 | /* 2 | ISO 8601 duration format 3 | // https://en.wikipedia.org/wiki/ISO_8601#Durations 4 | "P3Y6M4DT12H30M5S" 5 | P the start of the duration representation. 6 | Y the number of years. 7 | M the number of months. 8 | W the number of weeks. 9 | D the number of days. 10 | T of the representation. 11 | H the number of hours. 12 | M the number of minutes. 13 | S the number of seconds. 14 | */ 15 | 16 | const fmt = (n) => Math.abs(n) || 0 17 | 18 | const toISO = function (diff) { 19 | let iso = 'P' 20 | iso += fmt(diff.years) + 'Y' 21 | iso += fmt(diff.months) + 'M' 22 | iso += fmt(diff.days) + 'DT' 23 | iso += fmt(diff.hours) + 'H' 24 | iso += fmt(diff.minutes) + 'M' 25 | iso += fmt(diff.seconds) + 'S' 26 | return iso 27 | } 28 | export default toISO -------------------------------------------------------------------------------- /src/methods/since/getDiff.js: -------------------------------------------------------------------------------- 1 | 2 | //get number of hours/minutes... between the two dates 3 | function getDiff(a, b) { 4 | const isBefore = a.isBefore(b) 5 | const later = isBefore ? b : a 6 | let earlier = isBefore ? a : b 7 | earlier = earlier.clone() 8 | const diff = { 9 | years: 0, 10 | months: 0, 11 | days: 0, 12 | hours: 0, 13 | minutes: 0, 14 | seconds: 0 15 | } 16 | Object.keys(diff).forEach((unit) => { 17 | if (earlier.isSame(later, unit)) { 18 | return 19 | } 20 | const max = earlier.diff(later, unit) 21 | earlier = earlier.add(max, unit) 22 | diff[unit] = max 23 | }) 24 | //reverse it, if necessary 25 | if (isBefore) { 26 | Object.keys(diff).forEach((u) => { 27 | if (diff[u] !== 0) { 28 | diff[u] *= -1 29 | } 30 | }) 31 | } 32 | return diff 33 | } 34 | export default getDiff -------------------------------------------------------------------------------- /src/methods/since/index.js: -------------------------------------------------------------------------------- 1 | import { beADate } from '../../fns.js' 2 | import toISO from './_iso.js' 3 | import getDiff from './getDiff.js' 4 | import soften from './soften.js' 5 | import { 6 | pastString, 7 | futureString, 8 | nowString, 9 | presentString, 10 | pastDistanceString, 11 | futureDistanceString 12 | } from "../../data/distance.js"; 13 | //by spencermountain + Shaun Grady 14 | 15 | //create the human-readable diff between the two dates 16 | const since = (start, end) => { 17 | end = beADate(end, start) 18 | const diff = getDiff(start, end) 19 | const isNow = Object.keys(diff).every((u) => !diff[u]) 20 | if (isNow === true) { 21 | return { 22 | diff, 23 | rounded: nowString(), 24 | qualified: nowString(), 25 | precise: nowString(), 26 | abbreviated: [], 27 | iso: 'P0Y0M0DT0H0M0S', 28 | direction: presentString(), 29 | } 30 | } 31 | let precise 32 | let direction = futureString() 33 | 34 | let { rounded, qualified } = soften(diff) 35 | const { englishValues, abbreviated } = soften(diff) 36 | 37 | //make them into a string 38 | precise = englishValues.splice(0, 2).join(', ') 39 | //handle before/after logic 40 | if (start.isAfter(end) === true) { 41 | rounded = pastDistanceString(rounded) 42 | qualified = pastDistanceString(qualified) 43 | precise = pastDistanceString(precise) 44 | direction = pastString() 45 | } else { 46 | rounded = futureDistanceString(rounded) 47 | qualified = futureDistanceString(qualified) 48 | precise = futureDistanceString(precise) 49 | } 50 | // https://en.wikipedia.org/wiki/ISO_8601#Durations 51 | // P[n]Y[n]M[n]DT[n]H[n]M[n]S 52 | const iso = toISO(diff) 53 | return { 54 | diff, 55 | rounded, 56 | qualified, 57 | precise, 58 | abbreviated, 59 | iso, 60 | direction, 61 | } 62 | } 63 | 64 | export default since 65 | -------------------------------------------------------------------------------- /src/methods/since/soften.js: -------------------------------------------------------------------------------- 1 | //our conceptual 'break-points' for each unit 2 | import { unitsString } from "../../data/units.js"; 3 | import { almostString, overString } from "../../data/distance.js"; 4 | 5 | const qualifiers = { 6 | months: { 7 | almost: 10, 8 | over: 4 9 | }, 10 | days: { 11 | almost: 25, 12 | over: 10 13 | }, 14 | hours: { 15 | almost: 20, 16 | over: 8 17 | }, 18 | minutes: { 19 | almost: 50, 20 | over: 20 21 | }, 22 | seconds: { 23 | almost: 50, 24 | over: 20 25 | } 26 | } 27 | 28 | // Expects a plural unit arg 29 | function pluralize(value, unit) { 30 | if (value === 1) { 31 | return value + ' ' + unitsString(unit.slice(0, -1)) 32 | } 33 | return value + ' ' + unitsString(unit) 34 | } 35 | 36 | const toSoft = function (diff) { 37 | let rounded = null 38 | let qualified = null 39 | const abbreviated = [] 40 | const englishValues = [] 41 | //go through each value and create its text-representation 42 | Object.keys(diff).forEach((unit, i, units) => { 43 | const value = Math.abs(diff[unit]) 44 | if (value === 0) { 45 | return 46 | } 47 | abbreviated.push(value + unit[0]) 48 | const englishValue = pluralize(value, unit) 49 | englishValues.push(englishValue) 50 | if (!rounded) { 51 | rounded = englishValue 52 | qualified = englishValue 53 | if (i > 4) { 54 | return 55 | } 56 | //is it a 'almost' something, etc? 57 | const nextUnit = units[i + 1] 58 | const nextValue = Math.abs(diff[nextUnit]) 59 | if (nextValue > qualifiers[nextUnit].almost) { 60 | rounded = pluralize(value + 1, unit) 61 | qualified = almostString() + ' ' + rounded 62 | } else if (nextValue > qualifiers[nextUnit].over) { 63 | qualified = overString() + ' ' + englishValue 64 | } 65 | } 66 | }) 67 | 68 | return { qualified, rounded, abbreviated, englishValues } 69 | } 70 | export default toSoft 71 | -------------------------------------------------------------------------------- /src/timezone/guessTz.js: -------------------------------------------------------------------------------- 1 | //find the implicit iana code for this machine. 2 | //safely query the Intl object 3 | //based on - https://bitbucket.org/pellepim/jstimezonedetect/src 4 | const fallbackTZ = 'utc' // 5 | 6 | //this Intl object is not supported often, yet 7 | const safeIntl = () => { 8 | if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') { 9 | return null 10 | } 11 | const format = Intl.DateTimeFormat() 12 | if (typeof format === 'undefined' || typeof format.resolvedOptions === 'undefined') { 13 | return null 14 | } 15 | const timezone = format.resolvedOptions().timeZone 16 | if (!timezone) { 17 | return null 18 | } 19 | return timezone.toLowerCase() 20 | } 21 | 22 | const guessTz = () => { 23 | const timezone = safeIntl() 24 | if (timezone === null) { 25 | return fallbackTZ 26 | } 27 | return timezone 28 | } 29 | //do it once per computer 30 | export default guessTz 31 | -------------------------------------------------------------------------------- /src/timezone/parseOffset.js: -------------------------------------------------------------------------------- 1 | const isOffset = /(-?[0-9]+)h(rs)?/i 2 | const isNumber = /(-?[0-9]+)/ 3 | const utcOffset = /utc([\-+]?[0-9]+)/i 4 | const gmtOffset = /gmt([\-+]?[0-9]+)/i 5 | 6 | const toIana = function (num) { 7 | num = Number(num) 8 | if (num >= -13 && num <= 13) { 9 | num = num * -1 //it's opposite! 10 | num = (num > 0 ? '+' : '') + num //add plus sign 11 | return 'etc/gmt' + num 12 | } 13 | return null 14 | } 15 | 16 | const parseOffset = function (tz) { 17 | // '+5hrs' 18 | let m = tz.match(isOffset) 19 | if (m !== null) { 20 | return toIana(m[1]) 21 | } 22 | // 'utc+5' 23 | m = tz.match(utcOffset) 24 | if (m !== null) { 25 | return toIana(m[1]) 26 | } 27 | // 'GMT-5' (not opposite) 28 | m = tz.match(gmtOffset) 29 | if (m !== null) { 30 | const num = Number(m[1]) * -1 31 | return toIana(num) 32 | } 33 | // '+5' 34 | m = tz.match(isNumber) 35 | if (m !== null) { 36 | return toIana(m[1]) 37 | } 38 | return null 39 | } 40 | export default parseOffset 41 | -------------------------------------------------------------------------------- /src/timezone/quick.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import isSummer from './summerTime.js' 3 | 4 | // this method avoids having to do a full dst-calculation on every operation 5 | // it reproduces some things in ./index.js, but speeds up spacetime considerably 6 | const quickOffset = s => { 7 | const zones = s.timezones 8 | const obj = zones[s.tz] 9 | if (obj === undefined) { 10 | console.warn("Warning: couldn't find timezone " + s.tz) 11 | return 0 12 | } 13 | if (obj.dst === undefined) { 14 | return obj.offset 15 | } 16 | 17 | //get our two possible offsets 18 | const jul = obj.offset 19 | let dec = obj.offset + 1 // assume it's the same for now 20 | if (obj.hem === 'n') { 21 | dec = jul - 1 22 | } 23 | const split = obj.dst.split('->') 24 | const inSummer = isSummer(s.epoch, split[0], split[1], jul, dec) 25 | if (inSummer === true) { 26 | return jul 27 | } 28 | return dec 29 | } 30 | export default quickOffset 31 | -------------------------------------------------------------------------------- /src/timezone/summerTime.js: -------------------------------------------------------------------------------- 1 | const MSEC_IN_HOUR = 60 * 60 * 1000 2 | 3 | //convert our local date syntax a javascript UTC date 4 | const toUtc = (dstChange, offset, year) => { 5 | const [month, rest] = dstChange.split('/') 6 | const [day, hour] = rest.split(':') 7 | return Date.UTC(year, month - 1, day, hour) - (offset * MSEC_IN_HOUR) 8 | } 9 | 10 | // compare epoch with dst change events (in utc) 11 | const inSummerTime = (epoch, start, end, summerOffset, winterOffset) => { 12 | const year = new Date(epoch).getUTCFullYear() 13 | const startUtc = toUtc(start, winterOffset, year) 14 | const endUtc = toUtc(end, summerOffset, year) 15 | // simple number comparison now 16 | return epoch >= startUtc && epoch < endUtc 17 | } 18 | 19 | export default inSummerTime 20 | -------------------------------------------------------------------------------- /src/whereIts.js: -------------------------------------------------------------------------------- 1 | import Spacetime from './spacetime.js' 2 | // const timezones = require('../data'); 3 | 4 | const whereIts = (a, b) => { 5 | let start = new Spacetime(null) 6 | let end = new Spacetime(null) 7 | start = start.time(a) 8 | //if b is undefined, use as 'within one hour' 9 | if (b) { 10 | end = end.time(b) 11 | } else { 12 | end = start.add(59, 'minutes') 13 | } 14 | 15 | const startHour = start.hour() 16 | const endHour = end.hour() 17 | const tzs = Object.keys(start.timezones).filter((tz) => { 18 | if (tz.indexOf('/') === -1) { 19 | return false 20 | } 21 | const m = new Spacetime(null, tz) 22 | const hour = m.hour() 23 | //do 'calendar-compare' not real-time-compare 24 | if (hour >= startHour && hour <= endHour) { 25 | //test minutes too, if applicable 26 | if (hour === startHour && m.minute() < start.minute()) { 27 | return false 28 | } 29 | if (hour === endHour && m.minute() > end.minute()) { 30 | return false 31 | } 32 | return true 33 | } 34 | return false 35 | }) 36 | return tzs 37 | } 38 | export default whereIts 39 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | import spacetime from './lib/index.js' 2 | import test from 'tape' 3 | import api from '../api/index.js' 4 | 5 | test('test main methods', (t) => { 6 | Object.keys(api.main).forEach((k) => { 7 | const s = spacetime('1998-03-28') 8 | s[k]() 9 | t.ok(true, k) 10 | }) 11 | t.end() 12 | }) 13 | 14 | test('test getter methods', (t) => { 15 | Object.keys(api.getters).forEach((k) => { 16 | const s = spacetime('1998-03-28') 17 | s[k]() 18 | t.ok(true, k) 19 | }) 20 | t.end() 21 | }) 22 | 23 | test('test util methods', (t) => { 24 | Object.keys(api.utils).forEach((k) => { 25 | //skip these ones 26 | if (k === 'd' || k === 'log' || k === 'i18n' || k === 'weekStart') { 27 | t.ok(true, k) 28 | return 29 | } 30 | const s = spacetime('1998-03-28') 31 | s[k]() 32 | t.ok(true, k) 33 | }) 34 | t.end() 35 | }) 36 | -------------------------------------------------------------------------------- /test/clone.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('clone', (t) => { 5 | let a = spacetime('March 18, 1999 23:42:00', 'Canada/Eastern') 6 | let b = a.clone() 7 | t.equal(a.date(), 18, 'start-date') 8 | t.equal(a.hour(), 23, 'start hour') 9 | t.equal(a.isSame(b, 'hour'), true, 'same-hour') 10 | 11 | a = a.hour(7) 12 | t.equal(a.hour(), 7, 'new-hour') 13 | t.equal(b.hour(), 23, 'old-hour') 14 | 15 | b = b.date(17) 16 | t.equal(b.date(), 17, 'new-date') 17 | t.equal(a.date(), 18, 'old-date') 18 | 19 | t.end() 20 | }) 21 | -------------------------------------------------------------------------------- /test/day.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('set day forward', (t) => { 5 | const days = ['thu', 'fri', 'sat', 'sun', 'mon', 'tue', 'wed'] 6 | const d = spacetime('march 18 2021') //thursday 7 | days.forEach((day, i) => { 8 | const s = d.day(day, true) 9 | const want = d.add(i, 'days') 10 | t.equal(s.format('iso-short'), want.format('iso-short'), day) 11 | }) 12 | t.end() 13 | }) 14 | 15 | test('set day backward', (t) => { 16 | const days = ['tue', 'mon', 'sun', 'sat', 'fri', 'thu', 'wed'] 17 | const d = spacetime('march 23 2021') //tuesday 18 | days.forEach((day, i) => { 19 | const s = d.day(day, false) 20 | const want = d.minus(i, 'days') 21 | t.equal(s.format('iso-short'), want.format('iso-short'), day) 22 | }) 23 | t.end() 24 | }) 25 | -------------------------------------------------------------------------------- /test/dayTime.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('daytime-consistent', (t) => { 5 | let s = spacetime.now() 6 | const times = ['morning', 'afternoon', 'evening', 'night'] 7 | times.forEach((daytime) => { 8 | s = s.dayTime(daytime) 9 | t.equal(s.dayTime(), daytime, daytime + ' is ' + daytime) 10 | }) 11 | t.end() 12 | }) 13 | 14 | test('daytime-sanity-test', (t) => { 15 | let s = spacetime.now() 16 | let time = '2am' 17 | s = s.time(time) 18 | t.equal(s.dayTime(), 'night', time + ' is night') 19 | 20 | time = '7am' 21 | s = s.time(time) 22 | t.equal(s.dayTime(), 'morning', time + ' is morning') 23 | 24 | time = '7:01am' 25 | s = s.time(time) 26 | t.equal(s.dayTime(), 'morning', time + ' is morning') 27 | 28 | time = '11:59am' 29 | s = s.time(time) 30 | t.equal(s.dayTime(), 'morning', time + ' is morning') 31 | 32 | time = '12:00pm' 33 | s = s.time(time) 34 | t.equal(s.dayTime(), 'afternoon', time + ' is afternoon') 35 | 36 | time = '12:01pm' 37 | s = s.time(time) 38 | t.equal(s.dayTime(), 'afternoon', time + ' is afternoon') 39 | 40 | time = '2:47pm' 41 | s = s.time(time) 42 | t.equal(s.dayTime(), 'afternoon', time + ' is afternoon') 43 | 44 | time = '6pm' 45 | s = s.time(time) 46 | t.equal(s.dayTime(), 'evening', time + ' is evening') 47 | 48 | time = '6:02pm' 49 | s = s.time(time) 50 | t.equal(s.dayTime(), 'evening', time + ' is evening') 51 | 52 | time = '9:07pm' 53 | s = s.time(time) 54 | t.equal(s.dayTime(), 'evening', time + ' is evening') 55 | 56 | time = '11pm' 57 | s = s.time(time) 58 | t.equal(s.dayTime(), 'night', time + ' is night') 59 | 60 | time = '12am' 61 | s = s.time(time) 62 | t.equal(s.dayTime(), 'night', time + ' is night') 63 | 64 | time = '12:00am' 65 | s = s.time(time) 66 | t.equal(s.dayTime(), 'night', time + ' is night') 67 | 68 | time = '12:01am' 69 | s = s.time(time) 70 | t.equal(s.dayTime(), 'night', time + ' is night') 71 | 72 | t.end() 73 | }) 74 | -------------------------------------------------------------------------------- /test/daysThisMonth.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('test daysInMonth() on all months', (t) => { 5 | let d = spacetime('January 12, 2016') 6 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Jan. 2016') 7 | 8 | d = spacetime('February 12, 2016') 9 | t.equal(d.daysInMonth(), 29, 'daysInMonth - Feb. 2016 (29 Days, leap year)') 10 | 11 | d = spacetime('March 12, 2016') 12 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Mar. 2016') 13 | 14 | d = spacetime('April 12, 2016') 15 | t.equal(d.daysInMonth(), 30, 'daysInMonth - Apr. 2016') 16 | 17 | d = spacetime('May 12, 2016') 18 | t.equal(d.daysInMonth(), 31, 'daysInMonth - May 2016') 19 | 20 | d = spacetime('June 12, 2016') 21 | t.equal(d.daysInMonth(), 30, 'daysInMonth - Jun. 2016') 22 | 23 | d = spacetime('July 12, 2016') 24 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Jul. 2016') 25 | 26 | d = spacetime('August 12, 2016') 27 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Aug. 2016') 28 | 29 | d = spacetime('September 12, 2016') 30 | t.equal(d.daysInMonth(), 30, 'daysInMonth - Sep. 2016') 31 | 32 | d = spacetime('October 12, 2016') 33 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Oct. 2016') 34 | 35 | d = spacetime('November 12, 2016') 36 | t.equal(d.daysInMonth(), 30, 'daysInMonth - Oct. 2016') 37 | 38 | d = spacetime('December 12, 2016') 39 | t.equal(d.daysInMonth(), 31, 'daysInMonth - Oct. 2016') 40 | 41 | d = spacetime('February 12, 2023') 42 | t.equal(d.daysInMonth(), 28, 'daysInMonth - Feb. 2023 (28 Days, no leap year)') 43 | 44 | t.end() 45 | }) 46 | -------------------------------------------------------------------------------- /test/dst-diff.ignore.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | const useOldTz = require('./lib/useOldTz') 4 | 5 | // 2am is skipped 6 | test('spring-diff', (t) => { 7 | let before = spacetime('2020-03-08T01:00:00', 'America/Chicago') 8 | let after = spacetime('2020-03-08T03:00:00', 'America/Chicago') 9 | let delta = after.since(before).diff 10 | t.equal(delta.hours, 1, '1 hour later') 11 | 12 | before = spacetime('2020-03-08T01:59:00', 'America/Chicago') 13 | after = spacetime('2020-03-08T03:01:00', 'America/Chicago') 14 | delta = after.since(before).diff 15 | t.equal(delta.minutes, 2, '2 min later') 16 | 17 | t.end() 18 | }) 19 | 20 | // there are two 1:00ams 21 | test('fall-diff', (t) => { 22 | let before = spacetime('2020-11-01T01:50:00', 'America/Chicago') 23 | let after = spacetime('2020-11-01T03:10:00', 'America/Chicago') 24 | before = useOldTz(before) 25 | after = useOldTz(after) 26 | const delta = after.since(before).diff 27 | t.equal(delta.minutes, 20, '20 minutes later') 28 | t.end() 29 | }) 30 | -------------------------------------------------------------------------------- /test/epoch.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('fromUnixSeconds', (t) => { 5 | let mils = 1744200453183 6 | let secs = 1744200453 7 | let a = spacetime.fromUnixSeconds(secs) 8 | let b = spacetime(mils) 9 | t.ok(a.isSame('hour', b), 'mils=secs') 10 | 11 | let s = spacetime.fromUnixSeconds(secs, 'Canada/Eastern') 12 | t.equal(s.iso(), '2025-04-09T08:07:33.000-04:00', '8am et'); 13 | s = spacetime.fromUnixSeconds(secs, 'Canada/Pacific') 14 | t.equal(s.iso(), '2025-04-09T05:07:33.000-07:00', '5am pt'); 15 | 16 | // test getter method 17 | t.equal(s.epochSeconds(), secs, 'retrieve seconds') 18 | 19 | // test setter method 20 | let futureSeconds = 1830720600 21 | s = spacetime.now('UTC').epochSeconds(futureSeconds) 22 | t.equal(s.epochSeconds(), futureSeconds, 'set seconds') 23 | t.equal(s.iso(), '2028-01-05T21:30:00.000Z', 'is future seconds') 24 | t.end() 25 | }) 26 | -------------------------------------------------------------------------------- /test/every.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('every-unit', (t) => { 5 | const start = spacetime('April 6th 2019', 'Europe/Paris') 6 | const end = spacetime('April 20th 2019', 'Europe/Paris').add(1, 'hour') 7 | 8 | const days = start.every('day', end) 9 | t.equal(days.length, 15, '15 days') 10 | t.equal(days[0].timezone().name, 'Europe/Paris', 'results in right timezone') 11 | 12 | const weeks = start.every(' weEK ', end) 13 | t.equal(weeks.length, 2, '2 weeks') 14 | 15 | const years = start.every('years', end) 16 | t.equal(years.length, 0, '0 years') 17 | 18 | t.end() 19 | }) 20 | 21 | test('step-count', (t) => { 22 | const start = spacetime('April 6th 2019', 'Europe/Paris') 23 | const end = spacetime('April 20th 2019', 'Europe/Paris').add(3, 'years') 24 | 25 | const biannualInterval = start.every('quarter', end, 2) 26 | t.equal(biannualInterval.length, 6, 'every 2 quarters') 27 | t.equal(biannualInterval[0].timezone().name, 'Europe/Paris', 'results in right timezone') 28 | 29 | const fortnights = start.every('week', end, 2) 30 | t.equal(fortnights.length, 80, 'every fortnight') 31 | t.equal(biannualInterval[0].timezone().name, 'Europe/Paris', 'results in right timezone') 32 | 33 | const everyFourYears = start.every('years', end, 4) 34 | t.equal(everyFourYears.length, 0, 'interval/step count too large for range') 35 | 36 | t.end() 37 | }) 38 | 39 | test('monday-sunday', (t) => { 40 | const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] 41 | const start = spacetime('April 8th 2019').startOf('week') 42 | const end = start.endOf('week') 43 | const eachDay = start.every('day', end).map((d) => d.dayName()) 44 | t.deepEqual(eachDay, days, 'got mon-sunday') 45 | t.end() 46 | }) 47 | 48 | test('long-every is stable', (t) => { 49 | const d = spacetime('jan 1st 1872') 50 | d.every('year', 'jan 1st 1902').forEach((s) => { 51 | const year = s.year() 52 | t.equal(s.month(), 0, year + ' is-january') 53 | t.equal(s.date(), 1, year + ' is-first') 54 | }) 55 | t.end() 56 | }) 57 | -------------------------------------------------------------------------------- /test/findTz.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('whereits', (t) => { 5 | let tzs = spacetime.whereIts('9am') 6 | t.ok(tzs.length > 0, '9am somewhere') 7 | t.ok(tzs.length < 90, '9am-is-subset') 8 | tzs = spacetime.whereIts('10am') 9 | t.ok(tzs.length > 0, '10am somewhere') 10 | t.ok(tzs.length < 90, '10am-is-subset') 11 | tzs = spacetime.whereIts('8pm') 12 | t.ok(tzs.length > 0, '8pm somewhere') 13 | t.ok(tzs.length < 90, '8pm-is-subset') 14 | tzs = spacetime.whereIts('11pm') 15 | t.ok(tzs.length > 0, '11pm somewhere') 16 | t.ok(tzs.length < 90, '11pm-is-subset') 17 | 18 | tzs = spacetime.whereIts('9:00am', '11:00am') 19 | t.ok(tzs.length > 0, '9am-11am somewhere') 20 | t.ok(tzs.length < 120, '9am-11am-is-subset') 21 | 22 | tzs = spacetime.whereIts('9am', '11pm') 23 | t.ok(tzs.length > 0, '9am-11pm somewhere') 24 | t.ok(tzs.length < 503, '9am-11pm-is-subset') 25 | 26 | tzs = spacetime.whereIts('8pm', '11pm') 27 | t.ok(tzs.length > 0, '8pm-11pm somewhere') 28 | t.ok(tzs.length < 503, '8pm-11pm-is-subset') 29 | 30 | tzs = spacetime.whereIts('8pm', '7pm') 31 | t.ok(tzs.length === 0, '8pm-7pm nowhere') 32 | 33 | tzs = spacetime.whereIts('8pm', '7am') 34 | t.ok(tzs.length === 0, '8pm-apm nowhere') 35 | 36 | t.end() 37 | }) 38 | 39 | test('get all timezones method', (t) => { 40 | const obj = spacetime.timezones() 41 | t.ok(Object.keys(obj).length > 60, 'got a lot of timezones') 42 | t.equal( 43 | typeof obj['america/st_vincent'].offset, 44 | 'number', 45 | 'got a list of timeszones with offsets' 46 | ) 47 | t.end() 48 | }) 49 | 50 | test('throw-error-on-invalid', (t) => { 51 | try { 52 | spacetime('12pm', 'invalid-timezone') 53 | t.ok(false, 'did-not-throw-exception') 54 | } catch (e) { 55 | t.ok(true, 'threw-exception-on-input') 56 | } 57 | try { 58 | spacetime.now().goto('canada/nope') 59 | t.ok(false, 'goto-did-not-throw-exception') 60 | } catch (e) { 61 | t.ok(true, 'threw-exception-on-goto') 62 | } 63 | t.end() 64 | }) 65 | -------------------------------------------------------------------------------- /test/informal-tzs.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('informal timezones', (t) => { 5 | const arr = [ 6 | ['Toronto', 'America/Toronto'], 7 | ['toronto', 'America/Toronto'], 8 | ['toronto time', 'America/Toronto'], 9 | ['toronto standard time', 'America/Toronto'], 10 | ['eastern daylight', 'Canada/Eastern'], 11 | ['Jamaica', 'America/Jamaica'], 12 | // ['PST', 'America/Los_Angeles'], 13 | // ['pdt', 'America/Los_Angeles'], 14 | // ['bst', 'Europe/London'], 15 | ['pacific', 'America/Los_Angeles'], 16 | ['pacific standard', 'America/Los_Angeles'], 17 | ['pacific daylight', 'America/Los_Angeles'], 18 | ['GMT+8', '-8h'] 19 | // ['east african', 'eastern africa'] 20 | ] 21 | const date = 'November 11, 1999' 22 | arr.forEach((a) => { 23 | const left = spacetime(date, a[0]) 24 | const right = spacetime(date, a[1]) 25 | t.equal(left.format('nice'), right.format('nice'), a[0]) 26 | }) 27 | t.end() 28 | }) 29 | -------------------------------------------------------------------------------- /test/json.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('json in-out', (t) => { 5 | const arr = [ 6 | '2011-12-03T10:15:30.003+01:00', 7 | '2011-12-03T10:15:30.003Z', 8 | '2020-03-20T22:15:33.645-04:00', 9 | '2022-01-01T00:00:00.000Z', 10 | '2022-12-31T23:59:59.999Z', 11 | '2023-06-15T12:30:00.000-07:00' 12 | ] 13 | arr.forEach(str => { 14 | const a = spacetime(str) 15 | const json = a.json() 16 | const b = spacetime(json) 17 | t.equal(b.format('iso'), str, 'constr json' + str) 18 | const c = spacetime.now().json(json) 19 | t.equal(c.format('iso'), str, 'json input' + str) 20 | }) 21 | t.end() 22 | }) -------------------------------------------------------------------------------- /test/lib/dstParse.js: -------------------------------------------------------------------------------- 1 | //local time of fall dst change-over 2 | const dstParse = (dstChange, num) => { 3 | const fall = dstChange.split('->')[num] 4 | const [month, rest] = fall.split('/') 5 | let [day, hour] = rest.split(':') 6 | if (hour === '24') { 7 | hour = '0' 8 | day = Number(day) + 1 9 | } 10 | if (hour === '00') { 11 | hour = '23' 12 | day = Number(day) - 1 13 | } 14 | return { 15 | year: new Date().getFullYear(), 16 | month: Number(month) - 1, // 17 | date: Number(day), 18 | hour: Number(hour), 19 | minute: 2 20 | } 21 | } 22 | export default dstParse 23 | -------------------------------------------------------------------------------- /test/lib/index.js: -------------------------------------------------------------------------------- 1 | import src from '../../src/index.js' 2 | import build from '../../builds/spacetime.mjs' 3 | let lib = src 4 | //export dev, or compiled lib 5 | if (typeof process !== undefined && typeof module !== undefined) { 6 | if (process.env.TESTENV === 'prod') { 7 | console.log('== production build test 🚀 =='); 8 | lib = build 9 | } 10 | } 11 | 12 | export default lib -------------------------------------------------------------------------------- /test/lib/useOldTz.js: -------------------------------------------------------------------------------- 1 | //use the old dst changes, from 2017, when we made the tests 2 | const changeTz = (s) => { 3 | const timezones = s.timezones 4 | timezones['canada/eastern'].dst = '03/12:03->11/05:01' 5 | timezones['australia/canberra'].dst = '04/02:02->10/01:03' 6 | timezones['pacific/fiji'].dst = '01/15:02->11/05:03' 7 | timezones['europe/brussels'].dst = '03/29:02->10/25:03' 8 | timezones['america/chicago'].dst = '03/08:02->11/01:02' 9 | timezones['canada/pacific'].dst = '03/08:02->11/01:02' 10 | s.timezones = timezones 11 | return s 12 | } 13 | export default changeTz 14 | -------------------------------------------------------------------------------- /test/nearest.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('nearest', (t) => { 5 | const s = spacetime('jan 2 2019', 'Canada/Eastern') 6 | const month = s.nearest('month') 7 | const year = s.nearest('year') 8 | const quarter = s.nearest('quarter') 9 | t.equal(month.format('iso'), year.format('iso'), 'nearest year=nearest month') 10 | t.equal(quarter.format('iso'), year.format('iso'), 'nearest quarter=nearest month') 11 | t.end() 12 | }) 13 | 14 | test('nearest-time', (t) => { 15 | let s = spacetime('feb 20 2017', 'Canada/Pacific') 16 | s = s.time('3:29am') 17 | const hour = s.nearest('hour') 18 | t.equal(hour.format('time'), '3:00am', 'close-call nearest-hour') 19 | t.end() 20 | }) 21 | 22 | test('nearest-quarter-hour', (t) => { 23 | let s = spacetime([2019, 4, 8, 10, 11, 12], 'Canada/Eastern') 24 | s = s.nearest('quarter-hour') 25 | t.equal(s.format('iso'), '2019-05-08T10:15:00.000-04:00', 'nearest-quarterhour') 26 | t.end() 27 | }) 28 | -------------------------------------------------------------------------------- /test/now.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | import tk from 'timekeeper' 4 | 5 | test('now-is-now', (t) => { 6 | const time = new Date(1554092400000) // 4:20, april 1st 2019 GMT 7 | tk.travel(time) 8 | 9 | let d = spacetime(null, 'Etc/GMT') 10 | t.equal(d.format('nice-short'), 'Apr 1st, 4:20am', 'date object mocked to 4:20') 11 | 12 | d = spacetime.now('Etc/GMT') 13 | t.equal(d.format('nice-short'), 'Apr 1st, 4:20am', 'its 4:20 now') 14 | 15 | d = spacetime.now('Canada/Eastern') 16 | t.equal(d.format('nice-short'), 'Apr 1st, 12:20am', 'its not 4:20 in toronto') 17 | 18 | d = spacetime.today('Etc/GMT') 19 | t.equal(d.format('nice-short'), 'Apr 1st, 12:00am', 'its april 1st today') 20 | 21 | d = spacetime.tomorrow('Etc/GMT') 22 | t.equal(d.format('nice-short'), 'Apr 2nd, 12:00am', 'its april 2nd tomorrow') 23 | 24 | d = spacetime.yesterday('Etc/GMT') 25 | t.equal(d.format('nice-short'), 'Mar 31st, 12:00am', 'its march 31st yesterday') 26 | tk.reset() 27 | t.end() 28 | }) 29 | 30 | test('epoch-input', (t) => { 31 | const gmt420 = 1554092400000 // 4:20, april 1st 2019 GMT 32 | const time = new Date(gmt420) 33 | tk.travel(time) 34 | 35 | let moved = spacetime.now('Etc/GMT') //4:20 36 | moved = moved.goto('Canada/Eastern') 37 | 38 | const epoch = spacetime(gmt420, 'Canada/Eastern') 39 | t.equal(moved.format('nice-short'), epoch.format('nice-short'), 'epoch input moves with goto') 40 | 41 | const explicit = spacetime([2019, 3, 1, 0, 20], 'Canada/Eastern') 42 | t.ok(explicit.isSame(epoch, 'minute'), 'explicit inputs==epoch inputs') 43 | 44 | tk.reset() 45 | t.end() 46 | }) 47 | -------------------------------------------------------------------------------- /test/progress.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('progress', (t) => { 5 | let d = spacetime('December 31, 1999 23:59:58', 'Canada/Eastern') 6 | let obj = d.progress() 7 | t.ok(obj.year > 0.95, 'almost-done-year') 8 | t.ok(obj.quarter > 0.9, 'almost-done-quarter') 9 | t.ok(obj.month > 0.9, 'almost-done-month') 10 | t.ok(obj.week > 0.7, 'almost-done-week') //friday 11 | t.ok(obj.day > 0.95, 'almost-done-day') 12 | t.ok(obj.quarterHour > 0.9, 'almost-done-hour') 13 | t.ok(obj.hour > 0.95, 'almost-done-hour') 14 | t.ok(obj.minute > 0.95, 'almost-done-minute') 15 | 16 | d = d.startOf('year') 17 | obj = d.progress() 18 | t.ok(obj.year <= 0.1, 'just-starting-year') 19 | t.ok(obj.month <= 0.1, 'just-starting-month') 20 | t.ok(obj.day <= 0.1, 'just-starting-day') 21 | t.ok(obj.hour <= 0.1, 'just-starting-hour') 22 | t.ok(obj.minute <= 0.1, 'just-starting-minute') 23 | t.end() 24 | }) 25 | 26 | test('progress-param', (t) => { 27 | const s = spacetime('jan 2 2019', 'Canada/Eastern') 28 | t.equal(s.progress('year'), 0, 'start-year') 29 | t.equal(s.progress('month'), 0.03, 'early-month') 30 | t.end() 31 | }) 32 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('get', (t) => { 5 | const s = spacetime('February 22, 2017 15:30:00', 'Canada/Eastern') 6 | t.equal(s.date(), 22, '.date()') 7 | t.equal(s.year(), 2017, '.year()') 8 | t.equal(s.quarter(), 1, '.quarter()') 9 | t.equal(s.hour(), 15, '.hour()') 10 | t.equal(s.ampm(), 'pm', '.ampm()') 11 | t.equal(s.hourFloat(), 15.5, '.hourFloat()') 12 | t.equal(s.minute(), 30, '.minute()') 13 | t.equal(s.season(), 'winter', '.season()') 14 | t.equal(s.monthName(), 'february', '.month()') 15 | t.equal(s.dayName(), 'wednesday', '.day()') 16 | t.end() 17 | }) 18 | 19 | test('get-quarters', (t) => { 20 | let s = spacetime('January 22, 2017 15:42:00', 'Canada/Eastern') 21 | t.equal(s.quarter(), 1, '.quarter()') 22 | 23 | s = s.month(1) 24 | t.equal(s.quarter(), 1, '.quarter()') 25 | 26 | s = s.month('march') 27 | t.equal(s.quarter(), 1, '.quarter()') 28 | 29 | s = s.month(3) 30 | t.equal(s.quarter(), 2, '.quarter()') 31 | 32 | s = s.month('december') 33 | t.equal(s.quarter(), 4, '.quarter()') 34 | t.end() 35 | }) 36 | 37 | test('get-weeks', (t) => { 38 | let s = spacetime('January 1, 2015 2:00:00', 'Canada/Eastern') 39 | t.equal(s.week(), 1, '.weeks()1') 40 | s = s.month('december').date(29) 41 | t.equal(s.week(), 52, '.weeks()3') 42 | t.end() 43 | }) 44 | 45 | test('day-of-year', (t) => { 46 | let s = spacetime('January 5, 2017 2:00:00', 'Canada/Eastern') 47 | t.equal(s.ampm(), 'am', '.date()') 48 | t.equal(s.date(), 5, '.date()') 49 | t.equal(s.dayOfYear(), 5, 'jan-5th()') 50 | 51 | s = spacetime('February 1, 2017 2:00:00', 'Canada/Eastern') 52 | t.equal(s.dayOfYear(), 32, 'feb 1()') 53 | 54 | s = spacetime('February 11, 2017 2:00:00', 'Canada/Eastern') 55 | t.equal(s.dayOfYear(), 42, 'feb 1()') 56 | 57 | //after feb29th, there could be a leapyear 58 | // s = spacetime('December 31, 2017 2:00:00', 'Canada/Eastern'); 59 | // t.equal(s.dayOfYear(), 364, 'December 31()'); 60 | 61 | t.end() 62 | }) 63 | -------------------------------------------------------------------------------- /test/season.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | const south = [ 5 | 'Africa/Johannesburg', 6 | 'Brazil/Acre', 7 | 'Australia/Canberra', 8 | 'Asia/Jakarta', 9 | 'America/Argentina', 10 | 'Africa/Lusaka' 11 | ] 12 | const north = [ 13 | 'America/Detroit', 14 | 'Mexico/BajaSur', 15 | 'Canada/Eastern', 16 | 'Europe/Oslo', 17 | 'Asia/Baghdad', 18 | 'Asia/Istanbul' 19 | ] 20 | 21 | test('season-by-hemisphere', (t) => { 22 | //june 23 | let s = spacetime('june 6 2017', 'Canada/Eastern') 24 | south.forEach((tz) => { 25 | s = s.goto(tz) 26 | t.equal(s.season(), 'winter', tz + ' june-winter') 27 | }) 28 | north.forEach((tz) => { 29 | s = s.goto(tz) 30 | t.equal(s.season(), 'summer', tz + ' june-summer') 31 | }) 32 | t.end() 33 | }) 34 | 35 | test('set season - north', (t) => { 36 | let s = spacetime('winter', 'Canada/Eastern') 37 | t.equal(s.monthName(), 'december', 'winter .month()') 38 | t.equal(s.date(), 1, 'q1 .date()') 39 | 40 | s = spacetime('spring', 'Canada/Eastern') 41 | t.equal(s.monthName(), 'march', 'spring .month()') 42 | t.equal(s.date(), 1, 'spring .date()') 43 | 44 | s = spacetime('summer', 'Canada/Eastern') 45 | t.equal(s.monthName(), 'june', 'summer .month()') 46 | t.equal(s.date(), 1, 'summer .date()') 47 | 48 | s = spacetime('fall', 'Canada/Eastern') 49 | t.equal(s.monthName(), 'september', 'fall .month()') 50 | t.equal(s.date(), 1, 'fall .date()') 51 | 52 | s = spacetime('fall 2001', 'Canada/Eastern') 53 | t.equal(s.monthName(), 'september', 'fall year .month()') 54 | t.equal(s.date(), 1, 'fall year .date()') 55 | t.equal(s.year(), 2001, 'fall .year()') 56 | 57 | s = spacetime('fall of 1960', 'Canada/Eastern') 58 | t.equal(s.monthName(), 'september', 'fall of year .month()') 59 | t.equal(s.date(), 1, 'fall of year .date()') 60 | t.equal(s.year(), 1960, 'fall of .year()') 61 | 62 | t.end() 63 | }) 64 | 65 | test('season - south', (t) => { 66 | let s = spacetime('nov 11 2022', 'australia/adelaide') 67 | t.equal(s.season(), 'spring', 'south-spring') 68 | s = s.add(4, 'weeks') 69 | t.equal(s.season(), 'summer', 'south-summer') 70 | t.end() 71 | }) 72 | -------------------------------------------------------------------------------- /test/semi-destructive.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('non-destructive', (t) => { 5 | let s = spacetime([2017, 5, 25]) 6 | s = s.seconds(5) 7 | s = s.year(2025) 8 | t.equal(s.date(), 25, 'init-date') 9 | t.equal(s.seconds(), 5, 'still-5-seconds') 10 | 11 | //but this method 0's-out things: 12 | s = s.quarter('q2') 13 | t.equal(s.date(), 1, 'moved-date') 14 | t.equal(s.seconds(), 0, 'now-not-5-seconds') 15 | t.end() 16 | }) 17 | 18 | test('semi-destructive', (t) => { 19 | let s = spacetime('June 12, 2017 20:01:00', 'Australia/Brisbane') 20 | t.equal(s.date(), 12, 'date-init') 21 | s = s.month('march') 22 | t.equal(s.monthName(), 'march', 'now-march') 23 | t.equal(s.date(), 12, 'still-12th') 24 | 25 | s = spacetime('June 30, 2017 20:01:00', 'Australia/Brisbane') 26 | t.equal(s.date(), 30, 'date-init') 27 | s = s.month('february') 28 | t.equal(s.monthName(), 'february', 'now-february') 29 | //close-as-possible 30 | t.equal(s.date(), 28, 'now-28th') 31 | 32 | t.end() 33 | }) 34 | -------------------------------------------------------------------------------- /test/swapTz.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('swapTz', (t) => { 5 | const arr = [ 6 | 'Africa/Dar_es_Salaam', 7 | 'Africa/Porto-Novo', 8 | 'America/Blanc-Sablon', 9 | 'America/Port-au-Prince', 10 | 'America/Port_of_Spain', 11 | 'Europe/Isle_of_Man', 12 | 'Antarctica/DumontDUrville', 13 | 'Antarctica/McMurdo', 14 | 'Asia/Ust-Nera', 15 | 'Europe/Zagreb', 16 | 'America/Bahia_Banderas', 17 | 'Asia/Kuching', 18 | 'Etc/GMT+7', 19 | ] 20 | let s = spacetime('2011-12-03T10:15:30', 'america/montreal') 21 | t.equal(s.time(), '10:15am', 'first-time') 22 | arr.forEach(tz => { 23 | s = s.timezone(tz) 24 | t.equal(s.timezone().name, tz, 'swapped tz ', tz) 25 | t.equal(s.time(), '10:15am', 'swap time ' + tz) 26 | }) 27 | t.end() 28 | }) 29 | -------------------------------------------------------------------------------- /test/timezone-name.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('titlecase', (t) => { 5 | const arr = [ 6 | 'Africa/Dar_es_Salaam', 7 | 'Africa/Porto-Novo', 8 | 'America/Blanc-Sablon', 9 | 'America/Port-au-Prince', 10 | 'America/Port_of_Spain', 11 | 'Europe/Isle_of_Man', 12 | 'Antarctica/DumontDUrville', 13 | 'Antarctica/McMurdo', 14 | 'Asia/Ust-Nera', 15 | 'Europe/Zagreb', 16 | 'America/Bahia_Banderas', 17 | 'Asia/Kuching', 18 | 'Etc/GMT+7', 19 | ] 20 | arr.forEach(tz => { 21 | const s = spacetime.now(tz) 22 | t.equal(s.timezone().name, tz, tz) 23 | }) 24 | t.end() 25 | }) 26 | -------------------------------------------------------------------------------- /test/toNativeDate.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('toNativeDate-is-epoch', (t) => { 5 | let d = spacetime(1554092400000, 'Australia/Brisbane') // 4:20, april 1st 2019 GMT 6 | d = d.hour('3').minute('14') 7 | 8 | const localDate = d.toNativeDate() 9 | const localDateSeconds = localDate.getTime() 10 | 11 | t.equal(localDateSeconds, d.epoch, 'toNativeDate is not epoch') 12 | t.end() 13 | }) 14 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | import { default as test } from 'tape' 2 | import { spacetime } from './spacetime-static' 3 | 4 | test('typefile smoketest', (t: test.Test) => { 5 | t.ok(spacetime, 'import works') 6 | const d = spacetime('June 5th 2019') 7 | t.equal(d.format('iso-short'), '2019-06-05', 'basic-smoketest') 8 | t.end() 9 | }) 10 | 11 | // Add reference to the other files so they included in the test build 12 | import './constructor.test' 13 | import './types.test' 14 | -------------------------------------------------------------------------------- /test/types/spacetime-static.ts: -------------------------------------------------------------------------------- 1 | import { default as spacetimejs } from '../../builds/spacetime.cjs' 2 | import { SpacetimeStatic } from '../../types/constructors' 3 | 4 | export const spacetime: SpacetimeStatic = spacetimejs 5 | -------------------------------------------------------------------------------- /test/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "module": "commonjs", 7 | "lib": [], 8 | "allowJs": true, 9 | "checkJs": true, 10 | "declaration": true, 11 | "sourceMap": false, 12 | "importHelpers": true, 13 | "downlevelIteration": true, 14 | "isolatedModules": true, 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "noImplicitThis": true, 19 | "alwaysStrict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "moduleResolution": "node", 25 | "allowSyntheticDefaultImports": true 26 | } 27 | } -------------------------------------------------------------------------------- /test/types/types.test.ts: -------------------------------------------------------------------------------- 1 | import { default as test } from 'tape' 2 | import { spacetime } from './spacetime-static' 3 | 4 | test('Spacetime base properties exist', (t: test.Test) => { 5 | const obj = spacetime.now() 6 | 7 | t.ok(obj.d instanceof Date, '.d is a date') 8 | t.equal(typeof obj.epoch, 'number', '.epoch is a number') 9 | t.equal(typeof obj.silent, 'boolean', '.silent is a boolean') 10 | t.equal(typeof obj.tz, 'string', '.tz is a string') 11 | t.ok(obj.timezones != undefined, '.timezones exists') 12 | t.end() 13 | }) 14 | -------------------------------------------------------------------------------- /test/utcOffset.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | //https://en.wikipedia.org/wiki/ISO_8601 5 | //the zone designator would be "+01:00", "+0100", or simply "+01" 6 | test('set-offset-from-ISO-8601', (t) => { 7 | const defaultTz = 'Canada/Eastern' 8 | const arr = [ 9 | ['2017-04-03T08:00:00', defaultTz], 10 | ['2017-04-03T08:00:00-0700', 'Etc/GMT+7'], 11 | ['2017-04-03T08:00:00-1000', 'Etc/GMT+10'], 12 | ['2017-04-03T08:00:00+0700', 'Etc/GMT-7'], 13 | ['2017-10-03T08:00:00+0000', 'Etc/GMT'], 14 | // ['2017-04-03T08:00:00-0500', defaultTz], //the same 15 | ['2017-05-03T13:00:00+0500', 'Etc/GMT-5'], 16 | ['2017-04-02T08:00:00-10:00', 'Etc/GMT+10'], 17 | ['2017-04-11T02:00:00+10:00', 'Etc/GMT-10'], 18 | ['2017-04-02T08:00:00-01:00', 'Etc/GMT+1'], 19 | ['2017-04-11T02:00:00+01:00', 'Etc/GMT-1'], 20 | ['2018-04-10T08:00:00-03', 'Etc/GMT+3'], 21 | ['2017-04-03T12:00:00+03', 'Etc/GMT-3'], 22 | ['2017-04-03T01:00:00+00', 'Etc/GMT'], 23 | ['2017-04-03T01:00:00Z', 'Etc/GMT'], 24 | ['2019-02-22T20:00:00+05:30', 'Etc/GMT-5.5'], 25 | ['2019-02-22T01:00:00+0530', 'Etc/GMT-5.5'] 26 | ] 27 | arr.forEach((a) => { 28 | const s = spacetime(a[0], defaultTz) 29 | t.equal(s.timezone().name, a[1], a[0]) 30 | }) 31 | 32 | t.end() 33 | }) 34 | 35 | test('offset-should-be-consistant', (t) => { 36 | const s = spacetime('2019-03-13T18:00:00.000-05:00') 37 | t.equal(s.format('iso').slice(-6), '-05:00') 38 | t.end() 39 | }) 40 | -------------------------------------------------------------------------------- /test/validation.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import spacetime from './lib/index.js' 3 | 4 | test('large date numbers', function (t) { 5 | let d = spacetime([2019, 'february']) 6 | d = d.date(30) 7 | t.equal(d.date(), 28, 'feb is <= 28') 8 | 9 | d = spacetime([2019, 'june']) 10 | d = d.date(300) 11 | t.equal(d.date(), 30, 'june is <= 30') 12 | 13 | d = spacetime([2022, 'december', 900]) 14 | t.equal(d.date(), 31, 'dec is <= 31') 15 | t.end() 16 | }) 17 | 18 | test('small date numbers', function (t) { 19 | let d = spacetime([2019, 'february']) 20 | d = d.date(0) 21 | t.equal(d.date(), 1, 'date is >= 1') 22 | 23 | d = d.date(-10) 24 | t.equal(d.date(), 1, 'date is still >= 1') 25 | 26 | d = spacetime([2022, 'december', 0]) 27 | t.equal(d.date(), 1, 'dec is >= 1') 28 | 29 | t.end() 30 | }) 31 | 32 | test('large month numbers', function (t) { 33 | let d = spacetime([2019]) 34 | d = d.month(14) 35 | t.equal(d.monthName(), 'december', 'month is <= december') 36 | 37 | d = spacetime([2019]) 38 | d = d.month(-14) 39 | t.equal(d.monthName(), 'january', 'month is >= january') 40 | 41 | d = spacetime([2019, 13, 5]) 42 | t.equal(d.monthName(), 'december', 'array-set month is <= december') 43 | t.equal(d.date(), 5, 'date is still valid') 44 | t.end() 45 | }) 46 | -------------------------------------------------------------------------------- /types/constraints.d.ts: -------------------------------------------------------------------------------- 1 | export type TimeUnit = 2 | | 'millisecond' 3 | | 'second' 4 | | 'minute' 5 | | 'quarterHour' 6 | | 'hour' 7 | | 'day' 8 | | 'week' 9 | | 'month' 10 | | 'quarter' 11 | | 'season' 12 | | 'year' 13 | | 'decade' 14 | | 'century' 15 | | 'date' 16 | | 'milliseconds' //plural forms 17 | | 'seconds' 18 | | 'minutes' 19 | | 'quarterHours' 20 | | 'hours' 21 | | 'days' 22 | | 'weeks' 23 | | 'months' 24 | | 'quarters' 25 | | 'seasons' 26 | | 'years' 27 | | 'decades' 28 | | 'centuries' 29 | | 'dates' 30 | 31 | export type Format = 32 | | 'day' 33 | | 'day-short' 34 | | 'day-number' 35 | | 'day-ordinal' 36 | | 'day-pad' 37 | | 'date' 38 | | 'date-ordinal' 39 | | 'date-pad' 40 | | 'month' 41 | | 'month-short' 42 | | 'month-number' 43 | | 'month-ordinal' 44 | | 'month-pad' 45 | | 'year' 46 | | 'year-short' 47 | | 'time' 48 | | 'time-24' 49 | | 'hour' 50 | | 'hour-pad' 51 | | 'hour-24' 52 | | 'hour-24-pad' 53 | | 'minute' 54 | | 'minute-pad' 55 | | 'second' 56 | | 'second-pad' 57 | | 'millisecond' 58 | | 'ampm' 59 | | 'quarter' 60 | | 'season' 61 | | 'era' 62 | | 'timezone' 63 | | 'offset' 64 | | 'numeric' 65 | | 'numeric-us' 66 | | 'numeric-uk' 67 | | 'mm/dd' 68 | | 'iso' 69 | | 'json' 70 | | 'iso-short' 71 | | 'iso-utc' 72 | | 'iso-full' 73 | | 'sql' 74 | | 'nice' 75 | | 'nice-year' 76 | | 'nice-day' 77 | | 'nice-full' 78 | | string 79 | 80 | export interface I18nOptions { 81 | /** Alternatives to Monday, Tuesday..*/ 82 | days?: { 83 | short: string[] 84 | long: string[] 85 | } 86 | /** Alternatives to Jan, Feb..*/ 87 | months?: { 88 | short: string[] 89 | long: string[] 90 | } 91 | /** Alternatives to am, pm*/ 92 | ampm?: { 93 | am: string 94 | pm: string 95 | } 96 | /** Default dayname formatting */ 97 | useTitleCase?: boolean 98 | } 99 | -------------------------------------------------------------------------------- /types/index.d.cts: -------------------------------------------------------------------------------- 1 | import type { SpacetimeStatic } from './constructors' 2 | 3 | declare const spacetime: SpacetimeStatic 4 | 5 | // We need to use a single default export here so everything lines up with the actual imported object from JS 6 | export = spacetime 7 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { SpacetimeStatic } from './constructors.js' 2 | 3 | declare const spacetime: SpacetimeStatic 4 | 5 | // We need to use a single default export here so everything lines up with the actual imported object from JS 6 | export default spacetime 7 | 8 | 9 | export { SpacetimeConstructor, SpacetimeConstructorOptions, SpacetimeStatic } from './constructors.js' 10 | export { Format, I18nOptions, TimeUnit } from './constraints.js' 11 | export { Spacetime, Diff, ParsableDate, Progress, Since, TimezoneMeta, TimezoneSet } from './types.js' 12 | -------------------------------------------------------------------------------- /zonefile/_prefixes.js: -------------------------------------------------------------------------------- 1 | //prefixes for iana names.. 2 | export default [ 3 | 'africa', 4 | 'america', 5 | 'asia', 6 | 'atlantic', 7 | 'australia', 8 | 'brazil', 9 | 'canada', 10 | 'chile', 11 | 'europe', 12 | 'indian', 13 | 'mexico', 14 | 'pacific', 15 | 'antarctica', 16 | 'etc' 17 | ] 18 | -------------------------------------------------------------------------------- /zonefile/pack.js: -------------------------------------------------------------------------------- 1 | //turn our timezone data into a small-as-possible string 2 | import { writeFileSync } from 'fs' 3 | import iana from './iana.js' 4 | import aliases from './aliases.js' 5 | import prefixes from './_prefixes.js' 6 | const all = {} 7 | 8 | // add aliases in 9 | Object.keys(aliases).forEach((k) => { 10 | const found = iana[aliases[k]] 11 | if (!found) { 12 | console.log('missing', aliases[k]) 13 | } 14 | iana[k] = Object.assign({}, found) 15 | }) 16 | 17 | //pack iana data into a [o|h] object 18 | Object.keys(iana).forEach((k) => { 19 | const o = iana[k] 20 | let key = o.offset + '|' + o.hem 21 | if (o.dst) { 22 | key += '|' + o.dst 23 | } 24 | all[key] = all[key] || [] 25 | const name = k.replace(/(.*?)\//, (a, prefix) => { 26 | const index = prefixes.indexOf(prefix) 27 | if (index !== -1) { 28 | return index + '/' 29 | } 30 | return a 31 | }) 32 | all[key].push(name) 33 | }) 34 | 35 | //add-in informal abbreviations 36 | // all = addHemisphere(all, informal.south, 's') 37 | // all = addHemisphere(all, informal.north, 'n') 38 | 39 | let keys = Object.keys(all) 40 | keys = keys.sort((a, b) => (a < b ? 1 : -1)) 41 | const result = {} 42 | keys.forEach((k) => { 43 | result[k] = all[k].join(',') 44 | }) 45 | 46 | // console.log(result) 47 | writeFileSync('./zonefile/_build.js', 'export default ' + JSON.stringify(result, null, 2)) 48 | -------------------------------------------------------------------------------- /zonefile/unpack.js: -------------------------------------------------------------------------------- 1 | import data from './_build.js' 2 | import prefixes from './_prefixes.js' 3 | 4 | const all = {} 5 | Object.keys(data).forEach((k) => { 6 | const split = k.split('|') 7 | const obj = { 8 | offset: Number(split[0]), 9 | hem: split[1] 10 | } 11 | if (split[2]) { 12 | obj.dst = split[2] 13 | } 14 | const names = data[k].split(',') 15 | names.forEach((str) => { 16 | str = str.replace(/(^[0-9]+)\//, (before, num) => { 17 | num = Number(num) 18 | return prefixes[num] + '/' 19 | }) 20 | all[str] = obj 21 | }) 22 | }) 23 | 24 | all.utc = { 25 | offset: 0, 26 | hem: 'n' //default to northern hemisphere - (sorry!) 27 | } 28 | 29 | //add etc/gmt+n 30 | for (let i = -14; i <= 14; i += 0.5) { 31 | let num = i 32 | if (num > 0) { 33 | num = '+' + num 34 | } 35 | let name = 'etc/gmt' + num 36 | all[name] = { 37 | offset: i * -1, //they're negative! 38 | hem: 'n' //(sorry) 39 | } 40 | name = 'utc/gmt' + num //this one too, why not. 41 | all[name] = { 42 | offset: i * -1, 43 | hem: 'n' 44 | } 45 | } 46 | 47 | export default all 48 | --------------------------------------------------------------------------------