timewave

timewave v0.1.4

A tiny time simulation and date/time math library < 3k (minified/gzipped)

const clock = Clock(new Date(2022,1,1),{tz:'America/New_York',hz:1,tick:1000,run:true}) // refresh every second
        .plus("363d 23h 59m") // jump forward to 11:59PM on Dec 31st, 2022
        .setAlarm({for:new Date(2023,1,1)},() => console.log(`Happy New Year 2023!`)) // log in one minute

Small enough for every day date/time math. Powerful enough for games and simulations.

Timewave provides much of the same functionality as MomentJS and its replacement Luxon in a smaller package.

Timewave also provides the ability to create and run clocks in different timezones or at different refresh rates and speeds, e.g. you can have a clock that increments 5 seconds for every one second of real time.

Features

If you like timewave, check out:

Getting Started

Install from NPMJS or GitHub.

The file timewave.js in the root directory exports Clock,D, and Period. D is for Duration.

Transpiling is left to the consumer. However, the code will run directly in contemporary browsers.

Clock is a psuedo-class. It is a Proxy around a Date object. Nothing will ever be an instanceof a Clock. A Clock is an instanceof a Date.

D and Period are classes, but have been written in such a way that you do not need to use the new operator.

API

We will start with D(uration), followed by Period and build up to Clock.

D(uration)

Why would you want to use a duration? So you can so this:

const ms = D("1y 1w 1d").ms;
const future = new Date(Date.now()+D("2y 1q"));
D D(d:number|string|Clock|Date|Period)

Durations have computed data members ms, s, m, h, d, w, mo, q, y that return the number of milliseconds, seconds, minutes, hours, days, weeks, months, quarters, and years in the duration.

Array D.max(D[,D ...])
Array D.min(D[,D ...])
D d.minus(D)
D d.plus(D)
Formatting Durations

Durations strings are space delimited sequences of numbers followed by a valid duration suffix, e.g. 1w or -1w. The duration suffixes and their equivalent milliseconds are:

{
    ms: 1,
    s: 1000,
    m: 1000 * 60,
    h: 1000 * 60 * 60,
    d: 1000 * 60 * 60 * 24 * 365.2424177 / 365,
    w: 1000 * 60 * 60 * 24 * 365.2424177 / 52,
    mo: 1000 * 60 * 60 * 24 * 365.2424177 / 12,
    q: 1000 * 60 * 60 * 24 * 365.2424177 / 4,
    y: 1000 * 60 * 60 * 24 * 365.2424177
};

For convenience, the above is exposed as D.durations so you can use the values in your own code.

These are all valid durations:

D(1000); // duration is 1 second
D(new Date()); // duration is size of epoch
D(Period({start:Date.now(),end:Date.now()+10000})); // duration is 10 seconds
D("1d"); // duration is 1000 * 60 * 60 * 24 * 365.2424177 / 365
D("1y 1w 1d"); // you do the math!
Basic Duration Math

Basic duration math is conducted directly in Javascript using standard math operators. The result is always a number of milliseconds. Because basic durations do not have an associated date object, they do not need to account for DST and leap years.

const d = D("1m") + D("1s") + 1000; // d will be 62000

You can wrap the result in D and do a conversion by appending the type you desire using dot notation:

const d1 = D(D("1m") + D("1s") + 1000).ms; // d1 will be "62000ms"
    d2 = D("1m").plus(D("1s")  + 1000).ms; // d2 will be "62000ms"

Period

Period Period({start:Date|number,end:Date|number})

A ‘Period’ has computed data members ms, s, m, h, d, w, mo, q, y for the milliseconds, seconds, minutes, hours, days, weeks, months, quarters, and years in the period.

Period p.extend(amount:D|number)
Array Period.max(Period[,Period ...])
Array Period.min(Period[,Period ...])
Period p.shift(amount:D|number)

Clock

Clock Clock(?initialDate:Date|number=Date.now(),{?tz:string,?hz:number=60,?tick:number|string|D|Period=1000/hz,?run:boolean=false,?sync:boolean=true})

Note, when you provide new Date() or Date.now() as the initialDate along with a timezone it IS NOT adjusted. The Clock treats the Date provided as the Date in the timezone. For example:

const now = new Date(), // assume this is Thu May 12 2022 09:46:27 GMT-0700 (Pacific Daylight Time)
    nyc = Clock(now,{tz:"America/New_York"}); // this will be Thu May 12 2022 09:46:27 GMT-0400 (America/New_York Daylight Time)

Why does Clock behave this way? Well, what would you expect if you did the below? new Date() is just a special dynamic case of the same thing.

const date = new Date("1776-07-04 16:00"),
    nyc = Clock(date,{tz:"America/New_York"}); 

If you want an offset clock, do this instead:

const nyc = Clock().clone({tz:"America/New_York"}); // create a default clock in the current timezone and then clone it to New York
Clock Properties
const clock = Clock();
clock.seconds === clock.getSeconds(); // is true

These additional properties are also available:

Standard Date Methods
const clock = Clock(),
    loclms = clock.getTime(),
    nycms = clock.getTime("America/New_York"),
    chms = clock.getTime("America/Chicago");
Clock c.clone({?tz:string=sourceTz,?hz:number=sourceHz,?tick:number|string|D|Period=sourceTick,?run:boolean=sourceRun,?sync:boolean=sourceSync,?alarms:Array=sourceAlarms})
const clone = myClock.clone({tz:<some new tz>});

If you do not want to bring alarms into the clone, then do this:

const clone = myClock.clone({tz:<some new tz>,alarms:[]});
Array Clock.max(Clock[,Clock ...])
Array Clock.min(Clock[,Clock ...])
Clock c.minus(amount:number|string|D|Period)
Clock c.plus(amount:number|string|D|Period)
Clock c.reset({hz:number,tick:number|string|D|Period,sync:boolean,run:boolean=false}={})
Clock c.setAlarm({for:Date|Period},callback:(clock:Clock,complete:boolean) => {…},?name:string=callback.name)
Clock.start({hz:number,tick:number,sync:boolean}={})
Clock c.stop()

Testing

Current test coverage is shown below:

File % Stmts % Branch % Funcs % Lines
All files 88.73 84.79 77.46 90.03
timewave.js 88.73 84.79 77.46 90.03

Architecture

Why do we use a Proxy around Date for Clock? It makes for a very small and efficient code base that is easy to test. And, it allows us to delegate most leap year and leap second processing to the native JavaScript Date object.

Why do we use IANA names in the parenthetical portion of a Clock time string? It makes for a very small code base. We do not want to ship an IANA look-up table. And, technically the parenthetical portion of a Date string is an optional part of the Javascript spec.

We may implement a separately imported IANA look-up given sufficient demand.

Change History

Reverse Chronological Order

2022-05-20 v0.1.4 README typo correction

2022-05-13 v0.1.3 Updated docs.

2022-05-13 v0.1.2 Eliminated fixed use of tx offset internally. Now called every time it is needed in case DST goes into effect during the life of a Clock or Period. Improved date calculations for year, quarter, month, week, day. Added DST to Clock string representation.

2022-05-13 v0.1.1 Updated docs.

2022-05-13 v0.1.0 Unit test coverage of over 85%. Added alarms and more options for stopping and starting Clocks along with Clock stats collection. Comprehensive documentation.

2022-05-09 v0.0.1 Initial public commit