1mod locale;
2
3use ansi_term::{
4 Color::{Black, Cyan, Purple, Red, Yellow, RGB},
5 Style,
6};
7use chrono::Datelike;
8
9const REFORM_YEAR: u32 = 1099;
10
11const SPECIAL_LEAP_YEARS: u32 = (REFORM_YEAR / 100) - (REFORM_YEAR / 400);
13
14const MONTHS: usize = 12;
15const WEEKDAYS: u32 = 7;
16
17const COLUMN: usize = 3;
18const ROWS: usize = 4;
19const ROW_SIZE: usize = 7;
20
21static TOKEN: &str = "\n";
22
23fn is_leap_year(year: u32) -> bool {
24 if year <= REFORM_YEAR {
25 return year % 4 == 0;
26 }
27 (year % 4 == 0) ^ (year % 100 == 0) ^ (year % 400 == 0)
28}
29
30fn count_leap_years(year: u32) -> u32 {
31 if year <= REFORM_YEAR {
32 (year - 1) / 4
33 } else {
34 ((year - 1) / 4) - ((year - 1) / 100) + ((year - 1) / 400) + SPECIAL_LEAP_YEARS
35 }
36}
37
38fn days_by_year(year: u32) -> u32 {
39 if year < 1 {
40 0
41 } else {
42 (year - 1) * 365 + count_leap_years(year)
43 }
44}
45
46fn days_by_month(year: u32) -> Vec<u32> {
47 let feb_day: u32 = if is_leap_year(year) { 29 } else { 28 };
48 vec![0, 31, feb_day, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
49}
50
51fn days_by_date(
52 day: u32,
53 month: usize,
54 year: u32,
55 months_memoized: Vec<u32>,
56 year_memoized: u32,
57) -> u32 {
58 day + (if month > 1 {
59 months_memoized[month - 1]
60 } else {
61 0
62 }) + (if year > 1 { year_memoized } else { 0 })
63}
64
65fn get_days_accumulated_by_month(year: u32) -> (Vec<u32>, Vec<u32>) {
66 let days: Vec<u32> = days_by_month(year);
67 let accum = days
68 .iter()
69 .scan(0, |acc, &x| {
70 *acc = *acc + x;
71 Some(*acc)
72 })
73 .collect();
74 (accum, days)
75}
76
77fn first_day_printable(day_year: u32, starting_day: u32) -> String {
78 let mut printable = format!("");
79
80 if (day_year - starting_day) % WEEKDAYS == 0 {
81 printable.push_str(" ");
82 }
83 for i in 2..WEEKDAYS {
84 if (day_year - starting_day) % WEEKDAYS == i {
85 printable.push_str(&" ".repeat(i as usize - 1));
86 break;
87 }
88 }
89 printable
90}
91
92fn remain_day_printable(day: u32, day_year: u32, starting_day: u32) -> String {
93 let base = if ((day_year - starting_day) % WEEKDAYS) == 0 {
94 format!("{:3}{}", day, TOKEN)
95 } else {
96 String::default()
97 };
98
99 let complement = (1..WEEKDAYS)
100 .find_map(|i| ((day_year - starting_day) % WEEKDAYS == i).then(|| format!("{:3}", day)))
101 .unwrap_or_default();
102
103 format!("{}{}", base, complement)
104}
105
106fn body_printable(
107 year: u32,
108 month: usize,
109 days: u32,
110 months_memoized: Vec<u32>,
111 year_memoized: u32,
112 starting_day: u32,
113 week_numbers: bool,
114) -> Vec<String> {
115 let mut result = Vec::<String>::new();
116 let mut result_days = format!("");
117
118 (1..days + 1).for_each(|day| {
120 if day == 1 {
121 let first_day = days_by_date(1, month, year, months_memoized.clone(), year_memoized);
122 result_days.push_str(&first_day_printable(first_day, starting_day))
123 }
124 let day_year = days_by_date(day, month, year, months_memoized.clone(), year_memoized);
125 result_days.push_str(&remain_day_printable(day, day_year, starting_day))
126 });
127
128 result_days
130 .split(TOKEN)
131 .collect::<Vec<&str>>()
132 .into_iter()
133 .for_each(|i| result.push(i.to_string()));
134
135 for line in 0..result.len() {
136 let spaces =
137 21 - result[line].len() + (3 * (result[line].is_empty() && week_numbers) as usize);
138 result[line] += &" ".repeat(spaces);
139 }
140 if result.len() < 7 {
142 result.push(" ".repeat(21 + (3 * week_numbers as usize)));
143 }
144 result
145}
146
147fn month_printable(
148 year: u32,
149 month: usize,
150 days: u32,
151 months_memoized: Vec<u32>,
152 year_memoized: u32,
153 starting_day: u32,
154 month_names: Vec<String>,
155 week_names: Vec<String>,
156 week_numbers: bool,
157) -> Vec<String> {
158 let mut result = Vec::<String>::new();
159 let body = body_printable(
160 year,
161 month,
162 days,
163 months_memoized,
164 year_memoized,
165 starting_day,
166 week_numbers,
167 );
168 let month_name = &month_names[month - 1];
169 result.push(format!(" {:^20}", month_name));
170 let header = circular_week_name(week_names, starting_day as usize);
171 result.push(header);
172
173 body.into_iter().for_each(|item| {
174 result.push(item);
175 });
176 result
177}
178
179fn circular_week_name(week_name: Vec<String>, idx: usize) -> String {
180 let mut s = " ".to_string();
181 for i in idx..(ROW_SIZE - 1 + idx) {
182 s.push_str(&format!("{} ", week_name[i % ROW_SIZE]));
183 }
184 s.push_str(week_name[(ROW_SIZE - 1 + idx) % ROW_SIZE].as_str());
185 s.to_string()
186}
187
188pub fn calendar(
189 year: u32,
190 locale_str: &str,
191 starting_day: u32,
192 week_numbers: bool,
193) -> Vec<Vec<Vec<String>>> {
194 let mut rows: Vec<Vec<Vec<String>>> = vec![vec![vec![String::from("")]; COLUMN]; ROWS];
195 let mut row_counter = 0;
196 let mut column_counter = 0;
197 let mut week_counter = 1;
198 let (months_memoized, months) = get_days_accumulated_by_month(year);
199 let year_memoized = days_by_year(year);
200 let locale_info = locale::LocaleInfo::new(locale_str);
201
202 (1..MONTHS + 1).for_each(|month| {
203 rows[row_counter][column_counter] = month_printable(
204 year,
205 month,
206 months[month],
207 months_memoized.clone(),
208 year_memoized,
209 starting_day,
210 locale_info.month_names(),
211 locale_info.week_day_names(),
212 week_numbers,
213 );
214
215 if week_numbers {
216 for line in 0..rows[row_counter][column_counter].len() {
217 if line < 2 {
218 rows[row_counter][column_counter][line] = format!(
219 "{}{}",
220 " ".to_string(),
221 &rows[row_counter][column_counter][line]
222 )
223 } else if !&rows[row_counter][column_counter][line].trim().is_empty() {
224 rows[row_counter][column_counter][line] = format!(
225 "{}{}{}",
226 &" ".repeat(1 + (week_counter < 10) as usize),
227 &week_counter.to_string(),
228 &rows[row_counter][column_counter][line]
229 );
230
231 if rows[row_counter][column_counter][line]
232 .chars()
233 .last()
234 .unwrap()
235 != ' '
236 {
237 week_counter += 1;
238 }
239 }
240 }
241 }
242
243 column_counter = month % COLUMN;
244 if column_counter == 0 {
245 row_counter += 1;
246 }
247 });
248 rows
249}
250
251fn print_row(
252 row: &str,
253 starting_day: u32,
254 today_included: bool,
255 pos_today: u32,
256 monochromatic: bool,
257 week_numbers: bool,
258) {
259 let pos_saturday = (((6 - starting_day as i32) % 7) + 7) % 7 + (week_numbers as i32);
260 let pos_sunday = (((7 - starting_day as i32) % 7) + 7) % 7 + (week_numbers as i32);
261
262 let char_saturday = (1 + 3 * pos_saturday) as usize;
263 let char_sunday = (1 + 3 * pos_sunday) as usize;
264 let char_today = (1 + 3 * (pos_today + week_numbers as u32)) as usize;
265
266 let row = row
267 .split("")
268 .filter(|s| !s.is_empty())
269 .enumerate()
270 .map(|(i, s)| {
271 if monochromatic {
272 if today_included && (i == char_today || i == char_today + 1) {
273 Black.on(RGB(200, 200, 200)).paint(s)
274 } else {
275 ansi_term::Style::default().paint(s)
276 }
277 } else {
278 if today_included && (i == char_today || i == char_today + 1) {
279 Black.on(RGB(200, 200, 200)).paint(s)
280 } else if i == char_saturday || i == char_saturday + 1 {
281 Yellow.bold().paint(s)
282 } else if i == char_sunday || i == char_sunday + 1 {
283 Red.bold().paint(s)
284 } else if week_numbers && i < 3 {
285 Purple.bold().paint(s)
286 } else {
287 ansi_term::Style::default().paint(s)
288 }
289 }
290 })
291 .collect::<Vec<ansi_term::ANSIString>>();
292
293 print!("{} ", ansi_term::ANSIStrings(&row));
294}
295
296fn get_today_position(year: u32, month: u32, day: u32, starting_day: u32) -> (u32, u32, u32, u32) {
302 let (months_memoized, _) = get_days_accumulated_by_month(year);
303
304 let first_of_month = days_by_date(1, month as usize, year, months_memoized, days_by_year(year));
305 let first_offset = (first_of_month - starting_day - 1) % WEEKDAYS;
306
307 let row_index = (month - 1) / 3;
308 let col_index = (month - 1) % 3;
309
310 let absolute_pos = first_offset + day - 1;
311 let x = absolute_pos % 7;
312 let y = absolute_pos / 7;
313
314 (row_index, col_index, x, y)
315}
316
317pub fn display(
318 year: u32,
319 locale_str: &str,
320 starting_day: u32,
321 monochromatic: bool,
322 week_numbers: bool,
323) {
324 let rows = calendar(year, locale_str, starting_day, week_numbers);
325
326 let today = {
327 let now = chrono::Local::now();
328 (now.year() as u32, now.month(), now.day())
329 };
330
331 let t_pos = if today.0 == year {
332 Some(get_today_position(today.0, today.1, today.2, starting_day))
333 } else {
334 None
335 };
336
337 println!(
339 "{}{}",
340 " ".repeat(6 * week_numbers as usize),
341 Style::new().bold().paint(format!(" {:^63}", year))
342 );
343
344 for (r, row) in rows.iter().enumerate() {
345 for line in 0..8 {
346 for col in 0..3 {
347 if line == 0 {
348 if monochromatic {
349 print!("{} ", &row[col][line]);
350 } else {
351 print!("{} ", Cyan.bold().paint(&row[col][line]));
352 }
353 } else {
354 let (today_included, x) = {
356 if let Some(p) = t_pos {
357 if p.0 == r as u32 && p.1 == col as u32 && p.3 + 2 == line as u32 {
359 (true, p.2)
360 } else {
361 (false, 0)
362 }
363 } else {
364 (false, 0)
365 }
366 };
367 print_row(
369 &row[col][line],
370 starting_day,
371 today_included,
372 x,
373 monochromatic,
374 week_numbers,
375 );
376 }
377 }
378 println!();
379 }
380 }
381}
382
383#[test]
384fn test_circular_week_name() {
385 let locale_str = "en_US";
386 let locale_info = locale::LocaleInfo::new(locale_str);
387 let week_name = locale_info.week_day_names();
388 assert_eq!(
389 circular_week_name(week_name.clone(), 0),
390 " Su Mo Tu We Th Fr Sa"
391 );
392 assert_eq!(
393 circular_week_name(week_name.clone(), 1),
394 " Mo Tu We Th Fr Sa Su"
395 );
396 assert_eq!(
397 circular_week_name(week_name.clone(), 2),
398 " Tu We Th Fr Sa Su Mo"
399 );
400 assert_eq!(
401 circular_week_name(week_name.clone(), 3),
402 " We Th Fr Sa Su Mo Tu"
403 );
404 assert_eq!(
405 circular_week_name(week_name.clone(), 4),
406 " Th Fr Sa Su Mo Tu We"
407 );
408 assert_eq!(
409 circular_week_name(week_name.clone(), 5),
410 " Fr Sa Su Mo Tu We Th"
411 );
412 assert_eq!(
413 circular_week_name(week_name.clone(), 6),
414 " Sa Su Mo Tu We Th Fr"
415 );
416}
417
418#[test]
419fn test_circular_week_name_pt_br() {
420 let locale_str = "pt_BR";
421 let locale_info = locale::LocaleInfo::new(locale_str);
422 let week_name = locale_info.week_day_names();
423 assert_eq!(circular_week_name(week_name, 0), " Do Se Te Qu Qu Se Sá");
424}
425
426#[test]
427fn test_is_leap_year() {
428 let test_cases = [
429 (100, true),
430 (400, true),
431 (1000, true),
432 (1100, false),
433 (2022, false),
434 (2023, false),
435 (2024, true),
436 (2025, false),
437 (2300, false),
438 ];
439 for test_case in test_cases.iter() {
440 assert_eq!(
441 is_leap_year(test_case.0),
442 test_case.1,
443 "{} is {} a leap year",
444 test_case.0,
445 if test_case.1 { "" } else { "not" }
446 );
447 }
448}
449
450#[test]
451fn test_count_leap_years() {
452 let test_cases = [(400, 99), (401, 100), (1100, 274), (1200, 298), (2022, 498)];
453 for test_case in test_cases.iter() {
454 assert_eq!(
455 count_leap_years(test_case.0),
456 test_case.1,
457 "Year {}",
458 test_case.0
459 );
460 }
461}
462
463#[test]
464fn test_days_by_year() {
465 let test_cases = [
466 (0, 0),
467 (1, 0),
468 (2, 365),
469 (3, 730),
470 (4, 1095),
471 (5, 1461),
472 (6, 1826),
473 (7, 2191),
474 (8, 2556),
475 (9, 2922),
476 (10, 3287),
477 (400, 145734),
478 (401, 146100),
479 (402, 146465),
480 (403, 146830),
481 (404, 147195),
482 (800, 291834),
483 (801, 292200),
484 (802, 292565),
485 (803, 292930),
486 (804, 293295),
487 (2022, 738163),
488 (2023, 738528),
489 (2024, 738893),
490 (2025, 739259),
491 ];
492 for test_case in test_cases.iter() {
493 assert_eq!(
494 days_by_year(test_case.0),
495 test_case.1,
496 "Year {}",
497 test_case.0
498 );
499 }
500}
501
502#[test]
503fn test_days_by_month() {
504 let not_leap = vec![0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
505 let leap = vec![0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
506 let test_cases = [(2022, not_leap), (2024, leap)];
507 for test_case in test_cases.iter() {
508 assert_eq!(
509 days_by_month(test_case.0),
510 test_case.1,
511 "Year {}",
512 test_case.0
513 );
514 }
515}
516
517#[test]
518fn test_days_by_date() {
519 assert_eq!(
520 days_by_date(
521 0,
522 0,
523 0,
524 vec![0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
525 1
526 ),
527 0
528 );
529 assert_eq!(
530 days_by_date(
531 7,
532 4,
533 0,
534 vec![0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
535 1
536 ),
537 38
538 );
539}
540
541#[test]
542fn test_get_days_accumulated_by_month() {
543 assert_eq!(
544 get_days_accumulated_by_month(2000),
545 (
546 vec![0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366],
547 vec![0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
548 )
549 );
550 assert_eq!(
551 get_days_accumulated_by_month(1600),
552 (
553 vec![0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366],
554 vec![0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
555 )
556 );
557 assert_eq!(
558 get_days_accumulated_by_month(1700),
559 (
560 vec![0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
561 vec![0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
562 )
563 );
564}
565
566#[test]
567fn test_remain_day_printable() {
568 assert_eq!(remain_day_printable(1, 1, 1), " 1\n");
569 assert_eq!(remain_day_printable(1, 2, 1), " 1");
570 assert_eq!(remain_day_printable(2, 2, 1), " 2");
571 assert_eq!(remain_day_printable(31, 31, 1), " 31");
572 assert_eq!(remain_day_printable(31, 31, 7), " 31");
573}