Skip to content

Commit 760f5a5

Browse files
authored
Merge pull request #5 from cho45/modern
Modern
2 parents 09a3bb8 + a706cec commit 760f5a5

File tree

10 files changed

+207
-119
lines changed

10 files changed

+207
-119
lines changed

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 20
17+
- run: npm ci
18+
- run: npm test
19+
- run: npm run typecheck

.travis.yml

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,29 @@ https://github.com/cho45/micro-strptime.js
55

66
Micro strptime implementation on JavaScript.
77

8-
SYNOPSYS
8+
[![CI](https://github.com/cho45/micro-strptime.js/actions/workflows/ci.yml/badge.svg)](https://github.com/cho45/micro-strptime.js/actions)
9+
10+
SYNOPSIS
911
========
1012

11-
var strptime = require('micro-strptime').strptime;
12-
strptime('05/May/2012:09:00:00 +0900', '%d/%B/%Y:%H:%M:%S %Z');
13+
ESM (Node.js >= 14, type: module):
14+
15+
```js
16+
import { strptime } from 'micro-strptime';
17+
const date = strptime('05/May/2012:09:00:00 +0900', '%d/%B/%Y:%H:%M:%S %Z');
18+
```
19+
20+
TypeScript:
1321

14-
FORMAT DESCRIPTERS
22+
```ts
23+
import { strptime } from 'micro-strptime';
24+
const d: Date = strptime('2020-01-01', '%Y-%m-%d');
25+
```
26+
27+
FORMAT DESCRIPTORS
1528
==================
1629

17-
Current supported format descripters:
30+
Current supported format descriptors:
1831

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

49+
EXTENDABILITY
50+
=============
51+
52+
`strptime.fd` is intentionally defined as a mutable and accessible property so that users can override or extend the format descriptors at runtime.
3653

3754
LICENSE
3855
=======
3956

40-
MIT: http://cho45.github.com/mit-license
57+
MIT: http://cho45.github.com/mit-license

lib/micro-strptime.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Type definitions for micro-strptime.js
3+
* Project: https://github.com/cho45/micro-strptime.js
4+
*/
5+
6+
/**
7+
* Parse a date string using a strptime-like format string.
8+
* @param str - The date string to parse.
9+
* @param format - The format string (strptime style).
10+
* @returns Date object (UTC)
11+
* @throws Error if parsing fails or format is missing/invalid
12+
*/
13+
export function strptime(str: string, format: string): Date;
14+
15+
export namespace strptime {
16+
/**
17+
* Format descriptor table. You can override or extend this at runtime.
18+
*/
19+
let fd: Record<string, [string, (this: Date, matched: string) => void]>;
20+
/**
21+
* Month name to number mapping (Jan:0 ... Dec:11)
22+
*/
23+
let B: Record<string, number>;
24+
}

lib/micro-strptime.js

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,34 @@
22
* https://github.com/cho45/micro-strptime.js
33
* (c) cho45 http://cho45.github.com/mit-license
44
*/
5-
function strptime (str, format) {
5+
export function strptime (str, format) {
66
if (!format) throw Error("Missing format");
7-
var ff = [];
8-
var re = new RegExp(format.replace(/%(?:([a-zA-Z%])|('[^']+')|("[^"]+"))/g, function (_, a, b, c) {
9-
var fd = a || b || c;
10-
var d = strptime.fd[fd];
11-
if (!d) throw Error("Unknown format descripter: " + fd);
12-
ff.push(d[1]);
13-
return '(' + d[0] + ')';
14-
}), 'i');
15-
var matched = str.match(re);
7+
const ff = [];
8+
const re = new RegExp(
9+
format.replace(/%(?:([a-zA-Z%])|('[^']+')|("[^"]+"))/g, (_, a, b, c) => {
10+
const fd = a || b || c;
11+
const d = strptime.fd[fd];
12+
if (!d) throw Error(`Unknown format descriptor: ${fd}`);
13+
ff.push(d[1]);
14+
return `(${d[0]})`;
15+
}),
16+
'i'
17+
);
18+
const matched = str.match(re);
1619
if (!matched) throw Error('Failed to parse');
1720

18-
var date = new Date(0);
19-
for (var i = 0, len = ff.length; i < len; i++) {
20-
var fun = ff[i];
21-
if (!fun) continue;
22-
fun.call(date, matched[i + 1]);
23-
}
24-
if (date.utcDay){
25-
date.setUTCDate(date.utcDay);
26-
}
27-
if (date.timezone) {
28-
date = new Date(date.getTime() - date.timezone * 1000);
29-
}
21+
let date = new Date(0);
22+
ff.forEach((fun, i) => fun && fun.call(date, matched[i + 1]));
23+
if (date.utcDay) date.setUTCDate(date.utcDay);
24+
if (date.timezone) date = new Date(date.getTime() - date.timezone * 1000);
3025
if (date.AMPM) {
3126
if (date.getUTCHours() == 12) date.setUTCHours(date.getUTCHours() - 12);
3227
if (date.AMPM == 'PM') date.setUTCHours(date.getUTCHours() + 12);
3328
}
3429
return date;
3530
}
31+
// strptime.fd is intentionally defined as a mutable and accessible property
32+
// so that users can override or extend the format descriptors at runtime.
3633
strptime.fd = {
3734
'%' : [ '%', function () {} ],
3835
'a' : [ '[a-z]+', function(matched) {} ],
@@ -60,5 +57,3 @@ strptime.fd = {
6057
'p' : [ 'AM|PM', function (matched) { this.AMPM = matched } ]
6158
};
6259
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 };
63-
64-
this.strptime = strptime;

lib/micro-strptime.test-d.ts

Whitespace-only changes.

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"test": "test"
88
},
99
"scripts": {
10-
"test": "node test/test.js"
10+
"test": "node test/test.js",
11+
"typecheck": "tsc --noEmit test/micro-strptime.test-d.ts"
1112
},
1213
"repository": {
1314
"type": "git",
@@ -19,5 +20,10 @@
1920
"datetime"
2021
],
2122
"author": "cho45",
22-
"license": "MIT"
23+
"license": "MIT",
24+
"type": "module",
25+
"types": "lib/micro-strptime.d.ts",
26+
"devDependencies": {
27+
"typescript": "^5.8.3"
28+
}
2329
}

test/micro-strptime.test-d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// TypeScript type test for micro-strptime.js
2+
type _Assert<T extends true> = T;
3+
import { strptime } from '../lib/micro-strptime.js';
4+
5+
// 基本的な型チェック
6+
const d1: Date = strptime('2020-01-01', '%Y-%m-%d');
7+
8+
// 型エラー: 引数不足
9+
// @ts-expect-error
10+
strptime('2020-01-01');
11+
12+
// 型エラー: 第2引数の型違い
13+
// @ts-expect-error
14+
strptime('2020-01-01', 123);
15+
16+
// strptime.fd, strptime.B の型チェック
17+
strptime.fd['X'] = ['[0-9]+', function (this: Date, m: string) { this.setUTCFullYear(+m); }];
18+
const n: number = strptime.B['Jan'];
19+
20+
// fd の型エラー: 配列長不足
21+
// @ts-expect-error
22+
strptime.fd['Y'] = ['[0-9]{4}'];

test/test.js

Lines changed: 62 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,72 @@
11
#!/usr/bin/env node
22

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

6-
var anyErrors = false;
7+
test('strptime returns Date', () => {
8+
assert.ok(strptime('2012', '%Y') instanceof Date);
9+
});
710

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

10-
function assertThrows(callback, message){
11-
try{
12-
callback();
13-
}catch(error){
14-
assert.ok(error);
15-
assert.equal(error.message, message);
16-
return;
17-
}
18-
throw Error("Callback didn't throw any error.");
17+
function testDate(strings, date) {
18+
strings.forEach(i => {
19+
const expectedDate = new Date(date);
20+
let actualDate;
21+
try {
22+
actualDate = strptime(i.string, i.format);
23+
assert.equal(String(actualDate), String(expectedDate));
24+
} catch (e) {
25+
console.log(`FAIL: ${i.format}: ${i.string}`);
26+
console.log(`ACTUAL: ${String(actualDate)}`);
27+
console.log(`EXPECTED: ${String(expectedDate)}`);
28+
console.log(e);
29+
}
30+
});
1931
}
2032

21-
assertThrows(function () {
22-
strptime('xxxx', '%Y-%m-%d');
23-
}, 'Failed to parse');
33+
test('strptime date patterns', () => {
34+
testDate([
35+
{ string: '2012-05-05T09:00:00.00+09:00', format : '%Y-%m-%dT%H:%M:%S.%s%Z' },
36+
{ string: '20120505000000', format : '%Y%m%d%H%M%S' },
37+
{ string: '2012-05-05T09:00:00+09:00', format : '%Y-%m-%dT%H:%M:%S%Z' },
38+
{ string: '2012-05-05 09:00:00+09:00', format : '%Y-%m-%d %H:%M:%S%Z' },
39+
{ string: '2012-05-05 00:00:00+00:00', format : '%Y-%m-%d %H:%M:%S%Z' },
40+
{ string: '2012-05-05 00:00:00Z', format : '%Y-%m-%d %H:%M:%S%Z' },
41+
{ string: '05/May/2012:09:00:00 +0900', format : '%d/%B/%Y:%H:%M:%S %Z' },
42+
{ string: '05/5/2012:09:00:00 +0900', format : '%d/%m/%Y:%H:%M:%S %Z' },
43+
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%A, %d %B %Y %H:%M:%S %Z' },
44+
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%a, %d %b %Y %H:%M:%S %z' },
45+
{ string: 'Sat May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' },
46+
{ string: 'Saturday May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' }
47+
], Date.UTC(2012, 4, 5, 0, 0, 0));
48+
});
2449

25-
assertThrows(function () {
26-
strptime('xxxx');
27-
}, 'Missing format');
50+
test('strptime 12-hour digital clocks', () => {
51+
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));
52+
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));
53+
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));
54+
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));
55+
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));
56+
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));
57+
});
2858

