KItinerary

rct2ticket.cpp
1/*
2 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "rct2ticket.h"
8#include "logging.h"
9#include "uic9183ticketlayout.h"
10
11#include "text/pricefinder_p.h"
12
13#include <QDateTime>
14#include <QDebug>
15#include <QRegularExpression>
16
17#include <cmath>
18#include <cstring>
19
20using namespace KItinerary;
21
22namespace KItinerary {
23
24class Rct2TicketPrivate : public QSharedData
25{
26public:
27 QDate firstDayOfValidity() const;
28 QDateTime parseTime(const QString &dateStr, const QString &timeStr) const;
29 QString reservationPatternCapture(QStringView name) const;
30
32 QDateTime contextDt;
33};
34
35}
36
37QDate Rct2TicketPrivate::firstDayOfValidity() const
38{
39 const auto f = layout.text(3, 1, 48, 1);
40 const auto it = std::find_if(f.begin(), f.end(), [](QChar c) { return c.isDigit(); });
41 if (it == f.end()) {
42 return {};
43 }
44 const auto dtStr = QStringView(f).mid(std::distance(f.begin(), it));
45 auto dt = QDate::fromString(dtStr.left(10).toString(), QStringLiteral("dd.MM.yyyy"));
46 if (dt.isValid()) {
47 return dt;
48 }
49 dt = QDate::fromString(dtStr.left(8).toString(), QStringLiteral("dd.MM.yy"));
50 if (dt.isValid()) {
51 if (dt.year() < 2000) {
52 dt.setDate(dt.year() + 100, dt.month(), dt.day());
53 }
54 return dt;
55 }
56 dt = QDate::fromString(dtStr.left(4).toString(), QStringLiteral("yyyy"));
57 return dt;
58}
59
60QDateTime Rct2TicketPrivate::parseTime(const QString &dateStr, const QString &timeStr) const
61{
62 auto d = QDate::fromString(dateStr, QStringLiteral("dd.MM"));
63 if (!d.isValid()) {
64 d = QDate::fromString(dateStr, QStringLiteral("dd/MM"));
65 }
66 if (!d.isValid()) {
67 d = QDate::fromString(dateStr, QStringLiteral("dd-MM"));
68 }
69 auto t = QTime::fromString(timeStr, QStringLiteral("hh:mm"));
70 if (!t.isValid()) {
71 t = QTime::fromString(timeStr, QStringLiteral("hh.mm"));
72 }
73
74 const auto validDt = firstDayOfValidity();
75 const auto baseDate = validDt.isValid() ? validDt : contextDt.date();
76 auto dt = QDateTime({baseDate.year(), d.month(), d.day()}, t);
77 if (dt.isValid() && dt.date() < baseDate) {
78 dt = dt.addYears(1);
79 }
80 return dt;
81}
82
83static constexpr const char* res_patterns[] = {
84 "ZUG +(?P<train_number>\\d+) +(?P<train_category>[A-Z][A-Z0-9]+) +WAGEN +(?P<coach>\\d+) +PLATZ +(?P<seat>\\d[\\d, ]+)",
85 "ZUG +(?P<train_number>\\d+) +WAGEN +(?P<coach>\\d+) +PLATZ +(?P<seat>\\d[\\d, ]+)",
86};
87
88QString Rct2TicketPrivate::reservationPatternCapture(QStringView name) const
89{
90 const auto text = layout.text(8, 0, 72, 1);
91 for (const auto *pattern : res_patterns) {
94 Q_ASSERT(re.isValid());
95 const auto match = re.match(text);
96 if (match.hasMatch()) {
97 return match.captured(name);
98 }
99 }
100 return {};
101}
102
103
104// 6x "U_TLAY"
105// 2x version (always "01")
106// 4x record length, numbers as ASCII text
107// 4x ticket layout type ("RCT2")
108// 4x field count
109// Nx fields (see Rct2TicketField)
110Rct2Ticket::Rct2Ticket()
111 : d(new Rct2TicketPrivate)
112{
113}
114
115Rct2Ticket::Rct2Ticket(const Uic9183TicketLayout &layout)
116 : d(new Rct2TicketPrivate)
117{
118 d->layout = layout;
119}
120
121Rct2Ticket::Rct2Ticket(const Rct2Ticket&) = default;
122Rct2Ticket::~Rct2Ticket() = default;
123Rct2Ticket& Rct2Ticket::operator=(const Rct2Ticket&) = default;
124
126{
127 return d->layout.isValid() && d->layout.type() == QLatin1StringView("RCT2");
128}
129
131{
132 d->contextDt = contextDt;
133}
134
135QDate Rct2Ticket::firstDayOfValidity() const
136{
137 return d->firstDayOfValidity();
138}
139
140static constexpr const struct {
141 const char *name; // case folded
142 Rct2Ticket::Type type;
143} rct2_ticket_type_map[] = {
144 { "ticket+reservation", Rct2Ticket::TransportReservation },
145 { "fahrschein+reservierung", Rct2Ticket::TransportReservation },
146 { "menetjegy+helyjegy", Rct2Ticket::TransportReservation },
147 { "upgrade", Rct2Ticket::Upgrade },
148 { "aufpreis", Rct2Ticket::Upgrade },
149 { "ticket", Rct2Ticket::Transport },
150 { "billet", Rct2Ticket::Transport },
151 { "fahrkarte", Rct2Ticket::Transport },
152 { "fahrschein", Rct2Ticket::Transport },
153 { "cestovny listok", Rct2Ticket::Transport },
154 { "jizdenka", Rct2Ticket::Transport },
155 { "menetjegy", Rct2Ticket::Transport },
156 { "reservation", Rct2Ticket::Reservation },
157 { "reservierung", Rct2Ticket::Reservation },
158 { "helyjegy", Rct2Ticket::Reservation },
159 { "interrail", Rct2Ticket::RailPass },
160};
161
162Rct2Ticket::Type Rct2Ticket::type() const
163{
164 // in theory: columns 15 - 18 blank, columns 19 - 51 ticket type (1-based indices!)
165 // however, some providers overrun and also use the blank columns, so consider those too
166 // if they are really empty, we trim them anyway.
167 const auto typeName1 = d->layout.text(0, 14, 38, 1).trimmed().remove(QLatin1Char(' ')).toCaseFolded();
168 const auto typeName2 = d->layout.text(1, 14, 38, 1).trimmed().remove(QLatin1Char(' ')).toCaseFolded(); // used for alternative language type name
169
170 // prefer exact matches
171 for (auto it = std::begin(rct2_ticket_type_map); it != std::end(rct2_ticket_type_map); ++it) {
172 if (typeName1 == QLatin1StringView(it->name) ||
173 typeName2 == QLatin1String(it->name)) {
174 return it->type;
175 }
176 }
177 for (auto it = std::begin(rct2_ticket_type_map); it != std::end(rct2_ticket_type_map); ++it) {
178 if (typeName1.contains(QLatin1StringView(it->name)) ||
179 typeName2.contains(QLatin1String(it->name))) {
180 return it->type;
181 }
182 }
183
184 // alternatively, check all fields covering the title area, for even more creative placements...
185 for (const auto &f : d->layout.fields(0, 14, 38, 2)) {
186 for (auto it = std::begin(rct2_ticket_type_map); it != std::end(rct2_ticket_type_map); ++it) {
187 if (f.text().toCaseFolded().contains(QLatin1StringView(it->name))) {
188 return it->type;
189 }
190 }
191 }
192
193 return Unknown;
194}
195
196QString Rct2Ticket::title() const
197{
198 // RPT has shorter title fields
199 if (type() == Rct2Ticket::RailPass) {
200 return d->layout.text(0, 18, 19, 1);
201 }
202
203 // somewhat standard compliant layout
204 if (d->layout.text(0, 15, 3, 1).trimmed().isEmpty()) {
205 const auto s = d->layout.text(0, 18, 33, 1).trimmed();
206 return s.isEmpty() ? d->layout.text(1, 18, 33, 1).trimmed() : s;
207 }
208
209 // "creative" layout
210 return d->layout.text(0, 0, 52, 1).trimmed();
211}
212
213QString Rct2Ticket::passengerName() const
214{
215 const auto name = d->layout.text(0, 52, 19, 1).trimmed();
216 // sanity-check if this is a plausible name, e.g. Renfe has random other stuff here
217 return std::any_of(name.begin(), name.end(), [](QChar c) { return c.isDigit(); }) ? QString() : name;
218}
219
220QDateTime Rct2Ticket::outboundDepartureTime() const
221{
222 return d->parseTime(d->layout.text(6, 1, 5, 1).trimmed(), d->layout.text(6, 7, 5, 1).trimmed());
223}
224
225QDateTime Rct2Ticket::outboundArrivalTime() const
226{
227 auto dt = d->parseTime(d->layout.text(6, 52, 5, 1).trimmed(), d->layout.text(6, 58, 5, 1).trimmed());
228 if (dt.isValid() && dt < outboundDepartureTime()) {
229 dt = dt.addYears(1);
230 }
231 return dt;
232}
233
234static QString rct2Clean(const QString &s)
235{
236 // * is used to mark unset fields
237 if (std::all_of(s.begin(), s.end(), [](QChar c) { return c == QLatin1Char('*'); })) {
238 return {};
239 }
240 return s;
241}
242
243QString Rct2Ticket::outboundDepartureStation() const
244{
245 if (type() == RailPass) {
246 return {};
247 }
248
249 // 6, 13, 17, 1 would be according to spec, but why stick to that...
250 const auto fields = d->layout.containedFields(6, 13, 17, 1);
251 if (fields.size() == 1) {
252 return rct2Clean(fields[0].text().trimmed());
253 }
254 return rct2Clean(d->layout.text(6, 12, 18, 1).trimmed());
255}
256
257QString Rct2Ticket::outboundArrivalStation() const
258{
259 return type() != RailPass ? rct2Clean(d->layout.text(6, 34, 17, 1).trimmed()) : QString();
260}
261
262QString Rct2Ticket::outboundClass() const
263{
264 return rct2Clean(d->layout.text(6, 66, 5, 1).trimmed());
265}
266
267QDateTime Rct2Ticket::returnDepartureTime() const
268{
269 return d->parseTime(d->layout.text(7, 1, 5, 1).trimmed(), d->layout.text(7, 7, 5, 1).trimmed());
270}
271
272QDateTime Rct2Ticket::returnArrivalTime() const
273{
274 auto dt = d->parseTime(d->layout.text(7, 52, 5, 1).trimmed(), d->layout.text(7, 58, 5, 1).trimmed());
275 if (dt.isValid() && dt < returnDepartureTime()) {
276 dt = dt.addYears(1);
277 }
278 return dt;
279}
280
281QString Rct2Ticket::returnDepartureStation() const
282{
283 // 7, 13, 17, 1 would be according to spec, but you can guess by now how well that is followed...
284 return type() != RailPass ? rct2Clean(d->layout.text(7, 12, 18, 1).trimmed()) : QString();
285}
286
287QString Rct2Ticket::returnArrivalStation() const
288{
289 return type() != RailPass ? rct2Clean(d->layout.text(7, 34, 17, 1).trimmed()) : QString();
290}
291
292QString Rct2Ticket::returnClass() const
293{
294 return rct2Clean(d->layout.text(7, 66, 5, 1).trimmed());
295}
296
297QString Rct2Ticket::trainNumber() const
298{
299 const auto t = type();
300 if (t == Reservation || t == TransportReservation || t == Upgrade) {
301 auto num = d->reservationPatternCapture(u"train_number");
302 if (!num.isEmpty()) {
303 return d->reservationPatternCapture(u"train_category") + QLatin1Char(' ') + num;
304 }
305
306 const auto cat = d->layout.text(8, 13, 3, 1).trimmed();
307 num = d->layout.text(8, 7, 5, 1).trimmed();
308
309 // check for train number bleeding into our left neighbour field (happens e.g. on ÖBB IRT/RES tickets)
310 if (num.isEmpty() || num.at(0).isDigit()) {
311 const auto numPrefix = d->layout.text(8, 1, 6, 1);
312 for (int i = numPrefix.size() - 1; i >= 0; --i) {
313 if (numPrefix.at(i).isDigit()) {
314 num.prepend(numPrefix.at(i));
315 } else {
316 break;
317 }
318 }
319 }
320 num = num.trimmed();
321
322 if (!cat.isEmpty()) {
323 return cat + QLatin1Char(' ') + num;
324 }
325 return num;
326 }
327 return {};
328}
329
330QString Rct2Ticket::coachNumber() const
331{
332 const auto t = type();
333 if (t == Reservation || t == TransportReservation) {
334 const auto coach = d->reservationPatternCapture(u"coach");
335 return coach.isEmpty() ? d->layout.text(8, 26, 3, 1).trimmed() : coach;
336 }
337 return {};
338}
339
340QString Rct2Ticket::seatNumber() const
341{
342 const auto t = type();
343 if (t == Reservation || t == TransportReservation) {
344 const auto seat = d->reservationPatternCapture(u"seat");
345 if (!seat.isEmpty()) {
346 return seat;
347 }
348
349 const auto row8 = d->layout.text(8, 48, 23, 1).trimmed();
350 if (!row8.isEmpty()) {
351 return row8;
352 }
353 // rows 9/10 can contain seating details, let's use those as fallback if we don't find a number in the right field
354 return d->layout.text(9, 32, 19, 2).simplified();
355 }
356 return {};
357}
358
359QString Rct2Ticket::currency() const
360{
361 std::vector<PriceFinder::Result> result;
362 PriceFinder finder;
363 finder.findAll(d->layout.text(13, 52, 19, 1).remove(QLatin1Char('*')), result);
364 return result.size() == 1 ? result[0].currency : QString();
365}
366
367double Rct2Ticket::price() const
368{
369 std::vector<PriceFinder::Result> result;
370 PriceFinder finder;
371 finder.findAll(d->layout.text(13, 52, 19, 1).remove(QLatin1Char('*')), result);
372 return result.size() == 1 ? result[0].value : NAN;
373}
374
375#include "moc_rct2ticket.cpp"
376
RCT2 ticket layout payload of an UIC 918.3 ticket token.
Definition rct2ticket.h:23
Type
Type of RCT2 ticket.
Definition rct2ticket.h:69
@ Transport
Non-integrated Reservation Ticket (NRT)
Definition rct2ticket.h:70
@ RailPass
Rail Pass Ticket (RPT)
Definition rct2ticket.h:74
@ Upgrade
Update Document (UPG)
Definition rct2ticket.h:73
@ TransportReservation
Integration Reservation Ticket (IRT)
Definition rct2ticket.h:71
@ Unknown
ticket type could not be detected, or ticket type not supported yet
Definition rct2ticket.h:75
@ Reservation
Reservation Only Document (RES)
Definition rct2ticket.h:72
void setContextDate(const QDateTime &contextDt)
Date/time this ticket was first encountered, to recover possibly missing year numbers.
bool isValid() const
Returns whether this is a valid RCT2 ticket layout block.
Abstract base class for reservations.
Definition reservation.h:25
Parser for a U_TLAY block in a UIC 918-3 ticket container, such as a ERA TLB ticket.
Q_INVOKABLE QString text(int row, int column, int width, int height) const
Returns the text in the given coordinates.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime addYears(int nyears) const const
QDate date() const const
iterator begin()
iterator end()
qsizetype size() const const
QStringView mid(qsizetype start, qsizetype length) const const
QTime fromString(QStringView string, QStringView format)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:49 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.