Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit b3d11e5

Browse files
committed
Refactor app in JavaScript
1 parent 7198bd7 commit b3d11e5

File tree

15 files changed

+277
-124
lines changed

15 files changed

+277
-124
lines changed

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3+
}

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"editor.defaultFormatter": "esbenp.prettier-vscode",
3+
"editor.codeActionsOnSave": {
4+
"source.organizeImports": "always"
5+
}
6+
}

README.md

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,12 @@
22

33
Returns the difference between two times.
44

5-
## System requirements
6-
7-
* Ruby >= 3.3.0
8-
95
## Installation
106

11-
To add a symlink to the `time-diff` program from your local bin:
12-
137
```shell
14-
# From the project root directory
15-
./install.sh
8+
npm install -g @t-bowersox/time-diff
169
```
1710

18-
You may be prompted for `sudo` permission to create the symlink.
19-
2011
## Usage
2112

2213
```
@@ -25,12 +16,10 @@ time-diff <start_time> <end_time>
2516

2617
The program takes two arguments:
2718

28-
* `start_time`: The starting time of the duration to measure.
29-
* `end_time`: The ending time of the duration to measure.
19+
- `start_time`: The starting time of the duration to measure.
20+
- `end_time`: The ending time of the duration to measure.
3021

31-
The times provided must be date/time strings parseable by
32-
Ruby's [`Time::parse`](https://docs.ruby-lang.org/en/3.3/Time.html#method-c-parse) method, with the exception of a
33-
special `now` argument. When provided, the current time will be substituted.
22+
The times provided must be in a valid date/time format, with the exception of a special `now` argument. When provided, the current time will be substituted.
3423

3524
### Examples
3625

bin/cli.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env node
2+
3+
import process from "node:process";
4+
import { main } from "../src/index.js";
5+
6+
try {
7+
main();
8+
} catch (error) {
9+
process.exitCode = 1;
10+
console.error(error.message);
11+
}

eslint.config.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import globals from "globals";
22
import pluginJs from "@eslint/js";
33

4-
54
/** @type {import('eslint').Linter.Config[]} */
65
export default [
7-
{languageOptions: { globals: globals.node }},
6+
{ languageOptions: { globals: globals.node } },
87
pluginJs.configs.recommended,
9-
];
8+
];

install.sh

Lines changed: 0 additions & 1 deletion
This file was deleted.

package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
{
2-
"name": "time-diff",
2+
"name": "@t-bowersox/time-diff",
33
"version": "2.0.0",
44
"description": "A program that returns the difference between two times.",
5-
"main": "index.js",
5+
"main": "src/index.js",
6+
"type": "module",
67
"directories": {
78
"test": "test"
89
},
10+
"bin": {
11+
"time-diff": "bin/cli.js"
12+
},
913
"scripts": {
10-
"test": "echo \"Error: no test specified\" && exit 1"
14+
"format": "prettier . --write",
15+
"lint": "eslint . --fix",
16+
"test": "node --test test/"
1117
},
1218
"repository": {
1319
"type": "git",
@@ -29,5 +35,8 @@
2935
"eslint": "^9.14.0",
3036
"globals": "^15.12.0",
3137
"prettier": "3.3.3"
38+
},
39+
"engines": {
40+
"node": ">=20.18.0"
3241
}
3342
}

src/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { parseArgs } from "node:util";
2+
import { stringToDate } from "./lib/normalize.js";
3+
import { TimeDiff } from "./lib/time-diff.js";
4+
5+
export function main() {
6+
const { positionals } = parseArgs({ allowPositionals: true });
7+
8+
switch (positionals.length) {
9+
case 0:
10+
throw new Error("Usage: time-diff <start_time> <end_time>");
11+
case 1:
12+
throw new Error("An end time is required.");
13+
}
14+
15+
const startTime = stringToDate(positionals[0]);
16+
const endTime = stringToDate(positionals[1]);
17+
const diff = new TimeDiff(startTime, endTime);
18+
19+
console.log(diff.toString());
20+
}

src/lib/normalize.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//#region Types
2+
3+
/**
4+
* @typedef NormalizedTime
5+
* @property {number} hour
6+
* @property {number} min
7+
* @property {number} sec
8+
*/
9+
10+
//#endregion
11+
12+
//#region Constants
13+
14+
const TIME_REGEX =
15+
/^(?<hour>\d{1,2})?:?(?<min>\d{1,2})?:?(?<sec>\d{1,2})?\s?(?<ord>am|pm)?$/i;
16+
17+
//#endregion
18+
19+
/**
20+
* Normalizes the provirded hour, min, sec, and ordinal values.
21+
* @param {string|undefined} hour
22+
* @param {string|undefined} min
23+
* @param {string|undefined} sec
24+
* @param {string|undefined} ord
25+
* @returns {NormalizedTime} The time normalized on a 24h clock.
26+
*/
27+
function getNormalizedTime(hour, min, sec, ord) {
28+
hour = hour ? parseInt(hour) : 0;
29+
30+
if (ord?.toLowerCase() == "pm" && hour < 12) {
31+
hour += 12;
32+
}
33+
34+
min = min ? parseInt(min) : 0;
35+
sec = sec ? parseInt(sec) : 0;
36+
37+
return { hour, min, sec };
38+
}
39+
40+
/**
41+
* Attempts to create a valid `Date` from the given date-time string.
42+
* @param {string} dateTimeStr The date-time string to normalize.
43+
* @returns {Date} A date parsed from the provided date-time string.
44+
*/
45+
export function stringToDate(dateTimeStr) {
46+
if (dateTimeStr.toLowerCase() == "now") {
47+
return new Date();
48+
}
49+
50+
const timeMatch = dateTimeStr.match(TIME_REGEX);
51+
52+
if (timeMatch != null) {
53+
const { hour, min, sec, ord } = timeMatch.groups;
54+
const normalized = getNormalizedTime(hour, min, sec, ord);
55+
56+
const today = new Date();
57+
today.setHours(normalized.hour, normalized.min, normalized.sec);
58+
return today;
59+
}
60+
61+
return new Date(dateTimeStr);
62+
}

src/lib/time-diff.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"use strict";
2+
3+
//#region Types
4+
5+
/**
6+
* @typedef {Object} TimeUnits
7+
* @property {number} year
8+
* @property {number} month
9+
* @property {number} week
10+
* @property {number} day
11+
* @property {number} hour
12+
* @property {number} minute
13+
* @property {number} second
14+
*/
15+
16+
//#endregion
17+
18+
//#region Constants
19+
20+
/** One minute in seconds. */
21+
const ONE_MIN = 60;
22+
23+
/** One hour in seconds. */
24+
const ONE_HOUR = 60 * ONE_MIN;
25+
26+
/** One day in seconds. */
27+
const ONE_DAY = 24 * ONE_HOUR;
28+
29+
/** One week in seconds. */
30+
const ONE_WEEK = 7 * ONE_DAY;
31+
32+
/** One month in seconds. */
33+
const ONE_MONTH = 4 * ONE_WEEK;
34+
35+
/** One year in seconds. */
36+
const ONE_YEAR = 52 * ONE_WEEK;
37+
38+
//#endregion
39+
40+
//#region Classes
41+
42+
/** Represents the difference between two times. */
43+
export class TimeDiff {
44+
/** @type {Date} */
45+
#endTime;
46+
47+
/** @type {Date} */
48+
#startTime;
49+
50+
/** @type {Map<string, number>} */
51+
#units = new Map([
52+
["year", ONE_YEAR],
53+
["month", ONE_MONTH],
54+
["week", ONE_WEEK],
55+
["day", ONE_DAY],
56+
["hour", ONE_HOUR],
57+
["minute", ONE_MIN],
58+
]);
59+
60+
//#region Public API
61+
62+
/**
63+
* @param {Date} startTime The start of the date range.
64+
* @param {Date} endTime The end of the date range.
65+
*/
66+
constructor(startTime, endTime) {
67+
this.#validateTimes(startTime, endTime);
68+
this.#startTime = startTime;
69+
this.#endTime = endTime;
70+
}
71+
72+
toString() {
73+
if (this.value == 0) {
74+
return "No difference";
75+
}
76+
77+
return Object.entries(this.#timeToUnits())
78+
.filter((entry) => entry[1] > 0)
79+
.map(this.#entryToString)
80+
.join(", ");
81+
}
82+
83+
/** The time difference rounded to the nearest second. */
84+
get value() {
85+
const diffMs = this.#endTime - this.#startTime;
86+
return Math.round(diffMs / 1000);
87+
}
88+
89+
//#endregion
90+
91+
/**
92+
* Converts a `TimeUnits` value to a string.
93+
* @param {[keyof TimeUnits, number]} entry
94+
*/
95+
#entryToString(entry) {
96+
const [unitName, value] = entry;
97+
let str = `${value.toLocaleString()} ${unitName}`;
98+
99+
if (value != 1) {
100+
str += "s";
101+
}
102+
103+
return str;
104+
}
105+
106+
/**
107+
* Tests that a time is valid.
108+
* @param {Date} time The time to validate.
109+
*/
110+
#isValidTime(time) {
111+
return time instanceof Date && !isNaN(time.valueOf());
112+
}
113+
114+
/**
115+
* Returns the time difference as a dictionary, with each key representing
116+
* a unit of time (year, month, etc.).
117+
* @returns {TimeUnits}
118+
*/
119+
#timeToUnits() {
120+
const units = {};
121+
let diffSeconds = this.value;
122+
123+
this.#units.forEach((seconds, unit) => {
124+
units[unit] = 0;
125+
126+
while (diffSeconds >= seconds) {
127+
diffSeconds -= seconds;
128+
units[unit] += 1;
129+
}
130+
});
131+
132+
units.second = diffSeconds >= 0 ? diffSeconds : 0;
133+
return units;
134+
}
135+
136+
/**
137+
* Validates that the start and end times form a valid date range.
138+
* @param {Date} startTime The start of the date range.
139+
* @param {Date} endTime The end of the date range.
140+
* @throws If either the `startTime` or `endTime` are invalid.
141+
*/
142+
#validateTimes(startTime, endTime) {
143+
if (!this.#isValidTime(startTime)) {
144+
throw new Error("Start time is invalid.");
145+
}
146+
147+
if (!this.#isValidTime(endTime)) {
148+
throw new Error("End time is invalid.");
149+
}
150+
151+
if (endTime < startTime) {
152+
throw new Error("The end time must be greater than the start time.");
153+
}
154+
}
155+
}
156+
157+
//#endregion

0 commit comments

Comments
 (0)