module fahrplanparser; import kxml.xml : readDocument, XmlNode; import std.algorithm : map; import std.array : empty, front; import std.conv : to; import std.datetime : dur, TimeOfDay, DateTimeException; import std.string : format; version (unittest) { import unit_threaded; } import substitution; private: enum departureNodeName = "dp"; enum timeNodeName = "t"; enum realTimeNodeName = "rt"; enum departuresXPath = "/efa/dps/" ~ departureNodeName; template timeXPath(string _timeNodeName = timeNodeName) { enum timeXPath = "/st/" ~ _timeNodeName; } enum useRealTimeXPath = "/realtime"; enum lineXPath = "/m/nu"; enum directionXPath = "/m/des"; public: /*********************************** * Parses the departure monitor data and returns it as an associative array. * data is expected to contain valid XML as returned by queries sent to http://mobile.defas-fgi.de/beg/. */ auto parsedFahrplan(in string data) { // dfmt off return data.readDocument .parseXPath(departuresXPath) .map!(dp => ["departure" : "%02s:%02s".format(dp.departureTime.hour, dp.departureTime.minute), "delay" : dp.delay.total!"minutes".to!string, "line": dp.parseXPath(lineXPath).front.getCData, "direction": dp.parseXPath(directionXPath).front.getCData.substitute]); // dfmt on } /// @system unittest { import std.array : array; "".parsedFahrplan.array.shouldEqual([]); "".parsedFahrplan.array.shouldEqual([]); q"[ 1 1224 1242 6 Wernerwerkstraße ]".parsedFahrplan.array.shouldEqual([["direction" : "Wernerwerkstraße", "line" : "6", "departure" : "12:24", "delay" : "18"]]); q"[ 0 1224 6 Wernerwerkstraße ]".parsedFahrplan.array.shouldEqual([["direction" : "Wernerwerkstraße", "line" : "6", "departure" : "12:24", "delay" : "0"]]); q"[ 0 1224 6 Wernerwerkstraße 1 1353 1356 11 Burgweinting ]".parsedFahrplan.array.shouldEqual([["direction" : "Wernerwerkstraße", "line" : "6", "departure" : "12:24", "delay" : "0"], ["direction" : "Burgweinting", "line" : "11", "departure" : "13:53", "delay" : "3"]]); } private: class UnexpectedValueException(T) : Exception { this(T t, string node) @safe pure { super(`Unexpected value "%s" for node "%s"`.format(t, node)); } } class CouldNotFindeNodeException : Exception { this(string node) @safe pure { super(`Could not find node "%s"`.format(node)); } } auto departureTime(string _timeNodeName = timeNodeName)(XmlNode dp) in { assert(dp.getName == departureNodeName); } body { auto timeNodes = dp.parseXPath(timeXPath!_timeNodeName); if (timeNodes.empty) throw new CouldNotFindeNodeException(_timeNodeName); return TimeOfDay.fromISOString(timeNodes.front.getCData ~ "00"); } @system unittest { "0000".readDocument.parseXPath("/dp") .front.departureTime.shouldEqual(TimeOfDay(0, 0)); "0013".readDocument.parseXPath("/dp") .front.departureTime.shouldEqual(TimeOfDay(0, 13)); "1100".readDocument.parseXPath("/dp") .front.departureTime.shouldEqual(TimeOfDay(11, 00)); "1242".readDocument.parseXPath("/dp") .front.departureTime.shouldEqual(TimeOfDay(12, 42)); "2359".readDocument.parseXPath("/dp") .front.departureTime.shouldEqual(TimeOfDay(23, 59)); "2400".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "0061".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "2567".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "0".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "00".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "000000".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "00:00".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; "abcd".readDocument.parseXPath("/dp") .front.departureTime.shouldThrow!DateTimeException; } auto delay(XmlNode dp) in { assert(dp.getName == departureNodeName); } body { immutable useRealtimeString = dp.parseXPath(useRealTimeXPath).front.getCData; if (useRealtimeString == "0") return dur!"minutes"(0); else if (useRealtimeString == "1") { try { immutable expectedTime = dp.departureTime; immutable realTime = dp.departureTime!realTimeNodeName; auto timeDiff = realTime - expectedTime; if (timeDiff < dur!"minutes"(0)) timeDiff = dur!"hours"(24) + timeDiff; return timeDiff; } catch (CouldNotFindeNodeException e) { return dur!"minutes"(0); } } else throw new UnexpectedValueException!string(useRealtimeString, "realtime"); } @system unittest { import core.exception : AssertError; "0".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(0)); "".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(UnexpectedValueException!string); "2".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(UnexpectedValueException!string); "a".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(UnexpectedValueException!string); "1".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"seconds"(0)); "1".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(DateTimeException); "1".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"seconds"(0)); "".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(AssertError); "1".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(DateTimeException); "10000".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(DateTimeException); "10000".readDocument.parseXPath("/dp") .front.delay.shouldThrow!(DateTimeException); "100000000".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(0)); "100010000".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(1)); "117531751".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(2)); "110101000".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(10)); "113011242".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(19)); "100001242".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(678)); "100002359".readDocument.parseXPath("/dp") .front.delay.shouldEqual(dur!"minutes"(1)); }