Skip to content

Commit

Permalink
refactor: order component and implement test
Browse files Browse the repository at this point in the history
  • Loading branch information
JonDotsoy committed Jul 21, 2023
1 parent ca34330 commit 43221e0
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 234 deletions.
14 changes: 14 additions & 0 deletions __snapshots__/ndate.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const snapshot = {};

snapshot[`show help 1`] = `
"Usage: ndate [-] [--zero] [--date-style <full|long|medium|short|none>] [--time-style <full|long|medium|short|none>]
" +
" [--hour-cycles <h11|h12|h23|h24|none>] [--time-zone <time-zone>] [--local <locale>]
" +
" [--template <template>] [--json] [--utc] [--epoch] [--epoch-ms] [--date <date>]
" +
" [--help] [-j] [-d <date>] [-l <locale>] [-tz <time-zone>] [-z] [-h]
"
`;
snapshot[`render date 1`] = `"miércoles, 13 de diciembre de 2023, 00:00:00 hora de verano de Chile"`;
70 changes: 70 additions & 0 deletions handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
toDateStyle,
toTimeStyle,
dateParse,
toHourCycle,
} from "./lib/common.ts";
import { renderDateTemplate } from "./lib/renderDateTemplate.ts";
import { makeFlags } from "./makeFlags.ts";

