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