1use serde_json::{json, Map, Value};
9
10use super::utf16_cmp;
11use crate::dates::{day_from_number, day_number, format_day, parse_day, CalendarDay};
12
13pub const CALENDAR_LANES_DEFAULT: u32 = 3;
14pub const CALENDAR_LANES_MAX: u32 = 8;
15pub const TIMELINE_COLUMNS_DEFAULT: u32 = 12;
16pub const TIMELINE_COLUMNS_MAX: u32 = 52;
17pub const TIMELINE_SCALES: [&str; 3] = ["week", "month", "quarter"];
18pub const CALENDAR_TONES: [&str; 8] = ["chart1", "chart2", "chart3", "chart4", "chart5", "chart6", "chart7", "chart8"];
19pub const CALENDAR_WEEKDAYS_LONG: [&str; 7] = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
21const WEEKDAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
22const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
23
24#[derive(Debug, Clone)]
27pub struct CalendarSpan {
28 pub id: String,
29 pub start: i64,
30 pub end: i64,
31 pub tone: String,
32 pub value: Value,
33 pub order: usize,
34 pub row_key: Option<String>,
35}
36
37pub fn day_of(text: &str) -> Option<i64> {
39 parse_day(text).map(|d| day_number(d.y, d.m, d.d))
40}
41
42pub fn span_of(start_text: &str, end_text: Option<&str>) -> Option<(i64, i64)> {
45 let start = day_of(start_text)?;
46 let end = end_text.and_then(day_of);
47 Some((start, end.filter(|e| *e >= start).unwrap_or(start)))
48}
49
50pub fn day_text(n: i64) -> String {
51 format_day(day_from_number(n))
52}
53
54fn weekday_index(n: i64) -> usize {
56 (n + 3).rem_euclid(7) as usize
57}
58
59fn monday_of(n: i64) -> i64 {
60 n - weekday_index(n) as i64
61}
62
63fn is_tone_digit(v: &str) -> bool {
64 v.len() == 1 && matches!(v.as_bytes()[0], b'1'..=b'8')
65}
66
67pub fn tones_of(values: &[String]) -> Vec<String> {
71 let mut named: Vec<&str> = values.iter().map(String::as_str).filter(|v| !v.is_empty() && !is_tone_digit(v)).collect();
72 named.sort_by(|a, b| utf16_cmp(a, b));
73 named.dedup();
74 values
75 .iter()
76 .map(|v| {
77 if is_tone_digit(v) {
78 format!("chart{v}")
79 } else if v.is_empty() {
80 "chart1".to_string()
81 } else {
82 let rank = named.iter().position(|n| *n == v.as_str()).unwrap_or(0);
83 CALENDAR_TONES[rank % 8].to_string()
84 }
85 })
86 .collect()
87}
88
89fn lane_order(spans: &[CalendarSpan], from: i64, to: i64) -> Vec<&CalendarSpan> {
92 let mut out: Vec<&CalendarSpan> = spans.iter().filter(|s| s.end >= from && s.start <= to).collect();
93 out.sort_by(|a, b| {
94 let (af, bf) = (a.start.max(from), b.start.max(from));
95 let (al, bl) = (a.end.min(to) - af, b.end.min(to) - bf);
96 af.cmp(&bf).then(bl.cmp(&al)).then(a.order.cmp(&b.order))
97 });
98 out
99}
100
101fn take_lane(lanes: &mut Vec<Vec<bool>>, first: usize, last: usize, max: usize) -> Option<usize> {
104 for lane in 0..max {
105 if lanes.len() <= lane {
106 lanes.push(Vec::new());
107 }
108 let row = &mut lanes[lane];
109 if row.len() <= last {
110 row.resize(last + 1, false);
111 }
112 if row[first..=last].iter().any(|c| *c) {
113 continue;
114 }
115 row[first..=last].iter_mut().for_each(|c| *c = true);
116 return Some(lane);
117 }
118 None
119}
120
121pub struct WeekOptions {
122 pub lanes: u32,
123 pub today: Option<i64>,
124 pub selected: Option<i64>,
125 pub month: Option<i64>,
126}
127
128pub fn calendar_week(monday: i64, spans: &[CalendarSpan], o: &WeekOptions) -> Map<String, Value> {
130 let sunday = monday + 6;
131 let placed = lane_order(spans, monday, sunday);
132 let mut lanes: Vec<Vec<bool>> = Vec::new();
133 let mut hidden = [0u64; 7];
134 let mut segments: Vec<Value> = Vec::new();
135 for s in &placed {
136 let first = (s.start.max(monday) - monday) as usize;
137 let last = (s.end.min(sunday) - monday) as usize;
138 match take_lane(&mut lanes, first, last, o.lanes as usize) {
139 None => hidden[first..=last].iter_mut().for_each(|h| *h += 1),
140 Some(lane) => segments.push(json!({
141 "id": s.id, "col": first + 1, "span": last - first + 1, "lane": lane,
142 "contL": s.start < monday, "contR": s.end > sunday, "tone": s.tone, "more": 0, "row": s.value,
143 })),
144 }
145 }
146 for (c, n) in hidden.iter().enumerate() {
147 if *n > 0 {
148 segments.push(json!({
149 "id": format!("more:{}", day_text(monday + c as i64)), "col": c + 1, "span": 1, "lane": o.lanes,
150 "contL": false, "contR": false, "tone": null, "more": n, "row": null,
151 }));
152 }
153 }
154 let days: Vec<Value> = (0..7)
155 .map(|c| {
156 let n = monday + c as i64;
157 let d = day_from_number(n);
158 let covering: Vec<&&CalendarSpan> = placed.iter().filter(|s| s.start <= n && s.end >= n).collect();
159 let dots: Vec<Value> = covering.iter().take(3).map(|s| json!({"tone": s.tone})).collect();
160 json!({
161 "date": format_day(d), "day": d.d, "weekday": WEEKDAYS[c], "initial": &WEEKDAYS[c][..1],
162 "inMonth": o.month.map_or(true, |m| d.m == m), "weekend": c >= 5, "today": Some(n) == o.today,
163 "selected": Some(n) == o.selected, "total": covering.len(), "dots": dots,
164 })
165 })
166 .collect();
167 let mut out = Map::new();
168 out.insert("key".into(), json!(day_text(monday)));
169 out.insert("days".into(), Value::Array(days));
170 out.insert("segments".into(), Value::Array(segments));
171 out
172}
173
174pub fn range_label(from: i64, to: i64) -> String {
176 let a = day_from_number(from);
177 let b = day_from_number(to);
178 let tail = format!("{} {} {}", b.d, MONTHS[(b.m - 1) as usize], b.y);
179 if a.y != b.y {
180 format!("{} {} {} – {tail}", a.d, MONTHS[(a.m - 1) as usize], a.y)
181 } else if a.m != b.m {
182 format!("{} {} – {tail}", a.d, MONTHS[(a.m - 1) as usize])
183 } else {
184 format!("{} – {tail}", a.d)
185 }
186}
187
188pub fn day_label(n: i64) -> String {
190 let d = day_from_number(n);
191 format!("{}, {} {}", CALENDAR_WEEKDAYS_LONG[weekday_index(n)], d.d, MONTHS[(d.m - 1) as usize])
192}
193
194pub fn calendar_month(y: i64, m: i64, last_day: i64, spans: &[CalendarSpan], lanes: u32, today: i64, day: Option<&str>) -> Map<String, Value> {
196 let first = day_number(y, m, 1);
197 let last = day_number(y, m, last_day);
198 let asked = day.and_then(day_of);
199 let selected = match asked {
200 Some(a) if a >= first && a <= last => a,
201 _ if today >= first && today <= last => today,
202 _ => first,
203 };
204 let mut weeks = Vec::new();
205 let mut monday = monday_of(first);
206 while monday <= last {
207 weeks.push(Value::Object(calendar_week(monday, spans, &WeekOptions { lanes, today: Some(today), selected: Some(selected), month: Some(m) })));
208 monday += 7;
209 }
210 let items: Vec<Value> = lane_order(spans, selected, selected)
211 .iter()
212 .map(|s| json!({"id": s.id, "tone": s.tone, "start": day_text(s.start), "end": day_text(s.end), "days": s.end - s.start + 1, "row": s.value}))
213 .collect();
214 let mut out = Map::new();
215 out.insert("weeks".into(), Value::Array(weeks));
216 out.insert("day".into(), json!(day_text(selected)));
217 out.insert("agenda".into(), json!({"date": day_text(selected), "label": day_label(selected), "count": items.len(), "items": items}));
218 out
219}
220
221pub fn calendar_week_view(key: i64, spans: &[CalendarSpan], lanes: u32, today: i64) -> Value {
223 let monday = monday_of(key);
224 let week = calendar_week(monday, spans, &WeekOptions { lanes, today: Some(today), selected: None, month: None });
225 json!({
226 "key": day_text(monday), "label": range_label(monday, monday + 6), "prev": day_text(monday - 7), "next": day_text(monday + 7),
227 "today": day_text(today), "days": week["days"], "segments": week["segments"],
228 })
229}
230
231pub fn period_start(n: i64, scale: &str) -> i64 {
233 if scale == "week" {
234 return monday_of(n);
235 }
236 let d = day_from_number(n);
237 day_number(d.y, if scale == "month" { d.m } else { (d.m - 1) / 3 * 3 + 1 }, 1)
238}
239
240pub fn shift_period(start: i64, scale: &str, k: i64) -> i64 {
242 if scale == "week" {
243 return start + 7 * k;
244 }
245 let d = day_from_number(start);
246 let index = d.y * 12 + (d.m - 1) + k * if scale == "month" { 1 } else { 3 };
247 day_number(index.div_euclid(12), index.rem_euclid(12) + 1, 1)
248}
249
250fn column_label(d: CalendarDay, scale: &str) -> String {
251 let short = &MONTHS[(d.m - 1) as usize][..3];
252 match scale {
253 "week" => format!("{} {short}", d.d),
254 "month" => short.to_string(),
255 _ => format!("Q{}", (d.m - 1) / 3 + 1),
256 }
257}
258
259pub fn calendar_timeline(start: i64, spans: &[CalendarSpan], scale: &str, columns: u32, today: i64) -> Value {
261 let cols = columns as usize;
262 let p0 = period_start(start, scale);
263 let starts: Vec<i64> = (0..=cols as i64).map(|i| shift_period(p0, scale, i)).collect();
264 let axis_end = starts[cols] - 1;
265 let col_of = |n: i64| -> usize {
266 let mut c = 0;
267 while c + 1 < cols && starts[c + 1] <= n {
268 c += 1;
269 }
270 c
271 };
272 let axis_year = day_from_number(p0).y;
273 let columns_json: Vec<Value> = (0..cols)
274 .map(|i| json!({"key": day_text(starts[i]), "label": column_label(day_from_number(starts[i]), scale), "end": day_text(starts[i + 1] - 1), "today": today >= starts[i] && today < starts[i + 1]}))
275 .collect();
276 let mut bands: Vec<(String, usize, usize)> = Vec::new();
277 for (i, s) in starts.iter().take(cols).enumerate() {
278 let d = day_from_number(*s);
279 let label = if scale == "week" {
280 if d.y == axis_year { MONTHS[(d.m - 1) as usize].to_string() } else { format!("{} {}", MONTHS[(d.m - 1) as usize], d.y) }
281 } else {
282 d.y.to_string()
283 };
284 match bands.last_mut() {
285 Some(last) if last.0 == label => last.2 += 1,
286 _ => bands.push((label, i + 1, 1)),
287 }
288 }
289 let mut ordered: Vec<&CalendarSpan> = spans.iter().collect();
290 ordered.sort_by_key(|s| s.order);
291 let mut groups: Vec<(String, Vec<&CalendarSpan>)> = Vec::new();
292 for s in ordered {
293 if s.end < p0 || s.start > axis_end {
294 continue;
295 }
296 let key = s.row_key.clone().unwrap_or_else(|| s.id.clone());
297 match groups.iter_mut().find(|g| g.0 == key) {
298 Some(g) => g.1.push(s),
299 None => groups.push((key, vec![s])),
300 }
301 }
302 let rows: Vec<Value> = groups
303 .into_iter()
304 .enumerate()
305 .map(|(index, (key, list))| {
306 let mut lanes: Vec<Vec<bool>> = Vec::new();
307 let mut cells: Vec<(&CalendarSpan, usize, usize)> = list.iter().map(|s| (*s, col_of(s.start.max(p0)), col_of(s.end.min(axis_end)))).collect();
308 cells.sort_by(|a, b| a.1.cmp(&b.1).then((b.2 - b.1).cmp(&(a.2 - a.1))).then(a.0.order.cmp(&b.0.order)));
309 let items: Vec<Value> = cells
310 .iter()
311 .map(|(s, first, last)| {
312 let lane = take_lane(&mut lanes, *first, *last, usize::MAX).unwrap_or(0);
313 json!({
314 "id": s.id, "rowIndex": index, "col": first + 1, "span": last - first + 1, "lane": lane,
315 "contL": s.start < p0, "contR": s.end > axis_end, "tone": s.tone, "row": s.value,
316 })
317 })
318 .collect();
319 json!({"key": key, "index": index, "lanes": lanes.len().max(1), "count": items.len(), "items": items})
320 })
321 .collect();
322 let now = if today >= p0 && today <= axis_end {
323 let c = col_of(today);
324 let frac = (today - starts[c]) as f64 / (starts[c + 1] - starts[c]) as f64;
325 let percent = (((c as f64 + frac) / f64::from(columns)) * 10000.0).round() / 100.0;
326 json!({"col": c + 1, "percent": crate::json::number(percent)})
327 } else {
328 Value::Null
329 };
330 json!({
331 "scale": scale, "key": day_text(p0), "label": range_label(p0, axis_end),
332 "prev": day_text(shift_period(p0, scale, -i64::from(columns))), "next": day_text(starts[cols]), "today": day_text(today),
333 "columns": columns_json,
334 "bands": bands.into_iter().map(|(label, col, span)| json!({"label": label, "col": col, "span": span})).collect::<Vec<_>>(),
335 "rows": rows, "now": now,
336 })
337}