-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from bjuppa/plain-time
Implement PlainTime
- Loading branch information
Showing
12 changed files
with
387 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { SloppyTime } from "./support/date-time-types.ts"; | ||
import { HOURS_IN_DAY, MS_IN_HOUR } from "./constants.ts"; | ||
import { tallyMilliseconds } from "./support/tallyMilliseconds.ts"; | ||
import { intlParts } from "./support/intlParts.ts"; | ||
import { isTruthy } from "./support/isTruthy.ts"; | ||
|
||
export type FormatPlainTimeOptions = | ||
& Omit< | ||
Intl.DateTimeFormatOptions, | ||
| "timeZone" | ||
| "timeZoneName" | ||
| "timeStyle" | ||
| "dateStyle" | ||
| "weekday" | ||
| "year" | ||
| "month" | ||
| "day" | ||
| "era" | ||
> | ||
& { timeStyle?: "medium" | "short" }; | ||
|
||
const intlUtcFormat = Intl.DateTimeFormat("en", { | ||
hourCycle: "h23", | ||
hour: "2-digit", | ||
minute: "2-digit", | ||
second: "2-digit", | ||
fractionalSecondDigits: 3, | ||
timeZone: "UTC", | ||
}); | ||
|
||
/** Describes a basic time-of-day object with minimal properties */ | ||
export interface ComPlainTime { | ||
/** Hour (1-23) */ | ||
hour: number; | ||
/** Minute (0-59) */ | ||
minute: number; | ||
/** Second (0-59) */ | ||
second: number; | ||
/** Millisecond (0-999) */ | ||
millisecond: number; | ||
|
||
/** Tallied milliseconds since 00:00 */ | ||
valueOf: () => number; | ||
/** `hh:mm` / `hh:mm:ss` / `hh:mm:ss.sss` */ | ||
toString: () => string; | ||
/** `hh:mm` / `hh:mm:ss` / `hh:mm:ss.sss` */ | ||
toJSON: () => ReturnType<this["toString"]>; | ||
|
||
/** | ||
* Localize the time for display to a user. | ||
* | ||
* Defaults to "short" time-style if no options given. | ||
* | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat | Intl.DateTimeFormat options on MDN} | ||
*/ | ||
toLocaleString: ( | ||
locale?: Intl.LocalesArgument, | ||
options?: FormatPlainTimeOptions, | ||
) => string; | ||
|
||
constructor: PlainTimeFactory<this>; | ||
} | ||
|
||
/** | ||
* Describes a factory function that creates plain-time objects. | ||
*/ | ||
export interface PlainTimeFactory<T extends ComPlainTime> { | ||
(x: SloppyTime): T; | ||
} | ||
|
||
/** | ||
* Factory function for making basic plain-time objects with minimal properties. | ||
* | ||
* @param date A time object with optional properties `hour`, `minute`, `second` & 'millisecond' | ||
* @returns A new immutable plain-time object | ||
*/ | ||
export function PlainTime( | ||
{ hour = 0, minute = 0, second = 0, millisecond = 0 }: SloppyTime, | ||
): ComPlainTime { | ||
const ms = tallyMilliseconds({ hour, minute, second, millisecond }); | ||
|
||
if (ms < 0) { | ||
throw new TypeError( | ||
`Input tally must be positive: ${ | ||
JSON.stringify({ hour, minute, second, millisecond }) | ||
}`, | ||
); | ||
} | ||
if (ms >= HOURS_IN_DAY * MS_IN_HOUR) { | ||
throw new TypeError( | ||
`Input tally must be less than 24 hours: ${ | ||
JSON.stringify({ hour, minute, second, millisecond }) | ||
}`, | ||
); | ||
} | ||
|
||
const epochUtcDate = new Date(ms); | ||
|
||
const plainTime: ComPlainTime = { | ||
constructor: PlainTime, | ||
hour: epochUtcDate.getUTCHours(), | ||
minute: epochUtcDate.getUTCMinutes(), | ||
second: epochUtcDate.getUTCSeconds(), | ||
millisecond: epochUtcDate.getUTCMilliseconds(), | ||
|
||
valueOf() { | ||
return ms; | ||
}, | ||
toString() { | ||
const parts = intlParts(intlUtcFormat)(epochUtcDate); | ||
return [ | ||
parts["hour"], | ||
parts["minute"], | ||
parts["fractionalSecond"] !== "000" | ||
? `${parts["second"]}.${parts["fractionalSecond"]}` | ||
: parts["second"] !== "00" | ||
? parts["second"] | ||
: undefined, | ||
].filter(isTruthy).join(":"); | ||
}, | ||
toJSON() { | ||
return this.toString(); | ||
}, | ||
|
||
toLocaleString(locale = undefined, options = { timeStyle: "short" }) { | ||
return epochUtcDate.toLocaleTimeString(locale, { | ||
...options, | ||
timeZone: "UTC", | ||
}); | ||
}, | ||
}; | ||
|
||
Object.freeze(plainTime); | ||
|
||
return plainTime; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { PlainTime } from "./PlainTime.ts"; | ||
import { MS_IN_HOUR, MS_IN_MINUTE } from "./constants.ts"; | ||
import { assertEquals, assertThrows } from "./dev_deps.ts"; | ||
|
||
Deno.test("factory accepts number time parts", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
|
||
assertEquals(String(plainTime), "02:03:04.005"); | ||
}); | ||
|
||
Deno.test("factory accepts string time parts", () => { | ||
const plainTime = PlainTime({ | ||
hour: "02", | ||
minute: "03", | ||
second: "04", | ||
millisecond: "005", | ||
}); | ||
|
||
assertEquals(String(plainTime), "02:03:04.005"); | ||
}); | ||
|
||
Deno.test("factory throws error when tally is negative", () => { | ||
assertThrows(() => { | ||
PlainTime({ hour: 1, minute: -61 }); | ||
}); | ||
}); | ||
|
||
Deno.test("factory throws error when tally is 24 hours", () => { | ||
assertThrows(() => { | ||
PlainTime({ hour: 23, minute: 60 }); | ||
}); | ||
}); | ||
|
||
Deno.test("enumerable properties can not be set", async (t) => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
|
||
for (const property in plainTime) { | ||
await t.step(`property '${property}'`, () => { | ||
assertThrows(() => { | ||
// @ts-ignore: Bypass readonly | ||
plainDate[property] = 1; | ||
}); | ||
}); | ||
} | ||
}); | ||
|
||
Deno.test("value conversion returns number of milliseconds from 00:00", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
}); | ||
const ms = 2 * MS_IN_HOUR + 3 * MS_IN_MINUTE; | ||
|
||
assertEquals(plainTime.valueOf(), ms); | ||
assertEquals(Number(plainTime), ms); | ||
}); | ||
|
||
Deno.test("string representation is short for 0 seconds", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
}); | ||
const time = "02:03"; | ||
|
||
assertEquals(plainTime.toString(), time); | ||
assertEquals(plainTime.toJSON(), time); | ||
assertEquals(String(plainTime), time); | ||
}); | ||
|
||
Deno.test("string representation is medium for 0 milliseconds", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
}); | ||
const time = "02:03:04"; | ||
|
||
assertEquals(plainTime.toString(), time); | ||
assertEquals(plainTime.toJSON(), time); | ||
assertEquals(String(plainTime), time); | ||
}); | ||
|
||
Deno.test("string representation is long for fractional seconds", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
millisecond: 1, | ||
}); | ||
const time = "02:03:00.001"; | ||
|
||
assertEquals(plainTime.toString(), time); | ||
assertEquals(plainTime.toJSON(), time); | ||
assertEquals(String(plainTime), time); | ||
}); | ||
|
||
Deno.test("can be localized", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
}); | ||
|
||
assertEquals( | ||
plainTime.toLocaleString("en", { timeStyle: "medium" }), | ||
"2:03:00 AM", | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ComPlainTime, FormatPlainTimeOptions } from "../PlainTime.ts"; | ||
|
||
/** | ||
* Get a function curried with a locale, | ||
* to get another function curried with format options, | ||
* from which to get localized strings of its plain-time arguments. | ||
* | ||
* @param locale An `Intl` locale string, will use system's locale if not given | ||
* @returns A curried function that takes `Intl` format options and returns another curried function that operates on plain-times | ||
* | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat | Intl.DateTimeFormat options on MDN} | ||
*/ | ||
export function formatPlainTime(locale: Intl.LocalesArgument = undefined) { | ||
return (options: FormatPlainTimeOptions = {}) => | ||
(time: ComPlainTime): string => time.toLocaleString(locale, options); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { formatPlainTime } from "./formatPlainTime.ts"; | ||
import { PlainTime } from "../PlainTime.ts"; | ||
import { assert, assertEquals } from "../dev_deps.ts"; | ||
|
||
Deno.test("returns localized string for English locale", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
assertEquals(formatPlainTime("en")()(plainTime), "2:03:04 AM"); | ||
}); | ||
|
||
Deno.test("returns localized string for Swedish locale", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
assertEquals(formatPlainTime("sv")()(plainTime), "02:03:04"); | ||
}); | ||
|
||
Deno.test("returns localized string in default locale", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
assert(formatPlainTime()()(plainTime)); | ||
}); | ||
|
||
Deno.test("takes Intl options", () => { | ||
const plainTime = PlainTime({ | ||
hour: 2, | ||
minute: 3, | ||
second: 4, | ||
millisecond: 5, | ||
}); | ||
assertEquals( | ||
formatPlainTime("en")({ hour: "numeric", hourCycle: "h23" })(plainTime), | ||
"02", | ||
); | ||
}); |
Oops, something went wrong.