From 522c334708f41d13571f4957011b0e42848a84dc Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Fri, 1 Mar 2024 18:59:14 -0300 Subject: [PATCH] feat: initial commit --- .github/workflows/ci.yaml | 19 ++++++ .gitignore | 132 ++++++++++++++++++++++++++++++++++++ .idea/.gitignore | 5 ++ .idea/datelative.iml | 12 ++++ .idea/jsLibraryMappings.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ index.js | 110 ++++++++++++++++++++++++++++++ index.test.js | 118 ++++++++++++++++++++++++++++++++ package.json | 27 ++++++++ 10 files changed, 443 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/datelative.iml create mode 100644 .idea/jsLibraryMappings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 index.js create mode 100644 index.test.js create mode 100644 package.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..392a9b8 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +name: Run Tests + +on: + push: + branches: [ * ] + pull_request: + branches: [ * ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 20 + - run: npm ci + - run: npm test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d2198e --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +### Node template +# 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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/datelative.iml b/.idea/datelative.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/datelative.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a08e45 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..54a3fad --- /dev/null +++ b/index.js @@ -0,0 +1,110 @@ +const units = { + 'ms': 'milliseconds', + 'milliseconds': 'milliseconds', + 's': 'seconds', + 'sec': 'seconds', + 'second': 'seconds', + 'seconds': 'seconds', + 'Second': 'seconds', + 'Seconds': 'seconds', + 'm': 'minutes', + 'min': 'minutes', + 'minute': 'minutes', + 'minutes': 'minutes', + 'Min': 'minutes', + 'Minute': 'minutes', + 'Minutes': 'minutes', + 'h': 'hours', + 'hr': 'hours', + 'Hr': 'hours', + 'hour': 'hours', + 'hours': 'hours', + 'Hour': 'hours', + 'Hours': 'hours', + 'd': 'days', + 'D': 'days', + 'day': 'days', + 'days': 'days', + 'Day': 'days', + 'Days': 'days', + 'w': 'weeks', + 'W': 'weeks', + 'wk': 'weeks', + 'Wk': 'weeks', + 'week': 'weeks', + 'weeks': 'weeks', + 'Week': 'weeks', + 'Weeks': 'weeks', + 'M': 'months', + 'mon': 'months', + 'month': 'months', + 'months': 'months', + 'Month': 'months', + 'Months': 'months', + 'y': 'years', + 'Y': 'years', + 'yr': 'years', + 'yrs': 'years', + 'year': 'years', + 'years': 'years', + 'Year': 'years', + 'Years': 'years', + 'q': 'quarters', + 'qr': 'quarters', + 'Q': 'quarters', + 'Qr': 'quarters', + 'qtr': 'quarters', + 'quarter': 'quarters', + 'quarters': 'quarters', + 'Quarter': 'quarters', + 'Quarters': 'quarters' +}; + +const unitToMilliseconds = { + 'milliseconds': 1, + 'seconds': 1000, + 'minutes': 1000 * 60, + 'hours': 1000 * 60 * 60, + 'days': 1000 * 60 * 60 * 24, + 'weeks': 1000 * 60 * 60 * 24 * 7, + 'months': 1000 * 60 * 60 * 24 * 30, // Approximation + 'years': 1000 * 60 * 60 * 24 * 365, // Approximation + 'quarters': 1000 * 60 * 60 * 24 * 365 / 4 // Approximation +}; + +const parseableUnits = Object.keys(units).join('|'); +const regExp = new RegExp(`([-+])?(\\d*)\\s?(${parseableUnits})$`); + +/** + * Checks if a time expression is valid + * @param expression {string} - The time expression to check + * @return {boolean} + */ +export function isValid(expression) { + return regExp.test(expression); +} + +/** + * Converts a time expression to a Date object + * @param expression {string} - The time expression to convert + * @param currentDate {Date} - The current date to use as a reference + * @return {Date|null} + */ +export function relativeToDate(expression, currentDate = new Date()) { + if (typeof expression !== 'string') { + throw new TypeError('The expression should be a string'); + } + + if (!isValid(expression)) { + throw new Error('The expression is not a valid expression'); + } + + const match = expression.match(regExp); + const operator = match[1] === '-' ? -1 : 1; + const amount = parseInt(match[2]); + const unit = units[match[3]]; + const milliseconds = amount * unitToMilliseconds[unit]; + + const time = currentDate.getTime() + (operator * milliseconds); + return new Date(time); +} diff --git a/index.test.js b/index.test.js new file mode 100644 index 0000000..c1f105c --- /dev/null +++ b/index.test.js @@ -0,0 +1,118 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { relativeToDate } from './index.js'; + +describe('datelative', () => { + describe('Invalid expressions', () => { + it('should throw an error if the expression is not a string', () => { + assert.throws(() => relativeToDate(2), { + name: 'TypeError', + message: 'The expression should be a string' + }); + }); + + it('should throw an error if the expression is not a valid expression', () => { + assert.throws(() => relativeToDate('2'), { + name: 'Error', + message: 'The expression is not a valid expression' + }); + }); + }); + + describe('Valid expressions', () => { + [ + ['+2 days', 2 * 24 * 60 * 60 * 1000], + ['-2 days', -2 * 24 * 60 * 60 * 1000], + ['+2 weeks', 2 * 7 * 24 * 60 * 60 * 1000], + ['-2 weeks', -2 * 7 * 24 * 60 * 60 * 1000], + ['+2 months', 2 * 30 * 24 * 60 * 60 * 1000], + ['-2 months', -2 * 30 * 24 * 60 * 60 * 1000], + ['+2 years', 2 * 365 * 24 * 60 * 60 * 1000], + ['-2 years', -2 * 365 * 24 * 60 * 60 * 1000], + ['+2 quarters', 2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['-2 quarters', -2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['+2 milliseconds', 2], + ['-2 milliseconds', -2], + ['+2 seconds', 2 * 1000], + ['-2 seconds', -2 * 1000], + ['+2 minutes', 2 * 60 * 1000], + ['-2 minutes', -2 * 60 * 1000], + ['+2 hours', 2 * 60 * 60 * 1000], + ['-2 hours', -2 * 60 * 60 * 1000], + + ['2 days', 2 * 24 * 60 * 60 * 1000], + ['2 weeks', 2 * 7 * 24 * 60 * 60 * 1000], + ['2 months', 2 * 30 * 24 * 60 * 60 * 1000], + ['2 years', 2 * 365 * 24 * 60 * 60 * 1000], + ['2 quarters', 2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['2 milliseconds', 2], + ['2 seconds', 2 * 1000], + ['2 minutes', 2 * 60 * 1000], + ['2 hours', 2 * 60 * 60 * 1000], + + ['+2days', 2 * 24 * 60 * 60 * 1000], + ['-2days', -2 * 24 * 60 * 60 * 1000], + ['+2weeks', 2 * 7 * 24 * 60 * 60 * 1000], + ['-2weeks', -2 * 7 * 24 * 60 * 60 * 1000], + ['+2months', 2 * 30 * 24 * 60 * 60 * 1000], + ['-2months', -2 * 30 * 24 * 60 * 60 * 1000], + ['+2years', 2 * 365 * 24 * 60 * 60 * 1000], + ['-2years', -2 * 365 * 24 * 60 * 60 * 1000], + ['+2quarters', 2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['-2quarters', -2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['+2milliseconds', 2], + ['-2milliseconds', -2], + ['+2seconds', 2 * 1000], + ['-2seconds', -2 * 1000], + ['+2minutes', 2 * 60 * 1000], + ['-2minutes', -2 * 60 * 1000], + ['+2hours', 2 * 60 * 60 * 1000], + ['-2hours', -2 * 60 * 60 * 1000], + + // the other units + ['+2ms', 2], + ['-2ms', -2], + ['+2s', 2 * 1000], + ['-2s', -2 * 1000], + ['+2min', 2 * 60 * 1000], + ['-2min', -2 * 60 * 1000], + ['+2hr', 2 * 60 * 60 * 1000], + ['-2hr', -2 * 60 * 60 * 1000], + ['+2d', 2 * 24 * 60 * 60 * 1000], + ['-2d', -2 * 24 * 60 * 60 * 1000], + ['+2w', 2 * 7 * 24 * 60 * 60 * 1000], + ['-2w', -2 * 7 * 24 * 60 * 60 * 1000], + ['+2M', 2 * 30 * 24 * 60 * 60 * 1000], + ['-2M', -2 * 30 * 24 * 60 * 60 * 1000], + ['+2y', 2 * 365 * 24 * 60 * 60 * 1000], + ['-2y', -2 * 365 * 24 * 60 * 60 * 1000], + ['+2q', 2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['-2q', -2 * 365 / 4 * 24 * 60 * 60 * 1000], + + ['+2 ms', 2], + ['-2 ms', -2], + ['+2 s', 2 * 1000], + ['-2 s', -2 * 1000], + ['+2 min', 2 * 60 * 1000], + ['-2 min', -2 * 60 * 1000], + ['+2 hr', 2 * 60 * 60 * 1000], + ['-2 hr', -2 * 60 * 60 * 1000], + ['+2 d', 2 * 24 * 60 * 60 * 1000], + ['-2 d', -2 * 24 * 60 * 60 * 1000], + ['+2 w', 2 * 7 * 24 * 60 * 60 * 1000], + ['-2 w', -2 * 7 * 24 * 60 * 60 * 1000], + ['+2 M', 2 * 30 * 24 * 60 * 60 * 1000], + ['-2 M', -2 * 30 * 24 * 60 * 60 * 1000], + ['+2 y', 2 * 365 * 24 * 60 * 60 * 1000], + ['-2 y', -2 * 365 * 24 * 60 * 60 * 1000], + ['+2 q', 2 * 365 / 4 * 24 * 60 * 60 * 1000], + ['-2 q', -2 * 365 / 4 * 24 * 60 * 60 * 1000] + ].forEach(([expression, millis]) => { + it(expression, () => { + const reference = new Date(); + const expected = new Date(reference.getTime() + millis); + assert.deepEqual(relativeToDate(expression, reference), expected); + }); + }); + }); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8bfc13c --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "datelative", + "version": "0.0.1", + "description": "A library that converts relative string text to JavaScript dates", + "main": "index.js", + "type": "module", + "scripts": { + "test": "node --test", + "test:watch": "node --test --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/onhate/datelative.git" + }, + "keywords": [ + "date", + "relative", + "javascript" + ], + "author": "onhate", + "license": "ISC", + "bugs": { + "url": "https://github.com/onhate/datelative/issues" + }, + "homepage": "https://github.com/onhate/datelative#readme", + "dependencies": {} +} \ No newline at end of file