Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: CI

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

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
- run: npm run typecheck
4 changes: 0 additions & 4 deletions .travis.yml

This file was deleted.

31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ https://github.com/cho45/micro-strptime.js

Micro strptime implementation on JavaScript.

SYNOPSYS
[![CI](https://github.com/cho45/micro-strptime.js/actions/workflows/ci.yml/badge.svg)](https://github.com/cho45/micro-strptime.js/actions)

SYNOPSIS
========

var strptime = require('micro-strptime').strptime;
strptime('05/May/2012:09:00:00 +0900', '%d/%B/%Y:%H:%M:%S %Z');
ESM (Node.js >= 14, type: module):

```js
import { strptime } from 'micro-strptime';
const date = strptime('05/May/2012:09:00:00 +0900', '%d/%B/%Y:%H:%M:%S %Z');
```

TypeScript:

FORMAT DESCRIPTERS
```ts
import { strptime } from 'micro-strptime';
const d: Date = strptime('2020-01-01', '%Y-%m-%d');
```

FORMAT DESCRIPTORS
==================

Current supported format descripters:
Current supported format descriptors:

* %% : %
* %a : abbreviated name of day of week (just ignored)
Expand All @@ -30,11 +43,15 @@ Current supported format descripters:
* %s : milli second
* %z : timezone string like +0900 or -0300
* %Z : timezone string like '+09:00', '-03:00', 'Z' or 'UTC'.
* %I : hour (12-hour colock)
* %I : hour (12-hour clock)
* %p : AM or PM

EXTENDABILITY
=============

`strptime.fd` is intentionally defined as a mutable and accessible property so that users can override or extend the format descriptors at runtime.

LICENSE
=======

MIT: http://cho45.github.com/mit-license
MIT: http://cho45.github.com/mit-license
24 changes: 24 additions & 0 deletions lib/micro-strptime.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Type definitions for micro-strptime.js
* Project: https://github.com/cho45/micro-strptime.js
*/

/**
* Parse a date string using a strptime-like format string.
* @param str - The date string to parse.
* @param format - The format string (strptime style).
* @returns Date object (UTC)
* @throws Error if parsing fails or format is missing/invalid
*/
export function strptime(str: string, format: string): Date;

export namespace strptime {
/**
* Format descriptor table. You can override or extend this at runtime.
*/
let fd: Record<string, [string, (this: Date, matched: string) => void]>;
/**
* Month name to number mapping (Jan:0 ... Dec:11)
*/
let B: Record<string, number>;
}
43 changes: 19 additions & 24 deletions lib/micro-strptime.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,34 @@
* https://github.com/cho45/micro-strptime.js
* (c) cho45 http://cho45.github.com/mit-license
*/
function strptime (str, format) {
export function strptime (str, format) {
if (!format) throw Error("Missing format");
var ff = [];
var re = new RegExp(format.replace(/%(?:([a-zA-Z%])|('[^']+')|("[^"]+"))/g, function (_, a, b, c) {
var fd = a || b || c;
var d = strptime.fd[fd];
if (!d) throw Error("Unknown format descripter: " + fd);
ff.push(d[1]);
return '(' + d[0] + ')';
}), 'i');
var matched = str.match(re);
const ff = [];
const re = new RegExp(
format.replace(/%(?:([a-zA-Z%])|('[^']+')|("[^"]+"))/g, (_, a, b, c) => {
const fd = a || b || c;
const d = strptime.fd[fd];
if (!d) throw Error(`Unknown format descriptor: ${fd}`);
ff.push(d[1]);
return `(${d[0]})`;
}),
'i'
);
const matched = str.match(re);
if (!matched) throw Error('Failed to parse');

var date = new Date(0);
for (var i = 0, len = ff.length; i < len; i++) {
var fun = ff[i];
if (!fun) continue;
fun.call(date, matched[i + 1]);
}
if (date.utcDay){
date.setUTCDate(date.utcDay);
}
if (date.timezone) {
date = new Date(date.getTime() - date.timezone * 1000);
}
let date = new Date(0);
ff.forEach((fun, i) => fun && fun.call(date, matched[i + 1]));
if (date.utcDay) date.setUTCDate(date.utcDay);
if (date.timezone) date = new Date(date.getTime() - date.timezone * 1000);
if (date.AMPM) {
if (date.getUTCHours() == 12) date.setUTCHours(date.getUTCHours() - 12);
if (date.AMPM == 'PM') date.setUTCHours(date.getUTCHours() + 12);
}
return date;
}
// strptime.fd is intentionally defined as a mutable and accessible property
// so that users can override or extend the format descriptors at runtime.
strptime.fd = {
'%' : [ '%', function () {} ],
'a' : [ '[a-z]+', function(matched) {} ],
Expand Down Expand Up @@ -60,5 +57,3 @@ strptime.fd = {
'p' : [ 'AM|PM', function (matched) { this.AMPM = matched } ]
};
strptime.B = { "Jan": 0, "Feb": 1, "Mar": 2, "Apr": 3, "May": 4, "Jun": 5, "Jul": 6, "Aug": 7, "Sep": 8, "Oct": 9, "Nov": 10, "Dec": 11 };

this.strptime = strptime;
Empty file added lib/micro-strptime.test-d.ts
Empty file.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"test": "test"
},
"scripts": {
"test": "node test/test.js"
"test": "node test/test.js",
"typecheck": "tsc --noEmit test/micro-strptime.test-d.ts"
},
"repository": {
"type": "git",
Expand All @@ -19,5 +20,10 @@
"datetime"
],
"author": "cho45",
"license": "MIT"
"license": "MIT",
"type": "module",
"types": "lib/micro-strptime.d.ts",
"devDependencies": {
"typescript": "^5.8.3"
}
}
22 changes: 22 additions & 0 deletions test/micro-strptime.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// TypeScript type test for micro-strptime.js
type _Assert<T extends true> = T;
import { strptime } from '../lib/micro-strptime.js';

// 基本的な型チェック
const d1: Date = strptime('2020-01-01', '%Y-%m-%d');

// 型エラー: 引数不足
// @ts-expect-error
strptime('2020-01-01');

// 型エラー: 第2引数の型違い
// @ts-expect-error
strptime('2020-01-01', 123);

// strptime.fd, strptime.B の型チェック
strptime.fd['X'] = ['[0-9]+', function (this: Date, m: string) { this.setUTCFullYear(+m); }];
const n: number = strptime.B['Jan'];

// fd の型エラー: 配列長不足
// @ts-expect-error
strptime.fd['Y'] = ['[0-9]{4}'];
144 changes: 62 additions & 82 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,72 @@
#!/usr/bin/env node

var assert = require('assert');
var strptime = require('../lib/micro-strptime.js').strptime;
import test from 'node:test';
import assert from 'node:assert';
import { strptime } from '../lib/micro-strptime.js';

var anyErrors = false;
test('strptime returns Date', () => {
assert.ok(strptime('2012', '%Y') instanceof Date);
});

assert.ok(strptime('2012', '%Y') instanceof Date);
test('strptime throws on parse error', () => {
assert.throws(() => strptime('xxxx', '%Y-%m-%d'), /Failed to parse/);
assert.throws(() => strptime('xxxx'), /Missing format/);
assert.throws(() => strptime('2012', '%"unknown"'), /Unknown format descriptor: "unknown"/);
});

function assertThrows(callback, message){
try{
callback();
}catch(error){
assert.ok(error);
assert.equal(error.message, message);
return;
}
throw Error("Callback didn't throw any error.");
function testDate(strings, date) {
strings.forEach(i => {
const expectedDate = new Date(date);
let actualDate;
try {
actualDate = strptime(i.string, i.format);
assert.equal(String(actualDate), String(expectedDate));
} catch (e) {
console.log(`FAIL: ${i.format}: ${i.string}`);
console.log(`ACTUAL: ${String(actualDate)}`);
console.log(`EXPECTED: ${String(expectedDate)}`);
console.log(e);
}
});
}

assertThrows(function () {
strptime('xxxx', '%Y-%m-%d');
}, 'Failed to parse');
test('strptime date patterns', () => {
testDate([
{ string: '2012-05-05T09:00:00.00+09:00', format : '%Y-%m-%dT%H:%M:%S.%s%Z' },
{ string: '20120505000000', format : '%Y%m%d%H%M%S' },
{ string: '2012-05-05T09:00:00+09:00', format : '%Y-%m-%dT%H:%M:%S%Z' },
{ string: '2012-05-05 09:00:00+09:00', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '2012-05-05 00:00:00+00:00', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '2012-05-05 00:00:00Z', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '05/May/2012:09:00:00 +0900', format : '%d/%B/%Y:%H:%M:%S %Z' },
{ string: '05/5/2012:09:00:00 +0900', format : '%d/%m/%Y:%H:%M:%S %Z' },
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%A, %d %B %Y %H:%M:%S %Z' },
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%a, %d %b %Y %H:%M:%S %z' },
{ string: 'Sat May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' },
{ string: 'Saturday May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' }
], Date.UTC(2012, 4, 5, 0, 0, 0));
});

assertThrows(function () {
strptime('xxxx');
}, 'Missing format');
test('strptime 12-hour digital clocks', () => {
testDate([{ string: '2012-05-05 12:00:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 0, 0, 0));
testDate([{ string: '2012-05-05 12:01:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 0, 1, 0));
testDate([{ string: '2012-05-05 01:00:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 1, 0, 0));
testDate([{ string: '2012-05-05 12:00:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 12, 0, 0));
testDate([{ string: '2012-05-05 12:01:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 12, 1, 0));
testDate([{ string: '2012-05-05 01:00:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 13, 0, 0));
});

assertThrows(function () {
strptime('2012', '%"unknown"');
}, 'Unknown format descripter: "unknown"');
test('strptime 12-hour digital clocks (AM/PM前置)', () => {
testDate([{ string: '2012-05-05 AM 12:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 0, 0, 0));
testDate([{ string: '2012-05-05 AM 12:01:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 0, 1, 0));
testDate([{ string: '2012-05-05 AM 01:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 1, 0, 0));
testDate([{ string: '2012-05-05 PM 12:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 12, 0, 0));
testDate([{ string: '2012-05-05 PM 12:01:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 12, 1, 0));
testDate([{ string: '2012-05-05 PM 01:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 13, 0, 0));
});

function test (strings, date) {
strings.forEach(function (i) {
var actualDate;
var expectedDate = new Date(date);
try {
actualDate = strptime(i.string, i.format);
assert.equal(String(actualDate), String(expectedDate));
} catch (e) {
console.log("FAIL: %s: %s", i.format, i.string);
console.log("ACTUAL: " + String(actualDate));
console.log("EXPECTED: " + String(expectedDate));
console.log(e);
anyErrors = true;
}
});
}

test([
{ string: '2012-05-05T09:00:00.00+09:00', format : '%Y-%m-%dT%H:%M:%S.%s%Z' },
{ string: '20120505000000', format : '%Y%m%d%H%M%S' },
{ string: '2012-05-05T09:00:00+09:00', format : '%Y-%m-%dT%H:%M:%S%Z' },
{ string: '2012-05-05 09:00:00+09:00', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '2012-05-05 00:00:00+00:00', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '2012-05-05 00:00:00Z', format : '%Y-%m-%d %H:%M:%S%Z' },
{ string: '05/May/2012:09:00:00 +0900', format : '%d/%B/%Y:%H:%M:%S %Z' },
{ string: '05/5/2012:09:00:00 +0900', format : '%d/%m/%Y:%H:%M:%S %Z' },
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%A, %d %B %Y %H:%M:%S %Z' },
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%a, %d %b %Y %H:%M:%S %z' },
{ string: 'Sat May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' },
{ string: 'Saturday May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' }
], Date.UTC(2012, 4, 5, 0, 0, 0));

// 12-hour digital clocks format
test([{ string: '2012-05-05 12:00:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 0, 0, 0) );
test([{ string: '2012-05-05 12:01:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 0, 1, 0) );
test([{ string: '2012-05-05 01:00:00 AM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 1, 0, 0) );
test([{ string: '2012-05-05 12:00:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 12, 0, 0) );
test([{ string: '2012-05-05 12:01:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 12, 1, 0) );
test([{ string: '2012-05-05 01:00:00 PM', format : '%Y-%m-%d %I:%M:%S %p' } ], Date.UTC(2012, 4, 5, 13, 0, 0) );

test([{ string: '2012-05-05 AM 12:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 0, 0, 0) );
test([{ string: '2012-05-05 AM 12:01:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 0, 1, 0) );
test([{ string: '2012-05-05 AM 01:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 1, 0, 0) );
test([{ string: '2012-05-05 PM 12:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 12, 0, 0) );
test([{ string: '2012-05-05 PM 12:01:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 12, 1, 0) );
test([{ string: '2012-05-05 PM 01:00:00', format : '%Y-%m-%d %p %I:%M:%S' } ], Date.UTC(2012, 4, 5, 13, 0, 0) );

// Leap year
// https://github.com/cho45/micro-strptime.js/issues/2
test([
{ string: '29/Feb/2016:09:00:00 +0700', format : '%d/%B/%Y:%H:%M:%S %Z' }
], new Date("2016-02-29T02:00:00Z"));

if(anyErrors){
console.log("Tests failed.");
process.exit(1);
}else{
console.log("All tests passed.");
process.exit(0);
}
test('strptime leap year', () => {
testDate([
{ string: '29/Feb/2016:09:00:00 +0700', format : '%d/%B/%Y:%H:%M:%S %Z' }
], new Date("2016-02-29T02:00:00Z"));
});