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")]
203#[display("{}", self.into_char())]
204pub enum Handedness {
205 Left,
206 Right,
207 Switch,
208}
209
210impl Handedness {
211 #[must_use]
212 pub const fn into_char(self) -> char {
213 match self {
214 Self::Left => 'L',
215 Self::Right => 'R',
216 Self::Switch => 'S',
217 }
218 }
219}
220
221#[derive(Deserialize)]
222#[doc(hidden)]
223struct __HandednessStruct {
224 code: String,
225}
226
227#[derive(Debug, Error)]
229pub enum HandednessParseError {
230 #[error("Invalid handedness '{0}'")]
232 InvalidHandedness(String),
233}
234
235impl TryFrom<__HandednessStruct> for Handedness {
236 type Error = HandednessParseError;
237
238 fn try_from(value: __HandednessStruct) -> Result<Self, Self::Error> {
239 Ok(match &*value.code {
240 "L" => Self::Left,
241 "R" => Self::Right,
242 "S" => Self::Switch,
243 _ => return Err(HandednessParseError::InvalidHandedness(value.code)),
244 })
245 }
246}
247
248pub type NaiveDateRange = RangeInclusive<NaiveDate>;
255
256pub(crate) const MLB_API_DATE_FORMAT: &str = "%m/%d/%Y";
257
258pub(crate) fn deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<DateTime<Utc>, D::Error> {
262 let string = String::deserialize(deserializer)?;
263 let fmt = match (string.ends_with('Z'), string.contains('.')) {
264 (false, false) => "%FT%TZ%#z",
265 (false, true) => "%FT%TZ%.3f%#z",
266 (true, false) => "%FT%TZ",
267 (true, true) => "%FT%T%.3fZ",
268 };
269 NaiveDateTime::parse_from_str(&string, fmt).map(|x| x.and_utc()).map_err(D::Error::custom)
270}
271
272#[allow(clippy::unnecessary_wraps, reason = "serde return type")]
275pub(crate) fn try_deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error> {
276 Ok(deserialize_datetime(deserializer).ok())
277}
278
279pub(crate) fn deserialize_comma_separated_vec<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Vec<T>, D::Error>
283where
284 <T as FromStr>::Err: Debug,
285{
286 String::deserialize(deserializer)?
287 .split(", ")
288 .map(|entry| T::from_str(entry))
289 .collect::<Result<Vec<T>, <T as FromStr>::Err>>()
290 .map_err(|e| Error::custom(format!("{e:?}")))
291}
292
293#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
294pub enum TeamSide {
295 #[default]
296 Home,
297 Away,
298}
299
300impl Not for TeamSide {
301 type Output = Self;
302
303 fn not(self) -> Self::Output {
304 match self {
305 Self::Home => Self::Away,
306 Self::Away => Self::Home,
307 }
308 }
309}
310
311impl TeamSide {
312 #[must_use]
313 pub const fn is_home(self) -> bool {
314 matches!(self, Self::Home)
315 }
316
317 #[must_use]
318 pub const fn is_away(self) -> bool {
319 matches!(self, Self::Away)
320 }
321}
322
323pub fn deserialize_team_side_from_is_home<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TeamSide, D::Error> {
324 Ok(if bool::deserialize(deserializer)? { TeamSide::Home } else { TeamSide::Away })
325}
326
327#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
337pub struct HomeAway<T> {
338 pub home: T,
339 pub away: T,
340}
341
342impl<T> HomeAway<T> {
343 #[must_use]
345 pub const fn new(home: T, away: T) -> Self {
346 Self { home, away }
347 }
348
349 #[must_use]
351 pub fn choose(self, side: TeamSide) -> T {
352 match side {
353 TeamSide::Home => self.home,
354 TeamSide::Away => self.away,
355 }
356 }
357
358 #[must_use]
359 pub const fn as_ref(&self) -> HomeAway<&T> {
360 HomeAway {
361 home: &self.home,
362 away: &self.away,
363 }
364 }
365
366 #[must_use]
367 pub const fn as_mut(&mut self) -> HomeAway<&mut T> {
368 HomeAway {
369 home: &mut self.home,
370 away: &mut self.away,
371 }
372 }
373
374 #[must_use]
375 pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> HomeAway<U> {
376 HomeAway {
377 home: f(self.home),
378 away: f(self.away),
379 }
380 }
381
382 #[must_use]
384 pub fn swap(self) -> Self {
385 Self {
386 home: self.away,
387 away: self.home,
388 }
389 }
390
391 #[must_use]
392 pub fn zip<U>(self, other: HomeAway<U>) -> HomeAway<(T, U)> {
393 HomeAway {
394 home: (self.home, other.home),
395 away: (self.away, other.away),
396 }
397 }
398
399 #[must_use]
400 pub fn zip_with<U, V, F: FnMut(T, U) -> V>(self, other: HomeAway<U>, mut f: F) -> HomeAway<V> {
401 HomeAway {
402 home: f(self.home, other.home),
403 away: f(self.away, other.away),
404 }
405 }
406
407 #[must_use]
408 pub fn combine<U, F: FnOnce(T, T) -> U>(self, f: F) -> U {
409 f(self.home, self.away)
410 }
411
412 #[must_use]
414 pub fn added(self) -> <T as Add>::Output where T: Add {
415 self.home + self.away
416 }
417
418 #[must_use]
419 pub fn both(self, mut f: impl FnMut(T) -> bool) -> bool {
420 f(self.home) && f(self.away)
421 }
422
423 #[must_use]
424 pub fn either(self, mut f: impl FnMut(T) -> bool) -> bool {
425 f(self.home) || f(self.away)
426 }
427}
428
429impl<T> HomeAway<Option<T>> {
430 #[must_use]
431 pub fn flatten(self) -> Option<HomeAway<T>> {
432 Some(HomeAway {
433 home: self.home?,
434 away: self.away?,
435 })
436 }
437}
438
439impl<T> From<(T, T)> for HomeAway<T> {
440 fn from((home, away): (T, T)) -> Self {
441 Self {
442 home,
443 away
444 }
445 }
446}
447
448#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
452#[serde(rename_all = "camelCase")]
453pub struct Location {
454 pub address_line_1: Option<String>,
455 pub address_line_2: Option<String>,
456 pub address_line_3: Option<String>,
457 pub address_line_4: Option<String>,
458 pub attention: Option<String>,
459 #[serde(alias = "phone")]
460 pub phone_number: Option<String>,
461 pub city: Option<String>,
462 #[serde(alias = "province")]
463 pub state: Option<String>,
464 pub country: Option<String>,
465 #[serde(rename = "stateAbbrev")] pub state_abbreviation: Option<String>,
466 pub postal_code: Option<String>,
467 pub latitude: Option<f64>,
468 pub longitude: Option<f64>,
469 pub azimuth_angle: Option<f64>,
470 pub elevation: Option<u32>,
471}
472
473#[derive(Debug, Deserialize, PartialEq, Clone)]
475#[serde(rename_all = "camelCase")]
476pub struct FieldInfo {
477 pub capacity: u32,
478 pub turf_type: TurfType,
479 pub roof_type: RoofType,
480 pub left_line: Option<u32>,
481 pub left: Option<u32>,
482 pub left_center: Option<u32>,
483 pub center: Option<u32>,
484 pub right_center: Option<u32>,
485 pub right: Option<u32>,
486 pub right_line: Option<u32>,
487}
488
489#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Display)]
491pub enum TurfType {
492 #[serde(rename = "Artificial Turf")]
493 #[display("Artificial Turf")]
494 ArtificialTurf,
495
496 #[serde(rename = "Grass")]
497 #[display("Grass")]
498 Grass,
499}
500
501#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Display)]
503pub enum RoofType {
504 #[serde(rename = "Retractable")]
505 #[display("Retractable")]
506 Retractable,
507
508 #[serde(rename = "Open")]
509 #[display("Open")]
510 Open,
511
512 #[serde(rename = "Dome")]
513 #[display("Dome")]
514 Dome,
515}
516
517#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
519#[serde(rename_all = "camelCase")]
520pub struct TimeZoneData {
521 #[serde(rename = "id")]
522 pub timezone: chrono_tz::Tz,
523 pub offset: i32,
524 pub offset_at_game_time: i32,
525}
526
527#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
529pub struct ExternalReference {
530 #[serde(rename = "xrefId")]
531 pub id: String,
532 #[serde(rename = "xrefType")]
533 pub xref_type: String,
534 pub season: Option<SeasonId>,
535}
536
537#[derive(Debug, Deserialize, PartialEq, Clone)]
539#[serde(rename_all = "camelCase")]
540pub struct TrackingSystem {
541 pub id: TrackingSystemVendorId,
542 pub description: String,
543 pub pitch_vendor: Option<TrackingSystemVendor>,
544 pub hit_vendor: Option<TrackingSystemVendor>,
545 pub player_vendor: Option<TrackingSystemVendor>,
546 pub skeletal_vendor: Option<TrackingSystemVendor>,
547 pub bat_vendor: Option<TrackingSystemVendor>,
548 pub biomechanics_vendor: Option<TrackingSystemVendor>,
549}
550
551id!(TrackingSystemVendorId { id: u32 });
552
553#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
555pub struct TrackingSystemVendor {
556 pub id: TrackingSystemVendorId,
557 pub description: String,
558 #[serde(rename = "version")]
559 pub details: String,
560}
561
562#[derive(Debug, Copy, Clone)]
566pub enum IntegerOrFloatStat {
567 Integer(i64),
569 Float(f64),
571}
572
573impl PartialEq for IntegerOrFloatStat {
574 fn eq(&self, other: &Self) -> bool {
575 match (*self, *other) {
576 (Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
577 (Self::Float(lhs), Self::Float(rhs)) => lhs == rhs,
578
579 #[allow(clippy::cast_precision_loss, reason = "we checked if it's perfectly representable")]
580 #[allow(clippy::cast_possible_truncation, reason = "we checked if it's perfectly representable")]
581 (Self::Integer(int), Self::Float(float)) | (Self::Float(float), Self::Integer(int)) => {
582 if float.is_normal() && float.floor() == float && (i64::MIN as f64..-(i64::MIN as f64)).contains(&float) {
585 float as i64 == int
586 } else {
587 false
588 }
589 },
590 }
591 }
592}
593
594impl<'de> Deserialize<'de> for IntegerOrFloatStat {
595 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
596 where
597 D: Deserializer<'de>
598 {
599 struct Visitor;
600
601 impl serde::de::Visitor<'_> for Visitor {
602 type Value = IntegerOrFloatStat;
603
604 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
605 formatter.write_str("integer, float, or string that can be parsed to either")
606 }
607
608 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
609 where
610 E: Error,
611 {
612 Ok(IntegerOrFloatStat::Integer(v))
613 }
614
615 fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
616 where
617 E: Error,
618 {
619 Ok(IntegerOrFloatStat::Float(v))
620 }
621
622 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
623 where
624 E: Error,
625 {
626 if v == "-.--" || v == ".---" {
627 Ok(IntegerOrFloatStat::Float(0.0))
628 } else if let Ok(i) = v.parse::<i64>() {
629 Ok(IntegerOrFloatStat::Integer(i))
630 } else if let Ok(f) = v.parse::<f64>() {
631 Ok(IntegerOrFloatStat::Float(f))
632 } else {
633 Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
634 }
635 }
636 }
637
638 deserializer.deserialize_any(Visitor)
639 }
640}
641
642#[derive(Debug, Deserialize, Display)]
646#[display("An error occurred parsing the statsapi http request: {message}")]
647pub struct MLBError {
648 pub(crate) message: String,
649}
650
651impl MLBError {
652 pub fn new(message: impl Into<String>) -> Self {
653 Self { message: message.into() }
654 }
655}
656
657impl std::error::Error for MLBError {}
658
659#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
661#[serde(try_from = "&str")]
662pub struct RGBAColor {
663 pub red: u8,
664 pub green: u8,
665 pub blue: u8,
666 pub alpha: u8,
667}
668
669impl Display for RGBAColor {
670 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
671 write!(f, "0x{:02x}{:02x}{:02x}{:02x}", self.alpha, self.red, self.green, self.blue)
672 }
673}
674
675#[derive(Debug, Error)]
677pub enum RGBAColorFromStrError {
678 #[error("Invalid spec")]
679 InvalidFormat,
680 #[error(transparent)]
681 InvalidInt(#[from] ParseIntError),
682 #[error(transparent)]
683 InvalidFloat(#[from] ParseFloatError),
684}
685
686impl<'a> TryFrom<&'a str> for RGBAColor {
687 type Error = <Self as FromStr>::Err;
688
689 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
690 <Self as FromStr>::from_str(value)
691 }
692}
693
694impl FromStr for RGBAColor {
695 type Err = RGBAColorFromStrError;
696
697 #[allow(clippy::single_char_pattern, reason = "other patterns are strings, the choice to make that one a char does not denote any special case")]
699 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "intended behaviour with alpha channel")]
700 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
701 s = s.strip_suffix("rgba(").ok_or(Self::Err::InvalidFormat)?;
702 let (red, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
703 let red = red.parse::<u8>()?;
704 let (green, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
705 let green = green.parse::<u8>()?;
706 let (blue, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
707 let blue = blue.parse::<u8>()?;
708 let (alpha, s) = s.split_once(")").ok_or(Self::Err::InvalidFormat)?;
709 let alpha = (alpha.parse::<f32>()? * 255.0).round() as u8;
710 if !s.is_empty() { return Err(Self::Err::InvalidFormat); }
711 Ok(Self {
712 red,
713 green,
714 blue,
715 alpha
716 })
717 }
718}
719
720#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
722#[serde(try_from = "&str")]
723pub enum HeatmapTemperature {
724 Hot,
725 Warm,
726 Lukewarm,
727 Cool,
728 Cold,
729}
730
731impl<'a> TryFrom<&'a str> for HeatmapTemperature {
732 type Error = <Self as FromStr>::Err;
733
734 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
735 <Self as FromStr>::from_str(value)
736 }
737}
738
739pub(crate) fn write_nth(n: usize, f: &mut Formatter<'_>) -> std::fmt::Result {
740 write!(f, "{n}")?;
741 let (tens, ones) = (n / 10, n % 10);
742 let is_teen = (tens % 10) == 1;
743 if is_teen {
744 write!(f, "th")?;
745 } else {
746 write!(f, "{}", match ones {
747 1 => "st",
748 2 => "nd",
749 3 => "rd",
750 _ => "th",
751 })?;
752 }
753 Ok(())
754}
755
756pub fn deserialize_time_delta_from_hms<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TimeDelta, D::Error> {
759 let string = String::deserialize(deserializer)?;
760 let (hour, rest) = string.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
761 let (minute, second) = rest.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
762 let hour = hour.parse::<u32>().map_err(D::Error::custom)?;
763 let minute = minute.parse::<u32>().map_err(D::Error::custom)?;
764 let second = second.parse::<u32>().map_err(D::Error::custom)?;
765
766 TimeDelta::new(((hour * 24 + minute) * 60 + second) as _, 0).ok_or_else(|| D::Error::custom("Invalid time quantity, overflow."))
767}
768
769#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
771#[serde(try_from = "&str")]
772pub enum DayHalf {
773 AM,
774 PM,
775}
776
777impl DayHalf {
778 #[must_use]
780 pub fn into_24_hour_time(self, mut time: NaiveTime) -> NaiveTime {
781 if (self == Self::PM) ^ (time.hour() == 12) {
782 time += TimeDelta::hours(12);
783 }
784
785 time
786 }
787}
788
789impl<'a> TryFrom<&'a str> for DayHalf {
790 type Error = <Self as FromStr>::Err;
791
792 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
793 <Self as FromStr>::from_str(value)
794 }
795}
796
797#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
798#[serde(rename_all = "camelCase")]
799pub struct ResourceUsage {
800 pub used: u32,
801 pub remaining: u32,
802}
803
804#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
805#[serde(rename_all = "camelCase")]
806pub struct ResultHoldingResourceUsage {
807 #[serde(rename = "usedSuccessful")]
808 pub used_successfully: u32,
809 pub used_failed: u32,
810 pub remaining: u32,
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
818 fn test_ampm() {
819 assert_eq!(NaiveTime::from_hms_opt(0, 0, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
820 assert_eq!(NaiveTime::from_hms_opt(12, 0, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
821 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
822 assert_eq!(NaiveTime::from_hms_opt(12, 1, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
823 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
824 assert_eq!(NaiveTime::from_hms_opt(23, 59, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(11, 59, 0).unwrap()));
825 assert_eq!(NaiveTime::from_hms_opt(1, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(1, 1, 0).unwrap()));
826 }
827}