29-
assertThrows(function () {
30-
strptime('2012', '%"unknown"');
31-
}, 'Unknown format descripter: "unknown"');
59+
test('strptime 12-hour digital clocks (AM/PM前置)', () => {
60+
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));
61+
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));
62+
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));
63+
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));
64+
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));
65+
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));
66+
});
3267

33-
function test (strings, date) {
34-
strings.forEach(function (i) {
35-
var actualDate;
36-
var expectedDate = new Date(date);
37-
try {
38-
actualDate = strptime(i.string, i.format);
39-
assert.equal(String(actualDate), String(expectedDate));
40-
} catch (e) {
41-
console.log("FAIL: %s: %s", i.format, i.string);
42-
console.log("ACTUAL: " + String(actualDate));
43-
console.log("EXPECTED: " + String(expectedDate));
44-
console.log(e);
45-
anyErrors = true;
46-
}
47-
});
48-
}
49-
50-
test([
51-
{ string: '2012-05-05T09:00:00.00+09:00', format : '%Y-%m-%dT%H:%M:%S.%s%Z' },
52-
{ string: '20120505000000', format : '%Y%m%d%H%M%S' },
53-
{ string: '2012-05-05T09:00:00+09:00', format : '%Y-%m-%dT%H:%M:%S%Z' },
54-
{ string: '2012-05-05 09:00:00+09:00', format : '%Y-%m-%d %H:%M:%S%Z' },
55-
{ string: '2012-05-05 00:00:00+00:00', format : '%Y-%m-%d %H:%M:%S%Z' },
56-
{ string: '2012-05-05 00:00:00Z', format : '%Y-%m-%d %H:%M:%S%Z' },
57-
{ string: '05/May/2012:09:00:00 +0900', format : '%d/%B/%Y:%H:%M:%S %Z' },
58-
{ string: '05/5/2012:09:00:00 +0900', format : '%d/%m/%Y:%H:%M:%S %Z' },
59-
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%A, %d %B %Y %H:%M:%S %Z' },
60-
{ string: 'Sat, 05 May 2012 09:00:00 +0900', format : '%a, %d %b %Y %H:%M:%S %z' },
61-
{ string: 'Sat May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' },
62-
{ string: 'Saturday May 05 2012 09:00:00 GMT+0900 (JST)', format : '%A %B %d %Y %H:%M:%S GMT%Z' }
63-
], Date.UTC(2012, 4, 5, 0, 0, 0));
64-
65-
// 12-hour digital clocks format
66-
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) );
67-
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) );
68-
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) );
69-
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) );
70-
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) );
71-
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) );
72-
73-
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) );
74-
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) );
75-
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) );
76-
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) );
77-
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) );
78-
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) );
79-
80-
// Leap year
81-
// https://github.com/cho45/micro-strptime.js/issues/2
82-
test([
83-
{ string: '29/Feb/2016:09:00:00 +0700', format : '%d/%B/%Y:%H:%M:%S %Z' }
84-
], new Date("2016-02-29T02:00:00Z"));
85-
86-
if(anyErrors){
87-
console.log("Tests failed.");
88-
process.exit(1);
89-
}else{
90-
console.log("All tests passed.");
91-
process.exit(0);
92-
}
68+
test('strptime leap year', () => {
69+
testDate([
70+
{ string: '29/Feb/2016:09:00:00 +0700', format : '%d/%B/%Y:%H:%M:%S %Z' }
71+
], new Date("2016-02-29T02:00:00Z"));
72+
});

0 commit comments

Comments
 (0)