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 #[getset(get = "pub")]
55 tiplocs: HashMap<String, TIPLOC>,
56 #[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 pub fn new() -> Self {
72 Self {
73 extract_date_time: NaiveDateTime::MIN,
74 tiplocs: HashMap::new(),
75 schedules: HashMap::new(),
76 }
77 }
78
79 pub fn apply_file(&mut self, file: &CIFFile) -> Vec<(usize, ScheduleApplyError)> {
82 self.apply_records(file.records())
83 }
84
85 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 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 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 *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 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 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 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 #[getset(get = "pub")]
478 tiploc: String,
479 #[getset(get = "pub")]
481 three_alpha_code: String,
482 #[getset(get = "pub")]
484 description: String,
485 #[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 #[getset(get = "pub")]
495 train_uid: String,
496 #[getset(get = "pub")]
498 runs_from: NaiveDate,
499 #[getset(get = "pub")]
501 runs_to: NaiveDate,
502 #[getset(get = "pub")]
504 days_run: DaysRun,
505 #[getset(get = "pub")]
507 bank_holiday_running: BankHolidayRunning,
508 #[getset(get = "pub")]
510 atoc_code: String,
511 #[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 NotSpecified,
709 Class17201721Or1722,
711 Class141To144,
713 Class158168170Or175,
715 Class1650,
717 Class150153155Or156,
719 Class1651Or166,
721 Class220Or221,
723 Class159,
725 DMUPowerCarTrailer,
727 DMU2PowerCarsTrailer,
729 DMUPowerTwin,
731 AcceleratedTimings,
733 Class458,
735 Class380,
737 Class3501110MPH,
739 Class325ElectricParcelsUnit,
741 SpecificClass(u16),
743 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}