The Tollmaster's Amended Rulebook
# The Tollmaster's Amended Rulebook
The east gate of bitwick charges every cart that rolls through it. The rulebook has been amended so many times — a levy here, a rebate there, a waiver granted one winter and never struck out — that the clerk who used to read it has taken to her bed. The gate still needs a number for each cart.
The cart. cart is a mapping from attribute names to values, both strings (a plain object in TypeScript, a dict in Python). For example, a cart may carry a goods attribute, a wheels attribute, and so on. Nothing is assumed about which attributes exist.
The rules. rules is a list of rule objects, each with exactly these fields:
when— a mapping from attribute name to a required value, both strings.kind— one of"base","add","free".amount— an integer, possibly negative. Ignored whenkindis"free".rank— an integer, possibly negative.group— a string. Ignored unlesskindis"add". The empty string is an ordinary group name.
Matching. A rule matches the cart when every entry of when is satisfied. For a key k with required value v:
- if
vis"*", the cart must containk(any value); - if
vbegins with"!", the cart must containkand the cart's value must not equalvwithout that leading"!"; - otherwise the cart must contain
kand the cart's value must equalv.
An empty when matches every cart.
Precedence. Of two rules, the one with the greater rank outranks the other. If the ranks are equal, the one whose when has more entries outranks the other. If those counts are also equal, the one appearing later in rules outranks the other.
The toll.
1. Look at the matching rules whose kind is "base" or "free". If there are none, the base charge is 0. Otherwise take the single highest-precedence one among them: if it is "free", the toll is 0 and no surcharge is applied; if it is "base", the base charge is its amount. 2. Look at the matching rules whose kind is "add". Partition them by group; within each group only the highest-precedence one applies. The surcharge is the sum of those winners' amounts. 3. The toll is the base charge plus the surcharge, but never less than 0.
Write tollFor(rules, cart), which returns the toll as an integer.
examples
- case 1
rules = [ {when {}, base 10, rank 0}, {when {goods: ale}, base 20, rank 0}, {when {goods: ale, origin: north}, add 5, rank 0, group "origin"}, {when {origin: "*"}, add 2, rank 0, group "origin"} ]; cart = {goods: ale, origin: north}25 — both base rules match at rank 0, so the one naming more conditions wins (20); both surcharges are in group "origin", so only the more specific one applies (+5) - case 2
rules = [ {when {}, base 6, rank 0}, {when {origin: "!north"}, add -10, rank 0, group "rebate"} ]; cart = {origin: south}0 — the rebate matches because the origin is present and is not north; 6 - 10 is clamped up to 0 - case 3
rules = [ {when {pass: royal}, free, rank 0}, {when {}, base 100, rank 5} ]; cart = {pass: royal}100 — the waiver matches, but the base rule at rank 5 outranks it, so the waiver never takes effect
- reward
- 540 XP
- signature
tollFor
TypeScript, run as ES2022 — types are stripped, not checked. If a JS engine would run it, so will we.
- an empty rulebook charges nothing
- specificity breaks a rank tie, in the base and in a group
- a rebate cannot drive the toll below zero
- a higher-ranked base charge overrides a waiver
- a winning waiver also silences a high-ranked surcharge
- an exact tie goes to the later amendment, with an empty cart
- presence and negation both require the attribute to exist
- surcharges apply with no base rule at all
- the whole book read at once