Merge branch 'master' into 'release'
0.1.0 release See merge request !5
This commit is contained in:
commit
366fb9569e
9 changed files with 451 additions and 81 deletions
25
.gitlab/issue_templates/Bug.md
Normal file
25
.gitlab/issue_templates/Bug.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
Please add the following, if applicable, remove otherwise:
|
||||||
|
|
||||||
|
## Summary ##
|
||||||
|
|
||||||
|
(max. 60 words)
|
||||||
|
|
||||||
|
## Basic information ##
|
||||||
|
|
||||||
|
- Version:
|
||||||
|
- Component:
|
||||||
|
- Platform: (RasPi, x86_64, i686)
|
||||||
|
|
||||||
|
## Description ##
|
||||||
|
|
||||||
|
### Expected behaviour ###
|
||||||
|
|
||||||
|
### Actual behaviour ###
|
||||||
|
|
||||||
|
### Reproduction steps ##
|
||||||
|
|
||||||
|
(Please attach files if necessary)
|
||||||
|
|
||||||
|
## Other things to be mentioned ##
|
||||||
|
|
||||||
|
(Ideas for fixing, requests for comments, …)
|
16
.gitlab/issue_templates/Enhancement.md
Normal file
16
.gitlab/issue_templates/Enhancement.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
## Summary ##
|
||||||
|
|
||||||
|
(max 60 words)
|
||||||
|
|
||||||
|
## Basic information ##
|
||||||
|
|
||||||
|
- Component:
|
||||||
|
- New node needed: (Yes/No)
|
||||||
|
|
||||||
|
## Description ##
|
||||||
|
|
||||||
|
As a <role>, I want to <what?>
|
||||||
|
|
||||||
|
## Acceptance criteria ##
|
||||||
|
|
||||||
|
(How should this be in the end?)
|
5
.gitlab/merge_request_templates/Ready_to_Merge.md
Normal file
5
.gitlab/merge_request_templates/Ready_to_Merge.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
As mentioned in <Merge Request Reference>, this implements <What?>.
|
||||||
|
|
||||||
|
This includes the following changes:
|
||||||
|
- …
|
||||||
|
- …
|
9
.gitlab/merge_request_templates/Work_In_Progress.md
Normal file
9
.gitlab/merge_request_templates/Work_In_Progress.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
As mentioned in <Merge Request Reference>, this implements <What?>
|
||||||
|
|
||||||
|
For this, the following steps are needed:
|
||||||
|
|
||||||
|
- [ ] List
|
||||||
|
- [ ] of
|
||||||
|
- [ ] steps
|
||||||
|
- [ ] and
|
||||||
|
- [ ] substeps
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
bayernfahrplan
|
||||||
|
==============
|
||||||
|
|
||||||
|
A JSON-Getter written in D for bayernfahrplan
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
bayernfahrplan is a tool wich lets you fetch departure tables from
|
||||||
|
bayernfahrplan.de and output them as JSON.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: bayernfahrplan [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f --file The file that the data is written to.
|
||||||
|
-s --stop The bus stop for which to fetch data.
|
||||||
|
-r --replacement-file The file that contais the direction name replacement info.
|
||||||
|
-h --help This help information.
|
||||||
|
```
|
2
dub.json
2
dub.json
|
@ -5,7 +5,7 @@
|
||||||
"Oliver Rümpelein"
|
"Oliver Rümpelein"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"requests": "~>0.3.1",
|
"requests": "~>0.4.1",
|
||||||
"kxml": "~>1.0.1"
|
"kxml": "~>1.0.1"
|
||||||
},
|
},
|
||||||
"description": "A minimal D application.",
|
"description": "A minimal D application.",
|
||||||
|
|
104
source/app.d
104
source/app.d
|
@ -1,90 +1,72 @@
|
||||||
import std.algorithm : filter, map, startsWith;
|
import std.array : array, replace;
|
||||||
import std.array : array, empty, front, replace;
|
import std.datetime : Clock;
|
||||||
import std.conv : to;
|
import std.file : exists, isFile;
|
||||||
import std.datetime : dur, TimeOfDay, Clock;
|
import std.format : format;
|
||||||
import std.getopt : defaultGetoptPrinter, getopt;
|
import std.getopt : defaultGetoptPrinter, getopt;
|
||||||
import std.json : JSONValue;
|
import std.json : JSONValue;
|
||||||
import std.regex : ctRegex, matchAll;
|
import std.stdio : File, writeln;
|
||||||
import std.stdio : File, stdout, writeln;
|
|
||||||
import std.string : strip;
|
|
||||||
import std.typecons : tuple;
|
|
||||||
import std.format : format;
|
|
||||||
|
|
||||||
import kxml.xml : readDocument, XmlNode;
|
import requests : getContent;
|
||||||
|
|
||||||
import requests : postContent;
|
import fahrplanparser;
|
||||||
|
|
||||||
import substitution;
|
import substitution;
|
||||||
|
|
||||||
auto parseTime(in string input)
|
enum baseURL = "http://mobile.defas-fgi.de/beg/";
|
||||||
{
|
enum departureMonitorRequest = "XML_DM_REQUEST";
|
||||||
auto matches = matchAll(input, ctRegex!(`(?P<hours>\d+):(?P<minutes>\d+)`));
|
|
||||||
auto actualTime = TimeOfDay(matches.front["hours"].to!int, matches.front["minutes"].to!int);
|
|
||||||
matches.popFront;
|
|
||||||
if (!matches.empty)
|
|
||||||
{
|
|
||||||
auto expectedTime = TimeOfDay(matches.front["hours"].to!int,
|
|
||||||
matches.front["minutes"].to!int);
|
|
||||||
return tuple(expectedTime, actualTime - expectedTime);
|
|
||||||
|
|
||||||
}
|
|
||||||
return tuple(actualTime, dur!"minutes"(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto getRowContents(XmlNode[] rows)
|
|
||||||
{
|
|
||||||
return rows.map!(row => row.parseXPath("//td")[1 .. $ - 1].map!((column) {
|
|
||||||
auto link = column.parseXPath("//a");
|
|
||||||
if (!link.empty)
|
|
||||||
return link.front.getCData.replace("...", "");
|
|
||||||
return column.getCData;}));
|
|
||||||
}
|
|
||||||
|
|
||||||
void main(string[] args)
|
void main(string[] args)
|
||||||
{
|
{
|
||||||
string fileName;
|
string fileName;
|
||||||
string busStop = "Universität Regensburg";
|
string busStop = "Regensburg Universität";
|
||||||
string substitutionFileName = "replacement.txt";
|
string substitutionFileName = "replacement.txt";
|
||||||
|
// dfmt off
|
||||||
auto helpInformation = getopt(args,
|
auto helpInformation = getopt(args,
|
||||||
"file|f", "The file that the data is written to.", &fileName,
|
"file|f", "The file that the data is written to.", &fileName,
|
||||||
"stop|s", "The bus stop for which to fetch data.", &busStop,
|
"stop|s", "The bus stop for which to fetch data.", &busStop,
|
||||||
"replacement-file|r", "The file that contais the direction name replacement info.", &substitutionFileName);
|
"replacement-file|r", "The file that contais the direction name replacement info.", &substitutionFileName);
|
||||||
|
// dfmt on
|
||||||
|
|
||||||
if (helpInformation.helpWanted)
|
if (helpInformation.helpWanted)
|
||||||
{
|
{
|
||||||
defaultGetoptPrinter("Some information about the program.", helpInformation.options);
|
defaultGetoptPrinter("Usage: bayernfahrplan [options]\n\n Options:", helpInformation.options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto content = postContent("http://txt.bayern-fahrplan.de/textversion/bcl_abfahrtstafel",
|
// dfmt off
|
||||||
["limit" : "20",
|
auto content = getContent(baseURL ~ departureMonitorRequest,
|
||||||
"useRealtime" : "1",
|
["outputFormat" : "XML",
|
||||||
"name_dm" : busStop,
|
"language" : "de",
|
||||||
"mode" : "direct",
|
"stateless" : "1",
|
||||||
"type_dm" : "any",
|
"type_dm" : "stop",
|
||||||
"itdLPxx_bcl" : "true"]);
|
"name_dm" : busStop,
|
||||||
|
"useRealtime" : "1",
|
||||||
|
"mode" : "direct",
|
||||||
|
"ptOptionActive" : "1",
|
||||||
|
"mergeDep" : "1",
|
||||||
|
"limit" : "20",
|
||||||
|
"deleteAssignedStops_dm" : "1"]);
|
||||||
|
// dfmt on
|
||||||
|
|
||||||
|
if (substitutionFileName.exists && substitutionFileName.isFile)
|
||||||
|
{
|
||||||
|
loadSubstitutionFile(substitutionFileName);
|
||||||
|
}
|
||||||
|
|
||||||
auto currentTime = Clock.currTime;
|
auto currentTime = Clock.currTime;
|
||||||
loadSubstitutionFile(substitutionFileName);
|
|
||||||
JSONValue j = ["time" : "%02s:%02s".format(currentTime.hour, currentTime.minute)];
|
JSONValue j = ["time" : "%02s:%02s".format(currentTime.hour, currentTime.minute)];
|
||||||
j.object["departures"] = readDocument(cast(string) content.data)
|
|
||||||
.parseXPath(`//table[@id="departureMonitor"]/tbody/tr`)[1 .. $]
|
|
||||||
.getRowContents
|
|
||||||
.filter!(row => !row.empty)
|
|
||||||
.map!(a => ["departure" : a[0].parseTime[0].to!string[0 .. $ - 3],
|
|
||||||
"delay" : a[0].parseTime[1].total!"minutes".to!string,
|
|
||||||
"line" : a[1],
|
|
||||||
"direction" : a[2].substitute])
|
|
||||||
.array.JSONValue;
|
|
||||||
|
|
||||||
|
j.object["departures"] = (cast(string) content.data).parsedFahrplan.array.JSONValue;
|
||||||
|
auto output = j.toPrettyString.replace("\\/", "/");
|
||||||
if (fileName !is null)
|
if (fileName !is null)
|
||||||
{
|
{
|
||||||
auto output = File(fileName, "w");
|
auto outfile = File(fileName, "w");
|
||||||
scope(exit) output.close;
|
scope (exit)
|
||||||
output.writeln(j.toPrettyString.replace("\\/", "/"));
|
outfile.close;
|
||||||
|
outfile.writeln(output);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
j.toPrettyString.replace("\\/", "/").writeln;
|
output.writeln;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
242
source/fahrplanparser.d
Normal file
242
source/fahrplanparser.d
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
module fahrplanparser;
|
||||||
|
|
||||||
|
import std.algorithm : map;
|
||||||
|
import std.array : empty, front;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.datetime : dur, TimeOfDay, DateTimeException;
|
||||||
|
import std.string : format;
|
||||||
|
|
||||||
|
import kxml.xml : readDocument, XmlNode;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
auto xml = "";
|
||||||
|
assert(xml.parsedFahrplan.array == []);
|
||||||
|
|
||||||
|
xml = "<efa><dps></dps></efa>";
|
||||||
|
assert(xml.parsedFahrplan.array == []);
|
||||||
|
|
||||||
|
xml = "<efa><dps><dp><realtime>1</realtime><st><t>1224</t><rt>1242</rt></st><m><nu>6</nu><des>Wernerwerkstraße</des></m></dp></dps></efa>";
|
||||||
|
assert(xml.parsedFahrplan.array == [["direction" : "Wernerwerkstraße",
|
||||||
|
"line" : "6", "departure" : "12:24", "delay" : "18"]]);
|
||||||
|
|
||||||
|
xml = "<efa><dps><dp><realtime>0</realtime><st><t>1224</t></st><m><nu>6</nu><des>Wernerwerkstraße</des></m></dp></dps></efa>";
|
||||||
|
assert(xml.parsedFahrplan.array == [["direction" : "Wernerwerkstraße",
|
||||||
|
"line" : "6", "departure" : "12:24", "delay" : "0"]]);
|
||||||
|
|
||||||
|
xml = "<efa><dps><dp><realtime>0</realtime><st><t>1224</t></st><m><nu>6</nu><des>Wernerwerkstraße</des></m></dp><dp><realtime>1</realtime><st><t>1353</t><rt>1356</rt></st><m><nu>11</nu><des>Burgweinting</des></m></dp></dps></efa>";
|
||||||
|
assert(xml.parsedFahrplan.array == [["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
|
||||||
|
{
|
||||||
|
import std.exception : assertThrown;
|
||||||
|
|
||||||
|
auto xml = "<dp><st><t>0000</t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.departureTime == TimeOfDay(0, 0));
|
||||||
|
|
||||||
|
xml = "<dp><st><t>0013</t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.departureTime == TimeOfDay(0, 13));
|
||||||
|
|
||||||
|
xml = "<dp><st><t>1100</t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.departureTime == TimeOfDay(11, 00));
|
||||||
|
|
||||||
|
xml = "<dp><st><t>1242</t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.departureTime == TimeOfDay(12, 42));
|
||||||
|
|
||||||
|
xml = "<dp><st><t>2359</t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.departureTime == TimeOfDay(23, 59));
|
||||||
|
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>2400</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>0061</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>2567</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t></t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>0</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>00</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>000000</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>00:00</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
assertThrown!DateTimeException("<dp><st><t>abcd</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front.departureTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 std.exception : assertThrown;
|
||||||
|
import core.exception : AssertError;
|
||||||
|
|
||||||
|
auto xml = "<dp><realtime>0</realtime></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(0));
|
||||||
|
|
||||||
|
xml = "<dp><realtime></realtime></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!(UnexpectedValueException!string)(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>2</realtime></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!(UnexpectedValueException!string)(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>a</realtime></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!(UnexpectedValueException!string)(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!AssertError(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><t></t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!DateTimeException(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt></rt></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!AssertError(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><st><rt></rt><t></t></st></dp>".readDocument.parseXPath("/dp").front;
|
||||||
|
assertThrown!AssertError(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt></rt><t></t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front;
|
||||||
|
assertThrown!DateTimeException(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>0000</rt><t></t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front;
|
||||||
|
assertThrown!DateTimeException(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt></rt><t>0000</t></st></dp>".readDocument.parseXPath("/dp")
|
||||||
|
.front;
|
||||||
|
assertThrown!DateTimeException(xml.delay);
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>0000</rt><t>0000</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(0));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>0001</rt><t>0000</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(1));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>1753</rt><t>1751</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(2));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>1010</rt><t>1000</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(10));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>1301</rt><t>1242</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(19));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>0000</rt><t>1242</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(678));
|
||||||
|
|
||||||
|
xml = "<dp><realtime>1</realtime><st><rt>0000</rt><t>2359</t></st></dp>"
|
||||||
|
.readDocument.parseXPath("/dp").front;
|
||||||
|
assert(xml.delay == dur!"minutes"(1));
|
||||||
|
}
|
|
@ -1,35 +1,105 @@
|
||||||
module substitution;
|
module substitution;
|
||||||
|
|
||||||
|
import std.file : slurp;
|
||||||
|
import std.meta : AliasSeq;
|
||||||
|
import std.traits : Parameters;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
void loadSubstitutionFile(string fileName)
|
/***********************************
|
||||||
|
* Loads a substitution dictonary from a file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void loadSubstitutionFile(alias slurpFun = slurp)(string fileName)
|
||||||
|
if (is(Parameters!(slurpFun!(string, string)) == AliasSeq!(string, const char[])))
|
||||||
{
|
{
|
||||||
import std.file : slurp, exists, isFile;
|
|
||||||
import std.array : assocArray;
|
|
||||||
import std.algorithm.iteration : each;
|
import std.algorithm.iteration : each;
|
||||||
|
|
||||||
if (fileName.exists && fileName.isFile)
|
map = (string[string]).init;
|
||||||
{
|
slurpFun!(string, string)(fileName, `"%s" = "%s"`).each!(pair => map[pair[0]] = pair[1]);
|
||||||
auto data = slurp!(string, string)(fileName, `"%s" = "%s"`);
|
|
||||||
map = (string[string]).init;
|
|
||||||
data.each!(pair => map[pair[0]] = pair[1]);
|
|
||||||
foreach (pair; data)
|
|
||||||
{
|
|
||||||
map[pair[0]] = pair[1];
|
|
||||||
}
|
|
||||||
map.rehash;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
map = (string[string]).init;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto substitute(string s)
|
///
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
import std.typecons : Tuple, tuple;
|
||||||
|
|
||||||
|
static Tuple!(string, string)[] mockSlurpEmpty(Type1, Type2)(string filename, in char[] format)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSubstitutionFile!mockSlurpEmpty("");
|
||||||
|
assert(map.length == 0);
|
||||||
|
|
||||||
|
static Tuple!(string, string)[] mockSlurpEmptyEntry(Type1, Type2)(string filename,
|
||||||
|
in char[] format)
|
||||||
|
{
|
||||||
|
return [tuple("", "")];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSubstitutionFile!mockSlurpEmptyEntry("");
|
||||||
|
assert("" in map);
|
||||||
|
assert(map.length == 1);
|
||||||
|
assert(map[""] == "");
|
||||||
|
|
||||||
|
static Tuple!(string, string)[] mockSlurpSingleEntry(Type1, Type2)(string filename,
|
||||||
|
in char[] format)
|
||||||
|
{
|
||||||
|
return [tuple("foo", "bar")];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSubstitutionFile!mockSlurpSingleEntry("");
|
||||||
|
assert("foo" in map);
|
||||||
|
assert(map.length == 1);
|
||||||
|
assert(map["foo"] == "bar");
|
||||||
|
|
||||||
|
static Tuple!(string, string)[] mockSlurpMultipleEntries(Type1, Type2)(
|
||||||
|
string filename, in char[] format)
|
||||||
|
{
|
||||||
|
return [tuple("", ""), tuple("0", "1"), tuple("Text in", "wird durch diesen ersetzt")];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSubstitutionFile!mockSlurpMultipleEntries("");
|
||||||
|
assert("" in map);
|
||||||
|
assert("0" in map);
|
||||||
|
assert("Text in" in map);
|
||||||
|
assert(map.length == 3);
|
||||||
|
assert(map[""] == "");
|
||||||
|
assert(map["0"] == "1");
|
||||||
|
assert(map["Text in"] == "wird durch diesen ersetzt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Substitutes a string with its corresponding replacement, if one is available.
|
||||||
|
* Otherwise just returns the original string.
|
||||||
|
*/
|
||||||
|
|
||||||
|
auto substitute(string s) @safe nothrow
|
||||||
{
|
{
|
||||||
return s in map ? map[s] : s;
|
return s in map ? map[s] : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
map[""] = "";
|
||||||
|
assert(substitute("") == "");
|
||||||
|
|
||||||
|
map["a"] = "b";
|
||||||
|
assert(substitute("a") == "b");
|
||||||
|
|
||||||
|
map["Regensburg Danziger Freiheit"] = "Danziger Freiheit";
|
||||||
|
assert(substitute("Regensburg Danziger Freiheit") == "Danziger Freiheit");
|
||||||
|
|
||||||
|
map["Regensburg Danziger Freiheit"] = "Anderer Test";
|
||||||
|
assert(substitute("Regensburg Danziger Freiheit") == "Anderer Test");
|
||||||
|
|
||||||
|
assert(substitute("z") == "z");
|
||||||
|
|
||||||
|
assert(substitute("Regensburg Hauptbahnhof") == "Regensburg Hauptbahnhof");
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
string[string] map;
|
string[string] map;
|
||||||
|
|
Loading…
Reference in a new issue