1use crate::calendar_arithmetic::ArithmeticDate;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::error::{
8 DateAddError, DateFromFieldsError, DateNewError, EcmaReferenceYearError, UnknownEraError,
9};
10use crate::options::DateFromFieldsOptions;
11use crate::options::{DateAddOptions, DateDifferenceOptions};
12use crate::types::DateFields;
13use crate::{Calendar, Date, RangeError, types};
14use calendrical_calculations::rata_die::RataDie;
15use tinystr::tinystr;
16
17#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
48#[allow(clippy::exhaustive_structs)] pub struct Indian;
50
51#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
53pub struct IndianDateInner(ArithmeticDate<Indian>);
54
55const DAY_OFFSET: u16 = 80;
58const YEAR_OFFSET: i32 = 78;
60
61impl DateFieldsResolver for Indian {
62 type YearInfo = i32;
63
64 fn days_in_provided_month(year: i32, month: u8) -> u8 {
65 30
67 + (month <= 6) as u8
69 - (month == 1 && !calendrical_calculations::gregorian::is_leap_year(year + YEAR_OFFSET)) as u8
71 }
72
73 #[inline]
74 fn extended_year_from_era_year_unchecked(
75 &self,
76 era: &[u8],
77 era_year: i32,
78 ) -> Result<i32, UnknownEraError> {
79 match era {
80 b"shaka" => Ok(era_year),
81 _ => Err(UnknownEraError),
82 }
83 }
84
85 #[inline]
86 fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
87 extended_year
88 }
89
90 #[inline]
91 fn extended_from_year_info(&self, year_info: Self::YearInfo) -> i32 {
92 year_info
93 }
94
95 #[inline]
96 fn reference_year_from_month_day(
97 &self,
98 month: types::Month,
99 day: u8,
100 ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
101 let (ordinal_month, false) = (month.number(), month.is_leap()) else {
102 return Err(EcmaReferenceYearError::MonthNotInCalendar);
103 };
104 let shaka_year = if ordinal_month < 10 || (ordinal_month == 10 && day <= 10) {
107 1894
108 } else {
109 1893
110 };
111 Ok(shaka_year)
112 }
113
114 fn to_rata_die_inner(year: Self::YearInfo, month: u8, day: u8) -> RataDie {
115 let date = IndianDateInner(ArithmeticDate::new_unchecked(year, month, day));
117 let day_of_year_indian = Indian.day_of_year(&date).0; let days_in_year = Indian.days_in_year(&date);
119
120 let mut year_iso = date.0.year() + YEAR_OFFSET;
121 let day_of_year_iso = if day_of_year_indian + DAY_OFFSET > days_in_year {
123 year_iso += 1;
124 day_of_year_indian + DAY_OFFSET - days_in_year
126 } else {
127 day_of_year_indian + DAY_OFFSET
128 };
129
130 calendrical_calculations::gregorian::day_before_year(year_iso) + day_of_year_iso as i64
131 }
132}
133
134impl crate::cal::scaffold::UnstableSealed for Indian {}
135impl Calendar for Indian {
136 type DateInner = IndianDateInner;
137 type Year = types::EraYear;
138 type DateCompatibilityError = core::convert::Infallible;
139
140 fn new_date(
141 &self,
142 year: types::YearInput,
143 month: types::Month,
144 day: u8,
145 ) -> Result<Self::DateInner, DateNewError> {
146 ArithmeticDate::from_input_year_month_code_day(year, month, day, self).map(IndianDateInner)
147 }
148
149 fn from_fields(
150 &self,
151 fields: DateFields,
152 options: DateFromFieldsOptions,
153 ) -> Result<Self::DateInner, DateFromFieldsError> {
154 ArithmeticDate::from_fields(fields, options, self).map(IndianDateInner)
155 }
156
157 fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
159 let iso_year = calendrical_calculations::gregorian::year_from_fixed(rd)
160 .unwrap_or_else(|e| e.saturate());
161 let day_of_year_iso =
163 (rd - calendrical_calculations::gregorian::day_before_year(iso_year)) as u16;
164 let mut year = iso_year - YEAR_OFFSET;
166 let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
168 year -= 1;
169 let n_days = if calendrical_calculations::gregorian::is_leap_year(year + YEAR_OFFSET) {
170 366
171 } else {
172 365
173 };
174
175 n_days + day_of_year_iso - DAY_OFFSET
177 } else {
178 day_of_year_iso - DAY_OFFSET
179 };
180 let mut month = 1;
181 let mut day = day_of_year_indian as i32;
182 while month <= 12 {
183 let month_days = Self::days_in_provided_month(year, month) as i32;
184 if day <= month_days {
185 break;
186 } else {
187 day -= month_days;
188 month += 1;
189 }
190 }
191
192 debug_assert!(day <= Self::days_in_provided_month(year, month) as i32);
193 let day = day.try_into().unwrap_or(1);
194
195 IndianDateInner(ArithmeticDate::new_unchecked(year, month, day))
197 }
198
199 fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
201 date.0.to_rata_die()
202 }
203
204 fn has_cheap_iso_conversion(&self) -> bool {
205 false
206 }
207
208 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
209 Self::months_in_provided_year(date.0.year())
210 }
211
212 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
213 if self.is_in_leap_year(date) { 366 } else { 365 }
214 }
215
216 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
217 Self::days_in_provided_month(date.0.year(), date.0.month())
218 }
219
220 fn add(
221 &self,
222 date: &Self::DateInner,
223 duration: types::DateDuration,
224 options: DateAddOptions,
225 ) -> Result<Self::DateInner, DateAddError> {
226 date.0.added(duration, self, options).map(IndianDateInner)
227 }
228
229 fn until(
230 &self,
231 date1: &Self::DateInner,
232 date2: &Self::DateInner,
233 options: DateDifferenceOptions,
234 ) -> types::DateDuration {
235 date1.0.until(&date2.0, self, options)
236 }
237
238 fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
239 Ok(())
240 }
241
242 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
243 let extended_year = date.0.year();
244 types::EraYear {
245 era_index: Some(0),
246 era: tinystr!(16, "shaka"),
247 year: extended_year,
248 extended_year,
249 ambiguity: types::YearAmbiguity::CenturyRequired,
250 }
251 }
252
253 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
254 calendrical_calculations::gregorian::is_leap_year(date.0.year() + YEAR_OFFSET)
255 }
256
257 fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
258 types::MonthInfo::new(self, date.0)
259 }
260
261 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
262 types::DayOfMonth(date.0.day())
263 }
264
265 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
266 types::DayOfYear(
267 (
268 30 * (date.0.month() as u16 - 1)
270 + if date.0.month() - 1 < 6 { date.0.month() as u16 - 1 } else { 6 }
272 - (date.0.month() > 1 && !calendrical_calculations::gregorian::is_leap_year(date.0.year() + YEAR_OFFSET)) as u16
274 ) + date.0.day() as u16,
275 )
276 }
277
278 fn debug_name(&self) -> &'static str {
279 "Indian"
280 }
281
282 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
283 Some(crate::preferences::CalendarAlgorithm::Indian)
284 }
285}
286
287impl Indian {
288 pub fn new() -> Self {
290 Self
291 }
292}
293
294impl Date<Indian> {
295 pub fn try_new_indian(year: i32, month: u8, day: u8) -> Result<Date<Indian>, RangeError> {
311 ArithmeticDate::from_year_month_day(year, month, day, &Indian)
312 .map(IndianDateInner)
313 .map(|inner| Date::from_raw(inner, Indian))
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::types::RataDie;
321
322 #[test]
323 fn roundtrip_indian() {
324 let cases = [
329 TestCase {
330 rd: Date::try_new_iso(2022, 8, 29).unwrap().to_rata_die(),
331 year: 1944,
332 month: 6,
333 day: 7,
334 },
335 TestCase {
336 rd: Date::try_new_iso(2021, 8, 29).unwrap().to_rata_die(),
337 year: 1943,
338 month: 6,
339 day: 7,
340 },
341 TestCase {
342 rd: Date::try_new_iso(2020, 8, 29).unwrap().to_rata_die(),
343 year: 1942,
344 month: 6,
345 day: 7,
346 },
347 TestCase {
348 rd: Date::try_new_iso(2019, 8, 29).unwrap().to_rata_die(),
349 year: 1941,
350 month: 6,
351 day: 7,
352 },
353 TestCase {
354 rd: Date::try_new_iso(2023, 1, 27).unwrap().to_rata_die(),
355 year: 1944,
356 month: 11,
357 day: 7,
358 },
359 TestCase {
360 rd: Date::try_new_iso(2022, 1, 27).unwrap().to_rata_die(),
361 year: 1943,
362 month: 11,
363 day: 7,
364 },
365 TestCase {
366 rd: Date::try_new_iso(2021, 1, 27).unwrap().to_rata_die(),
367 year: 1942,
368 month: 11,
369 day: 7,
370 },
371 TestCase {
372 rd: Date::try_new_iso(2020, 1, 27).unwrap().to_rata_die(),
373 year: 1941,
374 month: 11,
375 day: 7,
376 },
377 ];
378
379 for case in cases {
380 check_case(case);
381 }
382 }
383
384 #[derive(Debug)]
385 struct TestCase {
386 rd: RataDie,
387 year: i32,
388 month: u8,
389 day: u8,
390 }
391
392 fn check_case(case: TestCase) {
393 let date = Date::from_rata_die(case.rd, Indian);
394
395 assert_eq!(date.to_rata_die(), case.rd, "{case:?}");
396
397 assert_eq!(date.era_year().year, case.year, "{case:?}");
398 assert_eq!(date.month().ordinal, case.month, "{case:?}");
399 assert_eq!(date.day_of_month().0, case.day, "{case:?}");
400
401 assert_eq!(
402 Date::try_new_indian(
403 date.era_year().extended_year,
404 date.month().ordinal,
405 date.day_of_month().0
406 ),
407 Ok(date)
408 );
409 }
410
411 #[test]
412 fn test_cases_near_epoch_start() {
413 let cases = [
414 TestCase {
415 rd: Date::try_new_iso(79, 3, 23).unwrap().to_rata_die(),
416 year: 1,
417 month: 1,
418 day: 2,
419 },
420 TestCase {
421 rd: Date::try_new_iso(79, 3, 22).unwrap().to_rata_die(),
422 year: 1,
423 month: 1,
424 day: 1,
425 },
426 TestCase {
427 rd: Date::try_new_iso(79, 3, 21).unwrap().to_rata_die(),
428 year: 0,
429 month: 12,
430 day: 30,
431 },
432 TestCase {
433 rd: Date::try_new_iso(79, 3, 20).unwrap().to_rata_die(),
434 year: 0,
435 month: 12,
436 day: 29,
437 },
438 TestCase {
439 rd: Date::try_new_iso(78, 3, 21).unwrap().to_rata_die(),
440 year: -1,
441 month: 12,
442 day: 30,
443 },
444 ];
445
446 for case in cases {
447 check_case(case);
448 }
449 }
450
451 #[test]
452 fn test_cases_near_rd_zero() {
453 let cases = [
454 TestCase {
455 rd: Date::try_new_iso(1, 3, 22).unwrap().to_rata_die(),
456 year: -77,
457 month: 1,
458 day: 1,
459 },
460 TestCase {
461 rd: Date::try_new_iso(1, 3, 21).unwrap().to_rata_die(),
462 year: -78,
463 month: 12,
464 day: 30,
465 },
466 TestCase {
467 rd: Date::try_new_iso(1, 1, 1).unwrap().to_rata_die(),
468 year: -78,
469 month: 10,
470 day: 11,
471 },
472 TestCase {
473 rd: Date::try_new_iso(0, 3, 21).unwrap().to_rata_die(),
474 year: -78,
475 month: 1,
476 day: 1,
477 },
478 TestCase {
479 rd: Date::try_new_iso(0, 1, 1).unwrap().to_rata_die(),
480 year: -79,
481 month: 10,
482 day: 11,
483 },
484 TestCase {
485 rd: Date::try_new_iso(-1, 3, 21).unwrap().to_rata_die(),
486 year: -80,
487 month: 12,
488 day: 30,
489 },
490 ];
491
492 for case in cases {
493 check_case(case);
494 }
495 }
496
497 #[test]
498 fn test_roundtrip_near_rd_zero() {
499 for i in -1000..=1000 {
500 let initial = RataDie::new(i);
501 let result = Date::from_rata_die(initial, Indian).to_rata_die();
502 assert_eq!(
503 initial, result,
504 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
505 );
506 }
507 }
508
509 #[test]
510 fn test_roundtrip_near_epoch_start() {
511 for i in 27570..=29570 {
513 let initial = RataDie::new(i);
514 let result = Date::from_rata_die(initial, Indian).to_rata_die();
515 assert_eq!(
516 initial, result,
517 "Roundtrip failed for initial: {initial:?}, result: {result:?}"
518 );
519 }
520 }
521
522 #[test]
523 fn test_directionality_near_rd_zero() {
524 for i in -100..=100 {
525 for j in -100..=100 {
526 let rd_i = RataDie::new(i);
527 let rd_j = RataDie::new(j);
528
529 let indian_i = Date::from_rata_die(rd_i, Indian);
530 let indian_j = Date::from_rata_die(rd_j, Indian);
531
532 assert_eq!(
533 i.cmp(&j),
534 indian_i.cmp(&indian_j),
535 "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}"
536 );
537 }
538 }
539 }
540
541 #[test]
542 fn test_directionality_near_epoch_start() {
543 for i in 28470..=28670 {
545 for j in 28470..=28670 {
546 let indian_i = Date::from_rata_die(RataDie::new(i), Indian);
547 let indian_j = Date::from_rata_die(RataDie::new(j), Indian);
548
549 assert_eq!(
550 i.cmp(&j),
551 indian_i.cmp(&indian_j),
552 "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}"
553 );
554 }
555 }
556 }
557
558 #[test]
559 fn test_constructor_roundtrip() {
560 let rds = crate::tests::get_interesting_rds();
561 for rd in rds {
562 let date = Date::from_rata_die(rd, Indian);
563 let reconstructed = Date::try_new_indian(
564 date.year().extended_year(),
565 date.month().ordinal,
566 date.day_of_month().0,
567 )
568 .unwrap();
569 assert_eq!(
570 reconstructed.to_rata_die(),
571 rd,
572 "Indian failed for RD {:?}",
573 rd
574 );
575 }
576 }
577}