nifty-tou
v1.2.0
Published
A delightful little data model and utilities for working with time-of-use tariff policies.
Downloads
10
Readme
Nifty ToU (Time of Use)
Nifty ToU is a delightful little JavaScript library for working with time-of-use based tariffs. It aims to be easy to use, reliable, and without any external dependencies.
A central class in Nifty ToU is TemporalRangesTariff, that defines a set of tariff values with a set of time-based constraints. The implication is that the tariff values apply only when all the time-based constraints are valid.
The time-based constraints are encoded as IntRange objects, that are integer ranges with minimum and maximum values that define the bounds of the constraint. The supported time-based constraints are:
| Constraint | Example | Bounds | | :---------------------- | :-------------- | :---------------------------------------------- | | month range | January - March | 1 - 12 (January - December, inclusive) | | day of month range | 1 - 31 | 1 - 31 (inclusive) | | day of week range | Monday - Friday | 1 - 7 (Monday - Sunday, inclusive) | | minute of day range | 00:00 - 08:30 | 0 - 1440 (inclusive minimum, exclusive maximum) |
Here are some example uses of the TemporalRangesTariff
class:
// a tariff that applies in the morning of any day of the year
const tt = new TemporalRangesTariff(
TemporalRangesTariff.ALL_MONTHS,
TemporalRangesTariff.ALL_DAYS_OF_MONTH,
TemporalRangesTariff.ALL_DAYS_OF_WEEK,
new IntRange(0, 720), // midnight - noon
[
new TariffRate("Morning Fixed", 1.25),
new TariffRate("Morning Variable", 0.1),
]
);
tt.appliesAt(new Date("2024-01-05T01:00")); // true, in the morning
tt.appliesAt(new Date("2024-01-05T13:00")); // false, in the afternoon
Tariff schedules
The TemporalRangesTariffSchedule class defines a schedule, or collection of date-based tariff rules that allows you resolve a set of tariff rates for a given date. For example, imagine we add another tariff to the previous example, and then resolve the rates for a date:
// a tariff that applies after noon of any day of the year
const tt2 = new TemporalRangesTariff(
TemporalRangesTariff.ALL_MONTHS,
TemporalRangesTariff.ALL_DAYS_OF_MONTH,
TemporalRangesTariff.ALL_DAYS_OF_WEEK,
new IntRange(720, 1440), // noon - midnight
[new TariffRate("Afternoon Fixed", 2.34)]
);
// create a scheule with our two tariff rules
const schedule = new TemporalRangesTariffSchedule([tt, tt2]);
// resolve the rates that apply on a morning date (8 AM)
const rates = schedule.resolve(new Date("2024-01-05T08:00"));
// rates like:
{
"Morning Fixed": {amount: 1.25},
"Morning Variable": {amount: 0.10}
}
Multiple rules matching
By default, a schedule will resolve the rates for the first available tariff matching
a given date. You can turn on multiple match mode by passing an additional true
argument to the constructor. For example, imagine we add another tariff to the previous
examples, and then resolve the rates for a date:
// a tariff that applies after noon of any day of the year
const tt3 = new TemporalRangesTariff(
TemporalRangesTariff.ALL_MONTHS,
TemporalRangesTariff.ALL_DAYS_OF_MONTH,
TemporalRangesTariff.ALL_DAYS_OF_WEEK,
TemporalRangesTariff.ALL_MINUTES_OF_DAY,
[new TariffRate("Any Time", 3.45)]
);
// create a scheule with our three tariff rules, allowing mutiple matches
const schedule = new TemporalRangesTariffSchedule([tt, tt2, tt3], true);
// resolve the rates that apply on a morning date (8 AM)
const rates = schedule.resolve(new Date("2024-01-05T08:00"));
// rates like:
{
"Morning Fixed": {amount: 1.25},
"Morning Variable": {amount: 0.10},
"All Time": {amount: 3.45}
}
Year-based tariff schedules
If you would like to model a tariff schedule with rules that change over the time, the
YearTemporalRangesTariffSchedule
class extends the TemporalRangesTariffSchedule
with support for year-based rules.
For example, imagine a tariff schedule like this:
| Year | Months | Days | Weekdays | Hours | Name | Rate | | :--- | :----- | :--- | :------- | :---- | :--- | :--- | | 2023 | * | * | * | 0-12 | AM | 1.23 | | 2023 | * | * | * | 12-24 | PM | 2.34 | | 2022 | * | * | * | 0-12 | AM | 1.12 | | 2022 | * | * | * | 12-24 | PM | 2.23 | | 2000 | * | * | * | 0-12 | AM | 0.12 | | 2000 | * | * | * | 12-24 | PM | 0.23 |
You can model this schedule like this:
// define the schedule rules with year constraints
const rules = [
YearTemporalRangesTariff.parseYears(
"en-US",
"2023",
"Jan-Dec",
"1-31",
"Mon-Sun",
"0-12",
[new TariffRate("AM", 1.23)]
),
YearTemporalRangesTariff.parseYears(
"en-US",
"2023",
"Jan-Dec",
"1-31",
"Mon-Sun",
"12-24",
[new TariffRate("AM", 2.34)]
),
YearTemporalRangesTariff.parseYears(
"en-US",
"2022",
"Jan-Dec",
"1-31",
"Mon-Sun",
"0-12",
[new TariffRate("AM", 1.12)]
),
YearTemporalRangesTariff.parseYears(
"en-US",
"2022",
"Jan-Dec",
"1-31",
"Mon-Sun",
"12-24",
[new TariffRate("AM", 2.23)]
),
YearTemporalRangesTariff.parseYears(
"en-US",
"2000",
"Jan-Dec",
"1-31",
"Mon-Sun",
"0-12",
[new TariffRate("AM", 0.12)]
),
YearTemporalRangesTariff.parseYears(
"en-US",
"2000",
"Jan-Dec",
"1-31",
"Mon-Sun",
"12-24",
[new TariffRate("AM", 0.23)]
),
];
// define the schedule, with the `yearExtend` option set
const s = new YearTemporalRangesTariffSchedule(rules, {
yearExtend: true, // allow "gap fill" year matching
});
// exact match rules
s.resolve(new Date("2023-01-01T08:00")) === { AM: 1.23 };
s.resolve(new Date("2022-01-01T08:00")) === { AM: 1.12 };
s.resolve(new Date("2000-01-01T08:00")) === { AM: 0.12 };
// gap-fill match a future date, based on previously avaialble year rule
s.resolve(new Date("2050-01-01T08:00")) === { AM: 1.23 }; // 2023 rule
// gap-fill match inbetween year rules, based on previously avaialble year
s.resolve(new Date("2010-01-01T08:00")) === { AM: 1.12 }; // 2000 rule
Integer amounts
The TariffRate class can be constructed with an exponent
property to avoid floating-point values if desired. For example:
// these floating point rates:
new TariffRate("Morning Fixed", 1.25);
new TariffRate("Morning Variable", 0.1);
// could be expressed in integer form:
new TariffRate("Morning Fixed", 125, -2);
new TariffRate("Morning Variable", 1, -1);
Chronological tariffs
The ChronoTariff class can be used to model a time-based "fixed" tariff, such as a daily or monthly charge. For example:
// construct a chronological tariff @ 10/day
const tariff = new ChronoTariff(ChronoTariffUnit.DAYS, 10);
// calculate the tariff cost over a 7 day time range
const cost =
tariff.rate *
tariff.quantity(
new Date("2024-01-01T00:00:00Z"),
new Date("2024-01-08T00:00:00Z"),
true
);
cost === 70; // 7 days @ 10/day
Language support
Nifty ToU supports parsing and formatting text-based range values, in different languages. For example the following produce the same range constraints and rate values (only the rate names remain language specific):
// US English
const tt = TemporalRangesTariff.parse(
"en-US",
"Jan - Dec",
"1 - 31",
"Mon - Fri",
"0 - 24",
[TariffRate.parse("en-US", "Morning Fixed", "1.23")]
);
TemporalRangesTariff.format(
"en-US",
ChronoField.MONTH_OF_YEAR,
new IntRange(1, 3)
) === "Jan - Mar";
// German
const tt = TemporalRangesTariff.parse(
"de",
"Januar - Dezember",
"1 - 31",
"Montag - Freitag",
"00:00 - 24:00",
[TariffRate.parse("de", "Morgen behoben", "1,23")]
);
TemporalRangesTariff.format(
"de",
ChronoField.MONTH_OF_YEAR,
new IntRange(1, 3)
) === "Jan - Mär";
// Japanese
const tt = TemporalRangesTariff.parse(
"ja-JP",
"1月~12月",
"1~31",
"月曜日~金曜日",
"0~24",
[TariffRate.parse("ja-JP", "固定価格(午前中)", "1.23")]
);
TemporalRangesTariff.format(
"ja-JP",
ChronoField.MONTH_OF_YEAR,
new IntRange(1, 3)
) === "1月~3月";
Documentation
The API documentation is published to https://solarnetwork.github.io/nifty-tou/, and is also available in Markdown form in the docs/md directory.
Building from source
To build Nifty ToU yourself, clone or download this repository. You need to have Node 16+ installed. Then:
# initialize dependencies
npm ci
# build
npm run build
Running the build
script will execute the TypeScript compiler and generate JavaScript files
in lib/
directory.
Building API documentation
To build the API documentation, you must first build the package
and then run npm run apidocs
. For example:
npm run apidocs
Unit tests
You can run the unit tests with npm test
. For example:
npm test
...
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:month:fr-FR
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:month:nums:fr-FR
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:week:en-US
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:week:nums:en-US
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:week:fr-FR
✔ ChronoFieldParserTests › ChronoFieldParser:parseRange:week:nums:fr-FR
─
107 tests passed
-------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ChronoFieldParser.ts | 100 | 100 | 100 | 100 |
IntRange.ts | 100 | 100 | 100 | 100 |
NumberParser.ts | 100 | 100 | 100 | 100 |
TariffRate.ts | 100 | 100 | 100 | 100 |
TemporalRangesTariff.ts | 100 | 100 | 100 | 100 |
utils.ts | 100 | 100 | 100 | 100 |
-------------------------|---------|----------|---------|---------|-------------------
Test coverage
Having a well-tested and reliable library is a core goal of this project. Unit tests are executed
automatically after every push into the main
branch of this repository and their associated code
coverage is uploaded to Codecov.