Skip to content

Commit

Permalink
fix(std/datetime):: 12 and 24 support (denoland#7661)
Browse files Browse the repository at this point in the history
  • Loading branch information
timreichen committed Sep 24, 2020
1 parent 82db913 commit 9c75e48
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 59 deletions.
38 changes: 20 additions & 18 deletions std/datetime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Simple helper to help parse date strings into `Date`, with additional functions.

## Usage

The following symbols are supported:
The following symbols from
[unicode LDML](http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
are supported:

- `yyyy` - numeric year
- `yy` - 2-digit year
Expand All @@ -13,8 +15,10 @@ The following symbols are supported:
- `d` - numeric day
- `dd` - 2-digit day

- `h` - numeric hour
- `hh` - 2-digit hour
- `H` - numeric hour (0-23 hours)
- `HH` - 2-digit hour (00-23 hours)
- `h` - numeric hour (1-12 hours)
- `hh` - 2-digit hour (01-12 hours)
- `m` - numeric minute
- `mm` - 2-digit minute
- `s` - numeric second
Expand All @@ -38,10 +42,10 @@ import { parse } from 'https://deno.land/std/datetime/mod.ts'
parse("20-01-2019", "dd-MM-yyyy") // output : new Date(2019, 0, 20)
parse("2019-01-20", "yyyy-MM-dd") // output : new Date(2019, 0, 20)
parse("2019-01-20", "dd.MM.yyyy") // output : new Date(2019, 0, 20)
parse("01-20-2019 16:34", "MM-dd-yyyy hh:mm") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 16:34", "MM-dd-yyyy HH:mm") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 04:34 PM", "MM-dd-yyyy hh:mm a") // output : new Date(2019, 0, 20, 16, 34)
parse("16:34 01-20-2019", "hh:mm MM-dd-yyyy") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 16:34:23.123", "MM-dd-yyyy hh:mm:ss.SSS") // output : new Date(2019, 0, 20, 16, 34, 23, 123)
parse("16:34 01-20-2019", "HH:mm MM-dd-yyyy") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 16:34:23.123", "MM-dd-yyyy HH:mm:ss.SSS") // output : new Date(2019, 0, 20, 16, 34, 23, 123)
...
```

Expand All @@ -50,18 +54,16 @@ parse("01-20-2019 16:34:23.123", "MM-dd-yyyy hh:mm:ss.SSS") // output : new Date
Takes an input `date` and a `formatString` to format to a `string`.

```ts
import { format } from 'https://deno.land/std/datetime/mod.ts'

format(new Date(2019, 0, 20), "dd-MM-yyyy") // output : "20-01-2019"
format(new Date(2019, 0, 20), "yyyy-MM-dd") // output : "2019-01-20"
format(new Date(2019, 0, 20), "dd.MM.yyyy") // output : "2019-01-20"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy hh:mm") // output : "01-20-2019 16:34"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy hh:mm a") // output : "01-20-2019 04:34 PM"
format(new Date(2019, 0, 20, 16, 34), "hh:mm MM-dd-yyyy") // output : "16:34 01-20-2019"
format(new Date(2019, 0, 20, 16, 34, 23, 123), "MM-dd-yyyy hh:mm:ss.SSS") // output : "01-20-2019 16:34:23.123"
format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd") // output : "today: 2019-01-20"

...
import { format } from "https://deno.land/std/datetime/mod.ts";

format(new Date(2019, 0, 20), "dd-MM-yyyy"); // output : "20-01-2019"
format(new Date(2019, 0, 20), "yyyy-MM-dd"); // output : "2019-01-20"
format(new Date(2019, 0, 20), "dd.MM.yyyy"); // output : "2019-01-20"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy HH:mm"); // output : "01-20-2019 16:34"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy hh:mm a"); // output : "01-20-2019 04:34 PM"
format(new Date(2019, 0, 20, 16, 34), "HH:mm MM-dd-yyyy"); // output : "16:34 01-20-2019"
format(new Date(2019, 0, 20, 16, 34, 23, 123), "MM-dd-yyyy HH:mm:ss.SSS"); // output : "01-20-2019 16:34:23.123"
format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd"); // output : "today: 2019-01-20"
```

### dayOfYear
Expand Down
67 changes: 51 additions & 16 deletions std/datetime/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TestFunction,
TestResult,
Tokenizer,
ReceiverResult,
} from "./tokenizer.ts";

function digits(value: string | number, count = 2): string {
Expand Down Expand Up @@ -52,7 +53,7 @@ function createMatchTestFunction(match: RegExp): TestFunction {
};
}

// according to unicode symbols (http://userguide.icu-project.org/formatparse/datetime)
// according to unicode symbols (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
const defaultRules = [
{
test: createLiteralTestFunction("yyyy"),
Expand Down Expand Up @@ -81,13 +82,29 @@ const defaultRules = [
},

{
test: createLiteralTestFunction("hh"),
test: createLiteralTestFunction("HH"),
fn: (): CallbackResult => ({ type: "hour", value: "2-digit" }),
},
{
test: createLiteralTestFunction("h"),
test: createLiteralTestFunction("H"),
fn: (): CallbackResult => ({ type: "hour", value: "numeric" }),
},
{
test: createLiteralTestFunction("hh"),
fn: (): CallbackResult => ({
type: "hour",
value: "2-digit",
hour12: true,
}),
},
{
test: createLiteralTestFunction("h"),
fn: (): CallbackResult => ({
type: "hour",
value: "numeric",
hour12: true,
}),
},
{
test: createLiteralTestFunction("mm"),
fn: (): CallbackResult => ({ type: "minute", value: "2-digit" }),
Expand Down Expand Up @@ -143,27 +160,35 @@ const defaultRules = [
},
];

type FormatPart = { type: DateTimeFormatPartTypes; value: string | number };
type FormatPart = {
type: DateTimeFormatPartTypes;
value: string | number;
hour12?: boolean;
};
type Format = FormatPart[];

export class DateTimeFormatter {
#format: Format;

constructor(formatString: string, rules: Rule[] = defaultRules) {
const tokenizer = new Tokenizer(rules);
this.#format = tokenizer.tokenize(formatString, ({ type, value }) => ({
type,
value,
})) as Format;
this.#format = tokenizer.tokenize(
formatString,
({ type, value, hour12 }) => {
const result = {
type,
value,
} as unknown as ReceiverResult;
if (hour12) result.hour12 = hour12 as boolean;
return result;
},
) as Format;
}

format(date: Date, options: Options = {}): string {
let string = "";

const utc = options.timeZone === "UTC";
const hour12 = this.#format.find(
(token: FormatPart) => token.type === "dayPeriod",
);

for (const token of this.#format) {
const type = token.type;
Expand Down Expand Up @@ -225,7 +250,7 @@ export class DateTimeFormatter {
}
case "hour": {
let value = utc ? date.getUTCHours() : date.getHours();
value -= hour12 && date.getHours() > 12 ? 12 : 0;
value -= token.hour12 && date.getHours() > 12 ? 12 : 0;
switch (token.value) {
case "numeric": {
string += value;
Expand Down Expand Up @@ -290,7 +315,7 @@ export class DateTimeFormatter {
// break
}
case "dayPeriod": {
string += hour12 ? (date.getHours() >= 12 ? "PM" : "AM") : "";
string += token.value ? (date.getHours() >= 12 ? "PM" : "AM") : "";
break;
}
case "literal": {
Expand Down Expand Up @@ -377,10 +402,20 @@ export class DateTimeFormatter {
switch (token.value) {
case "numeric": {
value = /^\d{1,2}/.exec(string)?.[0] as string;
if (token.hour12 && parseInt(value) > 12) {
console.error(
`Trying to parse hour greater than 12. Use 'H' instead of 'h'.`,
);
}
break;
}
case "2-digit": {
value = /^\d{2}/.exec(string)?.[0] as string;
if (token.hour12 && parseInt(value) > 12) {
console.error(
`Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`,
);
}
break;
}
default:
Expand Down Expand Up @@ -425,9 +460,8 @@ export class DateTimeFormatter {
break;
}
case "fractionalSecond": {
value = new RegExp(`^\\d{${token.value}}`).exec(
string,
)?.[0] as string;
value = new RegExp(`^\\d{${token.value}}`).exec(string)
?.[0] as string;
break;
}
case "timeZoneName": {
Expand Down Expand Up @@ -463,6 +497,7 @@ export class DateTimeFormatter {
);
}
parts.push({ type, value });

string = string.slice(value.length);
}

Expand Down
91 changes: 69 additions & 22 deletions std/datetime/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,43 @@ Deno.test({
name: "[std/datetime] parse",
fn: () => {
assertEquals(
datetime.parse("01-03-2019 16:30", "MM-dd-yyyy hh:mm"),
datetime.parse("01-03-2019 16:30", "MM-dd-yyyy HH:mm"),
new Date(2019, 0, 3, 16, 30),
);
assertEquals(
datetime.parse("01.03.2019 16:30", "MM.dd.yyyy hh:mm"),
datetime.parse("01.03.2019 16:30", "MM.dd.yyyy HH:mm"),
new Date(2019, 0, 3, 16, 30),
);
assertEquals(
datetime.parse("03-01-2019 16:31", "dd-MM-yyyy hh:mm"),
datetime.parse("01.03.2019 16:30", "MM.dd.yyyy HH:mm"),
new Date(2019, 0, 3, 16, 30),
);
assertEquals(
datetime.parse("03-01-2019 16:31", "dd-MM-yyyy HH:mm"),
new Date(2019, 0, 3, 16, 31),
);
assertEquals(
datetime.parse("2019-01-03 16:32", "yyyy-MM-dd hh:mm"),
datetime.parse("2019-01-03 16:32", "yyyy-MM-dd HH:mm"),
new Date(2019, 0, 3, 16, 32),
);
assertEquals(
datetime.parse("16:33 01-03-2019", "hh:mm MM-dd-yyyy"),
datetime.parse("16:33 01-03-2019", "HH:mm MM-dd-yyyy"),
new Date(2019, 0, 3, 16, 33),
);
assertEquals(
datetime.parse("01-03-2019 16:33:23.123", "MM-dd-yyyy hh:mm:ss.SSS"),
datetime.parse("01-03-2019 16:33:23.123", "MM-dd-yyyy HH:mm:ss.SSS"),
new Date(2019, 0, 3, 16, 33, 23, 123),
);
assertEquals(
datetime.parse("01-03-2019 09:33 PM", "MM-dd-yyyy hh:mm a"),
datetime.parse("01-03-2019 09:33 PM", "MM-dd-yyyy HH:mm a"),
new Date(2019, 0, 3, 21, 33),
);
assertEquals(
datetime.parse("16:34 03-01-2019", "hh:mm dd-MM-yyyy"),
datetime.parse("16:34 03-01-2019", "HH:mm dd-MM-yyyy"),
new Date(2019, 0, 3, 16, 34),
);
assertEquals(
datetime.parse("16:35 2019-01-03", "hh:mm yyyy-MM-dd"),
datetime.parse("16:35 2019-01-03", "HH:mm yyyy-MM-dd"),
new Date(2019, 0, 3, 16, 35),
);
assertEquals(
Expand Down Expand Up @@ -73,30 +77,73 @@ Deno.test({
Deno.test({
name: "[std/datetime] format",
fn: () => {
// Date
assertEquals(
"2019-01-01",
datetime.format(new Date("2019-01-01T03:24:00"), "yyyy-MM-dd"),
datetime.format(new Date("2019-01-01"), "yyyy-MM-dd"),
);
assertEquals(
"01.01.2019",
datetime.format(new Date("2019-01-01T03:24:00"), "dd.MM.yyyy"),
datetime.format(new Date("2019-01-01"), "dd.MM.yyyy"),
);

// 00 hours
assertEquals(
"01:00:00",
datetime.format(new Date("2019-01-01T01:00:00"), "HH:mm:ss"),
);
assertEquals(
"13:00:00",
datetime.format(new Date("2019-01-01T13:00:00"), "HH:mm:ss"),
);

// 12 hours
assertEquals(
"01:00:00",
datetime.format(new Date("2019-01-01T01:00:00"), "hh:mm:ss"),
);
assertEquals(
"01:00:00",
datetime.format(new Date("2019-01-01T13:00:00"), "hh:mm:ss"),
);

// milliseconds
assertEquals(
"13:00:00.000",
datetime.format(new Date("2019-01-01T13:00:00"), "HH:mm:ss.SSS"),
);
assertEquals(
"13:00:00.000",
datetime.format(new Date("2019-01-01T13:00:00.000"), "HH:mm:ss.SSS"),
);
assertEquals(
"03:24:00",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss"),
"13:00:00.123",
datetime.format(new Date("2019-01-01T13:00:00.123"), "HH:mm:ss.SSS"),
);

// day period
assertEquals(
"01:00:00 AM",
datetime.format(new Date("2019-01-01T01:00:00"), "HH:mm:ss a"),
);
assertEquals(
"03:24:00.532",
datetime.format(new Date("2019-01-01T03:24:00.532"), "hh:mm:ss.SSS"),
"01:00:00 AM",
datetime.format(new Date("2019-01-01T01:00:00"), "hh:mm:ss a"),
);
assertEquals(
"03:24:00 AM",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss a"),
"01:00:00 PM",
datetime.format(new Date("2019-01-01T13:00:00"), "hh:mm:ss a"),
);
assertEquals(
"09:24:00 PM",
datetime.format(new Date("2019-01-01T21:24:00"), "hh:mm:ss a"),
"21:00:00 PM",
datetime.format(new Date("2019-01-01T21:00:00"), "HH:mm:ss a"),
);
assertEquals(
"09:00:00 PM",
datetime.format(new Date("2019-01-01T21:00:00"), "hh:mm:ss a"),
);

// quoted literal
assertEquals(
datetime.format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd"),
"today: 2019-01-20",
Expand Down Expand Up @@ -181,9 +228,9 @@ Deno.test({
Deno.test({
name: "[std/datetime] weekOfYear",
fn: () => {
assertEquals(datetime.weekOfYear(new Date("2020-01-05T03:24:00")), 1);
assertEquals(datetime.weekOfYear(new Date("2020-12-28T03:24:00")), 53); // 53 weeks in 2020
assertEquals(datetime.weekOfYear(new Date("2020-06-28T03:24:00")), 26);
assertEquals(datetime.weekOfYear(new Date("2020-01-05T03:00:00")), 1);
assertEquals(datetime.weekOfYear(new Date("2020-12-28T03:00:00")), 53); // 53 weeks in 2020
assertEquals(datetime.weekOfYear(new Date("2020-06-28T03:00:00")), 26);

// iso weeks year starting sunday
assertEquals(datetime.weekOfYear(new Date(2012, 0, 1)), 52);
Expand Down
11 changes: 8 additions & 3 deletions std/datetime/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ export type Token = {
type: string;
value: string | number;
index: number;
[key: string]: unknown;
};

interface ReceiverResult {
[name: string]: string | number;
export interface ReceiverResult {
[name: string]: string | number | unknown;
}
export type CallbackResult = { type: string; value: string | number };
export type CallbackResult = {
type: string;
value: string | number;
[key: string]: unknown;
};
type CallbackFunction = (value: unknown) => CallbackResult;

export type TestResult = { value: unknown; length: number } | undefined;
Expand Down

0 comments on commit 9c75e48

Please sign in to comment.