export const handler = async function*(args: string[]): AsyncGenerator<Uint8Array> {
let {
hourCycle, dateStyle, timeStyle, insertFinalNewLine, local, timeZone, date, outputAsEpoch, outputAsEpochMS, outputAsJSON, outputAsUTC, stdinReadable, showHelp, template, transformOptions, optionsLabels,
} = makeFlags(args);

if(showHelp) {
const items = Object.keys(transformOptions)
.map(item => {
const label = optionsLabels[item]?.label;
return label ? `[${item} ${label}]` : `[${item}]`;
});

const textUsage = `Usage: ndate`;

const lines: string[] = [];
let currentLine: string | undefined;
for(const item of items) {
if(!currentLine) {
currentLine = lines.length ? `${' '.repeat(textUsage.length)}` : `${textUsage}`;
}

currentLine = `${currentLine} ${item}`;
if(currentLine.length > 80) {
lines.push(currentLine);
currentLine = undefined;
}
}

if(currentLine) lines.push(currentLine);

for(const line of lines) {
yield new TextEncoder().encode(line);
yield new TextEncoder().encode('\n');
}

return;
}

if(timeZone) Deno.env.set(`TZ`, timeZone);
if(local) Deno.env.set(`LANG`, local);

if(stdinReadable) {
const buff = new Uint8Array(256);
await Deno.stdin.read(buff);
const text = new TextDecoder().decode(buff.subarray(0, buff.findIndex(p => p === 0))).trim();
date = dateParse(text);
}

const toOutput = () => {
if(template) return renderDateTemplate(template, date, local, { dateStyle: toDateStyle(dateStyle), timeStyle: toTimeStyle(timeStyle), timeZone, hourCycle: toHourCycle(hourCycle) });
if(outputAsEpochMS) return Math.floor(date.getTime()).toString();
if(outputAsEpoch) return Math.floor(date.getTime() / 1000).toString();
if(outputAsJSON) return date.toJSON();
if(outputAsUTC) return date.toUTCString();
return date.toLocaleString(local, { dateStyle: toDateStyle(dateStyle), timeStyle: toTimeStyle(timeStyle), timeZone, hourCycle: toHourCycle(hourCycle) });
};

yield new TextEncoder().encode(toOutput());

if(insertFinalNewLine) yield new TextEncoder().encode(`\n`);
};
18 changes: 18 additions & 0 deletions lib/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const dateStyles = ["full", "long", "medium", "short", "none"] as const;
export const timeStyles = ["full", "long", "medium", "short", "none"] as const;
export type DateStyle = (typeof dateStyles)[number];
export type TimeStyle = (typeof dateStyles)[number];
export const toDateStyleString = (v: unknown): DateStyle => typeof v === "string" ? dateStyles.find(dateStyle => dateStyle.startsWith(v)) ?? "none" : "none";
export const toDateStyle = (v: DateStyle) => v === "none" ? undefined : v
export const toTimeStyleString = (v: unknown): TimeStyle => typeof v === "string" ? timeStyles.find(timeStyle => timeStyle.startsWith(v)) ?? "none" : "none";
export const toTimeStyle = (v: TimeStyle) => v === "none" ? undefined : v
export const dateParse = (val: string): Date => {
const d = new Date(val);
if (Number.isNaN(Number(d))) {
throw new Error(`the ${Deno.inspect(val)} time stamp no valid`);
}
return d;
}
export const hourCycles = ["h11", "h12", "h23", "h24", "none"] as const
export const toHourCycleString = (v: unknown): typeof hourCycles[number] => typeof v === "string" ? hourCycles.find(hourCycle => hourCycle.startsWith(v)) ?? 'none' : 'none'
export const toHourCycle = (v: typeof hourCycles[number]) => v === 'none' ? undefined : v
88 changes: 88 additions & 0 deletions lib/renderDateTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export function renderDateTemplate(template: string, date: Date, locales?: string, options?: Intl.DateTimeFormatOptions) {
const localeString = date.toLocaleString(locales, options);
const parts = new Intl.DateTimeFormat(locales, options).formatToParts(date).map(({ type, value }) => [`local_${type}`, value] as const);

const epochMS = Math.floor(date.getTime());
const epoch = Math.floor(date.getTime() / 1000);

const isoString = date.toISOString();
const utcString = date.toUTCString();

const values: Record<string, any> = Object.fromEntries([
[`epoch`, epoch],
[`epoch_ms`, epochMS],
[`json`, isoString],
[`iso`, isoString],
[`iso8601`, isoString],
[`utc`, utcString],
[`rfc7231`, utcString],
[`local`, localeString],
["time", date.getTime()],

["YYYY", date.getFullYear().toString().padStart(4, `0`)],
["MM", (date.getMonth() + 1).toString().padStart(2, `0`)],
["DD", date.getDate().toString().padStart(2, `0`)],

["hh", date.getHours().toString().padStart(2, `0`)],
["mm", date.getMinutes().toString().padStart(2, `0`)],
["ss", date.getSeconds().toString().padStart(2, `0`)],
["ms", date.getMilliseconds().toString().padStart(3, `0`)],


["full_year", date.getFullYear()],
["month", date.getMonth() + 1],
["date", date.getDate()],
["day", date.getDay()],
["hours", date.getHours()],
["minutes", date.getMinutes()],
["seconds", date.getSeconds()],
["milliseconds", date.getMilliseconds()],
["timezone_offset", date.getTimezoneOffset()],

["utc_full_year", date.getUTCFullYear()],
["utc_month", date.getUTCMonth() + 1],
["utc_date", date.getUTCDate()],
["utc_day", date.getUTCDay()],
["utc_hours", date.getUTCHours()],
["utc_minutes", date.getUTCMinutes()],
["utc_seconds", date.getUTCSeconds()],
["utc_milliseconds", date.getUTCMilliseconds()],
...parts,
]);

const transforms: Record<string, (value: string, options?: string) => string> = {
json: value => JSON.stringify(value),
sub: (value, options) => {
const [start = 0, end] = options?.split(':') ?? [];
return value.toString().substring(Number(start), Number(end));
},
padStart: (values, options) => {
const [maxLength = 0, fillString = ' '] = options?.split(':') ?? [];
return values.toString().padStart(Number(maxLength), fillString);
},
padEnd: (values, options) => {
const [maxLength = 0, fillString = ' '] = options?.split(':') ?? [];
return values.toString().padEnd(Number(maxLength), fillString);
},
default: value => value,
};

const regexp = /\{\{(?<keyword>\w+)(\:(?<transform>\w+)(\:(?<transform_options>[\w\:\-]+))?)?\}\}/g;
const stringTemplate: string[] = [];
const substitution: string[] = [];
let p: RegExpExecArray | null = null;
let lastPointer = 0;
do {
p = regexp.exec(template);
if(p) {
stringTemplate.push(template.substring(lastPointer, p.index));
lastPointer = p.index + p.at(0)!.length;
const transform = transforms[p.groups?.transform ?? 'default'] ?? transforms.default;
substitution.push(transform(values[p.groups!.keyword] ?? '', p.groups!.transform_options));
}
} while(p !== null);

stringTemplate.push(template.substring(lastPointer));

return String.raw({ raw: stringTemplate }, ...substitution);
}
109 changes: 109 additions & 0 deletions makeFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
dateStyles,
timeStyles,
DateStyle,
TimeStyle,
toDateStyleString,
toTimeStyleString,
dateParse,
hourCycles,
toHourCycleString,
} from "./lib/common.ts";

