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

KDE's Doxygen guidelines are available online.