From 7bc8ce03d5865e96106e26783d0bb95f204b2fff Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 10 Jul 2024 23:52:12 +0200 Subject: [PATCH 1/8] initial commit --- datetime/_date_time_formatter.ts | 2 +- datetime/_date_time_formatter_test.ts | 42 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index 8d38d1007036..5b5dc6ea50c9 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -544,7 +544,7 @@ export class DateTimeFormatter { break; } case "dayPeriod": { - value = /^(A|P)M/.exec(string)?.[0] as string; + value = /^(A|P)\.?M\.?/i.exec(string)?.[0] as string; break; } case "literal": { diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index add91aaa63eb..ec552b6afec7 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -284,6 +284,48 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { ]), +date, ); + assertEquals( + +formatter.partsToDate([ + { type: "year", value: "2020" }, + { type: "month", value: "01" }, + { type: "day", value: "01" }, + { type: "hour", value: "00" }, + { type: "minute", value: "00" }, + { type: "second", value: "00" }, + { type: "fractionalSecond", value: "000" }, + { type: "dayPeriod", value: "am" }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "year", value: "2020" }, + { type: "month", value: "01" }, + { type: "day", value: "01" }, + { type: "hour", value: "00" }, + { type: "minute", value: "00" }, + { type: "second", value: "00" }, + { type: "fractionalSecond", value: "000" }, + { type: "dayPeriod", value: "a.m." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "year", value: "2020" }, + { type: "month", value: "01" }, + { type: "day", value: "01" }, + { type: "hour", value: "00" }, + { type: "minute", value: "00" }, + { type: "second", value: "00" }, + { type: "fractionalSecond", value: "000" }, + { type: "dayPeriod", value: "am." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); }); Deno.test("dateTimeFormatter.partsToDate() sets utc", () => { From 413b8a0d36935dd8b10e20470ba63c1e96c0ebd6 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Aug 2024 11:12:40 +0200 Subject: [PATCH 2/8] update --- datetime/_date_time_formatter.ts | 158 +++++++++++++++++++------- datetime/_date_time_formatter_test.ts | 102 +++++++++++++++++ 2 files changed, 222 insertions(+), 38 deletions(-) diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index 5b5dc6ea50c9..528aba05e4ed 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -401,23 +401,38 @@ export class DateTimeFormatter { return string; } + #throwInvalidValueError(type: string, value: string, string: string) { + if (!value) { + throw Error( + `value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`, + ); + } + } parseToParts(string: string): DateTimeFormatPart[] { const parts: DateTimeFormatPart[] = []; + let index = 0; for (const token of this.#format) { + const substring = string.slice(index); const type = token.type; let value = ""; - switch (token.type) { + switch (type) { case "year": { switch (token.value) { case "numeric": { - value = /^\d{1,4}/.exec(string)?.[0] as string; + value = /^\d{1,4}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "2-digit": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } default: @@ -430,23 +445,38 @@ export class DateTimeFormatter { case "month": { switch (token.value) { case "numeric": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "2-digit": { - value = /^\d{2}/.exec(string)?.[0] as string; + value = /^\d{2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "narrow": { - value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "short": { - value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "long": { - value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } default: @@ -459,11 +489,17 @@ export class DateTimeFormatter { case "day": { switch (token.value) { case "numeric": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "2-digit": { - value = /^\d{2}/.exec(string)?.[0] as string; + value = /^\d{2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } default: @@ -476,7 +512,10 @@ export class DateTimeFormatter { case "hour": { switch (token.value) { case "numeric": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; if (token.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'H' instead of 'h'.`, @@ -485,7 +524,10 @@ export class DateTimeFormatter { break; } case "2-digit": { - value = /^\d{2}/.exec(string)?.[0] as string; + value = /^\d{2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; if (token.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`, @@ -503,11 +545,17 @@ export class DateTimeFormatter { case "minute": { switch (token.value) { case "numeric": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "2-digit": { - value = /^\d{2}/.exec(string)?.[0] as string; + value = /^\d{2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } default: @@ -520,11 +568,17 @@ export class DateTimeFormatter { case "second": { switch (token.value) { case "numeric": { - value = /^\d{1,2}/.exec(string)?.[0] as string; + value = /^\d{1,2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "2-digit": { - value = /^\d{2}/.exec(string)?.[0] as string; + value = /^\d{2}/.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } default: @@ -535,50 +589,61 @@ export class DateTimeFormatter { break; } case "fractionalSecond": { - value = new RegExp(`^\\d{${token.value}}`).exec(string) + value = new RegExp(`^\\d{${token.value}}`).exec(substring) ?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "timeZoneName": { value = token.value as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } case "dayPeriod": { - value = /^(A|P)\.?M\.?/i.exec(string)?.[0] as string; + value = /^[AP](?:\.M\.|M\.?)/i.exec(substring)?.[0] as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + switch (value.toUpperCase()) { + case "AM": + case "AM.": + case "A.M.": + value = "AM"; + break; + case "PM": + case "PM.": + case "P.M.": + value = "PM"; + break; + default: + throw new Error(`dayPeriod '${value}' is not supported.`); + } + parts.push({ type, value }); + index += value.length; break; } case "literal": { - if (!string.startsWith(token.value as string)) { + if (!substring.startsWith(token.value as string)) { throw Error( - `Literal "${token.value}" not found "${string.slice(0, 25)}"`, + `Literal "${token.value}" not found "${substring.slice(0, 25)}"`, ); } value = token.value as string; + if (!value) this.#throwInvalidValueError(type, value, substring); + parts.push({ type, value }); + index += value.length; break; } - default: throw Error(`${token.type} ${token.value}`); } - - if (!value) { - throw Error( - `value not valid for token { ${type} ${value} } ${ - string.slice( - 0, - 25, - ) - }`, - ); - } - parts.push({ type, value }); - - string = string.slice(value.length); } - if (string.length) { + if (index < string.length) { throw Error( - `datetime string was not fully parsed! ${string.slice(0, 25)}`, + `datetime string was not fully parsed! ${string.slice(index)}`, ); } @@ -644,7 +709,24 @@ export class DateTimeFormatter { const dayPeriod = parts.find( (part: DateTimeFormatPart) => part.type === "dayPeriod", ); - if (dayPeriod?.value === "PM") value += 12; + if (dayPeriod) { + switch (dayPeriod.value.toUpperCase()) { + case "AM": + case "AM.": + case "A.M.": + // ignore + break; + case "PM": + case "PM.": + case "P.M.": + value += 12; + break; + default: + throw new Error( + `dayPeriod '${dayPeriod.value}' is not supported.`, + ); + } + } utc ? date.setUTCHours(value) : date.setHours(value); break; } diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index ec552b6afec7..a033022f3e80 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -124,7 +124,14 @@ Deno.test("dateTimeFormatter.format()", () => { new Date(2020, 0, 1, 23, 59, 59), "2020-01-01 23:59:59 PM", ], + [ + "yyyy-MM-dd hh:mm:ss a", + new Date(2020, 0, 1, 23, 59, 59), + "2020-01-01 11:59:59 PM", + ], ["yyyy-MM-dd a", new Date(2020, 0, 1), "2020-01-01 AM"], + ["yyyy-MM-dd HH:mm:ss a", new Date(2020, 0, 1), "2020-01-01 00:00:00 AM"], + ["yyyy-MM-dd hh:mm:ss a", new Date(2020, 0, 1), "2020-01-01 12:00:00 AM"], ["yyyy", new Date(2020, 0, 1), "2020"], ["MM", new Date(2020, 0, 1), "01"], ] as const; @@ -327,6 +334,101 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { +date, ); }); +Deno.test("dateTimeFormatter.partsToDate() works with dayPeriod", () => { + const date = new Date("2020-01-01T00:00:00.000Z"); + using _time = new FakeTime(date); + const format = "HH a"; + const formatter = new DateTimeFormatter(format); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "AM" }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "AM." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "A.M." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "am" }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "am." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "a.m." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); +}); +Deno.test("dateTimeFormatter.partsToDate() throws with invalid dayPeriods", () => { + const date = new Date("2020-01-01T00:00:00.000Z"); + using _time = new FakeTime(date); + const format = "HH a"; + const formatter = new DateTimeFormatter(format); + assertThrows(() => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "A.M" }, + { type: "timeZoneName", value: "UTC" }, + ]) + ); + assertThrows(() => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "a.m" }, + { type: "timeZoneName", value: "UTC" }, + ]) + ); + assertThrows(() => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "P.M" }, + { type: "timeZoneName", value: "UTC" }, + ]) + ); + assertThrows(() => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "p.m" }, + { type: "timeZoneName", value: "UTC" }, + ]) + ); + assertThrows(() => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "noon" }, + { type: "timeZoneName", value: "UTC" }, + ]) + ); +}); Deno.test("dateTimeFormatter.partsToDate() sets utc", () => { const date = new Date("2020-01-01T00:00:00.000Z"); From cbae8478dade6963934c501fa3228506f8350365 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Aug 2024 11:17:27 +0200 Subject: [PATCH 3/8] update --- datetime/_date_time_formatter.ts | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index 528aba05e4ed..e0d6f3341686 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -231,6 +231,13 @@ type FormatPart = { }; type Format = FormatPart[]; +function throwInvalidValueError(type: string, value: string, string: string) { + if (!value) { + throw Error( + `value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`, + ); + } +} export class DateTimeFormatter { #format: Format; @@ -401,13 +408,6 @@ export class DateTimeFormatter { return string; } - #throwInvalidValueError(type: string, value: string, string: string) { - if (!value) { - throw Error( - `value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`, - ); - } - } parseToParts(string: string): DateTimeFormatPart[] { const parts: DateTimeFormatPart[] = []; @@ -423,14 +423,14 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,4}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "2-digit": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; @@ -446,35 +446,35 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "2-digit": { value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "narrow": { value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "short": { value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "long": { value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; @@ -490,14 +490,14 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "2-digit": { value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; @@ -513,7 +513,7 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; if (token.hour12 && parseInt(value) > 12) { @@ -525,7 +525,7 @@ export class DateTimeFormatter { } case "2-digit": { value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; if (token.hour12 && parseInt(value) > 12) { @@ -546,14 +546,14 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "2-digit": { value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; @@ -569,14 +569,14 @@ export class DateTimeFormatter { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "2-digit": { value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; @@ -591,21 +591,21 @@ export class DateTimeFormatter { case "fractionalSecond": { value = new RegExp(`^\\d{${token.value}}`).exec(substring) ?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "timeZoneName": { value = token.value as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } case "dayPeriod": { value = /^[AP](?:\.M\.|M\.?)/i.exec(substring)?.[0] as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); switch (value.toUpperCase()) { case "AM": case "AM.": @@ -631,7 +631,7 @@ export class DateTimeFormatter { ); } value = token.value as string; - if (!value) this.#throwInvalidValueError(type, value, substring); + if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; From 0d5ee624ed2fd2e16a847e5b6642c4acb4e42b67 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Aug 2024 19:25:36 +0200 Subject: [PATCH 4/8] Merge branch 'main' into datetime-fix-am/pm-variants --- .github/dependency_graph.svg | 72 +-- _tools/check_circular_package_dependencies.ts | 2 + _tools/check_docs.ts | 2 + cache/_serialize_arg_list.ts | 87 +++ cache/_serialize_arg_list_test.ts | 184 ++++++ cache/deno.json | 9 + cache/lru_cache.ts | 151 +++++ cache/lru_cache_test.ts | 24 + cache/memoize.ts | 144 +++++ cache/memoize_test.ts | 561 ++++++++++++++++++ cache/mod.ts | 26 + datetime/_date_time_formatter.ts | 189 +++--- datetime/_date_time_formatter_test.ts | 14 +- datetime/format.ts | 13 +- datetime/format_test.ts | 4 +- datetime/parse.ts | 4 +- deno.json | 1 + io/buf_reader.ts | 306 +++++++++- io/buf_writer.ts | 397 ++++++++++++- io/buffer.ts | 277 ++++++++- io/copy.ts | 4 +- io/copy_n.ts | 13 + io/iterate_reader.ts | 38 +- io/limited_reader.ts | 81 ++- io/mod.ts | 7 + io/multi_reader.ts | 54 +- io/read_all.ts | 14 +- io/read_delim.ts | 18 +- io/read_int.ts | 17 +- io/read_lines.ts | 15 +- io/read_long.ts | 19 +- io/read_range.ts | 16 +- io/read_short.ts | 17 +- io/read_string_delim.ts | 16 +- io/reader_from_stream_reader.ts | 10 +- io/slice_long_to_bytes.ts | 15 +- io/string_reader.ts | 26 +- io/string_writer.ts | 67 ++- io/to_readable_stream.ts | 19 +- io/to_writable_stream.ts | 13 +- io/write_all.ts | 42 +- 41 files changed, 2681 insertions(+), 307 deletions(-) create mode 100644 cache/_serialize_arg_list.ts create mode 100644 cache/_serialize_arg_list_test.ts create mode 100644 cache/deno.json create mode 100644 cache/lru_cache.ts create mode 100644 cache/lru_cache_test.ts create mode 100644 cache/memoize.ts create mode 100644 cache/memoize_test.ts create mode 100644 cache/mod.ts diff --git a/.github/dependency_graph.svg b/.github/dependency_graph.svg index dba8b04d4fa2..241ff2651dbc 100644 --- a/.github/dependency_graph.svg +++ b/.github/dependency_graph.svg @@ -63,32 +63,38 @@ async - + +cache + +cache + + + cli cli - + collections collections - + crypto crypto - + csv csv - + streams streams @@ -106,32 +112,32 @@ - + data-\nstructures data- structures - + datetime datetime - + dotenv dotenv - + encoding encoding - + expect expect @@ -149,20 +155,20 @@ - + fmt fmt - + front-\nmatter front- matter - + toml toml @@ -174,7 +180,7 @@ - + yaml yaml @@ -192,13 +198,13 @@ - + fs fs - + path path @@ -210,13 +216,13 @@ - + html html - + http http @@ -252,7 +258,7 @@ - + media-\ntypes media- @@ -265,7 +271,7 @@ - + net net @@ -277,13 +283,13 @@ - + ini ini - + json json @@ -295,7 +301,7 @@ - + jsonc jsonc @@ -307,7 +313,7 @@ - + log log @@ -331,7 +337,7 @@ - + msgpack msgpack @@ -343,19 +349,19 @@ - + regexp regexp - + semver semver - + testing testing @@ -397,19 +403,19 @@ - + text text - + ulid ulid - + url url @@ -421,7 +427,7 @@ - + uuid uuid @@ -439,7 +445,7 @@ - + webgpu webgpu diff --git a/_tools/check_circular_package_dependencies.ts b/_tools/check_circular_package_dependencies.ts index 79bb26cf6fad..9c7203ca7e36 100644 --- a/_tools/check_circular_package_dependencies.ts +++ b/_tools/check_circular_package_dependencies.ts @@ -39,6 +39,7 @@ type Mod = | "assert" | "async" | "bytes" + | "cache" | "cli" | "collections" | "crypto" @@ -80,6 +81,7 @@ const ENTRYPOINTS: Record = { assert: ["mod.ts"], async: ["mod.ts"], bytes: ["mod.ts"], + cache: ["mod.ts"], cli: ["mod.ts"], collections: ["mod.ts"], crypto: ["mod.ts"], diff --git a/_tools/check_docs.ts b/_tools/check_docs.ts index 95ee6875101f..e5e83dcfc3b7 100644 --- a/_tools/check_docs.ts +++ b/_tools/check_docs.ts @@ -31,6 +31,7 @@ const ENTRY_POINTS = [ "../assert/mod.ts", "../async/mod.ts", "../bytes/mod.ts", + "../cache/mod.ts", "../cli/mod.ts", "../crypto/mod.ts", "../collections/mod.ts", @@ -50,6 +51,7 @@ const ENTRY_POINTS = [ "../http/mod.ts", "../ini/mod.ts", "../internal/mod.ts", + "../io/mod.ts", "../json/mod.ts", "../jsonc/mod.ts", "../media_types/mod.ts", diff --git a/cache/_serialize_arg_list.ts b/cache/_serialize_arg_list.ts new file mode 100644 index 000000000000..43e0395bfd5d --- /dev/null +++ b/cache/_serialize_arg_list.ts @@ -0,0 +1,87 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import type { MemoizationCache } from "./memoize.ts"; + +/** + * Default serialization of arguments list for use as cache keys. Equivalence + * follows [`SameValueZero`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero) + * reference equality, such that `getKey(x, y) === getKey(x, y)` for all values + * of `x` and `y`, but `getKey({}) !== getKey({})`. + * + * @param cache The cache for which the keys will be used. + * @returns `getKey`, the function for getting cache keys. + */ + +export function _serializeArgList( + cache: MemoizationCache, +): (this: unknown, ...args: unknown[]) => string { + const weakKeyToKeySegmentCache = new WeakMap(); + const weakKeySegmentToKeyCache = new Map(); + let i = 0; + + const registry = new FinalizationRegistry((keySegment) => { + for (const key of weakKeySegmentToKeyCache.get(keySegment) ?? []) { + cache.delete(key); + } + weakKeySegmentToKeyCache.delete(keySegment); + }); + + return function getKey(...args) { + const weakKeySegments: string[] = []; + const keySegments = [this, ...args].map((arg) => { + if (typeof arg === "undefined") return "undefined"; + if (typeof arg === "bigint") return `${arg}n`; + + if (typeof arg === "number") { + return String(arg); + } + + if ( + arg === null || + typeof arg === "string" || + typeof arg === "boolean" + ) { + // This branch will need to be updated if further types are added to + // the language that support value equality, + // e.g. https://github.com/tc39/proposal-record-tuple + return JSON.stringify(arg); + } + + try { + assertWeakKey(arg); + } catch { + if (typeof arg === "symbol") { + return `Symbol.for(${JSON.stringify(arg.description)})`; + } + // Non-weak keys other than `Symbol.for(...)` are handled by the branches above. + throw new Error( + "Should be unreachable. Please open an issue at https://github.com/denoland/std/issues/new", + ); + } + + if (!weakKeyToKeySegmentCache.has(arg)) { + const keySegment = `{${i++}}`; + weakKeySegments.push(keySegment); + registry.register(arg, keySegment); + weakKeyToKeySegmentCache.set(arg, keySegment); + } + + const keySegment = weakKeyToKeySegmentCache.get(arg)!; + weakKeySegments.push(keySegment); + return keySegment; + }); + + const key = keySegments.join(","); + + for (const keySegment of weakKeySegments) { + const keys = weakKeySegmentToKeyCache.get(keySegment) ?? []; + keys.push(key); + weakKeySegmentToKeyCache.set(keySegment, keys); + } + + return key; + }; +} + +function assertWeakKey(arg: unknown): asserts arg is WeakKey { + new WeakRef(arg as WeakKey); +} diff --git a/cache/_serialize_arg_list_test.ts b/cache/_serialize_arg_list_test.ts new file mode 100644 index 000000000000..f44f16b81e0f --- /dev/null +++ b/cache/_serialize_arg_list_test.ts @@ -0,0 +1,184 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { assertEquals } from "@std/assert"; +import { _serializeArgList } from "./_serialize_arg_list.ts"; +import { delay } from "@std/async"; + +Deno.test("_serializeArgList() serializes simple numbers", () => { + const getKey = _serializeArgList(new Map()); + assertEquals(getKey(1), "undefined,1"); + assertEquals(getKey(1, 2), "undefined,1,2"); + assertEquals(getKey(1, 2, 3), "undefined,1,2,3"); +}); + +Deno.test("_serializeArgList() serializes reference types", () => { + const getKey = _serializeArgList(new Map()); + const obj = {}; + const arr: [] = []; + const sym = Symbol("xyz"); + + assertEquals(getKey(obj), "undefined,{0}"); + assertEquals(getKey(obj, obj), "undefined,{0},{0}"); + + assertEquals(getKey(arr), "undefined,{1}"); + assertEquals(getKey(sym), "undefined,{2}"); + assertEquals( + getKey(obj, arr, sym), + "undefined,{0},{1},{2}", + ); +}); + +Deno.test("_serializeArgList() gives same results as SameValueZero algorithm", async (t) => { + /** + * [`SameValueZero`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero), + * used by [`Set`](https://tc39.es/ecma262/multipage/keyed-collections.html#sec-set-objects): + * + * > Distinct values are discriminated using the SameValueZero comparison algorithm. + */ + const sameValueZero = (x: unknown, y: unknown) => new Set([x, y]).size === 1; + + const getKey = _serializeArgList(new Map()); + + const values = [ + 1, + "1", + '"1"', + 1n, + 0, + -0, + 0n, + true, + "true", + null, + undefined, + Infinity, + -Infinity, + NaN, + {}, + {}, + Symbol("x"), + Symbol.for("x"), + ]; + + await t.step("Serialization of values", () => { + assertEquals( + getKey(...values), + 'undefined,1,"1","\\"1\\"",1n,0,0,0n,true,"true",null,undefined,Infinity,-Infinity,NaN,{0},{1},{2},Symbol.for("x")', + ); + }); + + await t.step("Gives consistent serialization for each value", () => { + for (const x of values) { + assertEquals(getKey(x), getKey(x)); + } + }); + + await t.step("Gives same equivalence for each pair of values", () => { + for (const x of values) { + for (const y of values) { + const expectedEquivalence = sameValueZero(x, y); + const actualEquivalence = getKey(x) === getKey(y); + assertEquals(actualEquivalence, expectedEquivalence); + } + } + }); +}); + +Deno.test("_serializeArgList() discriminates on `this` arg", () => { + const getKey = _serializeArgList(new Map()); + const obj1 = {}; + const obj2 = {}; + + assertEquals(getKey(), "undefined"); + assertEquals(getKey.call(obj1), "{0}"); + assertEquals(getKey.call(obj2), "{1}"); + assertEquals(getKey.call(obj1, obj2), "{0},{1}"); +}); + +Deno.test("_serializeArgList() allows garbage collection for weak keys", async () => { + // @ts-expect-error - Triggering true garbage collection is only available + // with `--v8-flags="--expose-gc"`, so we mock `FinalizationRegistry` with + // `using` and some `Symbol.dispose` trickery if it's not available. Run this + // test with `deno test --v8-flags="--expose-gc"` to test actual gc behavior + // (however, even calling `globalThis.gc` doesn't _guarantee_ garbage + // collection, so this may be flaky between v8 versions etc.) + const gc = globalThis.gc as undefined | (() => void); + + class MockFinalizationRegistry extends FinalizationRegistry { + #cleanupCallback: (heldValue: T) => void; + + constructor(cleanupCallback: (heldValue: T) => void) { + super(cleanupCallback); + this.#cleanupCallback = cleanupCallback; + } + + override register(target: WeakKey, heldValue: T) { + Object.assign(target, { + onCleanup: () => { + this.#cleanupCallback(heldValue); + }, + }); + } + } + + function makeRegisterableObject() { + const onCleanup = null as (() => void) | null; + return { + onCleanup, + [Symbol.dispose]() { + this.onCleanup?.(); + }, + }; + } + + const OriginalFinalizationRegistry = FinalizationRegistry; + + try { + if (!gc) { + globalThis.FinalizationRegistry = MockFinalizationRegistry; + } + + const cache = new Map(); + const getKey = _serializeArgList(cache); + + using outerScopeObj = makeRegisterableObject(); + + const k1 = getKey(outerScopeObj); + const k2 = getKey(globalThis); + const k3 = getKey("primitive"); + const k4 = getKey(globalThis, "primitive"); + const k5 = getKey(globalThis, "primitive", outerScopeObj); + + const persistentKeys = new Set([k1, k2, k3, k4, k5]); + + await (async () => { + using obj1 = makeRegisterableObject(); + using obj2 = makeRegisterableObject(); + + const k6 = getKey(obj1); + const k7 = getKey(obj2); + const k8 = getKey(obj1, obj2); + const k9 = getKey(obj1, globalThis); + const k10 = getKey(obj1, "primitive"); + const k11 = getKey(obj1, outerScopeObj); + + const ephemeralKeys = new Set([k6, k7, k8, k9, k10, k11]); + + const keys = new Set([...ephemeralKeys, ...persistentKeys]); + for (const [idx, key] of [...keys].entries()) { + cache.set(key, idx + 1); + } + + gc?.(); + // wait for gc to run + await delay(0); + assertEquals(cache.size, keys.size); + })(); + + gc?.(); + // wait for gc to run + await delay(0); + assertEquals(cache.size, persistentKeys.size); + } finally { + globalThis.FinalizationRegistry = OriginalFinalizationRegistry; + } +}); diff --git a/cache/deno.json b/cache/deno.json new file mode 100644 index 000000000000..1685e5bfd0ed --- /dev/null +++ b/cache/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@std/cache", + "version": "0.1.0", + "exports": { + ".": "./mod.ts", + "./lru-cache": "./lru_cache.ts", + "./memoize": "./memoize.ts" + } +} diff --git a/cache/lru_cache.ts b/cache/lru_cache.ts new file mode 100644 index 000000000000..d5e2791aa06e --- /dev/null +++ b/cache/lru_cache.ts @@ -0,0 +1,151 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import type { MemoizationCache } from "./memoize.ts"; +export type { MemoizationCache }; + +/** + * [Least-recently-used]( + * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU + * ) cache. + * + * Automatically removes entries above the max size based on when they were + * last accessed with `get`, `set`, or `has`. + * + * @typeParam K The type of the cache keys. + * @typeParam V The type of the cache values. + * + * @example Basic usage + * ```ts + * import { LruCache } from "@std/cache"; + * import { assert, assertEquals } from "@std/assert"; + * + * const MAX_SIZE = 3; + * const cache = new LruCache(MAX_SIZE); + * + * cache.set("a", 1); + * cache.set("b", 2); + * cache.set("c", 3); + * cache.set("d", 4); + * + * // most recent values are stored up to `MAX_SIZE` + * assertEquals(cache.get("b"), 2); + * assertEquals(cache.get("c"), 3); + * assertEquals(cache.get("d"), 4); + * + * // less recent values are removed + * assert(!cache.has("a")); + * ``` + */ +export class LruCache extends Map + implements MemoizationCache { + /** + * The maximum number of entries to store in the cache. + * + * @example Max size + * ```ts no-assert + * import { LruCache } from "@std/cache"; + * import { assertEquals } from "@std/assert"; + * + * const cache = new LruCache(100); + * assertEquals(cache.maxSize, 100); + * ``` + */ + maxSize: number; + + /** + * Constructs a new `LruCache`. + * + * @param maxSize The maximum number of entries to store in the cache. + */ + constructor(maxSize: number) { + super(); + this.maxSize = maxSize; + } + + #setMostRecentlyUsed(key: K, value: V): void { + // delete then re-add to ensure most recently accessed elements are last + super.delete(key); + super.set(key, value); + } + + #pruneToMaxSize(): void { + if (this.size > this.maxSize) { + this.delete(this.keys().next().value); + } + } + + /** + * Checks whether an element with the specified key exists or not. + * + * @param key The key to check. + * @returns `true` if the cache contains the specified key, otherwise `false`. + * + * @example Checking for the existence of a key + * ```ts + * import { LruCache } from "@std/cache"; + * import { assert } from "@std/assert"; + * + * const cache = new LruCache(100); + * + * cache.set("a", 1); + * assert(cache.has("a")); + * ``` + */ + override has(key: K): boolean { + const exists = super.has(key); + + if (exists) { + this.#setMostRecentlyUsed(key, super.get(key)!); + } + + return exists; + } + + /** + * Gets the element with the specified key. + * + * @param key The key to get the value for. + * @returns The value associated with the specified key, or `undefined` if the key is not present in the cache. + * + * @example Getting a value from the cache + * ```ts + * import { LruCache } from "@std/cache"; + * import { assertEquals } from "@std/assert"; + * + * const cache = new LruCache(100); + * + * cache.set("a", 1); + * assertEquals(cache.get("a"), 1); + * ``` + */ + override get(key: K): V | undefined { + if (super.has(key)) { + const value = super.get(key)!; + this.#setMostRecentlyUsed(key, value); + return value; + } + + return undefined; + } + + /** + * Sets the specified key to the specified value. + * + * @param key The key to set the value for. + * @param value The value to set. + * @returns `this` for chaining. + * + * @example Setting a value in the cache + * ```ts no-assert + * import { LruCache } from "@std/cache"; + * + * const cache = new LruCache(100); + * cache.set("a", 1); + * ``` + */ + override set(key: K, value: V): this { + this.#setMostRecentlyUsed(key, value); + this.#pruneToMaxSize(); + + return this; + } +} diff --git a/cache/lru_cache_test.ts b/cache/lru_cache_test.ts new file mode 100644 index 000000000000..ea5908e3efd5 --- /dev/null +++ b/cache/lru_cache_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { assert, assertEquals } from "@std/assert"; +import { LruCache } from "./lru_cache.ts"; + +Deno.test("LruCache deletes least-recently-used", () => { + const cache = new LruCache(3); + + cache.set(1, "!"); + cache.set(2, "!"); + cache.set(1, "updated"); + cache.set(3, "!"); + cache.set(4, "!"); + + assertEquals(cache.size, 3); + assert(!cache.has(2)); + assertEquals(cache.get(2), undefined); + assertEquals([...cache.keys()], [1, 3, 4]); + assertEquals(cache.get(3), "!"); + assertEquals(cache.get(1), "updated"); + + cache.delete(3); + assertEquals(cache.size, 2); + assertEquals(cache.get(3), undefined); +}); diff --git a/cache/memoize.ts b/cache/memoize.ts new file mode 100644 index 000000000000..29ca4593ed7e --- /dev/null +++ b/cache/memoize.ts @@ -0,0 +1,144 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore no-unused-vars +import type { LruCache } from "./lru_cache.ts"; +import { _serializeArgList } from "./_serialize_arg_list.ts"; + +/** + * A cache suitable for use with {@linkcode memoize}. + */ +export type MemoizationCache = { + has: (key: K) => boolean; + get: (key: K) => V | undefined; + set: (key: K, val: V) => unknown; + delete: (key: K) => unknown; +}; + +/** + * Options for {@linkcode memoize}. + * + * @typeParam Fn The type of the function to memoize. + * @typeParam Key The type of the cache key. + * @typeParam Cache The type of the cache. + */ +export type MemoizeOptions< + Fn extends (...args: never[]) => unknown, + Key, + Cache extends MemoizationCache>, +> = { + /** + * Provide a custom cache for getting previous results. By default, a new + * {@linkcode Map} object is instantiated upon memoization and used as a cache, with no + * limit on the number of results to be cached. + * + * Alternatively, you can supply a {@linkcode LruCache} with a specified max + * size to limit memory usage. + */ + cache?: Cache; + /** + * Function to get a unique cache key from the function's arguments. By + * default, a composite key is created from all the arguments plus the `this` + * value, using reference equality to check for equivalence. + * + * @example + * ```ts + * import { memoize } from "@std/cache"; + * import { assertEquals } from "@std/assert"; + * + * const fn = memoize(({ value }: { cacheKey: number; value: number }) => { + * return value; + * }, { getKey: ({ cacheKey }) => cacheKey }); + * + * assertEquals(fn({ cacheKey: 1, value: 2 }), 2); + * assertEquals(fn({ cacheKey: 1, value: 99 }), 2); + * assertEquals(fn({ cacheKey: 2, value: 99 }), 99); + * ``` + */ + getKey?: (this: ThisParameterType, ...args: Parameters) => Key; +}; + +/** + * Cache the results of a function based on its arguments. + * + * @typeParam Fn The type of the function to memoize. + * @typeParam Key The type of the cache key. + * @typeParam Cache The type of the cache. + * @param fn The function to memoize + * @param options Options for memoization + * + * @returns The memoized function. + * + * @example Basic usage + * ```ts + * import { memoize } from "@std/cache"; + * import { assertEquals } from "@std/assert"; + * + * // fibonacci function, which is very slow for n > ~30 if not memoized + * const fib = memoize((n: bigint): bigint => { + * return n <= 2n ? 1n : fib(n - 1n) + fib(n - 2n); + * }); + * + * assertEquals(fib(100n), 354224848179261915075n); + * ``` + * + * > [!NOTE] + * > * By default, memoization is on the basis of all arguments passed to the + * > function, with equality determined by reference. This means that, for + * > example, passing a memoized function as `arr.map(func)` will not use the + * > cached results, as the index is implicitly passed as an argument. To + * > avoid this, you can pass a custom `getKey` option or use the memoized + * > function inside an anonymous callback like `arr.map((x) => func(x))`. + * > * Memoization will not cache thrown errors and will eject promises from + * > the cache upon rejection. If you want to retain errors or rejected + * > promises in the cache, you will need to catch and return them. + */ +export function memoize< + Fn extends (...args: never[]) => unknown, + Key = string, + Cache extends MemoizationCache> = Map< + Key, + ReturnType + >, +>( + fn: Fn, + options?: MemoizeOptions, +): Fn { + const cache = options?.cache ?? new Map(); + const getKey = options?.getKey ?? + _serializeArgList( + cache as MemoizationCache, + ) as unknown as ( + (this: ThisParameterType, ...args: Parameters) => Key + ); + const memoized = function ( + this: ThisParameterType, + ...args: Parameters + ): ReturnType { + const key = getKey.apply(this, args) as Key; + + if (cache.has(key)) { + return cache.get(key)!; + } + + let val = fn.apply(this, args) as ReturnType; + + if (val instanceof Promise) { + val = val.catch((reason) => { + cache.delete(key); + throw reason; + }) as typeof val; + } + + cache.set(key, val); + + return val; + } as Fn; + + return Object.defineProperties( + memoized, + { + length: { value: fn.length }, + name: { value: fn.name }, + }, + ); +} diff --git a/cache/memoize_test.ts b/cache/memoize_test.ts new file mode 100644 index 000000000000..7e162b9c1f90 --- /dev/null +++ b/cache/memoize_test.ts @@ -0,0 +1,561 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertAlmostEquals, + assertEquals, + assertRejects, +} from "@std/assert"; +import { delay } from "@std/async"; +import { memoize } from "./memoize.ts"; +import { LruCache } from "./lru_cache.ts"; + +Deno.test( + "memoize() memoizes nullary function (lazy/singleton)", + async (t) => { + await t.step("async function", async () => { + let numTimesCalled = 0; + + const db = { + connect() { + ++numTimesCalled; + return Promise.resolve({}); + }, + }; + + const getConn = memoize(async () => await db.connect()); + const conn = await getConn(); + assertEquals(numTimesCalled, 1); + const conn2 = await getConn(); + // equal by reference + assert(conn2 === conn); + assertEquals(numTimesCalled, 1); + }); + + await t.step("sync function", async () => { + const firstHitDate = memoize(() => new Date()); + + const date = firstHitDate(); + + await delay(10); + + const date2 = firstHitDate(); + + assertEquals(date, date2); + }); + }, +); + +Deno.test("memoize() allows simple memoization with primitive arg", () => { + let numTimesCalled = 0; + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }); + + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 1); + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 1); + assertEquals(fn(888), -888); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() is performant for expensive fibonacci function", () => { + const fib = memoize((n: bigint): bigint => + n <= 2n ? 1n : fib(n - 1n) + fib(n - 2n) + ); + + const startTime = Date.now(); + assertEquals(fib(100n), 354224848179261915075n); + + assertAlmostEquals(Date.now(), startTime, 10); +}); + +Deno.test("memoize() allows multiple primitive args", () => { + let numTimesCalled = 0; + const fn = memoize((a: number, b: number) => { + ++numTimesCalled; + return a + b; + }); + + assertEquals(fn(7, 8), 15); + assertEquals(numTimesCalled, 1); + assertEquals(fn(7, 8), 15); + assertEquals(numTimesCalled, 1); + assertEquals(fn(7, 9), 16); + assertEquals(numTimesCalled, 2); + assertEquals(fn(8, 7), 15); + assertEquals(numTimesCalled, 3); +}); + +Deno.test("memoize() allows ...spread primitive args", () => { + let numTimesCalled = 0; + const fn = memoize((...ns: number[]) => { + ++numTimesCalled; + return ns.reduce((total, val) => total + val, 0); + }); + + assertEquals(fn(), 0); + assertEquals(fn(), 0); + assertEquals(numTimesCalled, 1); + assertEquals(fn(7), 7); + assertEquals(fn(7), 7); + assertEquals(numTimesCalled, 2); + assertEquals(fn(7, 8), 15); + assertEquals(fn(7, 8), 15); + assertEquals(numTimesCalled, 3); + assertEquals(fn(7, 8, 9), 24); + assertEquals(fn(7, 8, 9), 24); + assertEquals(numTimesCalled, 4); +}); + +Deno.test( + "memoize() caches unary function by all passed args by default (implicit extra args as array callback)", + () => { + let numTimesCalled = 0; + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }); + + assertEquals([1, 1, 2, 2].map(fn), [-1, -1, -2, -2]); + assertEquals(numTimesCalled, 4); + }, +); + +Deno.test("memoize() preserves `this` binding`", () => { + class X { + readonly key = "CONSTANT"; + timesCalled = 0; + + #method() { + return 1; + } + + method() { + ++this.timesCalled; + return this.#method(); + } + } + + const x = new X(); + + const method = x.method.bind(x); + + const fn = memoize(method); + assertEquals(fn(), 1); + + const fn2 = memoize(x.method).bind(x); + assertEquals(fn2(), 1); +}); + +// based on https://github.com/lodash/lodash/blob/4.17.15/test/test.js#L14704-L14716 +Deno.test("memoize() uses `this` binding of function for `getKey`", () => { + type Obj = { b: number; c: number; memoized: (a: number) => number }; + + let numTimesCalled = 0; + + const fn = function (this: Obj, a: number) { + ++numTimesCalled; + return a + this.b + this.c; + }; + const getKey = function (this: Obj, a: number) { + return JSON.stringify([a, this.b, this.c]); + }; + + const memoized = memoize(fn, { getKey }); + + const obj: Obj = { memoized, "b": 2, "c": 3 }; + assertEquals(obj.memoized(1), 6); + assertEquals(numTimesCalled, 1); + + assertEquals(obj.memoized(1), 6); + assertEquals(numTimesCalled, 1); + + obj.b = 3; + obj.c = 5; + assertEquals(obj.memoized(1), 9); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() allows reference arg with default caching", () => { + let numTimesCalled = 0; + const fn = memoize((sym: symbol) => { + ++numTimesCalled; + return sym; + }); + const sym1 = Symbol(); + const sym2 = Symbol(); + + fn(sym1); + assertEquals(numTimesCalled, 1); + fn(sym1); + assertEquals(numTimesCalled, 1); + fn(sym2); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() allows multiple reference args with default caching", () => { + let numTimesCalled = 0; + const fn = memoize((obj1: unknown, obj2: unknown) => { + ++numTimesCalled; + return { obj1, obj2 }; + }); + const obj1 = {}; + const obj2 = {}; + + fn(obj1, obj1); + assertEquals(numTimesCalled, 1); + fn(obj1, obj1); + assertEquals(numTimesCalled, 1); + fn(obj1, obj2); + assertEquals(numTimesCalled, 2); + fn(obj2, obj2); + assertEquals(numTimesCalled, 3); + fn(obj2, obj1); + assertEquals(numTimesCalled, 4); +}); + +Deno.test("memoize() allows non-primitive arg with `getKey`", () => { + let numTimesCalled = 0; + const fn = memoize((d: Date) => { + ++numTimesCalled; + return new Date(0 - d.valueOf()); + }, { getKey: (n) => n.valueOf() }); + const date1 = new Date(42); + const date2 = new Date(888); + + assertEquals(fn(date1), new Date(-42)); + assertEquals(numTimesCalled, 1); + assertEquals(fn(date1), new Date(-42)); + assertEquals(numTimesCalled, 1); + assertEquals(fn(date2), new Date(-888)); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() allows non-primitive arg with `getKey`", () => { + const fn = memoize(({ value }: { cacheKey: number; value: number }) => { + return value; + }, { getKey: ({ cacheKey }) => cacheKey }); + + assertEquals(fn({ cacheKey: 1, value: 2 }), 2); + assertEquals(fn({ cacheKey: 1, value: 99 }), 2); + assertEquals(fn({ cacheKey: 2, value: 99 }), 99); +}); + +Deno.test( + "memoize() allows multiple non-primitive args with `getKey` returning primitive", + () => { + let numTimesCalled = 0; + + const fn = memoize((...args: { val: number }[]) => { + ++numTimesCalled; + return args.reduce((total, { val }) => total + val, 0); + }, { getKey: (...args) => JSON.stringify(args) }); + + assertEquals(fn({ val: 1 }, { val: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ val: 1 }, { val: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ val: 2 }, { val: 1 }), 3); + assertEquals(numTimesCalled, 2); + }, +); + +Deno.test( + "memoize() allows multiple non-primitive args with `getKey` returning stringified array of primitives", + () => { + let numTimesCalled = 0; + + const fn = memoize((...args: { val: number }[]) => { + ++numTimesCalled; + return args.reduce((total, { val }) => total + val, 0); + }, { getKey: (...args) => JSON.stringify(args.map((arg) => arg.val)) }); + + assertEquals(fn({ val: 1 }, { val: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ val: 1 }, { val: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ val: 2 }, { val: 1 }), 3); + assertEquals(numTimesCalled, 2); + }, +); + +Deno.test( + "memoize() allows multiple non-primitive args of different types, `getKey` returning custom string from props", + () => { + let numTimesCalled = 0; + + const fn = memoize((one: { one: number }, two: { two: number }) => { + ++numTimesCalled; + return one.one + two.two; + }, { getKey: (one, two) => `${one.one},${two.two}` }); + + assertEquals(fn({ one: 1 }, { two: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ one: 1 }, { two: 2 }), 3); + assertEquals(numTimesCalled, 1); + assertEquals(fn({ one: 2 }, { two: 1 }), 3); + assertEquals(numTimesCalled, 2); + }, +); + +Deno.test("memoize() allows primitive arg with `getKey`", () => { + let numTimesCalled = 0; + const fn = memoize((arg: string | number | boolean) => { + ++numTimesCalled; + + try { + return JSON.parse(String(arg)) as string | number | boolean; + } catch { + return arg; + } + }, { getKey: (arg) => String(arg) }); + + assertEquals(fn("true"), true); + assertEquals(numTimesCalled, 1); + assertEquals(fn(true), true); + assertEquals(numTimesCalled, 1); + + assertEquals(fn("42"), 42); + assertEquals(numTimesCalled, 2); + assertEquals(fn(42), 42); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() works with async functions", async () => { + // wait time per call of the original (un-memoized) function + const DELAY_MS = 100; + // max amount of execution time per call of the memoized function + const TOLERANCE_MS = 5; + + const startTime = Date.now(); + const fn = memoize(async (n: number) => { + await delay(DELAY_MS); + return 0 - n; + }); + + const nums = [42, 888, 42, 42, 42, 42, 888, 888, 888, 888]; + const expected = [-42, -888, -42, -42, -42, -42, -888, -888, -888, -888]; + const results: number[] = []; + + // call in serial to test time elapsed + for (const num of nums) { + results.push(await fn(num)); + } + + assertEquals(results, expected); + + const numUnique = new Set(nums).size; + + assertAlmostEquals( + Date.now() - startTime, + numUnique * DELAY_MS, + nums.length * TOLERANCE_MS, + ); +}); + +Deno.test( + "memoize() doesn’t cache rejected promises for future function calls", + async () => { + let rejectNext = true; + const fn = memoize(async (n: number) => { + await Promise.resolve(); + const thisCallWillReject = rejectNext; + rejectNext = !rejectNext; + if (thisCallWillReject) { + throw new Error(); + } + return 0 - n; + }); + + // first call rejects + await assertRejects(() => fn(42)); + // second call succeeds (rejected response is discarded) + assertEquals(await fn(42), -42); + // subsequent calls also succeed (successful response from cache is used) + assertEquals(await fn(42), -42); + }, +); + +Deno.test( + "memoize() causes async functions called in parallel to return the same promise (even if rejected)", + async () => { + let rejectNext = true; + const fn = memoize(async (n: number) => { + await Promise.resolve(); + if (rejectNext) { + rejectNext = false; + throw new Error(`Rejected ${n}`); + } + return 0 - n; + }); + + const promises = [42, 42, 888, 888].map((x) => fn(x)); + + const results = await Promise.allSettled(promises); + + assert(promises[1] === promises[0]); + assert(results[1]!.status === "rejected"); + assert(results[1]!.reason.message === "Rejected 42"); + + assert(promises[3] === promises[2]); + assert(results[3]!.status === "fulfilled"); + assert(results[3]!.value === -888); + }, +); + +Deno.test("memoize() allows passing a `Map` as a cache", () => { + let numTimesCalled = 0; + const cache = new Map(); + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }, { cache }); + + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 1); + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 1); +}); + +Deno.test("memoize() allows passing a custom cache object", () => { + let numTimesCalled = 0; + + const uselessCache = { + has: () => false, + get: () => { + throw new Error("`has` is always false, so `get` is never called"); + }, + set: () => {}, + delete: () => {}, + keys: () => [], + }; + + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }, { cache: uselessCache }); + + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 1); + assertEquals(fn(42), -42); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() deletes stale entries of passed `LruCache`", () => { + let numTimesCalled = 0; + + const MAX_SIZE = 5; + + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }, { cache: new LruCache(MAX_SIZE) }); + + assertEquals(fn(0), 0); + assertEquals(fn(0), 0); + assertEquals(numTimesCalled, 1); + + for (let i = 1; i < MAX_SIZE; ++i) { + assertEquals(fn(i), 0 - i); + assertEquals(fn(i), 0 - i); + assertEquals(numTimesCalled, i + 1); + } + + assertEquals(fn(MAX_SIZE), 0 - MAX_SIZE); + assertEquals(fn(MAX_SIZE), 0 - MAX_SIZE); + assertEquals(numTimesCalled, MAX_SIZE + 1); + + assertEquals(fn(0), 0); + assertEquals(fn(0), 0); + assertEquals(numTimesCalled, MAX_SIZE + 2); +}); + +Deno.test("memoize() only caches single latest result with a `LruCache` of maxSize=1", () => { + let numTimesCalled = 0; + + const fn = memoize((n: number) => { + ++numTimesCalled; + return 0 - n; + }, { cache: new LruCache(1) }); + + assertEquals(fn(0), 0); + assertEquals(fn(0), 0); + assertEquals(numTimesCalled, 1); + + assertEquals(fn(1), -1); + assertEquals(numTimesCalled, 2); +}); + +Deno.test("memoize() preserves function length", () => { + assertEquals(memoize.length, 2); + + assertEquals(memoize(() => {}).length, 0); + assertEquals(memoize((_arg) => {}).length, 1); + assertEquals(memoize((_1, _2) => {}).length, 2); + assertEquals(memoize((..._args) => {}).length, 0); + assertEquals(memoize((_1, ..._args) => {}).length, 1); +}); + +Deno.test("memoize() preserves function name", () => { + assertEquals(memoize.name, "memoize"); + + const fn1 = () => {}; + function fn2() {} + const obj = { ["!"]: () => {} }; + + assertEquals(memoize(() => {}).name, ""); + assertEquals(memoize(fn1).name, "fn1"); + assertEquals(memoize(fn1.bind({})).name, "bound fn1"); + assertEquals(memoize(fn2).name, "fn2"); + assertEquals(memoize(function fn3() {}).name, "fn3"); + assertEquals(memoize(obj["!"]).name, "!"); +}); + +Deno.test("memoize() has correct TS types", async (t) => { + await t.step("simple types", () => { + // no need to run, only for type checking + void (() => { + const fn: (this: number, x: number) => number = (_) => 1; + const memoized = memoize(fn); + + const _fn2: typeof fn = memoized; + const _fn3: Omit = fn; + + const _t1: ThisParameterType = 1; + // @ts-expect-error Type 'string' is not assignable to type 'number'. + const _t2: ThisParameterType = "1"; + + const _a1: Parameters[0] = 1; + // @ts-expect-error Type 'string' is not assignable to type 'number'. + const _a2: Parameters[0] = "1"; + // @ts-expect-error Tuple type '[x: number]' of length '1' has no element at index '1'. + const _a3: Parameters[1] = {} as never; + + const _r1: ReturnType = 1; + // @ts-expect-error Type 'string' is not assignable to type 'number'. + const _r2: ReturnType = "1"; + }); + }); + + await t.step("memoize() correctly preserves generic types", () => { + // no need to run, only for type checking + void (() => { + const fn = (x: T): T => x; + const memoized = memoize(fn); + + const _fn2: typeof fn = memoized; + const _fn3: Omit = fn; + + const _r1: number = fn(1); + const _r2: string = fn("1"); + // @ts-expect-error Type 'string' is not assignable to type 'number'. + const _r3: number = fn("1"); + + const _fn4: typeof fn = (n: number) => n; + // @ts-expect-error Type 'string' is not assignable to type 'number'. + const _fn5: typeof fn = (n: number) => n; + }); + }); +}); diff --git a/cache/mod.ts b/cache/mod.ts new file mode 100644 index 000000000000..96384df880a4 --- /dev/null +++ b/cache/mod.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. + +/** + * In-memory cache utilities, such as memoization and caches with different + * expiration policies. + * + * ```ts + * import { memoize, LruCache } from "@std/cache"; + * import { assertEquals } from "@std/assert"; + * + * const cache = new LruCache(1000); + * + * // fibonacci function, which is very slow for n > ~30 if not memoized + * const fib = memoize((n: bigint): bigint => { + * return n <= 2n ? 1n : fib(n - 1n) + fib(n - 2n); + * }, { cache }); + * + * assertEquals(fib(100n), 354224848179261915075n); + * ``` + * + * @module + */ + +export * from "./memoize.ts"; +export * from "./lru_cache.ts"; diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index 1a687366f06a..afba65c75690 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -36,12 +36,11 @@ type FormatPart = { value: string | number; hour12?: boolean; }; -type Format = FormatPart[]; function throwInvalidValueError(type: string, value: string, string: string) { if (!value) { throw Error( - `value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`, + `value not valid for part { ${type} ${value} } ${string.slice(0, 25)}`, ); } } @@ -50,83 +49,83 @@ const LITERAL_REGEXP = /^(?.+?\s*)/; const SYMBOL_REGEXP = /^(?([a-zA-Z])\2*)/; // according to unicode symbols (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) -function formatToParts(format: string) { - const tokens: Format = []; +function formatToFormatParts(format: string) { + const formatParts: FormatPart[] = []; let index = 0; while (index < format.length) { const substring = format.slice(index); const symbol = SYMBOL_REGEXP.exec(substring)?.groups?.symbol; switch (symbol) { case "yyyy": - tokens.push({ type: "year", value: "numeric" }); + formatParts.push({ type: "year", value: "numeric" }); index += symbol.length; continue; case "yy": - tokens.push({ type: "year", value: "2-digit" }); + formatParts.push({ type: "year", value: "2-digit" }); index += symbol.length; continue; case "MM": - tokens.push({ type: "month", value: "2-digit" }); + formatParts.push({ type: "month", value: "2-digit" }); index += symbol.length; continue; case "M": - tokens.push({ type: "month", value: "numeric" }); + formatParts.push({ type: "month", value: "numeric" }); index += symbol.length; continue; case "dd": - tokens.push({ type: "day", value: "2-digit" }); + formatParts.push({ type: "day", value: "2-digit" }); index += symbol.length; continue; case "d": - tokens.push({ type: "day", value: "numeric" }); + formatParts.push({ type: "day", value: "numeric" }); index += symbol.length; continue; case "HH": - tokens.push({ type: "hour", value: "2-digit" }); + formatParts.push({ type: "hour", value: "2-digit" }); index += symbol.length; continue; case "H": - tokens.push({ type: "hour", value: "numeric" }); + formatParts.push({ type: "hour", value: "numeric" }); index += symbol.length; continue; case "hh": - tokens.push({ type: "hour", value: "2-digit", hour12: true }); + formatParts.push({ type: "hour", value: "2-digit", hour12: true }); index += symbol.length; continue; case "h": - tokens.push({ type: "hour", value: "numeric", hour12: true }); + formatParts.push({ type: "hour", value: "numeric", hour12: true }); index += symbol.length; continue; case "mm": - tokens.push({ type: "minute", value: "2-digit" }); + formatParts.push({ type: "minute", value: "2-digit" }); index += symbol.length; continue; case "m": - tokens.push({ type: "minute", value: "numeric" }); + formatParts.push({ type: "minute", value: "numeric" }); index += symbol.length; continue; case "ss": - tokens.push({ type: "second", value: "2-digit" }); + formatParts.push({ type: "second", value: "2-digit" }); index += symbol.length; continue; case "s": - tokens.push({ type: "second", value: "numeric" }); + formatParts.push({ type: "second", value: "numeric" }); index += symbol.length; continue; case "SSS": - tokens.push({ type: "fractionalSecond", value: 3 }); + formatParts.push({ type: "fractionalSecond", value: 3 }); index += symbol.length; continue; case "SS": - tokens.push({ type: "fractionalSecond", value: 2 }); + formatParts.push({ type: "fractionalSecond", value: 2 }); index += symbol.length; continue; case "S": - tokens.push({ type: "fractionalSecond", value: 1 }); + formatParts.push({ type: "fractionalSecond", value: 1 }); index += symbol.length; continue; case "a": - tokens.push({ type: "dayPeriod", value: 1 }); + formatParts.push({ type: "dayPeriod", value: 1 }); index += symbol.length; continue; } @@ -134,25 +133,48 @@ function formatToParts(format: string) { const quotedLiteralMatch = QUOTED_LITERAL_REGEXP.exec(substring); if (quotedLiteralMatch) { const value = quotedLiteralMatch.groups!.value as string; - tokens.push({ type: "literal", value }); + formatParts.push({ type: "literal", value }); index += quotedLiteralMatch[0].length; continue; } const literalGroups = LITERAL_REGEXP.exec(substring)!.groups!; const value = literalGroups.value as string; - tokens.push({ type: "literal", value }); + formatParts.push({ type: "literal", value }); index += value.length; } - return tokens; + return formatParts; +} + +function sortDateTimeFormatParts( + parts: DateTimeFormatPart[], +): DateTimeFormatPart[] { + let result: DateTimeFormatPart[] = []; + const typeArray = [ + "year", + "month", + "day", + "hour", + "minute", + "second", + "fractionalSecond", + ]; + for (const type of typeArray) { + const current = parts.findIndex((el) => el.type === type); + if (current !== -1) { + result = result.concat(parts.splice(current, 1)); + } + } + result = result.concat(parts); + return result; } export class DateTimeFormatter { - #format: Format; + #formatParts: FormatPart[]; constructor(formatString: string) { - this.#format = formatToParts(formatString); + this.#formatParts = formatToFormatParts(formatString); } format(date: Date, options: Options = {}): string { @@ -160,13 +182,13 @@ export class DateTimeFormatter { const utc = options.timeZone === "UTC"; - for (const token of this.#format) { - const type = token.type; + for (const part of this.#formatParts) { + const type = part.type; switch (type) { case "year": { const value = utc ? date.getUTCFullYear() : date.getFullYear(); - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -177,14 +199,14 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; } case "month": { const value = (utc ? date.getUTCMonth() : date.getMonth()) + 1; - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -195,14 +217,14 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; } case "day": { const value = utc ? date.getUTCDate() : date.getDate(); - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -213,18 +235,18 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; } case "hour": { let value = utc ? date.getUTCHours() : date.getHours(); - if (token.hour12) { + if (part.hour12) { if (value === 0) value = 12; else if (value > 12) value -= 12; } - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -235,14 +257,14 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; } case "minute": { const value = utc ? date.getUTCMinutes() : date.getMinutes(); - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -253,14 +275,14 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; } case "second": { const value = utc ? date.getUTCSeconds() : date.getSeconds(); - switch (token.value) { + switch (part.value) { case "numeric": { string += value; break; @@ -271,7 +293,7 @@ export class DateTimeFormatter { } default: throw Error( - `FormatterError: value "${token.value}" is not supported`, + `FormatterError: value "${part.value}" is not supported`, ); } break; @@ -280,12 +302,12 @@ export class DateTimeFormatter { const value = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); - string += digits(value, Number(token.value)); + string += digits(value, Number(part.value)); break; } // FIXME(bartlomieju) case "timeZoneName": { - // string += utc ? "Z" : token.value + // string += utc ? "Z" : part.value break; } case "dayPeriod": { @@ -293,30 +315,30 @@ export class DateTimeFormatter { break; } case "literal": { - string += token.value; + string += part.value; break; } default: - throw Error(`FormatterError: { ${token.type} ${token.value} }`); + throw Error(`FormatterError: { ${part.type} ${part.value} }`); } } return string; } - parseToParts(string: string): DateTimeFormatPart[] { + formatToParts(string: string): DateTimeFormatPart[] { const parts: DateTimeFormatPart[] = []; let index = 0; - for (const token of this.#format) { + for (const part of this.#formatParts) { const substring = string.slice(index); - const type = token.type; + const type = part.type; let value = ""; switch (type) { case "year": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,4}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); @@ -333,13 +355,13 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "month": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); @@ -377,13 +399,13 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "day": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); @@ -400,19 +422,19 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "hour": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; - if (token.hour12 && parseInt(value) > 12) { + if (part.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'H' instead of 'h'.`, ); @@ -424,7 +446,7 @@ export class DateTimeFormatter { if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; - if (token.hour12 && parseInt(value) > 12) { + if (part.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`, ); @@ -433,13 +455,13 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "minute": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); @@ -456,13 +478,13 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "second": { - switch (token.value) { + switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(substring)?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); @@ -479,13 +501,13 @@ export class DateTimeFormatter { } default: throw Error( - `ParserError: value "${token.value}" is not supported`, + `ParserError: value "${part.value}" is not supported`, ); } break; } case "fractionalSecond": { - value = new RegExp(`^\\d{${token.value}}`).exec(substring) + value = new RegExp(`^\\d{${part.value}}`).exec(substring) ?.[0] as string; if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); @@ -493,7 +515,7 @@ export class DateTimeFormatter { break; } case "timeZoneName": { - value = token.value as string; + value = part.value as string; if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; @@ -521,19 +543,19 @@ export class DateTimeFormatter { break; } case "literal": { - if (!substring.startsWith(token.value as string)) { + if (!substring.startsWith(part.value as string)) { throw Error( - `Literal "${token.value}" not found "${substring.slice(0, 25)}"`, + `Literal "${part.value}" not found "${substring.slice(0, 25)}"`, ); } - value = token.value as string; + value = part.value as string; if (!value) throwInvalidValueError(type, value, substring); parts.push({ type, value }); index += value.length; break; } default: - throw Error(`${token.type} ${token.value}`); + throw Error(`${part.type} ${part.value}`); } } @@ -546,29 +568,9 @@ export class DateTimeFormatter { return parts; } - /** sort & filter dateTimeFormatPart */ - sortDateTimeFormatPart(parts: DateTimeFormatPart[]): DateTimeFormatPart[] { - let result: DateTimeFormatPart[] = []; - const typeArray = [ - "year", - "month", - "day", - "hour", - "minute", - "second", - "fractionalSecond", - ]; - for (const type of typeArray) { - const current = parts.findIndex((el) => el.type === type); - if (current !== -1) { - result = result.concat(parts.splice(current, 1)); - } - } - result = result.concat(parts); - return result; - } - partsToDate(parts: DateTimeFormatPart[]): Date { + parts = sortDateTimeFormatParts(parts); + const date = new Date(); const utc = parts.find( (part) => part.type === "timeZoneName" && part.value === "UTC", @@ -647,8 +649,7 @@ export class DateTimeFormatter { } parse(string: string): Date { - const parts = this.parseToParts(string); - const sortParts = this.sortDateTimeFormatPart(parts); - return this.partsToDate(sortParts); + const parts = this.formatToParts(string); + return this.partsToDate(parts); } } diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index 78e0bbdb6e1b..06891c886ff9 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -43,10 +43,10 @@ Deno.test("dateTimeFormatter.parse()", () => { assertEquals(formatter.parse("2020-01-01"), new Date(2020, 0, 1)); }); -Deno.test("dateTimeFormatter.parseToParts()", () => { +Deno.test("dateTimeFormatter.formatToParts()", () => { const format = "yyyy-MM-dd"; const formatter = new DateTimeFormatter(format); - assertEquals(formatter.parseToParts("2020-01-01"), [ + assertEquals(formatter.formatToParts("2020-01-01"), [ { type: "year", value: "2020" }, { type: "literal", value: "-" }, { type: "month", value: "01" }, @@ -55,21 +55,21 @@ Deno.test("dateTimeFormatter.parseToParts()", () => { ]); }); -Deno.test("dateTimeFormatter.parseToParts() throws on an empty string", () => { +Deno.test("dateTimeFormatter.formatToParts() throws on an empty string", () => { const format = "yyyy-MM-dd"; const formatter = new DateTimeFormatter(format); assertThrows( - () => formatter.parseToParts(""), + () => formatter.formatToParts(""), Error, - "value not valid for token", + "value not valid for part", ); }); -Deno.test("dateTimeFormatter.parseToParts() throws on a string which exceeds the format", () => { +Deno.test("dateTimeFormatter.formatToParts() throws on a string which exceeds the format", () => { const format = "yyyy-MM-dd"; const formatter = new DateTimeFormatter(format); assertThrows( - () => formatter.parseToParts("2020-01-01T00:00:00.000Z"), + () => formatter.formatToParts("2020-01-01T00:00:00.000Z"), Error, "datetime string was not fully parsed!", ); diff --git a/datetime/format.ts b/datetime/format.ts index 39a0c761905c..1e18bdbb6595 100644 --- a/datetime/format.ts +++ b/datetime/format.ts @@ -6,11 +6,11 @@ import { DateTimeFormatter } from "./_date_time_formatter.ts"; /** Options for {@linkcode format}. */ export interface FormatOptions { /** - * Whether returns the formatted date in UTC instead of local time. + * The timezone the formatted date should be in. * - * @default {false} + * @default {undefined} */ - utc?: boolean; + timeZone?: "UTC"; } /** @@ -69,7 +69,7 @@ export interface FormatOptions { * * const date = new Date(2019, 0, 20, 16, 34, 23, 123); * - * assertEquals(format(date, "yyyy-MM-dd HH:mm:ss", { utc: true }), "2019-01-20 05:34:23"); + * assertEquals(format(date, "yyyy-MM-dd HH:mm:ss", { timeZone: "UTC" }), "2019-01-20 05:34:23"); * ``` */ export function format( @@ -78,8 +78,5 @@ export function format( options: FormatOptions = {}, ): string { const formatter = new DateTimeFormatter(formatString); - return formatter.format( - date, - options.utc ? { timeZone: "UTC" } : undefined, - ); + return formatter.format(date, options); } diff --git a/datetime/format_test.ts b/datetime/format_test.ts index 425895c6a14e..e9da4efe1081 100644 --- a/datetime/format_test.ts +++ b/datetime/format_test.ts @@ -108,7 +108,7 @@ Deno.test({ format( new Date("2019-01-01T13:00:00.000+09:00"), "yyyy-MM-dd HH:mm:ss.SSS", - { utc: true }, + { timeZone: "UTC" }, ), ); @@ -117,7 +117,7 @@ Deno.test({ format( new Date("2019-01-01T13:00:00.000-05:00"), "yyyy-MM-dd HH:mm:ss.SSS", - { utc: true }, + { timeZone: "UTC" }, ), ); }, diff --git a/datetime/parse.ts b/datetime/parse.ts index 710025639231..e6784cff37e1 100644 --- a/datetime/parse.ts +++ b/datetime/parse.ts @@ -46,7 +46,5 @@ import { DateTimeFormatter } from "./_date_time_formatter.ts"; */ export function parse(dateString: string, formatString: string): Date { const formatter = new DateTimeFormatter(formatString); - const parts = formatter.parseToParts(dateString); - const sortParts = formatter.sortDateTimeFormatPart(parts); - return formatter.partsToDate(sortParts); + return formatter.parse(dateString); } diff --git a/deno.json b/deno.json index 3bb4531dc7cb..2b6b3d6430e3 100644 --- a/deno.json +++ b/deno.json @@ -97,6 +97,7 @@ "./assert", "./async", "./bytes", + "./cache", "./cli", "./collections", "./crypto", diff --git a/io/buf_reader.ts b/io/buf_reader.ts index 3bff1a140a07..8f934099e675 100644 --- a/io/buf_reader.ts +++ b/io/buf_reader.ts @@ -11,10 +11,49 @@ const CR = "\r".charCodeAt(0); const LF = "\n".charCodeAt(0); /** + * Thrown when a write operation is attempted on a full buffer. + * + * @example Usage + * ```ts + * import { BufWriter, BufferFullError, Writer } from "@std/io"; + * import { assert, assertEquals } from "@std/assert"; + * + * const writer: Writer = { + * write(p: Uint8Array): Promise { + * throw new BufferFullError(p); + * } + * }; + * const bufWriter = new BufWriter(writer); + * try { + * await bufWriter.write(new Uint8Array([1, 2, 3])); + * } catch (err) { + * assert(err instanceof BufferFullError); + * assertEquals(err.partial, new Uint8Array([3])); + * } + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class BufferFullError extends Error { + /** + * The partially read bytes + * + * @example Usage + * ```ts + * import { BufferFullError } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const err = new BufferFullError(new Uint8Array(2)); + * assertEquals(err.partial, new Uint8Array(2)); + * ``` + */ partial: Uint8Array; + + /** + * Construct a new instance. + * + * @param partial The bytes partially read + */ constructor(partial: Uint8Array) { super("Buffer full"); this.name = this.constructor.name; @@ -23,11 +62,41 @@ export class BufferFullError extends Error { } /** + * Thrown when a read from a stream fails to read the + * requested number of bytes. + * + * @example Usage + * ```ts + * import { PartialReadError } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const err = new PartialReadError(new Uint8Array(2)); + * assertEquals(err.name, "PartialReadError"); + * + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class PartialReadError extends Error { + /** + * The partially read bytes + * + * @example Usage + * ```ts + * import { PartialReadError } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const err = new PartialReadError(new Uint8Array(2)); + * assertEquals(err.partial, new Uint8Array(2)); + * ``` + */ partial: Uint8Array; + /** + * Construct a {@linkcode PartialReadError}. + * + * @param partial The bytes partially read + */ constructor(partial: Uint8Array) { super("Encountered UnexpectedEof, data only partially read"); this.name = this.constructor.name; @@ -36,16 +105,34 @@ export class PartialReadError extends Error { } /** - * Result type returned by of BufReader.readLine(). + * Result type returned by of {@linkcode BufReader.readLine}. * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export interface ReadLineResult { + /** The line read */ line: Uint8Array; + /** `true if the end of the line has not been reached, `false` otherwise. */ more: boolean; } /** + * Implements buffering for a {@linkcode Reader} object. + * + * @example Usage + * ```ts + * import { BufReader } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const encoder = new TextEncoder(); + * const decoder = new TextDecoder(); + * + * const reader = new BufReader(new Deno.Buffer(encoder.encode("hello world"))); + * const buf = new Uint8Array(11); + * await reader.read(buf); + * assertEquals(decoder.decode(buf), "hello world"); + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class BufReader implements Reader { @@ -55,11 +142,33 @@ export class BufReader implements Reader { #w = 0; // buf write position. #eof = false; - /** return new BufReader unless r is BufReader */ + /** + * Returns a new {@linkcode BufReader} if `r` is not already one. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assert } from "@std/assert/assert"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = BufReader.create(reader); + * assert(bufReader instanceof BufReader); + * ``` + * + * @param r The reader to read from. + * @param size The size of the buffer. + * @returns A new {@linkcode BufReader} if `r` is not already one. + */ static create(r: Reader, size: number = DEFAULT_BUF_SIZE): BufReader { return r instanceof BufReader ? r : new BufReader(r, size); } + /** + * Constructs a new {@linkcode BufReader} for the given reader and buffer size. + * + * @param rd The reader to read from. + * @param size The size of the buffer. + */ constructor(rd: Reader, size: number = DEFAULT_BUF_SIZE) { if (size < MIN_BUF_SIZE) { size = MIN_BUF_SIZE; @@ -67,11 +176,42 @@ export class BufReader implements Reader { this.#reset(new Uint8Array(size), rd); } - /** Returns the size of the underlying buffer in bytes. */ + /** + * Returns the size of the underlying buffer in bytes. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * + * assertEquals(bufReader.size(), 4096); + * ``` + * + * @returns The size of the underlying buffer in bytes. + */ size(): number { return this.#buf.byteLength; } + /** + * Returns the number of bytes that can be read from the current buffer. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * await bufReader.read(new Uint8Array(5)); + * assertEquals(bufReader.buffered(), 6); + * ``` + * + * @returns Number of bytes that can be read from the buffer + */ buffered(): number { return this.#w - this.#r; } @@ -107,8 +247,23 @@ export class BufReader implements Reader { ); }; - /** Discards any buffered data, resets all state, and switches - * the buffered reader to read from r. + /** + * Discards any buffered data, resets all state, and switches + * the buffered reader to read from `r`. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * await bufReader.read(new Uint8Array(5)); + * bufReader.reset(reader); + * assertEquals(bufReader.buffered(), 6); + * ``` + * + * @param r The reader to read from. */ reset(r: Reader) { this.#reset(this.#buf, r); @@ -122,11 +277,27 @@ export class BufReader implements Reader { // this.lastCharSize = -1; }; - /** reads data into p. - * It returns the number of bytes read into p. - * The bytes are taken from at most one Read on the underlying Reader, - * hence n may be less than len(p). - * To read exactly len(p) bytes, use io.ReadFull(b, p). + /** + * Reads data into `p`. + * + * The bytes are taken from at most one `read()` on the underlying `Reader`, + * hence n may be less than `len(p)`. + * To read exactly `len(p)` bytes, use `io.ReadFull(b, p)`. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const buf = new Uint8Array(5); + * await bufReader.read(buf); + * assertEquals(new TextDecoder().decode(buf), "hello"); + * ``` + * + * @param p The buffer to read data into. + * @returns The number of bytes read into `p`. */ async read(p: Uint8Array): Promise { let rr: number | null = p.byteLength; @@ -161,7 +332,8 @@ export class BufReader implements Reader { return copied; } - /** reads exactly `p.length` bytes into `p`. + /** + * Reads exactly `p.length` bytes into `p`. * * If successful, `p` is returned. * @@ -174,6 +346,22 @@ export class BufReader implements Reader { * buffer that has been successfully filled with data. * * Ported from https://golang.org/pkg/io/#ReadFull + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const buf = new Uint8Array(5); + * await bufReader.readFull(buf); + * assertEquals(new TextDecoder().decode(buf), "hello"); + * ``` + * + * @param p The buffer to read data into. + * @returns The buffer `p` if the read is successful, `null` if the end of the + * underlying stream has been reached, and there are no more bytes available in the buffer. */ async readFull(p: Uint8Array): Promise { let bytesRead = 0; @@ -191,7 +379,22 @@ export class BufReader implements Reader { return p; } - /** Returns the next byte [0, 255] or `null`. */ + /** + * Returns the next byte ([0, 255]) or `null`. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const byte = await bufReader.readByte(); + * assertEquals(byte, 104); + * ``` + * + * @returns The next byte ([0, 255]) or `null`. + */ async readByte(): Promise { while (this.#r === this.#w) { if (this.#eof) return null; @@ -203,14 +406,31 @@ export class BufReader implements Reader { return c; } - /** readString() reads until the first occurrence of delim in the input, + /** + * Reads until the first occurrence of delim in the input, * returning a string containing the data up to and including the delimiter. * If ReadString encounters an error before finding a delimiter, * it returns the data read before the error and the error itself * (often `null`). * ReadString returns err !== null if and only if the returned data does not end * in delim. - * For simple uses, a Scanner may be more convenient. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const str = await bufReader.readString(" "); + * assertEquals(str, "hello "); + * + * const str2 = await bufReader.readString(" "); + * assertEquals(str2, "world"); + * ``` + * + * @param delim The delimiter to read until. + * @returns The string containing the data up to and including the delimiter. */ async readString(delim: string): Promise { if (delim.length !== 1) { @@ -221,8 +441,9 @@ export class BufReader implements Reader { return new TextDecoder().decode(buffer); } - /** `readLine()` is a low-level line-reading primitive. Most callers should - * use `readString('\n')` instead or use a Scanner. + /** + * A low-level line-reading primitive. Most callers should use + * `readString('\n')` instead. * * `readLine()` tries to return a single line, not including the end-of-line * bytes. If the line was too long for the buffer then `more` is set and the @@ -231,7 +452,7 @@ export class BufReader implements Reader { * of the line. The returned buffer is only valid until the next call to * `readLine()`. * - * The text returned from ReadLine does not include the line end ("\r\n" or + * The text returned from this method does not include the line end ("\r\n" or * "\n"). * * When the end of the underlying stream is reached, the final bytes in the @@ -239,9 +460,20 @@ export class BufReader implements Reader { * without a final line end. When there are no more trailing bytes to read, * `readLine()` returns `null`. * - * Calling `unreadByte()` after `readLine()` will always unread the last byte - * read (possibly a character belonging to the line end) even if that byte is - * not part of the line returned by `readLine()`. + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello\nworld")); + * const bufReader = new BufReader(reader); + * const line1 = await bufReader.readLine(); + * assertEquals(new TextDecoder().decode(line1!.line), "hello"); + * const line2 = await bufReader.readLine(); + * assertEquals(new TextDecoder().decode(line2!.line), "world"); + * ``` + * + * @returns The line read. */ async readLine(): Promise { let line: Uint8Array | null = null; @@ -295,7 +527,8 @@ export class BufReader implements Reader { return { line, more: false }; } - /** `readSlice()` reads until the first occurrence of `delim` in the input, + /** + * Reads until the first occurrence of `delim` in the input, * returning a slice pointing at the bytes in the buffer. The bytes stop * being valid at the next read. * @@ -310,6 +543,20 @@ export class BufReader implements Reader { * * Because the data returned from `readSlice()` will be overwritten by the * next I/O operation, most clients should use `readString()` instead. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const slice = await bufReader.readSlice(0x20); + * assertEquals(new TextDecoder().decode(slice!), "hello "); + * ``` + * + * @param delim The delimiter to read until. + * @returns A slice pointing at the bytes in the buffer. */ async readSlice(delim: number): Promise { let s = 0; // search start index @@ -361,7 +608,8 @@ export class BufReader implements Reader { return slice; } - /** `peek()` returns the next `n` bytes without advancing the reader. The + /** + * Returns the next `n` bytes without advancing the reader. The * bytes stop being valid at the next read call. * * When the end of the underlying stream is reached, but there are unread @@ -371,6 +619,20 @@ export class BufReader implements Reader { * If an error is encountered before `n` bytes are available, `peek()` throws * an error with the `partial` property set to a slice of the buffer that * contains the bytes that were available before the error occurred. + * + * @example Usage + * ```ts + * import { BufReader, Buffer } from "@std/io"; + * import { assertEquals } from "@std/assert/equals"; + * + * const reader = new Buffer(new TextEncoder().encode("hello world")); + * const bufReader = new BufReader(reader); + * const peeked = await bufReader.peek(5); + * assertEquals(new TextDecoder().decode(peeked!), "hello"); + * ``` + * + * @param n The number of bytes to peek. + * @returns The next `n` bytes without advancing the reader. */ async peek(n: number): Promise { if (n < 0) { diff --git a/io/buf_writer.ts b/io/buf_writer.ts index c6a77d03710e..b4723474d221 100644 --- a/io/buf_writer.ts +++ b/io/buf_writer.ts @@ -6,57 +6,261 @@ import type { Writer, WriterSync } from "./types.ts"; const DEFAULT_BUF_SIZE = 4096; -abstract class AbstractBufBase { +/** + * AbstractBufBase is a base class which other classes can embed to + * implement the {@inkcode Reader} and {@linkcode Writer} interfaces. + * It provides basic implementations of those interfaces based on a buffer + * array. + * + * @example Usage + * ```ts no-assert + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { Reader } from "@std/io/types"; + * + * class MyBufReader extends AbstractBufBase { + * constructor(buf: Uint8Array) { + * super(buf); + * } + * } + * ``` + * + * @internal + */ +export abstract class AbstractBufBase { + /** + * The buffer + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.buf, buf); + * ``` + */ buf: Uint8Array; + /** + * The used buffer bytes + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.usedBufferBytes, 0); + * ``` + */ usedBufferBytes = 0; + /** + * The error + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.err, null); + * ``` + */ err: Error | null = null; + /** + * Construct a {@linkcode AbstractBufBase} instance + * + * @param buf The buffer to use. + */ constructor(buf: Uint8Array) { this.buf = buf; } - /** Size returns the size of the underlying buffer in bytes. */ + /** + * Size returns the size of the underlying buffer in bytes. + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.size(), 1024); + * ``` + * + * @return the size of the buffer in bytes. + */ size(): number { return this.buf.byteLength; } - /** Returns how many bytes are unused in the buffer. */ + /** + * Returns how many bytes are unused in the buffer. + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.available(), 1024); + * ``` + * + * @return the number of bytes that are unused in the buffer. + */ available(): number { return this.buf.byteLength - this.usedBufferBytes; } - /** buffered returns the number of bytes that have been written into the + /** + * buffered returns the number of bytes that have been written into the * current buffer. + * + * @example Usage + * ```ts + * import { AbstractBufBase } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * class MyBuffer extends AbstractBufBase {} + * + * const buf = new Uint8Array(1024); + * const mb = new MyBuffer(buf); + * + * assertEquals(mb.buffered(), 0); + * ``` + * + * @return the number of bytes that have been written into the current buffer. */ buffered(): number { return this.usedBufferBytes; } } -/** BufWriter implements buffering for an deno.Writer object. +/** + * `BufWriter` implements buffering for an {@linkcode Writer} object. * If an error occurs writing to a Writer, no more data will be * accepted and all subsequent writes, and flush(), will return the error. * After all data has been written, the client should call the * flush() method to guarantee all data has been forwarded to * the underlying deno.Writer. * + * @example Usage + * ```ts + * import { BufWriter } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer = { + * write(p: Uint8Array): Promise { + * return Promise.resolve(p.length); + * } + * }; + * + * const bufWriter = new BufWriter(writer); + * const data = new Uint8Array(1024); + * + * await bufWriter.write(data); + * await bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class BufWriter extends AbstractBufBase implements Writer { #writer: Writer; - /** return new BufWriter unless writer is BufWriter */ + /** + * return new BufWriter unless writer is BufWriter + * + * @example Usage + * ```ts + * import { BufWriter } from "@std/io/buf-writer"; + * import { Writer } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: Writer = { + * write(p: Uint8Array): Promise { + * return Promise.resolve(p.length); + * } + * }; + * + * const bufWriter = BufWriter.create(writer); + * const data = new Uint8Array(1024); + * + * await bufWriter.write(data); + * + * assertEquals(bufWriter.buffered(), 1024); + * ``` + * + * @param writer The writer to wrap. + * @param size The size of the buffer. + * + * @return a new {@linkcode BufWriter} instance. + */ static create(writer: Writer, size: number = DEFAULT_BUF_SIZE): BufWriter { return writer instanceof BufWriter ? writer : new BufWriter(writer, size); } + /** + * Construct a new {@linkcode BufWriter} + * + * @param writer The writer to wrap. + * @param size The size of the buffer. + */ constructor(writer: Writer, size: number = DEFAULT_BUF_SIZE) { super(new Uint8Array(size <= 0 ? DEFAULT_BUF_SIZE : size)); this.#writer = writer; } - /** Discards any unflushed buffered data, clears any error, and + /** + * Discards any unflushed buffered data, clears any error, and * resets buffer to write its output to w. + * + * @example Usage + * ```ts + * import { BufWriter } from "@std/io/buf-writer"; + * import { Writer } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: Writer = { + * write(p: Uint8Array): Promise { + * return Promise.resolve(p.length); + * } + * }; + * + * const bufWriter = new BufWriter(writer); + * const data = new Uint8Array(1024); + * + * await bufWriter.write(data); + * + * assertEquals(bufWriter.buffered(), 1024); + * + * bufWriter.reset(writer); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * + * @param w The writer to write to. */ reset(w: Writer) { this.err = null; @@ -64,7 +268,30 @@ export class BufWriter extends AbstractBufBase implements Writer { this.#writer = w; } - /** Flush writes any buffered data to the underlying io.Writer. */ + /** + * Flush writes any buffered data to the underlying io.Writer. + * + * @example Usage + * ```ts + * import { BufWriter } from "@std/io/buf-writer"; + * import { Writer } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: Writer = { + * write(p: Uint8Array): Promise { + * return Promise.resolve(p.length); + * } + * }; + * + * const bufWriter = new BufWriter(writer); + * const data = new Uint8Array(1024); + * + * await bufWriter.write(data); + * await bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + */ async flush() { if (this.err !== null) throw this.err; if (this.usedBufferBytes === 0) return; @@ -86,10 +313,32 @@ export class BufWriter extends AbstractBufBase implements Writer { this.usedBufferBytes = 0; } - /** Writes the contents of `data` into the buffer. If the contents won't fully + /** + * Writes the contents of `data` into the buffer. If the contents won't fully * fit into the buffer, those bytes that are copied into the buffer will be flushed * to the writer and the remaining bytes are then copied into the now empty buffer. * + * @example Usage + * ```ts + * import { BufWriter } from "@std/io/buf-writer"; + * import { Writer } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: Writer = { + * write(p: Uint8Array): Promise { + * return Promise.resolve(p.length); + * } + * }; + * + * const bufWriter = new BufWriter(writer); + * const data = new Uint8Array(1024); + * + * await bufWriter.write(data); + * + * assertEquals(bufWriter.buffered(), 1024); + * ``` + * + * @param data The data to write to the buffer. * @return the number of bytes written to the buffer. */ async write(data: Uint8Array): Promise { @@ -126,19 +375,66 @@ export class BufWriter extends AbstractBufBase implements Writer { } } -/** BufWriterSync implements buffering for a deno.WriterSync object. +/** + * BufWriterSync implements buffering for a deno.WriterSync object. * If an error occurs writing to a WriterSync, no more data will be * accepted and all subsequent writes, and flush(), will return the error. * After all data has been written, the client should call the * flush() method to guarantee all data has been forwarded to * the underlying deno.WriterSync. * + * @example Usage + * ```ts + * import { BufWriterSync } from "@std/io/buf-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer = { + * writeSync(p: Uint8Array): number { + * return p.length; + * } + * }; + * + * const bufWriter = new BufWriterSync(writer); + * const data = new Uint8Array(1024); + * + * bufWriter.writeSync(data); + * bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class BufWriterSync extends AbstractBufBase implements WriterSync { #writer: WriterSync; - /** return new BufWriterSync unless writer is BufWriterSync */ + /** + * return new BufWriterSync unless writer is BufWriterSync + * + * @example Usage + * ```ts + * import { BufWriterSync } from "@std/io/buf-writer"; + * import { WriterSync } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: WriterSync = { + * writeSync(p: Uint8Array): number { + * return p.length; + * } + * }; + * + * const bufWriter = BufWriterSync.create(writer); + * const data = new Uint8Array(1024); + * bufWriter.writeSync(data); + * bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * + * @param writer The writer to wrap. + * @param size The size of the buffer. + * @returns a new {@linkcode BufWriterSync} instance. + */ static create( writer: WriterSync, size: number = DEFAULT_BUF_SIZE, @@ -148,13 +444,43 @@ export class BufWriterSync extends AbstractBufBase implements WriterSync { : new BufWriterSync(writer, size); } + /** + * Construct a new {@linkcode BufWriterSync} + * + * @param writer The writer to wrap. + * @param size The size of the buffer. + */ constructor(writer: WriterSync, size: number = DEFAULT_BUF_SIZE) { super(new Uint8Array(size <= 0 ? DEFAULT_BUF_SIZE : size)); this.#writer = writer; } - /** Discards any unflushed buffered data, clears any error, and + /** + * Discards any unflushed buffered data, clears any error, and * resets buffer to write its output to w. + * + * @example Usage + * ```ts + * import { BufWriterSync } from "@std/io/buf-writer"; + * import { WriterSync } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: WriterSync = { + * writeSync(p: Uint8Array): number { + * return p.length; + * } + * }; + * + * const bufWriter = new BufWriterSync(writer); + * const data = new Uint8Array(1024); + * + * bufWriter.writeSync(data); + * bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * + * @param w The writer to write to. */ reset(w: WriterSync) { this.err = null; @@ -162,7 +488,30 @@ export class BufWriterSync extends AbstractBufBase implements WriterSync { this.#writer = w; } - /** Flush writes any buffered data to the underlying io.WriterSync. */ + /** + * Flush writes any buffered data to the underlying io.WriterSync. + * + * @example Usage + * ```ts + * import { BufWriterSync } from "@std/io/buf-writer"; + * import { WriterSync } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: WriterSync = { + * writeSync(p: Uint8Array): number { + * return p.length; + * } + * }; + * + * const bufWriter = new BufWriterSync(writer); + * const data = new Uint8Array(1024); + * + * bufWriter.writeSync(data); + * bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + */ flush() { if (this.err !== null) throw this.err; if (this.usedBufferBytes === 0) return; @@ -189,6 +538,28 @@ export class BufWriterSync extends AbstractBufBase implements WriterSync { * buffer is the flushed to the writer and the remaining bytes are copied into * the now empty buffer. * + * @example Usage + * ```ts + * import { BufWriterSync } from "@std/io/buf-writer"; + * import { WriterSync } from "@std/io/types"; + * import { assertEquals } from "@std/assert/equals"; + * + * const writer: WriterSync = { + * writeSync(p: Uint8Array): number { + * return p.length; + * } + * }; + * + * const bufWriter = new BufWriterSync(writer); + * const data = new Uint8Array(1024); + * + * bufWriter.writeSync(data); + * bufWriter.flush(); + * + * assertEquals(bufWriter.buffered(), 0); + * ``` + * + * @param data The data to write to the buffer. * @return the number of bytes written to the buffer. */ writeSync(data: Uint8Array): number { diff --git a/io/buffer.ts b/io/buffer.ts index a05bc986de1a..cfd918ecae6b 100644 --- a/io/buffer.ts +++ b/io/buffer.ts @@ -11,7 +11,8 @@ import type { Reader, ReaderSync, Writer, WriterSync } from "./types.ts"; const MIN_READ = 32 * 1024; const MAX_SIZE = 2 ** 32 - 2; -/** A variable-sized buffer of bytes with `read()` and `write()` methods. +/** + * A variable-sized buffer of bytes with `read()` and `write()` methods. * * Buffer is almost always used with some I/O like files and sockets. It allows * one to buffer up a download from a socket. Buffer grows and shrinks as @@ -25,49 +26,145 @@ const MAX_SIZE = 2 ** 32 - 2; * ArrayBuffer. * * Based on {@link https://golang.org/pkg/bytes/#Buffer | Go Buffer}. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, ")); + * await buf.write(new TextEncoder().encode("world!")); + * + * const data = new Uint8Array(13); + * await buf.read(data); + * + * assertEquals(new TextDecoder().decode(data), "Hello, world!"); + * ``` */ - export class Buffer implements Writer, WriterSync, Reader, ReaderSync { #buf: Uint8Array; // contents are the bytes buf[off : len(buf)] #off = 0; // read at buf[off], write at buf[buf.byteLength] + /** + * Constructs a new instance with the specified {@linkcode ArrayBuffer} as its + * initial contents. + * + * @param ab The ArrayBuffer to use as the initial contents of the buffer. + */ constructor(ab?: ArrayBufferLike | ArrayLike) { this.#buf = ab === undefined ? new Uint8Array(0) : new Uint8Array(ab); } - /** Returns a slice holding the unread portion of the buffer. + /** + * Returns a slice holding the unread portion of the buffer. * * The slice is valid for use only until the next buffer modification (that * is, only until the next call to a method like `read()`, `write()`, * `reset()`, or `truncate()`). If `options.copy` is false the slice aliases the buffer content at * least until the next buffer modification, so immediate changes to the * slice will affect the result of future reads. - * @param [options={ copy: true }] + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * + * const slice = buf.bytes(); + * assertEquals(new TextDecoder().decode(slice), "Hello, world!"); + * ``` + * + * @param options The options for the slice. + * @returns A slice holding the unread portion of the buffer. */ bytes(options = { copy: true }): Uint8Array { if (options.copy === false) return this.#buf.subarray(this.#off); return this.#buf.slice(this.#off); } - /** Returns whether the unread portion of the buffer is empty. */ + /** + * Returns whether the unread portion of the buffer is empty. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * assertEquals(buf.empty(), true); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * assertEquals(buf.empty(), false); + * ``` + * + * @returns `true` if the unread portion of the buffer is empty, `false` + * otherwise. + */ empty(): boolean { return this.#buf.byteLength <= this.#off; } - /** A read only number of bytes of the unread portion of the buffer. */ + /** + * A read only number of bytes of the unread portion of the buffer. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * + * assertEquals(buf.length, 13); + * ``` + * + * @returns The number of bytes of the unread portion of the buffer. + */ get length(): number { return this.#buf.byteLength - this.#off; } - /** The read only capacity of the buffer's underlying byte slice, that is, - * the total space allocated for the buffer's data. */ + /** + * The read only capacity of the buffer's underlying byte slice, that is, + * the total space allocated for the buffer's data. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * assertEquals(buf.capacity, 0); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * assertEquals(buf.capacity, 13); + * ``` + * + * @returns The capacity of the buffer. + */ get capacity(): number { return this.#buf.buffer.byteLength; } - /** Discards all but the first `n` unread bytes from the buffer but + /** + * Discards all but the first `n` unread bytes from the buffer but * continues to use the same allocated storage. It throws if `n` is - * negative or greater than the length of the buffer. */ + * negative or greater than the length of the buffer. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * buf.truncate(6); + * assertEquals(buf.length, 6); + * ``` + * + * @param n The number of bytes to keep. + */ truncate(n: number) { if (n === 0) { this.reset(); @@ -79,6 +176,20 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { this.#reslice(this.#off + n); } + /** + * Resets the contents + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * buf.reset(); + * assertEquals(buf.length, 0); + * ``` + */ reset() { this.#reslice(0); this.#off = 0; @@ -100,9 +211,29 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { this.#buf = new Uint8Array(this.#buf.buffer, 0, len); } - /** Reads the next `p.length` bytes from the buffer or until the buffer is + /** + * Reads the next `p.length` bytes from the buffer or until the buffer is * drained. Returns the number of bytes read. If the buffer has no data to - * return, the return is EOF (`null`). */ + * return, the return is EOF (`null`). + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * + * const data = new Uint8Array(5); + * const res = await buf.read(data); + * + * assertEquals(res, 5); + * assertEquals(new TextDecoder().decode(data), "Hello"); + * ``` + * + * @param p The buffer to read data into. + * @returns The number of bytes read. + */ readSync(p: Uint8Array): number | null { if (this.empty()) { // Buffer is empty, reset to recover space. @@ -118,25 +249,85 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { return nread; } - /** Reads the next `p.length` bytes from the buffer or until the buffer is + /** + * Reads the next `p.length` bytes from the buffer or until the buffer is * drained. Resolves to the number of bytes read. If the buffer has no * data to return, resolves to EOF (`null`). * * NOTE: This methods reads bytes synchronously; it's provided for * compatibility with `Reader` interfaces. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * await buf.write(new TextEncoder().encode("Hello, world!")); + * + * const data = new Uint8Array(5); + * const res = await buf.read(data); + * + * assertEquals(res, 5); + * assertEquals(new TextDecoder().decode(data), "Hello"); + * ``` + * + * @param p The buffer to read data into. + * @returns The number of bytes read. */ read(p: Uint8Array): Promise { const rr = this.readSync(p); return Promise.resolve(rr); } + /** + * Writes the given data to the buffer. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * const data = new TextEncoder().encode("Hello, world!"); + * buf.writeSync(data); + * + * const slice = buf.bytes(); + * assertEquals(new TextDecoder().decode(slice), "Hello, world!"); + * ``` + * + * @param p The data to write to the buffer. + * @returns The number of bytes written. + */ writeSync(p: Uint8Array): number { const m = this.#grow(p.byteLength); return copy(p, this.#buf, m); } - /** NOTE: This methods writes bytes synchronously; it's provided for - * compatibility with `Writer` interface. */ + /** + * Writes the given data to the buffer. Resolves to the number of bytes + * written. + * + * > [!NOTE] + * > This methods writes bytes synchronously; it's provided for compatibility + * > with the {@linkcode Writer} interface. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * const data = new TextEncoder().encode("Hello, world!"); + * await buf.write(data); + * + * const slice = buf.bytes(); + * assertEquals(new TextDecoder().decode(slice), "Hello, world!"); + * ``` + * + * @param p The data to write to the buffer. + * @returns The number of bytes written. + */ write(p: Uint8Array): Promise { const n = this.writeSync(p); return Promise.resolve(n); @@ -180,7 +371,20 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { * throw. If the buffer can't grow it will throw an error. * * Based on Go Lang's - * {@link https://golang.org/pkg/bytes/#Buffer.Grow | Buffer.Grow}. */ + * {@link https://golang.org/pkg/bytes/#Buffer.Grow | Buffer.Grow}. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * buf.grow(10); + * assertEquals(buf.capacity, 10); + * ``` + * + * @param n The number of bytes to grow the buffer by. + */ grow(n: number) { if (n < 0) { throw Error("Buffer.grow: negative count"); @@ -189,12 +393,30 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { this.#reslice(m); } - /** Reads data from `r` until EOF (`null`) and appends it to the buffer, + /** + * Reads data from `r` until EOF (`null`) and appends it to the buffer, * growing the buffer as needed. It resolves to the number of bytes read. * If the buffer becomes too large, `.readFrom()` will reject with an error. * * Based on Go Lang's - * {@link https://golang.org/pkg/bytes/#Buffer.ReadFrom | Buffer.ReadFrom}. */ + * {@link https://golang.org/pkg/bytes/#Buffer.ReadFrom | Buffer.ReadFrom}. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { StringReader } from "@std/io/string-reader"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * const r = new StringReader("Hello, world!"); + * const n = await buf.readFrom(r); + * + * assertEquals(n, 13); + * ``` + * + * @param r The reader to read from. + * @returns The number of bytes read. + */ async readFrom(r: Reader): Promise { let n = 0; const tmp = new Uint8Array(MIN_READ); @@ -224,7 +446,24 @@ export class Buffer implements Writer, WriterSync, Reader, ReaderSync { * buffer becomes too large, `.readFromSync()` will throw an error. * * Based on Go Lang's - * {@link https://golang.org/pkg/bytes/#Buffer.ReadFrom | Buffer.ReadFrom}. */ + * {@link https://golang.org/pkg/bytes/#Buffer.ReadFrom | Buffer.ReadFrom}. + * + * @example Usage + * ```ts + * import { Buffer } from "@std/io/buffer"; + * import { StringReader } from "@std/io/string-reader"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new Buffer(); + * const r = new StringReader("Hello, world!"); + * const n = buf.readFromSync(r); + * + * assertEquals(n, 13); + * ``` + * + * @param r The reader to read from. + * @returns The number of bytes read. + */ readFromSync(r: ReaderSync): number { let n = 0; const tmp = new Uint8Array(MIN_READ); diff --git a/io/copy.ts b/io/copy.ts index 45aecaa6b780..ac6520025f2e 100644 --- a/io/copy.ts +++ b/io/copy.ts @@ -10,8 +10,8 @@ import type { Reader, Writer } from "./types.ts"; * an error occurs. It resolves to the number of bytes copied or rejects with * the first error encountered while copying. * - * @example - * ```ts + * @example Usage + * ```ts no-eval * import { copy } from "@std/io/copy"; * * const source = await Deno.open("my_file.txt"); diff --git a/io/copy_n.ts b/io/copy_n.ts index e62300686311..1c81febb01cd 100644 --- a/io/copy_n.ts +++ b/io/copy_n.ts @@ -7,9 +7,22 @@ const DEFAULT_BUFFER_SIZE = 32 * 1024; /** * Copy N size at the most. If read size is lesser than N, then returns nread + * + * @example Usage + * ```ts + * import { copyN } from "@std/io/copy-n"; + * import { assertEquals } from "@std/assert/equals"; + * + * const source = await Deno.open("README.md"); + * + * const res = await copyN(source, Deno.stdout, 10); + * assertEquals(res, 10); + * ``` + * * @param r Reader * @param dest Writer * @param size Read size + * @returns Number of bytes copied * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/iterate_reader.ts b/io/iterate_reader.ts index a989f9300307..9d24800f0090 100644 --- a/io/iterate_reader.ts +++ b/io/iterate_reader.ts @@ -9,24 +9,21 @@ export type { Reader, ReaderSync }; /** * Turns a {@linkcode Reader} into an async iterator. * - * @example - * ```ts + * @example Usage + * ```ts no-assert * import { iterateReader } from "@std/io/iterate-reader"; * - * using file = await Deno.open("/etc/passwd"); + * using file = await Deno.open("README.md"); * for await (const chunk of iterateReader(file)) { * console.log(chunk); * } * ``` * - * Second argument can be used to tune size of a buffer. - * Default size of the buffer is 32kB. - * - * @example - * ```ts + * @example Usage with buffer size + * ```ts no-assert * import { iterateReader } from "@std/io/iterate-reader"; * - * using file = await Deno.open("/etc/passwd"); + * using file = await Deno.open("README.md"); * const iter = iterateReader(file, { * bufSize: 1024 * 1024 * }); @@ -34,6 +31,11 @@ export type { Reader, ReaderSync }; * console.log(chunk); * } * ``` + * + * @param reader The reader to read from + * @param options The options + * @param options.bufSize The size of the buffer to use + * @returns The async iterator of Uint8Array chunks */ export async function* iterateReader( reader: Reader, @@ -56,27 +58,31 @@ export async function* iterateReader( /** * Turns a {@linkcode ReaderSync} into an iterator. * + * @example Usage * ```ts * import { iterateReaderSync } from "@std/io/iterate-reader"; + * import { assert } from "@std/assert/assert" * - * using file = Deno.openSync("/etc/passwd"); + * using file = Deno.openSync("README.md"); * for (const chunk of iterateReaderSync(file)) { - * console.log(chunk); + * assert(chunk instanceof Uint8Array); * } * ``` * * Second argument can be used to tune size of a buffer. * Default size of the buffer is 32kB. * + * @example Usage with buffer size * ```ts * import { iterateReaderSync } from "@std/io/iterate-reader"; - - * using file = await Deno.open("/etc/passwd"); + * import { assert } from "@std/assert/assert" + * + * using file = await Deno.open("README.md"); * const iter = iterateReaderSync(file, { * bufSize: 1024 * 1024 * }); * for (const chunk of iter) { - * console.log(chunk); + * assert(chunk instanceof Uint8Array); * } * ``` * @@ -84,6 +90,10 @@ export async function* iterateReader( * a view on that buffer on each iteration. It is therefore caller's * responsibility to copy contents of the buffer if needed; otherwise the * next iteration will overwrite contents of previously returned chunk. + * + * @param reader The reader to read from + * @param options The options + * @returns The iterator of Uint8Array chunks */ export function* iterateReaderSync( reader: ReaderSync, diff --git a/io/limited_reader.ts b/io/limited_reader.ts index 78fc0ed44ec2..982cca6ed77a 100644 --- a/io/limited_reader.ts +++ b/io/limited_reader.ts @@ -1,26 +1,97 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. +import type { Reader } from "./types.ts"; + /** - * A `LimitedReader` reads from `reader` but limits the amount of data returned to just `limit` bytes. + * Reads from `reader` but limits the amount of data returned to just `limit` bytes. * Each call to `read` updates `limit` to reflect the new amount remaining. * `read` returns `null` when `limit` <= `0` or * when the underlying `reader` returns `null`. - */ -import type { Reader } from "./types.ts"; - -/** + * + * @example Usage + * ```ts + * import { StringReader } from "@std/io/string-reader"; + * import { LimitedReader } from "@std/io/limited-reader"; + * import { readAll } from "@std/io/read-all"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r = new StringReader("hello world"); + * const lr = new LimitedReader(r, 5); + * const res = await readAll(lr); + * + * assertEquals(new TextDecoder().decode(res), "hello"); + * ``` + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class LimitedReader implements Reader { + /** + * The reader to read from + * + * @example Usage + * ```ts + * import { StringReader } from "@std/io/string-reader"; + * import { LimitedReader } from "@std/io/limited-reader"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r = new StringReader("hello world"); + * const lr = new LimitedReader(r, 5); + * + * assertEquals(lr.reader, r); + * ``` + */ reader: Reader; + /** + * The number of bytes to limit the reader to + * + * @example Usage + * ```ts + * import { StringReader } from "@std/io/string-reader"; + * import { LimitedReader } from "@std/io/limited-reader"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r = new StringReader("hello world"); + * const lr = new LimitedReader(r, 5); + * + * assertEquals(lr.limit, 5); + * ``` + */ limit: number; + /** + * Construct a new instance. + * + * @param reader The reader to read from. + * @param limit The number of bytes to limit the reader to. + */ constructor(reader: Reader, limit: number) { this.reader = reader; this.limit = limit; } + /** + * Reads data from the reader. + * + * @example Usage + * ```ts + * import { StringReader } from "@std/io/string-reader"; + * import { LimitedReader } from "@std/io/limited-reader"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r = new StringReader("hello world"); + * const lr = new LimitedReader(r, 5); + * + * const data = new Uint8Array(5); + * const res = await lr.read(data); + * + * assertEquals(res, 5); + * assertEquals(new TextDecoder().decode(data), "hello"); + * ``` + * + * @param p The buffer to read data into. + * @returns The number of bytes read. + */ async read(p: Uint8Array): Promise { if (this.limit <= 0) { return null; diff --git a/io/mod.ts b/io/mod.ts index 56750e0df945..7694718f0441 100644 --- a/io/mod.ts +++ b/io/mod.ts @@ -6,6 +6,13 @@ * `Reader` and `Writer` interfaces are deprecated in Deno, and so many of these * utilities are also deprecated. Consider using web streams instead. * + * ```ts no-assert + * import { toReadableStream, toWritableStream } from "@std/io"; + * + * await toReadableStream(Deno.stdin) + * .pipeTo(toWritableStream(Deno.stdout)); + * ``` + * * @module */ diff --git a/io/multi_reader.ts b/io/multi_reader.ts index 69d37c4e3336..d9bb206e8554 100644 --- a/io/multi_reader.ts +++ b/io/multi_reader.ts @@ -4,7 +4,23 @@ import type { Reader } from "./types.ts"; /** - * Reader utility for combining multiple readers + * Reader utility for combining multiple readers. + * + * @example Usage + * ```ts + * import { MultiReader } from "@std/io/multi-reader"; + * import { StringReader } from "@std/io/string-reader"; + * import { readAll } from "@std/io/read-all"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r1 = new StringReader("hello"); + * const r2 = new StringReader("world"); + * const mr = new MultiReader([r1, r2]); + * + * const res = await readAll(mr); + * + * assertEquals(new TextDecoder().decode(res), "helloworld"); + * ``` * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ @@ -12,10 +28,46 @@ export class MultiReader implements Reader { readonly #readers: Reader[]; #currentIndex = 0; + /** + * Construct a new instance. + * + * @param readers The readers to combine. + */ constructor(readers: Reader[]) { this.#readers = [...readers]; } + /** + * Reads data from the readers. + * + * @example Usage + * ```ts + * import { MultiReader } from "@std/io/multi-reader"; + * import { StringReader } from "@std/io/string-reader"; + * import { readAll } from "@std/io/read-all"; + * import { assertEquals } from "@std/assert/equals"; + * + * const r1 = new StringReader("hello"); + * const r2 = new StringReader("world"); + * const mr = new MultiReader([r1, r2]); + * + * const data = new Uint8Array(5); + * const res = await mr.read(data); + * + * assertEquals(res, 5); + * assertEquals(new TextDecoder().decode(data), "hello"); + * + * const res2 = await mr.read(data); + * assertEquals(res2, 0); + * + * const res3 = await mr.read(data); + * assertEquals(res3, 5); + * assertEquals(new TextDecoder().decode(data), "world"); + * ``` + * + * @param p The buffer to read data into. + * @returns The number of bytes read. + */ async read(p: Uint8Array): Promise { const r = this.#readers[this.#currentIndex]; if (!r) return null; diff --git a/io/read_all.ts b/io/read_all.ts index 7264ef44309e..89aa965d81f5 100644 --- a/io/read_all.ts +++ b/io/read_all.ts @@ -9,8 +9,8 @@ import type { Reader, ReaderSync } from "./types.ts"; * Read {@linkcode Reader} `r` until EOF (`null`) and resolve to the content as * {@linkcode Uint8Array}. * - * @example - * ```ts + * @example Usage + * ```ts no-eval * import { readAll } from "@std/io/read-all"; * * // Example from stdin @@ -20,6 +20,9 @@ import type { Reader, ReaderSync } from "./types.ts"; * using file = await Deno.open("my_file.txt", {read: true}); * const myFileContent = await readAll(file); * ``` + * + * @param reader The reader to read from + * @returns The content as Uint8Array */ export async function readAll(reader: Reader): Promise { const chunks: Uint8Array[] = []; @@ -41,8 +44,8 @@ export async function readAll(reader: Reader): Promise { * Synchronously reads {@linkcode ReaderSync} `r` until EOF (`null`) and returns * the content as {@linkcode Uint8Array}. * - * @example - * ```ts + * @example Usage + * ```ts no-eval * import { readAllSync } from "@std/io/read-all"; * * // Example from stdin @@ -52,6 +55,9 @@ export async function readAll(reader: Reader): Promise { * using file = Deno.openSync("my_file.txt", {read: true}); * const myFileContent = readAllSync(file); * ``` + * + * @param reader The reader to read from + * @returns The content as Uint8Array */ export function readAllSync(reader: ReaderSync): Uint8Array { const chunks: Uint8Array[] = []; diff --git a/io/read_delim.ts b/io/read_delim.ts index 721185cebf1d..62645f0c1282 100644 --- a/io/read_delim.ts +++ b/io/read_delim.ts @@ -26,7 +26,23 @@ function createLPS(pat: Uint8Array): Uint8Array { } /** - * Read delimited bytes from a Reader. + * Read delimited bytes from a {@linkcode Reader} through an + * {@linkcode AsyncIterableIterator} of {@linkcode Uint8Array}. + * + * @example Usage + * ```ts + * import { readDelim } from "@std/io/read-delim"; + * import { assert } from "@std/assert/assert" + * + * const fileReader = await Deno.open("README.md"); + * for await (const chunk of readDelim(fileReader, new TextEncoder().encode("\n"))) { + * assert(chunk instanceof Uint8Array); + * } + * ``` + * + * @param reader The reader to read from + * @param delim The delimiter to read until + * @returns The {@linkcode AsyncIterableIterator} of {@linkcode Uint8Array}s. * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/read_int.ts b/io/read_int.ts index a46621c210f1..2812423d6e9b 100644 --- a/io/read_int.ts +++ b/io/read_int.ts @@ -4,8 +4,21 @@ import type { BufReader } from "./buf_reader.ts"; import { readShort } from "./read_short.ts"; /** - * Read big endian 32bit integer from BufReader - * @param buf + * Read big endian 32bit integer from a {@linkcode BufReader}. + * + * @example Usage + * ```ts + * import { BufReader } from "@std/io/buf-reader"; + * import { readInt } from "@std/io/read-int"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new BufReader(new Deno.Buffer(new Uint8Array([0x12, 0x34, 0x56, 0x78]))); + * const int = await readInt(buf); + * assertEquals(int, 0x12345678); + * ``` + * + * @param buf The buffer reader to read from + * @returns The 32bit integer * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/read_lines.ts b/io/read_lines.ts index c4a7e6bfd488..384b4d398e1e 100644 --- a/io/read_lines.ts +++ b/io/read_lines.ts @@ -6,21 +6,24 @@ import { BufReader } from "./buf_reader.ts"; import { concat } from "@std/bytes/concat"; /** - * Read strings line-by-line from a Reader. + * Read strings line-by-line from a {@linkcode Reader}. * - * @example + * @example Usage * ```ts * import { readLines } from "@std/io/read-lines"; - * import * as path from "@std/path"; + * import { assert } from "@std/assert/assert" * - * const filename = path.join(Deno.cwd(), "std/io/README.md"); - * let fileReader = await Deno.open(filename); + * let fileReader = await Deno.open("README.md"); * * for await (let line of readLines(fileReader)) { - * console.log(line); + * assert(typeof line === "string"); * } * ``` * + * @param reader The reader to read from + * @param decoderOpts The options + * @returns The async iterator of strings + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export async function* readLines( diff --git a/io/read_long.ts b/io/read_long.ts index 9725ffa48d93..35f201610527 100644 --- a/io/read_long.ts +++ b/io/read_long.ts @@ -6,8 +6,23 @@ import { readInt } from "./read_int.ts"; const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER); /** - * Read big endian 64bit long from BufReader - * @param buf + * Read big endian 64bit long from a {@linkcode BufReader}. + * + * @example Usage + * ```ts + * import { BufReader } from "@std/io/buf-reader"; + * import { readLong } from "@std/io/read-long"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new BufReader(new Deno.Buffer(new Uint8Array([0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x9a]))); + * const long = await readLong(buf); + * assertEquals(long, 0x123456789a); + * ``` + * + * @param buf The BufReader to read from + * @returns The 64bit long + * @throws {Deno.errors.UnexpectedEof} If the reader returns unexpected EOF + * @throws {RangeError} If the long value is too big to be represented as a JavaScript number * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/read_range.ts b/io/read_range.ts index 3b0c15e81716..aa2e9e01d56e 100644 --- a/io/read_range.ts +++ b/io/read_range.ts @@ -6,6 +6,8 @@ import type { Reader, ReaderSync } from "./types.ts"; const DEFAULT_BUFFER_SIZE = 32 * 1024; /** + * The range of bytes to read from a file or other resource that is readable. + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export interface ByteRange { @@ -21,7 +23,8 @@ export interface ByteRange { * seekable. The range start and end are inclusive of the bytes within that * range. * - * ```ts + * @example Usage + * ```ts no-eval * import { assertEquals } from "@std/assert"; * import { readRange } from "@std/io/read-range"; * @@ -31,6 +34,10 @@ export interface ByteRange { * assertEquals(bytes.length, 10); * ``` * + * @param r The reader to read from + * @param range The range of bytes to read + * @returns The bytes read + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export async function readRange( @@ -67,7 +74,8 @@ export async function readRange( * readable and seekable. The range start and end are inclusive of the bytes * within that range. * - * ```ts + * @example Usage + * ```ts no-eval * import { assertEquals } from "@std/assert"; * import { readRangeSync } from "@std/io/read-range"; * @@ -77,6 +85,10 @@ export async function readRange( * assertEquals(bytes.length, 10); * ``` * + * @param r The reader to read from + * @param range The range of bytes to read + * @returns The bytes read + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export function readRangeSync( diff --git a/io/read_short.ts b/io/read_short.ts index 690bff6dbf6b..617dfeb0eb77 100644 --- a/io/read_short.ts +++ b/io/read_short.ts @@ -3,8 +3,21 @@ import type { BufReader } from "./buf_reader.ts"; /** - * Read big endian 16bit short from BufReader - * @param buf + * Read big endian 16bit short from a {@linkcode BufReader}. + * + * @example Usage + * ```ts + * import { BufReader } from "@std/io/buf-reader"; + * import { readShort } from "@std/io/read-short"; + * import { assertEquals } from "@std/assert/equals"; + * + * const buf = new BufReader(new Deno.Buffer(new Uint8Array([0x12, 0x34]))); + * const short = await readShort(buf); + * assertEquals(short, 0x1234); + * ``` + * + * @param buf The reader to read from + * @returns The 16bit short * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/read_string_delim.ts b/io/read_string_delim.ts index 30fe5a0c72dd..7a34eddd8e3d 100644 --- a/io/read_string_delim.ts +++ b/io/read_string_delim.ts @@ -5,21 +5,25 @@ import type { Reader } from "./types.ts"; import { readDelim } from "./read_delim.ts"; /** - * Read Reader chunk by chunk, splitting based on delimiter. + * Read {@linkcode Reader} chunk by chunk, splitting based on delimiter. * - * @example + * @example Usage * ```ts * import { readStringDelim } from "@std/io/read-string-delim"; - * import * as path from "@std/path"; + * import { assert } from "@std/assert/assert" * - * const filename = path.join(Deno.cwd(), "std/io/README.md"); - * let fileReader = await Deno.open(filename); + * let fileReader = await Deno.open("README.md"); * * for await (let line of readStringDelim(fileReader, "\n")) { - * console.log(line); + * assert(typeof line === "string"); * } * ``` * + * @param reader The reader to read from + * @param delim The delimiter to split the reader by + * @param decoderOpts The options + * @returns The async iterator of strings + * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export async function* readStringDelim( diff --git a/io/reader_from_stream_reader.ts b/io/reader_from_stream_reader.ts index e7d8b3c356b2..279764157e71 100644 --- a/io/reader_from_stream_reader.ts +++ b/io/reader_from_stream_reader.ts @@ -8,17 +8,19 @@ import type { Reader } from "./types.ts"; /** * Create a {@linkcode Reader} from a {@linkcode ReadableStreamDefaultReader}. * - * @example - * ```ts + * @example Usage + * ```ts no-assert * import { copy } from "@std/io/copy"; * import { readerFromStreamReader } from "@std/io/reader-from-stream-reader"; * * const res = await fetch("https://deno.land"); - * using file = await Deno.open("./deno.land.html", { create: true, write: true }); * * const reader = readerFromStreamReader(res.body!.getReader()); - * await copy(reader, file); + * await copy(reader, Deno.stdout); * ``` + * + * @param streamReader The stream reader to read from + * @returns The reader */ export function readerFromStreamReader( streamReader: ReadableStreamDefaultReader, diff --git a/io/slice_long_to_bytes.ts b/io/slice_long_to_bytes.ts index 60b4fc24bca9..f401652b3c09 100644 --- a/io/slice_long_to_bytes.ts +++ b/io/slice_long_to_bytes.ts @@ -2,9 +2,20 @@ // This module is browser compatible. /** - * Slice number into 64bit big endian byte array + * Slice number into 64bit big endian byte array. + * + * @example Usage + * ```ts + * import { sliceLongToBytes } from "@std/io/slice-long-to-bytes"; + * import { assertEquals } from "@std/assert/equals"; + * + * const dest = sliceLongToBytes(0x123456789a); + * assertEquals(dest, [0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x9a]); + * ``` + * * @param d The number to be sliced - * @param dest The sliced array + * @param dest The array to store the sliced bytes + * @returns The sliced bytes * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ diff --git a/io/string_reader.ts b/io/string_reader.ts index 787697725cf8..f97d6b70c63f 100644 --- a/io/string_reader.ts +++ b/io/string_reader.ts @@ -6,35 +6,29 @@ import { Buffer } from "./buffer.ts"; /** * Reader utility for strings. * - * @example + * @example Usage * ```ts * import { StringReader } from "@std/io/string-reader"; + * import { assertEquals } from "@std/assert/equals"; * * const data = new Uint8Array(6); * const r = new StringReader("abcdef"); * const res0 = await r.read(data); * const res1 = await r.read(new Uint8Array(6)); * - * // Number of bytes read - * console.log(res0); // 6 - * console.log(res1); // null, no byte left to read. EOL - * - * // text - * - * console.log(new TextDecoder().decode(data)); // abcdef - * ``` - * - * **Output:** - * - * ```text - * 6 - * null - * abcdef + * assertEquals(res0, 6); + * assertEquals(res1, null); + * assertEquals(new TextDecoder().decode(data), "abcdef"); * ``` * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. */ export class StringReader extends Buffer { + /** + * Construct a new instance. + * + * @param s The string to read. + */ constructor(s: string) { super(new TextEncoder().encode(s).buffer); } diff --git a/io/string_writer.ts b/io/string_writer.ts index b08c38ee25c2..63ca18cd0647 100644 --- a/io/string_writer.ts +++ b/io/string_writer.ts @@ -8,7 +8,7 @@ const decoder = new TextDecoder(); /** * Writer utility for buffering string chunks. * - * @example + * @example Usage * ```ts * import { * copyN, @@ -16,23 +16,16 @@ const decoder = new TextDecoder(); * StringWriter, * } from "@std/io"; * import { copy } from "@std/io/copy"; + * import { assertEquals } from "@std/assert/equals"; * * const w = new StringWriter("base"); * const r = new StringReader("0123456789"); * await copyN(r, w, 4); // copy 4 bytes * - * // Number of bytes read - * console.log(w.toString()); //base0123 + * assertEquals(w.toString(), "base0123"); * * await copy(r, w); // copy all - * console.log(w.toString()); // base0123456789 - * ``` - * - * **Output:** - * - * ```text - * base0123 - * base0123456789 + * assertEquals(w.toString(), "base0123456789"); * ``` * * @deprecated This will be removed in 1.0.0. Use the {@link https://developer.mozilla.org/en-US/docs/Web/API/Streams_API | Web Streams API} instead. @@ -43,6 +36,11 @@ export class StringWriter implements Writer, WriterSync { #cache: string | undefined; #base: string; + /** + * Construct a new instance. + * + * @param base The base string to write to the buffer. + */ constructor(base = "") { const c = new TextEncoder().encode(base); this.#chunks.push(c); @@ -50,10 +48,42 @@ export class StringWriter implements Writer, WriterSync { this.#base = base; } + /** + * Writes the bytes to the buffer asynchronously. + * + * @example Usage + * ```ts + * import { StringWriter } from "@std/io/string-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const w = new StringWriter("base"); + * await w.write(new TextEncoder().encode("0123")); + * assertEquals(w.toString(), "base0123"); + * ``` + * + * @param p The bytes to write to the buffer. + * @returns The number of bytes written to the buffer in total. + */ write(p: Uint8Array): Promise { return Promise.resolve(this.writeSync(p)); } + /** + * Writes the bytes to the buffer synchronously. + * + * @example Usage + * ```ts + * import { StringWriter } from "@std/io/string-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const w = new StringWriter("base"); + * w.writeSync(new TextEncoder().encode("0123")); + * assertEquals(w.toString(), "base0123"); + * ``` + * + * @param p The bytes to write to the buffer. + * @returns The number of bytes written to the buffer in total. + */ writeSync(p: Uint8Array): number { this.#chunks.push(new Uint8Array(p)); this.#byteLength += p.byteLength; @@ -61,6 +91,21 @@ export class StringWriter implements Writer, WriterSync { return p.byteLength; } + /** + * Returns the string written to the buffer. + * + * @example Usage + * ```ts + * import { StringWriter } from "@std/io/string-writer"; + * import { assertEquals } from "@std/assert/equals"; + * + * const w = new StringWriter("base"); + * await w.write(new TextEncoder().encode("0123")); + * assertEquals(w.toString(), "base0123"); + * ``` + * + * @returns the string written to the buffer. + */ toString(): string { if (this.#cache) { return this.#cache; diff --git a/io/to_readable_stream.ts b/io/to_readable_stream.ts index ade9ed043cfe..8c9586259941 100644 --- a/io/to_readable_stream.ts +++ b/io/to_readable_stream.ts @@ -14,11 +14,14 @@ export interface ToReadableStreamOptions { */ autoClose?: boolean; - /** The size of chunks to allocate to read, the default is ~16KiB, which is - * the maximum size that Deno operations can currently support. */ + /** + * The size of chunks to allocate to read. + * + * @default {16640} + */ chunkSize?: number; - /** The queuing strategy to create the `ReadableStream` with. */ + /** The queuing strategy to create the {@linkcode ReadableStream} with. */ strategy?: QueuingStrategy; } @@ -30,13 +33,17 @@ export interface ToReadableStreamOptions { * will be read. When `null` is returned from the reader, the stream will be * closed along with the reader (if it is also a `Closer`). * - * @example - * ```ts + * @example Usage + * ```ts no-assert * import { toReadableStream } from "@std/io/to-readable-stream"; * - * const file = await Deno.open("./file.txt", { read: true }); + * const file = await Deno.open("./README.md", { read: true }); * const fileStream = toReadableStream(file); * ``` + * + * @param reader The reader to read from + * @param options The options + * @returns The readable stream */ export function toReadableStream( reader: Reader | (Reader & Closer), diff --git a/io/to_writable_stream.ts b/io/to_writable_stream.ts index 8299828a1379..ace7a4dc3b29 100644 --- a/io/to_writable_stream.ts +++ b/io/to_writable_stream.ts @@ -19,15 +19,18 @@ export interface toWritableStreamOptions { /** * Create a {@linkcode WritableStream} from a {@linkcode Writer}. * - * @example - * ```ts + * @example Usage + * ```ts no-assert * import { toWritableStream } from "@std/io/to-writable-stream"; * - * const file = await Deno.open("./file.txt", { create: true, write: true }); - * await ReadableStream.from("Hello World") + * await ReadableStream.from(["Hello World"]) * .pipeThrough(new TextEncoderStream()) - * .pipeTo(toWritableStream(file)); + * .pipeTo(toWritableStream(Deno.stdout)); * ``` + * + * @param writer The writer to write to + * @param options The options + * @returns The writable stream */ export function toWritableStream( writer: Writer, diff --git a/io/write_all.ts b/io/write_all.ts index 152ff06185f8..ccb09f64138c 100644 --- a/io/write_all.ts +++ b/io/write_all.ts @@ -6,19 +6,25 @@ import type { Writer, WriterSync } from "./types.ts"; /** * Write all the content of the array buffer (`arr`) to the writer (`w`). * - * @example - * ```ts + * @example Writing to stdout + * ```ts no-assert * import { writeAll } from "@std/io/write-all"; - - * // Example writing to stdout - * let contentBytes = new TextEncoder().encode("Hello World"); + * + * const contentBytes = new TextEncoder().encode("Hello World"); * await writeAll(Deno.stdout, contentBytes); + * ``` + * + * @example Writing to file + * ```ts no-eval no-assert + * import { writeAll } from "@std/io/write-all"; * - * // Example writing to file - * contentBytes = new TextEncoder().encode("Hello World"); - * using file = await Deno.open('test.file', {write: true}); + * const contentBytes = new TextEncoder().encode("Hello World"); + * using file = await Deno.open('test.file', { write: true }); * await writeAll(file, contentBytes); * ``` + * + * @param writer The writer to write to + * @param data The data to write */ export async function writeAll(writer: Writer, data: Uint8Array) { let nwritten = 0; @@ -31,19 +37,25 @@ export async function writeAll(writer: Writer, data: Uint8Array) { * Synchronously write all the content of the array buffer (`arr`) to the * writer (`w`). * - * @example - * ```ts + * @example "riting to stdout + * ```ts no-assert * import { writeAllSync } from "@std/io/write-all"; * - * // Example writing to stdout - * let contentBytes = new TextEncoder().encode("Hello World"); + * const contentBytes = new TextEncoder().encode("Hello World"); * writeAllSync(Deno.stdout, contentBytes); + * ``` * - * // Example writing to file - * contentBytes = new TextEncoder().encode("Hello World"); - * using file = Deno.openSync('test.file', {write: true}); + * @example Writing to file + * ```ts no-eval no-assert + * import { writeAllSync } from "@std/io/write-all"; + * + * const contentBytes = new TextEncoder().encode("Hello World"); + * using file = Deno.openSync("test.file", { write: true }); * writeAllSync(file, contentBytes); * ``` + * + * @param writer The writer to write to + * @param data The data to write */ export function writeAllSync(writer: WriterSync, data: Uint8Array) { let nwritten = 0; From 89712aca0f420ec3ec93a8d062103409f13da51f Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Fri, 9 Aug 2024 09:57:05 +0200 Subject: [PATCH 5/8] clean diff --- datetime/_date_time_formatter.ts | 125 +++++++++---------------------- 1 file changed, 37 insertions(+), 88 deletions(-) diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index afba65c75690..c6a09f952774 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -37,13 +37,6 @@ type FormatPart = { hour12?: boolean; }; -function throwInvalidValueError(type: string, value: string, string: string) { - if (!value) { - throw Error( - `value not valid for part { ${type} ${value} } ${string.slice(0, 25)}`, - ); - } -} const QUOTED_LITERAL_REGEXP = /^(')(?\\.|[^\']*)\1/; const LITERAL_REGEXP = /^(?.+?\s*)/; const SYMBOL_REGEXP = /^(?([a-zA-Z])\2*)/; @@ -330,27 +323,19 @@ export class DateTimeFormatter { formatToParts(string: string): DateTimeFormatPart[] { const parts: DateTimeFormatPart[] = []; - let index = 0; for (const part of this.#formatParts) { - const substring = string.slice(index); const type = part.type; let value = ""; - switch (type) { + switch (part.type) { case "year": { switch (part.value) { case "numeric": { - value = /^\d{1,4}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,4}/.exec(string)?.[0] as string; break; } case "2-digit": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; break; } default: @@ -363,38 +348,23 @@ export class DateTimeFormatter { case "month": { switch (part.value) { case "numeric": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { - value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{2}/.exec(string)?.[0] as string; break; } case "narrow": { - value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } case "short": { - value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } case "long": { - value = /^[a-zA-Z]+/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } default: @@ -407,17 +377,11 @@ export class DateTimeFormatter { case "day": { switch (part.value) { case "numeric": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { - value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{2}/.exec(string)?.[0] as string; break; } default: @@ -430,10 +394,7 @@ export class DateTimeFormatter { case "hour": { switch (part.value) { case "numeric": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; if (part.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'H' instead of 'h'.`, @@ -442,10 +403,7 @@ export class DateTimeFormatter { break; } case "2-digit": { - value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{2}/.exec(string)?.[0] as string; if (part.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`, @@ -463,17 +421,11 @@ export class DateTimeFormatter { case "minute": { switch (part.value) { case "numeric": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { - value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{2}/.exec(string)?.[0] as string; break; } default: @@ -486,17 +438,11 @@ export class DateTimeFormatter { case "second": { switch (part.value) { case "numeric": { - value = /^\d{1,2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { - value = /^\d{2}/.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; + value = /^\d{2}/.exec(string)?.[0] as string; break; } default: @@ -507,23 +453,16 @@ export class DateTimeFormatter { break; } case "fractionalSecond": { - value = new RegExp(`^\\d{${part.value}}`).exec(substring) + value = new RegExp(`^\\d{${part.value}}`).exec(string) ?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; break; } case "timeZoneName": { value = part.value as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; break; } case "dayPeriod": { - value = /^[AP](?:\.M\.|M\.?)/i.exec(substring)?.[0] as string; - if (!value) throwInvalidValueError(type, value, substring); + value = /^[AP](?:\.M\.|M\.?)/i.exec(string)?.[0] as string; switch (value.toUpperCase()) { case "AM": case "AM.": @@ -538,30 +477,40 @@ export class DateTimeFormatter { default: throw new Error(`dayPeriod '${value}' is not supported.`); } - parts.push({ type, value }); - index += value.length; break; } case "literal": { - if (!substring.startsWith(part.value as string)) { + if (!string.startsWith(part.value as string)) { throw Error( - `Literal "${part.value}" not found "${substring.slice(0, 25)}"`, + `Literal "${part.value}" not found "${string.slice(0, 25)}"`, ); } value = part.value as string; - if (!value) throwInvalidValueError(type, value, substring); - parts.push({ type, value }); - index += value.length; break; } + default: throw Error(`${part.type} ${part.value}`); } + + if (!value) { + throw Error( + `value not valid for part { ${type} ${value} } ${ + string.slice( + 0, + 25, + ) + }`, + ); + } + parts.push({ type, value }); + + string = string.slice(value.length); } - if (index < string.length) { + if (string.length) { throw Error( - `datetime string was not fully parsed! ${string.slice(index)}`, + `datetime string was not fully parsed! ${string.slice(0, 25)}`, ); } From 4ae2c0a03a789545712fa6af9a891fc300dbaa59 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 9 Aug 2024 10:15:27 +0200 Subject: [PATCH 6/8] add pm tests --- datetime/_date_time_formatter_test.ts | 126 ++++++++++++++++++++------ 1 file changed, 98 insertions(+), 28 deletions(-) diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index 06891c886ff9..e8f3dd62753d 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -137,7 +137,7 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { +date, ); }); -Deno.test("dateTimeFormatter.partsToDate() works with dayPeriod", () => { +Deno.test("dateTimeFormatter.partsToDate() works with am dayPeriod", () => { const date = new Date("2020-01-01T00:00:00.000Z"); using _time = new FakeTime(date); const format = "HH a"; @@ -191,45 +191,115 @@ Deno.test("dateTimeFormatter.partsToDate() works with dayPeriod", () => { +date, ); }); -Deno.test("dateTimeFormatter.partsToDate() throws with invalid dayPeriods", () => { - const date = new Date("2020-01-01T00:00:00.000Z"); +Deno.test("dateTimeFormatter.partsToDate() works with pm dayPeriod", () => { + const date = new Date("2020-01-01T13:00:00.000Z"); using _time = new FakeTime(date); const format = "HH a"; const formatter = new DateTimeFormatter(format); - assertThrows(() => - formatter.partsToDate([ - { type: "hour", value: "00" }, - { type: "dayPeriod", value: "A.M" }, + + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "PM" }, { type: "timeZoneName", value: "UTC" }, - ]) + ]), + +date, ); - assertThrows(() => - formatter.partsToDate([ - { type: "hour", value: "00" }, - { type: "dayPeriod", value: "a.m" }, + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "PM." }, { type: "timeZoneName", value: "UTC" }, - ]) + ]), + +date, ); - assertThrows(() => - formatter.partsToDate([ - { type: "hour", value: "00" }, - { type: "dayPeriod", value: "P.M" }, + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "P.M." }, { type: "timeZoneName", value: "UTC" }, - ]) + ]), + +date, ); - assertThrows(() => - formatter.partsToDate([ - { type: "hour", value: "00" }, - { type: "dayPeriod", value: "p.m" }, + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "pm" }, { type: "timeZoneName", value: "UTC" }, - ]) + ]), + +date, ); - assertThrows(() => - formatter.partsToDate([ - { type: "hour", value: "00" }, - { type: "dayPeriod", value: "noon" }, + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "pm." }, { type: "timeZoneName", value: "UTC" }, - ]) + ]), + +date, + ); + assertEquals( + +formatter.partsToDate([ + { type: "hour", value: "01" }, + { type: "dayPeriod", value: "p.m." }, + { type: "timeZoneName", value: "UTC" }, + ]), + +date, + ); +}); +Deno.test("dateTimeFormatter.partsToDate() throws with invalid dayPeriods", () => { + const date = new Date("2020-01-01T00:00:00.000Z"); + using _time = new FakeTime(date); + const format = "HH a"; + const formatter = new DateTimeFormatter(format); + assertThrows( + () => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "A.M" }, + { type: "timeZoneName", value: "UTC" }, + ]), + Error, + "dayPeriod 'A.M' is not supported.", + ); + assertThrows( + () => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "a.m" }, + { type: "timeZoneName", value: "UTC" }, + ]), + Error, + "dayPeriod 'a.m' is not supported.", + ); + assertThrows( + () => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "P.M" }, + { type: "timeZoneName", value: "UTC" }, + ]), + Error, + "dayPeriod 'P.M' is not supported.", + ); + assertThrows( + () => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "p.m" }, + { type: "timeZoneName", value: "UTC" }, + ]), + Error, + "dayPeriod 'p.m' is not supported.", + ); + assertThrows( + () => + formatter.partsToDate([ + { type: "hour", value: "00" }, + { type: "dayPeriod", value: "noon" }, + { type: "timeZoneName", value: "UTC" }, + ]), + Error, + "dayPeriod 'noon' is not supported.", ); }); From 2d524c482f6d18da827b9e922e561e0bd8e9db67 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 9 Aug 2024 10:17:36 +0200 Subject: [PATCH 7/8] remove dead code --- datetime/_date_time_formatter_test.ts | 44 --------------------------- 1 file changed, 44 deletions(-) diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index e8f3dd62753d..e90b81d3c838 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -94,48 +94,6 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { ]), +date, ); - assertEquals( - +formatter.partsToDate([ - { type: "year", value: "2020" }, - { type: "month", value: "01" }, - { type: "day", value: "01" }, - { type: "hour", value: "00" }, - { type: "minute", value: "00" }, - { type: "second", value: "00" }, - { type: "fractionalSecond", value: "000" }, - { type: "dayPeriod", value: "am" }, - { type: "timeZoneName", value: "UTC" }, - ]), - +date, - ); - assertEquals( - +formatter.partsToDate([ - { type: "year", value: "2020" }, - { type: "month", value: "01" }, - { type: "day", value: "01" }, - { type: "hour", value: "00" }, - { type: "minute", value: "00" }, - { type: "second", value: "00" }, - { type: "fractionalSecond", value: "000" }, - { type: "dayPeriod", value: "a.m." }, - { type: "timeZoneName", value: "UTC" }, - ]), - +date, - ); - assertEquals( - +formatter.partsToDate([ - { type: "year", value: "2020" }, - { type: "month", value: "01" }, - { type: "day", value: "01" }, - { type: "hour", value: "00" }, - { type: "minute", value: "00" }, - { type: "second", value: "00" }, - { type: "fractionalSecond", value: "000" }, - { type: "dayPeriod", value: "am." }, - { type: "timeZoneName", value: "UTC" }, - ]), - +date, - ); }); Deno.test("dateTimeFormatter.partsToDate() works with am dayPeriod", () => { const date = new Date("2020-01-01T00:00:00.000Z"); @@ -247,8 +205,6 @@ Deno.test("dateTimeFormatter.partsToDate() works with pm dayPeriod", () => { ); }); Deno.test("dateTimeFormatter.partsToDate() throws with invalid dayPeriods", () => { - const date = new Date("2020-01-01T00:00:00.000Z"); - using _time = new FakeTime(date); const format = "HH a"; const formatter = new DateTimeFormatter(format); assertThrows( From 3535144775d2fbf2fa78f57c997eaac235aabd74 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 9 Aug 2024 11:04:28 +0200 Subject: [PATCH 8/8] update --- datetime/_date_time_formatter_test.ts | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index e90b81d3c838..11a313e54c72 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -81,7 +81,7 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { const format = "yyyy-MM-dd HH:mm:ss.SSS a"; const formatter = new DateTimeFormatter(format); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "year", value: "2020" }, { type: "month", value: "01" }, { type: "day", value: "01" }, @@ -92,7 +92,7 @@ Deno.test("dateTimeFormatter.partsToDate()", () => { { type: "dayPeriod", value: "AM" }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); }); Deno.test("dateTimeFormatter.partsToDate() works with am dayPeriod", () => { @@ -101,52 +101,52 @@ Deno.test("dateTimeFormatter.partsToDate() works with am dayPeriod", () => { const format = "HH a"; const formatter = new DateTimeFormatter(format); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "AM" }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "AM." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "A.M." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "am" }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "am." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "00" }, { type: "dayPeriod", value: "a.m." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); }); Deno.test("dateTimeFormatter.partsToDate() works with pm dayPeriod", () => { @@ -156,52 +156,52 @@ Deno.test("dateTimeFormatter.partsToDate() works with pm dayPeriod", () => { const formatter = new DateTimeFormatter(format); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "PM" }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "PM." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "P.M." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "pm" }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "pm." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); assertEquals( - +formatter.partsToDate([ + formatter.partsToDate([ { type: "hour", value: "01" }, { type: "dayPeriod", value: "p.m." }, { type: "timeZoneName", value: "UTC" }, ]), - +date, + date, ); }); Deno.test("dateTimeFormatter.partsToDate() throws with invalid dayPeriods", () => { @@ -288,6 +288,6 @@ Deno.test("dateTimeFormatter.partsToDate() sets utc", () => { ] as const; for (const [format, input, output] of cases) { const formatter = new DateTimeFormatter(format); - assertEquals(+formatter.partsToDate([...input]), +output); + assertEquals(formatter.partsToDate([...input]), output); } });