use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ShiftType {
Morning,
Afternoon,
Rest,
Night,
Study,
}
impl ShiftType {
pub fn label(&self) -> &'static str {
match self {
ShiftType::Morning => "早",
ShiftType::Afternoon => "中",
ShiftType::Rest => "休",
ShiftType::Night => "夜",
ShiftType::Study => "学",
}
}
pub fn full_label(&self) -> &'static str {
match self {
ShiftType::Morning => "早班",
ShiftType::Afternoon => "中班",
ShiftType::Rest => "休班",
ShiftType::Night => "夜班",
ShiftType::Study => "学习班",
}
}
pub fn label_en(&self) -> &'static str {
match self {
ShiftType::Morning => "AM",
ShiftType::Afternoon => "PM",
ShiftType::Rest => "R ",
ShiftType::Night => "NT",
ShiftType::Study => "TR",
}
}
pub fn label_en_padded(&self) -> &'static str {
match self {
ShiftType::Morning => "AM ",
ShiftType::Afternoon => "PM ",
ShiftType::Rest => "R ",
ShiftType::Night => "NT ",
ShiftType::Study => "TR ",
}
}
pub fn full_label_en(&self) -> &'static str {
match self {
ShiftType::Morning => "Morning",
ShiftType::Afternoon => "Afternoon",
ShiftType::Rest => "Rest",
ShiftType::Night => "Night",
ShiftType::Study => "Study",
}
}
pub fn is_work(&self) -> bool {
matches!(self, ShiftType::Morning | ShiftType::Afternoon | ShiftType::Night)
}
pub fn is_rest(&self) -> bool {
matches!(self, ShiftType::Rest | ShiftType::Study)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShiftInfo {
pub date: chrono::NaiveDate,
pub day_of_cycle: u32,
pub cycle_index: u32,
pub shift_type: ShiftType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShiftCycleConfig {
pub cycle: Vec<ShiftType>,
pub cycle_length: u32,
pub reference_date: chrono::NaiveDate,
pub total_teams: u32,
}
pub fn team_name(id: u32) -> String {
let prefix = match id {
1 => "一", 2 => "二", 3 => "三",
4 => "四", 5 => "五", 6 => "六",
_ => return format!("{}值", id),
};
format!("{}值", prefix)
}
pub fn successor_team_id(team_id: u32, total_teams: u32) -> u32 {
assert!(total_teams >= 1, "total_teams must be >= 1");
assert!(team_id >= 1, "team_id must be >= 1");
(team_id % total_teams) + 1
}
pub fn predecessor_team_id(team_id: u32, total_teams: u32) -> u32 {
assert!(total_teams >= 1, "total_teams must be >= 1");
assert!(team_id >= 1, "team_id must be >= 1");
(team_id + total_teams - 2) % total_teams + 1
}
impl ShiftCycleConfig {
pub fn new(cycle: Vec<ShiftType>, reference_date: chrono::NaiveDate, total_teams: u32) -> Self {
let cycle_length = cycle.len() as u32;
assert!(cycle_length >= 1, "cycle must be non-empty");
assert!(total_teams >= 1, "total_teams must be >= 1");
Self { cycle, cycle_length, reference_date, total_teams }
}
pub fn successor_of(&self, team_id: u32) -> u32 {
successor_team_id(team_id, self.total_teams)
}
pub fn predecessor_of(&self, team_id: u32) -> u32 {
predecessor_team_id(team_id, self.total_teams)
}
pub fn team_phase_offset(&self, team_id: u32) -> u32 {
(team_id - 1) * (self.cycle_length / self.total_teams)
}
pub fn shift_handover(
&self,
date: chrono::NaiveDate,
team_id: u32,
) -> Option<(u32, u32)> {
use crate::calculator::get_shift_type_for_date;
let my_shift = get_shift_type_for_date(date, self, self.team_phase_offset(team_id));
if my_shift.is_rest() {
return None; }
let (pred_shift, succ_shift) = match my_shift {
ShiftType::Morning => (ShiftType::Night, ShiftType::Afternoon),
ShiftType::Afternoon => (ShiftType::Morning, ShiftType::Night),
ShiftType::Night => (ShiftType::Afternoon, ShiftType::Morning),
_ => unreachable!(), };
let mut pred_team: Option<u32> = None;
let mut succ_team: Option<u32> = None;
for t in 1..=self.total_teams {
if t == team_id {
continue;
}
let shift = get_shift_type_for_date(date, self, self.team_phase_offset(t));
if shift == pred_shift {
pred_team = Some(t);
}
if shift == succ_shift {
succ_team = Some(t);
}
if pred_team.is_some() && succ_team.is_some() {
break;
}
}
match (pred_team, succ_team) {
(Some(p), Some(s)) => Some((p, s)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cycle::default_config;
#[test]
fn successor_team_1_is_2() {
assert_eq!(successor_team_id(1, 6), 2);
}
#[test]
fn successor_team_6_wraps_to_1() {
assert_eq!(successor_team_id(6, 6), 1);
}
#[test]
fn successor_team_3_is_4() {
assert_eq!(successor_team_id(3, 6), 4);
}
#[test]
fn successor_single_team_wraps_to_self() {
assert_eq!(successor_team_id(1, 1), 1);
}
#[test]
fn predecessor_team_1_is_6() {
assert_eq!(predecessor_team_id(1, 6), 6);
}
#[test]
fn predecessor_team_2_is_1() {
assert_eq!(predecessor_team_id(2, 6), 1);
}
#[test]
fn predecessor_team_6_is_5() {
assert_eq!(predecessor_team_id(6, 6), 5);
}
#[test]
fn predecessor_team_3_is_2() {
assert_eq!(predecessor_team_id(3, 6), 2);
}
#[test]
fn predecessor_single_team_wraps_to_self() {
assert_eq!(predecessor_team_id(1, 1), 1);
}
#[test]
fn config_successor_of() {
let config = default_config();
assert_eq!(config.successor_of(1), 2);
assert_eq!(config.successor_of(6), 1);
}
#[test]
fn config_predecessor_of() {
let config = default_config();
assert_eq!(config.predecessor_of(1), 6);
assert_eq!(config.predecessor_of(2), 1);
}
#[test]
fn all_successors_are_unique() {
let mut succs: Vec<u32> = (1..=6).map(|t| successor_team_id(t, 6)).collect();
succs.sort();
assert_eq!(succs, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn all_predecessors_are_unique() {
let mut preds: Vec<u32> = (1..=6).map(|t| predecessor_team_id(t, 6)).collect();
preds.sort();
assert_eq!(preds, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn pred_succ_cycle() {
for t in 1..=6 {
let succ = successor_team_id(t, 6);
assert_eq!(predecessor_team_id(succ, 6), t,
"predecessor(successor({})) should be {}", t, t);
}
}
}