impl/typescript.ts
2,877 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { epochDayFromIso } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts"; ← from money.sum ^1.0.0 · built alongside by fune
import { ROAMING_TARIFFS, ROAMING_TARIFFS_HISTORY } from "./telecoms_roaming_charge_data.ts"; ← this capability’s own data, compiled from data/roaming-tariffs.json into the same file by fune build
import { type RoamingCharge, type RoamingUsage } from "./telecoms_roaming_charge_types.ts";
const COUNTRY = /^[A-Z]{2}$/;
/**
* Price roaming usage in one country on one date: find the tariff's zone for
* the country in force on that date, then multiply each kind of usage by its
* price. Exact integer arithmetic throughout.
*/
export function roamingCharge(tariff: string, country: string, onDate: string, usage: RoamingUsage): RoamingCharge {
if (!COUNTRY.test(country)) {
throw new RangeError(`country must be an ISO 3166-1 alpha-2 code in capitals, received "${country}"`);
}
epochDayFromIso(onDate);
const counts = [usage.days, usage.dataMb, usage.callOutMinutes, usage.callInMinutes, usage.smsOut];
if (counts.some((n) => !Number.isInteger(n) || n < 0)) {
throw new RangeError("usage must be whole numbers of 0 or more");
}
const covers = (rule: (typeof ROAMING_TARIFFS)[number]) => rule.tariff === tariff && rule.countries.split(" ").includes(country);
let best: (typeof ROAMING_TARIFFS)[number] | null = null;
for (const rule of ROAMING_TARIFFS) {
if (!covers(rule)) continue;
if (onDate < rule.validFrom) continue;
if (rule.validTo !== null && onDate > rule.validTo) continue;
if (best === null || rule.validFrom > best.validFrom) best = rule;
}
if (best === null) {
// A history=current build has dropped the rows that ended; answering an
// older date with today's prices would be wrong, so say what happened.
if (ROAMING_TARIFFS_HISTORY !== "full") {
let earliest: string | null = null;
for (const rule of ROAMING_TARIFFS) {
if (covers(rule) && (earliest === null || rule.validFrom < earliest)) earliest = rule.validFrom;
}
if (earliest !== null && onDate < earliest) {
throw new RangeError(
`no roaming zone for ${country} in tariff ${tariff} on ${onDate}: this build was installed with history=${ROAMING_TARIFFS_HISTORY}, ` +
`so it only carries rules from ${earliest}. Reinstall with history=full to price older usage.`
);
}
}
throw new RangeError(`no roaming zone for ${country} in tariff ${tariff} on ${onDate}`);
}
const c = best.currency;
const dailyFees = money(usage.days * best.dailyFeeMinor, c);
const data = money(usage.dataMb * best.dataPerMbMinor, c);
const calls = money(usage.callOutMinutes * best.callOutPerMinuteMinor + usage.callInMinutes * best.callInPerMinuteMinor, c);
const sms = money(usage.smsOut * best.smsOutMinor, c);
return { tariff, zone: best.zone, dailyFees, data, calls, sms, total: sumMoney([dailyFees, data, calls, sms], c) };
}