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