From f9d6e977e92b05a6bf5291deb4c53815faaca416 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 20 Sep 2025 15:47:47 +0200 Subject: [PATCH] Add timespan class --- timespan.js | 129 +++++++++++++++++++++++++++++++++++++++++++++++ timespan.test.js | 81 +++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 timespan.js create mode 100644 timespan.test.js diff --git a/timespan.js b/timespan.js new file mode 100644 index 0000000..5963840 --- /dev/null +++ b/timespan.js @@ -0,0 +1,129 @@ +/** + * Represents a timespan with a start and end time. + */ +export class Timespan { + // constructors + constructor(start = Date.now(), end = Date.now()) { + if (start > end) { + throw new Error("Start time must be less than or equal to end time."); + } + this.start = start; + this.end = end; + } + + static fromDates(startDate, endDate) { + return new Timespan(startDate, endDate); + } + + static fromMillis(millis) { + return new Timespan(0, millis); + } + + static fromSeconds(seconds) { + return Timespan.fromMillis(seconds * 1000); + } + + // methods + get totalMillis() { + return this.end - this.start; + } + + get millis() { + return Math.floor((this.totalMillis) % 1000); + } + + get totalSeconds() { + return Math.floor(this.totalMillis / 1000); + } + + get seconds() { + return Math.floor((this.totalMillis) / 1000 % 60); + } + + get totalMinutes() { + return Math.floor(this.totalMillis / 1000 / 60); + } + + get minutes() { + return Math.floor(this.totalMillis / 1000 / 60 % 60); + } + + get totalHours() { + return Math.floor(this.totalMillis / 1000 / 60 / 60); + } + + get hours() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 % 24); + } + + get totalDays() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24); + } + + get days() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 % 30.44); // Average days in a month + } + + get weekDays() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 % 7); + } + + get totalWeeks() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 7); + } + + get weeks() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 7 % 4.345); // Average weeks in a month + } + + get totalMonths() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 30.44); // Average days in a month + } + + get months() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 30.44 % 12); // Average days in a month + } + + get totalYears() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 365.25); // Average days in a year + } + + get years() { + return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 365.25 % 1); // Average days in a year + } + + toString(includeWeeks = true, includeMillis = true) { + const parts = []; + if (this.totalYears >= 1) parts.push(`${this.totalYears} years`); + if (this.totalMonths >= 1) parts.push(`${this.months} months`); + if (includeWeeks && this.totalWeeks >= 1) parts.push(`${this.weeks} weeks`); + if (this.totalDays >= 1) parts.push(`${includeWeeks ? this.weekDays : this.days} days`); + if (this.totalHours >= 1) parts.push(`${this.hours} hours`); + if (this.totalMinutes >= 1) parts.push(`${this.minutes} minutes`); + if (this.totalSeconds >= 1) parts.push(`${this.seconds} seconds`); + if (includeMillis) parts.push(`${this.millis} milliseconds`); + return parts.join(", "); + } + + toShortString(includeWeeks = true, includeMillis = true, withSpaces = false) { + const parts = []; + if (this.totalYears >= 1) parts.push(`${this.totalYears}y`); + if (this.totalMonths >= 1) parts.push(`${this.months}mo`); + if (includeWeeks && this.totalWeeks >= 1) parts.push(`${this.weeks}w`); + if (this.totalDays >= 1) parts.push(`${includeWeeks ? this.weekDays : this.days}d`); + if (this.totalHours >= 1) parts.push(`${this.hours}h`); + if (this.totalMinutes >= 1) parts.push(`${this.minutes}m`); + if (this.totalSeconds >= 1) parts.push(`${this.seconds}s`); + if (includeMillis) parts.push(`${this.millis}ms`); + return parts.join(withSpaces ? " " : ""); + } + + get startTime() { + return new Date(this.start); + } + + get endTime() { + return new Date(this.end); + } + +} \ No newline at end of file diff --git a/timespan.test.js b/timespan.test.js new file mode 100644 index 0000000..00a7a1f --- /dev/null +++ b/timespan.test.js @@ -0,0 +1,81 @@ +// noinspection ES6ConvertRequireIntoImport - this is CJS, not MJS + +const { it } = require("node:test"); +const assert = require("node:assert/strict"); +const { Timespan } = require("./timespan"); + +it("should be able to return zero", () => { + const ts = new Timespan(); + assert.equal(ts.totalMillis, 0); +}); + +it("should be able to return timespan from milliseconds", () => { + const ts = Timespan.fromMillis(1000); + assert.equal(ts.totalMillis, 1000); + assert.equal(ts.totalSeconds, 1); +}); + +it("should be able to return timespan from seconds", () => { + const ts = Timespan.fromSeconds(60); + assert.equal(ts.totalMillis, 60000); + assert.equal(ts.totalSeconds, 60); + assert.equal(ts.totalMinutes, 1); + assert.equal(ts.minutes, 1); + assert.equal(ts.hours, 0); + assert.equal(ts.days, 0); +}); + +it("should be pure", () => { + const count = 1000; + const timestamps = []; + for (let i = 0; i < count; i++) { + timestamps.push(Timespan.fromMillis(8972347984)); + for (const ts2 of timestamps) { + assert.equal(ts2.totalMillis, 8972347984); + assert.equal(ts2.totalSeconds, 8972347); + assert.equal(ts2.totalMinutes, 149539); + assert.equal(ts2.totalHours, 2492); + assert.equal(ts2.totalDays, 103); + assert.equal(ts2.totalWeeks, 14); + assert.equal(ts2.totalMonths, 3); + assert.equal(ts2.totalYears, 0); + + assert.equal(ts2.millis, 984); + assert.equal(ts2.seconds, 7); + assert.equal(ts2.minutes, 19); + assert.equal(ts2.hours, 20); + assert.equal(ts2.days, 12); + assert.equal(ts2.weekDays, 5); + assert.equal(ts2.weeks, 1); + assert.equal(ts2.months, 3); + assert.equal(ts2.years, 0); + } + } +}); + +it("should be able to stringify", () => { + const ts = Timespan.fromMillis(8972347984); + assert.equal(ts.toString(), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds"); + assert.equal(ts.toString(true), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds"); + assert.equal(ts.toString(true, false), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds"); + assert.equal(ts.toString(false), "3 months, 12 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds"); + assert.equal(ts.toString(false, false), "3 months, 12 days, 20 hours, 19 minutes, 7 seconds"); +}); + +it("should be able to shortStringify", () => { + const ts = Timespan.fromMillis(8972347984); + assert.equal(ts.toShortString(), "3mo1w5d20h19m7s984ms"); + assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms"); + assert.equal(ts.toShortString(true, false), "3mo1w5d20h19m7s"); + assert.equal(ts.toShortString(false), "3mo12d20h19m7s984ms"); + assert.equal(ts.toShortString(false, false), "3mo12d20h19m7s"); +}); + +it("should be able to shortStringify with spaces", () => { + const ts = Timespan.fromMillis(8972347984); + assert.equal(ts.toShortString(undefined, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms"); + assert.equal(ts.toShortString(true, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms"); + assert.equal(ts.toShortString(true, false, true), "3mo 1w 5d 20h 19m 7s"); + assert.equal(ts.toShortString(false, undefined, true), "3mo 12d 20h 19m 7s 984ms"); + assert.equal(ts.toShortString(false, false, true), "3mo 12d 20h 19m 7s"); +}); \ No newline at end of file