diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000..580861c --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,30 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..ea2d329 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,36 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test + + publish-gpr: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://npm.pkg.github.com/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml deleted file mode 100644 index 36694cb..0000000 --- a/.github/workflows/ruby.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake -# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby - -name: Ruby - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - name: Run tests - run: bundle exec rake test diff --git a/.gitignore b/.gitignore index 45a71ad..c6bba59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,88 +1,130 @@ -*.gem -*.rbc -/.config -/coverage/ -/InstalledFiles -/pkg/ -/spec/reports/ -/spec/examples.txt -/test/tmp/ -/test/version_tmp/ -/tmp/ - -# Used by dotenv library to load environment variables. -# .env - -# Ignore Byebug command history file. -.byebug_history - -## Specific to RubyMotion: -.dat* -.repl_history -build/ -*.bridgesupport -build-iPhoneOS/ -build-iPhoneSimulator/ - -## Specific to RubyMotion (use of CocoaPods): -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# vendor/Pods/ - -## Documentation cache and generated files: -/.yardoc/ -/_yardoc/ -/doc/ -/rdoc/ - -## Environment normalization: -/.bundle/ -/vendor/bundle -/lib/bundler/man/ - -# for a library or gem, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# Gemfile.lock -# .ruby-version -# .ruby-gemset - -# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: -.rvmrc - -# Used by RuboCop. Remote config files pulled in from inherit_from directive. -# .rubocop-https?--* - -# Cache files for Sublime Text -*.tmlanguage.cache -*.tmPreferences.cache -*.stTheme.cache - -# Workspace files are user-specific -*.sublime-workspace - -# Project files should be checked into the repository, unless a significant -# proportion of contributors will probably not be using Sublime Text -# *.sublime-project - -# SFTP configuration file -sftp-config.json -sftp-config-alt*.json - -# Package control specific files -Package Control.last-run -Package Control.ca-list -Package Control.ca-bundle -Package Control.system-ca-bundle -Package Control.cache/ -Package Control.ca-certs/ -Package Control.merged-ca-bundle -Package Control.user-ca-bundle -oscrypto-ca-bundle.crt -bh_unicode_properties.cache - -# Sublime-github package stores a github token in this file -# https://packagecontrol.io/packages/sublime-github -GitHub.sublime-settings +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1b8ac88 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 15a2799..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.3.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1d7ac85 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..95f630a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 36679f4..0000000 --- a/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -# gem "rails" -gem "debug", group: %i[develop test] -gem "minitest", group: %i[develop test] -gem "rake" -gem "standard", group: %i[develop] -gem "simplecov", group: %i[test] diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 0bf1fdd..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,82 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - ast (2.4.2) - debug (1.9.1) - irb (~> 1.10) - reline (>= 0.3.8) - docile (1.4.0) - io-console (0.7.2) - irb (1.11.2) - rdoc - reline (>= 0.4.2) - json (2.7.1) - language_server-protocol (3.17.0.3) - lint_roller (1.1.0) - minitest (5.20.0) - parallel (1.24.0) - parser (3.3.0.5) - ast (~> 2.4.1) - racc - psych (5.1.2) - stringio - racc (1.7.3) - rainbow (3.1.1) - rake (13.1.0) - rdoc (6.6.2) - psych (>= 4.0.0) - regexp_parser (2.9.0) - reline (0.4.2) - io-console (~> 0.5) - rexml (3.3.9) - rubocop (1.62.0) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) - rubocop-performance (1.20.2) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) - ruby-progressbar (1.13.0) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.4) - standard (1.34.0) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.60) - standard-custom (~> 1.0.0) - standard-performance (~> 1.3) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.3.1) - lint_roller (~> 1.1) - rubocop-performance (~> 1.20.2) - stringio (3.1.0) - unicode-display_width (2.5.0) - -PLATFORMS - arm64-darwin-23 - ruby - -DEPENDENCIES - debug - minitest - rake - simplecov - standard - -BUNDLED WITH - 2.5.3 diff --git a/README.md b/README.md index 0a5f241..cd0a74f 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,14 @@ Returns the difference between two times. ## System requirements -* Ruby >= 3.3.0 +Node.js >= 20.18.0 ## Installation -To add a symlink to the `time-diff` program from your local bin: - ```shell -# From the project root directory -./install.sh +npm install -g @t-bowersox/time-diff ``` -You may be prompted for `sudo` permission to create the symlink. - ## Usage ``` @@ -25,12 +20,10 @@ time-diff The program takes two arguments: -* `start_time`: The starting time of the duration to measure. -* `end_time`: The ending time of the duration to measure. +- `start_time`: The starting time of the duration to measure. +- `end_time`: The ending time of the duration to measure. -The times provided must be date/time strings parseable by -Ruby's [`Time::parse`](https://docs.ruby-lang.org/en/3.3/Time.html#method-c-parse) method, with the exception of a -special `now` argument. When provided, the current time will be substituted. +The times provided must be in a valid date/time format, with the exception of a special `now` argument. When provided, the current time will be substituted. ### Examples @@ -55,8 +48,8 @@ time-diff now 2099-01-01 # => 75 years, 1 month, 1 week, 4 days, 10 hours, 56 minutes, 20 seconds time-diff foo bar -# => Error: foo and bar are not valid times. +# => Start time is invalid. time-diff 9:00am -# => Error: You must provide a start time and end time. +# => An end time is required. ``` diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 91b10ac..0000000 --- a/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require "minitest/test_task" -require "standard/rake" - -Minitest::TestTask.create # named test, sensible defaults diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..5d13469 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +import process from "node:process"; +import { parseArgs } from "node:util"; +import { getTimeDiff } from "../src/index.js"; + +try { + const { positionals } = parseArgs({ allowPositionals: true }); + console.log(getTimeDiff(positionals)); +} catch (error) { + process.exitCode = 1; + console.error(error.message); +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7742f79 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,8 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, +]; diff --git a/install.sh b/install.sh deleted file mode 100755 index c8ece1b..0000000 --- a/install.sh +++ /dev/null @@ -1 +0,0 @@ -sudo ln -sf "$PWD/src/main.rb" /usr/local/bin/time-diff diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e5238d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1109 @@ +{ + "name": "time-diff", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "time-diff", + "version": "2.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.14.0", + "eslint": "^9.14.0", + "globals": "^15.12.0", + "prettier": "3.3.3" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", + "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.14.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d0f770 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "@t-bowersox/time-diff", + "version": "1.0.0", + "description": "A program that returns the difference between two times.", + "main": "src/index.js", + "type": "module", + "directories": { + "test": "test" + }, + "bin": { + "time-diff": "bin/cli.js" + }, + "scripts": { + "format": "prettier . --write", + "lint": "eslint . --fix", + "test": "node --test test/", + "test:watch": "node --test --watch test/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/t-bowersox/time-diff.git" + }, + "keywords": [ + "dates", + "times", + "cli" + ], + "author": "t-bowersox", + "license": "MIT", + "bugs": { + "url": "https://github.com/t-bowersox/time-diff/issues" + }, + "homepage": "https://github.com/t-bowersox/time-diff#readme", + "devDependencies": { + "@eslint/js": "^9.14.0", + "eslint": "^9.14.0", + "globals": "^15.12.0", + "prettier": "3.3.3" + }, + "engines": { + "node": ">=20.18.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..41d35b0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,19 @@ +import { stringToDate } from "./lib/normalize.js"; +import { TimeDiff } from "./lib/time-diff.js"; + +/** + * Returns the difference between two times. + * @param {[string, string]} times The start and end times to compare. + * @returns {string} The difference between the start and end times. + */ +export function getTimeDiff(times) { + switch (times.length) { + case 0: + throw new Error("Usage: time-diff "); + case 1: + throw new Error("An end time is required."); + } + + const [start, end] = times; + return new TimeDiff(stringToDate(start), stringToDate(end)).toString(); +} diff --git a/src/lib/normalize.js b/src/lib/normalize.js new file mode 100644 index 0000000..2cba260 --- /dev/null +++ b/src/lib/normalize.js @@ -0,0 +1,68 @@ +//#region Types + +/** + * @typedef NormalizedTime + * @property {number} hour + * @property {number} min + * @property {number} sec + */ + +//#endregion + +//#region Constants + +const TIME_REGEX = + /^(?\d{1,2})?:?(?\d{1,2})?:?(?\d{1,2})?\s?(?am|pm)?$/i; + +//#endregion + +/** + * Normalizes the provirded hour, min, sec, and ordinal values. + * @param {string|undefined} hour The hour to normalize. + * @param {string|undefined} min The minute to normalize. + * @param {string|undefined} sec The second to normalize. + * @param {string|undefined} mer The meridiem (AM/PM), if using 12h format. + * @returns {NormalizedTime} The time normalized on a 24h clock. + */ +function getNormalizedTime(hour, min, sec, mer) { + hour = hour ? parseInt(hour) : 0; + + if (mer?.toLowerCase() == "am" && hour == 12) { + hour -= 12; + } else if (mer?.toLowerCase() == "pm" && hour < 12) { + hour += 12; + } + + min = min ? parseInt(min) : 0; + sec = sec ? parseInt(sec) : 0; + + return { hour, min, sec }; +} + +/** + * Attempts to create a valid `Date` from the given date-time string. + * @param {string} dateTimeStr The date-time string to normalize. + * @returns {Date} A date parsed from the provided date-time string. + */ +export function stringToDate(dateTimeStr) { + if (typeof dateTimeStr != "string") { + dateTimeStr = String(dateTimeStr); + } + + if (dateTimeStr.toLowerCase() == "now") { + return new Date(); + } + + const timeMatch = dateTimeStr.match(TIME_REGEX); + + if (timeMatch != null) { + const { hour, min, sec, mer } = timeMatch.groups; + const normalized = getNormalizedTime(hour, min, sec, mer); + + const today = new Date(); + today.setHours(normalized.hour, normalized.min, normalized.sec); + return today; + } + + return new Date(dateTimeStr); +} diff --git a/src/lib/time-diff.js b/src/lib/time-diff.js new file mode 100644 index 0000000..e4cfe4c --- /dev/null +++ b/src/lib/time-diff.js @@ -0,0 +1,135 @@ +import units from "./units.js"; + +//#region Types + +/** + * @typedef {Object} TimeUnits + * @property {number} year + * @property {number} month + * @property {number} week + * @property {number} day + * @property {number} hour + * @property {number} minute + * @property {number} second + */ + +//#endregion + +//#region Classes + +/** Represents the difference between two times. */ +export class TimeDiff { + /** @type {Date} */ + #endTime; + + /** @type {Date} */ + #startTime; + + /** @type {Map} */ + #units = new Map([ + ["year", units.year], + ["month", units.month], + ["week", units.week], + ["day", units.day], + ["hour", units.hour], + ["minute", units.minute], + ]); + + //#region Public API + + /** + * @param {Date} startTime The start of the date range. + * @param {Date} endTime The end of the date range. + */ + constructor(startTime, endTime) { + this.#validateTimes(startTime, endTime); + this.#startTime = startTime; + this.#endTime = endTime; + } + + toString() { + if (this.seconds == 0) { + return "No difference"; + } + + return Object.entries(this.#timeToUnits()) + .filter((entry) => entry[1] > 0) + .map(this.#entryToString) + .join(", "); + } + + /** The time difference rounded to the nearest second. */ + get seconds() { + const diffMs = this.#endTime - this.#startTime; + return Math.round(diffMs / 1000); + } + + //#endregion + + /** + * Converts a `TimeUnits` value to a string. + * @param {[keyof TimeUnits, number]} entry + */ + #entryToString(entry) { + const [unitName, value] = entry; + let str = `${value.toLocaleString()} ${unitName}`; + + if (value != 1) { + str += "s"; + } + + return str; + } + + /** + * Tests that a time is valid. + * @param {Date} time The time to validate. + */ + #isValidTime(time) { + return time instanceof Date && !isNaN(time.valueOf()); + } + + /** + * Returns the time difference as a dictionary, with each key representing + * a unit of time (year, month, etc.). + * @returns {TimeUnits} + */ + #timeToUnits() { + const units = {}; + let diffSeconds = this.seconds; + + this.#units.forEach((seconds, unit) => { + units[unit] = 0; + + while (diffSeconds >= seconds) { + diffSeconds -= seconds; + units[unit] += 1; + } + }); + + units.second = diffSeconds >= 0 ? diffSeconds : 0; + return units; + } + + /** + * Validates that the start and end times form a valid date range. + * @param {Date} startTime The start of the date range. + * @param {Date} endTime The end of the date range. + * @throws If either the `startTime` or `endTime` are invalid. + */ + #validateTimes(startTime, endTime) { + if (!this.#isValidTime(startTime)) { + throw new Error("Start time is invalid."); + } + + if (!this.#isValidTime(endTime)) { + throw new Error("End time is invalid."); + } + + if (endTime < startTime) { + throw new Error("The end time must be greater than the start time."); + } + } +} + +//#endregion diff --git a/src/lib/time_diff.rb b/src/lib/time_diff.rb deleted file mode 100644 index bf44e88..0000000 --- a/src/lib/time_diff.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require "time" - -# One minute in seconds. -ONE_MIN = 60 - -# One hour (60 minutes) in seconds. -ONE_HOUR = 60 * ONE_MIN - -# One day (24 hours) in seconds. -ONE_DAY = 24 * ONE_HOUR - -# One week (7 days) in seconds. -ONE_WEEK = 7 * ONE_DAY - -# One month (4 weeks) in seconds. -ONE_MONTH = 4 * ONE_WEEK - -# One year (52 weeks) in seconds. -ONE_YEAR = 52 * ONE_WEEK - -# Represents the difference between two times. -class TimeDiff - # @param [Time] start_time - # @param [Time] end_time - def initialize(start_time, end_time) - raise "Start time is invalid." unless start_time.is_a? Time - raise "End time is invalid." unless end_time.is_a? Time - raise "The end time must be greater than the start time." if end_time < start_time - - # @type [Time] - @start_time = start_time - - # @type [Time] - @end_time = end_time - - @units = {year: ONE_YEAR, month: ONE_MONTH, week: ONE_WEEK, day: ONE_DAY, hour: ONE_HOUR, minute: ONE_MIN} - end - - # The time difference rounded to the nearest second. - def value - Float(@end_time - @start_time).round - end - - # Returns the time difference as a hash, with each key representing a unit of time (year, month, etc.). - def to_h - diff_secs = value - breakdown = Hash.new(0) - - @units.each do |unit, secs| - while diff_secs >= secs - diff_secs -= secs - breakdown[unit] += 1 - end - end - - breakdown[:second] = diff_secs if diff_secs.positive? - breakdown - end - - # Returns the time difference as a string, broken down by units of time (years, months, etc.). - def to_s - if value.zero? - "No difference." - else - to_h.map { |u, c| unit_to_s(u, c) }.join(", ") - end - end - - private - - # Converts a time difference to a string. - # @param [Symbol | String] unit - # @param [Integer] count - def unit_to_s(unit, count) - "#{count} #{unit}#{"s" if count != 1}" - end -end diff --git a/src/lib/units.js b/src/lib/units.js new file mode 100644 index 0000000..95f07eb --- /dev/null +++ b/src/lib/units.js @@ -0,0 +1,26 @@ +export default { + /** One minute in seconds. */ + get minute() { + return 60; + }, + /** One hour in seconds. */ + get hour() { + return this.minute * 60; + }, + /** One day in seconds. */ + get day() { + return this.hour * 24; + }, + /** One week in seconds. */ + get week() { + return this.day * 7; + }, + /** One month in seconds. */ + get month() { + return this.week * 4; + }, + /** One year in seconds. */ + get year() { + return this.day * 365; + }, +}; diff --git a/src/main.rb b/src/main.rb deleted file mode 100755 index 9fd3f9d..0000000 --- a/src/main.rb +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env ruby - -# frozen_string_literal: true - -require "time" -require_relative "lib/time_diff" - -def exit_with_error(error_msg) - puts error_msg - exit false -end - -# @param [Array] times An array containing two times to compare. -def main(times) - start_time, end_time = times.map { |time| (time.downcase == "now") ? Time.new : Time.parse(time) } - puts TimeDiff.new(start_time, end_time) -rescue ArgumentError - exit_with_error "Error: #{times.join(" and ")} are not valid times." -rescue RuntimeError => e - exit_with_error e.message -end - -main(ARGV) if __FILE__ == $PROGRAM_NAME diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..c50470d --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,27 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { getTimeDiff } from "../src/index.js"; +import { TimeDiff } from "../src/lib/time-diff.js"; + +describe("getTimeDiff", () => { + it("Should throw usage instructions if no times provided", () => { + assert.throws(() => { + getTimeDiff([]); + }, new Error("Usage: time-diff ")); + }); + + it("Should throw error if only one time provided", () => { + assert.throws(() => { + getTimeDiff(["8:00 am"]); + }, new Error("An end time is required.")); + }); + + it("Should return the difference bweteen two times as a string", () => { + const start = new Date(2024, 0, 1, 12, 0, 0); + const end = new Date(2024, 0, 16, 14, 2, 2); + const diff = new TimeDiff(start, end); + const expectedString = "2 weeks, 1 day, 2 hours, 2 minutes, 2 seconds"; + + assert.equal(diff.toString(), expectedString); + }); +}); diff --git a/test/main_test.rb b/test/main_test.rb deleted file mode 100644 index acea85c..0000000 --- a/test/main_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require "minitest/autorun" -require "time" -require_relative "../src/main" - -describe :main do - it "should abort if arguments are invalid times" do - out, = capture_io do - expect { main(%w[foo bar]) }.must_raise(SystemExit) - end - - expect(out).must_match(/foo and bar are not valid times/) - end - - it "should abort if not enough arguments provided" do - out, = capture_io do - expect { main(["12:00"]) }.must_raise(SystemExit) - end - - expect(out).must_match(/End time is invalid/) - end - - it "should abort if end time is before start time" do - out, = capture_io do - expect { main(%w[12:00 8:00]) }.must_raise(SystemExit) - end - - expect(out).must_match(/The end time must be greater than the start time/) - end - - it 'should substitute the current time for the "now" argument' do - Time.stub :new, Time.new(2024, 1, 3) do - expect { main(%w[2024-01-01 now]) }.must_output(/^2 days$/) - end - end - - it 'should treat "now" argument as case insensitive' do - Time.stub :new, Time.new(2024, 1, 3) do - expect { main(%w[2024-01-01 NoW]) }.must_output(/^2 days$/) - end - end - - it "should output the difference between two times" do - expect { main(%w[12:00pm 2:30pm]) }.must_output(/^2 hours, 30 minutes$/) - end -end diff --git a/test/normalize.test.js b/test/normalize.test.js new file mode 100644 index 0000000..ccb791f --- /dev/null +++ b/test/normalize.test.js @@ -0,0 +1,93 @@ +import assert from "node:assert"; +import { afterEach, beforeEach, describe, it, mock } from "node:test"; +import { stringToDate } from "../src/lib/normalize.js"; + +describe("stringToDate", () => { + beforeEach(() => { + mock.timers.enable({ + apis: ["Date"], + now: new Date(2024, 0, 1, 0, 0, 0, 0), + }); + }); + + afterEach(() => { + mock.timers.reset(); + }); + + it('should return the current date if "now" is provided', () => { + assert.deepStrictEqual(stringToDate("now"), new Date()); + }); + + it("should return a date when given only a time", () => { + const testSet = [ + ["12:00 am", 0, 0], + ["12:00am", 0, 0], + ["12:00 AM", 0, 0], + ["12:00AM", 0, 0], + ["1:30 am", 1, 30], + ["1:30 AM", 1, 30], + ["1:30am", 1, 30], + ["1:30AM", 1, 30], + ["01:30AM", 1, 30], + ["01:30am", 1, 30], + ["01:30 am", 1, 30], + ["01:30 AM", 1, 30], + ["01:30", 1, 30], + ["12:00 pm", 12, 0], + ["12:00pm", 12, 0], + ["12:00 PM", 12, 0], + ["12:00PM", 12, 0], + ["1:30 pm", 13, 30], + ["1:30 PM", 13, 30], + ["1:30pm", 13, 30], + ["1:30PM", 13, 30], + ["01:30PM", 13, 30], + ["01:30pm", 13, 30], + ["01:30 pm", 13, 30], + ["01:30 PM", 13, 30], + ["13:30", 13, 30], + ]; + + for (const [time, hour, min] of testSet) { + const expectedDate = new Date(); + expectedDate.setHours(hour, min); + + assert.deepEqual( + stringToDate(time), + expectedDate, + `${time} should yield ${expectedDate.toISOString()}.`, + ); + } + }); + + it("should return a date from the provided string", () => { + const expectedDate = new Date(); + expectedDate.setFullYear(2024, 11, 31); + expectedDate.setHours(0, 0); + + const testSet = [ + "12/31/2024 12:00 am", + "2024-12-31T00:00:00", + "Dec 31, 2024 12:00 am", + "31 Dec, 2024 12:00 am", + "December 31, 2024 12:00 AM", + ]; + + for (const dateStr of testSet) { + assert.deepEqual( + stringToDate(dateStr), + expectedDate, + `${dateStr} should yield ${expectedDate}.`, + ); + } + }); + + it("should handle invalid inputs", () => { + assert.doesNotThrow(() => stringToDate("foobar")); + assert.doesNotThrow(() => stringToDate()); + assert.doesNotThrow(() => stringToDate(null)); + assert.doesNotThrow(() => stringToDate(42)); + assert.doesNotThrow(() => stringToDate(["foo,", "bar"])); + assert.doesNotThrow(() => stringToDate({ foo: "bar" })); + }); +}); diff --git a/test/time-diff.test.js b/test/time-diff.test.js new file mode 100644 index 0000000..e24ed5b --- /dev/null +++ b/test/time-diff.test.js @@ -0,0 +1,49 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { TimeDiff } from "../src/lib/time-diff.js"; + +describe("TimeDiff", () => { + it("should throw exception if start time is invalid", () => { + assert.throws(() => { + new TimeDiff(new Date("foobar"), new Date()); + }, new Error("Start time is invalid.")); + }); + + it("should throw exception if end time is invalid", () => { + assert.throws(() => { + new TimeDiff(new Date(), new Date("foobar")); + }, new Error("End time is invalid.")); + }); + + it("should throw exception if end time is less than start time", () => { + assert.throws(() => { + new TimeDiff(new Date(2024, 0, 2), new Date(2024, 0, 1)); + }, new Error("The end time must be greater than the start time.")); + }); + + it("should return the difference between two dates rounded to the nearest second", () => { + const start = new Date(2024, 0, 1, 12, 0, 0); + const end = new Date(2024, 0, 1, 12, 0, 0); + const diff = new TimeDiff(start, end); + const expectedSeconds = Math.round((end - start) / 1000); + + assert.equal(diff.seconds, expectedSeconds); + }); + + it("should summarize the difference between two dates as a string", () => { + const start = new Date(2024, 0, 1, 12, 0, 0); + const end = new Date(2024, 0, 16, 14, 2, 2); + const diff = new TimeDiff(start, end); + const expectedString = "2 weeks, 1 day, 2 hours, 2 minutes, 2 seconds"; + + assert.equal(diff.toString(), expectedString); + }); + + it("should indicate if there is no difference between two dates", () => { + const start = new Date(2024, 0, 1, 0, 0, 0); + const end = new Date(2024, 0, 1, 0, 0, 0); + const diff = new TimeDiff(start, end); + + assert.equal(diff.toString(), "No difference"); + }); +}); diff --git a/test/time_diff_test.rb b/test/time_diff_test.rb deleted file mode 100644 index ad78653..0000000 --- a/test/time_diff_test.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require "minitest/autorun" -require "time" -require_relative "../src/lib/time_diff" - -describe TimeDiff do - it "should raise error if start time is invalid" do - # noinspection RubyMismatchedArgumentType - expect { TimeDiff.new("8:00", Time.parse("10:00")) }.must_raise(RuntimeError, /Start time is invalid/) - end - - it "should raise error if end time is invalid" do - # noinspection RubyMismatchedArgumentType - expect { TimeDiff.new(Time.parse("8:00"), "10:00") }.must_raise(RuntimeError, /End time is invalid/) - end - - it "should raise error if end time is less than start time" do - expect { TimeDiff.new(Time.parse("10:00"), Time.parse("8:00")) }.must_raise( - RuntimeError, /The end time must be greater than the start time/ - ) - end - - describe :value do - it "should return the value of the difference in seconds" do - start_time = Time.parse("8:00") - end_time = Time.parse("10:30") - expected = (ONE_HOUR * 2) + (ONE_MIN * 30) - - expect(TimeDiff.new(start_time, end_time).value).must_equal(expected) - end - end - - describe :to_h do - it "should return years" do - diff = TimeDiff.new(Time.parse("2023-01-01"), Time.parse("2023-12-31")) - expect(diff.to_h[:year]).must_equal(1) - end - - it "should return months" do - diff = TimeDiff.new(Time.parse("2024-01-01"), Time.parse("2024-02-01")) - expect(diff.to_h[:month]).must_equal(1) - end - - it "should return weeks" do - diff = TimeDiff.new(Time.parse("2024-01-01"), Time.parse("2024-01-15")) - expect(diff.to_h[:week]).must_equal(2) - end - - it "should return days" do - diff = TimeDiff.new(Time.parse("2024-01-01"), Time.parse("2024-01-05")) - expect(diff.to_h[:day]).must_equal(4) - end - - it "should return hours" do - diff = TimeDiff.new(Time.parse("8:00"), Time.parse("10:00")) - expect(diff.to_h[:hour]).must_equal(2) - end - - it "should return minutes" do - diff = TimeDiff.new(Time.parse("8:00"), Time.parse("8:15")) - expect(diff.to_h[:minute]).must_equal(15) - end - - it "should return seconds" do - diff = TimeDiff.new(Time.parse("8:00"), Time.parse("8:00:30")) - expect(diff.to_h[:second]).must_equal(30) - end - - it "should return compound difference" do - diff = TimeDiff.new( - Time.parse("2023-01-01 00:00:00"), - Time.parse("2024-02-14 12:15:30") - ) - - expect(diff.to_h).must_equal( - {year: 1, month: 1, week: 2, day: 3, hour: 12, minute: 15, second: 30} - ) - end - end - - describe :to_s do - it 'should print "No difference" if diff is zero' do - diff = TimeDiff.new(Time.parse("8:00"), Time.parse("8:00")) - expect(diff.to_s).must_equal("No difference.") - end - - it "should print difference" do - diff = TimeDiff.new( - Time.parse("2023-01-01 00:00:00"), - Time.parse("2024-02-14 12:15:30") - ) - - expect(diff.to_s).must_equal( - "1 year, 1 month, 2 weeks, 3 days, 12 hours, 15 minutes, 30 seconds" - ) - end - end -end diff --git a/time-diff.sublime-project b/time-diff.sublime-project deleted file mode 100644 index 7a4ad52..0000000 --- a/time-diff.sublime-project +++ /dev/null @@ -1,19 +0,0 @@ -{ - "folders": - [ - { - "path": "." - } - ], - "build_systems": - [ - { - "name": "Standard Ruby", - "env": { - "PATH": "~/.rbenv/shims:$PATH" - }, - "shell_cmd": "standardrb --fix $file", - "file_patterns": ["*.rb", "Rakefile", "Gemfile*"] - } - ] -}