Skip to main content

nr_cif/
schedule.rs

1use std::{collections::HashMap, str::FromStr};
2
3use bitflags::bitflags;
4use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
5use getset::Getters;
6use log::{info, trace, warn};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::types::{CIFFile, CIFRecord};
12
13#[derive(Error, Debug)]
14pub enum ScheduleApplyError {
15    #[error("the data being applied is older than the data already loaded")]
16    AttemptingToApplyOlderData,
17    #[error("invalid extract date and time in header record")]
18    InvalidHeaderDateTime(String),
19    #[error("invalid date in basic schedule record")]
20    InvalidScheduleDate(String),
21    #[error("invalid days run in basic schedule record")]
22    InvalidDaysRun(String),
23    #[error("invalid train status in basic schedule record")]
24    InvalidTrainStatus(char),
25    #[error("invalid train category in basic schedule record")]
26    InvalidTrainCategory(String),
27    #[error("invalid train power type in basic schedule record")]
28    InvalidPowerType(String),
29    #[error("invalid timing load in basic schedule record")]
30    InvalidTimingLoad(String),
31    #[error("invalid operating characteristic in basic schedule record")]
32    InvalidOperatingCharacteristic(char),
33    #[error("invalid seating class in basic schedule record")]
34    InvalidSeatingClass(char),
35    #[error("invalid sleepers value in basic schedule record")]
36    InvalidSleepers(char),
37    #[error("invalid reservations in basic schedule record")]
38    InvalidReservations(char),
39    #[error("invalid catering code in basic schedule record")]
40    InvalidCateringCode(char),
41    #[error("invalid STP indicator in basic schedule record")]
42    InvalidSTPIndicator(char),
43    #[error("invalid journey time in location record")]
44    InvalidJourneyTime(String),
45}
46
47#[derive(Debug, Clone, Getters)]
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
49pub struct ScheduleDatabase {
50    #[getset(get = "pub")]
51    extract_date_time: NaiveDateTime,
52    /// A map of TIPLOC to [`TIPLOC`] objects, with extra information like name and
53    /// CRS (3 alpha code).
54    #[getset(get = "pub")]
55    tiplocs: HashMap<String, TIPLOC>,
56    /// A map of schedule UIDs to a list of applicable schedules. These should be filtered by the
57    /// validity date for the period in question, then you should get the one at the latest index
58    /// valid in your time period. This will be the schedule to take effect.
59    #[getset(get = "pub")]
60    schedules: HashMap<String, Vec<Schedule>>,
61}
62
63impl Default for ScheduleDatabase {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl ScheduleDatabase {
70    /// Create a new [`ScheduleDatabase`].
71    pub fn new() -> Self {
72        Self {
73            extract_date_time: NaiveDateTime::MIN,
74            tiplocs: HashMap::new(),
75            schedules: HashMap::new(),
76        }
77    }
78
79    /// Apply a file onto this schedule database.
80    /// This can reset the database if this includes a full update.
81    pub fn apply_file(&mut self, file: &CIFFile) -> Vec<(usize, ScheduleApplyError)> {
82        self.apply_records(file.records())
83    }
84
85    /// Finds a CRS from a given TIPLOC. If the TIPLOC is not immediately
86    /// associated with a CRS, this will check other TIPLOCs associated with
87    /// the same STANOX.
88    pub fn get_crs_from_tiploc<S: AsRef<str>>(&self, tiploc: S) -> Vec<String> {
89        if let Some(base_tiploc) = self.tiplocs().get(tiploc.as_ref()) {
90            if !base_tiploc.three_alpha_code().is_empty() {
91                return vec![base_tiploc.three_alpha_code().clone()];
92            }
93
94            let stanox = *base_tiploc.stanox();
95            let mut crs = vec![];
96            for (_, tiploc) in self.tiplocs().iter().filter(|(_, t)| stanox == *t.stanox()) {
97                if !tiploc.three_alpha_code().is_empty() {
98                    crs.push(tiploc.three_alpha_code().clone());
99                }
100            }
101            return crs;
102        }
103        vec![]
104    }
105
106    /// Apply a list of records onto this schedule database.
107    /// This can reset the database if this includes a full update.
108    /// Returns a list of errors and their respective record index.
109    pub fn apply_records(&mut self, records: &[CIFRecord]) -> Vec<(usize, ScheduleApplyError)> {
110        let mut bundle = vec![];
111        let mut errors = vec![];
112        for (record_idx, record) in records.iter().enumerate() {
113            if record_idx % 10000 == 0 {
114                info!(
115                    "Processing record #{}. (shown every 10000 records)",
116                    record_idx + 1
117                );
118            }
119            bundle.push(record);
120
121            // Check type
122            let submit = match &record {
123                CIFRecord::Header { .. } => true,
124                CIFRecord::Trailer => true,
125                CIFRecord::Association { .. } => true,
126                CIFRecord::LocationTerminate { .. } => true,
127                CIFRecord::TIPLOCInsert { .. } => true,
128                CIFRecord::TIPLOCAmend { .. } => true,
129                CIFRecord::TIPLOCDelete { .. } => true,
130                CIFRecord::BasicSchedule { .. } => {
131                    if let CIFRecord::BasicSchedule {
132                        transaction_type,
133                        stp_indicator,
134                        ..
135                    } = record
136                    {
137                        // only submit a BS record alone if it's a delete, or a cancellation
138                        *transaction_type == 'D' || *stp_indicator == 'C'
139                    } else {
140                        false
141                    }
142                }
143                _ => false,
144            };
145            trace!("Record: {:?}, submitting: {submit}", record);
146
147            if submit {
148                let r = if bundle.len() == 1 {
149                    self.apply_single_record(bundle[0])
150                } else {
151                    self.apply_record_bundle(&bundle)
152                };
153                if let Err(e) = r {
154                    #[cfg(feature = "panic-on-first-error")]
155                    {
156                        log::error!("Error at record {record_idx}, line {}", record_idx + 1);
157                        log::error!("Error: {e:?}");
158                        log::error!("Records: {:?}", bundle);
159                    }
160                    errors.push((record_idx, e));
161                    #[cfg(feature = "panic-on-first-error")]
162                    panic!(
163                        "Came across an error and the `panic-on-first-error` feature is enabled."
164                    );
165                }
166                bundle.clear();
167            }
168        }
169        errors
170    }
171
172    /// Apply a single record. A bundle consists of either:
173    /// - A header record.
174    /// - A TIPLOC record.
175    /// - An association record.
176    /// - A lone BS record for a delete type.
177    /// - A trailer record.
178    fn apply_single_record(&mut self, record: &CIFRecord) -> Result<(), ScheduleApplyError> {
179        match record {
180            CIFRecord::Header {
181                date_of_extract,
182                time_of_extract,
183                update_indicator,
184                ..
185            } => {
186                if *update_indicator == 'F' {
187                    // full update, empty database
188                    info!("Received full update, clearing database.");
189                    self.tiplocs.clear();
190                }
191                let date = NaiveDate::parse_from_str(date_of_extract, "%d%m%y").map_err(|_| {
192                    ScheduleApplyError::InvalidHeaderDateTime(date_of_extract.clone())
193                })?;
194                let time = NaiveTime::parse_from_str(time_of_extract, "%H%M").map_err(|_| {
195                    ScheduleApplyError::InvalidHeaderDateTime(time_of_extract.clone())
196                })?;
197                let date_and_time = date.and_time(time);
198                if self.extract_date_time > date_and_time {
199                    return Err(ScheduleApplyError::AttemptingToApplyOlderData);
200                }
201                self.extract_date_time = date_and_time;
202            }
203
204            CIFRecord::TIPLOCInsert {
205                tiploc,
206                tps_description,
207                three_alpha_code,
208                stanox,
209                ..
210            } => {
211                info!("New TIPLOC: {}", tiploc.trim());
212                self.tiplocs.insert(
213                    tiploc.trim().to_string(),
214                    TIPLOC {
215                        tiploc: tiploc.trim().to_string(),
216                        three_alpha_code: three_alpha_code.trim().to_string(),
217                        description: tps_description.trim().to_string(),
218                        stanox: *stanox,
219                    },
220                );
221            }
222            CIFRecord::TIPLOCAmend {
223                tiploc,
224                tps_description,
225                three_alpha_code,
226                new_tiploc,
227                stanox,
228                ..
229            } => {
230                info!("Amendment for TIPLOC {}", tiploc.trim());
231                let tiploc = if new_tiploc.trim().is_empty() {
232                    tiploc.trim().to_string()
233                } else {
234                    self.tiplocs.remove(&tiploc.trim().to_string());
235                    new_tiploc.trim().to_string()
236                };
237                self.tiplocs.insert(
238                    tiploc.clone(),
239                    TIPLOC {
240                        tiploc,
241                        three_alpha_code: three_alpha_code.trim().to_string(),
242                        description: tps_description.trim().to_string(),
243                        stanox: *stanox,
244                    },
245                );
246            }
247            CIFRecord::TIPLOCDelete { tiploc } => {
248                info!("Removed TIPLOC {}", tiploc.trim());
249                self.tiplocs.remove(tiploc.trim());
250            }
251
252            CIFRecord::BasicSchedule {
253                transaction_type,
254                train_uid,
255                date_runs_from,
256                date_runs_to,
257                days_run,
258                bank_holiday_running,
259                train_status,
260                train_category,
261                train_identity,
262                portion_id,
263                power_type,
264                timing_load,
265                speed,
266                operating_characteristics,
267                seating_class,
268                sleepers,
269                reservations,
270                catering_code,
271                stp_indicator,
272                ..
273            } => {
274                assert!(
275                    *transaction_type == 'D' || *stp_indicator == 'C',
276                    "transaction type must be delete, or it must be a cancellation to be processed as a single record"
277                );
278                if *transaction_type == 'D' {
279                    self.schedules.remove(train_uid);
280                } else {
281                    let mut sch = Schedule::new();
282                    bs_record_to_schedule(
283                        &mut sch,
284                        train_uid,
285                        date_runs_from,
286                        date_runs_to,
287                        days_run,
288                        bank_holiday_running,
289                        train_status,
290                        train_category,
291                        train_identity,
292                        portion_id,
293                        power_type,
294                        timing_load,
295                        speed,
296                        operating_characteristics,
297                        seating_class,
298                        sleepers,
299                        reservations,
300                        catering_code,
301                        stp_indicator,
302                    )?;
303                    self.schedules
304                        .entry(train_uid.clone())
305                        .and_modify(|v| v.push(sch));
306                }
307            }
308
309            _ => (),
310        }
311        Ok(())
312    }
313
314    /// Apply a bundle of records. A bundle consists of either:
315    /// - A schedule, from the BS record to the LT record, for a new or revise types.
316    fn apply_record_bundle(
317        &mut self,
318        record_bundle: &Vec<&CIFRecord>,
319    ) -> Result<(), ScheduleApplyError> {
320        let mut schedule = Schedule::new();
321
322        for record in record_bundle {
323            match record {
324                CIFRecord::BasicSchedule {
325                    transaction_type,
326                    train_uid,
327                    date_runs_from,
328                    date_runs_to,
329                    days_run,
330                    bank_holiday_running,
331                    train_status,
332                    train_category,
333                    train_identity,
334                    portion_id,
335                    power_type,
336                    timing_load,
337                    speed,
338                    operating_characteristics,
339                    seating_class,
340                    sleepers,
341                    reservations,
342                    catering_code,
343                    stp_indicator,
344                    ..
345                } => {
346                    let uid = train_uid.trim().to_string();
347                    if *transaction_type == 'R' && !self.schedules.contains_key(&uid) {
348                        warn!("A record is trying to revise schedule {uid}, but it doesn't exist in the database. Inserting it as new...");
349                    }
350
351                    bs_record_to_schedule(
352                        &mut schedule,
353                        &uid,
354                        date_runs_from,
355                        date_runs_to,
356                        days_run,
357                        bank_holiday_running,
358                        train_status,
359                        train_category,
360                        train_identity,
361                        portion_id,
362                        power_type,
363                        timing_load,
364                        speed,
365                        operating_characteristics,
366                        seating_class,
367                        sleepers,
368                        reservations,
369                        catering_code,
370                        stp_indicator,
371                    )?;
372                }
373                CIFRecord::BasicScheduleExtended {
374                    atoc_code,
375                    applicable_timetable_code,
376                    ..
377                } => {
378                    schedule.atoc_code = atoc_code.trim().to_string();
379                    schedule.subject_to_performance_monitoring = *applicable_timetable_code == 'Y';
380                }
381                CIFRecord::LocationOrigin {
382                    location,
383                    scheduled_departure_time,
384                    public_departure_time,
385                    platform,
386                    line,
387                    activity,
388                    ..
389                } => schedule.journey.push(JourneyLocation {
390                    tiploc: location[0..7].trim().to_string(),
391                    arrival_time: None,
392                    departure_time: Some(scheduled_departure_time.parse()?),
393                    passing_time: None,
394                    public_arrival: None,
395                    public_departure: Some(public_departure_time.parse()?),
396                    platform: platform.trim().to_string(),
397                    line: line.trim().to_string(),
398                    activity: activity.trim().to_string(),
399                }),
400                CIFRecord::LocationIntermediate {
401                    location,
402                    scheduled_arrival_time,
403                    scheduled_departure_time,
404                    scheduled_pass,
405                    public_arrival_time,
406                    public_departure_time,
407                    platform,
408                    line,
409                    activity,
410                    ..
411                } => schedule.journey.push(JourneyLocation {
412                    tiploc: location[0..7].trim().to_string(),
413                    arrival_time: if scheduled_arrival_time.trim().is_empty() {
414                        None
415                    } else {
416                        Some(scheduled_arrival_time.parse()?)
417                    },
418                    departure_time: if scheduled_departure_time.trim().is_empty() {
419                        None
420                    } else {
421                        Some(scheduled_departure_time.parse()?)
422                    },
423                    passing_time: if scheduled_pass.trim().is_empty() {
424                        None
425                    } else {
426                        Some(scheduled_pass.parse()?)
427                    },
428                    public_arrival: if public_arrival_time.trim().is_empty() {
429                        None
430                    } else {
431                        Some(public_arrival_time.parse()?)
432                    },
433                    public_departure: if public_departure_time.trim().is_empty() {
434                        None
435                    } else {
436                        Some(public_departure_time.parse()?)
437                    },
438                    platform: platform.trim().to_string(),
439                    line: line.trim().to_string(),
440                    activity: activity.trim().to_string(),
441                }),
442                CIFRecord::LocationTerminate {
443                    location,
444                    scheduled_arrival_time,
445                    public_arrival_time,
446                    platform,
447                    activity,
448                    ..
449                } => schedule.journey.push(JourneyLocation {
450                    tiploc: location[0..7].trim().to_string(),
451                    arrival_time: Some(scheduled_arrival_time.parse()?),
452                    departure_time: None,
453                    passing_time: None,
454                    public_arrival: Some(public_arrival_time.parse()?),
455                    public_departure: None,
456                    platform: platform.trim().to_string(),
457                    line: String::new(),
458                    activity: activity.trim().to_string(),
459                }),
460
461                _ => (),
462            }
463        }
464
465        self.schedules
466            .entry(schedule.train_uid.clone())
467            .and_modify(|v| v.push(schedule.clone()))
468            .or_insert(vec![schedule]);
469        Ok(())
470    }
471}
472
473#[derive(Debug, Clone, Getters)]
474#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
475pub struct TIPLOC {
476    /// The TIPLOC code of this location.
477    #[getset(get = "pub")]
478    tiploc: String,
479    /// A 3 letter CRS code, if one is present for this location, or an empty string.
480    #[getset(get = "pub")]
481    three_alpha_code: String,
482    /// The description of this location.
483    #[getset(get = "pub")]
484    description: String,
485    /// The STANOX of this location.
486    #[getset(get = "pub")]
487    stanox: u32,
488}
489
490#[derive(Debug, Clone, Getters)]
491#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
492pub struct Schedule {
493    /// The service identifier.
494    #[getset(get = "pub")]
495    train_uid: String,
496    /// When does this service start running.
497    #[getset(get = "pub")]
498    runs_from: NaiveDate,
499    /// When does this service stop running.
500    #[getset(get = "pub")]
501    runs_to: NaiveDate,
502    /// Days in which this service operates. A bitflag.
503    #[getset(get = "pub")]
504    days_run: DaysRun,
505    /// Details about bank holiday running.
506    #[getset(get = "pub")]
507    bank_holiday_running: BankHolidayRunning,
508    /// Train operating company code.
509    #[getset(get = "pub")]
510    atoc_code: String,
511    /// Is this train subject to performance monitoring.
512    #[getset(get = "pub")]
513    subject_to_performance_monitoring: bool,
514    #[getset(get = "pub")]
515    train_status: TrainStatus,
516    #[getset(get = "pub")]
517    train_category: TrainCategory,
518    #[getset(get = "pub")]
519    headcode: String,
520    #[getset(get = "pub")]
521    portion_id: char,
522    #[getset(get = "pub")]
523    power_type: PowerType,
524    #[getset(get = "pub")]
525    timing_load: TimingLoad,
526    #[getset(get = "pub")]
527    speed: u32,
528    #[getset(get = "pub")]
529    operating_characteristics: Vec<OperatingCharacteristic>,
530    #[getset(get = "pub")]
531    seating_class: SeatingClass,
532    #[getset(get = "pub")]
533    sleepers: Sleepers,
534    #[getset(get = "pub")]
535    reservations: Reservations,
536    #[getset(get = "pub")]
537    catering: Vec<Catering>,
538    #[getset(get = "pub")]
539    stp_indicator: STPIndicator,
540    #[getset(get = "pub")]
541    journey: Vec<JourneyLocation>,
542}
543
544impl Schedule {
545    fn new() -> Self {
546        Self {
547            train_uid: String::new(),
548            runs_from: NaiveDate::MIN,
549            runs_to: NaiveDate::MIN,
550            days_run: DaysRun::empty(),
551            bank_holiday_running: BankHolidayRunning::RunsNormally,
552            atoc_code: String::new(),
553            subject_to_performance_monitoring: false,
554            train_status: TrainStatus::PassengerAndParcels,
555            train_category: TrainCategory::NotSpecified,
556            headcode: String::new(),
557            portion_id: ' ',
558            power_type: PowerType::Diesel,
559            timing_load: TimingLoad::LoadInTonnes(0),
560            speed: 0,
561            operating_characteristics: vec![],
562            seating_class: SeatingClass::NotSpecified,
563            sleepers: Sleepers::NotSpecified,
564            reservations: Reservations::Possible,
565            catering: vec![],
566            stp_indicator: STPIndicator::PermanentAssociation,
567            journey: vec![],
568        }
569    }
570}
571
572bitflags! {
573    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
574    #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
575    pub struct DaysRun: u8 {
576        const MONDAY    = 0b1000000;
577        const TUESDAY   = 0b0100000;
578        const WEDNESDAY = 0b0010000;
579        const THURSDAY  = 0b0001000;
580        const FRIDAY    = 0b0000100;
581        const SATURDAY  = 0b0000010;
582        const SUNDAY    = 0b0000001;
583
584        const WEEKDAYS = Self::MONDAY.bits() | Self::TUESDAY.bits() | Self::WEDNESDAY.bits() | Self::THURSDAY.bits() | Self::FRIDAY.bits();
585        const WEEKENDS = Self::SATURDAY.bits() | Self::SUNDAY.bits();
586    }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq)]
590#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
591pub enum BankHolidayRunning {
592    RunsNormally,
593    NotOnSpecificBankHolidayMondays,
594    NotOnGlasgowBankHolidays,
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
599pub enum TrainStatus {
600    Bus,
601    Freight,
602    PassengerAndParcels,
603    Ship,
604    Trip,
605    STPPassengerAndParcels,
606    STPFreight,
607    STPTrip,
608    STPShip,
609    STPBus,
610    NotSpecified,
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
615pub enum TrainCategory {
616    NotSpecified,
617    LondonUnderground,
618    UnadvertisedOrdinaryPassenger,
619    OrdinaryPassenger,
620    StaffTrain,
621    Mixed,
622    ChannelTunnel,
623    Sleeper,
624    International,
625    Motorail,
626    UnadvertisedExpress,
627    ExpressPassenger,
628    SleeperDomestic,
629    BusReplacementDueToEngineering,
630    BusWTTService,
631    Ship,
632    EmptyCoachingStock,
633    ECSLondonUnderground,
634    ECSAndStaff,
635    Postal,
636    PostOfficeControlledParcels,
637    Parcels,
638    EmptyNPCCS,
639    Departmental,
640    CivilEngineer,
641    MechanicalAndElectricalEngineer,
642    Stores,
643    Test,
644    SignalAndTelecommunicationsEngineer,
645    LocomotiveAndBrakeVan,
646    LightLocomotive,
647    RfDAutomotiveComponents,
648    RfDAutomotiveVehicles,
649    RfDEdibleProducts,
650    RfDIndustrialMinerals,
651    RfDChemicals,
652    RfDBuildingMaterials,
653    RfDGeneralMerchandise,
654    RfDEuropean,
655    RfDFreightlinerContracts,
656    RfDFreightlinerOther,
657    CoalDistributive,
658    CoalElectricityMGR,
659    CoalOtherAndNuclear,
660    Metals,
661    Aggregates,
662    DomesticAndIndustrialWaste,
663    BuildingMaterials,
664    PetroleumProducts,
665    RfDEuropeanChannelTunnelMixed,
666    RfDEuropeanChannelTunnelIntermodal,
667    RfDEuropeanChannelTunnelAutomotive,
668    RfDEuropeanChannelTunnelContractServices,
669    RfDEuropeanChannelTunnelHaulmark,
670    RfDEuropeanChannelTunnelJointVenture,
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
674#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
675pub enum PowerType {
676    Diesel,
677    DieselElectricMultipleUnit,
678    DieselMechanicalMultipleUnit,
679    Electric,
680    ElectroDiesel,
681    EMUPlusLocomotive,
682    ElectricMultipleUnit,
683    HighSpeedTrain,
684    NotSpecified,
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq)]
688#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
689pub enum OperatingCharacteristic {
690    VacuumBraked,
691    TimedAt100MPH,
692    DOOCoachingStockTrains,
693    ConveysMark4Coaches,
694    GuardRequired,
695    TimedAt110MPH,
696    PushPullTrain,
697    RunsAsRequired,
698    AirConditionedWithPASystem,
699    SteamHeated,
700    RunsToTerminalsAsRequired,
701    MayConveyTrafficToSB1CGauge,
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
706pub enum TimingLoad {
707    /// Unspecifed
708    NotSpecified,
709    /// Class 170/0, 172/1, 172/2
710    Class17201721Or1722,
711    /// Class 141 to 144
712    Class141To144,
713    /// Class 158, 168, 170 or 175
714    Class158168170Or175,
715    /// Class 165/0
716    Class1650,
717    /// Class 150, 153, 155 or 156
718    Class150153155Or156,
719    /// Class 165/1 or 166
720    Class1651Or166,
721    /// Class 220 or 221
722    Class220Or221,
723    /// Class 159
724    Class159,
725    /// DMU (Power Car + Trailer)
726    DMUPowerCarTrailer,
727    /// DMU (2 Power Cars + Trailer)
728    DMU2PowerCarsTrailer,
729    /// DMU (Power Twin)
730    DMUPowerTwin,
731    /// Accelerated Timings EMU
732    AcceleratedTimings,
733    /// Class 458
734    Class458,
735    /// Class 380
736    Class380,
737    /// Class 350/1 (110 mph)
738    Class3501110MPH,
739    /// Class 325 Electric Parcels Unit
740    Class325ElectricParcelsUnit,
741    /// Specific class
742    SpecificClass(u16),
743    /// Load in tonnes
744    LoadInTonnes(u16),
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
748#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
749pub enum SeatingClass {
750    FirstAndStandard,
751    StandardOnly,
752    NotSpecified,
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
756#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
757pub enum Sleepers {
758    FirstAndStandard,
759    FirstOnly,
760    StandardOnly,
761    NotSpecified,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
766pub enum Reservations {
767    Compulsory,
768    CompulsoryForBicycles,
769    Recommended,
770    Possible,
771    NotSpecified,
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq)]
775#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
776pub enum Catering {
777    NotSpecified,
778    BuffetService,
779    RestaurantCarForFirstClass,
780    HotFood,
781    MealForFirstClass,
782    WheelchairReservations,
783    Restaurant,
784    TrolleyService,
785}
786
787#[derive(Debug, Clone, Copy, PartialEq, Eq)]
788#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
789pub enum STPIndicator {
790    NewSTPAssociation,
791    STPCancellationOfPermanentAssociation,
792    STPOverlayOfPermanentAssociation,
793    PermanentAssociation,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Getters)]
797#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
798pub struct JourneyLocation {
799    #[getset(get = "pub")]
800    tiploc: String,
801    #[getset(get = "pub")]
802    arrival_time: Option<JourneyTime>,
803    #[getset(get = "pub")]
804    departure_time: Option<JourneyTime>,
805    #[getset(get = "pub")]
806    passing_time: Option<JourneyTime>,
807    #[getset(get = "pub")]
808    public_arrival: Option<JourneyTime>,
809    #[getset(get = "pub")]
810    public_departure: Option<JourneyTime>,
811    #[getset(get = "pub")]
812    platform: String,
813    #[getset(get = "pub")]
814    line: String,
815    #[getset(get = "pub")]
816    activity: String,
817}
818
819#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Getters)]
820#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
821pub struct JourneyTime {
822    #[getset(get = "pub")]
823    hour: u8,
824    #[getset(get = "pub")]
825    minute: u8,
826    #[getset(get = "pub")]
827    half: bool,
828}
829
830impl FromStr for JourneyTime {
831    type Err = ScheduleApplyError;
832
833    fn from_str(s: &str) -> Result<Self, Self::Err> {
834        let mut time = Self {
835            hour: 0,
836            minute: 0,
837            half: false,
838        };
839        let hour = &s[0..2];
840        let min = &s[2..4];
841
842        time.hour = hour
843            .parse()
844            .map_err(|_| ScheduleApplyError::InvalidJourneyTime(s.to_string()))?;
845        time.minute = min
846            .parse()
847            .map_err(|_| ScheduleApplyError::InvalidJourneyTime(s.to_string()))?;
848
849        if let Some(c) = s.chars().nth(4) {
850            if c == 'H' {
851                time.half = true;
852            } else if c == ' ' {
853                time.half = false;
854            } else {
855                return Err(ScheduleApplyError::InvalidJourneyTime(s.to_string()));
856            }
857        }
858        Ok(time)
859    }
860}
861
862#[allow(clippy::too_many_arguments)]
863fn bs_record_to_schedule(
864    schedule: &mut Schedule,
865    uid: &str,
866    date_runs_from: &str,
867    date_runs_to: &str,
868    days_run: &str,
869    bank_holiday_running: &char,
870    train_status: &char,
871    train_category: &str,
872    train_identity: &str,
873    portion_id: &char,
874    power_type: &str,
875    timing_load: &str,
876    speed: &str,
877    operating_characteristics: &str,
878    seating_class: &char,
879    sleepers: &char,
880    reservations: &char,
881    catering_code: &str,
882    stp_indicator: &char,
883) -> Result<(), ScheduleApplyError> {
884    schedule.train_uid = uid.to_string();
885    schedule.runs_from = NaiveDate::parse_from_str(date_runs_from, "%y%m%d")
886        .map_err(|_| ScheduleApplyError::InvalidScheduleDate(date_runs_from.to_string()))?;
887    schedule.runs_to = NaiveDate::parse_from_str(date_runs_to, "%y%m%d")
888        .map_err(|_| ScheduleApplyError::InvalidScheduleDate(date_runs_to.to_string()))?;
889    schedule.days_run = DaysRun::from_bits(
890        u8::from_str_radix(days_run, 2)
891            .map_err(|_| ScheduleApplyError::InvalidDaysRun(days_run.to_string()))?,
892    )
893    .ok_or(ScheduleApplyError::InvalidDaysRun(days_run.to_string()))?;
894    schedule.bank_holiday_running = match bank_holiday_running {
895        'X' => BankHolidayRunning::NotOnSpecificBankHolidayMondays,
896        'G' => BankHolidayRunning::NotOnGlasgowBankHolidays,
897        _ => BankHolidayRunning::RunsNormally,
898    };
899    schedule.train_status = match train_status {
900        ' ' => TrainStatus::NotSpecified,
901        'B' => TrainStatus::Bus,
902        'F' => TrainStatus::Freight,
903        'P' => TrainStatus::PassengerAndParcels,
904        'S' => TrainStatus::Ship,
905        'T' => TrainStatus::Trip,
906        '1' => TrainStatus::STPPassengerAndParcels,
907        '2' => TrainStatus::STPFreight,
908        '3' => TrainStatus::STPTrip,
909        '4' => TrainStatus::STPShip,
910        '5' => TrainStatus::STPBus,
911        _ => return Err(ScheduleApplyError::InvalidTrainStatus(*train_status)),
912    };
913    schedule.train_category = match train_category {
914        "  " => TrainCategory::NotSpecified,
915        "OL" => TrainCategory::LondonUnderground,
916        "OU" => TrainCategory::UnadvertisedOrdinaryPassenger,
917        "OO" => TrainCategory::OrdinaryPassenger,
918        "OS" => TrainCategory::StaffTrain,
919        "OW" => TrainCategory::Mixed,
920        "XC" => TrainCategory::ChannelTunnel,
921        "XD" => TrainCategory::Sleeper,
922        "XI" => TrainCategory::International,
923        "XR" => TrainCategory::Motorail,
924        "XU" => TrainCategory::UnadvertisedExpress,
925        "XX" => TrainCategory::ExpressPassenger,
926        "XZ" => TrainCategory::SleeperDomestic,
927        "BR" => TrainCategory::BusReplacementDueToEngineering,
928        "BS" => TrainCategory::BusWTTService,
929        "SS" => TrainCategory::Ship,
930        "EE" => TrainCategory::EmptyCoachingStock,
931        "EL" => TrainCategory::ECSLondonUnderground,
932        "ES" => TrainCategory::ECSAndStaff,
933        "JJ" => TrainCategory::Postal,
934        "PM" => TrainCategory::PostOfficeControlledParcels,
935        "PP" => TrainCategory::Parcels,
936        "PV" => TrainCategory::EmptyNPCCS,
937        "DD" => TrainCategory::Departmental,
938        "DH" => TrainCategory::CivilEngineer,
939        "DI" => TrainCategory::MechanicalAndElectricalEngineer,
940        "DQ" => TrainCategory::Stores,
941        "DT" => TrainCategory::Test,
942        "DY" => TrainCategory::SignalAndTelecommunicationsEngineer,
943        "ZB" => TrainCategory::LocomotiveAndBrakeVan,
944        "ZZ" => TrainCategory::LightLocomotive,
945        "J2" => TrainCategory::RfDAutomotiveComponents,
946        "H2" => TrainCategory::RfDAutomotiveVehicles,
947        "J3" => TrainCategory::RfDEdibleProducts,
948        "J4" => TrainCategory::RfDIndustrialMinerals,
949        "J5" => TrainCategory::RfDChemicals,
950        "J6" => TrainCategory::RfDBuildingMaterials,
951        "J8" => TrainCategory::RfDGeneralMerchandise,
952        "H8" => TrainCategory::RfDEuropean,
953        "J9" => TrainCategory::RfDFreightlinerContracts,
954        "H9" => TrainCategory::RfDFreightlinerOther,
955        "A0" => TrainCategory::CoalDistributive,
956        "E0" => TrainCategory::CoalElectricityMGR,
957        "B0" => TrainCategory::CoalOtherAndNuclear,
958        "B1" => TrainCategory::Metals,
959        "B4" => TrainCategory::Aggregates,
960        "B5" => TrainCategory::DomesticAndIndustrialWaste,
961        "B6" => TrainCategory::BuildingMaterials,
962        "B7" => TrainCategory::PetroleumProducts,
963        "H0" => TrainCategory::RfDEuropeanChannelTunnelMixed,
964        "H1" => TrainCategory::RfDEuropeanChannelTunnelIntermodal,
965        "H3" => TrainCategory::RfDEuropeanChannelTunnelAutomotive,
966        "H4" => TrainCategory::RfDEuropeanChannelTunnelContractServices,
967        "H5" => TrainCategory::RfDEuropeanChannelTunnelHaulmark,
968        "H6" => TrainCategory::RfDEuropeanChannelTunnelJointVenture,
969        _ => {
970            return Err(ScheduleApplyError::InvalidTrainCategory(
971                train_category.to_string(),
972            ))
973        }
974    };
975    schedule.headcode = train_identity.trim().to_string();
976    schedule.portion_id = *portion_id;
977    schedule.power_type = match power_type.trim() {
978        "" => PowerType::NotSpecified,
979        "D" => PowerType::Diesel,
980        "DEM" => PowerType::DieselElectricMultipleUnit,
981        "DMU" => PowerType::DieselMechanicalMultipleUnit,
982        "E" => PowerType::Electric,
983        "ED" => PowerType::ElectroDiesel,
984        "EML" => PowerType::EMUPlusLocomotive,
985        "EMU" => PowerType::ElectricMultipleUnit,
986        "HST" => PowerType::HighSpeedTrain,
987        _ => return Err(ScheduleApplyError::InvalidPowerType(power_type.to_string())),
988    };
989    schedule.timing_load = if schedule.power_type == PowerType::DieselMechanicalMultipleUnit
990        || schedule.power_type == PowerType::DieselElectricMultipleUnit
991    {
992        match timing_load.trim() {
993            "" => TimingLoad::NotSpecified,
994            "69" => TimingLoad::Class17201721Or1722,
995            "A" => TimingLoad::Class141To144,
996            "E" => TimingLoad::Class158168170Or175,
997            "N" => TimingLoad::Class1650,
998            "S" => TimingLoad::Class150153155Or156,
999            "T" => TimingLoad::Class1651Or166,
1000            "V" => TimingLoad::Class220Or221,
1001            "X" => TimingLoad::Class159,
1002            "D1" => TimingLoad::DMUPowerCarTrailer,
1003            "D2" => TimingLoad::DMU2PowerCarsTrailer,
1004            "D3" => TimingLoad::DMUPowerTwin,
1005            _ => {
1006                if let Ok(n) = timing_load.trim().parse::<u16>() {
1007                    TimingLoad::SpecificClass(n)
1008                } else {
1009                    return Err(ScheduleApplyError::InvalidTimingLoad(
1010                        timing_load.to_string(),
1011                    ));
1012                }
1013            }
1014        }
1015    } else if schedule.power_type == PowerType::ElectricMultipleUnit {
1016        match timing_load.trim() {
1017            "" => TimingLoad::NotSpecified,
1018            "AT" => TimingLoad::AcceleratedTimings,
1019            "E" => TimingLoad::Class458,
1020            "0" => TimingLoad::Class380,
1021            "506" => TimingLoad::Class3501110MPH,
1022            _ => {
1023                if let Ok(n) = timing_load.trim().parse::<u16>() {
1024                    TimingLoad::SpecificClass(n)
1025                } else {
1026                    return Err(ScheduleApplyError::InvalidTimingLoad(
1027                        timing_load.to_string(),
1028                    ));
1029                }
1030            }
1031        }
1032    } else if schedule.power_type == PowerType::Diesel
1033        || schedule.power_type == PowerType::Electric
1034        || schedule.power_type == PowerType::ElectroDiesel
1035    {
1036        if timing_load.trim().is_empty() {
1037            TimingLoad::NotSpecified
1038        } else if schedule.power_type == PowerType::Electric && timing_load.trim() == "325" {
1039            TimingLoad::Class325ElectricParcelsUnit
1040        } else if let Ok(n) = timing_load.trim().parse::<u16>() {
1041            TimingLoad::LoadInTonnes(n)
1042        } else {
1043            return Err(ScheduleApplyError::InvalidTimingLoad(
1044                timing_load.to_string(),
1045            ));
1046        }
1047    } else {
1048        TimingLoad::NotSpecified
1049    };
1050    schedule.speed = speed.trim().parse().ok().unwrap_or(0);
1051    for c in operating_characteristics.chars() {
1052        match c {
1053            'B' => schedule
1054                .operating_characteristics
1055                .push(OperatingCharacteristic::VacuumBraked),
1056            'C' => schedule
1057                .operating_characteristics
1058                .push(OperatingCharacteristic::TimedAt100MPH),
1059            'D' => schedule
1060                .operating_characteristics
1061                .push(OperatingCharacteristic::DOOCoachingStockTrains),
1062            'E' => schedule
1063                .operating_characteristics
1064                .push(OperatingCharacteristic::ConveysMark4Coaches),
1065            'G' => schedule
1066                .operating_characteristics
1067                .push(OperatingCharacteristic::GuardRequired),
1068            'M' => schedule
1069                .operating_characteristics
1070                .push(OperatingCharacteristic::TimedAt110MPH),
1071            'P' => schedule
1072                .operating_characteristics
1073                .push(OperatingCharacteristic::PushPullTrain),
1074            'Q' => schedule
1075                .operating_characteristics
1076                .push(OperatingCharacteristic::RunsAsRequired),
1077            'R' => schedule
1078                .operating_characteristics
1079                .push(OperatingCharacteristic::AirConditionedWithPASystem),
1080            'S' => schedule
1081                .operating_characteristics
1082                .push(OperatingCharacteristic::SteamHeated),
1083            'Y' => schedule
1084                .operating_characteristics
1085                .push(OperatingCharacteristic::RunsToTerminalsAsRequired),
1086            'Z' => schedule
1087                .operating_characteristics
1088                .push(OperatingCharacteristic::MayConveyTrafficToSB1CGauge),
1089            ' ' => (),
1090            _ => return Err(ScheduleApplyError::InvalidOperatingCharacteristic(c)),
1091        };
1092    }
1093    schedule.seating_class = match seating_class {
1094        ' ' => SeatingClass::FirstAndStandard,
1095        'B' => SeatingClass::FirstAndStandard,
1096        'S' => SeatingClass::StandardOnly,
1097        _ => return Err(ScheduleApplyError::InvalidSeatingClass(*seating_class)),
1098    };
1099    schedule.sleepers = match sleepers {
1100        'B' => Sleepers::FirstAndStandard,
1101        'F' => Sleepers::FirstOnly,
1102        'S' => Sleepers::StandardOnly,
1103        ' ' => Sleepers::NotSpecified,
1104        _ => return Err(ScheduleApplyError::InvalidSleepers(*sleepers)),
1105    };
1106    schedule.reservations = match reservations {
1107        'A' => Reservations::Compulsory,
1108        'E' => Reservations::CompulsoryForBicycles,
1109        'R' => Reservations::Recommended,
1110        'S' => Reservations::Possible,
1111        ' ' => Reservations::NotSpecified,
1112        _ => return Err(ScheduleApplyError::InvalidReservations(*reservations)),
1113    };
1114    for c in catering_code.chars() {
1115        match c {
1116            'C' => schedule.catering.push(Catering::BuffetService),
1117            'F' => schedule.catering.push(Catering::RestaurantCarForFirstClass),
1118            'H' => schedule.catering.push(Catering::HotFood),
1119            'M' => schedule.catering.push(Catering::MealForFirstClass),
1120            'P' => schedule.catering.push(Catering::WheelchairReservations),
1121            'R' => schedule.catering.push(Catering::Restaurant),
1122            'T' => schedule.catering.push(Catering::TrolleyService),
1123            ' ' => (),
1124            _ => return Err(ScheduleApplyError::InvalidCateringCode(c)),
1125        };
1126    }
1127    schedule.stp_indicator = match stp_indicator {
1128        'C' => STPIndicator::STPCancellationOfPermanentAssociation,
1129        'N' => STPIndicator::NewSTPAssociation,
1130        'O' => STPIndicator::STPOverlayOfPermanentAssociation,
1131        'P' => STPIndicator::PermanentAssociation,
1132        _ => return Err(ScheduleApplyError::InvalidSTPIndicator(*stp_indicator)),
1133    };
1134    Ok(())
1135}