1use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
21use crate::error::DateError;
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::rata_die::RataDie;
24use tinystr::tinystr;
25
26#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
40#[allow(clippy::exhaustive_structs)] pub struct Indian;
42
43#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
45pub struct IndianDateInner(ArithmeticDate<Indian>);
46
47impl CalendarArithmetic for Indian {
48 type YearInfo = i32;
49
50 fn days_in_provided_month(year: i32, month: u8) -> u8 {
51 if month == 1 {
52 if Self::provided_year_is_leap(year) {
53 31
54 } else {
55 30
56 }
57 } else if (2..=6).contains(&month) {
58 31
59 } else if (7..=12).contains(&month) {
60 30
61 } else {
62 0
63 }
64 }
65
66 fn months_in_provided_year(_: i32) -> u8 {
67 12
68 }
69
70 fn provided_year_is_leap(year: i32) -> bool {
71 Iso::provided_year_is_leap(year + 78)
72 }
73
74 fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
75 (12, 30)
76 }
77
78 fn days_in_provided_year(year: i32) -> u16 {
79 if Self::provided_year_is_leap(year) {
80 366
81 } else {
82 365
83 }
84 }
85}
86
87const DAY_OFFSET: u16 = 80;
90const YEAR_OFFSET: i32 = 78;
92
93impl crate::cal::scaffold::UnstableSealed for Indian {}
94impl Calendar for Indian {
95 type DateInner = IndianDateInner;
96 type Year = types::EraYear;
97 fn from_codes(
98 &self,
99 era: Option<&str>,
100 year: i32,
101 month_code: types::MonthCode,
102 day: u8,
103 ) -> Result<Self::DateInner, DateError> {
104 let year = match era {
105 Some("shaka") | None => year,
106 Some(_) => return Err(DateError::UnknownEra),
107 };
108 ArithmeticDate::new_from_codes(self, year, month_code, day).map(IndianDateInner)
109 }
110
111 fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
112 self.from_iso(Iso.from_rata_die(rd))
113 }
114
115 fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
116 Iso.to_rata_die(&self.to_iso(date))
117 }
118
119 fn from_iso(&self, iso: IsoDateInner) -> IndianDateInner {
121 let day_of_year_iso = Iso::day_of_year(iso);
123 let mut year = iso.0.year - YEAR_OFFSET;
125 let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
127 year -= 1;
128 let n_days = Self::days_in_provided_year(year);
129
130 n_days + day_of_year_iso - DAY_OFFSET
132 } else {
133 day_of_year_iso - DAY_OFFSET
134 };
135 IndianDateInner(ArithmeticDate::date_from_year_day(
136 year,
137 day_of_year_indian as u32,
138 ))
139 }
140
141 fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
143 let day_of_year_indian = date.0.day_of_year().0; let days_in_year = date.0.days_in_year();
145
146 let mut year = date.0.year + YEAR_OFFSET;
147 let day_of_year_iso = if day_of_year_indian + DAY_OFFSET > days_in_year {
149 year += 1;
150 day_of_year_indian + DAY_OFFSET - days_in_year
152 } else {
153 day_of_year_indian + DAY_OFFSET
154 };
155 Iso::iso_from_year_day(year, day_of_year_iso)
156 }
157
158 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
159 date.0.months_in_year()
160 }
161
162 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
163 date.0.days_in_year()
164 }
165
166 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
167 date.0.days_in_month()
168 }
169
170 fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
171 date.0.offset_date(offset, &());
172 }
173
174 #[allow(clippy::field_reassign_with_default)]
175 fn until(
176 &self,
177 date1: &Self::DateInner,
178 date2: &Self::DateInner,
179 _calendar2: &Self,
180 _largest_unit: DateDurationUnit,
181 _smallest_unit: DateDurationUnit,
182 ) -> DateDuration<Self> {
183 date1.0.until(date2.0, _largest_unit, _smallest_unit)
184 }
185
186 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
187 types::EraYear {
188 era_index: Some(0),
189 era: tinystr!(16, "shaka"),
190 year: self.extended_year(date),
191 ambiguity: types::YearAmbiguity::CenturyRequired,
192 }
193 }
194
195 fn extended_year(&self, date: &Self::DateInner) -> i32 {
196 date.0.extended_year()
197 }
198
199 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
200 Self::provided_year_is_leap(date.0.year)
201 }
202
203 fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
204 date.0.month()
205 }
206
207 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
208 date.0.day_of_month()
209 }
210
211 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
212 date.0.day_of_year()
213 }
214
215 fn debug_name(&self) -> &'static str {
216 "Indian"
217 }
218
219 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
220 Some(crate::preferences::CalendarAlgorithm::Indian)
221 }
222}
223
224impl Indian {
225 pub fn new() -> Self {
227 Self
228 }
229}
230
231impl Date<Indian> {
232 pub fn try_new_indian(year: i32, month: u8, day: u8) -> Result<Date<Indian>, RangeError> {
245 ArithmeticDate::new_from_ordinals(year, month, day)
246 .map(IndianDateInner)
247 .map(|inner| Date::from_raw(inner, Indian))
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use calendrical_calculations::rata_die::RataDie;
255 fn assert_roundtrip(y: i32, m: u8, d: u8, iso_y: i32, iso_m: u8, iso_d: u8) {
256 let indian =
257 Date::try_new_indian(y, m, d).expect("Indian date should construct successfully");
258 let iso = indian.to_iso();
259
260 assert_eq!(
261 iso.era_year().year,
262 iso_y,
263 "{y}-{m}-{d}: ISO year did not match"
264 );
265 assert_eq!(
266 iso.month().ordinal,
267 iso_m,
268 "{y}-{m}-{d}: ISO month did not match"
269 );
270 assert_eq!(
271 iso.day_of_month().0,
272 iso_d,
273 "{y}-{m}-{d}: ISO day did not match"
274 );
275
276 let roundtrip = iso.to_calendar(Indian);
277
278 assert_eq!(
279 roundtrip.era_year().year,
280 indian.era_year().year,
281 "{y}-{m}-{d}: roundtrip year did not match"
282 );
283 assert_eq!(
284 roundtrip.month().ordinal,
285 indian.month().ordinal,
286 "{y}-{m}-{d}: roundtrip month did not match"
287 );
288 assert_eq!(
289 roundtrip.day_of_month(),
290 indian.day_of_month(),
291 "{y}-{m}-{d}: roundtrip day did not match"
292 );
293 }
294
295 #[test]
296 fn roundtrip_indian() {
297 assert_roundtrip(1944, 6, 7, 2022, 8, 29);
302 assert_roundtrip(1943, 6, 7, 2021, 8, 29);
303 assert_roundtrip(1942, 6, 7, 2020, 8, 29);
304 assert_roundtrip(1941, 6, 7, 2019, 8, 29);
305 assert_roundtrip(1944, 11, 7, 2023, 1, 27);
306 assert_roundtrip(1943, 11, 7, 2022, 1, 27);
307 assert_roundtrip(1942, 11, 7, 2021, 1, 27);
308 assert_roundtrip(1941, 11, 7, 2020, 1, 27);
309 }
310
311 #[derive(Debug)]
312 struct TestCase {
313 iso_year: i32,
314 iso_month: u8,
315 iso_day: u8,
316 expected_year: i32,
317 expected_month: u8,
318 expected_day: u8,
319 }
320
321 fn check_case(case: TestCase) {
322 let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
323 let indian = iso.to_calendar(Indian);
324 assert_eq!(
325 indian.era_year().year,
326 case.expected_year,
327 "Year check failed for case: {case:?}"
328 );
329 assert_eq!(
330 indian.month().ordinal,
331 case.expected_month,
332 "Month check failed for case: {case:?}"
333 );
334 assert_eq!(
335 indian.day_of_month().0,
336 case.expected_day,
337 "Day check failed for case: {case:?}"
338 );
339 }
340
341 #[test]
342 fn test_cases_near_epoch_start() {
343 let cases = [
344 TestCase {
345 iso_year: 79,
346 iso_month: 3,
347 iso_day: 23,
348 expected_year: 1,
349 expected_month: 1,
350 expected_day: 2,
351 },
352 TestCase {
353 iso_year: 79,
354 iso_month: 3,
355 iso_day: 22,
356 expected_year: 1,
357 expected_month: 1,
358 expected_day: 1,
359 },
360 TestCase {
361 iso_year: 79,
362 iso_month: 3,
363 iso_day: 21,
364 expected_year: 0,
365 expected_month: 12,
366 expected_day: 30,
367 },
368 TestCase {
369 iso_year: 79,
370 iso_month: 3,
371 iso_day: 20,
372 expected_year: 0,
373 expected_month: 12,
374 expected_day: 29,
375 },
376 TestCase {
377 iso_year: 78,
378 iso_month: 3,
379 iso_day: 21,
380 expected_year: -1,
381 expected_month: 12,
382 expected_day: 30,
383 },
384 ];
385
386 for case in cases {
387 check_case(case);
388 }
389 }
390
391 #[test]
392 fn test_cases_near_rd_zero() {
393 let cases = [
394 TestCase {
395 iso_year: 1,
396 iso_month: 3,
397 iso_day: 22,
398 expected_year: -77,
399 expected_month: 1,
400 expected_day: 1,
401 },
402 TestCase {
403 iso_year: 1,
404 iso_month: 3,
405 iso_day: 21,
406 expected_year: -78,
407 expected_month: 12,
408 expected_day: 30,
409 },
410 TestCase {
411 iso_year: 1,
412 iso_month: 1,
413 iso_day: 1,
414 expected_year: -78,
415 expected_month: 10,
416 expected_day: 11,
417 },
418 TestCase {
419 iso_year: 0,
420 iso_month: 3,
421 iso_day: 21,
422 expected_year: -78,
423 expected_month: 1,
424 expected_day: 1,
425 },
426 TestCase {
427 iso_year: 0,
428 iso_month: 1,
429 iso_day: 1,
430 expected_year: -79,
431 expected_month: 10,
432 expected_day: 11,
433 },
434 TestCase {
435 iso_year: -1,
436 iso_month: 3,
437 iso_day: 21,
438 expected_year: -80,
439 expected_month: 12,
440 expected_day: 30,
441 },
442 ];
443
444 for case in cases {
445 check_case(case);
446 }
447 }
448
449 #[test]
450 fn test_roundtrip_near_rd_zero() {
451 for i in -1000..=1000 {
452 let initial = RataDie::new(i);
453 let result = Date::from_rata_die(initial, Iso)
454 .to_calendar(Indian)
455 .to_iso()
456 .to_rata_die();
457 assert_eq!(
458 initial, result,
459 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
460 );
461 }
462 }
463
464 #[test]
465 fn test_roundtrip_near_epoch_start() {
466 for i in 27570..=29570 {
468 let initial = RataDie::new(i);
469 let result = Date::from_rata_die(initial, Iso)
470 .to_calendar(Indian)
471 .to_iso()
472 .to_rata_die();
473 assert_eq!(
474 initial, result,
475 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
476 );
477 }
478 }
479
480 #[test]
481 fn test_directionality_near_rd_zero() {
482 for i in -100..=100 {
483 for j in -100..=100 {
484 let rd_i = RataDie::new(i);
485 let rd_j = RataDie::new(j);
486
487 let indian_i = Date::from_rata_die(rd_i, Indian);
488 let indian_j = Date::from_rata_die(rd_j, Indian);
489
490 assert_eq!(i.cmp(&j), indian_i.cmp(&indian_j), "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}");
491 }
492 }
493 }
494
495 #[test]
496 fn test_directionality_near_epoch_start() {
497 for i in 28470..=28670 {
499 for j in 28470..=28670 {
500 let indian_i = Date::from_rata_die(RataDie::new(i), Indian);
501 let indian_j = Date::from_rata_die(RataDie::new(j), Indian);
502
503 assert_eq!(i.cmp(&j), indian_i.cmp(&indian_j), "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}");
504 }
505 }
506 }
507}