1#![allow(clippy::redundant_pub_crate, reason = "re-exported as pub lol")]
4
5use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, DateTime, Utc, TimeDelta, Timelike};
6use derive_more::{Display, FromStr};
7use serde::de::Error;
8use serde::{Deserialize, Deserializer};
9use std::fmt::{Debug, Display, Formatter};
10use std::num::{ParseFloatError, ParseIntError};
11use std::ops::{Add, RangeInclusive};
12use std::str::FromStr;
13use std::ops::Not;
14use thiserror::Error;
15use crate::season::SeasonId;
16
17#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
19#[serde(from = "__CopyrightStruct")]
20pub enum Copyright {
21 Typical {
23 year: u32,
25 },
26 UnknownSpec(Box<str>),
28}
29
30#[derive(Deserialize)]
31#[doc(hidden)]
32struct __CopyrightStruct(String);
33
34impl From<__CopyrightStruct> for Copyright {
35 fn from(value: __CopyrightStruct) -> Self {
36 let __CopyrightStruct(value) = value;
37 if let Some(value) = value.strip_prefix("Copyright ") && let Some(value) = value.strip_suffix(" MLB Advanced Media, L.P. Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt") && let Ok(year) = value.parse::<u32>() {
38 Self::Typical { year }
39 } else {
40 Self::UnknownSpec(value.into_boxed_str())
41 }
42 }
43}
44
45impl Display for Copyright {
46 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Typical { year } => write!(f, "Copyright {year} MLB Advanced Media, L.P. Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt"),
49 Self::UnknownSpec(copyright) => write!(f, "{copyright}"),
50 }
51 }
52}
53
54impl Default for Copyright {
55 #[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
56 fn default() -> Self {
57 Self::Typical { year: Local::now().year() as _ }
58 }
59}
60
61pub fn try_from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Option<T>, D::Error> {
65 Ok(String::deserialize(deserializer)?.parse::<T>().ok())
66}
67
68pub fn from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<T, D::Error>
74where
75 <T as FromStr>::Err: Debug,
76{
77 String::deserialize(deserializer)?.parse::<T>().map_err(|e| Error::custom(format!("{e:?}")))
78}
79
80pub fn from_yes_no<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
85 #[derive(Deserialize)]
86 #[repr(u8)]
87 enum Boolean {
88 #[serde(rename = "Y")]
89 Yes = 1,
90 #[serde(rename = "N")]
91 No = 0,
92 }
93
94 Ok(match Boolean::deserialize(deserializer)? {
95 Boolean::Yes => true,
96 Boolean::No => false,
97 })
98}
99
100#[derive(Debug, PartialEq, Eq, Copy, Clone)]
104pub enum HeightMeasurement {
105 FeetAndInches { feet: u8, inches: u8 },
107 Centimeters { cm: u16 },
109}
110
111impl FromStr for HeightMeasurement {
112 type Err = HeightMeasurementParseError;
113
114 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 if let Some((feet, Some((inches, "")))) = s.split_once("' ").map(|(feet, rest)| (feet, rest.split_once('"'))) {
119 let feet = feet.parse::<u8>()?;
120 let inches = inches.parse::<u8>()?;
121 Ok(Self::FeetAndInches { feet, inches })
122 } else if let Some((cm, "")) = s.split_once("cm") {
123 let cm = cm.parse::<u16>()?;
124 Ok(Self::Centimeters { cm })
125 } else {
126 Err(HeightMeasurementParseError::UnknownSpec(s.to_owned()))
127 }
128 }
129}
130
131impl<'de> Deserialize<'de> for HeightMeasurement {
132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133 where
134 D: Deserializer<'de>
135 {
136 String::deserialize(deserializer)?.parse().map_err(D::Error::custom)
137 }
138}
139
140#[derive(Debug, Error)]
142pub enum HeightMeasurementParseError {
143 #[error(transparent)]
145 ParseIntError(#[from] ParseIntError),
146 #[error("Unknown height '{0}'")]
148 UnknownSpec(String),
149}
150
151#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, Default)]
153pub enum PlayerPool {
154 #[default]
156 #[display("ALL")]
157 All,
158 #[display("QUALIFIED")]
160 Qualified,
161 #[display("ROOKIES")]
163 Rookies,
164 #[display("QUALIFIED_ROOKIES")]
166 QualifiedAndRookies,
167 #[display("ORGANIZATION")]
169 Organization,
170 #[display("ORGANIZATION_NO_MLB")]
172 OrganizationNotMlb,
173 #[display("CURRENT")]
175 Current,
176 #[display("ALL_CURRENT")]
178 AllCurrent,
179 #[display("QUALIFIED_CURRENT")]
181 QualifiedAndCurrent,
182}
183
184#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
188pub enum Gender {
189 #[serde(rename = "M")]
190 Male,
191 #[serde(rename = "F")]
192 Female,
193 #[default]
194 #[serde(other)]
195 Other,
196}
197
198#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
202#[serde(try_from = "__HandednessStruct")]
203pub enum Handedness {
204 #[display("L")]
205 Left,
206 #[display("R")]
207 Right,
208 #[display("S")]
209 Switch,
210}
211
212#[derive(Deserialize)]
213#[doc(hidden)]
214struct __HandednessStruct {
215 code: String,
216}
217
218#[derive(Debug, Error)]
220pub enum HandednessParseError {
221 #[error("Invalid handedness '{0}'")]
223 InvalidHandedness(String),
224}
225
226impl TryFrom<__HandednessStruct> for Handedness {
227 type Error = HandednessParseError;
228
229 fn try_from(value: __HandednessStruct) -> Result<Self, Self::Error> {
230 Ok(match &*value.code {
231 "L" => Self::Left,
232 "R" => Self::Right,
233 "S" => Self::Switch,
234 _ => return Err(HandednessParseError::InvalidHandedness(value.code)),
235 })
236 }
237}
238
239pub type NaiveDateRange = RangeInclusive<NaiveDate>;
246
247pub(crate) const MLB_API_DATE_FORMAT: &str = "%m/%d/%Y";
248
249pub(crate) fn deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<DateTime<Utc>, D::Error> {
253 let string = String::deserialize(deserializer)?;
254 let fmt = match (string.ends_with('Z'), string.contains('.')) {
255 (false, false) => "%FT%TZ%#z",
256 (false, true) => "%FT%TZ%.3f%#z",
257 (true, false) => "%FT%TZ",
258 (true, true) => "%FT%T%.3fZ",
259 };
260 NaiveDateTime::parse_from_str(&string, fmt).map(|x| x.and_utc()).map_err(D::Error::custom)
261}
262
263#[allow(clippy::unnecessary_wraps, reason = "serde return type")]
266pub(crate) fn try_deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error> {
267 Ok(deserialize_datetime(deserializer).ok())
268}
269
270pub(crate) fn deserialize_comma_separated_vec<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Vec<T>, D::Error>
274where
275 <T as FromStr>::Err: Debug,
276{
277 String::deserialize(deserializer)?
278 .split(", ")
279 .map(|entry| T::from_str(entry))
280 .collect::<Result<Vec<T>, <T as FromStr>::Err>>()
281 .map_err(|e| Error::custom(format!("{e:?}")))
282}
283
284#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
285pub enum TeamSide {
286 #[default]
287 Home,
288 Away,
289}
290
291impl Not for TeamSide {
292 type Output = Self;
293
294 fn not(self) -> Self::Output {
295 match self {
296 Self::Home => Self::Away,
297 Self::Away => Self::Home,
298 }
299 }
300}
301
302impl TeamSide {
303 #[must_use]
304 pub const fn is_home(self) -> bool {
305 matches!(self, Self::Home)
306 }
307
308 #[must_use]
309 pub const fn is_away(self) -> bool {
310 matches!(self, Self::Away)
311 }
312}
313
314pub fn deserialize_team_side_from_is_home<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TeamSide, D::Error> {
315 Ok(if bool::deserialize(deserializer)? { TeamSide::Home } else { TeamSide::Away })
316}
317
318#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
328pub struct HomeAway<T> {
329 pub home: T,
330 pub away: T,
331}
332
333impl<T> HomeAway<T> {
334 #[must_use]
336 pub const fn new(home: T, away: T) -> Self {
337 Self { home, away }
338 }
339
340 #[must_use]
342 pub fn choose(self, side: TeamSide) -> T {
343 match side {
344 TeamSide::Home => self.home,
345 TeamSide::Away => self.away,
346 }
347 }
348
349 #[must_use]
350 pub const fn as_ref(&self) -> HomeAway<&T> {
351 HomeAway {
352 home: &self.home,
353 away: &self.away,
354 }
355 }
356
357 #[must_use]
358 pub const fn as_mut(&mut self) -> HomeAway<&mut T> {
359 HomeAway {
360 home: &mut self.home,
361 away: &mut self.away,
362 }
363 }
364
365 #[must_use]
366 pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> HomeAway<U> {
367 HomeAway {
368 home: f(self.home),
369 away: f(self.away),
370 }
371 }
372
373 #[must_use]
375 pub fn swap(self) -> Self {
376 Self {
377 home: self.away,
378 away: self.home,
379 }
380 }
381
382 #[must_use]
383 pub fn zip<U>(self, other: HomeAway<U>) -> HomeAway<(T, U)> {
384 HomeAway {
385 home: (self.home, other.home),
386 away: (self.away, other.away),
387 }
388 }
389
390 #[must_use]
391 pub fn zip_with<U, V, F: FnMut(T, U) -> V>(self, other: HomeAway<U>, mut f: F) -> HomeAway<V> {
392 HomeAway {
393 home: f(self.home, other.home),
394 away: f(self.away, other.away),
395 }
396 }
397
398 #[must_use]
399 pub fn combine<U, F: FnOnce(T, T) -> U>(self, f: F) -> U {
400 f(self.home, self.away)
401 }
402
403 #[must_use]
405 pub fn added(self) -> <T as Add>::Output where T: Add {
406 self.home + self.away
407 }
408
409 #[must_use]
410 pub fn both(self, mut f: impl FnMut(T) -> bool) -> bool {
411 f(self.home) && f(self.away)
412 }
413
414 #[must_use]
415 pub fn either(self, mut f: impl FnMut(T) -> bool) -> bool {
416 f(self.home) || f(self.away)
417 }
418}
419
420impl<T> HomeAway<Option<T>> {
421 #[must_use]
422 pub fn flatten(self) -> Option<HomeAway<T>> {
423 Some(HomeAway {
424 home: self.home?,
425 away: self.away?,
426 })
427 }
428}
429
430impl<T> From<(T, T)> for HomeAway<T> {
431 fn from((home, away): (T, T)) -> Self {
432 Self {
433 home,
434 away
435 }
436 }
437}
438
439#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
443#[serde(rename_all = "camelCase")]
444pub struct Location {
445 pub address_line_1: Option<String>,
446 pub address_line_2: Option<String>,
447 pub address_line_3: Option<String>,
448 pub address_line_4: Option<String>,
449 pub attention: Option<String>,
450 #[serde(alias = "phone")]
451 pub phone_number: Option<String>,
452 pub city: Option<String>,
453 #[serde(alias = "province")]
454 pub state: Option<String>,
455 pub country: Option<String>,
456 #[serde(rename = "stateAbbrev")] pub state_abbreviation: Option<String>,
457 pub postal_code: Option<String>,
458 pub latitude: Option<f64>,
459 pub longitude: Option<f64>,
460 pub azimuth_angle: Option<f64>,
461 pub elevation: Option<u32>,
462}
463
464#[derive(Debug, Deserialize, PartialEq, Clone)]
466#[serde(rename_all = "camelCase")]
467pub struct FieldInfo {
468 pub capacity: u32,
469 pub turf_type: TurfType,
470 pub roof_type: RoofType,
471 pub left_line: Option<u32>,
472 pub left: Option<u32>,
473 pub left_center: Option<u32>,
474 pub center: Option<u32>,
475 pub right_center: Option<u32>,
476 pub right: Option<u32>,
477 pub right_line: Option<u32>,
478}
479
480#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Display)]
482pub enum TurfType {
483 #[serde(rename = "Artificial Turf")]
484 #[display("Artificial Turf")]
485 ArtificialTurf,
486
487 #[serde(rename = "Grass")]
488 #[display("Grass")]
489 Grass,
490}
491
492#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Display)]
494pub enum RoofType {
495 #[serde(rename = "Retractable")]
496 #[display("Retractable")]
497 Retractable,
498
499 #[serde(rename = "Open")]
500 #[display("Open")]
501 Open,
502
503 #[serde(rename = "Dome")]
504 #[display("Dome")]
505 Dome,
506}
507
508#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
510#[serde(rename_all = "camelCase")]
511pub struct TimeZoneData {
512 #[serde(rename = "id")]
513 pub timezone: chrono_tz::Tz,
514 pub offset: i32,
515 pub offset_at_game_time: i32,
516}
517
518#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
520pub struct ExternalReference {
521 #[serde(rename = "xrefId")]
522 pub id: String,
523 #[serde(rename = "xrefType")]
524 pub xref_type: String,
525 pub season: Option<SeasonId>,
526}
527
528#[derive(Debug, Deserialize, PartialEq, Clone)]
530#[serde(rename_all = "camelCase")]
531pub struct TrackingSystem {
532 pub id: TrackingSystemVendorId,
533 pub description: String,
534 pub pitch_vendor: Option<TrackingSystemVendor>,
535 pub hit_vendor: Option<TrackingSystemVendor>,
536 pub player_vendor: Option<TrackingSystemVendor>,
537 pub skeletal_vendor: Option<TrackingSystemVendor>,
538 pub bat_vendor: Option<TrackingSystemVendor>,
539 pub biomechanics_vendor: Option<TrackingSystemVendor>,
540}
541
542id!(TrackingSystemVendorId { id: u32 });
543
544#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
546pub struct TrackingSystemVendor {
547 pub id: TrackingSystemVendorId,
548 pub description: String,
549 #[serde(rename = "version")]
550 pub details: String,
551}
552
553#[derive(Debug, Copy, Clone)]
557pub enum IntegerOrFloatStat {
558 Integer(i64),
560 Float(f64),
562}
563
564impl PartialEq for IntegerOrFloatStat {
565 fn eq(&self, other: &Self) -> bool {
566 match (*self, *other) {
567 (Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
568 (Self::Float(lhs), Self::Float(rhs)) => lhs == rhs,
569
570 #[allow(clippy::cast_precision_loss, reason = "we checked if it's perfectly representable")]
571 #[allow(clippy::cast_possible_truncation, reason = "we checked if it's perfectly representable")]
572 (Self::Integer(int), Self::Float(float)) | (Self::Float(float), Self::Integer(int)) => {
573 if float.is_normal() && float.floor() == float && (i64::MIN as f64..-(i64::MIN as f64)).contains(&float) {
576 float as i64 == int
577 } else {
578 false
579 }
580 },
581 }
582 }
583}
584
585impl<'de> Deserialize<'de> for IntegerOrFloatStat {
586 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
587 where
588 D: Deserializer<'de>
589 {
590 struct Visitor;
591
592 impl serde::de::Visitor<'_> for Visitor {
593 type Value = IntegerOrFloatStat;
594
595 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
596 formatter.write_str("integer, float, or string that can be parsed to either")
597 }
598
599 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
600 where
601 E: Error,
602 {
603 Ok(IntegerOrFloatStat::Integer(v))
604 }
605
606 fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
607 where
608 E: Error,
609 {
610 Ok(IntegerOrFloatStat::Float(v))
611 }
612
613 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
614 where
615 E: Error,
616 {
617 if v == "-.--" || v == ".---" {
618 Ok(IntegerOrFloatStat::Float(0.0))
619 } else if let Ok(i) = v.parse::<i64>() {
620 Ok(IntegerOrFloatStat::Integer(i))
621 } else if let Ok(f) = v.parse::<f64>() {
622 Ok(IntegerOrFloatStat::Float(f))
623 } else {
624 Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
625 }
626 }
627 }
628
629 deserializer.deserialize_any(Visitor)
630 }
631}
632
633#[derive(Debug, Deserialize, Display)]
637#[display("An error occurred parsing the statsapi http request: {message}")]
638pub struct MLBError {
639 pub(crate) message: String,
640}
641
642impl MLBError {
643 pub fn new(message: impl Into<String>) -> Self {
644 Self { message: message.into() }
645 }
646}
647
648impl std::error::Error for MLBError {}
649
650#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
652#[serde(try_from = "&str")]
653pub struct RGBAColor {
654 pub red: u8,
655 pub green: u8,
656 pub blue: u8,
657 pub alpha: u8,
658}
659
660impl Display for RGBAColor {
661 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
662 write!(f, "0x{:02x}{:02x}{:02x}{:02x}", self.alpha, self.red, self.green, self.blue)
663 }
664}
665
666#[derive(Debug, Error)]
668pub enum RGBAColorFromStrError {
669 #[error("Invalid spec")]
670 InvalidFormat,
671 #[error(transparent)]
672 InvalidInt(#[from] ParseIntError),
673 #[error(transparent)]
674 InvalidFloat(#[from] ParseFloatError),
675}
676
677impl<'a> TryFrom<&'a str> for RGBAColor {
678 type Error = <Self as FromStr>::Err;
679
680 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
681 <Self as FromStr>::from_str(value)
682 }
683}
684
685impl FromStr for RGBAColor {
686 type Err = RGBAColorFromStrError;
687
688 #[allow(clippy::single_char_pattern, reason = "other patterns are strings, the choice to make that one a char does not denote any special case")]
690 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "intended behaviour with alpha channel")]
691 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
692 s = s.strip_suffix("rgba(").ok_or(Self::Err::InvalidFormat)?;
693 let (red, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
694 let red = red.parse::<u8>()?;
695 let (green, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
696 let green = green.parse::<u8>()?;
697 let (blue, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
698 let blue = blue.parse::<u8>()?;
699 let (alpha, s) = s.split_once(")").ok_or(Self::Err::InvalidFormat)?;
700 let alpha = (alpha.parse::<f32>()? * 255.0).round() as u8;
701 if !s.is_empty() { return Err(Self::Err::InvalidFormat); }
702 Ok(Self {
703 red,
704 green,
705 blue,
706 alpha
707 })
708 }
709}
710
711#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
713#[serde(try_from = "&str")]
714pub enum HeatmapTemperature {
715 Hot,
716 Warm,
717 Lukewarm,
718 Cool,
719 Cold,
720}
721
722impl<'a> TryFrom<&'a str> for HeatmapTemperature {
723 type Error = <Self as FromStr>::Err;
724
725 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
726 <Self as FromStr>::from_str(value)
727 }
728}
729
730pub(crate) fn write_nth(n: usize, f: &mut Formatter<'_>) -> std::fmt::Result {
731 write!(f, "{n}")?;
732 let (tens, ones) = (n / 10, n % 10);
733 let is_teen = (tens % 10) == 1;
734 if is_teen {
735 write!(f, "th")?;
736 } else {
737 write!(f, "{}", match ones {
738 1 => "st",
739 2 => "nd",
740 3 => "rd",
741 _ => "th",
742 })?;
743 }
744 Ok(())
745}
746
747pub fn deserialize_time_delta_from_hms<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TimeDelta, D::Error> {
750 let string = String::deserialize(deserializer)?;
751 let (hour, rest) = string.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
752 let (minute, second) = rest.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
753 let hour = hour.parse::<u32>().map_err(D::Error::custom)?;
754 let minute = minute.parse::<u32>().map_err(D::Error::custom)?;
755 let second = second.parse::<u32>().map_err(D::Error::custom)?;
756
757 TimeDelta::new(((hour * 24 + minute) * 60 + second) as _, 0).ok_or_else(|| D::Error::custom("Invalid time quantity, overflow."))
758}
759
760#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
762#[serde(try_from = "&str")]
763pub enum DayHalf {
764 AM,
765 PM,
766}
767
768impl DayHalf {
769 #[must_use]
771 pub fn into_24_hour_time(self, mut time: NaiveTime) -> NaiveTime {
772 if (self == Self::PM) ^ (time.hour() == 12) {
773 time += TimeDelta::hours(12);
774 }
775
776 time
777 }
778}
779
780impl<'a> TryFrom<&'a str> for DayHalf {
781 type Error = <Self as FromStr>::Err;
782
783 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
784 <Self as FromStr>::from_str(value)
785 }
786}
787
788#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
789#[serde(rename_all = "camelCase")]
790pub struct ResourceUsage {
791 pub used: u32,
792 pub remaining: u32,
793}
794
795#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
796#[serde(rename_all = "camelCase")]
797pub struct ResultHoldingResourceUsage {
798 #[serde(rename = "usedSuccessful")]
799 pub used_successfully: u32,
800 pub used_failed: u32,
801 pub remaining: u32,
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807
808 #[test]
809 fn test_ampm() {
810 assert_eq!(NaiveTime::from_hms_opt(0, 0, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
811 assert_eq!(NaiveTime::from_hms_opt(12, 0, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
812 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
813 assert_eq!(NaiveTime::from_hms_opt(12, 1, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
814 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
815 assert_eq!(NaiveTime::from_hms_opt(23, 59, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(11, 59, 0).unwrap()));
816 assert_eq!(NaiveTime::from_hms_opt(1, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(1, 1, 0).unwrap()));
817 }
818}