Skip to main content

shift_algorithm/
types.rs

1//! Core data types for the shift scheduling system.
2
3use serde::{Deserialize, Serialize};
4
5/// The five shift types in a standard Chinese rotating shift system.
6///
7/// # Variants
8///
9/// | Variant | Label | Full Label | Category |
10/// |---------|-------|-----------|----------|
11/// | [`Morning`](ShiftType::Morning) | 早 | 早班 | Work |
12/// | [`Afternoon`](ShiftType::Afternoon) | 中 | 中班 | Work |
13/// | [`Rest`](ShiftType::Rest) | 休 | 休班 | Rest |
14/// | [`Night`](ShiftType::Night) | 夜 | 夜班 | Work |
15/// | [`Study`](ShiftType::Study) | 学 | 学习班 | Rest |
16///
17/// # Example
18///
19/// ```rust
20/// use shift_algorithm::ShiftType;
21///
22/// assert!(ShiftType::Morning.is_work());
23/// assert!(!ShiftType::Rest.is_work());
24/// assert!(ShiftType::Rest.is_rest());
25/// assert!(ShiftType::Study.is_rest());
26/// ```
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
29pub enum ShiftType {
30    /// 早班 — morning shift
31    Morning,
32    /// 中班 — afternoon shift
33    Afternoon,
34    /// 休班 — rest day
35    Rest,
36    /// 夜班 — night shift
37    Night,
38    /// 学习班 — study/training day (counts as rest for scheduling purposes)
39    Study,
40}
41
42impl ShiftType {
43    /// Short Chinese label (single character).
44    pub fn label(&self) -> &'static str {
45        match self {
46            ShiftType::Morning => "早",
47            ShiftType::Afternoon => "中",
48            ShiftType::Rest => "休",
49            ShiftType::Night => "夜",
50            ShiftType::Study => "学",
51        }
52    }
53
54    /// Full Chinese label.
55    pub fn full_label(&self) -> &'static str {
56        match self {
57            ShiftType::Morning => "早班",
58            ShiftType::Afternoon => "中班",
59            ShiftType::Rest => "休班",
60            ShiftType::Night => "夜班",
61            ShiftType::Study => "学习班",
62        }
63    }
64
65    /// Short English label.
66    pub fn label_en(&self) -> &'static str {
67        match self {
68            ShiftType::Morning => "AM",
69            ShiftType::Afternoon => "PM",
70            ShiftType::Rest => "Off",
71            ShiftType::Night => "NT",
72            ShiftType::Study => "TR",
73        }
74    }
75
76    /// Full English label.
77    pub fn full_label_en(&self) -> &'static str {
78        match self {
79            ShiftType::Morning => "Morning",
80            ShiftType::Afternoon => "Afternoon",
81            ShiftType::Rest => "Rest",
82            ShiftType::Night => "Night",
83            ShiftType::Study => "Study",
84        }
85    }
86
87    /// Returns `true` if this is a working shift (Morning, Afternoon, or Night).
88    ///
89    /// Used for counting work days, consecutive work stats, etc.
90    pub fn is_work(&self) -> bool {
91        matches!(self, ShiftType::Morning | ShiftType::Afternoon | ShiftType::Night)
92    }
93
94    /// Returns `true` if this counts as rest (Rest or Study).
95    ///
96    /// Study days are treated as rest because the worker is not on duty.
97    pub fn is_rest(&self) -> bool {
98        matches!(self, ShiftType::Rest | ShiftType::Study)
99    }
100}
101
102/// Result of querying what shift falls on a given date.
103///
104/// Returned by [`get_shift_info`](crate::get_shift_info).
105///
106/// # Fields
107///
108/// | Field | Type | Range | Description |
109/// |-------|------|-------|-------------|
110/// | `date` | `NaiveDate` | — | The queried date |
111/// | `day_of_cycle` | `u32` | `1..=cycle_length` | Which day in the cycle (1-based) |
112/// | `cycle_index` | `u32` | `0..=cycle_length-1` | Zero-based index into the cycle array |
113/// | `shift_type` | [`ShiftType`] | — | The shift type for this date |
114///
115/// ```rust
116/// use shift_algorithm::cycle::default_config;
117/// use shift_algorithm::get_shift_info;
118///
119/// let config = default_config();
120/// let info = get_shift_info(config.reference_date, &config, 0);
121///
122/// assert_eq!(info.day_of_cycle, 1);
123/// assert_eq!(info.cycle_index, 0);
124/// ```
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ShiftInfo {
127    /// The queried date.
128    pub date: chrono::NaiveDate,
129    /// Day index within the cycle (1-based, 1..=cycle_length).
130    pub day_of_cycle: u32,
131    /// Zero-based index into the cycle array (0..=cycle_length-1).
132    pub cycle_index: u32,
133    /// The shift type for this date.
134    pub shift_type: ShiftType,
135}
136
137/// Runtime shift cycle configuration.
138///
139/// The default 42-day, 6-team configuration is available via
140/// [`default_config()`](crate::cycle::default_config).
141///
142/// # Custom cycles
143///
144/// ```rust
145/// use shift_algorithm::{ShiftCycleConfig, ShiftType};
146/// use chrono::NaiveDate;
147///
148/// let config = ShiftCycleConfig {
149///     cycle: vec![ShiftType::Morning, ShiftType::Afternoon, ShiftType::Rest],
150///     cycle_length: 3,
151///     reference_date: NaiveDate::from_ymd_opt(2025, 12, 15).unwrap(),
152///     total_teams: 1,
153/// };
154/// ```
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ShiftCycleConfig {
157    /// The ordered list of shift types defining one full cycle.
158    /// Must have length == `cycle_length`.
159    pub cycle: Vec<ShiftType>,
160    /// Number of days in one full cycle (= `cycle.len()`).
161    pub cycle_length: u32,
162    /// The anchor reference date. Day 1 of the cycle falls on this date.
163    /// Default: 2025-12-15.
164    pub reference_date: chrono::NaiveDate,
165    /// Total number of teams sharing this cycle.
166    /// Each team is offset by `cycle_length / total_teams` days.
167    /// Default: 6.
168    pub total_teams: u32,
169}
170
171/// Chinese team name for a team ID.
172///
173/// ```rust
174/// use shift_algorithm::team_name;
175/// assert_eq!(team_name(1), "一值");
176/// assert_eq!(team_name(3), "三值");
177/// assert_eq!(team_name(6), "六值");
178/// ```
179pub fn team_name(id: u32) -> String {
180    let prefix = match id {
181        1 => "一", 2 => "二", 3 => "三",
182        4 => "四", 5 => "五", 6 => "六",
183        _ => return format!("{}值", id),
184    };
185    format!("{}值", prefix)
186}
187
188impl ShiftCycleConfig {
189    /// Create a new config, validating that `cycle.len() == cycle_length`.
190    ///
191    /// # Panics
192    /// Panics if `cycle.len() != cycle_length as usize`.
193    pub fn new(cycle: Vec<ShiftType>, reference_date: chrono::NaiveDate, total_teams: u32) -> Self {
194        let cycle_length = cycle.len() as u32;
195        assert!(cycle_length >= 1, "cycle must be non-empty");
196        assert!(total_teams >= 1, "total_teams must be >= 1");
197        Self { cycle, cycle_length, reference_date, total_teams }
198    }
199
200    /// Team phase offset in days.
201    ///
202    /// Formula: `(team_id - 1) * (cycle_length / total_teams)`.
203    ///
204    /// For a 42-day, 6-team cycle:
205    /// - Team 1 (一值): offset 0
206    /// - Team 2 (二值): offset 7
207    /// - Team 3 (三值): offset 14
208    /// - ...
209    /// - Team 6 (六值): offset 35
210    ///
211    /// ```rust
212    /// use shift_algorithm::cycle::default_config;
213    ///
214    /// let config = default_config();
215    /// assert_eq!(config.team_phase_offset(1), 0);
216    /// assert_eq!(config.team_phase_offset(2), 7);
217    /// assert_eq!(config.team_phase_offset(6), 35);
218    /// ```
219    pub fn team_phase_offset(&self, team_id: u32) -> u32 {
220        (team_id - 1) * (self.cycle_length / self.total_teams)
221    }
222}