跳到正文
DEV Community· Hirantha Sandaruwan Lakshan·· 2 小時前AI 評分29

將斯里蘭卡高速公路收費公報轉為查詢表

Turning Sri Lanka's expressway toll gazette into a lookup table

AI 導讀

作者把斯里蘭卡高速公路收費公報整理成查詢表及收費計算器,供用戶按互通立交和車種查詢收費。南部、外環及卡圖納亞克高速公路的表格涵蓋29個互通立交,共有1,218個三角形表格單元;資料經兩次獨立輸入、逐格比對,並以測試檢查。公報編號及日期與收費資料一同保存,方便日後更新。

正文

Sri Lanka's expressway tolls are set by gazette. The current tables for the Southern, Outer Circular and Katunayake expressways (E01, E02, E03) are in Gazette Extraordinary No. 2467/49 of 17 December 2025. The Central Expressway section (E04) has its own, No. 2329/17 of 24 April 2023. The Road Development Authority's expressway division publishes them on its toll rates page.

They are hard to read on a phone. You need the row for one interchange, the column for the other and the right table for your vehicle. So I turned them into a small lookup and a toll calculator. Here is what the data looks like in code and how I checked it.

The shape of the table

Each vehicle category has one table. The interchanges run down the side and along the top in the same order, from Katunayake (B4) to Hambantota Gate (A21), 29 in all. The fee from A to B is the same as from B to A, so the gazette prints only the lower half: a triangle.

That maps directly to an array of rows, where row n holds the fees from interchange n to every interchange before it:

type Matrix = readonly (readonly (number | null)[])[];

// Interchanges in the gazette's table order.
const SOUTH = [
  { id: "katunayake", name: "Katunayake", exit: "B4" },
  { id: "ja-ela", name: "Ja-Ela", exit: "B3" },
  { id: "kerawalapitiya", name: "Kerawalapitiya", exit: "B2" },
  // … 29 in all
];

// Category 1: row n = fees from interchange n+1 to interchanges 0..n
const CAT1: Matrix = [
  [200],                 // Ja-Ela → Katunayake
  [250, 200],            // Kerawalapitiya → Katunayake, Ja-Ela
  [400, 250, 200],       // Peliyagoda → …
  // …
];

The lookup is three lines once you sort the two indexes:

const lo = Math.min(a, b);
const hi = Math.max(a, b);
const fee = fees[hi - 1]?.[lo] ?? null;

No duplicated upper half to keep in step, and the code has the same shape as the source. That made the checking much easier.

null is information, not a gap

Some cells in the gazette are blacked out as "not functional". Kotalawala, Kapuduwa and Bedigama are half interchanges: you can only travel one way from them. In the matrix those cells are null, and the calculator says why there's no fee instead of guessing one:

if (fee === null) {
  return {
    ok: false,
    reason: `The gazette gives no fee between ${from.name} and ${to.name}: that interchange does not connect in this direction.`,
  };
}

It is tempting to "fill in" a missing cell with the nearest sensible number. For data people pay money against, I'd rather show a clear "no fee here".

Transcribing about 1,200 numbers without trusting yourself

Three triangles of 29 interchanges are 3 × 406 = 1,218 cells, plus the Central Expressway tables. One typo and someone gets a wrong toll.

So the tables were typed in twice, independently, and the two copies were compared cell by cell.

After that, tests check what a correct table has to look like, not just a few hand-picked values:

test("every table is complete, and categories never get cheaper upwards", () => {
  for (let i = 0; i < SOUTH.length; i++)
    for (let j = i + 1; j < SOUTH.length; j++) {
      const f = ["1", "2", "3"].map((c) => fee(SOUTH[i].id, SOUTH[j].id, c));
      if (f.every((x) => x === null)) continue;                 // a not-functional pair
      assert.ok(f.every((x) => x !== null));                    // otherwise all three categories exist
      assert.ok(f[0] <= f[1] && f[1] <= f[2]);                  // a bigger vehicle never pays less
    }
});

test("from Kottawa southwards, a farther exit never costs less", () => {
  for (const c of ["1", "2", "3"]) {
    let last = 0;
    for (const to of SOUTH.slice(kottawa + 1)) {
      const f = fee("kottawa", to.id, c);
      assert.ok(f >= last);
      last = f;
    }
  }
});

These invariants catch the kind of mistake a spot check misses: a digit dropped in the middle of a row, or two cells swapped.

(The source has one quirk worth knowing: for Aparekka to Kapuduwa, category 1 is blacked out but categories 2 and 3 are simply left blank. Both become null, and the test has a comment saying why.)

Keep the date next to the data

Tolls change. The gazette number and date live in the code next to the matrix, and the page shows them:

export const GAZETTE_SOUTH = { no: "2467/49", date: "2025-12-17" } as const;
export const GAZETTE_CENTRAL = { no: "2329/17", date: "2023-04-24" } as const;

When the next gazette comes out, there is one place to change, and the tests say whether the new table still makes sense.

A few results

From the current tables:

Trip Category 1 Category 2 Category 3
Kottawa → Pinnaduwa (Galle) Rs. 500 Rs. 900 Rs. 1,500
Kottawa → Godagama (Matara) Rs. 650 Rs. 1,150 Rs. 1,950
Katunayake → Hambantota Gate Rs. 1,300 Rs. 2,350 Rs. 3,950
Mirigama North → Kurunegala (E04) Rs. 300 Rs. 450 Rs. 700

You can pick any two interchanges here: Sri Lanka expressway toll calculator.

If you've turned another gazette table into data, I'd like to hear how you checked it.


This article was written with the help of AI. The code, numbers and sources come from my own project and were checked against it before publishing.

來源:DEV Community · dev.to