From 81410eb22d60233218cdcd5b4934cc3bb415d302 Mon Sep 17 00:00:00 2001 From: Jason Humphrey Date: Fri, 6 Jun 2025 20:39:54 -0500 Subject: [PATCH 1/3] feat: add date/time parsing --- CHANGELOG.md | 7 ++++- README.md | 10 ++++++- dist/auto-parse.esm.js | 55 +++++++++++++++++++++++++++++++++++++++ dist/auto-parse.js | 55 +++++++++++++++++++++++++++++++++++++++ docs/RELEASE_NOTES_2.2.md | 10 +++++++ index.d.ts | 1 + index.js | 50 +++++++++++++++++++++++++++++++++++ package.json | 2 +- test/date-time.test.js | 29 +++++++++++++++++++++ test/performance.test.js | 13 +++++++++ 10 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 docs/RELEASE_NOTES_2.2.md create mode 100644 test/date-time.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ca395..d377570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,4 +33,9 @@ - Map, Set and typed array support - Simple math expression evaluation - Optional env variable expansion and function-string parsing -- Advanced features must be enabled individually via options \ No newline at end of file +- Advanced features must be enabled individually via options + +## 2.2.0 (2025-06-08) + +- Built-in date/time recognition for ISO 8601 and common local formats +- New `parseDates` option to enable the feature diff --git a/README.md b/README.md index d482d00..803afb5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A small utility that automatically converts strings and other values into the mo - Converts `Map:` and `Set:` strings into real objects - Supports typed arrays - Evaluates simple math expressions +- Recognizes common date/time formats - Optional environment variable expansion - Optional function-string parsing - Advanced features are disabled by default and can be enabled individually @@ -60,6 +61,7 @@ autoParse('yes', { booleanSynonyms: true }) // => true autoParse('Map:[["a",1]]', { parseMapSets: true }).get('a') // => 1 autoParse('Uint8Array[1,2]', { parseTypedArrays: true })[0] // => 1 autoParse('2 + 3 * 4', { parseExpressions: true }) // => 14 +autoParse('2023-06-01', { parseDates: true }) // => Date object process.env.TEST_ENV = '123' autoParse('$TEST_ENV', { expandEnv: true }) // => 123 const double = autoParse('x => x * 2', { parseFunctionStrings: true }) @@ -123,9 +125,10 @@ More examples can be found in the [`examples/`](examples) directory. - `parseMapSets` – convert `Map:` and `Set:` strings. - `parseTypedArrays` – support typed array notation. - `parseExpressions` – evaluate simple math expressions. +- `parseDates` – recognize ISO 8601 and common local date/time strings. - `currencySymbols` – object mapping extra currency symbols to codes, e.g. `{ 'r$': 'BRL', "\u20BA": 'TRY' }`. -## Benchmarks (v2.1.0) +## Benchmarks (v2.2.0) The following timings are measured on Node.js using `npm test` and represent roughly how long it takes to parse 10 000 values after warm‑up: @@ -139,6 +142,7 @@ The following timings are measured on Node.js using `npm test` and represent rou | plain objects | ~3 | | options combined | ~6 | | plugin hook | ~4 | +| date/time parse | ~5 | Even a single parse is extremely fast: @@ -152,6 +156,7 @@ Even a single parse is extremely fast: | plain objects | ~0.0003 | | options combined | ~0.0006 | | plugin hook | ~0.0004 | +| date/time parse | ~0.0005 | These numbers demonstrate the parser runs in well under a millisecond for typical values, so performance should never be a concern. @@ -186,6 +191,9 @@ strings, Map and Set objects, typed arrays, simple expression evaluation and optional environment variable and function-string handling. See [docs/RELEASE_NOTES_2.1.md](docs/RELEASE_NOTES_2.1.md) for details. +Version 2.2 introduces optional date/time recognition. See +[docs/RELEASE_NOTES_2.2.md](docs/RELEASE_NOTES_2.2.md) for details. + ## Contributing 1. Fork the repository and create a branch for your feature or fix. diff --git a/dist/auto-parse.esm.js b/dist/auto-parse.esm.js index 90e003c..a135678 100644 --- a/dist/auto-parse.esm.js +++ b/dist/auto-parse.esm.js @@ -202,6 +202,56 @@ var require_auto_parse = __commonJS({ } return null; } + function parseDateTimeString(str) { + const iso = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/; + if (iso.test(str)) { + const d = new Date(str); + if (!Number.isNaN(d.getTime())) + return d; + } + let m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?)?$/i.exec(str); + if (m) { + let [, month, day, year, h, min, sec, ap] = m; + const date = new Date(Number(year), Number(month) - 1, Number(day)); + if (h !== void 0) { + h = Number(h); + if (ap) { + ap = ap.toLowerCase(); + if (ap === "pm" && h < 12) + h += 12; + if (ap === "am" && h === 12) + h = 0; + } + date.setHours(h, Number(min), Number(sec || 0), 0); + } + return date; + } + m = /^(\d{1,2})-(\d{1,2})-(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(str); + if (m) { + const [, day, month, year, h, min, sec] = m; + const date = new Date(Number(year), Number(month) - 1, Number(day)); + if (h !== void 0) { + date.setHours(Number(h), Number(min), Number(sec || 0), 0); + } + return date; + } + m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?$/i.exec(str); + if (m) { + let [, h, min, sec, ap] = m; + h = Number(h); + if (ap) { + ap = ap.toLowerCase(); + if (ap === "pm" && h < 12) + h += 12; + if (ap === "am" && h === 12) + h = 0; + } + const date = /* @__PURE__ */ new Date(); + date.setHours(h, Number(min), Number(sec || 0), 0); + return date; + } + return null; + } function parseExpressionString(str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -411,6 +461,11 @@ var require_auto_parse = __commonJS({ if (fn) return returnIfAllowed(fn, options, originalValue); } + if (options.parseDates) { + const dt = parseDateTimeString(trimmed); + if (dt) + return returnIfAllowed(dt, options, originalValue); + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })); if (value === "undefined" || value === "") { return returnIfAllowed(void 0, options, originalValue); diff --git a/dist/auto-parse.js b/dist/auto-parse.js index 5cc29a3..1bad518 100644 --- a/dist/auto-parse.js +++ b/dist/auto-parse.js @@ -195,6 +195,56 @@ function parseMapSetString(str, options) { } return null; } +function parseDateTimeString(str) { + const iso = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/; + if (iso.test(str)) { + const d = new Date(str); + if (!Number.isNaN(d.getTime())) + return d; + } + let m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?)?$/i.exec(str); + if (m) { + let [, month, day, year, h, min, sec, ap] = m; + const date = new Date(Number(year), Number(month) - 1, Number(day)); + if (h !== void 0) { + h = Number(h); + if (ap) { + ap = ap.toLowerCase(); + if (ap === "pm" && h < 12) + h += 12; + if (ap === "am" && h === 12) + h = 0; + } + date.setHours(h, Number(min), Number(sec || 0), 0); + } + return date; + } + m = /^(\d{1,2})-(\d{1,2})-(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(str); + if (m) { + const [, day, month, year, h, min, sec] = m; + const date = new Date(Number(year), Number(month) - 1, Number(day)); + if (h !== void 0) { + date.setHours(Number(h), Number(min), Number(sec || 0), 0); + } + return date; + } + m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?$/i.exec(str); + if (m) { + let [, h, min, sec, ap] = m; + h = Number(h); + if (ap) { + ap = ap.toLowerCase(); + if (ap === "pm" && h < 12) + h += 12; + if (ap === "am" && h === 12) + h = 0; + } + const date = /* @__PURE__ */ new Date(); + date.setHours(h, Number(min), Number(sec || 0), 0); + return date; + } + return null; +} function parseExpressionString(str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -404,6 +454,11 @@ function autoParse(value, typeOrOptions) { if (fn) return returnIfAllowed(fn, options, originalValue); } + if (options.parseDates) { + const dt = parseDateTimeString(trimmed); + if (dt) + return returnIfAllowed(dt, options, originalValue); + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })); if (value === "undefined" || value === "") { return returnIfAllowed(void 0, options, originalValue); diff --git a/docs/RELEASE_NOTES_2.2.md b/docs/RELEASE_NOTES_2.2.md new file mode 100644 index 0000000..1243c10 --- /dev/null +++ b/docs/RELEASE_NOTES_2.2.md @@ -0,0 +1,10 @@ +# Release Notes: Version 2.2 + +Version 2.2 adds optional date and time parsing to **auto-parse**. + +- ISO 8601 strings like `2023-04-05T12:00:00Z` are recognized automatically. +- Localized formats such as `03/10/2020` or `10-03-2020 14:30` are supported. +- Standalone times like `8:45 PM` return `Date` objects for the current day. +- Enable via the `parseDates` option. It is disabled by default. + +See the [CHANGELOG](../CHANGELOG.md) for a detailed history. diff --git a/index.d.ts b/index.d.ts index 46a4b7a..e1b146c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -17,6 +17,7 @@ export interface AutoParseOptions { parseMapSets?: boolean; parseTypedArrays?: boolean; parseExpressions?: boolean; + parseDates?: boolean; type?: any; } export type Parser = (value: any, type?: any, options?: AutoParseOptions) => any | undefined; diff --git a/index.js b/index.js index 74acedd..b5a83d2 100644 --- a/index.js +++ b/index.js @@ -230,6 +230,52 @@ function parseMapSetString (str, options) { return null } +function parseDateTimeString (str) { + const iso = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/ + if (iso.test(str)) { + const d = new Date(str) + if (!Number.isNaN(d.getTime())) return d + } + let m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?)?$/i.exec(str) + if (m) { + let [, month, day, year, h, min, sec, ap] = m + const date = new Date(Number(year), Number(month) - 1, Number(day)) + if (h !== undefined) { + h = Number(h) + if (ap) { + ap = ap.toLowerCase() + if (ap === 'pm' && h < 12) h += 12 + if (ap === 'am' && h === 12) h = 0 + } + date.setHours(h, Number(min), Number(sec || 0), 0) + } + return date + } + m = /^(\d{1,2})-(\d{1,2})-(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(str) + if (m) { + const [, day, month, year, h, min, sec] = m + const date = new Date(Number(year), Number(month) - 1, Number(day)) + if (h !== undefined) { + date.setHours(Number(h), Number(min), Number(sec || 0), 0) + } + return date + } + m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?$/i.exec(str) + if (m) { + let [, h, min, sec, ap] = m + h = Number(h) + if (ap) { + ap = ap.toLowerCase() + if (ap === 'pm' && h < 12) h += 12 + if (ap === 'am' && h === 12) h = 0 + } + const date = new Date() + date.setHours(h, Number(min), Number(sec || 0), 0) + return date + } + return null +} + function parseExpressionString (str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -480,6 +526,10 @@ function autoParse (value, typeOrOptions) { const fn = parseFunctionString(trimmed) if (fn) return returnIfAllowed(fn, options, originalValue) } + if (options.parseDates) { + const dt = parseDateTimeString(trimmed) + if (dt) return returnIfAllowed(dt, options, originalValue) + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })) if (value === 'undefined' || value === '') { return returnIfAllowed(undefined, options, originalValue) diff --git a/package.json b/package.json index d681936..0992483 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auto-parse", - "version": "2.1.0", + "version": "2.2.0", "description": "Automatically convert any value to its best matching JavaScript type. Supports numbers, booleans, objects, arrays, BigInt, Symbol, comma-separated numbers, prefix stripping, allowed type enforcement and a plugin API.", "main": "index.js", "types": "index.d.ts", diff --git a/test/date-time.test.js b/test/date-time.test.js new file mode 100644 index 0000000..9b45bb7 --- /dev/null +++ b/test/date-time.test.js @@ -0,0 +1,29 @@ +const autoParse = require('../index.js') +const { assert } = require('chai') + +describe('Date/time parsing', function () { + it('parses ISO dates', function () { + const d = autoParse('2023-06-01T12:00:00Z', { parseDates: true }) + assert.instanceOf(d, Date) + assert.strictEqual(d.toISOString(), '2023-06-01T12:00:00.000Z') + }) + + it('parses US dates', function () { + const d = autoParse('03/10/2020', { parseDates: true }) + assert.instanceOf(d, Date) + assert.strictEqual(d.getFullYear(), 2020) + assert.strictEqual(d.getMonth(), 2) + assert.strictEqual(d.getDate(), 10) + }) + + it('parses time strings', function () { + const d = autoParse('13:45', { parseDates: true }) + assert.instanceOf(d, Date) + assert.strictEqual(d.getHours(), 13) + assert.strictEqual(d.getMinutes(), 45) + }) + + it('defaults to string when disabled', function () { + assert.strictEqual(autoParse('2023-06-01'), '2023-06-01') + }) +}) diff --git a/test/performance.test.js b/test/performance.test.js index 84a6147..9a27fc8 100644 --- a/test/performance.test.js +++ b/test/performance.test.js @@ -141,4 +141,17 @@ describe('Performance', () => { console.log('expression parse time', time) expect(time).toBeLessThan(300) }) + + test('date parse performance', () => { + for (let i = 0; i < 1000; i++) { + autoParse('2023-06-01T12:00:00Z', { parseDates: true }) + } + const time = benchmark(() => { + for (let i = 0; i < 10000; i++) { + autoParse('2023-06-01T12:00:00Z', { parseDates: true }) + } + }) + console.log('date parse time', time) + expect(time).toBeLessThan(300) + }) }) From 4aca218bc78ac76cae3a6acbf6dc5b9c7f83e1b3 Mon Sep 17 00:00:00 2001 From: Jason Humphrey Date: Fri, 6 Jun 2025 20:47:07 -0500 Subject: [PATCH 2/3] Add date parsing example --- examples/README.md | 1 + examples/dates.js | 5 +++++ package-lock.json | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 examples/dates.js diff --git a/examples/README.md b/examples/README.md index db76678..5f0d2ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,3 +6,4 @@ them with `node ` from this directory. - `basic.js` showcases parsing of primitive values and objects. - `plugin.js` illustrates registering a simple plugin. - `types.js` covers advanced types like `BigInt` and `Symbol`. +- `dates.js` demonstrates the optional date/time parsing capability. diff --git a/examples/dates.js b/examples/dates.js new file mode 100644 index 0000000..9d7e0cb --- /dev/null +++ b/examples/dates.js @@ -0,0 +1,5 @@ +const autoParse = require('..') + +console.log('ISO:', autoParse('2023-06-01T12:00:00Z', { parseDates: true })) +console.log('US:', autoParse('03/10/2020 2:30 PM', { parseDates: true })) +console.log('Time:', autoParse('13:45', { parseDates: true })) diff --git a/package-lock.json b/package-lock.json index 7102c89..6b49d19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "auto-parse", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "auto-parse", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "devDependencies": { "chai": "^4.3.4", From f3d8f288abb3b870defa25ae7718287fae9c91a0 Mon Sep 17 00:00:00 2001 From: Jason Humphrey Date: Fri, 6 Jun 2025 20:51:05 -0500 Subject: [PATCH 3/3] Add all-options example --- examples/README.md | 1 + examples/all-options.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 examples/all-options.js diff --git a/examples/README.md b/examples/README.md index 5f0d2ba..ac0d368 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,3 +7,4 @@ them with `node ` from this directory. - `plugin.js` illustrates registering a simple plugin. - `types.js` covers advanced types like `BigInt` and `Symbol`. - `dates.js` demonstrates the optional date/time parsing capability. +- `all-options.js` exercises every available option in one script. diff --git a/examples/all-options.js b/examples/all-options.js new file mode 100644 index 0000000..88b366d --- /dev/null +++ b/examples/all-options.js @@ -0,0 +1,24 @@ +const autoParse = require('..') + +process.env.TEST_ENV = '123' + +console.log('preserveLeadingZeros:', autoParse('0005', { preserveLeadingZeros: true })) +console.log('allowedTypes:', autoParse('42', { allowedTypes: ['string'] })) +console.log('stripStartChars:', autoParse('#5', { stripStartChars: '#' })) +console.log('parseCommaNumbers:', autoParse('1,234', { parseCommaNumbers: true })) +console.log('parseCurrency:', autoParse('$9.99', { parseCurrency: true })) +console.log('currencyAsObject:', autoParse('€9.99', { parseCurrency: true, currencyAsObject: true })) +console.log('currencySymbols:', autoParse('R$5', { parseCurrency: true, currencySymbols: { 'R$': 'BRL' } })) +console.log('parsePercent:', autoParse('85%', { parsePercent: true })) +console.log('percentAsObject:', autoParse('85%', { parsePercent: true, percentAsObject: true })) +console.log('parseUnits:', autoParse('10px', { parseUnits: true })) +console.log('parseRanges:', autoParse('1..3', { parseRanges: true })) +console.log('rangeAsObject:', autoParse('1..3', { parseRanges: true, rangeAsObject: true })) +console.log('booleanSynonyms:', autoParse('yes', { booleanSynonyms: true })) +console.log('parseMapSets:', autoParse('Map:[["a",1]]', { parseMapSets: true })) +console.log('parseTypedArrays:', autoParse('Uint8Array[1,2]', { parseTypedArrays: true })) +console.log('parseExpressions:', autoParse('2 + 3 * 4', { parseExpressions: true })) +console.log('parseDates:', autoParse('2023-06-01', { parseDates: true })) +console.log('expandEnv:', autoParse('$TEST_ENV', { expandEnv: true })) +console.log('parseFunctionStrings:', autoParse('x => x * 2', { parseFunctionStrings: true })(3)) +console.log('type option:', autoParse('9007199254740991', { type: BigInt }))