import gleam/http/response.{Response} import gleam/http/request.{Request} import gleam/http.{Get} import gleam/bit_builder.{BitBuilder} import gleam/erlang/process import gleam/io import gleam/int import gleam/string import gleam/bit_string import gleam/list import gleam/map.{Map} import gleam/result.{lazy_unwrap} import gleam/uri import gleam/hackney import gleam/option.{None, Some} import mist fn do_distlist( b: String, distlist: List(Int), grapheme: String, new_distlist: List(Int), last_dist: Int, ) { case #(b, distlist) { #("", _) -> list.reverse(new_distlist) #(_, [distlist_hd, distlist_snd, ..distlist_tl]) -> { let assert Ok(b_hd) = string.first(b) let b_tl = string.drop_left(b, up_to: 1) let diff = case #(b_hd, grapheme) { #(a, b) if a != b -> 1 _ -> 0 } let minimum = int.min(int.min(last_dist + 1, distlist_snd + 1), distlist_hd + diff) do_distlist( b_tl, [distlist_snd, ..distlist_tl], grapheme, [minimum, ..new_distlist], minimum, ) } } } fn do_distance(a: String, b: String, distlist: List(Int), step: Int) { case a { "" -> result.unwrap(list.last(distlist), -1) _ -> { let assert Ok(src_hd) = string.first(a) let src_tl = string.drop_left(a, up_to: 1) let distlist = do_distlist(b, distlist, src_hd, [step], step) do_distance(src_tl, b, distlist, step + 1) } } } fn levenshtein(a: String, b: String) -> Int { case #(a, b) { #(a, b) if a == b -> 0 #("", b) -> string.length(b) #(a, "") -> string.length(a) #(a, b) -> { let distlist = list.range(0, string.length(b)) do_distance(a, b, distlist, 1) } } } fn unpercent(encoded: String) -> String { let #([head], chunks) = encoded |> string.split(on: "%") |> list.split(at: 1) let assert Ok(res) = chunks |> list.map(fn(str) { case string.length(str) < 2 { True -> bit_string.from_string(str) False -> { let assert Ok(codepoint) = str |> string.slice(at_index: 0, length: 2) |> int.base_parse(16) <> } } }) |> list.prepend(bit_string.from_string(head)) |> bit_string.concat |> bit_string.to_string res } fn guess_station(query: String, stations: Map(String, String)) -> String { query stations |> map.keys |> list.map(fn(a) { #(levenshtein(query, a), a) }) |> list.fold( from: #(string.length(query), query), with: fn(a, b) { case a.0 < b.0 { True -> a False -> b } }, ) |> fn(a: #(Int, String)) { a.1 } } fn the_lookup( query: String, stations: Map(String, String), ds100s: Map(String, String), ) -> String { map.get(ds100s, query) |> lazy_unwrap(fn() { io.println(query) map.get(stations, query) |> lazy_unwrap(fn() { guess_station(query, stations) }) }) } fn lookup_station( request: Request(t), stations: Map(String, String), ds100s: Map(String, String), baseurl: String, ) -> Response(BitBuilder) { let #(code, text) = case request { Request(method: Get, path: "/help", ..) | Request(method: Get, path: "/", ..) -> #( 200, "ds100 → Name: " <> baseurl <> "/NN\n" <> "Name → ds100: " <> baseurl <> "/Nürnberg Hbf", ) Request(method: Get, path: "/" <> path, ..) -> #( 200, the_lookup(unpercent(path), stations, ds100s), ) _ -> #(404, "intended usage is e.g. curl " <> baseurl <> "/FF") } let body = bit_builder.from_string(text) response.new(code) |> response.prepend_header( "x-data-source", "https://data.deutschebahn.com/dataset/data-betriebsstellen.html", ) |> response.prepend_header( "x-sources-at", "https://stuebinm.eu/git/bahnhof.name", ) |> response.prepend_header("content-type", "text/plain; charset=utf8") |> response.set_body(body) } fn fetch_data() -> Result(String, hackney.Error) { let assert Ok(uri) = uri.parse( "https://download-data.deutschebahn.com/static/datasets/betriebsstellen/DBNetz-Betriebsstellenverzeichnis-Stand2021-10.csv", ) let assert Ok(request) = request.from_uri(uri) let assert Ok(response) = hackney.send(request) Ok(response.body) } fn read_csv() -> List(#(String, String)) { // let assert Ok(contents) = file.read(path) let assert Ok(contents) = fetch_data() contents // the file doesn't use quotes, so this is fine |> string.split(on: "\n") |> list.map(fn(a) { string.split(a, on: ";") }) |> list.filter_map(fn(fields) { case fields { [_, ds100, name, ..] -> Ok(#(name, ds100)) _ -> Error(fields) } }) } pub fn main() { let baseurl = "https://bahnhof.name" let stations = read_csv() let stationmap = stations |> map.from_list let ds100map = stations |> list.map(fn(a) { #(a.1, a.0) }) |> map.from_list mist.run_service( 2345, fn(req) { lookup_station(req, stationmap, ds100map, baseurl) }, max_body_limit: 100, ) process.sleep_forever() }