Skip to main content

hrdf_parser/
models.rs

1use std::{
2    collections::BTreeSet,
3    hash::{DefaultHasher, Hash, Hasher},
4};
5
6use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
7use rustc_hash::FxHashMap;
8use serde::{Deserialize, Serialize};
9use strum_macros::{self, Display, EnumString};
10
11use thiserror::Error;
12
13use crate::{
14    error::{HResult, HrdfError},
15    storage::DataStorage,
16    utils::{add_1_day, sub_1_day},
17};
18
19pub(crate) type JourneyId = (i32, String); // (legacy_id, administration)
20
21// ------------------------------------------------------------------------------------------------
22// --- Model
23// ------------------------------------------------------------------------------------------------
24
25pub trait Model<M: Model<M>> {
26    // Primary key type.
27    type K: Copy + Eq + Hash + Serialize + for<'a> Deserialize<'a>;
28
29    fn id(&self) -> M::K;
30}
31
32macro_rules! impl_Model {
33    ($m:ty) => {
34        impl Model<$m> for $m {
35            type K = i32;
36
37            fn id(&self) -> Self::K {
38                self.id
39            }
40        }
41    };
42}
43
44// ------------------------------------------------------------------------------------------------
45// --- Attribute
46// ------------------------------------------------------------------------------------------------
47
48#[derive(Debug, Serialize, Deserialize)]
49pub struct Attribute {
50    id: i32,
51    designation: String,
52    stop_scope: i16,
53    main_sorting_priority: i16,
54    secondary_sorting_priority: i16,
55    description: FxHashMap<Language, String>,
56}
57
58impl_Model!(Attribute);
59
60impl Attribute {
61    pub fn new(
62        id: i32,
63        designation: String,
64        stop_scope: i16,
65        main_sorting_priority: i16,
66        secondary_sorting_priority: i16,
67    ) -> Self {
68        Self {
69            id,
70            designation,
71            stop_scope,
72            main_sorting_priority,
73            secondary_sorting_priority,
74            description: FxHashMap::default(),
75        }
76    }
77
78    // Getters/Setters
79
80    pub fn set_description(&mut self, language: Language, value: &str) {
81        self.description.insert(language, value.to_string());
82    }
83}
84
85// ------------------------------------------------------------------------------------------------
86// --- BitField
87// ------------------------------------------------------------------------------------------------
88
89#[derive(Debug, Serialize, Deserialize)]
90pub struct BitField {
91    id: i32,
92    bits: Vec<u8>,
93}
94
95impl_Model!(BitField);
96
97impl BitField {
98    pub fn new(id: i32, bits: Vec<u8>) -> Self {
99        Self { id, bits }
100    }
101
102    // Getters/Setters
103
104    pub fn bits(&self) -> &Vec<u8> {
105        &self.bits
106    }
107}
108
109// ------------------------------------------------------------------------------------------------
110// --- Color
111// ------------------------------------------------------------------------------------------------
112
113#[derive(Debug, Default, Serialize, Deserialize)]
114pub struct Color {
115    r: i16,
116    g: i16,
117    b: i16,
118}
119
120#[allow(unused)]
121impl Color {
122    pub fn new(r: i16, g: i16, b: i16) -> Self {
123        Self { r, g, b }
124    }
125
126    // Getters/Setters
127
128    pub fn r(&self) -> i16 {
129        self.r
130    }
131
132    pub fn g(&self) -> i16 {
133        self.g
134    }
135
136    pub fn b(&self) -> i16 {
137        self.b
138    }
139}
140
141// ------------------------------------------------------------------------------------------------
142// --- CoordinateSystem
143// ------------------------------------------------------------------------------------------------
144
145#[derive(Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
146pub enum CoordinateSystem {
147    #[default]
148    LV95,
149    WGS84,
150}
151
152// ------------------------------------------------------------------------------------------------
153// --- Coordinates
154// ------------------------------------------------------------------------------------------------
155
156#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
157pub struct Coordinates {
158    coordinate_system: CoordinateSystem,
159    x: f64,
160    y: f64,
161}
162
163#[allow(unused)]
164impl Coordinates {
165    pub fn new(coordinate_system: CoordinateSystem, x: f64, y: f64) -> Self {
166        Self {
167            coordinate_system,
168            x,
169            y,
170        }
171    }
172
173    // Getters/Setters
174
175    pub fn easting(&self) -> Option<f64> {
176        match self.coordinate_system {
177            CoordinateSystem::LV95 => Some(self.x),
178            CoordinateSystem::WGS84 => None,
179        }
180    }
181
182    pub fn northing(&self) -> Option<f64> {
183        match self.coordinate_system {
184            CoordinateSystem::LV95 => Some(self.y),
185            CoordinateSystem::WGS84 => None,
186        }
187    }
188
189    pub fn latitude(&self) -> Option<f64> {
190        match self.coordinate_system {
191            CoordinateSystem::WGS84 => Some(self.x),
192            CoordinateSystem::LV95 => None,
193        }
194    }
195
196    pub fn longitude(&self) -> Option<f64> {
197        match self.coordinate_system {
198            CoordinateSystem::WGS84 => Some(self.y),
199            CoordinateSystem::LV95 => None,
200        }
201    }
202}
203
204// ------------------------------------------------------------------------------------------------
205// --- Direction
206// ------------------------------------------------------------------------------------------------
207
208#[derive(Debug, Serialize, Deserialize)]
209pub struct Direction {
210    id: i32,
211    name: String,
212}
213
214impl_Model!(Direction);
215
216impl Direction {
217    pub fn new(id: i32, name: String) -> Self {
218        Self { id, name }
219    }
220}
221
222// ------------------------------------------------------------------------------------------------
223// --- DirectionType
224// ------------------------------------------------------------------------------------------------
225
226#[derive(
227    Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, EnumString, Serialize, Deserialize,
228)]
229pub enum DirectionType {
230    #[default]
231    #[strum(serialize = "R")]
232    Outbound,
233
234    #[strum(serialize = "H")]
235    Return,
236}
237
238// ------------------------------------------------------------------------------------------------
239// --- Holiday
240// ------------------------------------------------------------------------------------------------
241
242#[derive(Debug, Serialize, Deserialize)]
243pub struct Holiday {
244    id: i32,
245    date: NaiveDate,
246    name: FxHashMap<Language, String>,
247}
248
249impl_Model!(Holiday);
250
251impl Holiday {
252    pub fn new(id: i32, date: NaiveDate, name: FxHashMap<Language, String>) -> Self {
253        Self { id, date, name }
254    }
255}
256
257// ------------------------------------------------------------------------------------------------
258// --- ExchangeTimeAdministration
259// ------------------------------------------------------------------------------------------------
260
261#[derive(Debug, Serialize, Deserialize)]
262pub struct ExchangeTimeAdministration {
263    id: i32,
264    stop_id: Option<i32>, // A None value means that the exchange time applies to all stops if there is no specific entry for the stop and the 2 administrations.
265    administration_1: String,
266    administration_2: String,
267    duration: i16, // Exchange time from administration 1 to administration 2 is in minutes.
268}
269
270impl_Model!(ExchangeTimeAdministration);
271
272impl ExchangeTimeAdministration {
273    pub fn new(
274        id: i32,
275        stop_id: Option<i32>,
276        administration_1: String,
277        administration_2: String,
278        duration: i16,
279    ) -> Self {
280        Self {
281            id,
282            stop_id,
283            administration_1,
284            administration_2,
285            duration,
286        }
287    }
288
289    // Getters/Setters
290
291    pub fn stop_id(&self) -> Option<i32> {
292        self.stop_id
293    }
294
295    pub fn administration_1(&self) -> &str {
296        &self.administration_1
297    }
298
299    pub fn administration_2(&self) -> &str {
300        &self.administration_2
301    }
302
303    pub fn duration(&self) -> i16 {
304        self.duration
305    }
306}
307
308// ------------------------------------------------------------------------------------------------
309// --- ExchangeTimeJourney
310// ------------------------------------------------------------------------------------------------
311
312#[derive(Debug, Serialize, Deserialize)]
313pub struct ExchangeTimeJourney {
314    id: i32,
315    stop_id: i32,
316    journey_legacy_id_1: i32,
317    administration_1: String,
318    journey_legacy_id_2: i32,
319    administration_2: String,
320    duration: i16, // Exchange time from journey 1 to journey 2 is in minutes.
321    is_guaranteed: bool,
322    bit_field_id: Option<i32>,
323}
324
325impl_Model!(ExchangeTimeJourney);
326
327impl ExchangeTimeJourney {
328    pub fn new(
329        id: i32,
330        stop_id: i32,
331        (journey_legacy_id_1, administration_1): JourneyId,
332        (journey_legacy_id_2, administration_2): JourneyId,
333        duration: i16,
334        is_guaranteed: bool,
335        bit_field_id: Option<i32>,
336    ) -> Self {
337        Self {
338            id,
339            stop_id,
340            journey_legacy_id_1,
341            administration_1,
342            journey_legacy_id_2,
343            administration_2,
344            duration,
345            is_guaranteed,
346            bit_field_id,
347        }
348    }
349
350    // Getters/Setters
351
352    pub fn stop_id(&self) -> i32 {
353        self.stop_id
354    }
355
356    pub fn journey_legacy_id_1(&self) -> i32 {
357        self.journey_legacy_id_1
358    }
359
360    pub fn administration_1(&self) -> &str {
361        &self.administration_1
362    }
363
364    pub fn journey_legacy_id_2(&self) -> i32 {
365        self.journey_legacy_id_2
366    }
367
368    pub fn administration_2(&self) -> &str {
369        &self.administration_2
370    }
371
372    pub fn duration(&self) -> i16 {
373        self.duration
374    }
375
376    pub fn bit_field_id(&self) -> Option<i32> {
377        self.bit_field_id
378    }
379}
380
381// ------------------------------------------------------------------------------------------------
382// --- ExchangeTimeLine
383// ------------------------------------------------------------------------------------------------
384
385#[derive(Debug, Serialize, Deserialize)]
386pub struct ExchangeTimeLine {
387    id: i32,
388    stop_id: Option<i32>,
389    line_1: LineInfo,
390    line_2: LineInfo,
391    duration: i16, // Exchange time from line 1 to line 2 is in minutes.
392    is_guaranteed: bool,
393}
394
395impl_Model!(ExchangeTimeLine);
396
397#[derive(Debug, Serialize, Deserialize)]
398pub(crate) struct LineInfo {
399    administration: String,
400    transport_type_id: i32,
401    line_id: Option<String>,
402    direction: Option<DirectionType>,
403}
404
405impl LineInfo {
406    pub(crate) fn new(
407        administration: String,
408        transport_type_id: i32,
409        line_id: Option<String>,
410        direction: Option<DirectionType>,
411    ) -> Self {
412        Self {
413            administration,
414            transport_type_id,
415            line_id,
416            direction,
417        }
418    }
419}
420
421impl ExchangeTimeLine {
422    pub(crate) fn new(
423        id: i32,
424        stop_id: Option<i32>,
425        line_1: LineInfo,
426        line_2: LineInfo,
427        duration: i16,
428        is_guaranteed: bool,
429    ) -> Self {
430        Self {
431            id,
432            stop_id,
433            line_1,
434            line_2,
435            duration,
436            is_guaranteed,
437        }
438    }
439}
440
441// ------------------------------------------------------------------------------------------------
442// --- InformationText
443// ------------------------------------------------------------------------------------------------
444
445#[derive(Debug, Serialize, Deserialize)]
446pub struct InformationText {
447    id: i32,
448    content: FxHashMap<Language, String>,
449}
450
451impl_Model!(InformationText);
452
453impl InformationText {
454    pub fn new(id: i32) -> Self {
455        Self {
456            id,
457            content: FxHashMap::default(),
458        }
459    }
460
461    // Getters/Setters
462
463    pub fn set_content(&mut self, language: Language, value: &str) {
464        self.content.insert(language, value.to_string());
465    }
466}
467
468// ------------------------------------------------------------------------------------------------
469// --- Journey
470// ------------------------------------------------------------------------------------------------
471
472#[derive(Debug, Default, Serialize, Deserialize)]
473pub struct Journey {
474    id: i32,
475    legacy_id: i32,
476    administration: String,
477    metadata: FxHashMap<JourneyMetadataType, Vec<JourneyMetadataEntry>>,
478    route: Vec<JourneyRouteEntry>,
479}
480
481impl_Model!(Journey);
482
483impl Journey {
484    pub fn new(id: i32, legacy_id: i32, administration: String) -> Self {
485        Self {
486            id,
487            legacy_id,
488            administration,
489            metadata: FxHashMap::default(),
490            route: Vec::new(),
491        }
492    }
493
494    // Getters/Setters
495
496    pub fn administration(&self) -> &str {
497        &self.administration
498    }
499
500    pub fn legacy_id(&self) -> i32 {
501        self.legacy_id
502    }
503
504    fn metadata(&self) -> &FxHashMap<JourneyMetadataType, Vec<JourneyMetadataEntry>> {
505        &self.metadata
506    }
507
508    pub fn route(&self) -> &Vec<JourneyRouteEntry> {
509        &self.route
510    }
511
512    // Functions
513
514    pub fn add_metadata_entry(&mut self, k: JourneyMetadataType, v: JourneyMetadataEntry) {
515        self.metadata.entry(k).or_default().push(v);
516    }
517
518    pub fn add_route_entry(&mut self, entry: JourneyRouteEntry) {
519        self.route.push(entry);
520    }
521
522    pub(crate) fn bit_field_id(&self) -> JResult<Option<i32>> {
523        let entry = self
524            .metadata()
525            .get(&JourneyMetadataType::BitField)
526            .ok_or(JourneyError::MissingBitFieldMetadata)?;
527
528        Ok(entry
529            .first()
530            .ok_or(JourneyError::EmptyJourneyMetadata)?
531            .bit_field_id)
532    }
533
534    pub fn transport_type_id(&self) -> HResult<i32> {
535        let entry = self
536            .metadata()
537            .get(&JourneyMetadataType::TransportType)
538            .ok_or(JourneyError::MissingTransportType)?;
539        entry
540            .first()
541            .ok_or::<HrdfError>((JourneyError::EmptyJourneyMetadata).into())?
542            .resource_id
543            .ok_or(JourneyError::MissingRessourceId.into())
544    }
545
546    pub fn transport_type<'a>(
547        &'a self,
548        data_storage: &'a DataStorage,
549    ) -> HResult<&'a TransportType> {
550        let transport_id = self.transport_type_id()?;
551        data_storage
552            .transport_types()
553            .find(transport_id)
554            .ok_or(JourneyError::TransportIdNotFound(transport_id).into())
555    }
556
557    pub fn first_stop_id(&self) -> HResult<i32> {
558        Ok(self
559            .route
560            .first()
561            .ok_or(JourneyError::EmptyRoute)?
562            .stop_id())
563    }
564
565    pub fn is_first_stop(&self, stop_id: i32, ignore_loop: bool) -> HResult<bool> {
566        if ignore_loop && self.first_stop_id()? == self.last_stop_id()? {
567            Ok(false)
568        } else {
569            Ok(stop_id == self.first_stop_id()?)
570        }
571    }
572
573    pub fn last_stop_id(&self) -> HResult<i32> {
574        Ok(self.route.last().ok_or(JourneyError::EmptyRoute)?.stop_id())
575    }
576
577    pub fn is_last_stop(&self, stop_id: i32, ignore_loop: bool) -> HResult<bool> {
578        if ignore_loop && self.first_stop_id()? == self.last_stop_id()? {
579            Ok(false)
580        } else {
581            Ok(stop_id == self.last_stop_id()?)
582        }
583    }
584
585    pub fn count_stops(&self, departure_stop_id: i32, arrival_stop_id: i32) -> usize {
586        self.route()
587            .iter()
588            .skip_while(|stop| stop.stop_id() != departure_stop_id)
589            .take_while(|stop| stop.stop_id() != arrival_stop_id)
590            .count()
591            + 1
592    }
593
594    pub fn hash_route(&self, departure_stop_id: i32) -> Option<u64> {
595        let index = self
596            .route
597            .iter()
598            .position(|route_entry| route_entry.stop_id() == departure_stop_id)?;
599
600        let mut hasher = DefaultHasher::new();
601        self.route
602            .iter()
603            .skip(index)
604            .map(|route_entry| route_entry.stop_id())
605            .collect::<BTreeSet<_>>()
606            .hash(&mut hasher);
607        Some(hasher.finish())
608    }
609
610    /// unwrap: Do not call this function if the stop is not part of the route.
611    /// unwrap: Do not call this function if the stop has no departure time (only the last stop has no departure time).
612    pub fn departure_time_of(&self, stop_id: i32) -> HResult<(NaiveTime, bool)> {
613        let route = self.route();
614        let index = route
615            .iter()
616            .position(|route_entry| route_entry.stop_id() == stop_id)
617            .ok_or_else(|| HrdfError::MissingStopId(stop_id))?;
618        let departure_time = route[index]
619            .departure_time()
620            .ok_or_else(|| HrdfError::MissingDepartureTime(index))?;
621
622        Ok((
623            departure_time,
624            // The departure time is on the next day if this evaluates to true.
625            departure_time
626                < route
627                    .first()
628                    .ok_or(HrdfError::MissingRoute)?
629                    .departure_time()
630                    .ok_or(HrdfError::MissingDepartureTime(0))?,
631        ))
632    }
633
634    /// The date must correspond to the route's first entry.
635    /// Do not call this function if the stop is not part of the route.
636    /// Do not call this function if the stop has no departure time (only the last stop has no departure time).
637    pub fn departure_at_of(&self, stop_id: i32, date: NaiveDate) -> HResult<NaiveDateTime> {
638        match self.departure_time_of(stop_id)? {
639            (departure_time, false) => Ok(NaiveDateTime::new(date, departure_time)),
640            (departure_time, true) => Ok(NaiveDateTime::new(add_1_day(date)?, departure_time)),
641        }
642    }
643
644    /// The date must be associated with the origin_stop_id.
645    /// Do not call this function if the stop is not part of the route.
646    pub fn departure_at_of_with_origin(
647        &self,
648        stop_id: i32,
649        date: NaiveDate,
650        // If it's not a departure date, it's an arrival date.
651        is_departure_date: bool,
652        origin_stop_id: i32,
653    ) -> HResult<NaiveDateTime> {
654        let (departure_time, is_next_day) = self.departure_time_of(stop_id)?;
655        let (_, origin_is_next_day) = if is_departure_date {
656            self.departure_time_of(origin_stop_id)?
657        } else {
658            self.arrival_time_of(origin_stop_id)?
659        };
660
661        match (is_next_day, origin_is_next_day) {
662            (true, false) => Ok(NaiveDateTime::new(add_1_day(date)?, departure_time)),
663            (false, true) => Ok(NaiveDateTime::new(sub_1_day(date)?, departure_time)),
664            _ => Ok(NaiveDateTime::new(date, departure_time)),
665        }
666    }
667
668    /// The date must correspond to the route's first entry.
669    /// Do not call this function if the stop is not part of the route.
670    /// Do not call this function if the stop has no arrival time (only the first stop has no arrival time).
671    pub fn arrival_at_of(&self, stop_id: i32, date: NaiveDate) -> HResult<NaiveDateTime> {
672        match self.arrival_time_of(stop_id)? {
673            (arrival_time, false) => Ok(NaiveDateTime::new(date, arrival_time)),
674            (arrival_time, true) => Ok(NaiveDateTime::new(add_1_day(date)?, arrival_time)),
675        }
676    }
677
678    pub fn arrival_time_of(&self, stop_id: i32) -> HResult<(NaiveTime, bool)> {
679        let route = self.route();
680        let index = route
681            .iter()
682            // The first route entry has no arrival time.
683            .skip(1)
684            .position(|route_entry| route_entry.stop_id() == stop_id)
685            .map(|i| i + 1)
686            .ok_or_else(|| HrdfError::MissingStopId(stop_id))?;
687        let arrival_time = route[index]
688            .arrival_time()
689            .ok_or_else(|| HrdfError::MissingArrivalTime(index))?;
690
691        Ok((
692            arrival_time,
693            // The arrival time is on the next day if this evaluates to true.
694            arrival_time
695                < route
696                    .first()
697                    .ok_or(HrdfError::MissingRoute)?
698                    .departure_time()
699                    .ok_or(HrdfError::MissingDepartureTime(0))?,
700        ))
701    }
702
703    /// The date must be associated with the origin_stop_id.
704    pub fn arrival_at_of_with_origin(
705        &self,
706        stop_id: i32,
707        date: NaiveDate,
708        // If it's not a departure date, it's an arrival date.
709        is_departure_date: bool,
710        origin_stop_id: i32,
711    ) -> HResult<NaiveDateTime> {
712        let (arrival_time, is_next_day) = self.arrival_time_of(stop_id)?;
713        let (_, origin_is_next_day) = if is_departure_date {
714            self.departure_time_of(origin_stop_id)?
715        } else {
716            self.arrival_time_of(origin_stop_id)?
717        };
718
719        match (is_next_day, origin_is_next_day) {
720            (true, false) => Ok(NaiveDateTime::new(add_1_day(date)?, arrival_time)),
721            (false, true) => Ok(NaiveDateTime::new(sub_1_day(date)?, arrival_time)),
722            _ => Ok(NaiveDateTime::new(date, arrival_time)),
723        }
724    }
725
726    /// Excluding departure stop.
727    pub fn route_section(
728        &self,
729        departure_stop_id: i32,
730        arrival_stop_id: i32,
731    ) -> Vec<&JourneyRouteEntry> {
732        let mut route_iter = self.route().iter();
733
734        for route_entry in route_iter.by_ref() {
735            if route_entry.stop_id() == departure_stop_id {
736                break;
737            }
738        }
739
740        let mut result = Vec::new();
741
742        for route_entry in route_iter {
743            result.push(route_entry);
744
745            if route_entry.stop_id() == arrival_stop_id {
746                break;
747            }
748        }
749
750        result
751    }
752}
753
754type JResult<T> = Result<T, JourneyError>;
755
756#[derive(Debug, Error)]
757pub enum JourneyError {
758    #[error("Missing MitField Metadata")]
759    MissingBitFieldMetadata,
760    #[error("JourneyMetaData is empty")]
761    EmptyJourneyMetadata,
762    #[error("Missing Transport Type Metadata")]
763    MissingTransportType,
764    #[error("Missing Reoussirce Id")]
765    MissingRessourceId,
766    #[error("Transport Id: {0} not found")]
767    TransportIdNotFound(i32),
768    #[error("Empty Route")]
769    EmptyRoute,
770    #[error("Stop Id: {0} not found")]
771    StopIdNotFound(i32),
772}
773
774// ------------------------------------------------------------------------------------------------
775// --- JourneyMetadataType
776// ------------------------------------------------------------------------------------------------
777
778#[derive(Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
779pub enum JourneyMetadataType {
780    #[default]
781    Attribute,
782    BitField,
783    Direction,
784    InformationText,
785    Line,
786    ExchangeTimeBoarding,
787    ExchangeTimeDisembarking,
788    TransportType,
789}
790
791// ------------------------------------------------------------------------------------------------
792// --- JourneyMetadataEntry
793// ------------------------------------------------------------------------------------------------
794
795#[derive(Debug, Serialize, Deserialize)]
796pub struct JourneyMetadataEntry {
797    from_stop_id: Option<i32>,
798    until_stop_id: Option<i32>,
799    resource_id: Option<i32>,
800    bit_field_id: Option<i32>,
801    departure_time: Option<NaiveTime>,
802    arrival_time: Option<NaiveTime>,
803    extra_field_1: Option<String>,
804    extra_field_2: Option<i32>,
805}
806
807impl JourneyMetadataEntry {
808    #[allow(clippy::too_many_arguments)]
809    pub fn new(
810        from_stop_id: Option<i32>,
811        until_stop_id: Option<i32>,
812        resource_id: Option<i32>,
813        bit_field_id: Option<i32>,
814        departure_time: Option<NaiveTime>,
815        arrival_time: Option<NaiveTime>,
816        extra_field_1: Option<String>,
817        extra_field_2: Option<i32>,
818    ) -> Self {
819        Self {
820            from_stop_id,
821            until_stop_id,
822            resource_id,
823            bit_field_id,
824            departure_time,
825            arrival_time,
826            extra_field_1,
827            extra_field_2,
828        }
829    }
830}
831
832// ------------------------------------------------------------------------------------------------
833// --- JourneyRouteEntry
834// ------------------------------------------------------------------------------------------------
835
836#[derive(Debug, Serialize, Deserialize)]
837pub struct JourneyRouteEntry {
838    stop_id: i32,
839    arrival_time: Option<NaiveTime>,
840    departure_time: Option<NaiveTime>,
841}
842
843impl JourneyRouteEntry {
844    pub fn new(
845        stop_id: i32,
846        arrival_time: Option<NaiveTime>,
847        departure_time: Option<NaiveTime>,
848    ) -> Self {
849        Self {
850            stop_id,
851            arrival_time,
852            departure_time,
853        }
854    }
855
856    // Getters/Setters
857
858    pub fn stop_id(&self) -> i32 {
859        self.stop_id
860    }
861
862    pub fn arrival_time(&self) -> &Option<NaiveTime> {
863        &self.arrival_time
864    }
865
866    pub fn departure_time(&self) -> &Option<NaiveTime> {
867        &self.departure_time
868    }
869
870    // Functions
871
872    pub fn stop<'a>(&'a self, data_storage: &'a DataStorage) -> HResult<&'a Stop> {
873        let stop_id = self.stop_id();
874        data_storage
875            .stops()
876            .find(stop_id)
877            .ok_or(JourneyError::StopIdNotFound(stop_id).into())
878    }
879}
880
881// ------------------------------------------------------------------------------------------------
882// --- JourneyPlatform
883// ------------------------------------------------------------------------------------------------
884
885#[derive(Debug, Serialize, Deserialize)]
886pub struct JourneyPlatform {
887    journey_legacy_id: i32,
888    administration: String,
889    platform_id: i32,
890    time: Option<NaiveTime>,
891    bit_field_id: Option<i32>,
892}
893
894impl JourneyPlatform {
895    pub fn new(
896        journey_legacy_id: i32,
897        administration: String,
898        platform_id: i32,
899        time: Option<NaiveTime>,
900        bit_field_id: Option<i32>,
901    ) -> Self {
902        Self {
903            journey_legacy_id,
904            administration,
905            platform_id,
906            time,
907            bit_field_id,
908        }
909    }
910}
911
912impl Model<JourneyPlatform> for JourneyPlatform {
913    type K = (i32, i32);
914
915    fn id(&self) -> Self::K {
916        (self.journey_legacy_id, self.platform_id)
917    }
918}
919
920// ------------------------------------------------------------------------------------------------
921// --- Language
922// ------------------------------------------------------------------------------------------------
923
924#[derive(
925    Clone, Copy, Debug, Default, Display, Eq, Hash, PartialEq, EnumString, Serialize, Deserialize,
926)]
927pub enum Language {
928    #[default]
929    #[strum(serialize = "deu", serialize = "DE")]
930    German,
931
932    #[strum(serialize = "fra", serialize = "FR")]
933    French,
934
935    #[strum(serialize = "ita", serialize = "IT")]
936    Italian,
937
938    #[strum(serialize = "eng", serialize = "EN")]
939    English,
940}
941
942// ------------------------------------------------------------------------------------------------
943// --- Line
944// ------------------------------------------------------------------------------------------------
945
946#[derive(Debug, Default, Serialize, Deserialize)]
947pub struct Line {
948    id: i32,
949    name: String,
950    short_name: String,
951    long_name: String,
952    region_name: String,
953    internal_designation: String,
954    description: String,
955    text_color: Color,
956    background_color: Color,
957    infotext_id: i32,
958}
959
960impl_Model!(Line);
961
962impl Line {
963    pub fn new(id: i32, name: String) -> Self {
964        Self {
965            id,
966            name,
967            short_name: String::default(),
968            long_name: String::default(),
969            region_name: String::default(),
970            internal_designation: String::default(),
971            description: String::default(),
972            text_color: Color::default(),
973            background_color: Color::default(),
974            infotext_id: -1,
975        }
976    }
977
978    // Getters/Setters
979
980    pub fn set_short_name(&mut self, value: String) {
981        self.short_name = value;
982    }
983
984    pub fn set_long_name(&mut self, value: String) {
985        self.long_name = value;
986    }
987
988    pub fn set_region_name(&mut self, value: String) {
989        self.region_name = value;
990    }
991
992    pub fn set_internal_designation(&mut self, value: String) {
993        self.internal_designation = value;
994    }
995
996    pub fn set_description(&mut self, value: String) {
997        self.description = value;
998    }
999
1000    pub fn set_text_color(&mut self, value: Color) {
1001        self.text_color = value;
1002    }
1003
1004    pub fn set_background_color(&mut self, value: Color) {
1005        self.background_color = value;
1006    }
1007
1008    pub fn set_infotext_id(&mut self, value: i32) {
1009        self.infotext_id = value;
1010    }
1011}
1012
1013// ------------------------------------------------------------------------------------------------
1014// --- Platform
1015// ------------------------------------------------------------------------------------------------
1016
1017#[derive(Debug, Serialize, Deserialize)]
1018pub struct Platform {
1019    id: i32,
1020    name: String,
1021    sectors: Option<String>,
1022    stop_id: i32,
1023    sloid: String,
1024    lv95_coordinates: Coordinates,
1025    wgs84_coordinates: Coordinates,
1026}
1027
1028impl_Model!(Platform);
1029
1030impl Platform {
1031    pub fn new(id: i32, name: String, sectors: Option<String>, stop_id: i32) -> Self {
1032        Self {
1033            id,
1034            name,
1035            sectors,
1036            stop_id,
1037            sloid: String::default(),
1038            lv95_coordinates: Coordinates::default(),
1039            wgs84_coordinates: Coordinates::default(),
1040        }
1041    }
1042
1043    // Getters/Setters
1044
1045    pub fn set_sloid(&mut self, value: String) {
1046        self.sloid = value;
1047    }
1048
1049    pub fn set_lv95_coordinates(&mut self, value: Coordinates) {
1050        self.lv95_coordinates = value;
1051    }
1052
1053    pub fn set_wgs84_coordinates(&mut self, value: Coordinates) {
1054        self.wgs84_coordinates = value;
1055    }
1056}
1057
1058// ------------------------------------------------------------------------------------------------
1059// --- Stop
1060// ------------------------------------------------------------------------------------------------
1061
1062#[derive(Debug, Serialize, Deserialize)]
1063pub struct Stop {
1064    id: i32,
1065    name: String,
1066    long_name: Option<String>,
1067    abbreviation: Option<String>,
1068    synonyms: Option<Vec<String>>,
1069    lv95_coordinates: Option<Coordinates>,
1070    wgs84_coordinates: Option<Coordinates>,
1071    exchange_priority: i16,
1072    exchange_flag: i16,
1073    exchange_time: Option<(i16, i16)>, // (InterCity exchange time, Exchange time for all other journey types)
1074    restrictions: i16,
1075    sloid: String,
1076    boarding_areas: Vec<String>,
1077}
1078
1079impl_Model!(Stop);
1080
1081impl Stop {
1082    pub fn new(
1083        id: i32,
1084        name: String,
1085        long_name: Option<String>,
1086        abbreviation: Option<String>,
1087        synonyms: Option<Vec<String>>,
1088    ) -> Self {
1089        Self {
1090            id,
1091            name,
1092            long_name,
1093            abbreviation,
1094            synonyms,
1095            lv95_coordinates: None,
1096            wgs84_coordinates: None,
1097            exchange_priority: 8, // 8 is the default priority.
1098            exchange_flag: 0,
1099            exchange_time: None,
1100            restrictions: 0,
1101            sloid: String::default(),
1102            boarding_areas: Vec::new(),
1103        }
1104    }
1105
1106    // Getters/Setters
1107
1108    pub fn name(&self) -> &str {
1109        &self.name
1110    }
1111
1112    pub fn lv95_coordinates(&self) -> Option<Coordinates> {
1113        self.lv95_coordinates
1114    }
1115
1116    pub fn set_lv95_coordinates(&mut self, value: Coordinates) {
1117        self.lv95_coordinates = Some(value);
1118    }
1119
1120    pub fn wgs84_coordinates(&self) -> Option<Coordinates> {
1121        self.wgs84_coordinates
1122    }
1123
1124    pub fn set_wgs84_coordinates(&mut self, value: Coordinates) {
1125        self.wgs84_coordinates = Some(value);
1126    }
1127
1128    pub fn set_exchange_priority(&mut self, value: i16) {
1129        self.exchange_priority = value;
1130    }
1131
1132    pub fn exchange_flag(&self) -> i16 {
1133        self.exchange_flag
1134    }
1135
1136    pub fn set_exchange_flag(&mut self, value: i16) {
1137        self.exchange_flag = value;
1138    }
1139
1140    pub fn exchange_time(&self) -> Option<(i16, i16)> {
1141        self.exchange_time
1142    }
1143
1144    pub fn set_exchange_time(&mut self, value: Option<(i16, i16)>) {
1145        self.exchange_time = value;
1146    }
1147
1148    pub fn set_restrictions(&mut self, value: i16) {
1149        self.restrictions = value;
1150    }
1151
1152    pub fn set_sloid(&mut self, value: String) {
1153        self.sloid = value;
1154    }
1155
1156    // Functions
1157
1158    pub fn add_boarding_area(&mut self, value: String) {
1159        self.boarding_areas.push(value);
1160    }
1161
1162    pub fn can_be_used_as_exchange_point(&self) -> bool {
1163        self.exchange_flag() != 0
1164    }
1165}
1166
1167// ------------------------------------------------------------------------------------------------
1168// --- StopConnection
1169// ------------------------------------------------------------------------------------------------
1170
1171#[derive(Debug, Default, Serialize, Deserialize)]
1172pub struct StopConnection {
1173    id: i32,
1174    stop_id_1: i32,
1175    stop_id_2: i32,
1176    duration: i16, // Exchange time from stop 1 to stop 2 is in minutes.
1177    attribute: i32,
1178}
1179
1180impl_Model!(StopConnection);
1181
1182impl StopConnection {
1183    pub fn new(id: i32, stop_id_1: i32, stop_id_2: i32, duration: i16) -> Self {
1184        Self {
1185            id,
1186            stop_id_1,
1187            stop_id_2,
1188            duration,
1189            attribute: 0,
1190        }
1191    }
1192
1193    // Getters/Setters
1194
1195    pub fn stop_id_1(&self) -> i32 {
1196        self.stop_id_1
1197    }
1198
1199    pub fn stop_id_2(&self) -> i32 {
1200        self.stop_id_2
1201    }
1202
1203    pub fn duration(&self) -> i16 {
1204        self.duration
1205    }
1206
1207    pub fn set_attribute(&mut self, value: i32) {
1208        self.attribute = value;
1209    }
1210}
1211
1212// ------------------------------------------------------------------------------------------------
1213// --- ThroughService
1214// ------------------------------------------------------------------------------------------------
1215
1216#[derive(Debug, Serialize, Deserialize)]
1217pub struct ThroughService {
1218    id: i32,
1219    journey_1_id: JourneyId,
1220    journey_1_stop_id: i32, // Last stop of journey 1.
1221    journey_2_id: JourneyId,
1222    journey_2_stop_id: i32, // First stop of journey 2.
1223    bit_field_id: i32,
1224}
1225
1226impl_Model!(ThroughService);
1227
1228impl ThroughService {
1229    pub fn new(
1230        id: i32,
1231        journey_1_id: JourneyId,
1232        journey_1_stop_id: i32,
1233        journey_2_id: JourneyId,
1234        journey_2_stop_id: i32,
1235        bit_field_id: i32,
1236    ) -> Self {
1237        Self {
1238            id,
1239            journey_1_id,
1240            journey_1_stop_id,
1241            journey_2_id,
1242            journey_2_stop_id,
1243            bit_field_id,
1244        }
1245    }
1246
1247    pub fn journey_1_id(&self) -> &JourneyId {
1248        &self.journey_1_id
1249    }
1250
1251    pub fn journey_1_stop_id(&self) -> i32 {
1252        self.journey_1_stop_id
1253    }
1254
1255    pub fn journey_2_id(&self) -> &JourneyId {
1256        &self.journey_2_id
1257    }
1258
1259    pub fn journey_2_stop_id(&self) -> i32 {
1260        self.journey_2_stop_id
1261    }
1262
1263    pub fn bit_field_id(&self) -> i32 {
1264        self.bit_field_id
1265    }
1266}
1267
1268// ------------------------------------------------------------------------------------------------
1269// --- TimetableMetadataEntry
1270// ------------------------------------------------------------------------------------------------
1271
1272#[derive(Debug, Serialize, Deserialize)]
1273pub struct TimetableMetadataEntry {
1274    id: i32,
1275    key: String,
1276    value: String,
1277}
1278
1279impl_Model!(TimetableMetadataEntry);
1280
1281impl TimetableMetadataEntry {
1282    pub fn new(id: i32, key: String, value: String) -> Self {
1283        Self { id, key, value }
1284    }
1285
1286    // Getters/Setters
1287
1288    pub fn key(&self) -> &str {
1289        &self.key
1290    }
1291
1292    pub fn value(&self) -> &str {
1293        &self.value
1294    }
1295
1296    /// unwrap: Do not call this function if the value is not a date.
1297    pub fn value_as_naive_date(&self) -> NaiveDate {
1298        NaiveDate::parse_from_str(self.value(), "%Y-%m-%d").unwrap()
1299    }
1300}
1301
1302// ------------------------------------------------------------------------------------------------
1303// --- TransportCompany
1304// ------------------------------------------------------------------------------------------------
1305
1306#[derive(Debug, Serialize, Deserialize)]
1307pub struct TransportCompany {
1308    id: i32,
1309    short_name: FxHashMap<Language, String>,
1310    long_name: FxHashMap<Language, String>,
1311    full_name: FxHashMap<Language, String>,
1312    administrations: Vec<String>,
1313}
1314
1315impl_Model!(TransportCompany);
1316
1317impl TransportCompany {
1318    pub fn new(id: i32) -> Self {
1319        Self {
1320            id,
1321            short_name: FxHashMap::default(),
1322            long_name: FxHashMap::default(),
1323            full_name: FxHashMap::default(),
1324            administrations: Vec::new(),
1325        }
1326    }
1327
1328    // Getters/Setters
1329
1330    pub fn set_administrations(&mut self, administrations: Vec<String>) {
1331        self.administrations = administrations;
1332    }
1333
1334    pub fn set_short_name(&mut self, language: Language, value: &str) {
1335        self.short_name.insert(language, value.to_string());
1336    }
1337
1338    pub fn set_long_name(&mut self, language: Language, value: &str) {
1339        self.long_name.insert(language, value.to_string());
1340    }
1341
1342    pub fn set_full_name(&mut self, language: Language, value: &str) {
1343        self.full_name.insert(language, value.to_string());
1344    }
1345}
1346
1347// ------------------------------------------------------------------------------------------------
1348// --- TransportType
1349// ------------------------------------------------------------------------------------------------
1350
1351#[derive(Debug, Default, Serialize, Deserialize)]
1352pub struct TransportType {
1353    id: i32,
1354    designation: String,
1355    product_class_id: i16,
1356    tariff_group: String,
1357    output_control: i16,
1358    short_name: String,
1359    surcharge: i16,
1360    flag: String,
1361    product_class_name: FxHashMap<Language, String>,
1362    category_name: FxHashMap<Language, String>,
1363}
1364
1365impl_Model!(TransportType);
1366
1367impl TransportType {
1368    #[allow(clippy::too_many_arguments)]
1369    pub fn new(
1370        id: i32,
1371        designation: String,
1372        product_class_id: i16,
1373        tariff_group: String,
1374        output_control: i16,
1375        short_name: String,
1376        surcharge: i16,
1377        flag: String,
1378    ) -> Self {
1379        Self {
1380            id,
1381            designation,
1382            product_class_id,
1383            tariff_group,
1384            output_control,
1385            short_name,
1386            surcharge,
1387            flag,
1388            product_class_name: FxHashMap::default(),
1389            category_name: FxHashMap::default(),
1390        }
1391    }
1392
1393    // Getters/Setters
1394
1395    pub fn designation(&self) -> &str {
1396        &self.designation
1397    }
1398
1399    pub fn product_class_id(&self) -> i16 {
1400        self.product_class_id
1401    }
1402
1403    pub fn set_product_class_name(&mut self, language: Language, value: &str) {
1404        self.product_class_name.insert(language, value.to_string());
1405    }
1406
1407    pub fn set_category_name(&mut self, language: Language, value: &str) {
1408        self.category_name.insert(language, value.to_string());
1409    }
1410}
1411
1412// ------------------------------------------------------------------------------------------------
1413// --- Version
1414// ------------------------------------------------------------------------------------------------
1415
1416struct NaiveDateRange(NaiveDate, NaiveDate);
1417
1418impl NaiveDateRange {
1419    fn new(date_from: NaiveDate, date_until: NaiveDate) -> Self {
1420        NaiveDateRange(date_from, date_until)
1421    }
1422    fn contains(&self, date: &NaiveDate) -> bool {
1423        self.0 <= *date && self.1 >= *date
1424    }
1425}
1426
1427#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1428#[allow(non_camel_case_types)]
1429pub enum Version {
1430    V_5_20_1_0,
1431    V_5_40_41_2_0_2,
1432    V_5_40_41_2_0_3,
1433    V_5_40_41_2_0_4,
1434    V_5_40_41_2_0_5,
1435    V_5_40_41_2_0_6,
1436    V_5_40_41_2_0_7,
1437}
1438
1439impl Version {
1440    fn timetable_2026() -> NaiveDateRange {
1441        NaiveDateRange::new(
1442            NaiveDate::from_ymd_opt(2025, 12, 14).unwrap(),
1443            NaiveDate::from_ymd_opt(2026, 12, 12).unwrap(),
1444        )
1445    }
1446    fn timetable_2025() -> NaiveDateRange {
1447        NaiveDateRange::new(
1448            NaiveDate::from_ymd_opt(2024, 12, 15).unwrap(),
1449            NaiveDate::from_ymd_opt(2025, 12, 13).unwrap(),
1450        )
1451    }
1452    fn timetable_2024() -> NaiveDateRange {
1453        NaiveDateRange::new(
1454            NaiveDate::from_ymd_opt(2023, 12, 10).unwrap(),
1455            NaiveDate::from_ymd_opt(2024, 12, 14).unwrap(),
1456        )
1457    }
1458    fn timetable_2023() -> NaiveDateRange {
1459        NaiveDateRange::new(
1460            NaiveDate::from_ymd_opt(2022, 12, 11).unwrap(),
1461            NaiveDate::from_ymd_opt(2023, 12, 13).unwrap(),
1462        )
1463    }
1464    fn timetable_2022() -> NaiveDateRange {
1465        NaiveDateRange::new(
1466            NaiveDate::from_ymd_opt(2021, 12, 12).unwrap(),
1467            NaiveDate::from_ymd_opt(2022, 12, 10).unwrap(),
1468        )
1469    }
1470    // fn timetable_2021() -> NaiveDateRange {
1471    //     NaiveDateRange::new(
1472    //         NaiveDate::from_ymd_opt(2020, 12, 13).unwrap(),
1473    //         NaiveDate::from_ymd_opt(2021, 12, 11).unwrap(),
1474    //     )
1475    // }
1476    // fn timetable_2020() -> NaiveDateRange {
1477    //     NaiveDateRange::new(
1478    //         NaiveDate::from_ymd_opt(2019, 12, 15).unwrap(),
1479    //         NaiveDate::from_ymd_opt(2021, 12, 12).unwrap(),
1480    //     )
1481    // }
1482    pub(crate) fn try_url(date: NaiveDate) -> HResult<String> {
1483        if Self::timetable_2026().contains(&date) {
1484            Ok(String::from(
1485                "https://data.opentransportdata.swiss/en/dataset/timetable-54-2026-hrdf/permalink",
1486            ))
1487        } else if Self::timetable_2025().contains(&date) {
1488            Ok(String::from(
1489                "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2025-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2025_20251205_205244.zip",
1490            ))
1491        } else if Self::timetable_2024().contains(&date) {
1492            Ok(String::from(
1493                "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2024-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2024_20241213_205621.zip",
1494            ))
1495        } else if Self::timetable_2023().contains(&date) {
1496            Ok(String::from(
1497                "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2023-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2023_20231206_204217.zip",
1498            ))
1499        } else if Self::timetable_2022().contains(&date) {
1500            Ok(String::from(
1501                "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2022-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2022_20221207_205110.zip",
1502            ))
1503        // } else if Self::timetable_2021().contains(&date) {
1504        //     Ok(String::from(
1505        //         "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2021-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2021_20211208_204836.zip",
1506        //     ))
1507        // } else if Self::timetable_2020().contains(&date) {
1508        //     Ok(String::from(
1509        //         "https://archive.opentransportdata.swiss/timetable_hrdf/timetable-2020-hrdf-54/OeV_Sammlung_CH_HRDF_5_40_41_2020_20201207_074253.zip",
1510        //     ))
1511        } else {
1512            Err(HrdfError::OutOfRangeDate(date))
1513        }
1514    }
1515}
1516
1517impl TryFrom<NaiveDate> for Version {
1518    type Error = HrdfError;
1519
1520    // Required method
1521    fn try_from(date: NaiveDate) -> Result<Self, Self::Error> {
1522        if Self::timetable_2026().contains(&date)
1523            || Self::timetable_2025().contains(&date)
1524            || Self::timetable_2024().contains(&date)
1525        {
1526            Ok(Version::V_5_40_41_2_0_7)
1527        } else if Self::timetable_2023().contains(&date) || Self::timetable_2022().contains(&date) {
1528            Ok(Version::V_5_40_41_2_0_5)
1529        // } else if Self::timetable_2021().contains(&date) {
1530        //     Ok(Version::V_5_40_41_2_0_4)
1531        // } else if Self::timetable_2020().contains(&date) {
1532        //     Ok(Version::V_5_40_41_2_0_4)
1533        } else {
1534            Err(HrdfError::OutOfRangeDate(date))
1535        }
1536    }
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541    use super::*;
1542    use chrono::{NaiveDate, NaiveTime};
1543
1544    fn build_route_entry(
1545        stop_id: i32,
1546        arrival: Option<&str>,
1547        departure: Option<&str>,
1548    ) -> JourneyRouteEntry {
1549        let arrival_time = arrival.map(|value| NaiveTime::parse_from_str(value, "%H:%M").unwrap());
1550        let departure_time =
1551            departure.map(|value| NaiveTime::parse_from_str(value, "%H:%M").unwrap());
1552        JourneyRouteEntry::new(stop_id, arrival_time, departure_time)
1553    }
1554
1555    fn build_midnight_journey() -> Journey {
1556        let mut journey = Journey::new(1, 100, "CH".to_string());
1557        journey.add_route_entry(build_route_entry(1, None, Some("23:50")));
1558        journey.add_route_entry(build_route_entry(2, Some("00:10"), Some("00:15")));
1559        journey.add_route_entry(build_route_entry(3, Some("00:30"), None));
1560        journey
1561    }
1562
1563    #[test]
1564    fn coordinates_accessors_match_system() {
1565        let lv95 = Coordinates::new(CoordinateSystem::LV95, 2600000.0, 1200000.0);
1566        assert_eq!(lv95.easting(), Some(2600000.0));
1567        assert_eq!(lv95.northing(), Some(1200000.0));
1568        assert_eq!(lv95.latitude(), None);
1569        assert_eq!(lv95.longitude(), None);
1570
1571        let wgs84 = Coordinates::new(CoordinateSystem::WGS84, 46.948, 7.447);
1572        assert_eq!(wgs84.easting(), None);
1573        assert_eq!(wgs84.northing(), None);
1574        assert_eq!(wgs84.latitude(), Some(46.948));
1575        assert_eq!(wgs84.longitude(), Some(7.447));
1576    }
1577
1578    #[test]
1579    fn stop_exchange_flag_controls_exchange_point() {
1580        let mut stop = Stop::new(1, "Bern".to_string(), None, None, None);
1581        assert!(!stop.can_be_used_as_exchange_point());
1582        stop.set_exchange_flag(1);
1583        assert!(stop.can_be_used_as_exchange_point());
1584    }
1585
1586    #[test]
1587    fn journey_last_stop_logic_handles_loops() {
1588        let mut journey = Journey::new(1, 100, "CH".to_string());
1589        journey.add_route_entry(build_route_entry(1, None, Some("08:00")));
1590        journey.add_route_entry(build_route_entry(2, Some("08:10"), Some("08:15")));
1591        journey.add_route_entry(build_route_entry(1, Some("08:30"), None));
1592
1593        assert!(journey.is_last_stop(1, false).unwrap());
1594        assert!(!journey.is_last_stop(1, true).unwrap());
1595        assert!(!journey.is_last_stop(2, false).unwrap());
1596    }
1597
1598    #[test]
1599    fn journey_counts_and_sections_are_consistent() {
1600        let mut journey = Journey::new(1, 100, "CH".to_string());
1601        journey.add_route_entry(build_route_entry(1, None, Some("08:00")));
1602        journey.add_route_entry(build_route_entry(2, Some("08:10"), Some("08:15")));
1603        journey.add_route_entry(build_route_entry(3, Some("08:30"), Some("08:35")));
1604        journey.add_route_entry(build_route_entry(4, Some("08:50"), None));
1605
1606        assert_eq!(journey.count_stops(1, 3), 3);
1607        let section = journey.route_section(1, 3);
1608        let ids: Vec<i32> = section.iter().map(|entry| entry.stop_id()).collect();
1609        assert_eq!(ids, vec![2, 3]);
1610    }
1611
1612    #[test]
1613    fn journey_time_calculations_cross_midnight() {
1614        let journey = build_midnight_journey();
1615        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
1616
1617        let (departure_time, is_next_day) = journey.departure_time_of(2).unwrap();
1618        assert_eq!(departure_time, NaiveTime::from_hms_opt(0, 15, 0).unwrap());
1619        assert!(is_next_day);
1620
1621        let (arrival_time, is_next_day) = journey.arrival_time_of(2).unwrap();
1622        assert_eq!(arrival_time, NaiveTime::from_hms_opt(0, 10, 0).unwrap());
1623        assert!(is_next_day);
1624
1625        let departure_at = journey.departure_at_of(2, date).unwrap();
1626        assert_eq!(
1627            departure_at,
1628            NaiveDate::from_ymd_opt(2024, 1, 2)
1629                .unwrap()
1630                .and_time(NaiveTime::from_hms_opt(0, 15, 0).unwrap())
1631        );
1632
1633        let arrival_at = journey.arrival_at_of_with_origin(2, date, true, 1).unwrap();
1634        assert_eq!(
1635            arrival_at,
1636            NaiveDate::from_ymd_opt(2024, 1, 2)
1637                .unwrap()
1638                .and_time(NaiveTime::from_hms_opt(0, 10, 0).unwrap())
1639        );
1640    }
1641
1642    #[test]
1643    fn journey_bit_field_id_requires_metadata() {
1644        let journey = Journey::new(1, 100, "CH".to_string());
1645        let err = journey.bit_field_id().unwrap_err();
1646        match err {
1647            JourneyError::MissingBitFieldMetadata => {}
1648            other => panic!("Error should be MissingBitFieldMetadata but is: {other:?}"),
1649        }
1650    }
1651
1652    #[test]
1653    fn timetable_metadata_entry_parses_date() {
1654        let entry =
1655            TimetableMetadataEntry::new(1, "start_date".to_string(), "2024-12-15".to_string());
1656        assert_eq!(
1657            entry.value_as_naive_date(),
1658            NaiveDate::from_ymd_opt(2024, 12, 15).unwrap()
1659        );
1660    }
1661
1662    #[test]
1663    fn version_resolution_matches_date_ranges() {
1664        let in_2026 = NaiveDate::from_ymd_opt(2026, 6, 1).unwrap();
1665        assert_eq!(
1666            Version::try_from(in_2026).unwrap(),
1667            Version::V_5_40_41_2_0_7
1668        );
1669        let url = Version::try_url(in_2026).unwrap();
1670        assert!(url.contains(
1671            "https://data.opentransportdata.swiss/en/dataset/timetable-54-2026-hrdf/permalink"
1672        ));
1673
1674        let in_2025 = NaiveDate::from_ymd_opt(2025, 6, 1).unwrap();
1675        assert_eq!(
1676            Version::try_from(in_2025).unwrap(),
1677            Version::V_5_40_41_2_0_7
1678        );
1679        let url = Version::try_url(in_2025).unwrap();
1680        assert!(url.contains("timetable-2025-hrdf"));
1681
1682        let in_2024 = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
1683        assert_eq!(
1684            Version::try_from(in_2024).unwrap(),
1685            Version::V_5_40_41_2_0_7
1686        );
1687        let url = Version::try_url(in_2024).unwrap();
1688        assert!(url.contains("timetable-2024-hrdf"));
1689
1690        let in_2023 = NaiveDate::from_ymd_opt(2023, 6, 1).unwrap();
1691        assert_eq!(
1692            Version::try_from(in_2023).unwrap(),
1693            Version::V_5_40_41_2_0_5
1694        );
1695        let url = Version::try_url(in_2023).unwrap();
1696        assert!(url.contains("timetable-2023-hrdf"));
1697
1698        let in_2022 = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
1699        assert_eq!(
1700            Version::try_from(in_2022).unwrap(),
1701            Version::V_5_40_41_2_0_5
1702        );
1703        let url = Version::try_url(in_2022).unwrap();
1704        assert!(url.contains("timetable-2022-hrdf"));
1705    }
1706
1707    #[test]
1708    #[should_panic]
1709    fn version_resolution_not_matching_date_ranges() {
1710        let in_2021 = NaiveDate::from_ymd_opt(2021, 6, 1).unwrap();
1711        Version::try_from(in_2021).unwrap();
1712    }
1713
1714    #[test]
1715    #[should_panic]
1716    fn url_resolution_not_matching_date_ranges() {
1717        let in_2021 = NaiveDate::from_ymd_opt(2021, 6, 1).unwrap();
1718        Version::try_url(in_2021).unwrap();
1719    }
1720}