Skip to content

Commit 5249202

Browse files
committed
Initial @axhxrx/date as JSR package
0 parents  commit 5249202

File tree

9 files changed

+266
-0
lines changed

9 files changed

+266
-0
lines changed

.dprint.jsonc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"typescript": {
3+
"whileStatement.bracePosition": "nextLine",
4+
"arrayExpression.trailingCommas": "onlyMultiLine",
5+
"quoteStyle": "preferSingle",
6+
"quoteProps": "asNeeded",
7+
"bracePosition": "nextLine",
8+
"preferHanging": true,
9+
"nextControlFlowPosition": "nextLine"
10+
},
11+
"plugins": [
12+
"https://plugins.dprint.dev/typescript-0.88.8.wasm"
13+
]
14+
}

assertNever.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it } from 'https://deno.land/[email protected]/testing/bdd.ts';
2+
import { assertEquals, assertThrows } from 'jsr:@std/assert';
3+
import { assertNever } from "./assertNever.ts";
4+
5+
describe('assertNever', () => {
6+
it('should exist', () => {
7+
assertEquals(typeof assertNever, 'function');
8+
});
9+
10+
it('should work', () => {
11+
let state: 'happy' | 'sad' | 'mad';
12+
let result = '🤔';
13+
14+
state = 'happy' as unknown as 'happy' | 'sad' | 'mad';
15+
state = 'mad' as unknown as 'happy' | 'sad' | 'mad';
16+
17+
switch (state) {
18+
case 'happy':
19+
result = '😀';
20+
break;
21+
case 'sad':
22+
result = '😢';
23+
break;
24+
case 'mad':
25+
result = '😠';
26+
break;
27+
default:
28+
assertNever(state);
29+
}
30+
31+
const errFragment = 'Should never happen: mad';
32+
33+
const invalidNonexhaustiveSwitch = () => {
34+
switch (state) {
35+
case 'happy':
36+
result = '😀';
37+
break;
38+
default:
39+
// @ts-expect-error Argument of type '"sad" | "mad"' is not assignable to parameter of type 'undefined'.
40+
assertNever(state);
41+
}
42+
};
43+
44+
assertThrows(invalidNonexhaustiveSwitch, Error, errFragment);
45+
46+
assertEquals(result, '😠');
47+
});
48+
});

