Tiny, immutable date utilities for JavaScript and TypeScript. Formatting,
strict parsing, format-aware validation, calendar-correct comparison, and a
built-in profiler — over the native Date, with zero runtime dependencies.
import { add, between, getString, isValid, parse } from 'timesolver';
getString(new Date(), 'YYYY-MM-DD HH:mm:ss'); // '2026-08-28 09:41:07'
add('2024-01-31T00:00:00', 1, 'month'); // 2024-02-29, not 2024-03-02
between('2020-01-01', '2020-02-01', 'month'); // exactly 1
isValid('31-02-2020', 'DD-MM-YYYY'); // false
parse('17/03/2024', 'DD/MM/YYYY'); // Date, or throws- Immutable. No function touches the
Dateyou pass in. - One grammar for three jobs.
getString,parseandisValidshare a single token table, so any format that renders also parses and validates. dayjs needs a plugin for the same thing. - Calendar-correct. Month and year differences are whole numbers where they should be; day arithmetic survives daylight saving.
- Typed at the source. Declarations are generated from the TypeScript implementation, so they cannot drift.
- Small. 4.8 kB minified and gzipped for the whole library, tree-shakable down to what you import, no dependencies.
- Loud, not silent. Bad input throws
TimeSolverErrorwith acode; nothing is written to the console, and nothing returns a sentinel you have to remember to check.
Upgrading from 1.x? See the migration guide — most call sites need no changes, but the semantics they relied on were often wrong.
npm install timesolverRequires Node.js 20 or newer. The <script> bundle targets ES2018; the module
builds target ES2022, so transpile them if you support older browsers.
ES modules, tree-shakable:
import { add, getString } from 'timesolver';CommonJS:
const { add, getString } = require('timesolver');Whole namespace, as in 1.x:
import timeSolver from 'timesolver';
timeSolver.getString(new Date(), 'YYYYMMDD');Browser, no bundler:
<script src="https://unpkg.com/timesolver/dist/timesolver.global.js"></script>
<script>
console.log(timeSolver.getString(new Date(), 'YYYY-MM-DD'));
</script>All documentation lives in docs/: usage guide · recipes · API reference · migration from 1.x · support policy · benchmarks. The usage guide is also in 繁體中文 and 日本語.
| Arithmetic | add subtract startOf endOf |
| Comparison | between equal after before afterToday beforeToday |
| Ranges | isBetween min max clamp |
| Strings | getString parse isValid |
| Calendar | getFullWeek getAbbrWeek getFullMonth getAbbrMonth getQuarter getQuarterByMonth getFirstMonthByQuarter isLeapYear daysInMonth monthName monthAbbreviation weekdayName weekdayAbbreviation |
| Week numbers | getISOWeek getISOWeekYear getWeekOfYear |
| Profiling | createProfiler (also at timesolver/profiler) |
| Errors | TimeSolverError with code: INVALID_DATE INVALID_UNIT INVALID_FORMAT INVALID_ARGUMENT |
Every function takes a Date, epoch milliseconds, or a string Date can parse.
Case-insensitive. Plural forms are accepted too (days, months); the full
table is in the API reference.
| Unit | Aliases |
|---|---|
| millisecond | millisecond mill ms msec |
| second | second sec s |
| minute | minute min |
| hour | hour hr h |
| day | day d |
| week | week w |
| month | month mon m |
| quarter | quarter q |
| year | year yr y |
| Token | Output | Token | Output |
|---|---|---|---|
YYYY YY |
2026 26 |
mm m |
07 7 |
MMMM MMM |
January Jan |
ss s |
09 9 |
MM M |
01 1 |
SSS |
042 |
DD D |
05 5 |
A a |
PM pm |
dddd ddd |
Monday Mon |
Q |
1 |
HH H |
13 13 |
Z ZZ |
+08:00 +0800 |
hh h |
01 1 |
[text] |
literal text |
Every format name 1.x accepted still works, in any case, including the ones
where MM meant minutes. The tokenizer recognises 36 such names: the 27 that
timesolver@1.2.0 shipped, plus the nine DD-first names added afterwards.
between(from, to, unit) returns the signed difference to − from, and the
rule per unit is deliberate rather than incidental:
| Units | Basis | Consequence |
|---|---|---|
millisecond … hour |
exact elapsed time | a 23-hour daylight-saving day really is 23 hours |
day, week |
local calendar | that same day is 1, and the same clock time on adjacent dates is always exactly 1 day apart |
month, quarter, year |
local calendar, with the remainder scaled by the month it falls in | January 1 to February 1 is exactly 1 |
between(a, b, unit) === -between(b, a, unit) holds for every unit.
Weeks start on Sunday by default, matching Date#getDay. startOf, endOf,
equal, after and before take { weekStartsOn } — 0 for Sunday through
6 for Saturday — so ISO-8601 weeks are one argument away:
startOf(date, 'week'); // Sunday
startOf(date, 'week', { weekStartsOn: 1 }); // Monday, ISO-8601
endOf(date, 'week', { weekStartsOn: 6 }); // Friday, for a Saturday-start weekThe timeLook helper from 1.x, rewritten as an isolated timeline on a
monotonic clock:
import { createProfiler } from 'timesolver/profiler';
const profiler = createProfiler();
profiler.start();
await loadRows();
profiler.mark('load');
render();
profiler.mark('render');
profiler.print();
// [timeSolver] 2 mark(s) in 128.412 ms
// 1. load 96.210 ms 74.9% <- slowest
// 2. render 32.202 ms 25.1%report() returns { total, slowest, marks: [{ label, ms, share }] }, so tests
and dashboards can read the numbers instead of scraping console output. The 1.x
names timeLookStart(), timeLook(label) and timeLookReport() still work.
Deliberately absent, because Temporal and Intl already do them better and
matching them would cost the size advantage:
- Time zones. Every function reads the host time zone.
ZandZZrender the current offset but cannot be parsed. - Locales. Month and weekday names are English. For localised output use
Intl.DateTimeFormat. - Durations and humanisers. No
fromNow(), nohumanize().
One inherited sharp edge worth knowing: new Date('2024-03-10') is parsed as
UTC midnight by the language itself, while new Date('2024-03-10T00:00') is
local. This library passes strings to Date, so the same rule applies. Pass a
Date or include a time when it matters.
Setup, scripts, quality gates and the release process are in CONTRIBUTING.md. Bug reports and feature requests go through the issue templates; vulnerabilities go through SECURITY.md.
Design decisions, including the audit that motivated 2.0.0, are recorded in docs/specs.
MIT © Sean Chou