emoji_clock_2/
lib.rs

1#![no_std]
2#![deny(missing_docs)]
3
4//! # Examples
5//!
6//! ```
7//! #[cfg(feature = "time")]
8//! {
9//!   use time::Time;
10//!   use emoji_clock_2::{Clock, Rounding};
11//!   let clock = Clock::new(Time::from_hms(12, 29, 00).unwrap()).with_rounding(Rounding::Floor);
12//!   assert_eq!("🕛", clock.to_string());
13//! }
14//! ```
15//!
16//! ```
17//! #[cfg(feature = "chrono")]
18//! {
19//!   use chrono::NaiveTime;
20//!   use emoji_clock_2::{Clock, Meridiem};
21//!   let am_clock = Clock::new(NaiveTime::from_hms_opt(9, 15, 00).unwrap()).with_meridiem(Meridiem::default());
22//!   assert_eq!("🕤🌞", am_clock.to_string());
23//!   let pm_clock = Clock::new(NaiveTime::from_hms_opt(21, 44, 00).unwrap()).with_meridiem(Meridiem{ am: '🌞', pm: '🌙' });
24//!   assert_eq!("🕤🌙", pm_clock.to_string());
25//! }
26//! ```
27
28/// Renders a clock in emoji
29/// # Examples
30///
31/// ```
32/// #[cfg(feature = "time")]
33/// {
34///   use time::Time;
35///   use emoji_clock_2::{Clock, Rounding};
36///   let clock = Clock::new(Time::from_hms(12, 29, 00).unwrap()).with_rounding(Rounding::Floor);
37///   assert_eq!("🕛", clock.to_string());
38/// }
39/// ```
40///
41/// ```
42/// #[cfg(feature = "chrono")]
43/// {
44///   use chrono::NaiveTime;
45///   use emoji_clock_2::{Clock, Meridiem};
46///   let am_clock = Clock::new(NaiveTime::from_hms_opt(9, 15, 00).unwrap()).with_meridiem(Meridiem::default());
47///   assert_eq!("🕤🌞", am_clock.to_string());
48///   let pm_clock = Clock::new(NaiveTime::from_hms_opt(21, 44, 00).unwrap()).with_meridiem(Meridiem{ am: '🌞', pm: '🌙' });
49///   assert_eq!("🕤🌙", pm_clock.to_string());
50/// }
51/// ```
52#[derive(Debug, Copy, Clone)]
53pub struct Clock<Time>
54where
55	Time: TimeLike,
56{
57	time: Time,
58	rounding: Rounding,
59	meridiem: Option<Meridiem>,
60}
61
62impl<T> core::fmt::Display for Clock<T>
63where
64	T: TimeLike,
65{
66	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67		let (hour, half_hour) = self.rounding.round(&self.time);
68		let clock = clock_emoji(hour, half_hour);
69
70		if let Some(Meridiem { am, pm }) = self.meridiem {
71			#[allow(clippy::zero_prefixed_literal)]
72			let meridiem = match hour {
73				00..=11 => am,
74				12..=23 => pm,
75				_ => return Err(core::fmt::Error),
76			};
77			write!(f, "{clock}{meridiem}")
78		} else {
79			write!(f, "{clock}")
80		}
81	}
82}
83
84impl<Time> Clock<Time>
85where
86	Time: TimeLike,
87{
88	/// Creates a new clock with round strategy and no meridiem indicators
89	#[must_use]
90	pub const fn new(time: Time) -> Self {
91		Self {
92			time,
93			rounding: Rounding::Round,
94			meridiem: None,
95		}
96	}
97	/// Sets rounding strategy ([Round](crate::Rounding::Round) by default)
98	#[must_use]
99	pub fn with_rounding(self, rounding: Rounding) -> Self {
100		Self {
101			time: self.time,
102			rounding,
103			meridiem: self.meridiem,
104		}
105	}
106	/// Enables meridiem (AM/PM) indicators (disabled by default)
107	#[must_use]
108	pub fn with_meridiem(self, meridiem: Meridiem) -> Self {
109		Self {
110			time: self.time,
111			rounding: self.rounding,
112			meridiem: Some(meridiem),
113		}
114	}
115}
116
117/// The common set of methods for time
118pub trait TimeLike {
119	/// Returns the hour number from 0 to 23
120	fn hour(&self) -> u8;
121	/// Returns the minute number from 0 to 59
122	fn minute(&self) -> u8;
123}
124
125/// Meridiem (AM/PM) indicators
126#[derive(Debug, Copy, Clone)]
127pub struct Meridiem {
128	/// AM indicator
129	pub am: char,
130	/// PM indicator
131	pub pm: char,
132}
133
134impl Default for Meridiem {
135	/// Default meridiem (AM/PM) indicators (🌞/🌝)
136	fn default() -> Self {
137		Self {
138			am: '🌞', pm: '🌝'
139		}
140	}
141}
142
143/// Strategies for rounding time to emoji clock precision (30-minute granularity)
144#[derive(Debug, Copy, Clone)]
145#[non_exhaustive]
146pub enum Rounding {
147	/// Round to the nearest emoji clock
148	/// - 01:45 - 02:14 : 02:00 🕑
149	/// - 02:15 - 02:44 : 02:30 🕝
150	Round,
151	/// Round down to the nearest emoji clock
152	/// - 02:00 - 02:29 : 02:00 🕑
153	/// - 02:30 - 02:59 : 02:30 🕝
154	Floor,
155	/// Round up to the nearest emoji clock
156	/// - 01:31 - 02:00 : 02:00 🕑
157	/// - 02:01 - 02:30 : 02:30 🕝
158	Ceil,
159}
160
161/// Returns the emoji clock with the correct hour hand and minute hand, accurate to 30-minute precision
162///
163/// # Panics
164///
165/// Panics if the resulting character is not valid
166#[must_use]
167pub fn clock_emoji(hour: u8, half_hour: bool) -> char {
168	char::from_u32(128336 + (u32::from(hour) + 11) % 12 + if half_hour { 12 } else { 0 }).unwrap()
169}
170
171impl Rounding {
172	/// Rounds time to 30-min precision
173	fn round<Time>(&self, time: &Time) -> (u8, bool)
174	where
175		Time: TimeLike,
176	{
177		let (hour, minute) = (time.hour(), time.minute());
178		assert!(hour <= 23, "Hour greater than 23");
179		#[allow(clippy::zero_prefixed_literal)]
180		match *self {
181			Self::Floor => match minute {
182				00..=29 => (hour, false),
183				30..=59 => (hour, true),
184				_ => panic!("Minute greater than 59"),
185			},
186			Self::Ceil => match minute {
187				0 => (hour, false),
188				01..=30 => (hour, true),
189				31..=59 => ((hour + 1) % 24, false),
190				_ => panic!("Minute greater than 59"),
191			},
192			Self::Round => match minute {
193				00..=14 => (hour, false),
194				15..=44 => (hour, true),
195				45..=59 => ((hour + 1) % 24, false),
196				_ => panic!("Minute greater than 59"),
197			},
198		}
199	}
200}
201
202#[cfg(feature = "chrono")]
203mod chrono;
204#[cfg(feature = "time")]
205mod time;
206
207#[cfg(test)]
208mod tests;
209
210impl TimeLike for (u8, u8) {
211	fn hour(&self) -> u8 {
212		self.0
213	}
214	fn minute(&self) -> u8 {
215		self.1
216	}
217}
218
219impl core::convert::From<(char, char)> for Meridiem {
220	fn from((am, pm): (char, char)) -> Self {
221		Self { am, pm }
222	}
223}
224
225impl<Time> core::ops::Deref for Clock<Time>
226where
227	Time: TimeLike,
228{
229	type Target = Time;
230	fn deref(&self) -> &Self::Target {
231		&self.time
232	}
233}
234
235impl<Time> core::ops::DerefMut for Clock<Time>
236where
237	Time: TimeLike,
238{
239	fn deref_mut(&mut self) -> &mut Self::Target {
240		&mut self.time
241	}
242}