assertNever.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
Utility function for [exhaustiveness checking](http://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checkin). Use it to get compile-time errors for non-exhaustive `switch` statements. Example:
3+
4+
```ts
5+
let state: "happy" | "sad" | "mad";
6+
switch(state) {
7+
case "happy": return "😀";
8+
case "sad": return "😢";
9+
// case "mad" : return "😠";
10+
default:
11+
assertNever(state);
12+
// ERROR:
13+
// Argument of type '"mad"' is not assignable
14+
// to parameter of type 'never'.ts(2345)
15+
//
16+
// Uncomment the third case to fix the error by making the switch exhaustive.
17+
}
18+
```
19+
*/
20+
export const assertNever = (x?: never): never =>
21+
{
22+
throw new Error('Should never happen: ' + x);
23+
};

dateToFormat.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { assertNever } from './assertNever.ts';
2+
import { dateToIS08601WithTimeZoneOffset } from './dateToIS08601WithTimeZoneOffset.ts';
3+
4+
/**
5+
All supported date format strings. This only includes formats we actually use.
6+
*/
7+
type DateStringFormat =
8+
| 'YYYY-MM-DD'
9+
| 'YYYY-MM-DD HH:mm'
10+
| 'YYYY-MM-DD HH:mm:ss'
11+
| 'YYYYMMDD'; // because sometimes... you know :-/
12+
13+
/**
14+
Returns a string expressing a date (the current date, unless otherwise specified) in a particular format.
15+
*/
16+
export const dateToFormat = (format: DateStringFormat, date?: Date) =>
17+
{
18+
const fullDateString = dateToIS08601WithTimeZoneOffset(date);
19+
const [dateString, timeString] = fullDateString.split('T');
20+
const [year, month, day] = dateString.split('-');
21+
22+
const timeComponents = timeString.split(':');
23+
const hour = timeComponents[0];
24+
const minute = timeComponents[1];
25+
const second = timeComponents[2].substring(0, 2);
26+
// const _twoDigitYear = year.slice(2); // NEVER!
27+
const twoDigitMonth = month;
28+
const twoDigitDay = day;
29+
const twoDigitHour = hour;
30+
const twoDigitMinute = minute;
31+
const twoDigitSecond = second;
32+
33+
switch (format)
34+
{
35+
case 'YYYY-MM-DD':
36+
return `${year}-${twoDigitMonth}-${twoDigitDay}`;
37+
case 'YYYY-MM-DD HH:mm':
38+
return `${year}-${twoDigitMonth}-${twoDigitDay} ${twoDigitHour}:${twoDigitMinute}`;
39+
case 'YYYY-MM-DD HH:mm:ss':
40+
return `${year}-${twoDigitMonth}-${twoDigitDay} ${twoDigitHour}:${twoDigitMinute}:${twoDigitSecond}`;
41+
case 'YYYYMMDD':
42+
return `${year}${twoDigitMonth}${twoDigitDay}`;
43+
default:
44+
assertNever(format);
45+
throw new Error(
46+
`DateUtils.to(): invariant violation: illegal format: ${format}`,
47+
);
48+
}
49+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { assertEquals } from 'jsr:@std/assert';
2+
import { dateToIS08601WithTimeZoneOffset } from "./dateToIS08601WithTimeZoneOffset.ts";
3+
4+
const testTimezoneOffset = -540; // JST
5+
6+
7+
Deno.test('format a valid date', () =>
8+
{
9+
const date = new Date('2023-10-01T12:00:00Z');
10+
const result = dateToIS08601WithTimeZoneOffset(date, testTimezoneOffset);
11+
assertEquals(result, '2023-10-01T21:00:00+09:00');
12+
});
13+
14+
Deno.test('format date with positive non-hour offset', () =>
15+
{
16+
const date = new Date('2023-10-01T12:00:00+05:30');
17+
const result = dateToIS08601WithTimeZoneOffset(date, testTimezoneOffset);
18+
assertEquals(result, '2023-10-01T15:30:00+09:00');
19+
});
20+
21+
Deno.test('format date with negative offset', () =>
22+
{
23+
const date = new Date('2023-10-01T12:00:00-04:00');
24+
const result = dateToIS08601WithTimeZoneOffset(date, testTimezoneOffset);
25+
assertEquals(result, '2023-10-02T01:00:00+09:00');
26+
});
27+
28+
Deno.test('return ERR_INVALID_DATE for invalid JS Date object', () =>
29+
{
30+
const date = new Date('invalid-date-string');
31+
const result = dateToIS08601WithTimeZoneOffset(date, testTimezoneOffset);
32+
assertEquals(result, 'ERR_INVALID_DATE');
33+
});
34+
35+
Deno.test('format ignoring milliseconds', () =>
36+
{
37+
const date = new Date('2023-10-01T12:00:00.123Z');
38+
const result = dateToIS08601WithTimeZoneOffset(date, testTimezoneOffset);
39+
assertEquals(result, '2023-10-01T21:00:00+09:00');
40+
});
41+

dateToIS08601WithTimeZoneOffset.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Render an [ISO 8601])(https://xkcd.com/1179/) style date in the browser's local timezone,
3+
* e.g. `2021-03-06T16:45:14+09:00`.
4+
*
5+
* This was originally developed to have an unambiguous and machine-parseable (but still fairly human-friendly) date-formatting mechanism for viewing and copying log data.
6+
*
7+
* However, it might be generally useful any time you want an ISO 8601 date.
8+
*/
9+
export const dateToIS08601WithTimeZoneOffset = (
10+
date: Date = new Date(),
11+
timeZoneOffset?: number,
12+
) =>
13+
{
14+
// We can't do better than this in JS, if we don't want to throw an error...
15+
if (isNaN(date.getTime())) {
16+
return "ERR_INVALID_DATE";
17+
}
18+
19+
// Thanks, Obama! https://stackoverflow.com/questions/17415579/how-to-iso-8601-format-a-date-with-timezone-offset-in-javascript
20+
21+
const offset = timeZoneOffset ?? date.getTimezoneOffset();
22+
23+
const tzo = -offset;
24+
const dif = tzo >= 0 ? '+' : '-';
25+
const pad = (num: number) =>
26+
{
27+
const norm = Math.floor(Math.abs(num));
28+
return (norm < 10 ? '0' : '') + norm;
29+
};
30+
31+
return (
32+
date.getFullYear()
33+
+ '-'
34+
+ pad(date.getMonth() + 1)
35+
+ '-'
36+
+ pad(date.getDate())
37+
+ 'T'
38+
+ pad(date.getHours())
39+
+ ':'
40+
+ pad(date.getMinutes())
41+
+ ':'
42+
+ pad(date.getSeconds())
43+
+ dif
44+
+ pad(tzo / 60)
45+
+ ':'
46+
+ pad(tzo % 60)
47+
);
48+
};

deno.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@axhxrx/date",
3+
"version": "0.1.0",
4+
"exports": "./mod.ts",
5+
"tasks": {
6+
"dev": "deno run --watch main.ts"
7+
}
8+
}

deno.lock

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

main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { dateToFormat } from "./dateToFormat.ts";
2+
3+
if (import.meta.main) {
4+
console.log(dateToFormat("YYYY-MM-DD HH:mm:ss"));
5+
}

0 commit comments

Comments
 (0)