export function makeFlags(args: string[]) {
let hourCycle: (typeof hourCycles)[number] = 'none';
let dateStyle: DateStyle = 'full';
let timeStyle: TimeStyle = 'full';
let insertFinalNewLine = true;
let local: string | undefined;
let timeZone: string | undefined;
let date = new Date();
let outputAsEpoch = false;
let outputAsEpochMS = false;
let outputAsJSON = false;
let outputAsUTC = false;
let stdinReadable = false;
let showHelp = false;
let template: string | undefined;
let crontab: string | undefined;

const transformOptions: Record<string, (nextArgument: () => string | undefined) => void> = {
'-': () => { stdinReadable = true; },
'--zero': () => { insertFinalNewLine = false; },
'--date-style': (nextArgument) => { dateStyle = toDateStyleString(nextArgument()); },
'--time-style': (nextArgument) => { timeStyle = toTimeStyleString(nextArgument()); },
'--hour-cycles': (nextArgument) => { hourCycle = toHourCycleString(nextArgument()) },
'--time-zone': (nextArgument) => { timeZone = nextArgument(); },
'--local': (nextArgument) => { local = nextArgument(); },
'--template': (nextArgument) => { template = nextArgument(); },
'--json': () => { outputAsJSON = true; },
'--utc': () => { outputAsUTC = true; },
'--epoch': () => { outputAsEpoch = true; },
'--epoch-ms': () => { outputAsEpochMS = true; },
'--date': (nextArgument) => {
const v = nextArgument();
if(v) {
date = dateParse(v);
}
},
'--help': () => { showHelp = true; },
get "-j"() { return this['--json']; },
get "-d"() { return this["--date"]; },
get "-l"() { return this["--local"]; },
get "-tz"() { return this["--time-zone"]; },
get "-z"() { return this["--zero"]; },
get "-h"() { return this["--help"]; },
};

const optionsLabels: Record<string, undefined | { label?: string; }> = {
'--date-style': { label: `<${dateStyles.join('|')}>` },
'--time-style': { label: `<${timeStyles.join('|')}>` },
'--hour-cycles': { label: `<${hourCycles.join('|')}>` },
'--local': { label: `<locale>` },
'--date': { label: `<date>` },
'--template': { label: `<template>` },
'--time-zone': { label: `<time-zone>` },
get "-j"() { return this['--json']; },
get "-d"() { return this["--date"]; },
get "-l"() { return this["--local"]; },
get "-tz"() { return this["--time-zone"]; },
get "-z"() { return this["--zero"]; },
get "-h"() { return this["--help"]; },
};

const parseCrontab = (str: string) => {
const res = /^((?<predefined>@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(?<every>@every ((?<every_value>\d+)(?<every_value_sign>ns|us|µs|ms|s|m|h))+)|(?<cron>(?<cron_minute>((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)) (?<cron_hour>((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)) (?<cron_day_month>((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)) (?<cron_month>((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)) (?<cron_day_week>((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*))))$/.exec(str);

if(!res) return null;

if(res.groups?.predefined)
return {
predefined: res.groups.predefined
};
};

for(let argsCursor = args[Symbol.iterator](), { value: arg, done } = argsCursor.next();!done;{ value: arg, done } = argsCursor.next()) {
const next = (): string | undefined => argsCursor.next().value;
if(typeof arg === "string" && arg in transformOptions) { transformOptions[arg](next); }
}

return {
hourCycle,
dateStyle,
timeStyle,
insertFinalNewLine,
local,
timeZone,
date,
outputAsEpoch,
outputAsEpochMS,
outputAsJSON,
outputAsUTC,
stdinReadable,
showHelp,
template,
crontab,
transformOptions,
optionsLabels,
};
}
34 changes: 34 additions & 0 deletions ndate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assertSnapshot } from "https://deno.land/std@0.195.0/testing/snapshot.ts";
import { handler } from "./handler.ts";

const joinUint8ArrayGenerator = async (
generator: AsyncGenerator<Uint8Array>,
) => {
let buffEnd = new Uint8Array();
for await (const line of generator) {
buffEnd = new Uint8Array([...buffEnd, ...line]);
}
return buffEnd;
};

Deno.test("show help", async (t) => {
await assertSnapshot(
t,
new TextDecoder().decode(
await joinUint8ArrayGenerator(
handler(["-h"]),
),
),
);
});

Deno.test("render date", async (t) => {
await assertSnapshot(
t,
new TextDecoder().decode(
await joinUint8ArrayGenerator(
handler(["-z", "-d", "2023-12-13 00:00","-l","es-cl",'-tz',"America/Santiago"]),
),
),
);
});
Loading

0 comments on commit 43221e0

Please sign in to comment.