aboutsummaryrefslogtreecommitdiffstats
path: root/lib/libtmi8/src
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2024-05-02 20:27:40 +0200
committerLibravatar Rutger Broekhoff2024-05-02 20:27:40 +0200
commit17a3ea880402338420699e03bcb24181e4ff3924 (patch)
treeda666ef91e0b60d20aa0b01529644c136fd1f4ab /lib/libtmi8/src
downloadoeuf-17a3ea880402338420699e03bcb24181e4ff3924.tar.gz
oeuf-17a3ea880402338420699e03bcb24181e4ff3924.zip
Initial commit
Based on dc4ba6a
Diffstat (limited to 'lib/libtmi8/src')
-rw-r--r--lib/libtmi8/src/kv1_index.cpp461
-rw-r--r--lib/libtmi8/src/kv1_lexer.cpp152
-rw-r--r--lib/libtmi8/src/kv1_parser.cpp1258
-rw-r--r--lib/libtmi8/src/kv1_types.cpp773
-rw-r--r--lib/libtmi8/src/kv6_parquet.cpp102
5 files changed, 2746 insertions, 0 deletions
diff --git a/lib/libtmi8/src/kv1_index.cpp b/lib/libtmi8/src/kv1_index.cpp
new file mode 100644
index 0000000..23e9596
--- /dev/null
+++ b/lib/libtmi8/src/kv1_index.cpp
@@ -0,0 +1,461 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_index.hpp>
4
5Kv1Index::Kv1Index(Kv1Records *records) : records(records) {
6 organizational_units.reserve(records->organizational_units.size());
7 for (size_t i = 0; i < records->organizational_units.size(); i++) {
8 auto *it = &records->organizational_units[i];
9 organizational_units[it->key] = it;
10 }
11 higher_organizational_units.reserve(records->higher_organizational_units.size());
12 for (size_t i = 0; i < records->higher_organizational_units.size(); i++) {
13 auto *it = &records->higher_organizational_units[i];
14 higher_organizational_units[it->key] = it;
15 }
16 user_stop_points.reserve(records->user_stop_points.size());
17 for (size_t i = 0; i < records->user_stop_points.size(); i++) {
18 auto *it = &records->user_stop_points[i];
19 user_stop_points[it->key] = it;
20 }
21 user_stop_areas.reserve(records->user_stop_areas.size());
22 for (size_t i = 0; i < records->user_stop_areas.size(); i++) {
23 auto *it = &records->user_stop_areas[i];
24 user_stop_areas[it->key] = it;
25 }
26 timing_links.reserve(records->timing_links.size());
27 for (size_t i = 0; i < records->timing_links.size(); i++) {
28 auto *it = &records->timing_links[i];
29 timing_links[it->key] = it;
30 }
31 links.reserve(records->links.size());
32 for (size_t i = 0; i < records->links.size(); i++) {
33 auto *it = &records->links[i];
34 links[it->key] = it;
35 }
36 lines.reserve(records->lines.size());
37 for (size_t i = 0; i < records->lines.size(); i++) {
38 auto *it = &records->lines[i];
39 lines[it->key] = it;
40 }
41 destinations.reserve(records->destinations.size());
42 for (size_t i = 0; i < records->destinations.size(); i++) {
43 auto *it = &records->destinations[i];
44 destinations[it->key] = it;
45 }
46 journey_patterns.reserve(records->journey_patterns.size());
47 for (size_t i = 0; i < records->journey_patterns.size(); i++) {
48 auto *it = &records->journey_patterns[i];
49 journey_patterns[it->key] = it;
50 }
51 concession_financer_relations.reserve(records->concession_financer_relations.size());
52 for (size_t i = 0; i < records->concession_financer_relations.size(); i++) {
53 auto *it = &records->concession_financer_relations[i];
54 concession_financer_relations[it->key] = it;
55 }
56 concession_areas.reserve(records->concession_areas.size());
57 for (size_t i = 0; i < records->concession_areas.size(); i++) {
58 auto *it = &records->concession_areas[i];
59 concession_areas[it->key] = it;
60 }
61 financers.reserve(records->financers.size());
62 for (size_t i = 0; i < records->financers.size(); i++) {
63 auto *it = &records->financers[i];
64 financers[it->key] = it;
65 }
66 journey_pattern_timing_links.reserve(records->journey_pattern_timing_links.size());
67 for (size_t i = 0; i < records->journey_pattern_timing_links.size(); i++) {
68 auto *it = &records->journey_pattern_timing_links[i];
69 journey_pattern_timing_links[it->key] = it;
70 }
71 points.reserve(records->points.size());
72 for (size_t i = 0; i < records->points.size(); i++) {
73 auto *it = &records->points[i];
74 points[it->key] = it;
75 }
76 point_on_links.reserve(records->point_on_links.size());
77 for (size_t i = 0; i < records->point_on_links.size(); i++) {
78 auto *it = &records->point_on_links[i];
79 point_on_links[it->key] = it;
80 }
81 icons.reserve(records->icons.size());
82 for (size_t i = 0; i < records->icons.size(); i++) {
83 auto *it = &records->icons[i];
84 icons[it->key] = it;
85 }
86 notices.reserve(records->notices.size());
87 for (size_t i = 0; i < records->notices.size(); i++) {
88 auto *it = &records->notices[i];
89 notices[it->key] = it;
90 }
91 time_demand_groups.reserve(records->time_demand_groups.size());
92 for (size_t i = 0; i < records->time_demand_groups.size(); i++) {
93 auto *it = &records->time_demand_groups[i];
94 time_demand_groups[it->key] = it;
95 }
96 time_demand_group_run_times.reserve(records->time_demand_group_run_times.size());
97 for (size_t i = 0; i < records->time_demand_group_run_times.size(); i++) {
98 auto *it = &records->time_demand_group_run_times[i];
99 time_demand_group_run_times[it->key] = it;
100 }
101 period_groups.reserve(records->period_groups.size());
102 for (size_t i = 0; i < records->period_groups.size(); i++) {
103 auto *it = &records->period_groups[i];
104 period_groups[it->key] = it;
105 }
106 specific_days.reserve(records->specific_days.size());
107 for (size_t i = 0; i < records->specific_days.size(); i++) {
108 auto *it = &records->specific_days[i];
109 specific_days[it->key] = it;
110 }
111 timetable_versions.reserve(records->timetable_versions.size());
112 for (size_t i = 0; i < records->timetable_versions.size(); i++) {
113 auto *it = &records->timetable_versions[i];
114 timetable_versions[it->key] = it;
115 }
116 public_journeys.reserve(records->public_journeys.size());
117 for (size_t i = 0; i < records->public_journeys.size(); i++) {
118 auto *it = &records->public_journeys[i];
119 public_journeys[it->key] = it;
120 }
121 period_group_validities.reserve(records->period_group_validities.size());
122 for (size_t i = 0; i < records->period_group_validities.size(); i++) {
123 auto *it = &records->period_group_validities[i];
124 period_group_validities[it->key] = it;
125 }
126 exceptional_operating_days.reserve(records->exceptional_operating_days.size());
127 for (size_t i = 0; i < records->exceptional_operating_days.size(); i++) {
128 auto *it = &records->exceptional_operating_days[i];
129 exceptional_operating_days[it->key] = it;
130 }
131 schedule_versions.reserve(records->schedule_versions.size());
132 for (size_t i = 0; i < records->schedule_versions.size(); i++) {
133 auto *it = &records->schedule_versions[i];
134 schedule_versions[it->key] = it;
135 }
136 public_journey_passing_times.reserve(records->public_journey_passing_times.size());
137 for (size_t i = 0; i < records->public_journey_passing_times.size(); i++) {
138 auto *it = &records->public_journey_passing_times[i];
139 public_journey_passing_times[it->key] = it;
140 }
141 operating_days.reserve(records->operating_days.size());
142 for (size_t i = 0; i < records->operating_days.size(); i++) {
143 auto *it = &records->operating_days[i];
144 operating_days[it->key] = it;
145 }
146}
147
148size_t Kv1Index::size() const {
149 return organizational_units.size()
150 + higher_organizational_units.size()
151 + user_stop_points.size()
152 + user_stop_areas.size()
153 + timing_links.size()
154 + links.size()
155 + lines.size()
156 + destinations.size()
157 + journey_patterns.size()
158 + concession_financer_relations.size()
159 + concession_areas.size()
160 + financers.size()
161 + journey_pattern_timing_links.size()
162 + points.size()
163 + point_on_links.size()
164 + icons.size()
165 + notices.size()
166 + time_demand_groups.size()
167 + time_demand_group_run_times.size()
168 + period_groups.size()
169 + specific_days.size()
170 + timetable_versions.size()
171 + public_journeys.size()
172 + period_group_validities.size()
173 + exceptional_operating_days.size()
174 + schedule_versions.size()
175 + public_journey_passing_times.size()
176 + operating_days.size();
177}
178
179void kv1LinkRecords(Kv1Index &index) {
180 for (auto &orunorun : index.records->higher_organizational_units) {
181 Kv1OrganizationalUnit::Key orun_parent_key(
182 orunorun.key.data_owner_code,
183 orunorun.key.organizational_unit_code_parent);
184 Kv1OrganizationalUnit::Key orun_child_key(
185 orunorun.key.data_owner_code,
186 orunorun.key.organizational_unit_code_child);
187 orunorun.p_organizational_unit_parent = index.organizational_units[orun_parent_key];
188 orunorun.p_organizational_unit_child = index.organizational_units[orun_child_key];
189 }
190 for (auto &usrstop : index.records->user_stop_points) {
191 Kv1Point::Key point_key(
192 usrstop.key.data_owner_code,
193 usrstop.key.user_stop_code);
194 usrstop.p_point = index.points[point_key];
195 if (!usrstop.user_stop_area_code.empty()) {
196 Kv1UserStopArea::Key usrstar_key(
197 usrstop.key.data_owner_code,
198 usrstop.user_stop_area_code);
199 usrstop.p_user_stop_area = index.user_stop_areas[usrstar_key];
200 }
201 }
202 for (auto &tili : index.records->timing_links) {
203 Kv1UserStopPoint::Key usrstop_begin_key(
204 tili.key.data_owner_code,
205 tili.key.user_stop_code_begin);
206 Kv1UserStopPoint::Key usrstop_end_key(
207 tili.key.data_owner_code,
208 tili.key.user_stop_code_end);
209 tili.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
210 tili.p_user_stop_end = index.user_stop_points[usrstop_end_key];
211 }
212 for (auto &link : index.records->links) {
213 Kv1UserStopPoint::Key usrstop_begin_key(
214 link.key.data_owner_code,
215 link.key.user_stop_code_begin);
216 Kv1UserStopPoint::Key usrstop_end_key(
217 link.key.data_owner_code,
218 link.key.user_stop_code_end);
219 link.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
220 link.p_user_stop_end = index.user_stop_points[usrstop_end_key];
221 }
222 for (auto &line : index.records->lines) {
223 if (!line.line_icon)
224 continue;
225 Kv1Icon::Key icon_key(
226 line.key.data_owner_code,
227 *line.line_icon);
228 line.p_line_icon = index.icons[icon_key];
229 }
230 for (auto &jopa : index.records->journey_patterns) {
231 Kv1Line::Key line_key(
232 jopa.key.data_owner_code,
233 jopa.key.line_planning_number);
234 jopa.p_line = index.lines[line_key];
235 }
236 for (auto &confinrel : index.records->concession_financer_relations) {
237 Kv1ConcessionArea::Key conarea_key(
238 confinrel.key.data_owner_code,
239 confinrel.concession_area_code);
240 confinrel.p_concession_area = index.concession_areas[conarea_key];
241 if (!confinrel.financer_code.empty()) {
242 Kv1Financer::Key financer_key(
243 confinrel.key.data_owner_code,
244 confinrel.financer_code);
245 confinrel.p_financer = index.financers[financer_key];
246 }
247 }
248 for (auto &jopatili : index.records->journey_pattern_timing_links) {
249 Kv1Line::Key line_key(
250 jopatili.key.data_owner_code,
251 jopatili.key.line_planning_number);
252 Kv1JourneyPattern::Key jopa_key(
253 jopatili.key.data_owner_code,
254 jopatili.key.line_planning_number,
255 jopatili.key.journey_pattern_code);
256 Kv1UserStopPoint::Key usrstop_begin_key(
257 jopatili.key.data_owner_code,
258 jopatili.user_stop_code_begin);
259 Kv1UserStopPoint::Key usrstop_end_key(
260 jopatili.key.data_owner_code,
261 jopatili.user_stop_code_end);
262 Kv1ConcessionFinancerRelation::Key confinrel_key(
263 jopatili.key.data_owner_code,
264 jopatili.con_fin_rel_code);
265 Kv1Destination::Key dest_key(
266 jopatili.key.data_owner_code,
267 jopatili.dest_code);
268 jopatili.p_line = index.lines[line_key];
269 jopatili.p_journey_pattern = index.journey_patterns[jopa_key];
270 jopatili.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
271 jopatili.p_user_stop_end = index.user_stop_points[usrstop_end_key];
272 jopatili.p_con_fin_rel = index.concession_financer_relations[confinrel_key];
273 jopatili.p_dest = index.destinations[dest_key];
274 if (jopatili.line_dest_icon) {
275 Kv1Icon::Key icon_key{
276 jopatili.key.data_owner_code,
277 *jopatili.line_dest_icon,
278 };
279 jopatili.p_line_dest_icon = index.icons[icon_key];
280 }
281 }
282 for (auto &pool : index.records->point_on_links) {
283 Kv1UserStopPoint::Key usrstop_begin_key(
284 pool.key.data_owner_code,
285 pool.key.user_stop_code_begin);
286 Kv1UserStopPoint::Key usrstop_end_key(
287 pool.key.data_owner_code,
288 pool.key.user_stop_code_end);
289 Kv1Point::Key point_key(
290 pool.key.point_data_owner_code,
291 pool.key.point_code);
292 pool.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
293 pool.p_user_stop_end = index.user_stop_points[usrstop_end_key];
294 pool.p_point = index.points[point_key];
295 }
296 for (auto &ntcassgnm : index.records->notice_assignments) {
297 Kv1Notice::Key notice_key(
298 ntcassgnm.data_owner_code,
299 ntcassgnm.notice_code);
300 ntcassgnm.p_notice = index.notices[notice_key];
301 }
302 for (auto &timdemgrp : index.records->time_demand_groups) {
303 Kv1Line::Key line_key(
304 timdemgrp.key.data_owner_code,
305 timdemgrp.key.line_planning_number);
306 Kv1JourneyPattern::Key jopa_key(
307 timdemgrp.key.data_owner_code,
308 timdemgrp.key.line_planning_number,
309 timdemgrp.key.journey_pattern_code);
310 timdemgrp.p_line = index.lines[line_key];
311 timdemgrp.p_journey_pattern = index.journey_patterns[jopa_key];
312 }
313 for (auto &timdemrnt : index.records->time_demand_group_run_times) {
314 Kv1Line::Key line_key(
315 timdemrnt.key.data_owner_code,
316 timdemrnt.key.line_planning_number);
317 Kv1JourneyPattern::Key jopa_key(
318 timdemrnt.key.data_owner_code,
319 timdemrnt.key.line_planning_number,
320 timdemrnt.key.journey_pattern_code);
321 Kv1TimeDemandGroup::Key timdemgrp_key(
322 timdemrnt.key.data_owner_code,
323 timdemrnt.key.line_planning_number,
324 timdemrnt.key.journey_pattern_code,
325 timdemrnt.key.time_demand_group_code);
326 Kv1UserStopPoint::Key usrstop_begin_key(
327 timdemrnt.key.data_owner_code,
328 timdemrnt.user_stop_code_begin);
329 Kv1UserStopPoint::Key usrstop_end_key(
330 timdemrnt.key.data_owner_code,
331 timdemrnt.user_stop_code_end);
332 Kv1JourneyPatternTimingLink::Key jopatili_key(
333 timdemrnt.key.data_owner_code,
334 timdemrnt.key.line_planning_number,
335 timdemrnt.key.journey_pattern_code,
336 timdemrnt.key.timing_link_order);
337 timdemrnt.p_line = index.lines[line_key];
338 timdemrnt.p_user_stop_end = index.user_stop_points[usrstop_end_key];
339 timdemrnt.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
340 timdemrnt.p_journey_pattern = index.journey_patterns[jopa_key];
341 timdemrnt.p_time_demand_group = index.time_demand_groups[timdemgrp_key];
342 timdemrnt.p_journey_pattern_timing_link = index.journey_pattern_timing_links[jopatili_key];
343 }
344 for (auto &tive : index.records->timetable_versions) {
345 Kv1OrganizationalUnit::Key orun_key(
346 tive.key.data_owner_code,
347 tive.key.organizational_unit_code);
348 Kv1PeriodGroup::Key pegr_key(
349 tive.key.data_owner_code,
350 tive.key.period_group_code);
351 Kv1SpecificDay::Key specday_key(
352 tive.key.data_owner_code,
353 tive.key.specific_day_code);
354 tive.p_organizational_unit = index.organizational_units[orun_key];
355 tive.p_period_group = index.period_groups[pegr_key];
356 tive.p_specific_day = index.specific_days[specday_key];
357 }
358 for (auto &pujo : index.records->public_journeys) {
359 Kv1TimetableVersion::Key tive_key(
360 pujo.key.data_owner_code,
361 pujo.key.organizational_unit_code,
362 pujo.key.timetable_version_code,
363 pujo.key.period_group_code,
364 pujo.key.specific_day_code);
365 Kv1OrganizationalUnit::Key orun_key(
366 pujo.key.data_owner_code,
367 pujo.key.organizational_unit_code);
368 Kv1PeriodGroup::Key pegr_key(
369 pujo.key.data_owner_code,
370 pujo.key.period_group_code);
371 Kv1SpecificDay::Key specday_key(
372 pujo.key.data_owner_code,
373 pujo.key.specific_day_code);
374 Kv1Line::Key line_key(
375 pujo.key.data_owner_code,
376 pujo.key.line_planning_number);
377 Kv1TimeDemandGroup::Key timdemgrp_key(
378 pujo.key.data_owner_code,
379 pujo.key.line_planning_number,
380 pujo.journey_pattern_code,
381 pujo.time_demand_group_code);
382 Kv1JourneyPattern::Key jopa_key(
383 pujo.key.data_owner_code,
384 pujo.key.line_planning_number,
385 pujo.journey_pattern_code);
386 pujo.p_timetable_version = index.timetable_versions[tive_key];
387 pujo.p_organizational_unit = index.organizational_units[orun_key];
388 pujo.p_period_group = index.period_groups[pegr_key];
389 pujo.p_specific_day = index.specific_days[specday_key];
390 pujo.p_line = index.lines[line_key];
391 pujo.p_time_demand_group = index.time_demand_groups[timdemgrp_key];
392 pujo.p_journey_pattern = index.journey_patterns[jopa_key];
393 }
394 for (auto &pegrval : index.records->period_group_validities) {
395 Kv1OrganizationalUnit::Key orun_key(
396 pegrval.key.data_owner_code,
397 pegrval.key.organizational_unit_code);
398 Kv1PeriodGroup::Key pegr_key(
399 pegrval.key.data_owner_code,
400 pegrval.key.period_group_code);
401 pegrval.p_organizational_unit = index.organizational_units[orun_key];
402 pegrval.p_period_group = index.period_groups[pegr_key];
403 }
404 for (auto &excopday : index.records->exceptional_operating_days) {
405 Kv1OrganizationalUnit::Key orun_key(
406 excopday.key.data_owner_code,
407 excopday.key.organizational_unit_code);
408 Kv1SpecificDay::Key specday_key(
409 excopday.key.data_owner_code,
410 excopday.specific_day_code);
411 Kv1PeriodGroup::Key pegr_key(
412 excopday.key.data_owner_code,
413 excopday.period_group_code);
414 excopday.p_organizational_unit = index.organizational_units[orun_key];
415 excopday.p_specific_day = index.specific_days[specday_key];
416 excopday.p_period_group = index.period_groups[pegr_key];
417 }
418 for (auto &schedvers : index.records->schedule_versions) {
419 Kv1OrganizationalUnit::Key orun_key(
420 schedvers.key.data_owner_code,
421 schedvers.key.organizational_unit_code);
422 schedvers.p_organizational_unit = index.organizational_units[orun_key];
423 }
424 for (auto &pujopass : index.records->public_journey_passing_times) {
425 Kv1OrganizationalUnit::Key orun_key(
426 pujopass.key.data_owner_code,
427 pujopass.key.organizational_unit_code);
428 Kv1ScheduleVersion::Key schedvers_key(
429 pujopass.key.data_owner_code,
430 pujopass.key.organizational_unit_code,
431 pujopass.key.schedule_code,
432 pujopass.key.schedule_type_code);
433 Kv1Line::Key line_key(
434 pujopass.key.data_owner_code,
435 pujopass.key.line_planning_number);
436 Kv1JourneyPattern::Key jopa_key(
437 pujopass.key.data_owner_code,
438 pujopass.key.line_planning_number,
439 pujopass.journey_pattern_code);
440 Kv1UserStopPoint::Key usrstop_key(
441 pujopass.key.data_owner_code,
442 pujopass.user_stop_code);
443 pujopass.p_organizational_unit = index.organizational_units[orun_key];
444 pujopass.p_schedule_version = index.schedule_versions[schedvers_key];
445 pujopass.p_line = index.lines[line_key];
446 pujopass.p_journey_pattern = index.journey_patterns[jopa_key];
447 pujopass.p_user_stop = index.user_stop_points[usrstop_key];
448 }
449 for (auto &operday : index.records->operating_days) {
450 Kv1OrganizationalUnit::Key orun_key(
451 operday.key.data_owner_code,
452 operday.key.organizational_unit_code);
453 Kv1ScheduleVersion::Key schedvers_key(
454 operday.key.data_owner_code,
455 operday.key.organizational_unit_code,
456 operday.key.schedule_code,
457 operday.key.schedule_type_code);
458 operday.p_organizational_unit = index.organizational_units[orun_key];
459 operday.p_schedule_version = index.schedule_versions[schedvers_key];
460 }
461}
diff --git a/lib/libtmi8/src/kv1_lexer.cpp b/lib/libtmi8/src/kv1_lexer.cpp
new file mode 100644
index 0000000..028127b
--- /dev/null
+++ b/lib/libtmi8/src/kv1_lexer.cpp
@@ -0,0 +1,152 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_lexer.hpp>
4
5Kv1Lexer::Kv1Lexer(std::string_view input)
6 : input(input), slice(input)
7{}
8
9// Does not eat newline character.
10void Kv1Lexer::eatRestOfLine() {
11 size_t end = slice.size();
12 for (size_t i = 0; i < slice.size(); i++) {
13 if (slice[i] == '\r' || slice[i] == '\n') {
14 end = i;
15 break;
16 }
17 }
18 slice = slice.substr(end);
19}
20
21void Kv1Lexer::lexOptionalHeader() {
22 if (slice.starts_with('[')) eatRestOfLine();
23}
24
25void Kv1Lexer::lexOptionalComment() {
26 if (slice.starts_with(';')) eatRestOfLine();
27}
28
29inline bool Kv1Lexer::isWhitespace(int c) {
30 return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\v';
31}
32
33void Kv1Lexer::readQuotedColumn() {
34 Kv1Token token{ .type = KV1_TOKEN_CELL };
35
36 if (slice.size() == 0 || slice[0] != '"') {
37 errors.push_back("(internal error) readQuotedColumn: slice[0] != '\"'");
38 return;
39 }
40 slice = slice.substr(1);
41 while (true) {
42 size_t quote = slice.find('"');
43 if (quote == std::string_view::npos) {
44 errors.push_back("readQuotedColumn: no matching closing quote found");
45 return;
46 }
47 if (quote+1 == slice.size() || slice[quote + 1] != '"') {
48 token.data.append(slice.substr(0, quote));
49 break;
50 }
51 token.data.append(slice.substr(0, quote + 1));
52 slice = slice.substr(quote + 2);
53 }
54
55 size_t end = slice.size();
56 for (size_t i = 0; i < slice.size(); i++) {
57 if (slice[i] == '|' || slice[i] == '\r' || slice[i] == '\n') {
58 end = i;
59 break;
60 }
61 if (!isWhitespace(slice[i])) {
62 errors.push_back("readQuotedColumn: encountered non-whitespace character after closing quote");
63 return;
64 }
65 }
66 if (end != std::string_view::npos) slice = slice.substr(end);
67 else slice = slice.substr(slice.size());
68
69 tokens.push_back(std::move(token));
70}
71
72void Kv1Lexer::readUnquotedColumn() {
73 size_t end = slice.size();
74 size_t content_end = 0;
75 for (size_t i = 0; i < slice.size(); i++) {
76 if (slice[i] == '|' || slice[i] == '\r' || slice[i] == '\n') {
77 end = i;
78 break;
79 } else if (!isWhitespace(slice[i])) {
80 content_end = i + 1;
81 }
82 }
83 tokens.emplace_back(KV1_TOKEN_CELL, std::string(slice.substr(0, content_end)));
84 if (end != std::string_view::npos) slice = slice.substr(end);
85 else slice = slice.substr(slice.size());
86}
87
88void Kv1Lexer::lexRow() {
89 size_t cols = 0;
90 while (slice.size() > 0 && slice[0] != '\r' && slice[0] != '\n') {
91 if (slice[0] == '"') readQuotedColumn();
92 else readUnquotedColumn();
93 if (!errors.empty()) return;
94 cols++;
95 if (slice.size() != 0) {
96 if (slice[0] == '|') {
97 slice = slice.substr(1);
98 // A newline/eof right after pipe? That means an empty field at the end
99 // of the record, we also want to emit that as a token.
100 if (slice.size() == 0 || slice[0] == '\r' || slice[0] == '\n') {
101 tokens.push_back({ .type = KV1_TOKEN_CELL });
102 }
103 } else if (slice[0] == '\r') {
104 if (slice.size() > 1 && slice[1] == '\n') slice = slice.substr(2);
105 else slice = slice.substr(1);
106 break;
107 } else if (slice[0] == '\n') {
108 slice = slice.substr(1);
109 break;
110 } else {
111 errors.push_back("lexRow: expected CR, LF or |");
112 return;
113 }
114 }
115 }
116 tokens.push_back({ .type = KV1_TOKEN_ROW_END });
117}
118
119// Returns true when a line ending was consumed.
120bool Kv1Lexer::eatWhitespace() {
121 for (size_t i = 0; i < slice.size(); i++) {
122 if (slice[i] == '\r') {
123 slice = slice.substr(i + 1);
124 if (slice.size() > 1 && slice[i + 1] == '\n')
125 slice = slice.substr(i + 2);
126 return true;
127 }
128 if (slice[i] == '\n') {
129 slice = slice.substr(i + 1);
130 return true;
131 }
132
133 if (slice[i] != ' ' && slice[i] != '\f' && slice[i] != '\t' && slice[i] != '\v') {
134 slice = slice.substr(i);
135 return false;
136 }
137 }
138 return false;
139}
140
141void Kv1Lexer::lex() {
142 lexOptionalHeader();
143 eatWhitespace();
144
145 while (errors.empty() && !slice.empty()) {
146 lexOptionalComment();
147 bool newline = eatWhitespace();
148 if (newline) continue;
149 // We are now either (1) at the end of the file or (2) at the start of some column data
150 if (errors.empty()) lexRow();
151 }
152}
diff --git a/lib/libtmi8/src/kv1_parser.cpp b/lib/libtmi8/src/kv1_parser.cpp
new file mode 100644
index 0000000..ac0c6bf
--- /dev/null
+++ b/lib/libtmi8/src/kv1_parser.cpp
@@ -0,0 +1,1258 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_parser.hpp>
4
5using rune = uint32_t;
6
7static size_t decodeUtf8Cp(std::string_view s, rune *dest = nullptr) {
8 rune res = 0xFFFD;
9 size_t length = 1;
10
11 if (s.size() == 0)
12 return 0;
13 const uint8_t *b = reinterpret_cast<const uint8_t *>(s.data());
14 if (!(b[0] & 0x80))
15 res = static_cast<rune>(b[0]);
16 else if ((b[0] & 0xE0) == 0xC0) {
17 length = 2;
18 if (s.size() >= 2 && (b[1] & 0xC0) == 0x80) {
19 res = static_cast<rune>(b[0] & ~0xC0) << 6;
20 res |= static_cast<rune>(b[1] & ~0x80);
21 }
22 } else if ((b[0] & 0xF0) == 0xE0) {
23 length = 3;
24 if (s.size() >= 3 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80) {
25 res = static_cast<rune>(b[0] & ~0xE0) << 12;
26 res |= static_cast<rune>(b[1] & ~0x80) << 6;
27 res |= static_cast<rune>(b[2] & ~0x80);
28 }
29 } else if (b[0] == 0xF0) {
30 length = 4;
31 if (s.size() >= 4 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80) {
32 res = static_cast<rune>(b[0] & ~0xF0) << 18;
33 res |= static_cast<rune>(b[1] & ~0x80) << 12;
34 res |= static_cast<rune>(b[2] & ~0x80) << 6;
35 res |= static_cast<rune>(b[3] & ~0x80);
36 }
37 }
38
39 if (dest)
40 *dest = res;
41 return length;
42}
43
44// Counts the number of codepoints in a valid UTF-8 string. Returns SIZE_MAX if
45// the string contains invalid UTF-8 codepoints.
46static size_t stringViewLengthUtf8(std::string_view sv) {
47 size_t codepoints = 0;
48 while (sv.size() > 0) {
49 size_t codepoint_size = decodeUtf8Cp(sv);
50 if (codepoint_size == 0) return SIZE_MAX;
51 codepoints++;
52 sv = sv.substr(codepoint_size);
53 }
54 return codepoints;
55}
56
57Kv1Parser::Kv1Parser(std::vector<Kv1Token> tokens, Kv1Records &parse_into)
58 : tokens(std::move(tokens)),
59 records(parse_into)
60{}
61
62bool Kv1Parser::atEnd() const {
63 return pos >= tokens.size();
64}
65
66void Kv1Parser::eatRowEnds() {
67 while (!atEnd() && tokens[pos].type == KV1_TOKEN_ROW_END) pos++;
68}
69
70const Kv1Token *Kv1Parser::cur() const {
71 if (atEnd()) return nullptr;
72 return &tokens[pos];
73}
74
75const std::string *Kv1Parser::eatCell(std::string_view parsing_what) {
76 const Kv1Token *tok = cur();
77 if (!tok) {
78 record_errors.push_back(std::format("Expected cell but got end of file when parsing {}", parsing_what));
79 return nullptr;
80 }
81 if (tok->type == KV1_TOKEN_ROW_END) {
82 record_errors.push_back(std::format("Expected cell but got end of row when parsing {}", parsing_what));
83 return nullptr;
84 }
85 pos++;
86 return &tok->data;
87}
88
89void Kv1Parser::requireString(std::string_view field, bool mandatory, size_t max_length, std::string_view value) {
90 if (value.empty() && mandatory) {
91 record_errors.push_back(std::format("{} has length zero but is required", field));
92 return;
93 }
94 size_t codepoints = stringViewLengthUtf8(value);
95 if (codepoints == SIZE_MAX) {
96 global_errors.push_back(std::format("{} contains invalid UTF-8 code points", field));
97 return;
98 }
99 if (codepoints > max_length) {
100 record_errors.push_back(std::format("{} has length ({}) that is greater than maximum length ({})",
101 field, value.size(), max_length));
102 }
103}
104
105static inline std::optional<bool> parseBoolean(std::string_view src) {
106 if (src == "1") return true;
107 if (src == "0") return false;
108 if (src == "true") return true;
109 if (src == "false") return false;
110 return std::nullopt;
111}
112
113std::optional<bool> Kv1Parser::requireBoolean(std::string_view field, bool mandatory, std::string_view value) {
114 if (value.empty()) {
115 if (mandatory)
116 record_errors.push_back(std::format("{} is required, but has no value", field));
117 return std::nullopt;
118 }
119 auto parsed = parseBoolean(value);
120 if (!parsed.has_value())
121 record_errors.push_back(std::format("{} should have value \"1\", \"0\", \"true\" or \"false\"", field));
122 return parsed;
123}
124
125static inline size_t countDigits(long x) {
126 size_t digits = 0;
127 while (x != 0) { digits++; x /= 10; }
128 return digits;
129}
130
131std::optional<double> Kv1Parser::requireNumber(std::string_view field, bool mandatory, size_t max_digits, std::string_view value) {
132 if (value.empty()) {
133 if (mandatory)
134 record_errors.push_back(std::format("{} has no value but is required", field));
135 return std::nullopt;
136 }
137
138 double parsed;
139 auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed);
140 if (ec != std::errc()) {
141 record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field));
142 return std::nullopt;
143 }
144 if (ptr != value.data() + value.size()) {
145 record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field));
146 return std::nullopt;
147 }
148
149 size_t digits = countDigits(static_cast<long>(parsed));
150 if (digits > max_digits) {
151 record_errors.push_back(std::format("{} contains more digits (in the integral part) ({}) than allowed ({})",
152 field, digits, max_digits));
153 return std::nullopt;
154 }
155
156 return parsed;
157}
158
159static inline bool isHexDigit(char c) {
160 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');
161}
162
163static inline uint8_t fromHex(char c) {
164 if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
165 else if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
166 return 0;
167}
168
169static std::optional<RgbColor> parseRgbColor(std::string_view src) {
170 bool valid = src.size() == 6
171 && isHexDigit(src[0]) && isHexDigit(src[1])
172 && isHexDigit(src[2]) && isHexDigit(src[3])
173 && isHexDigit(src[4]) && isHexDigit(src[5]);
174 if (!valid) return std::nullopt;
175 uint8_t r = static_cast<uint8_t>(fromHex(src[0]) << 4) + fromHex(src[1]);
176 uint8_t g = static_cast<uint8_t>(fromHex(src[2]) << 4) + fromHex(src[3]);
177 uint8_t b = static_cast<uint8_t>(fromHex(src[4]) << 4) + fromHex(src[5]);
178 return RgbColor{ r, g, b };
179}
180
181std::optional<RgbColor> Kv1Parser::requireRgbColor(std::string_view field, bool mandatory, std::string_view value) {
182 if (value.empty()) {
183 if (mandatory)
184 record_errors.push_back(std::format("{} is required, but has no value", field));
185 return std::nullopt;
186 }
187 auto parsed = parseRgbColor(value);
188 if (!parsed.has_value())
189 record_errors.push_back(std::format("{} should be an RGB color, i.e. a sequence of six hexadecimally represented nibbles", field));
190 return parsed;
191}
192
193std::optional<double> Kv1Parser::requireRdCoord(std::string_view field, bool mandatory, size_t min_digits, std::string_view value) {
194 if (value.empty()) {
195 if (mandatory)
196 record_errors.push_back(std::format("{} is required, but has no value", field));
197 return std::nullopt;
198 }
199 if (value.size() > 15) {
200 record_errors.push_back(std::format("{} may not have more than 15 characters", field));
201 return std::nullopt;
202 }
203
204 double parsed;
205 auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed);
206 if (ec != std::errc()) {
207 record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field));
208 return std::nullopt;
209 }
210 if (ptr != value.data() + value.size()) {
211 record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field));
212 return std::nullopt;
213 }
214
215 size_t digits = countDigits(static_cast<long>(parsed));
216 if (digits < min_digits) {
217 record_errors.push_back(std::format("{} contains less digits (in the integral part) ({}) than required ({}) [value: {}]",
218 field, digits, min_digits, value));
219 return std::nullopt;
220 }
221
222 return parsed;
223}
224
225std::string Kv1Parser::eatString(std::string_view field, bool mandatory, size_t max_length) {
226 auto value = eatCell(field);
227 if (!record_errors.empty()) return {};
228 requireString(field, mandatory, max_length, *value);
229 return std::move(*value);
230}
231
232std::optional<bool> Kv1Parser::eatBoolean(std::string_view field, bool mandatory) {
233 auto value = eatCell(field);
234 if (!record_errors.empty()) return {};
235 return requireBoolean(field, mandatory, *value);
236}
237
238std::optional<double> Kv1Parser::eatNumber(std::string_view field, bool mandatory, size_t max_digits) {
239 auto value = eatCell(field);
240 if (!record_errors.empty()) return {};
241 return requireNumber(field, mandatory, max_digits, *value);
242}
243
244std::optional<RgbColor> Kv1Parser::eatRgbColor(std::string_view field, bool mandatory) {
245 auto value = eatCell(field);
246 if (!record_errors.empty()) return {};
247 return requireRgbColor(field, mandatory, *value);
248}
249
250std::optional<double> Kv1Parser::eatRdCoord(std::string_view field, bool mandatory, size_t min_digits) {
251 auto value = eatCell(field);
252 if (!record_errors.empty()) return {};
253 return requireRdCoord(field, mandatory, min_digits, *value);
254}
255
256std::string Kv1Parser::parseHeader() {
257 auto record_type = eatString("<header>.Recordtype", true, 10);
258 auto version_number = eatString("<header>.VersionNumber", true, 2);
259 auto implicit_explicit = eatString("<header>.Implicit/Explicit", true, 1);
260 if (!record_errors.empty()) return {};
261
262 if (version_number != "1") {
263 record_errors.push_back("<header>.VersionNumber should be 1");
264 return "";
265 }
266 if (implicit_explicit != "I") {
267 record_errors.push_back("<header>.Implicit/Explicit should be 'I'");
268 return "";
269 }
270
271 return record_type;
272}
273
274void Kv1Parser::eatRestOfRow() {
275 while (!atEnd() && cur()->type != KV1_TOKEN_ROW_END) pos++;
276}
277
278void Kv1Parser::parse() {
279 while (!atEnd()) {
280 eatRowEnds();
281 if (atEnd()) return;
282
283 std::string record_type = parseHeader();
284 if (!record_errors.empty()) break;
285 if (!type_parsers.contains(record_type)) {
286 warns.push_back(std::format("Recordtype ({}) is bad or names a record type that this program cannot process",
287 record_type));
288 eatRestOfRow();
289 continue;
290 }
291
292 ParseFunc parseType = Kv1Parser::type_parsers.at(record_type);
293 (this->*parseType)();
294 if (cur() && cur()->type != KV1_TOKEN_ROW_END) {
295 record_errors.push_back(std::format("Parser function for Recordtype ({}) did not eat all record fields",
296 record_type));
297 eatRestOfRow();
298 }
299 if (!record_errors.empty()) {
300 global_errors.insert(global_errors.end(), record_errors.begin(), record_errors.end());
301 record_errors.clear();
302 }
303 }
304}
305
306void Kv1Parser::parseOrganizationalUnit() {
307 auto data_owner_code = eatString("ORUN.DataOwnerCode", true, 10);
308 auto organizational_unit_code = eatString("ORUN.OrganizationalUnitCode", true, 10);
309 auto name = eatString("ORUN.Name", true, 50);
310 auto organizational_unit_type = eatString("ORUN.OrganizationalUnitType", true, 10);
311 auto description = eatString("ORUN.Description", false, 255);
312 if (!record_errors.empty()) return;
313
314 records.organizational_units.emplace_back(
315 Kv1OrganizationalUnit::Key(
316 data_owner_code,
317 organizational_unit_code),
318 name,
319 organizational_unit_type,
320 description);
321}
322
323static inline bool isDigit(char c) {
324 return c >= '0' && c <= '9';
325}
326
327// Parse a string of the format YYYY-MM-DD.
328static std::optional<std::chrono::year_month_day> parseYyyymmdd(std::string_view src) {
329 bool valid = src.size() == 10
330 && isDigit(src[0]) && isDigit(src[1])
331 && isDigit(src[2]) && isDigit(src[3]) && src[4] == '-'
332 && isDigit(src[5]) && isDigit(src[6]) && src[7] == '-'
333 && isDigit(src[8]) && isDigit(src[9]);
334 if (!valid) return std::nullopt;
335 int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0';
336 int month = (src[5] - '0') * 10 + src[6] - '0';
337 int day = (src[8] - '0') * 10 + src[9] - '0';
338 return std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day);
339}
340
341// Parse a string of the format HH:MM:SS.
342static std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> parseHhmmss(std::string_view src) {
343 bool valid = src.size() == 8
344 && isDigit(src[0]) && isDigit(src[1]) && src[2] == ':'
345 && isDigit(src[3]) && isDigit(src[4]) && src[5] == ':'
346 && isDigit(src[6]) && isDigit(src[7]);
347 if (!valid) return std::nullopt;
348 int hh = (src[0] - '0') * 10 + src[1] - '0';
349 int mm = (src[3] - '0') * 10 + src[4] - '0';
350 int ss = (src[6] - '0') * 10 + src[7] - '0';
351 // The check for the hour not being greater than 32 comes from the fact the
352 // specification explicitly allows hours greater than 23, noting that the
353 // period 24:00-32:00 is equivalent to 00:00-08:00 in the next day, for
354 // exploitation of two days.
355 if (hh > 32 || mm > 59 || ss > 59) return std::nullopt;
356 return std::chrono::hh_mm_ss(std::chrono::hours(hh) + std::chrono::minutes(mm) + std::chrono::seconds(ss));
357}
358
359static std::optional<std::chrono::sys_seconds> parseDateTime(std::string_view src, const std::chrono::time_zone *amsterdam, std::string_view *error = nullptr) {
360#define ERROR(err) do { if (error) *error = err; return std::nullopt; } while (0)
361 if (src.size() > 23) ERROR("timestamp string is too big");
362 if (src.size() < 17) ERROR("timestamp string is too small");
363
364 bool valid_year = isDigit(src[0]) && isDigit(src[1]) && isDigit(src[2]) && isDigit(src[3]);
365 if (!valid_year) ERROR("year has bad format");
366
367 size_t month_off = src[4] == '-' ? 5 : 4;
368 size_t day_off = src[month_off + 2] == '-' ? month_off + 3 : month_off + 2;
369 size_t time_off = day_off + 2;
370 if (src[time_off] != 'T' && src[time_off] != ' ')
371 ERROR("missing date/time separator");
372 size_t tzd_off = time_off + 9;
373 // For clarity, TZD stands for Time Zone Designator. It often takes the form
374 // of Z (Zulu, UTC+00:00) or as an offset from UTC in hours and minutes,
375 // formatted as +|-HH:MM (e.g. +01:00, -12:00).
376
377 if (time_off + 8 >= src.size()) ERROR("bad format, not enough space for hh:mm:ss");
378
379 int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0';
380 int month = (src[month_off] - '0') * 10 + src[month_off + 1] - '0';
381 int day = (src[day_off] - '0') * 10 + src[day_off + 1] - '0';
382 int hour = (src[time_off + 1] - '0') * 10 + src[time_off + 2] - '0';
383 int minute = (src[time_off + 4] - '0') * 10 + src[time_off + 5] - '0';
384 int second = (src[time_off + 7] - '0') * 10 + src[time_off + 8] - '0';
385
386 auto date = std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day);
387 auto time = std::chrono::hours(hour) + std::chrono::minutes(minute) + std::chrono::seconds(second);
388
389 std::chrono::sys_seconds unix_start_of_day;
390 if (tzd_off < src.size()) {
391 unix_start_of_day = std::chrono::sys_days(date);
392 } else {
393 auto local_days = std::chrono::local_days(date);
394 std::chrono::zoned_seconds zoned_start_of_day = std::chrono::zoned_time(amsterdam, local_days);
395 unix_start_of_day = std::chrono::sys_seconds(zoned_start_of_day);
396 }
397
398 std::chrono::minutes offset(0);
399 if (tzd_off + 1 == src.size() && src[tzd_off] != 'Z') {
400 ERROR("bad TZD (missing Zulu indicator)");
401 } else if (tzd_off + 6 == src.size()) {
402 bool valid_tzd = (src[tzd_off] == '+' || src[tzd_off] == '-')
403 && isDigit(src[tzd_off + 1]) && isDigit(src[tzd_off + 2]) && src[tzd_off + 3] == ':'
404 && isDigit(src[tzd_off + 4]) && isDigit(src[tzd_off + 5]);
405 if (!valid_tzd) ERROR("bad offset TZD format (expected +|-hh:mm)");
406 int sign = src[tzd_off] == '-' ? -1 : 1;
407 int tzd_hh = (src[tzd_off + 1] - '0') * 10 + src[tzd_off + 2] - '0';
408 int tzd_mm = (src[tzd_off + 3] - '0') * 10 + src[tzd_off + 4] - '0';
409 offset = sign * std::chrono::minutes(tzd_hh * 60 + tzd_mm);
410 } else if (tzd_off < src.size()) {
411 // There is a TZD but we literally have no clue how to parse it :/
412 ERROR("cannot parse TZD of unexpected length");
413 }
414
415 return unix_start_of_day + time - offset;
416#undef ERROR
417}
418
419void Kv1Parser::parseHigherOrganizationalUnit() {
420 auto data_owner_code = eatString("ORUNORUN.DataOwnerCode", true, 10);
421 auto organizational_unit_code_parent = eatString("ORUNORUN.OrganizationalUnitCodeParent", true, 10);
422 auto organizational_unit_code_child = eatString("ORUNORUN.OrganizationalUnitCodeChild", true, 10);
423 auto valid_from_raw = eatString("ORUNORUN.ValidFrom", true, 10);
424 if (!record_errors.empty()) return;
425
426 auto valid_from = parseYyyymmdd(valid_from_raw);
427 if (!valid_from) {
428 record_errors.push_back("ORUNORUN.ValidFrom has invalid format, should be YYYY-MM-DD");
429 return;
430 }
431
432 records.higher_organizational_units.emplace_back(
433 Kv1HigherOrganizationalUnit::Key(
434 data_owner_code,
435 organizational_unit_code_parent,
436 organizational_unit_code_child,
437 *valid_from));
438}
439
440void Kv1Parser::parseUserStopPoint() {
441 auto data_owner_code = eatString ("USRSTOP.DataOwnerCode", true, 10);
442 auto user_stop_code = eatString ("USRSTOP.UserStopCode", true, 10);
443 auto timing_point_code = eatString ("USRSTOP.TimingPointCode", false, 10);
444 auto get_in = eatBoolean("USRSTOP.GetIn", true );
445 auto get_out = eatBoolean("USRSTOP.GetOut", true );
446 eatCell ("USRSTOP.<deprecated field #1>" );
447 auto name = eatString ("USRSTOP.Name", true, 50);
448 auto town = eatString ("USRSTOP.Town", true, 50);
449 auto user_stop_area_code = eatString ("USRSTOP.UserStopAreaCode", false, 10);
450 auto stop_side_code = eatString ("USRSTOP.StopSideCode", true, 10);
451 eatCell ("USRSTOP.<deprecated field #2>" );
452 eatCell ("USRSTOP.<deprecated field #3>" );
453 auto minimal_stop_time = eatNumber ("USRSTOP.MinimalStopTime", true, 5);
454 auto stop_side_length = eatNumber ("USRSTOP.StopSideLength", false, 3);
455 auto description = eatString ("USRSTOP.Description", false, 255);
456 auto user_stop_type = eatString ("USRSTOP.UserStopType", true, 10);
457 auto quay_code = eatString ("USRSTOP.QuayCode", false, 30);
458 if (!record_errors.empty()) return;
459
460 records.user_stop_points.emplace_back(
461 Kv1UserStopPoint::Key(
462 data_owner_code,
463 user_stop_code),
464 timing_point_code,
465 *get_in,
466 *get_out,
467 name,
468 town,
469 user_stop_area_code,
470 stop_side_code,
471 *minimal_stop_time,
472 stop_side_length,
473 description,
474 user_stop_type,
475 quay_code);
476}
477
478void Kv1Parser::parseUserStopArea() {
479 auto data_owner_code = eatString("USRSTAR.DataOwnerCode", true, 10);
480 auto user_stop_area_code = eatString("USRSTAR.UserStopAreaCode", true, 10);
481 auto name = eatString("USRSTAR.Name", true, 50);
482 auto town = eatString("USRSTAR.Town", true, 50);
483 eatCell ("USRSTAR.<deprecated field #1>" );
484 eatCell ("USRSTAR.<deprecated field #2>" );
485 auto description = eatString("USRSTAR.Description", false, 255);
486 if (!record_errors.empty()) return;
487
488 records.user_stop_areas.emplace_back(
489 Kv1UserStopArea::Key(
490 data_owner_code,
491 user_stop_area_code),
492 name,
493 town,
494 description);
495}
496
497void Kv1Parser::parseTimingLink() {
498 auto data_owner_code = eatString("TILI.DataOwnerCode", true, 10);
499 auto user_stop_code_begin = eatString("TILI.UserStopCodeBegin", true, 10);
500 auto user_stop_code_end = eatString("TILI.UserStopCodeEnd", true, 10);
501 auto minimal_drive_time = eatNumber("TILI.MinimalDriveTime", false, 5);
502 auto description = eatString("TILI.Description", false, 255);
503 if (!record_errors.empty()) return;
504
505 records.timing_links.emplace_back(
506 Kv1TimingLink::Key(
507 data_owner_code,
508 user_stop_code_begin,
509 user_stop_code_end),
510 minimal_drive_time,
511 description);
512}
513
514void Kv1Parser::parseLink() {
515 auto data_owner_code = eatString("LINK.DataOwnerCode", true, 10);
516 auto user_stop_code_begin = eatString("LINK.UserStopCodeBegin", true, 10);
517 auto user_stop_code_end = eatString("LINK.UserStopCodeEnd", true, 10);
518 eatCell("LINK.<deprecated field #1>" );
519 auto distance = eatNumber("LINK.Distance", true, 6);
520 auto description = eatString("LINK.Description", false, 255);
521 auto transport_type = eatString("LINK.TransportType", true, 5);
522 if (!record_errors.empty()) return;
523
524 records.links.emplace_back(
525 Kv1Link::Key(
526 data_owner_code,
527 user_stop_code_begin,
528 user_stop_code_end,
529 transport_type),
530 *distance,
531 description);
532}
533
534void Kv1Parser::parseLine() {
535 auto data_owner_code = eatString ("LINE.DataOwnerCode", true, 10);
536 auto line_planning_number = eatString ("LINE.LinePlanningNumber", true, 10);
537 auto line_public_number = eatString ("LINE.LinePublicNumber", true, 4);
538 auto line_name = eatString ("LINE.LineName", true, 50);
539 auto line_ve_tag_number = eatNumber ("LINE.LineVeTagNumber", true, 3);
540 auto description = eatString ("LINE.Description", false, 255);
541 auto transport_type = eatString ("LINE.TransportType", true, 5);
542 auto line_icon = eatNumber ("LINE.LineIcon", false, 4);
543 auto line_color = eatRgbColor("LINE.LineColor", false );
544 auto line_text_color = eatRgbColor("LINE.LineTextColor", false );
545 if (!record_errors.empty()) return;
546
547 // NOTE: This check, although it should be performed to comply with the
548 // specification, is not actually honored by transit operators (such as
549 // Connexxion) :/ That's enough reason to keep it disabled here for now.
550 // if (*line_ve_tag_number < 0 || *line_ve_tag_number > 399) {
551 // record_errors.push_back(std::format("LINE.LineVeTagNumber is out of range [0-399] with value {}", *line_ve_tag_number));
552 // return;
553 // }
554 if (*line_ve_tag_number != static_cast<short>(*line_ve_tag_number))
555 record_errors.push_back("LINE.LineVeTagNumber should be an integer");
556 if (line_icon && *line_icon != static_cast<short>(*line_icon))
557 record_errors.push_back("LINE.LineIcon should be an integer");
558 if (!record_errors.empty()) return;
559
560 records.lines.emplace_back(
561 Kv1Line::Key(
562 data_owner_code,
563 line_planning_number),
564 line_public_number,
565 line_name,
566 static_cast<short>(*line_ve_tag_number),
567 description,
568 transport_type,
569 static_cast<std::optional<short>>(line_icon),
570 line_color,
571 line_text_color);
572}
573
574void Kv1Parser::parseDestination() {
575 auto data_owner_code = eatString ("DEST.DataOwnerCode", true, 10);
576 auto dest_code = eatString ("DEST.DestCode", true, 10);
577 auto dest_name_full = eatString ("DEST.DestNameFull", true, 50);
578 auto dest_name_main = eatString ("DEST.DestNameMain", true, 24);
579 auto dest_name_detail = eatString ("DEST.DestNameDetail", false, 24);
580 auto relevant_dest_name_detail = eatBoolean ("DEST.RelevantDestNameDetail", true );
581 auto dest_name_main_21 = eatString ("DEST.DestNameMain21", true, 21);
582 auto dest_name_detail_21 = eatString ("DEST.DestNameDetail21", false, 21);
583 auto dest_name_main_19 = eatString ("DEST.DestNameMain19", true, 19);
584 auto dest_name_detail_19 = eatString ("DEST.DestNameDetail19", false, 19);
585 auto dest_name_main_16 = eatString ("DEST.DestNameMain16", true, 16);
586 auto dest_name_detail_16 = eatString ("DEST.DestNameDetail16", false, 16);
587 auto dest_icon = eatNumber ("DEST.DestIcon", false, 4);
588 auto dest_color = eatRgbColor("DEST.DestColor", false );
589 // NOTE: Deviating from the offical KV1 specification here. It specifies that
590 // the maximum length for this field should be 30, but then proceeds to
591 // specify that it should contain a RGB value comprising of three
592 // hexadecimally encoded octets, i.e. six characters. We assume that the
593 // latter is correct and the intended interpretation.
594 auto dest_text_color = eatRgbColor("DEST.DestTextColor", false );
595 if (!record_errors.empty()) return;
596
597 if (dest_icon && *dest_icon != static_cast<short>(*dest_icon)) {
598 record_errors.push_back("DEST.DestIcon should be an integer");
599 return;
600 }
601
602 records.destinations.emplace_back(
603 Kv1Destination::Key(
604 data_owner_code,
605 dest_code),
606 dest_name_full,
607 dest_name_main,
608 dest_name_detail,
609 *relevant_dest_name_detail,
610 dest_name_main_21,
611 dest_name_detail_21,
612 dest_name_main_19,
613 dest_name_detail_19,
614 dest_name_main_16,
615 dest_name_detail_16,
616 dest_icon,
617 dest_color,
618 dest_text_color);
619}
620
621void Kv1Parser::parseJourneyPattern() {
622 auto data_owner_code = eatString("JOPA.DataOwnerCode", true, 10);
623 auto line_planning_number = eatString("JOPA.LinePlanningNumber", true, 10);
624 auto journey_pattern_code = eatString("JOPA.JourneyPatternCode", true, 10);
625 auto journey_pattern_type = eatString("JOPA.JourneyPatternType", true, 10);
626 auto direction = eatString("JOPA.Direction", true, 1);
627 auto description = eatString("JOPA.Description", false, 255);
628 if (!record_errors.empty()) return;
629
630 if (direction != "1" && direction != "2" && direction != "A" && direction != "B") {
631 record_errors.push_back("JOPA.Direction should be in [1, 2, A, B]");
632 return;
633 }
634
635 records.journey_patterns.emplace_back(
636 Kv1JourneyPattern::Key(
637 data_owner_code,
638 line_planning_number,
639 journey_pattern_code),
640 journey_pattern_type,
641 direction[0],
642 description);
643}
644
645void Kv1Parser::parseConcessionFinancerRelation() {
646 auto data_owner_code = eatString("CONFINREL.DataOwnerCode", true, 10);
647 auto con_fin_rel_code = eatString("CONFINREL.ConFinRelCode", true, 10);
648 auto concession_area_code = eatString("CONFINREL.ConcessionAreaCode", true, 10);
649 auto financer_code = eatString("CONFINREL.FinancerCode", false, 10);
650 if (!record_errors.empty()) return;
651
652 records.concession_financer_relations.emplace_back(
653 Kv1ConcessionFinancerRelation::Key(
654 data_owner_code,
655 con_fin_rel_code),
656 concession_area_code,
657 financer_code);
658}
659
660void Kv1Parser::parseConcessionArea() {
661 auto data_owner_code = eatString("CONAREA.DataOwnerCode", true, 10);
662 auto concession_area_code = eatString("CONAREA.ConcessionAreaCode", true, 10);
663 auto description = eatString("CONAREA.Description", true, 255);
664 if (!record_errors.empty()) return;
665
666 records.concession_areas.emplace_back(
667 Kv1ConcessionArea::Key(
668 data_owner_code,
669 concession_area_code),
670 description);
671}
672
673void Kv1Parser::parseFinancer() {
674 auto data_owner_code = eatString("FINANCER.DataOwnerCode", true, 10);
675 auto financer_code = eatString("FINANCER.FinancerCode", true, 10);
676 auto description = eatString("FINANCER.Description", true, 255);
677 if (!record_errors.empty()) return;
678
679 records.financers.emplace_back(
680 Kv1Financer::Key(
681 data_owner_code,
682 financer_code),
683 description);
684}
685
686void Kv1Parser::parseJourneyPatternTimingLink() {
687 auto data_owner_code = eatString ("JOPATILI.DataOwnerCode", true, 10);
688 auto line_planning_number = eatString ("JOPATILI.LinePlanningNumber", true, 10);
689 auto journey_pattern_code = eatString ("JOPATILI.JourneyPatternCode", true, 10);
690 auto timing_link_order = eatNumber ("JOPATILI.TimingLinkOrder", true, 3);
691 auto user_stop_code_begin = eatString ("JOPATILI.UserStopCodeBegin", true, 10);
692 auto user_stop_code_end = eatString ("JOPATILI.UserStopCodeEnd", true, 10);
693 auto con_fin_rel_code = eatString ("JOPATILI.ConFinRelCode", true, 10);
694 auto dest_code = eatString ("JOPATILI.DestCode", true, 10);
695 eatCell ("JOPATILI.<deprecated field #1>" );
696 auto is_timing_stop = eatBoolean ("JOPATILI.IsTimingStop", true );
697 auto display_public_line = eatString ("JOPATILI.DisplayPublicLine", false, 4);
698 auto product_formula_type = eatNumber ("JOPATILI.ProductFormulaType", false, 4);
699 auto get_in = eatBoolean ("JOPATILI.GetIn", true );
700 auto get_out = eatBoolean ("JOPATILI.GetOut", true );
701 auto show_flexible_trip = eatString ("JOPATILI.ShowFlexibleTrip", false, 8);
702 auto line_dest_icon = eatNumber ("JOPATILI.LineDestIcon", false, 4);
703 auto line_dest_color = eatRgbColor("JOPATILI.LineDestColor", false );
704 auto line_dest_text_color = eatRgbColor("JOPATILI.LineDestTextColor", false );
705 if (!record_errors.empty()) return;
706
707 if (line_dest_icon && *line_dest_icon != static_cast<short>(*line_dest_icon))
708 record_errors.push_back("JOPATILI.LineDestIcon should be an integer");
709 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
710 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
711 record_errors.push_back("JOPATILI.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
712 if (!record_errors.empty()) return;
713
714 records.journey_pattern_timing_links.emplace_back(
715 Kv1JourneyPatternTimingLink::Key(
716 data_owner_code,
717 line_planning_number,
718 journey_pattern_code,
719 static_cast<short>(*timing_link_order)),
720 user_stop_code_begin,
721 user_stop_code_end,
722 con_fin_rel_code,
723 dest_code,
724 *is_timing_stop,
725 display_public_line,
726 product_formula_type,
727 *get_in,
728 *get_out,
729 show_flexible_trip,
730 line_dest_icon,
731 line_dest_color,
732 line_dest_text_color);
733}
734
735void Kv1Parser::parsePoint() {
736 auto data_owner_code = eatString("POINT.DataOwnerCode", true, 10);
737 auto point_code = eatString("POINT.PointCode", true, 10);
738 eatCell ("POINT.<deprecated field #1>" );
739 auto point_type = eatString("POINT.PointType", true, 10);
740 auto coordinate_system_type = eatString("POINT.CoordinateSystemType", true, 10);
741 // NOTE: We deviate from the specification here once again. The specification
742 // notes that LocationX_EW should contain 'at least 6 positions'. Assuming
743 // that this is referring to the amount of digits, we have to lower this to
744 // 4. Otherwise, some positions in the Netherlands and Belgium are
745 // unrepresentable.
746 auto location_x_ew = eatRdCoord("POINT.LocationX_EW", true, 4);
747 auto location_y_ew = eatRdCoord("POINT.LocationX_EW", true, 6);
748 auto location_z = eatRdCoord("POINT.LocationZ", false, 0);
749 auto description = eatString ("POINT.Description", false, 255);
750 if (!record_errors.empty()) return;
751
752 records.points.emplace_back(
753 Kv1Point::Key(
754 std::move(data_owner_code),
755 std::move(point_code)),
756 std::move(point_type),
757 std::move(coordinate_system_type),
758 *location_x_ew,
759 *location_y_ew,
760 location_z,
761 std::move(description));
762}
763
764void Kv1Parser::parsePointOnLink() {
765 auto data_owner_code = eatString("POOL.DataOwnerCode", true, 10);
766 auto user_stop_code_begin = eatString("POOL.UserStopCodeBegin", true, 10);
767 auto user_stop_code_end = eatString("POOL.UserStopCodeEnd", true, 10);
768 eatCell ("POOL.<deprecated field #1>" );
769 auto point_data_owner_code = eatString("POOL.PointDataOwnerCode", true, 10);
770 auto point_code = eatString("POOL.PointCode", true, 10);
771 auto distance_since_start_of_link = eatNumber("POOL.DistanceSinceStartOfLink", true, 5);
772 auto segment_speed = eatNumber("POOL.SegmentSpeed", false, 4);
773 auto local_point_speed = eatNumber("POOL.LocalPointSpeed", false, 4);
774 auto description = eatString("POOL.Description", false, 255);
775 auto transport_type = eatString("POOL.TransportType", true, 5);
776 if (!record_errors.empty()) return;
777
778 records.point_on_links.emplace_back(
779 Kv1PointOnLink::Key(
780 data_owner_code,
781 user_stop_code_begin,
782 user_stop_code_end,
783 point_data_owner_code,
784 point_code,
785 transport_type),
786 *distance_since_start_of_link,
787 segment_speed,
788 local_point_speed,
789 std::move(description));
790}
791
792void Kv1Parser::parseIcon() {
793 auto data_owner_code = eatString("ICON.DataOwnerCode", true, 10);
794 auto icon_number = eatNumber("ICON.IconNumber", true, 4);
795 auto icon_uri = eatString("ICON.IconURI", true, 1024);
796 if (!record_errors.empty()) return;
797
798 if (*icon_number != static_cast<short>(*icon_number)) {
799 record_errors.push_back("ICON.IconNumber should be an integer");
800 return;
801 }
802
803 records.icons.emplace_back(
804 Kv1Icon::Key(
805 data_owner_code,
806 static_cast<short>(*icon_number)),
807 icon_uri);
808}
809
810void Kv1Parser::parseNotice() {
811 auto data_owner_code = eatString("NOTICE.DataOwnerCode", true, 10);
812 auto notice_code = eatString("NOTICE.NoticeCode", true, 20);
813 auto notice_content = eatString("NOTICE.NoticeContent", true, 1024);
814 if (!record_errors.empty()) return;
815
816 records.notices.emplace_back(
817 Kv1Notice::Key(
818 data_owner_code,
819 notice_code),
820 notice_content);
821}
822
823void Kv1Parser::parseNoticeAssignment() {
824 auto data_owner_code = eatString("NTCASSGNM.DataOwnerCode", true, 10);
825 auto notice_code = eatString("NTCASSGNM.NoticeCode", true, 20);
826 auto assigned_object = eatString("NTCASSGNM.AssignedObject", true, 8);
827 auto timetable_version_code = eatString("NTCASSGNM.TimetableVersionCode", false, 10);
828 auto organizational_unit_code = eatString("NTCASSGNM.OrganizationalUnitCode", false, 10);
829 auto schedule_code = eatString("NTCASSGNM.ScheduleCode", false, 10);
830 auto schedule_type_code = eatString("NTCASSGNM.ScheduleTypeCode", false, 10);
831 auto period_group_code = eatString("NTCASSGNM.PeriodGroupCode", false, 10);
832 auto specific_day_code = eatString("NTCASSGNM.SpecificDayCode", false, 10);
833 auto day_type = eatString("NTCASSGNM.DayType", false, 7);
834 auto line_planning_number = eatString("NTCASSGNM.LinePlanningNumber", true, 10);
835 auto journey_number = eatNumber("NTCASSGNM.JourneyNumber", false, 6);
836 auto stop_order = eatNumber("NTCASSGNM.StopOrder", false, 4);
837 auto journey_pattern_code = eatString("NTCASSGNM.JourneyPatternCode", false, 10);
838 auto timing_link_order = eatNumber("NTCASSGNM.TimingLinkOrder", false, 3);
839 auto user_stop_code = eatString("NTCASSGNM.UserStopCode", false, 10);
840 if (!record_errors.empty()) return;
841
842 if (journey_number && *journey_number != static_cast<short>(*journey_number))
843 record_errors.push_back("NTCASSGNM.JourneyNumber should be an integer");
844 if (journey_number && (*journey_number < 0 || *journey_number > 999'999))
845 record_errors.push_back("NTCASSGNM.JourneyNumber should be within the range [0-999999]");
846 if (stop_order && *stop_order != static_cast<short>(*stop_order))
847 record_errors.push_back("NTCASSGNM.StopOrder should be an integer");
848 if (!journey_number && (assigned_object == "PUJO" || assigned_object == "PUJOPASS"))
849 record_errors.push_back("NTCASSGNM.JourneyNumber is required for AssignedObject PUJO/PUJOPASS");
850 if (journey_pattern_code.empty() && assigned_object == "JOPATILI")
851 record_errors.push_back("NTCASSGNM.JourneyPatternCode is required for AssignedObject JOPATILI");
852 if (!record_errors.empty()) return;
853
854 records.notice_assignments.emplace_back(
855 data_owner_code,
856 notice_code,
857 assigned_object,
858 timetable_version_code,
859 organizational_unit_code,
860 schedule_code,
861 schedule_type_code,
862 period_group_code,
863 specific_day_code,
864 day_type,
865 line_planning_number,
866 static_cast<std::optional<int>>(journey_number),
867 static_cast<std::optional<short>>(stop_order),
868 journey_pattern_code,
869 timing_link_order,
870 user_stop_code);
871}
872
873void Kv1Parser::parseTimeDemandGroup() {
874 auto data_owner_code = eatString("TIMDEMGRP.DataOwnerCode", true, 10);
875 auto line_planning_number = eatString("TIMDEMGRP.LinePlanningNumber", true, 10);
876 auto journey_pattern_code = eatString("TIMDEMGRP.JourneyPatternCode", true, 10);
877 auto time_demand_group_code = eatString("TIMDEMGRP.TimeDemandGroupCode", true, 10);
878 if (!record_errors.empty()) return;
879
880 records.time_demand_groups.emplace_back(
881 Kv1TimeDemandGroup::Key(
882 data_owner_code,
883 line_planning_number,
884 journey_pattern_code,
885 time_demand_group_code));
886}
887
888void Kv1Parser::parseTimeDemandGroupRunTime() {
889 auto data_owner_code = eatString("TIMDEMRNT.DataOwnerCode", true, 10);
890 auto line_planning_number = eatString("TIMDEMRNT.LinePlanningNumber", true, 10);
891 auto journey_pattern_code = eatString("TIMDEMRNT.JourneyPatternCode", true, 10);
892 auto time_demand_group_code = eatString("TIMDEMRNT.TimeDemandGroupCode", true, 10);
893 auto timing_link_order = eatNumber("TIMDEMRNT.TimingLinkOrder", true, 3);
894 auto user_stop_code_begin = eatString("TIMDEMRNT.UserStopCodeBegin", true, 10);
895 auto user_stop_code_end = eatString("TIMDEMRNT.UserStopCodeEnd", true, 10);
896 auto total_drive_time = eatNumber("TIMDEMRNT.TotalDriveTime", true, 5);
897 auto drive_time = eatNumber("TIMDEMRNT.DriveTime", true, 5);
898 auto expected_delay = eatNumber("TIMDEMRNT.ExpectedDelay", false, 5);
899 auto layover_time = eatNumber("TIMDEMRNT.LayOverTime", false, 5);
900 auto stop_wait_time = eatNumber("TIMDEMRNT.StopWaitTime", true, 5);
901 auto minimum_stop_time = eatNumber("TIMDEMRNT.MinimumStopTime", false, 5);
902 if (!record_errors.empty()) return;
903
904 if (timing_link_order && *timing_link_order != static_cast<short>(*timing_link_order)) {
905 record_errors.push_back("TIMDEMRNT.TimingLinkOrder should be an integer");
906 return;
907 }
908
909 records.time_demand_group_run_times.emplace_back(
910 Kv1TimeDemandGroupRunTime::Key(
911 data_owner_code,
912 line_planning_number,
913 journey_pattern_code,
914 time_demand_group_code,
915 static_cast<short>(*timing_link_order)),
916 user_stop_code_begin,
917 user_stop_code_end,
918 *total_drive_time,
919 *drive_time,
920 expected_delay,
921 layover_time,
922 *stop_wait_time,
923 minimum_stop_time);
924}
925
926void Kv1Parser::parsePeriodGroup() {
927 auto data_owner_code = eatString("PEGR.DataOwnerCode", true, 10);
928 auto period_group_code = eatString("PEGR.PeriodGroupCode", true, 10);
929 auto description = eatString("PEGR.Description", false, 255);
930 if (!record_errors.empty()) return;
931
932 records.period_groups.emplace_back(
933 Kv1PeriodGroup::Key(
934 data_owner_code,
935 period_group_code),
936 description);
937}
938
939void Kv1Parser::parseSpecificDay() {
940 auto data_owner_code = eatString("SPECDAY.DataOwnerCode", true, 10);
941 auto specific_day_code = eatString("SPECDAY.SpecificDayCode", true, 10);
942 auto name = eatString("SPECDAY.Name", true, 50);
943 auto description = eatString("SPECDAY.Description", false, 255);
944 if (!record_errors.empty()) return;
945
946 records.specific_days.emplace_back(
947 Kv1SpecificDay::Key(
948 data_owner_code,
949 specific_day_code),
950 name,
951 description);
952}
953
954void Kv1Parser::parseTimetableVersion() {
955 auto data_owner_code = eatString("TIVE.DataOwnerCode", true, 10);
956 auto organizational_unit_code = eatString("TIVE.OrganizationalUnitCode", true, 10);
957 auto timetable_version_code = eatString("TIVE.TimetableVersionCode", true, 10);
958 auto period_group_code = eatString("TIVE.PeriodGroupCode", true, 10);
959 auto specific_day_code = eatString("TIVE.SpecificDayCode", true, 10);
960 auto valid_from_raw = eatString("TIVE.ValidFrom", true, 10);
961 auto timetable_version_type = eatString("TIVE.TimetableVersionType", true, 10);
962 auto valid_thru_raw = eatString("TIVE.ValidThru", false, 10);
963 auto description = eatString("TIVE.Description", false, 255);
964 if (!record_errors.empty()) return;
965
966 auto valid_from = parseYyyymmdd(valid_from_raw);
967 if (!valid_from)
968 record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD");
969 std::optional<std::chrono::year_month_day> valid_thru;
970 if (!valid_thru_raw.empty()) {
971 valid_thru = parseYyyymmdd(valid_thru_raw);
972 if (!valid_thru) {
973 record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD");
974 }
975 }
976 if (!description.empty())
977 record_errors.push_back("TIVE.Description should be empty");
978 if (!record_errors.empty()) return;
979
980 records.timetable_versions.emplace_back(
981 Kv1TimetableVersion::Key(
982 data_owner_code,
983 organizational_unit_code,
984 timetable_version_code,
985 period_group_code,
986 specific_day_code),
987 *valid_from,
988 timetable_version_type,
989 valid_thru,
990 description);
991}
992
993void Kv1Parser::parsePublicJourney() {
994 auto data_owner_code = eatString ("PUJO.DataOwnerCode", true, 10);
995 auto timetable_version_code = eatString ("PUJO.TimetableVersionCode", true, 10);
996 auto organizational_unit_code = eatString ("PUJO.OrganizationalUnitCode", true, 10);
997 auto period_group_code = eatString ("PUJO.PeriodGroupCode", true, 10);
998 auto specific_day_code = eatString ("PUJO.SpecificDayCode", true, 10);
999 auto day_type = eatString ("PUJO.DayType", true, 7);
1000 auto line_planning_number = eatString ("PUJO.LinePlanningNumber", true, 10);
1001 auto journey_number = eatNumber ("PUJO.JourneyNumber", true, 6);
1002 auto time_demand_group_code = eatString ("PUJO.TimeDemandGroupCode", true, 10);
1003 auto journey_pattern_code = eatString ("PUJO.JourneyPatternCode", true, 10);
1004 auto departure_time_raw = eatString ("PUJO.DepartureTime", true, 8);
1005 auto wheelchair_accessible = eatString ("PUJO.WheelChairAccessible", true, 13);
1006 auto data_owner_is_operator = eatBoolean("PUJO.DataOwnerIsOperator", true );
1007 auto planned_monitored = eatBoolean("PUJO.PlannedMonitored", true );
1008 auto product_formula_type = eatNumber ("PUJO.ProductFormulaType", false, 4);
1009 auto show_flexible_trip = eatString ("PUJO.ShowFlexibleTrip", false, 8);
1010 if (!record_errors.empty()) return;
1011
1012 auto departure_time = parseHhmmss(departure_time_raw);
1013 if (!departure_time)
1014 record_errors.push_back("PUJO.DepartureTime has a bad format");
1015 if (*journey_number < 0 || *journey_number > 999'999)
1016 record_errors.push_back("PUJO.JourneyNumber should be within the range [0-999999]");
1017 if (*journey_number != static_cast<int>(*journey_number))
1018 record_errors.push_back("PUJO.JourneyNumber should be an integer");
1019 if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type))
1020 record_errors.push_back("PUJO.ProductFormulaType should be an integer");
1021 if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN")
1022 record_errors.push_back("PUJO.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]");
1023 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
1024 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
1025 record_errors.push_back("PUJO.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
1026 if (!record_errors.empty()) return;
1027
1028 records.public_journeys.emplace_back(
1029 Kv1PublicJourney::Key(
1030 data_owner_code,
1031 timetable_version_code,
1032 organizational_unit_code,
1033 period_group_code,
1034 specific_day_code,
1035 day_type,
1036 line_planning_number,
1037 static_cast<int>(*journey_number)),
1038 time_demand_group_code,
1039 journey_pattern_code,
1040 *departure_time,
1041 wheelchair_accessible,
1042 *data_owner_is_operator,
1043 *planned_monitored,
1044 product_formula_type,
1045 show_flexible_trip);
1046}
1047
1048void Kv1Parser::parsePeriodGroupValidity() {
1049 auto data_owner_code = eatString("PEGRVAL.DataOwnerCode", true, 10);
1050 auto organizational_unit_code = eatString("PEGRVAL.OrganizationalUnitCode", true, 10);
1051 auto period_group_code = eatString("PEGRVAL.PeriodGroupCode", true, 10);
1052 auto valid_from_raw = eatString("PEGRVAL.ValidFrom", true, 10);
1053 auto valid_thru_raw = eatString("PEGRVAL.ValidThru", true, 10);
1054 if (!record_errors.empty()) return;
1055
1056 auto valid_from = parseYyyymmdd(valid_from_raw);
1057 auto valid_thru = parseYyyymmdd(valid_thru_raw);
1058 if (!valid_from)
1059 record_errors.push_back("PEGRVAL.ValidFrom has invalid format, should be YYYY-MM-DD");
1060 if (!valid_thru)
1061 record_errors.push_back("PEGRVAL.ValidThru has invalid format, should be YYYY-MM-DD");
1062 if (!record_errors.empty()) return;
1063
1064 records.period_group_validities.emplace_back(
1065 Kv1PeriodGroupValidity::Key(
1066 data_owner_code,
1067 organizational_unit_code,
1068 period_group_code,
1069 *valid_from),
1070 *valid_thru);
1071}
1072
1073void Kv1Parser::parseExceptionalOperatingDay() {
1074 auto data_owner_code = eatString("EXCOPDAY.DataOwnerCode", true, 10);
1075 auto organizational_unit_code = eatString("EXCOPDAY.OrganizationalUnitCode", true, 10);
1076 auto valid_date_raw = eatString("EXCOPDAY.ValidDate", true, 23);
1077 auto day_type_as_on = eatString("EXCOPDAY.DayTypeAsOn", true, 7);
1078 auto specific_day_code = eatString("EXCOPDAY.SpecificDayCode", true, 10);
1079 auto period_group_code = eatString("EXCOPDAY.PeriodGroupCode", false, 10);
1080 auto description = eatString("EXCOPDAY.Description", false, 255);
1081 if (!record_errors.empty()) return;
1082
1083 std::string_view error;
1084 auto valid_date = parseDateTime(valid_date_raw, amsterdam, &error);
1085 if (!valid_date) {
1086 record_errors.push_back(std::format("EXCOPDAY.ValidDate has an bad format (value: {}): {}", valid_date_raw, error));
1087 return;
1088 }
1089
1090 records.exceptional_operating_days.emplace_back(
1091 Kv1ExceptionalOperatingDay::Key(
1092 data_owner_code,
1093 organizational_unit_code,
1094 *valid_date),
1095 day_type_as_on,
1096 specific_day_code,
1097 period_group_code,
1098 description);
1099}
1100
1101void Kv1Parser::parseScheduleVersion() {
1102 auto data_owner_code = eatString("SCHEDVERS.DataOwnerCode", true, 10);
1103 auto organizational_unit_code = eatString("SCHEDVERS.OrganizationalUnitCode", true, 10);
1104 auto schedule_code = eatString("SCHEDVERS.ScheduleCode", true, 10);
1105 auto schedule_type_code = eatString("SCHEDVERS.ScheduleTypeCode", true, 10);
1106 auto valid_from_raw = eatString("SCHEDVERS.ValidFrom", true, 10);
1107 auto valid_thru_raw = eatString("SCHEDVERS.ValidThru", false, 10);
1108 auto description = eatString("SCHEDVERS.Description", false, 255);
1109 if (!record_errors.empty()) return;
1110
1111 auto valid_from = parseYyyymmdd(valid_from_raw);
1112 if (!valid_from)
1113 record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD");
1114 std::optional<std::chrono::year_month_day> valid_thru;
1115 if (!valid_thru_raw.empty()) {
1116 valid_thru = parseYyyymmdd(valid_thru_raw);
1117 if (!valid_thru) {
1118 record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD");
1119 }
1120 }
1121 if (!description.empty())
1122 record_errors.push_back("SCHEDVERS.Description should be empty");
1123 if (!record_errors.empty()) return;
1124
1125 records.schedule_versions.emplace_back(
1126 Kv1ScheduleVersion::Key(
1127 data_owner_code,
1128 organizational_unit_code,
1129 schedule_code,
1130 schedule_type_code),
1131 *valid_from,
1132 valid_thru,
1133 description);
1134}
1135
1136void Kv1Parser::parsePublicJourneyPassingTimes() {
1137 auto data_owner_code = eatString ("PUJOPASS.DataOwnerCode", true, 10);
1138 auto organizational_unit_code = eatString ("PUJOPASS.OrganizationalUnitCode", true, 10);
1139 auto schedule_code = eatString ("PUJOPASS.ScheduleCode", true, 10);
1140 auto schedule_type_code = eatString ("PUJOPASS.ScheduleTypeCode", true, 10);
1141 auto line_planning_number = eatString ("PUJOPASS.LinePlanningNumber", true, 10);
1142 auto journey_number = eatNumber ("PUJOPASS.JourneyNumber", true, 6);
1143 auto stop_order = eatNumber ("PUJOPASS.StopOrder", true, 4);
1144 auto journey_pattern_code = eatString ("PUJOPASS.JourneyPatternCode", true, 10);
1145 auto user_stop_code = eatString ("PUJOPASS.UserStopCode", true, 10);
1146 auto target_arrival_time_raw = eatString ("PUJOPASS.TargetArrivalTime", false, 8);
1147 auto target_departure_time_raw = eatString ("PUJOPASS.TargetDepartureTime", false, 8);
1148 auto wheelchair_accessible = eatString ("PUJOPASS.WheelChairAccessible", true, 13);
1149 auto data_owner_is_operator = eatBoolean("PUJOPASS.DataOwnerIsOperator", true );
1150 auto planned_monitored = eatBoolean("PUJOPASS.PlannedMonitored", true );
1151 auto product_formula_type = eatNumber ("PUJOPASS.ProductFormulaType", false, 4);
1152 auto show_flexible_trip = eatString ("PUJOPASS.ShowFlexibleTrip", false, 8);
1153 if (!record_errors.empty()) return;
1154
1155 if (*journey_number < 0 || *journey_number > 999'999)
1156 record_errors.push_back("PUJOPASS.JourneyNumber should be within the range [0-999999]");
1157 if (*journey_number != static_cast<int>(*journey_number))
1158 record_errors.push_back("PUJOPASS.JourneyNumber should be an integer");
1159 if (*stop_order != static_cast<short>(*stop_order))
1160 record_errors.push_back("PUJOPASS.StopOrder should be an integer");
1161 if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type))
1162 record_errors.push_back("PUJOPASS.ProductFormulaType should be an integer");
1163 if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN")
1164 record_errors.push_back("PUJOPASS.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]");
1165 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
1166 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
1167 record_errors.push_back("PUJOPASS.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
1168 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_arrival_time;
1169 if (!target_arrival_time_raw.empty()) {
1170 target_arrival_time = parseHhmmss(target_arrival_time_raw);
1171 if (!target_arrival_time) {
1172 record_errors.push_back("PUJOPASS.TargetArrivalTime has invalid format, should be HH:MM:SS");
1173 }
1174 }
1175 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_departure_time;
1176 if (!target_departure_time_raw.empty()) {
1177 target_departure_time = parseHhmmss(target_departure_time_raw);
1178 if (!target_departure_time) {
1179 record_errors.push_back("PUJOPASS.TargetDepartureTime has invalid format, should be HH:MM:SS");
1180 }
1181 }
1182 if (!record_errors.empty()) return;
1183
1184 records.public_journey_passing_times.emplace_back(
1185 Kv1PublicJourneyPassingTimes::Key(
1186 data_owner_code,
1187 organizational_unit_code,
1188 schedule_code,
1189 schedule_type_code,
1190 line_planning_number,
1191 static_cast<int>(*journey_number),
1192 static_cast<short>(*stop_order)),
1193 journey_pattern_code,
1194 user_stop_code,
1195 target_arrival_time,
1196 target_departure_time,
1197 wheelchair_accessible,
1198 *data_owner_is_operator,
1199 *planned_monitored,
1200 product_formula_type,
1201 show_flexible_trip);
1202}
1203
1204void Kv1Parser::parseOperatingDay() {
1205 auto data_owner_code = eatString("OPERDAY.DataOwnerCode", true, 10);
1206 auto organizational_unit_code = eatString("OPERDAY.OrganizationalUnitCode", true, 10);
1207 auto schedule_code = eatString("OPERDAY.ScheduleCode", true, 10);
1208 auto schedule_type_code = eatString("OPERDAY.ScheduleTypeCode", true, 10);
1209 auto valid_date_raw = eatString("OPERDAY.ValidDate", true, 10);
1210 auto description = eatString("OPERDAY.Description", false, 255);
1211 if (!record_errors.empty()) return;
1212
1213 auto valid_date = parseYyyymmdd(valid_date_raw);
1214 if (!valid_date)
1215 record_errors.push_back("OPERDAY.ValidDate has invalid format, should be YYYY-MM-DD");
1216 if (!record_errors.empty()) return;
1217
1218 records.operating_days.emplace_back(
1219 Kv1OperatingDay::Key(
1220 data_owner_code,
1221 organizational_unit_code,
1222 schedule_code,
1223 schedule_type_code,
1224 *valid_date),
1225 description);
1226}
1227
1228const std::unordered_map<std::string_view, Kv1Parser::ParseFunc> Kv1Parser::type_parsers{
1229 { "ORUN", &Kv1Parser::parseOrganizationalUnit },
1230 { "ORUNORUN", &Kv1Parser::parseHigherOrganizationalUnit },
1231 { "USRSTOP", &Kv1Parser::parseUserStopPoint },
1232 { "USRSTAR", &Kv1Parser::parseUserStopArea },
1233 { "TILI", &Kv1Parser::parseTimingLink },
1234 { "LINK", &Kv1Parser::parseLink },
1235 { "LINE", &Kv1Parser::parseLine },
1236 { "DEST", &Kv1Parser::parseDestination },
1237 { "JOPA", &Kv1Parser::parseJourneyPattern },
1238 { "CONFINREL", &Kv1Parser::parseConcessionFinancerRelation },
1239 { "CONAREA", &Kv1Parser::parseConcessionArea },
1240 { "FINANCER", &Kv1Parser::parseFinancer },
1241 { "JOPATILI", &Kv1Parser::parseJourneyPatternTimingLink },
1242 { "POINT", &Kv1Parser::parsePoint },
1243 { "POOL", &Kv1Parser::parsePointOnLink },
1244 { "ICON", &Kv1Parser::parseIcon },
1245 { "NOTICE", &Kv1Parser::parseNotice },
1246 { "NTCASSGNM", &Kv1Parser::parseNoticeAssignment },
1247 { "TIMDEMGRP", &Kv1Parser::parseTimeDemandGroup },
1248 { "TIMDEMRNT", &Kv1Parser::parseTimeDemandGroupRunTime },
1249 { "PEGR", &Kv1Parser::parsePeriodGroup },
1250 { "SPECDAY", &Kv1Parser::parseSpecificDay },
1251 { "TIVE", &Kv1Parser::parseTimetableVersion },
1252 { "PUJO", &Kv1Parser::parsePublicJourney },
1253 { "PEGRVAL", &Kv1Parser::parsePeriodGroupValidity },
1254 { "EXCOPDAY", &Kv1Parser::parseExceptionalOperatingDay },
1255 { "SCHEDVERS", &Kv1Parser::parseScheduleVersion },
1256 { "PUJOPASS", &Kv1Parser::parsePublicJourneyPassingTimes },
1257 { "OPERDAY", &Kv1Parser::parseOperatingDay },
1258};
diff --git a/lib/libtmi8/src/kv1_types.cpp b/lib/libtmi8/src/kv1_types.cpp
new file mode 100644
index 0000000..49e306e
--- /dev/null
+++ b/lib/libtmi8/src/kv1_types.cpp
@@ -0,0 +1,773 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <boost/container_hash/hash.hpp>
4
5#include <tmi8/kv1_types.hpp>
6
7size_t Kv1Records::size() const {
8 return organizational_units.size()
9 + higher_organizational_units.size()
10 + user_stop_points.size()
11 + user_stop_areas.size()
12 + timing_links.size()
13 + links.size()
14 + lines.size()
15 + destinations.size()
16 + journey_patterns.size()
17 + concession_financer_relations.size()
18 + concession_areas.size()
19 + financers.size()
20 + journey_pattern_timing_links.size()
21 + points.size()
22 + point_on_links.size()
23 + icons.size()
24 + notices.size()
25 + notice_assignments.size()
26 + time_demand_groups.size()
27 + time_demand_group_run_times.size()
28 + period_groups.size()
29 + specific_days.size()
30 + timetable_versions.size()
31 + public_journeys.size()
32 + period_group_validities.size()
33 + exceptional_operating_days.size()
34 + schedule_versions.size()
35 + public_journey_passing_times.size()
36 + operating_days.size();
37}
38
39Kv1OrganizationalUnit::Key::Key(
40 std::string data_owner_code,
41 std::string organizational_unit_code)
42 : data_owner_code(std::move(data_owner_code)),
43 organizational_unit_code(std::move(organizational_unit_code))
44{}
45
46Kv1HigherOrganizationalUnit::Key::Key(
47 std::string data_owner_code,
48 std::string organizational_unit_code_parent,
49 std::string organizational_unit_code_child,
50 std::chrono::year_month_day valid_from)
51 : data_owner_code(std::move(data_owner_code)),
52 organizational_unit_code_parent(std::move(organizational_unit_code_parent)),
53 organizational_unit_code_child(std::move(organizational_unit_code_child)),
54 valid_from(valid_from)
55{}
56
57Kv1UserStopPoint::Key::Key(
58 std::string data_owner_code,
59 std::string user_stop_code)
60 : data_owner_code(std::move(data_owner_code)),
61 user_stop_code(std::move(user_stop_code))
62{}
63
64Kv1UserStopArea::Key::Key(
65 std::string data_owner_code,
66 std::string user_stop_area_code)
67 : data_owner_code(std::move(data_owner_code)),
68 user_stop_area_code(std::move(user_stop_area_code))
69{}
70
71Kv1TimingLink::Key::Key(
72 std::string data_owner_code,
73 std::string user_stop_code_begin,
74 std::string user_stop_code_end)
75 : data_owner_code(std::move(data_owner_code)),
76 user_stop_code_begin(std::move(user_stop_code_begin)),
77 user_stop_code_end(std::move(user_stop_code_end))
78{}
79
80Kv1Link::Key::Key(std::string data_owner_code,
81 std::string user_stop_code_begin,
82 std::string user_stop_code_end,
83 std::string transport_type)
84 : data_owner_code(std::move(data_owner_code)),
85 user_stop_code_begin(std::move(user_stop_code_begin)),
86 user_stop_code_end(std::move(user_stop_code_end)),
87 transport_type(std::move(transport_type))
88{}
89
90Kv1Line::Key::Key(std::string data_owner_code,
91 std::string line_planning_number)
92 : data_owner_code(std::move(data_owner_code)),
93 line_planning_number(std::move(line_planning_number))
94{}
95
96Kv1Destination::Key::Key(std::string data_owner_code,
97 std::string dest_code)
98 : data_owner_code(std::move(data_owner_code)),
99 dest_code(std::move(dest_code))
100{}
101
102Kv1JourneyPattern::Key::Key(std::string data_owner_code,
103 std::string line_planning_number,
104 std::string journey_pattern_code)
105 : data_owner_code(std::move(data_owner_code)),
106 line_planning_number(std::move(line_planning_number)),
107 journey_pattern_code(std::move(journey_pattern_code))
108{}
109
110Kv1ConcessionFinancerRelation::Key::Key(std::string data_owner_code,
111 std::string con_fin_rel_code)
112 : data_owner_code(std::move(data_owner_code)),
113 con_fin_rel_code(std::move(con_fin_rel_code))
114{}
115
116Kv1ConcessionArea::Key::Key(std::string data_owner_code,
117 std::string concession_area_code)
118 : data_owner_code(std::move(data_owner_code)),
119 concession_area_code(std::move(concession_area_code))
120{}
121
122Kv1Financer::Key::Key(std::string data_owner_code,
123 std::string financer_code)
124 : data_owner_code(std::move(data_owner_code)),
125 financer_code(std::move(financer_code))
126{}
127
128Kv1JourneyPatternTimingLink::Key::Key(std::string data_owner_code,
129 std::string line_planning_number,
130 std::string journey_pattern_code,
131 short timing_link_order)
132 : data_owner_code(std::move(data_owner_code)),
133 line_planning_number(std::move(line_planning_number)),
134 journey_pattern_code(journey_pattern_code),
135 timing_link_order(timing_link_order)
136{}
137
138Kv1Point::Key::Key(std::string data_owner_code,
139 std::string point_code)
140 : data_owner_code(std::move(data_owner_code)),
141 point_code(std::move(point_code))
142{}
143
144Kv1PointOnLink::Key::Key(std::string data_owner_code,
145 std::string user_stop_code_begin,
146 std::string user_stop_code_end,
147 std::string point_data_owner_code,
148 std::string point_code,
149 std::string transport_type)
150 : data_owner_code(std::move(data_owner_code)),
151 user_stop_code_begin(std::move(user_stop_code_begin)),
152 user_stop_code_end(std::move(user_stop_code_end)),
153 point_data_owner_code(std::move(point_data_owner_code)),
154 point_code(std::move(point_code)),
155 transport_type(std::move(transport_type))
156{}
157
158Kv1Icon::Key::Key(std::string data_owner_code,
159 short icon_number)
160 : data_owner_code(std::move(data_owner_code)),
161 icon_number(icon_number)
162{}
163
164Kv1Notice::Key::Key(std::string data_owner_code,
165 std::string notice_code)
166 : data_owner_code(std::move(data_owner_code)),
167 notice_code(std::move(notice_code))
168{}
169
170Kv1TimeDemandGroup::Key::Key(std::string data_owner_code,
171 std::string line_planning_number,
172 std::string journey_pattern_code,
173 std::string time_demand_group_code)
174 : data_owner_code(std::move(data_owner_code)),
175 line_planning_number(std::move(line_planning_number)),
176 journey_pattern_code(std::move(journey_pattern_code)),
177 time_demand_group_code(std::move(time_demand_group_code))
178{}
179
180Kv1TimeDemandGroupRunTime::Key::Key(std::string data_owner_code,
181 std::string line_planning_number,
182 std::string journey_pattern_code,
183 std::string time_demand_group_code,
184 short timing_link_order)
185 : data_owner_code(std::move(data_owner_code)),
186 line_planning_number(std::move(line_planning_number)),
187 journey_pattern_code(std::move(journey_pattern_code)),
188 time_demand_group_code(std::move(time_demand_group_code)),
189 timing_link_order(std::move(timing_link_order))
190{}
191
192Kv1PeriodGroup::Key::Key(std::string data_owner_code,
193 std::string period_group_code)
194 : data_owner_code(std::move(data_owner_code)),
195 period_group_code(std::move(period_group_code))
196{}
197
198Kv1SpecificDay::Key::Key(std::string data_owner_code,
199 std::string specific_day_code)
200 : data_owner_code(std::move(data_owner_code)),
201 specific_day_code(std::move(specific_day_code))
202{}
203
204Kv1TimetableVersion::Key::Key(std::string data_owner_code,
205 std::string organizational_unit_code,
206 std::string timetable_version_code,
207 std::string period_group_code,
208 std::string specific_day_code)
209 : data_owner_code(std::move(data_owner_code)),
210 organizational_unit_code(std::move(organizational_unit_code)),
211 timetable_version_code(std::move(timetable_version_code)),
212 period_group_code(std::move(period_group_code)),
213 specific_day_code(std::move(specific_day_code))
214{}
215
216Kv1PublicJourney::Key::Key(std::string data_owner_code,
217 std::string timetable_version_code,
218 std::string organizational_unit_code,
219 std::string period_group_code,
220 std::string specific_day_code,
221 std::string day_type,
222 std::string line_planning_number,
223 int journey_number)
224 : data_owner_code(std::move(data_owner_code)),
225 timetable_version_code(std::move(timetable_version_code)),
226 organizational_unit_code(std::move(organizational_unit_code)),
227 period_group_code(std::move(period_group_code)),
228 specific_day_code(std::move(specific_day_code)),
229 day_type(std::move(day_type)),
230 line_planning_number(std::move(line_planning_number)),
231 journey_number(journey_number)
232{}
233
234Kv1PeriodGroupValidity::Key::Key(std::string data_owner_code,
235 std::string organizational_unit_code,
236 std::string period_group_code,
237 std::chrono::year_month_day valid_from)
238 : data_owner_code(std::move(data_owner_code)),
239 organizational_unit_code(std::move(organizational_unit_code)),
240 period_group_code(std::move(period_group_code)),
241 valid_from(valid_from)
242{}
243
244Kv1ExceptionalOperatingDay::Key::Key(std::string data_owner_code,
245 std::string organizational_unit_code,
246 std::chrono::sys_seconds valid_date)
247 : data_owner_code(std::move(data_owner_code)),
248 organizational_unit_code(std::move(organizational_unit_code)),
249 valid_date(valid_date)
250{}
251
252Kv1ScheduleVersion::Key::Key(std::string data_owner_code,
253 std::string organizational_unit_code,
254 std::string schedule_code,
255 std::string schedule_type_code)
256 : data_owner_code(std::move(data_owner_code)),
257 organizational_unit_code(std::move(organizational_unit_code)),
258 schedule_code(std::move(schedule_code)),
259 schedule_type_code(std::move(schedule_type_code))
260{}
261
262Kv1PublicJourneyPassingTimes::Key::Key(std::string data_owner_code,
263 std::string organizational_unit_code,
264 std::string schedule_code,
265 std::string schedule_type_code,
266 std::string line_planning_number,
267 int journey_number,
268 short stop_order)
269 : data_owner_code(std::move(data_owner_code)),
270 organizational_unit_code(std::move(organizational_unit_code)),
271 schedule_code(std::move(schedule_code)),
272 schedule_type_code(std::move(schedule_type_code)),
273 line_planning_number(std::move(line_planning_number)),
274 journey_number(journey_number),
275 stop_order(stop_order)
276{}
277
278Kv1OperatingDay::Key::Key(std::string data_owner_code,
279 std::string organizational_unit_code,
280 std::string schedule_code,
281 std::string schedule_type_code,
282 std::chrono::year_month_day valid_date)
283 : data_owner_code(std::move(data_owner_code)),
284 organizational_unit_code(std::move(organizational_unit_code)),
285 schedule_code(std::move(schedule_code)),
286 schedule_type_code(std::move(schedule_type_code)),
287 valid_date(valid_date)
288{}
289
290bool operator==(const Kv1OrganizationalUnit::Key &a, const Kv1OrganizationalUnit::Key &b) {
291 return a.data_owner_code == b.data_owner_code
292 && a.organizational_unit_code == b.organizational_unit_code;
293}
294
295bool operator==(const Kv1HigherOrganizationalUnit::Key &a, const Kv1HigherOrganizationalUnit::Key &b) {
296 return a.data_owner_code == b.data_owner_code
297 && a.organizational_unit_code_parent == b.organizational_unit_code_parent
298 && a.organizational_unit_code_child == b.organizational_unit_code_child;
299}
300
301bool operator==(const Kv1UserStopPoint::Key &a, const Kv1UserStopPoint::Key &b) {
302 return a.data_owner_code == b.data_owner_code
303 && a.user_stop_code == b.user_stop_code;
304}
305
306bool operator==(const Kv1UserStopArea::Key &a, const Kv1UserStopArea::Key &b) {
307 return a.data_owner_code == b.data_owner_code
308 && a.user_stop_area_code == b.user_stop_area_code;
309}
310
311bool operator==(const Kv1TimingLink::Key &a, const Kv1TimingLink::Key &b) {
312 return a.data_owner_code == b.data_owner_code
313 && a.user_stop_code_begin == b.user_stop_code_begin
314 && a.user_stop_code_end == b.user_stop_code_end;
315}
316
317bool operator==(const Kv1Link::Key &a, const Kv1Link::Key &b) {
318 return a.data_owner_code == b.data_owner_code
319 && a.user_stop_code_begin == b.user_stop_code_begin
320 && a.user_stop_code_end == b.user_stop_code_end
321 && a.transport_type == b.transport_type;
322}
323
324bool operator==(const Kv1Line::Key &a, const Kv1Line::Key &b) {
325 return a.data_owner_code == b.data_owner_code
326 && a.line_planning_number == b.line_planning_number;
327}
328
329bool operator==(const Kv1Destination::Key &a, const Kv1Destination::Key &b) {
330 return a.data_owner_code == b.data_owner_code
331 && a.dest_code == b.dest_code;
332}
333
334bool operator==(const Kv1JourneyPattern::Key &a, const Kv1JourneyPattern::Key &b) {
335 return a.data_owner_code == b.data_owner_code
336 && a.line_planning_number == b.line_planning_number
337 && a.journey_pattern_code == b.journey_pattern_code;
338}
339
340bool operator==(const Kv1ConcessionFinancerRelation::Key &a, const Kv1ConcessionFinancerRelation::Key &b) {
341 return a.data_owner_code == b.data_owner_code
342 && a.con_fin_rel_code == b.con_fin_rel_code;
343}
344
345bool operator==(const Kv1ConcessionArea::Key &a, const Kv1ConcessionArea::Key &b) {
346 return a.data_owner_code == b.data_owner_code
347 && a.concession_area_code == b.concession_area_code;
348}
349
350bool operator==(const Kv1Financer::Key &a, const Kv1Financer::Key &b) {
351 return a.data_owner_code == b.data_owner_code
352 && a.financer_code == b.financer_code;
353}
354
355bool operator==(const Kv1JourneyPatternTimingLink::Key &a, const Kv1JourneyPatternTimingLink::Key &b) {
356 return a.data_owner_code == b.data_owner_code
357 && a.line_planning_number == b.line_planning_number
358 && a.journey_pattern_code == b.journey_pattern_code
359 && a.timing_link_order == b.timing_link_order;
360}
361
362bool operator==(const Kv1Point::Key &a, const Kv1Point::Key &b) {
363 return a.data_owner_code == b.data_owner_code
364 && a.point_code == b.point_code;
365}
366
367bool operator==(const Kv1PointOnLink::Key &a, const Kv1PointOnLink::Key &b) {
368 return a.data_owner_code == b.data_owner_code
369 && a.user_stop_code_begin == b.user_stop_code_begin
370 && a.user_stop_code_end == b.user_stop_code_end
371 && a.point_data_owner_code == b.point_data_owner_code
372 && a.point_code == b.point_code
373 && a.transport_type == b.transport_type;
374}
375
376bool operator==(const Kv1Icon::Key &a, const Kv1Icon::Key &b) {
377 return a.data_owner_code == b.data_owner_code
378 && a.icon_number == b.icon_number;
379}
380
381bool operator==(const Kv1Notice::Key &a, const Kv1Notice::Key &b) {
382 return a.data_owner_code == b.data_owner_code
383 && a.notice_code == b.notice_code;
384}
385
386bool operator==(const Kv1TimeDemandGroup::Key &a, const Kv1TimeDemandGroup::Key &b) {
387 return a.data_owner_code == b.data_owner_code
388 && a.line_planning_number == b.line_planning_number
389 && a.journey_pattern_code == b.journey_pattern_code
390 && a.time_demand_group_code == b.time_demand_group_code;
391}
392
393bool operator==(const Kv1TimeDemandGroupRunTime::Key &a, const Kv1TimeDemandGroupRunTime::Key &b) {
394 return a.data_owner_code == b.data_owner_code
395 && a.line_planning_number == b.line_planning_number
396 && a.journey_pattern_code == b.journey_pattern_code
397 && a.time_demand_group_code == b.time_demand_group_code
398 && a.timing_link_order == b.timing_link_order;
399}
400
401bool operator==(const Kv1PeriodGroup::Key &a, const Kv1PeriodGroup::Key &b) {
402 return a.data_owner_code == b.data_owner_code
403 && a.period_group_code == b.period_group_code;
404}
405
406bool operator==(const Kv1SpecificDay::Key &a, const Kv1SpecificDay::Key &b) {
407 return a.data_owner_code == b.data_owner_code
408 && a.specific_day_code == b.specific_day_code;
409}
410
411bool operator==(const Kv1TimetableVersion::Key &a, const Kv1TimetableVersion::Key &b) {
412 return a.data_owner_code == b.data_owner_code
413 && a.organizational_unit_code == b.organizational_unit_code
414 && a.timetable_version_code == b.timetable_version_code
415 && a.period_group_code == b.period_group_code
416 && a.specific_day_code == b.specific_day_code;
417}
418
419bool operator==(const Kv1PublicJourney::Key &a, const Kv1PublicJourney::Key &b) {
420 return a.data_owner_code == b.data_owner_code
421 && a.timetable_version_code == b.timetable_version_code
422 && a.organizational_unit_code == b.organizational_unit_code
423 && a.period_group_code == b.period_group_code
424 && a.specific_day_code == b.specific_day_code
425 && a.day_type == b.day_type
426 && a.line_planning_number == b.line_planning_number
427 && a.journey_number == b.journey_number;
428}
429
430bool operator==(const Kv1PeriodGroupValidity::Key &a, const Kv1PeriodGroupValidity::Key &b) {
431 return a.data_owner_code == b.data_owner_code
432 && a.organizational_unit_code == b.organizational_unit_code
433 && a.period_group_code == b.period_group_code
434 && a.valid_from == b.valid_from;
435}
436
437bool operator==(const Kv1ExceptionalOperatingDay::Key &a, const Kv1ExceptionalOperatingDay::Key &b) {
438 return a.data_owner_code == b.data_owner_code
439 && a.organizational_unit_code == b.organizational_unit_code
440 && a.valid_date == b.valid_date;
441}
442
443bool operator==(const Kv1ScheduleVersion::Key &a, const Kv1ScheduleVersion::Key &b) {
444 return a.data_owner_code == b.data_owner_code
445 && a.organizational_unit_code == b.organizational_unit_code
446 && a.schedule_code == b.schedule_code
447 && a.schedule_type_code == b.schedule_type_code;
448}
449
450bool operator==(const Kv1PublicJourneyPassingTimes::Key &a, const Kv1PublicJourneyPassingTimes::Key &b) {
451 return a.data_owner_code == b.data_owner_code
452 && a.organizational_unit_code == b.organizational_unit_code
453 && a.schedule_code == b.schedule_code
454 && a.schedule_type_code == b.schedule_type_code
455 && a.line_planning_number == b.line_planning_number
456 && a.journey_number == b.journey_number
457 && a.stop_order == b.stop_order;
458}
459
460bool operator==(const Kv1OperatingDay::Key &a, const Kv1OperatingDay::Key &b) {
461 return a.data_owner_code == b.data_owner_code
462 && a.organizational_unit_code == b.organizational_unit_code
463 && a.schedule_code == b.schedule_code
464 && a.schedule_type_code == b.schedule_type_code
465 && a.valid_date == b.valid_date;
466}
467
468namespace std::chrono {
469 static size_t hash_value(const year_month_day &ymd) {
470 size_t seed = 0;
471
472 boost::hash_combine(seed, int(ymd.year()));
473 boost::hash_combine(seed, unsigned(ymd.month()));
474 boost::hash_combine(seed, unsigned(ymd.day()));
475
476 return seed;
477 }
478
479 static size_t hash_value(const sys_seconds &s) {
480 return boost::hash<seconds::rep>()(s.time_since_epoch().count());
481 }
482}
483
484size_t hash_value(const Kv1OrganizationalUnit::Key &k) {
485 size_t seed = 0;
486
487 boost::hash_combine(seed, k.data_owner_code);
488 boost::hash_combine(seed, k.organizational_unit_code);
489
490 return seed;
491}
492
493size_t hash_value(const Kv1HigherOrganizationalUnit::Key &k) {
494 size_t seed = 0;
495
496 boost::hash_combine(seed, k.data_owner_code);
497 boost::hash_combine(seed, k.organizational_unit_code_parent);
498 boost::hash_combine(seed, k.organizational_unit_code_child);
499 boost::hash_combine(seed, k.valid_from);
500
501 return seed;
502}
503
504size_t hash_value(const Kv1UserStopPoint::Key &k) {
505 size_t seed = 0;
506
507 boost::hash_combine(seed, k.data_owner_code);
508 boost::hash_combine(seed, k.user_stop_code);
509
510 return seed;
511}
512
513size_t hash_value(const Kv1UserStopArea::Key &k) {
514 size_t seed = 0;
515
516 boost::hash_combine(seed, k.data_owner_code);
517 boost::hash_combine(seed, k.user_stop_area_code);
518
519 return seed;
520}
521
522size_t hash_value(const Kv1TimingLink::Key &k) {
523 size_t seed = 0;
524
525 boost::hash_combine(seed, k.data_owner_code);
526 boost::hash_combine(seed, k.user_stop_code_begin);
527 boost::hash_combine(seed, k.user_stop_code_end);
528
529 return seed;
530}
531
532size_t hash_value(const Kv1Link::Key &k) {
533 size_t seed = 0;
534
535 boost::hash_combine(seed, k.data_owner_code);
536 boost::hash_combine(seed, k.user_stop_code_begin);
537 boost::hash_combine(seed, k.user_stop_code_end);
538 boost::hash_combine(seed, k.transport_type);
539
540 return seed;
541}
542
543size_t hash_value(const Kv1Line::Key &k) {
544 size_t seed = 0;
545
546 boost::hash_combine(seed, k.data_owner_code);
547 boost::hash_combine(seed, k.line_planning_number);
548
549 return seed;
550}
551
552size_t hash_value(const Kv1Destination::Key &k) {
553 size_t seed = 0;
554
555 boost::hash_combine(seed, k.data_owner_code);
556 boost::hash_combine(seed, k.dest_code);
557
558 return seed;
559}
560
561size_t hash_value(const Kv1JourneyPattern::Key &k) {
562 size_t seed = 0;
563
564 boost::hash_combine(seed, k.data_owner_code);
565 boost::hash_combine(seed, k.line_planning_number);
566 boost::hash_combine(seed, k.journey_pattern_code);
567
568 return seed;
569}
570
571size_t hash_value(const Kv1ConcessionFinancerRelation::Key &k) {
572 size_t seed = 0;
573
574 boost::hash_combine(seed, k.data_owner_code);
575 boost::hash_combine(seed, k.con_fin_rel_code);
576
577 return seed;
578}
579
580size_t hash_value(const Kv1ConcessionArea::Key &k) {
581 size_t seed = 0;
582
583 boost::hash_combine(seed, k.data_owner_code);
584 boost::hash_combine(seed, k.concession_area_code);
585
586 return seed;
587}
588
589size_t hash_value(const Kv1Financer::Key &k) {
590 size_t seed = 0;
591
592 boost::hash_combine(seed, k.data_owner_code);
593 boost::hash_combine(seed, k.financer_code);
594
595 return seed;
596}
597
598size_t hash_value(const Kv1JourneyPatternTimingLink::Key &k) {
599 size_t seed = 0;
600
601 boost::hash_combine(seed, k.data_owner_code);
602 boost::hash_combine(seed, k.line_planning_number);
603 boost::hash_combine(seed, k.journey_pattern_code);
604 boost::hash_combine(seed, k.timing_link_order);
605
606 return seed;
607}
608
609size_t hash_value(const Kv1Point::Key &k) {
610 size_t seed = 0;
611
612 boost::hash_combine(seed, k.data_owner_code);
613 boost::hash_combine(seed, k.point_code);
614
615 return seed;
616}
617
618size_t hash_value(const Kv1PointOnLink::Key &k) {
619 size_t seed = 0;
620
621 boost::hash_combine(seed, k.data_owner_code);
622 boost::hash_combine(seed, k.user_stop_code_begin);
623 boost::hash_combine(seed, k.user_stop_code_end);
624 boost::hash_combine(seed, k.point_data_owner_code);
625 boost::hash_combine(seed, k.point_code);
626 boost::hash_combine(seed, k.transport_type);
627
628 return seed;
629}
630
631size_t hash_value(const Kv1Icon::Key &k) {
632 size_t seed = 0;
633
634 boost::hash_combine(seed, k.data_owner_code);
635 boost::hash_combine(seed, k.icon_number);
636
637 return seed;
638}
639
640size_t hash_value(const Kv1Notice::Key &k) {
641 size_t seed = 0;
642
643 boost::hash_combine(seed, k.data_owner_code);
644 boost::hash_combine(seed, k.notice_code);
645
646 return seed;
647}
648
649size_t hash_value(const Kv1TimeDemandGroup::Key &k) {
650 size_t seed = 0;
651
652 boost::hash_combine(seed, k.data_owner_code);
653 boost::hash_combine(seed, k.line_planning_number);
654 boost::hash_combine(seed, k.journey_pattern_code);
655 boost::hash_combine(seed, k.time_demand_group_code);
656
657 return seed;
658}
659
660size_t hash_value(const Kv1TimeDemandGroupRunTime::Key &k) {
661 size_t seed = 0;
662
663 boost::hash_combine(seed, k.data_owner_code);
664 boost::hash_combine(seed, k.line_planning_number);
665 boost::hash_combine(seed, k.journey_pattern_code);
666 boost::hash_combine(seed, k.time_demand_group_code);
667 boost::hash_combine(seed, k.timing_link_order);
668
669 return seed;
670}
671
672size_t hash_value(const Kv1PeriodGroup::Key &k) {
673 size_t seed = 0;
674
675 boost::hash_combine(seed, k.data_owner_code);
676 boost::hash_combine(seed, k.period_group_code);
677
678 return seed;
679}
680
681size_t hash_value(const Kv1SpecificDay::Key &k) {
682 size_t seed = 0;
683
684 boost::hash_combine(seed, k.data_owner_code);
685 boost::hash_combine(seed, k.specific_day_code);
686
687 return seed;
688}
689
690size_t hash_value(const Kv1TimetableVersion::Key &k) {
691 size_t seed = 0;
692
693 boost::hash_combine(seed, k.data_owner_code);
694 boost::hash_combine(seed, k.organizational_unit_code);
695 boost::hash_combine(seed, k.timetable_version_code);
696 boost::hash_combine(seed, k.period_group_code);
697 boost::hash_combine(seed, k.specific_day_code);
698
699 return seed;
700}
701
702size_t hash_value(const Kv1PublicJourney::Key &k) {
703 size_t seed = 0;
704
705 boost::hash_combine(seed, k.data_owner_code);
706 boost::hash_combine(seed, k.timetable_version_code);
707 boost::hash_combine(seed, k.organizational_unit_code);
708 boost::hash_combine(seed, k.period_group_code);
709 boost::hash_combine(seed, k.specific_day_code);
710 boost::hash_combine(seed, k.day_type);
711 boost::hash_combine(seed, k.line_planning_number);
712 boost::hash_combine(seed, k.journey_number);
713
714 return seed;
715}
716
717size_t hash_value(const Kv1PeriodGroupValidity::Key &k) {
718 size_t seed = 0;
719
720 boost::hash_combine(seed, k.data_owner_code);
721 boost::hash_combine(seed, k.organizational_unit_code);
722 boost::hash_combine(seed, k.period_group_code);
723 boost::hash_combine(seed, k.valid_from);
724
725 return seed;
726}
727
728size_t hash_value(const Kv1ExceptionalOperatingDay::Key &k) {
729 size_t seed = 0;
730
731 boost::hash_combine(seed, k.data_owner_code);
732 boost::hash_combine(seed, k.organizational_unit_code);
733 boost::hash_combine(seed, k.valid_date);
734
735 return seed;
736}
737
738size_t hash_value(const Kv1ScheduleVersion::Key &k) {
739 size_t seed = 0;
740
741 boost::hash_combine(seed, k.data_owner_code);
742 boost::hash_combine(seed, k.organizational_unit_code);
743 boost::hash_combine(seed, k.schedule_code);
744 boost::hash_combine(seed, k.schedule_type_code);
745
746 return seed;
747}
748
749size_t hash_value(const Kv1PublicJourneyPassingTimes::Key &k) {
750 size_t seed = 0;
751
752 boost::hash_combine(seed, k.data_owner_code);
753 boost::hash_combine(seed, k.organizational_unit_code);
754 boost::hash_combine(seed, k.schedule_code);
755 boost::hash_combine(seed, k.schedule_type_code);
756 boost::hash_combine(seed, k.line_planning_number);
757 boost::hash_combine(seed, k.journey_number);
758 boost::hash_combine(seed, k.stop_order);
759
760 return seed;
761}
762
763size_t hash_value(const Kv1OperatingDay::Key &k) {
764 size_t seed = 0;
765
766 boost::hash_combine(seed, k.data_owner_code);
767 boost::hash_combine(seed, k.organizational_unit_code);
768 boost::hash_combine(seed, k.schedule_code);
769 boost::hash_combine(seed, k.schedule_type_code);
770 boost::hash_combine(seed, k.valid_date);
771
772 return seed;
773}
diff --git a/lib/libtmi8/src/kv6_parquet.cpp b/lib/libtmi8/src/kv6_parquet.cpp
new file mode 100644
index 0000000..ca70b7f
--- /dev/null
+++ b/lib/libtmi8/src/kv6_parquet.cpp
@@ -0,0 +1,102 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv6_parquet.hpp>
4
5ParquetBuilder::ParquetBuilder() {
6 std::shared_ptr<arrow::Field> field_type, field_data_owner_code, field_line_planning_number, field_operating_day,
7 field_journey_number, field_reinforcement_number, field_timestamp, field_source,
8 field_punctuality, field_user_stop_code, field_passage_sequence_number,
9 field_vehicle_number, field_block_code, field_wheelchair_accessible,
10 field_number_of_coaches, field_rd_y, field_rd_x, field_distance_since_last_user_stop;
11 field_type = arrow::field("type", arrow::utf8());
12 field_data_owner_code = arrow::field("data_owner_code", arrow::utf8());
13 field_line_planning_number = arrow::field("line_planning_number", arrow::utf8());
14 field_operating_day = arrow::field("operating_day", arrow::date32());
15 field_journey_number = arrow::field("journey_number", arrow::uint32());
16 field_reinforcement_number = arrow::field("reinforcement_number", arrow::uint8());
17 field_timestamp = arrow::field("timestamp", arrow::timestamp(arrow::TimeUnit::SECOND));
18 field_source = arrow::field("source", arrow::utf8());
19 field_punctuality = arrow::field("punctuality", arrow::int16());
20 field_user_stop_code = arrow::field("user_stop_code", arrow::utf8());
21 field_passage_sequence_number = arrow::field("passage_sequence_number", arrow::uint16());
22 field_vehicle_number = arrow::field("vehicle_number", arrow::uint32());
23 field_block_code = arrow::field("block_code", arrow::uint32());
24 field_wheelchair_accessible = arrow::field("wheelchair_accessible", arrow::utf8());
25 field_number_of_coaches = arrow::field("number_of_coaches", arrow::uint8());
26 field_rd_y = arrow::field("rd_y", arrow::int32());
27 field_rd_x = arrow::field("rd_x", arrow::int32());
28 field_distance_since_last_user_stop = arrow::field("distance_since_last_user_stop", arrow::uint32());
29
30 schema = arrow::schema({ field_type, field_data_owner_code, field_line_planning_number,
31 field_operating_day, field_journey_number,
32 field_reinforcement_number, field_timestamp, field_source,
33 field_punctuality, field_user_stop_code,
34 field_passage_sequence_number, field_vehicle_number,
35 field_block_code, field_wheelchair_accessible,
36 field_number_of_coaches, field_rd_y, field_rd_x,
37 field_distance_since_last_user_stop });
38}
39
40arrow::Result<std::shared_ptr<arrow::Table>> ParquetBuilder::getTable() {
41 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> types, types.Finish());
42 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> data_owner_codes, data_owner_codes.Finish());
43 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> line_planning_numbers, line_planning_numbers.Finish());
44 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> operating_days, operating_days.Finish());
45 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> journey_numbers, journey_numbers.Finish());
46 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> reinforcement_numbers, reinforcement_numbers.Finish());
47 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> timestamps, timestamps.Finish());
48 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> sources, sources.Finish());
49 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> punctualities, punctualities.Finish());
50 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> user_stop_codes, user_stop_codes.Finish());
51 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> passage_sequence_numbers, passage_sequence_numbers.Finish());
52 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> vehicle_numbers, vehicle_numbers.Finish());
53 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> block_codes, block_codes.Finish());
54 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> wheelchair_accessibles, wheelchair_accessibles.Finish());
55 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> number_of_coaches, number_of_coaches.Finish());
56 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> rd_ys, rd_ys.Finish());
57 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> rd_xs, rd_xs.Finish());
58 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> distance_since_last_user_stops, distance_since_last_user_stops.Finish());
59
60 std::vector<std::shared_ptr<arrow::Array>> columns = { types, data_owner_codes, line_planning_numbers, operating_days,
61 journey_numbers, reinforcement_numbers, timestamps, sources,
62 punctualities, user_stop_codes, passage_sequence_numbers,
63 vehicle_numbers, block_codes, wheelchair_accessibles,
64 number_of_coaches, rd_ys, rd_xs,
65 distance_since_last_user_stops };
66 return arrow::Result(arrow::Table::Make(schema, columns));
67}
68
69arrow::Status writeArrowRecordsAsParquetFile(arrow::RecordBatchReader &rbr, std::filesystem::path filename) {
70 std::shared_ptr<parquet::WriterProperties> props = parquet::WriterProperties::Builder()
71 .compression(arrow::Compression::ZSTD)
72 ->created_by("oeuf-libtmi8")
73 ->version(parquet::ParquetVersion::PARQUET_2_6)
74 ->data_page_version(parquet::ParquetDataPageVersion::V2)
75 ->max_row_group_length(MAX_PARQUET_CHUNK)
76 ->build();
77
78 std::shared_ptr<parquet::ArrowWriterProperties> arrow_props = parquet::ArrowWriterProperties::Builder()
79 .store_schema()->build();
80
81 std::shared_ptr<arrow::io::FileOutputStream> out_file;
82 std::string filename_str = filename;
83 ARROW_ASSIGN_OR_RAISE(out_file, arrow::io::FileOutputStream::Open(filename_str + ".part"));
84
85 ARROW_ASSIGN_OR_RAISE(auto writer,
86 parquet::arrow::FileWriter::Open(*rbr.schema(), arrow::default_memory_pool(), out_file, props, arrow_props));
87 for (const auto &batchr : rbr) {
88 ARROW_ASSIGN_OR_RAISE(auto batch, batchr);
89 ARROW_RETURN_NOT_OK(writer->WriteRecordBatch(*batch));
90 }
91 ARROW_RETURN_NOT_OK(writer->Close());
92 ARROW_RETURN_NOT_OK(out_file->Close());
93
94 std::filesystem::rename(filename_str + ".part", filename);
95
96 return arrow::Status::OK();
97}
98
99arrow::Status writeArrowTableAsParquetFile(const arrow::Table &table, std::filesystem::path filename) {
100 auto tbr = arrow::TableBatchReader(table);
101 return writeArrowRecordsAsParquetFile(tbr, filename);
102}