#![allow(clippy::items_after_test_module, reason = "Not needed with separated tests")]
#![cfg_attr(test, allow(
non_snake_case,
clippy::arithmetic_side_effects,
clippy::cast_lossless,
clippy::cast_precision_loss,
clippy::cognitive_complexity,
clippy::default_numeric_fallback,
clippy::exhaustive_enums,
clippy::exhaustive_structs,
clippy::expect_used,
clippy::indexing_slicing,
clippy::let_underscore_must_use,
clippy::let_underscore_untyped,
clippy::missing_assert_message,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::panic,
clippy::print_stdout,
clippy::too_many_lines,
clippy::unwrap_in_result,
clippy::unwrap_used,
reason = "Not useful in unit tests"
))]
#[cfg(test)]
#[path = "tests/lib.rs"]
mod tests;
use core::{
fmt::{Debug, Display, Formatter},
fmt,
ops::{Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Sub, SubAssign},
};
#[cfg(feature = "chrono")]
use chrono::Weekday;
#[cfg(feature = "postgres")]
use ::{
bytes::BytesMut,
core::error::Error,
std::io::{Error as IoError, ErrorKind as IoErrorKind},
tokio_postgres::types::{FromSql, IsNull, ToSql, Type, to_sql_checked},
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Weekdays(u8);
impl Weekdays {
pub const MONDAY: Self = Self(0b100_0000);
pub const TUESDAY: Self = Self(0b010_0000);
pub const WEDNESDAY: Self = Self(0b001_0000);
pub const THURSDAY: Self = Self(0b000_1000);
pub const FRIDAY: Self = Self(0b000_0100);
pub const SATURDAY: Self = Self(0b000_0010);
pub const SUNDAY: Self = Self(0b000_0001);
pub const WEEKDAYS: Self = Self(0b111_1100);
pub const WEEKENDS: Self = Self(0b000_0011);
pub const ALL_DAYS: Self = Self(0b111_1111);
pub const NONE: Self = Self(0b000_0000);
const ALL_DAYS_MASK: u8 = 0b111_1111;
#[must_use]
pub const fn new(days: u8) -> Self {
Self(days & Self::ALL_DAYS_MASK)
}
#[must_use]
pub const fn contains(&self, day: Self) -> bool {
self.0 & day.0 == day.0
}
#[expect(clippy::cast_possible_truncation, reason = "Value is guaranteed to be <= 7")]
#[must_use]
pub const fn days(&self) -> u8 {
self.0.count_ones() as u8
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn is_weekday(&self) -> bool {
self.0 & Self::WEEKENDS.0 == 0
}
#[must_use]
pub const fn is_weekend(&self) -> bool {
self.0 & Self::WEEKDAYS.0 == 0
}
#[must_use]
pub const fn iter(&self) -> WeekdaysIter {
WeekdaysIter {
remaining: self.0,
position: 0,
}
}
#[cfg(feature = "chrono")]
#[must_use]
pub fn to_chrono_vec(&self) -> Vec<Weekday> {
[
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
]
.into_iter()
.filter(|&day| self.contains(Self::from(day)))
.collect()
}
#[must_use]
pub fn to_vec(&self) -> Vec<Self> {
self.iter().collect()
}
}
impl Add for Weekdays {
type Output = Self;
#[expect(clippy::suspicious_arithmetic_impl, reason = "Bitwise OR is the correct operation")]
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl AddAssign for Weekdays {
#[expect(clippy::suspicious_op_assign_impl, reason = "Bitwise OR is the correct operation")]
fn add_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl BitAnd for Weekdays {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl BitAndAssign for Weekdays {
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0;
}
}
impl BitOr for Weekdays {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Weekdays {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl BitXor for Weekdays {
type Output = Self;
fn bitxor(self, rhs: Self) -> Self::Output {
Self(self.0 ^ rhs.0)
}
}
impl BitXorAssign for Weekdays {
fn bitxor_assign(&mut self, rhs: Self) {
self.0 ^= rhs.0;
}
}
impl Debug for Weekdays {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Weekdays({:03b}_{:04b})", self.0 >> 4, self.0 & 0b1111)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Weekdays {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>,
{
u8::deserialize(deserializer).map(Self::new)
}
}
impl Display for Weekdays {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:05b}_{:02b}", self.0 >> 2, self.0 & 0b11)
}
}
#[cfg(feature = "chrono")]
impl From<Weekday> for Weekdays {
fn from(day: Weekday) -> Self {
match day {
Weekday::Mon => Self::MONDAY,
Weekday::Tue => Self::TUESDAY,
Weekday::Wed => Self::WEDNESDAY,
Weekday::Thu => Self::THURSDAY,
Weekday::Fri => Self::FRIDAY,
Weekday::Sat => Self::SATURDAY,
Weekday::Sun => Self::SUNDAY,
}
}
}
#[cfg(feature = "postgres")]
impl FromSql<'_> for Weekdays {
fn from_sql(ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
match ty {
&Type::BIT => Ok(
match raw.first() {
Some(&byte) => Self(byte & Self::ALL_DAYS_MASK),
None => Self(0)
}
),
unknown => Err(Box::new(IoError::new(
IoErrorKind::InvalidData,
format!("Invalid type for Weekdays: {unknown}"),
))),
}
}
fn accepts(ty: &Type) -> bool {
ty.name() == "bit"
}
}
impl IntoIterator for Weekdays {
type Item = Self;
type IntoIter = WeekdaysIter;
fn into_iter(self) -> Self::IntoIter {
WeekdaysIter {
remaining: self.0,
position: 0,
}
}
}
impl IntoIterator for &Weekdays {
type Item = Weekdays;
type IntoIter = WeekdaysIter;
fn into_iter(self) -> Self::IntoIter {
WeekdaysIter {
remaining: self.0,
position: 0,
}
}
}
impl Not for Weekdays {
type Output = Self;
fn not(self) -> Self::Output {
Self(!self.0 & Self::ALL_DAYS_MASK)
}
}
#[cfg(feature = "serde")]
impl Serialize for Weekdays {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(self.0)
}
}
impl Sub for Weekdays {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 & !rhs.0)
}
}
impl SubAssign for Weekdays {
fn sub_assign(&mut self, rhs: Self) {
self.0 &= !rhs.0;
}
}
#[cfg(feature = "postgres")]
impl ToSql for Weekdays {
fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
match ty {
&Type::BIT => {
out.extend_from_slice(&[self.0]);
Ok(IsNull::No)
}
unknown => Err(Box::new(IoError::new(
IoErrorKind::InvalidData,
format!("Invalid type for Weekdays: {unknown}"),
))),
}
}
fn accepts(ty: &Type) -> bool {
ty.name() == "bit"
}
to_sql_checked!();
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WeekdaysIter {
remaining: u8,
position: u8,
}
impl Iterator for WeekdaysIter {
type Item = Weekdays;
fn next(&mut self) -> Option<Self::Item> {
while self.position < 7 {
let current = 0b100_0000 >> self.position;
#[expect(clippy::arithmetic_side_effects, reason = "This is checked by the while loop")]
{ self.position += 1; }
if self.remaining & current != 0 {
return Some(Weekdays(current));
}
}
None
}
}