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