1use crate::any_calendar::AnyCalendarKind;
35use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36use crate::iso::Iso;
37use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
38use tinystr::tinystr;
39
40#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
56#[allow(clippy::exhaustive_structs)] pub struct Indian;
58
59#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
61pub struct IndianDateInner(ArithmeticDate<Indian>);
62
63impl CalendarArithmetic for Indian {
64 type YearInfo = ();
65
66 fn month_days(year: i32, month: u8, _data: ()) -> u8 {
67 if month == 1 {
68 if Self::is_leap_year(year, ()) {
69 31
70 } else {
71 30
72 }
73 } else if (2..=6).contains(&month) {
74 31
75 } else if (7..=12).contains(&month) {
76 30
77 } else {
78 0
79 }
80 }
81
82 fn months_for_every_year(_: i32, _data: ()) -> u8 {
83 12
84 }
85
86 fn is_leap_year(year: i32, _data: ()) -> bool {
87 Iso::is_leap_year(year + 78, ())
88 }
89
90 fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) {
91 (12, 30)
92 }
93
94 fn days_in_provided_year(year: i32, _data: ()) -> u16 {
95 if Self::is_leap_year(year, ()) {
96 366
97 } else {
98 365
99 }
100 }
101}
102
103const DAY_OFFSET: u16 = 80;
106const YEAR_OFFSET: i32 = 78;
108
109impl Calendar for Indian {
110 type DateInner = IndianDateInner;
111 fn date_from_codes(
112 &self,
113 era: types::Era,
114 year: i32,
115 month_code: types::MonthCode,
116 day: u8,
117 ) -> Result<Self::DateInner, CalendarError> {
118 if era.0 != tinystr!(16, "saka") && era.0 != tinystr!(16, "indian") {
119 return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
120 }
121
122 ArithmeticDate::new_from_codes(self, year, month_code, day).map(IndianDateInner)
123 }
124
125 fn date_from_iso(&self, iso: Date<Iso>) -> IndianDateInner {
127 let day_of_year_iso = Iso::day_of_year(*iso.inner());
129 let mut year = iso.inner().0.year - YEAR_OFFSET;
131 let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
133 year -= 1;
134 let n_days = Self::days_in_provided_year(year, ());
135
136 n_days + day_of_year_iso - DAY_OFFSET
138 } else {
139 day_of_year_iso - DAY_OFFSET
140 };
141 IndianDateInner(ArithmeticDate::date_from_year_day(
142 year,
143 day_of_year_indian as u32,
144 ))
145 }
146
147 fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
149 let day_of_year_indian = date.0.day_of_year();
150 let days_in_year = date.0.days_in_year();
151
152 let mut year = date.0.year + YEAR_OFFSET;
153 let day_of_year_iso = if day_of_year_indian + DAY_OFFSET >= days_in_year {
154 year += 1;
155 day_of_year_indian + DAY_OFFSET - days_in_year
157 } else {
158 day_of_year_indian + DAY_OFFSET
159 };
160
161 Iso::iso_from_year_day(year, day_of_year_iso)
162 }
163
164 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
165 date.0.months_in_year()
166 }
167
168 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
169 date.0.days_in_year()
170 }
171
172 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
173 date.0.days_in_month()
174 }
175
176 fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
177 Iso.day_of_week(Indian.date_to_iso(date).inner())
178 }
179
180 fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
181 date.0.offset_date(offset, &());
182 }
183
184 #[allow(clippy::field_reassign_with_default)]
185 fn until(
186 &self,
187 date1: &Self::DateInner,
188 date2: &Self::DateInner,
189 _calendar2: &Self,
190 _largest_unit: DateDurationUnit,
191 _smallest_unit: DateDurationUnit,
192 ) -> DateDuration<Self> {
193 date1.0.until(date2.0, _largest_unit, _smallest_unit)
194 }
195
196 fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
197 types::FormattableYear {
198 era: types::Era(tinystr!(16, "saka")),
199 number: date.0.year,
200 cyclic: None,
201 related_iso: None,
202 }
203 }
204
205 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
206 Self::is_leap_year(date.0.year, ())
207 }
208
209 fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
210 date.0.month()
211 }
212
213 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
214 date.0.day_of_month()
215 }
216
217 fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
218 let prev_year = types::FormattableYear {
219 era: types::Era(tinystr!(16, "saka")),
220 number: date.0.year - 1,
221 cyclic: None,
222 related_iso: None,
223 };
224 let next_year = types::FormattableYear {
225 era: types::Era(tinystr!(16, "saka")),
226 number: date.0.year + 1,
227 cyclic: None,
228 related_iso: None,
229 };
230 types::DayOfYearInfo {
231 day_of_year: date.0.day_of_year(),
232 days_in_year: date.0.days_in_year(),
233 prev_year,
234 days_in_prev_year: Indian::days_in_year_direct(date.0.year - 1),
235 next_year,
236 }
237 }
238
239 fn debug_name(&self) -> &'static str {
240 "Indian"
241 }
242
243 fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
244 Some(AnyCalendarKind::Indian)
245 }
246}
247
248impl Indian {
249 pub fn new() -> Self {
251 Self
252 }
253
254 fn days_in_year_direct(year: i32) -> u16 {
255 if Indian::is_leap_year(year, ()) {
256 366
257 } else {
258 365
259 }
260 }
261}
262
263impl Date<Indian> {
264 pub fn try_new_indian_date(
277 year: i32,
278 month: u8,
279 day: u8,
280 ) -> Result<Date<Indian>, CalendarError> {
281 ArithmeticDate::new_from_ordinals(year, month, day)
282 .map(IndianDateInner)
283 .map(|inner| Date::from_raw(inner, Indian))
284 }
285}
286
287impl DateTime<Indian> {
288 pub fn try_new_indian_datetime(
305 year: i32,
306 month: u8,
307 day: u8,
308 hour: u8,
309 minute: u8,
310 second: u8,
311 ) -> Result<DateTime<Indian>, CalendarError> {
312 Ok(DateTime {
313 date: Date::try_new_indian_date(year, month, day)?,
314 time: Time::try_new(hour, minute, second, 0)?,
315 })
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use calendrical_calculations::rata_die::RataDie;
323 fn assert_roundtrip(y: i32, m: u8, d: u8, iso_y: i32, iso_m: u8, iso_d: u8) {
324 let indian =
325 Date::try_new_indian_date(y, m, d).expect("Indian date should construct successfully");
326 let iso = indian.to_iso();
327
328 assert_eq!(
329 iso.year().number,
330 iso_y,
331 "{y}-{m}-{d}: ISO year did not match"
332 );
333 assert_eq!(
334 iso.month().ordinal as u8,
335 iso_m,
336 "{y}-{m}-{d}: ISO month did not match"
337 );
338 assert_eq!(
339 iso.day_of_month().0 as u8,
340 iso_d,
341 "{y}-{m}-{d}: ISO day did not match"
342 );
343
344 let roundtrip = iso.to_calendar(Indian);
345
346 assert_eq!(
347 roundtrip.year().number,
348 indian.year().number,
349 "{y}-{m}-{d}: roundtrip year did not match"
350 );
351 assert_eq!(
352 roundtrip.month().ordinal,
353 indian.month().ordinal,
354 "{y}-{m}-{d}: roundtrip month did not match"
355 );
356 assert_eq!(
357 roundtrip.day_of_month(),
358 indian.day_of_month(),
359 "{y}-{m}-{d}: roundtrip day did not match"
360 );
361 }
362
363 #[test]
364 fn roundtrip_indian() {
365 assert_roundtrip(1944, 6, 7, 2022, 8, 29);
370 assert_roundtrip(1943, 6, 7, 2021, 8, 29);
371 assert_roundtrip(1942, 6, 7, 2020, 8, 29);
372 assert_roundtrip(1941, 6, 7, 2019, 8, 29);
373 assert_roundtrip(1944, 11, 7, 2023, 1, 27);
374 assert_roundtrip(1943, 11, 7, 2022, 1, 27);
375 assert_roundtrip(1942, 11, 7, 2021, 1, 27);
376 assert_roundtrip(1941, 11, 7, 2020, 1, 27);
377 }
378
379 #[derive(Debug)]
380 struct TestCase {
381 iso_year: i32,
382 iso_month: u8,
383 iso_day: u8,
384 expected_year: i32,
385 expected_month: u32,
386 expected_day: u32,
387 }
388
389 fn check_case(case: TestCase) {
390 let iso = Date::try_new_iso_date(case.iso_year, case.iso_month, case.iso_day).unwrap();
391 let saka = iso.to_calendar(Indian);
392 assert_eq!(
393 saka.year().number,
394 case.expected_year,
395 "Year check failed for case: {case:?}"
396 );
397 assert_eq!(
398 saka.month().ordinal,
399 case.expected_month,
400 "Month check failed for case: {case:?}"
401 );
402 assert_eq!(
403 saka.day_of_month().0,
404 case.expected_day,
405 "Day check failed for case: {case:?}"
406 );
407 }
408
409 #[test]
410 fn test_cases_near_epoch_start() {
411 let cases = [
412 TestCase {
413 iso_year: 79,
414 iso_month: 3,
415 iso_day: 23,
416 expected_year: 1,
417 expected_month: 1,
418 expected_day: 2,
419 },
420 TestCase {
421 iso_year: 79,
422 iso_month: 3,
423 iso_day: 22,
424 expected_year: 1,
425 expected_month: 1,
426 expected_day: 1,
427 },
428 TestCase {
429 iso_year: 79,
430 iso_month: 3,
431 iso_day: 21,
432 expected_year: 0,
433 expected_month: 12,
434 expected_day: 30,
435 },
436 TestCase {
437 iso_year: 79,
438 iso_month: 3,
439 iso_day: 20,
440 expected_year: 0,
441 expected_month: 12,
442 expected_day: 29,
443 },
444 TestCase {
445 iso_year: 78,
446 iso_month: 3,
447 iso_day: 21,
448 expected_year: -1,
449 expected_month: 12,
450 expected_day: 30,
451 },
452 ];
453
454 for case in cases {
455 check_case(case);
456 }
457 }
458
459 #[test]
460 fn test_cases_near_rd_zero() {
461 let cases = [
462 TestCase {
463 iso_year: 1,
464 iso_month: 3,
465 iso_day: 22,
466 expected_year: -77,
467 expected_month: 1,
468 expected_day: 1,
469 },
470 TestCase {
471 iso_year: 1,
472 iso_month: 3,
473 iso_day: 21,
474 expected_year: -78,
475 expected_month: 12,
476 expected_day: 30,
477 },
478 TestCase {
479 iso_year: 1,
480 iso_month: 1,
481 iso_day: 1,
482 expected_year: -78,
483 expected_month: 10,
484 expected_day: 11,
485 },
486 TestCase {
487 iso_year: 0,
488 iso_month: 3,
489 iso_day: 21,
490 expected_year: -78,
491 expected_month: 1,
492 expected_day: 1,
493 },
494 TestCase {
495 iso_year: 0,
496 iso_month: 1,
497 iso_day: 1,
498 expected_year: -79,
499 expected_month: 10,
500 expected_day: 11,
501 },
502 TestCase {
503 iso_year: -1,
504 iso_month: 3,
505 iso_day: 21,
506 expected_year: -80,
507 expected_month: 12,
508 expected_day: 30,
509 },
510 ];
511
512 for case in cases {
513 check_case(case);
514 }
515 }
516
517 #[test]
518 fn test_roundtrip_near_rd_zero() {
519 for i in -1000..=1000 {
520 let initial = RataDie::new(i);
521 let result = Iso::fixed_from_iso(
522 Iso::iso_from_fixed(initial)
523 .to_calendar(Indian)
524 .to_calendar(Iso)
525 .inner,
526 );
527 assert_eq!(
528 initial, result,
529 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
530 );
531 }
532 }
533
534 #[test]
535 fn test_roundtrip_near_epoch_start() {
536 for i in 27570..=29570 {
538 let initial = RataDie::new(i);
539 let result = Iso::fixed_from_iso(
540 Iso::iso_from_fixed(initial)
541 .to_calendar(Indian)
542 .to_calendar(Iso)
543 .inner,
544 );
545 assert_eq!(
546 initial, result,
547 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
548 );
549 }
550 }
551
552 #[test]
553 fn test_directionality_near_rd_zero() {
554 for i in -100..=100 {
555 for j in -100..=100 {
556 let rd_i = RataDie::new(i);
557 let rd_j = RataDie::new(j);
558
559 let indian_i = Iso::iso_from_fixed(rd_i).to_calendar(Indian);
560 let indian_j = Iso::iso_from_fixed(rd_j).to_calendar(Indian);
561
562 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:?}");
563 }
564 }
565 }
566
567 #[test]
568 fn test_directionality_near_epoch_start() {
569 for i in 28470..=28670 {
571 for j in 28470..=28670 {
572 let indian_i = Iso::iso_from_fixed(RataDie::new(i)).to_calendar(Indian);
573 let indian_j = Iso::iso_from_fixed(RataDie::new(j)).to_calendar(Indian);
574
575 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:?}");
576 }
577 }
578 }
579}