Skip to main content

kosher_rust/zmanim/
primitives.rs

1//! Low-level zman formulas used to build higher-level presets.
2//!
3//! [`ZmanPrimitive`] is the internal expression language for zman calculations.
4//! Variants represent either:
5//! - base astronomical events (for example sunrise/sunset),
6//! - transformed events (fixed offsets or degree-based offsets), or
7//! - derived halachic times computed from two boundary events.
8//!
9//! Most users should prefer the ready-made constants in [`crate::zmanim::presets`].
10//! Use this module when you need to compose a custom zman definition that is
11//! not already provided by a preset.
12
13use core_maths::CoreFloat;
14use icu_calendar::{
15    options::{DateAddOptions, Overflow},
16    types::DateDuration,
17};
18use jiff::{SignedDuration as Duration, Timestamp, ToSpan, tz::TimeZone};
19
20use crate::{
21    calendar::{HebrewCalendarDate, HebrewHolidayCalendar, holiday::Holiday},
22    zmanim::{
23        ZmanLike, ZmanimCalculator,
24        molad::{is_same_gregorian_day, months_molad},
25        types::error::ZmanimError,
26    },
27};
28
29static CIVIL_ZENITH: f64 = 6.0;
30static NAUTICAL_ZENITH: f64 = 12.0;
31static ASTRONOMICAL_ZENITH: f64 = 18.0;
32
33/// A low-level building block for calculating zmanim.
34///
35/// These should typically not be used directly. Instead, use the presets in [`crate::zmanim::presets`].
36#[derive(Debug, Clone)]
37pub enum ZmanPrimitive {
38    /// Sunrise at the configured location/date.
39    ElevationAdjustedSunrise,
40    /// Sunrise at sea level (no elevation adjustment).
41    SeaLevelSunrise,
42    /// Sunrise using the configured elevation mode (sea-level or elevation-adjusted).
43    ConfiguredSunrise,
44    /// Sunset using the configured elevation mode (sea-level or elevation-adjusted).
45    ConfiguredSunset,
46    /// Solar transit (local apparent noon / astronomical chatzos).
47    SolarTransit,
48    /// Solar anti-transit (local apparent midnight / astronomical chatzos halayla).
49    SolarMidnight,
50    /// The midpoint between sunrise and sunset.
51    ChatzosHayomAsHalfDay,
52    /// Solar Transit or the midpoint between sunrise and sunset depending on the configuration.
53    ChatzosHayom,
54    /// The midpoint between sunset and the tomorrows sunrise.
55    ChatzosHalaylaAsHalfDay,
56    /// Solar Midnight or the midpoint between sunset and the tomorrows sunrise depending on the configuration.
57    ChatzosHalayla,
58    /// Sunset at the configured location/date.
59    ElevationAdjustedSunset,
60    /// Sunset at sea level (no elevation adjustment).
61    SeaLevelSunset,
62    /// Time before sunrise when the sun is `degrees` below the geometric horizon (no elevation adjustment).
63    SunriseOffsetByDegrees(f64),
64    /// Time after sunset when the sun is `degrees` below the geometric horizon (no elevation adjustment).
65    SunsetOffsetByDegrees(f64),
66    /// Local mean time at the given hour (0.0–24.0).
67    LocalMeanTime(f64),
68    /// Shabbos/Yom Tov candle lighting time based on configuration.
69    CandleLighting,
70    /// A fixed time offset from another [`ZmanPrimitive`].
71    Offset(&'static ZmanPrimitive, Duration),
72    /// An offset in "shaos zmaniyos" according to the GRA from another [`ZmanPrimitive`].
73    ZmanisOffset(&'static ZmanPrimitive, f64),
74    /// An offset expressed as a fraction of the half-day between two [`ZmanPrimitive`]s.
75    HalfDayBasedOffset(&'static ZmanPrimitive, &'static ZmanPrimitive, f64),
76    /// Sof zman shma derived from two bounding [`ZmanPrimitive`]s.
77    Shema(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
78    /// Mincha gedola derived from two bounding [`ZmanPrimitive`]s.
79    MinchaGedola(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
80    /// Samuch le-mincha ketana derived from two bounding [`ZmanPrimitive`]s.
81    SamuchLeMinchaKetana(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
82    /// Mincha ketana derived from two bounding [`ZmanPrimitive`]s.
83    MinchaKetana(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
84    /// Sof zman tefila derived from two bounding [`ZmanPrimitive`]s.
85    Tefila(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
86    /// Plag hamincha derived from two bounding [`ZmanPrimitive`]s.
87    PlagHamincha(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
88    /// Sof zman biur chametz derived from two bounding [`ZmanPrimitive`]s.
89    SofZmanBiurChametz(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
90    /// Sof zman achilas chametz derived from two bounding [`ZmanPrimitive`]s.
91    SofZmanAchilasChametz(&'static ZmanPrimitive, &'static ZmanPrimitive, bool),
92    /// Tzais according to the shita of Yeshivas Ateret Torah
93    TzaisAteretTorah,
94    /// The latest time of _Kiddush Levana_ calculated as 15 days after the molad.
95    SofZmanKidushLevana15Days,
96    /// The latest time of _Kiddush Levana_ according to the the opinion of the Maharil
97    /// that it is calculated as halfway between molad and molad.
98    SofZmanKidushLevanaBetweenMoldos,
99    /// The earliest time of _Kiddush Levana_ according to Rabbeinu Yonahs opinion that it can be said 3 days after the molad.
100    TchilasZmanKidushLevana3Days,
101    /// The earliest time of _Kiddush Levana_ according to the opinions that it should
102    /// not be said until 7 days after the molad.
103    TchilasZmanKidushLevana7Days,
104    /// Bain hashmashos (Rabbeinu Tam, 2-stars): `sunset + (sunrise - alos19.8°) * 5/18`.
105    BainHashmashosRt2Stars,
106    /// Mincha gedola (Ahavat Shalom): later of `chatzos + 30m` and `chatzos + 1/2 shaah`.
107    MinchaGedolaAhavatShalom,
108    /// Mincha gedola GRA, but no earlier than 30 minutes after chatzos.
109    MinchaGedolaGraGreaterThan30,
110    /// Mincha ketana (Ahavat Shalom): `2.5` shaos zmaniyos before tzais `3.8°` (day = alos16.1° → tzais3.8°).
111    MinchaKetanaAhavatShalom,
112    /// Plag hamincha (Ahavat Shalom): `1.25` shaos zmaniyos before tzais `3.8°` (day = alos16.1° → tzais3.8°).
113    PlagAhavatShalom,
114    /// The beginning of civil twilight (dawn), when the sun is 6° below the geometric horizon (96° zenith).
115    BeginCivilTwilight,
116    /// The end of civil twilight, when the sun is 6° below the geometric horizon (96° zenith).
117    EndCivilTwilight,
118    /// The beginning of nautical twilight, when the sun is 12° below the geometric horizon (102° zenith).
119    BeginNauticalTwilight,
120    /// The end of nautical twilight, when the sun is 12° below the geometric horizon (102° zenith).
121    EndNauticalTwilight,
122    /// The beginning of astronomical twilight, when the sun is 18° below the geometric horizon (108° zenith).
123    BeginAstronomicalTwilight,
124    /// The end of astronomical twilight, when the sun is 18° below the geometric horizon (108° zenith).
125    EndAstronomicalTwilight,
126    /// Configured sunset, or the westernmost solar azimuth when sunset does not occur.
127    SunsetOrWesternmostSolarAzimuth,
128    /// Configured sunrise, or the easternmost solar azimuth when sunrise does not occur.
129    SunriseOrEasternmostSolarAzimuth,
130}
131
132#[cfg(feature = "defmt")]
133impl defmt::Format for ZmanPrimitive {
134    fn format(&self, fmt: defmt::Formatter) {
135        match self {
136            ZmanPrimitive::ElevationAdjustedSunrise => defmt::write!(fmt, "ElevationAdjustedSunrise"),
137            ZmanPrimitive::SeaLevelSunrise => defmt::write!(fmt, "SeaLevelSunrise"),
138            ZmanPrimitive::ConfiguredSunrise => defmt::write!(fmt, "ConfiguredSunrise"),
139            ZmanPrimitive::ConfiguredSunset => defmt::write!(fmt, "ConfiguredSunset"),
140            ZmanPrimitive::SolarTransit => defmt::write!(fmt, "SolarTransit"),
141            ZmanPrimitive::SolarMidnight => defmt::write!(fmt, "SolarMidnight"),
142            ZmanPrimitive::ElevationAdjustedSunset => defmt::write!(fmt, "ElevationAdjustedSunset"),
143            ZmanPrimitive::SeaLevelSunset => defmt::write!(fmt, "SeaLevelSunset"),
144            ZmanPrimitive::SunriseOffsetByDegrees(degrees) => {
145                defmt::write!(fmt, "SunriseOffsetByDegrees({})", degrees)
146            }
147            ZmanPrimitive::SunsetOffsetByDegrees(degrees) => {
148                defmt::write!(fmt, "SunsetOffsetByDegrees({})", degrees)
149            }
150            ZmanPrimitive::LocalMeanTime(hour) => defmt::write!(fmt, "LocalMeanTime({})", hour),
151            ZmanPrimitive::CandleLighting => defmt::write!(fmt, "CandleLighting"),
152            ZmanPrimitive::Offset(primitive, duration) => {
153                defmt::write!(fmt, "Offset({}, {}s)", primitive, duration.as_secs_f64())
154            }
155            ZmanPrimitive::ZmanisOffset(primitive, hours) => {
156                defmt::write!(fmt, "ZmanisOffset({}, {})", primitive, hours)
157            }
158            ZmanPrimitive::HalfDayBasedOffset(start, end, fraction) => {
159                defmt::write!(fmt, "HalfDayBasedOffset({}, {}, {})", start, end, fraction)
160            }
161            ZmanPrimitive::Shema(start, end, fixed) => {
162                defmt::write!(fmt, "Shema({}, {}, {})", start, end, fixed)
163            }
164            ZmanPrimitive::MinchaGedola(start, end, fixed) => {
165                defmt::write!(fmt, "MinchaGedola({}, {}, {})", start, end, fixed)
166            }
167            ZmanPrimitive::SamuchLeMinchaKetana(start, end, fixed) => {
168                defmt::write!(fmt, "SamuchLeMinchaKetana({}, {}, {})", start, end, fixed)
169            }
170            ZmanPrimitive::MinchaKetana(start, end, fixed) => {
171                defmt::write!(fmt, "MinchaKetana({}, {}, {})", start, end, fixed)
172            }
173            ZmanPrimitive::Tefila(start, end, fixed) => {
174                defmt::write!(fmt, "Tefila({}, {}, {})", start, end, fixed)
175            }
176            ZmanPrimitive::PlagHamincha(start, end, fixed) => {
177                defmt::write!(fmt, "PlagHamincha({}, {}, {})", start, end, fixed)
178            }
179            ZmanPrimitive::SofZmanBiurChametz(start, end, fixed) => {
180                defmt::write!(fmt, "SofZmanBiurChametz({}, {}, {})", start, end, fixed)
181            }
182            ZmanPrimitive::SofZmanAchilasChametz(start, end, fixed) => {
183                defmt::write!(fmt, "SofZmanAchilasChametz({}, {}, {})", start, end, fixed)
184            }
185            ZmanPrimitive::TzaisAteretTorah => defmt::write!(fmt, "TzaisAteretTorah"),
186            ZmanPrimitive::SofZmanKidushLevana15Days => {
187                defmt::write!(fmt, "SofZmanKidushLevana15Days")
188            }
189            ZmanPrimitive::SofZmanKidushLevanaBetweenMoldos => {
190                defmt::write!(fmt, "SofZmanKidushLevanaBetweenMoldos")
191            }
192            ZmanPrimitive::TchilasZmanKidushLevana3Days => {
193                defmt::write!(fmt, "TchilasZmanKidushLevana3Days")
194            }
195            ZmanPrimitive::TchilasZmanKidushLevana7Days => {
196                defmt::write!(fmt, "TchilasZmanKidushLevana7Days")
197            }
198            ZmanPrimitive::BainHashmashosRt2Stars => defmt::write!(fmt, "BainHashmashosRt2Stars"),
199            ZmanPrimitive::MinchaGedolaAhavatShalom => defmt::write!(fmt, "MinchaGedolaAhavatShalom"),
200            ZmanPrimitive::MinchaGedolaGraGreaterThan30 => {
201                defmt::write!(fmt, "MinchaGedolaGraGreaterThan30")
202            }
203            ZmanPrimitive::MinchaKetanaAhavatShalom => defmt::write!(fmt, "MinchaKetanaAhavatShalom"),
204            ZmanPrimitive::PlagAhavatShalom => defmt::write!(fmt, "PlagAhavatShalom"),
205            ZmanPrimitive::BeginCivilTwilight => defmt::write!(fmt, "BeginCivilTwilight"),
206            ZmanPrimitive::EndCivilTwilight => defmt::write!(fmt, "EndCivilTwilight"),
207            ZmanPrimitive::BeginNauticalTwilight => defmt::write!(fmt, "BeginNauticalTwilight"),
208            ZmanPrimitive::EndNauticalTwilight => defmt::write!(fmt, "EndNauticalTwilight"),
209            ZmanPrimitive::BeginAstronomicalTwilight => defmt::write!(fmt, "BeginAstronomicalTwilight"),
210            ZmanPrimitive::EndAstronomicalTwilight => defmt::write!(fmt, "EndAstronomicalTwilight"),
211            ZmanPrimitive::SunsetOrWesternmostSolarAzimuth => {
212                defmt::write!(fmt, "SunsetOrWesternmostSolarAzimuth")
213            }
214            ZmanPrimitive::SunriseOrEasternmostSolarAzimuth => {
215                defmt::write!(fmt, "SunriseOrEasternmostSolarAzimuth")
216            }
217            ZmanPrimitive::ChatzosHayomAsHalfDay => defmt::write!(fmt, "ChatzosHayomAsHalfDay"),
218            ZmanPrimitive::ChatzosHayom => defmt::write!(fmt, "ChatzosHayom"),
219            ZmanPrimitive::ChatzosHalaylaAsHalfDay => defmt::write!(fmt, "ChatzosHalaylaAsHalfDay"),
220            ZmanPrimitive::ChatzosHalayla => defmt::write!(fmt, "ChatzosHalayla"),
221        }
222    }
223}
224
225impl ZmanLike for ZmanPrimitive {
226    fn calculate(&self, calculator: &ZmanimCalculator) -> Result<Timestamp, ZmanimError> {
227        match *self {
228            ZmanPrimitive::ConfiguredSunrise => {
229                if calculator.config.use_elevation {
230                    ZmanPrimitive::ElevationAdjustedSunrise.calculate(calculator)
231                } else {
232                    ZmanPrimitive::SeaLevelSunrise.calculate(calculator)
233                }
234            }
235            ZmanPrimitive::ConfiguredSunset => {
236                if calculator.config.use_elevation {
237                    ZmanPrimitive::ElevationAdjustedSunset.calculate(calculator)
238                } else {
239                    ZmanPrimitive::SeaLevelSunset.calculate(calculator)
240                }
241            }
242            ZmanPrimitive::ElevationAdjustedSunrise => {
243                crate::zmanim::astronomy::sunrise(calculator.date, &calculator.location, true)
244            }
245            ZmanPrimitive::SeaLevelSunrise => {
246                crate::zmanim::astronomy::sunrise(calculator.date, &calculator.location, false)
247            }
248            ZmanPrimitive::SolarTransit => crate::zmanim::astronomy::solar_noon(calculator.date, &calculator.location),
249            ZmanPrimitive::SolarMidnight => {
250                crate::zmanim::astronomy::solar_midnight(calculator.date, &calculator.location)
251            }
252            ZmanPrimitive::ElevationAdjustedSunset => {
253                crate::zmanim::astronomy::sunset(calculator.date, &calculator.location, true)
254            }
255            ZmanPrimitive::SeaLevelSunset => {
256                crate::zmanim::astronomy::sunset(calculator.date, &calculator.location, false)
257            }
258            ZmanPrimitive::SunriseOffsetByDegrees(degrees) => {
259                crate::zmanim::astronomy::sunrise_offset_by_degrees(calculator.date, &calculator.location, degrees)
260            }
261            ZmanPrimitive::SunsetOffsetByDegrees(degrees) => {
262                crate::zmanim::astronomy::sunset_offset_by_degrees(calculator.date, &calculator.location, degrees)
263            }
264            ZmanPrimitive::LocalMeanTime(hours) => {
265                let date = calculator.date;
266                let location = calculator.location.clone();
267                if !(0.0..24.0).contains(&hours) {
268                    return Err(ZmanimError::InvalidHours);
269                }
270
271                let adjusted_date = crate::zmanim::astronomy::adjusted_local_date(date, &location)?;
272                let midnight = adjusted_date.at(0, 0, 0, 0);
273                let lmt_nanos = CoreFloat::round(hours * 3600.0 * 1_000_000_000.0) as i64;
274                let offset_nanos = CoreFloat::round(location.longitude * 240.0 * 1_000_000_000.0) as i64;
275                let lmt_dt = midnight
276                    .checked_add(Duration::from_nanos(lmt_nanos))
277                    .and_then(|dt| dt.checked_sub(Duration::from_nanos(offset_nanos)))
278                    .map_err(|_| ZmanimError::TimeConversionError)?;
279                let utc = lmt_dt
280                    .to_zoned(TimeZone::UTC)
281                    .map_err(|_| ZmanimError::TimeConversionError)?
282                    .timestamp();
283
284                Ok(utc)
285            }
286            ZmanPrimitive::CandleLighting => {
287                // Sea-level sunset occurs earlier than elevation-adjusted sunset.
288                // Since candle lighting times are used strictly *l’chumrah* (stringently),
289                // we choose the earlier of the two values.
290                //
291                // This logic is intentionally limited to candle lighting. For other zmanim
292                // (e.g., sunset itself), an earlier time is not universally considered
293                // *l’chumrah*, so we do not apply this rule there.
294                let sunset = ZmanPrimitive::SeaLevelSunset.calculate(calculator)?;
295                Ok(sunset - calculator.config.candle_lighting_offset)
296            }
297            ZmanPrimitive::Offset(event, duration) => {
298                let event_time = event.calculate(calculator)?;
299                Ok(event_time + duration)
300            }
301            ZmanPrimitive::ZmanisOffset(event, hours) => {
302                let event_time = event.calculate(calculator)?;
303                let sunrise = calculator.calculate(&ZmanPrimitive::ConfiguredSunrise)?;
304                let sunset = calculator.calculate(&ZmanPrimitive::ConfiguredSunset)?;
305                let shaah_zmanis = sunset.duration_since(sunrise) / 12;
306                let offset = shaah_zmanis.mul_f64(hours);
307
308                Ok(event_time + offset)
309            }
310
311            ZmanPrimitive::HalfDayBasedOffset(event1, event2, hours) => {
312                let event1_time = event1.calculate(calculator)?;
313                let event2_time = event2.calculate(calculator)?;
314                let shaah_zmanis = event2_time.duration_since(event1_time) / 6;
315                let offset = shaah_zmanis.mul_f64(hours);
316                if hours >= 0.0 {
317                    Ok(event1_time + offset)
318                } else {
319                    Ok(event2_time + offset)
320                }
321            }
322            ZmanPrimitive::Shema(event1, event2, synchronous) => {
323                let event1_time = event1.calculate(calculator)?;
324                let event2_time = event2.calculate(calculator);
325                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
326                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
327                    let shaah_zmanis = chatzos.duration_since(event1_time) / 6;
328                    let offset = shaah_zmanis.mul_f64(3.0);
329                    Ok(event1_time + offset)
330                } else {
331                    let event2_time = event2_time?;
332                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
333                    let offset = shaah_zmanis.mul_f64(3.0);
334                    Ok(event1_time + offset)
335                }
336            }
337            ZmanPrimitive::MinchaGedola(event1, event2, synchronous) => {
338                let event1_time = event1.calculate(calculator);
339                let event2_time = event2.calculate(calculator)?;
340                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
341                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
342                    let shaah_zmanis = event2_time.duration_since(chatzos) / 6;
343                    let offset = shaah_zmanis.mul_f64(0.5);
344                    Ok(chatzos + offset)
345                } else {
346                    let event1_time = event1_time?;
347                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
348                    let offset = shaah_zmanis.mul_f64(6.5);
349                    Ok(event1_time + offset)
350                }
351            }
352            ZmanPrimitive::SamuchLeMinchaKetana(event1, event2, synchronous) => {
353                let event1_time = event1.calculate(calculator);
354                let event2_time = event2.calculate(calculator)?;
355                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
356                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
357                    let shaah_zmanis = event2_time.duration_since(chatzos) / 6;
358                    let offset = shaah_zmanis.mul_f64(3.0);
359                    Ok(chatzos + offset)
360                } else {
361                    let event1_time = event1_time?;
362                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
363                    let offset = shaah_zmanis.mul_f64(9.0);
364                    Ok(event1_time + offset)
365                }
366            }
367            ZmanPrimitive::MinchaKetana(event1, event2, synchronous) => {
368                let event1_time = event1.calculate(calculator);
369                let event2_time = event2.calculate(calculator)?;
370                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
371                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
372                    let shaah_zmanis = event2_time.duration_since(chatzos) / 6;
373                    let offset = shaah_zmanis.mul_f64(3.5);
374                    Ok(chatzos + offset)
375                } else {
376                    let event1_time = event1_time?;
377                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
378                    let offset = shaah_zmanis.mul_f64(9.5);
379                    Ok(event1_time + offset)
380                }
381            }
382            ZmanPrimitive::Tefila(event1, event2, synchronous) => {
383                let event1_time = event1.calculate(calculator)?;
384                let event2_time = event2.calculate(calculator);
385                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
386                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
387                    let shaah_zmanis = chatzos.duration_since(event1_time) / 6;
388                    let offset = shaah_zmanis.mul_f64(4.0);
389                    Ok(event1_time + offset)
390                } else {
391                    let event2_time = event2_time?;
392                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
393                    let offset = shaah_zmanis.mul_f64(4.0);
394                    Ok(event1_time + offset)
395                }
396            }
397            ZmanPrimitive::PlagHamincha(event1, event2, synchronous) => {
398                let event1_time = event1.calculate(calculator);
399                let event2_time = event2.calculate(calculator)?;
400                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
401                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
402                    let shaah_zmanis = event2_time.duration_since(chatzos) / 6;
403                    let offset = shaah_zmanis.mul_f64(4.75);
404                    Ok(chatzos + offset)
405                } else {
406                    let event1_time = event1_time?;
407                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
408                    let offset = shaah_zmanis.mul_f64(10.75);
409                    Ok(event1_time + offset)
410                }
411            }
412            ZmanPrimitive::SofZmanBiurChametz(event1, event2, synchronous) => {
413                if !calculator.date.holidays(false, false).any(|h| h == Holiday::ErevPesach) {
414                    return Err(ZmanimError::InvalidForDate);
415                }
416                let primitive = if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
417                    ZmanPrimitive::HalfDayBasedOffset(event1, &ZmanPrimitive::ChatzosHayom, 5.0)
418                } else {
419                    let event1_time = event1.calculate(calculator)?;
420                    let event2_time = event2.calculate(calculator)?;
421                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
422                    let offset = shaah_zmanis.mul_f64(5.0);
423                    return Ok(event1_time + offset);
424                };
425                primitive.calculate(calculator)
426            }
427            ZmanPrimitive::SofZmanAchilasChametz(event1, event2, synchronous) => {
428                if !calculator.date.holidays(false, false).any(|h| h == Holiday::ErevPesach) {
429                    return Err(ZmanimError::InvalidForDate);
430                }
431                let event1_time = event1.calculate(calculator)?;
432                let event2_time = event2.calculate(calculator);
433                if calculator.config.use_astronomical_chatzos_for_other_zmanim && synchronous {
434                    let chatzos = calculator.calculate(&ZmanPrimitive::ChatzosHayom)?;
435                    let shaah_zmanis = chatzos.duration_since(event1_time) / 6;
436                    let offset = shaah_zmanis.mul_f64(4.0);
437                    Ok(event1_time + offset)
438                } else {
439                    let event2_time = event2_time?;
440                    let shaah_zmanis = event2_time.duration_since(event1_time) / 12;
441                    let offset = shaah_zmanis.mul_f64(4.0);
442                    Ok(event1_time + offset)
443                }
444            }
445            ZmanPrimitive::TzaisAteretTorah => {
446                let sunset = ZmanPrimitive::ConfiguredSunset.calculate(calculator)?;
447                Ok(sunset + calculator.config.ateret_torah_sunset_offset)
448            }
449            ZmanPrimitive::SofZmanKidushLevana15Days => {
450                let tz = calculator
451                    .location
452                    .timezone
453                    .as_ref()
454                    .ok_or(ZmanimError::TimeZoneRequired)?;
455                let hebrew = calculator.date.hebrew_date();
456
457                if hebrew.day_of_month().0 < 11 || hebrew.day_of_month().0 > 17 {
458                    return Err(ZmanimError::InvalidForDate);
459                }
460                let molad = months_molad(&hebrew)? + Duration::from_hours(24 * 15);
461                if is_same_gregorian_day(&hebrew, &molad, tz) {
462                    return Ok(molad);
463                }
464                Err(ZmanimError::InvalidForDate)
465            }
466            ZmanPrimitive::SofZmanKidushLevanaBetweenMoldos => {
467                let tz = calculator
468                    .location
469                    .timezone
470                    .as_ref()
471                    .ok_or(ZmanimError::TimeZoneRequired)?;
472                let hebrew = calculator.date.hebrew_date();
473
474                if hebrew.day_of_month().0 < 11 || hebrew.day_of_month().0 > 16 {
475                    return Err(ZmanimError::InvalidForDate);
476                }
477                let molad = months_molad(&hebrew)?
478                    + Duration::from_hours(24 * 14 + 18)
479                    + Duration::from_mins(22)
480                    + Duration::from_secs(1)
481                    + Duration::from_millis(666);
482                if is_same_gregorian_day(&hebrew, &molad, tz) {
483                    return Ok(molad);
484                }
485                Err(ZmanimError::InvalidForDate)
486            }
487            ZmanPrimitive::TchilasZmanKidushLevana3Days => {
488                let tz = calculator
489                    .location
490                    .timezone
491                    .as_ref()
492                    .ok_or(ZmanimError::TimeZoneRequired)?;
493                let hebrew = calculator.date.hebrew_date();
494
495                if hebrew.day_of_month().0 > 5 && hebrew.day_of_month().0 < 30 {
496                    return Err(ZmanimError::InvalidForDate);
497                }
498                let molad = months_molad(&hebrew)? + Duration::from_hours(72);
499
500                if is_same_gregorian_day(&hebrew, &molad, tz) {
501                    return Ok(molad);
502                }
503
504                if hebrew.day_of_month().0 == 30 {
505                    let mut add_option = DateAddOptions::default();
506                    add_option.overflow = Some(Overflow::Constrain);
507
508                    let new = hebrew
509                        .try_added_with_options(DateDuration::for_months(1), add_option)
510                        .map_err(|_| ZmanimError::TimeConversionError)?;
511                    let molad = months_molad(&new)? + Duration::from_hours(72);
512                    if is_same_gregorian_day(&hebrew, &molad, tz) {
513                        return Ok(molad);
514                    }
515                }
516                Err(ZmanimError::InvalidForDate)
517            }
518            ZmanPrimitive::TchilasZmanKidushLevana7Days => {
519                let tz = calculator
520                    .location
521                    .timezone
522                    .as_ref()
523                    .ok_or(ZmanimError::TimeZoneRequired)?;
524                let hebrew = calculator.date.hebrew_date();
525
526                if hebrew.day_of_month().0 < 4 || hebrew.day_of_month().0 > 9 {
527                    return Err(ZmanimError::InvalidForDate);
528                }
529                let molad = months_molad(&hebrew)? + Duration::from_hours(168);
530                if is_same_gregorian_day(&hebrew, &molad, tz) {
531                    return Ok(molad);
532                }
533                Err(ZmanimError::InvalidForDate)
534            }
535            ZmanPrimitive::BainHashmashosRt2Stars => {
536                let alos19_point_8 = ZmanPrimitive::SunriseOffsetByDegrees(19.8).calculate(calculator)?;
537                let sunrise = ZmanPrimitive::ConfiguredSunrise.calculate(calculator)?;
538                let sunset = ZmanPrimitive::ConfiguredSunset.calculate(calculator)?;
539                let time_diff = sunrise.duration_since(alos19_point_8);
540                let offset = time_diff.as_millis() as f64 * (5.0 / 18.0);
541                Ok(sunset + Duration::from_millis(offset as i64))
542            }
543            ZmanPrimitive::MinchaGedolaAhavatShalom => {
544                let chatzos = ZmanPrimitive::ChatzosHayom.calculate(calculator)?;
545                let mincha_gedola_30 = chatzos + Duration::from_mins(30);
546
547                let alos = ZmanPrimitive::SunriseOffsetByDegrees(16.1).calculate(calculator)?;
548                let tzais = ZmanPrimitive::SunsetOffsetByDegrees(3.7).calculate(calculator)?;
549                let shaah_zmanis = tzais.duration_since(alos) / 12;
550                let mincha_alternative = chatzos + (shaah_zmanis / 2);
551                if mincha_gedola_30 > mincha_alternative {
552                    Ok(mincha_gedola_30)
553                } else {
554                    Ok(mincha_alternative)
555                }
556            }
557            ZmanPrimitive::MinchaGedolaGraGreaterThan30 => {
558                let mincha_gedola_30 = ZmanPrimitive::Offset(&ZmanPrimitive::ChatzosHayom, Duration::from_mins(30))
559                    .calculate(calculator)?;
560                let mincha_gedola_gra = ZmanPrimitive::MinchaGedola(
561                    &ZmanPrimitive::ConfiguredSunrise,
562                    &ZmanPrimitive::ConfiguredSunset,
563                    true,
564                )
565                .calculate(calculator)?;
566                Ok(mincha_gedola_30.max(mincha_gedola_gra))
567            }
568            ZmanPrimitive::MinchaKetanaAhavatShalom => {
569                let tzais = ZmanPrimitive::SunsetOffsetByDegrees(3.8).calculate(calculator)?;
570                let alos = ZmanPrimitive::SunriseOffsetByDegrees(16.1).calculate(calculator)?;
571                let shaah_zmanis = tzais.duration_since(alos) / 12;
572                Ok(tzais - (shaah_zmanis * 5 / 2))
573            }
574            ZmanPrimitive::PlagAhavatShalom => {
575                let tzais = ZmanPrimitive::SunsetOffsetByDegrees(3.8).calculate(calculator)?;
576                let alos = ZmanPrimitive::SunriseOffsetByDegrees(16.1).calculate(calculator)?;
577                let shaah_zmanis = tzais.duration_since(alos) / 12;
578                Ok(tzais - (shaah_zmanis * 5 / 4))
579            }
580            ZmanPrimitive::BeginCivilTwilight => {
581                calculator.calculate(&ZmanPrimitive::SunriseOffsetByDegrees(CIVIL_ZENITH))
582            }
583            ZmanPrimitive::EndCivilTwilight => {
584                calculator.calculate(&ZmanPrimitive::SunsetOffsetByDegrees(CIVIL_ZENITH))
585            }
586            ZmanPrimitive::BeginNauticalTwilight => {
587                calculator.calculate(&ZmanPrimitive::SunriseOffsetByDegrees(NAUTICAL_ZENITH))
588            }
589            ZmanPrimitive::EndNauticalTwilight => {
590                calculator.calculate(&ZmanPrimitive::SunsetOffsetByDegrees(NAUTICAL_ZENITH))
591            }
592            ZmanPrimitive::BeginAstronomicalTwilight => {
593                calculator.calculate(&ZmanPrimitive::SunriseOffsetByDegrees(ASTRONOMICAL_ZENITH))
594            }
595            ZmanPrimitive::EndAstronomicalTwilight => {
596                calculator.calculate(&ZmanPrimitive::SunsetOffsetByDegrees(ASTRONOMICAL_ZENITH))
597            }
598            ZmanPrimitive::SunsetOrWesternmostSolarAzimuth => {
599                match ZmanPrimitive::ConfiguredSunset.calculate(calculator) {
600                    Ok(sunset) => Ok(sunset),
601                    Err(ZmanimError::AllDay | ZmanimError::AllNight) => {
602                        crate::zmanim::astronomy::time_at_azimuth(calculator.date, &calculator.location, 270.0)
603                    }
604                    Err(error) => Err(error),
605                }
606            }
607            ZmanPrimitive::SunriseOrEasternmostSolarAzimuth => {
608                match ZmanPrimitive::ConfiguredSunrise.calculate(calculator) {
609                    Ok(sunrise) => Ok(sunrise),
610                    Err(ZmanimError::AllDay | ZmanimError::AllNight) => {
611                        crate::zmanim::astronomy::time_at_azimuth(calculator.date, &calculator.location, 90.0)
612                    }
613                    Err(error) => Err(error),
614                }
615            }
616            ZmanPrimitive::ChatzosHayomAsHalfDay => {
617                let sunrise = ZmanPrimitive::SeaLevelSunrise.calculate(calculator)?;
618                let sunset = ZmanPrimitive::SeaLevelSunset.calculate(calculator)?;
619                let diff = sunset.duration_since(sunrise) / 2;
620                Ok(sunrise + diff)
621            }
622            ZmanPrimitive::ChatzosHayom => {
623                if calculator.config.use_astronomical_chatzos {
624                    ZmanPrimitive::SolarTransit.calculate(calculator)
625                } else {
626                    match ZmanPrimitive::ChatzosHayomAsHalfDay.calculate(calculator) {
627                        Ok(chatzos) => Ok(chatzos),
628                        Err(_) => ZmanPrimitive::SolarTransit.calculate(calculator),
629                    }
630                }
631            }
632            ZmanPrimitive::ChatzosHalaylaAsHalfDay => {
633                let sunset = ZmanPrimitive::SeaLevelSunset.calculate(calculator)?;
634                // Create a copy of the calculator and increment the date by one day
635                let mut tomorrow = calculator.clone();
636                tomorrow.date = tomorrow
637                    .date
638                    .checked_add(1.day())
639                    .map_err(|_| ZmanimError::TimeConversionError)?;
640                let sunrise = ZmanPrimitive::SeaLevelSunrise.calculate(&tomorrow)?;
641                let diff = sunrise.duration_since(sunset) / 2;
642                Ok(sunset + diff)
643            }
644            ZmanPrimitive::ChatzosHalayla => {
645                if calculator.config.use_astronomical_chatzos {
646                    ZmanPrimitive::SolarMidnight.calculate(calculator)
647                } else {
648                    match ZmanPrimitive::ChatzosHalaylaAsHalfDay.calculate(calculator) {
649                        Ok(chatzos) => Ok(chatzos),
650                        Err(_) => ZmanPrimitive::SolarMidnight.calculate(calculator),
651                    }
652                }
653            }
654        }
655    }
656}
657
658#[cfg(feature = "alloc")]
659/// These methods are used to create more informed user docs for presets.
660impl ZmanPrimitive {
661    pub(crate) fn uses_astronomical_chatzos_for_other_zmanim_from_config(&self) -> bool {
662        match self {
663            ZmanPrimitive::Shema(_, _, synchronous)
664            | ZmanPrimitive::MinchaGedola(_, _, synchronous)
665            | ZmanPrimitive::SamuchLeMinchaKetana(_, _, synchronous)
666            | ZmanPrimitive::MinchaKetana(_, _, synchronous)
667            | ZmanPrimitive::Tefila(_, _, synchronous)
668            | ZmanPrimitive::PlagHamincha(_, _, synchronous)
669            | ZmanPrimitive::SofZmanBiurChametz(_, _, synchronous)
670            | ZmanPrimitive::SofZmanAchilasChametz(_, _, synchronous) => *synchronous,
671            ZmanPrimitive::MinchaGedolaGraGreaterThan30 => true,
672            ZmanPrimitive::ElevationAdjustedSunrise
673            | ZmanPrimitive::SeaLevelSunrise
674            | ZmanPrimitive::ConfiguredSunrise
675            | ZmanPrimitive::ConfiguredSunset
676            | ZmanPrimitive::SolarTransit
677            | ZmanPrimitive::SolarMidnight
678            | ZmanPrimitive::ChatzosHayomAsHalfDay
679            | ZmanPrimitive::ChatzosHayom
680            | ZmanPrimitive::ChatzosHalaylaAsHalfDay
681            | ZmanPrimitive::ChatzosHalayla
682            | ZmanPrimitive::ElevationAdjustedSunset
683            | ZmanPrimitive::SeaLevelSunset
684            | ZmanPrimitive::SunriseOffsetByDegrees(_)
685            | ZmanPrimitive::SunsetOffsetByDegrees(_)
686            | ZmanPrimitive::LocalMeanTime(_)
687            | ZmanPrimitive::CandleLighting
688            | ZmanPrimitive::Offset(_, _)
689            | ZmanPrimitive::ZmanisOffset(_, _)
690            | ZmanPrimitive::HalfDayBasedOffset(_, _, _)
691            | ZmanPrimitive::TzaisAteretTorah
692            | ZmanPrimitive::BainHashmashosRt2Stars
693            | ZmanPrimitive::SofZmanKidushLevana15Days
694            | ZmanPrimitive::SofZmanKidushLevanaBetweenMoldos
695            | ZmanPrimitive::TchilasZmanKidushLevana3Days
696            | ZmanPrimitive::TchilasZmanKidushLevana7Days
697            | ZmanPrimitive::MinchaGedolaAhavatShalom
698            | ZmanPrimitive::MinchaKetanaAhavatShalom
699            | ZmanPrimitive::PlagAhavatShalom
700            | ZmanPrimitive::BeginCivilTwilight
701            | ZmanPrimitive::EndCivilTwilight
702            | ZmanPrimitive::BeginNauticalTwilight
703            | ZmanPrimitive::EndNauticalTwilight
704            | ZmanPrimitive::BeginAstronomicalTwilight
705            | ZmanPrimitive::EndAstronomicalTwilight
706            | ZmanPrimitive::SunsetOrWesternmostSolarAzimuth
707            | ZmanPrimitive::SunriseOrEasternmostSolarAzimuth => false,
708        }
709    }
710
711    pub(crate) fn uses_astronomical_chatzos_from_config(&self) -> bool {
712        match self {
713            ZmanPrimitive::ChatzosHayom | ZmanPrimitive::ChatzosHalayla => true,
714            ZmanPrimitive::Offset(event, _) => event.uses_astronomical_chatzos_from_config(),
715            ZmanPrimitive::ElevationAdjustedSunrise
716            | ZmanPrimitive::SeaLevelSunrise
717            | ZmanPrimitive::ConfiguredSunrise
718            | ZmanPrimitive::ConfiguredSunset
719            | ZmanPrimitive::SolarTransit
720            | ZmanPrimitive::SolarMidnight
721            | ZmanPrimitive::ChatzosHayomAsHalfDay
722            | ZmanPrimitive::ChatzosHalaylaAsHalfDay
723            | ZmanPrimitive::ElevationAdjustedSunset
724            | ZmanPrimitive::SeaLevelSunset
725            | ZmanPrimitive::SunriseOffsetByDegrees(_)
726            | ZmanPrimitive::SunsetOffsetByDegrees(_)
727            | ZmanPrimitive::LocalMeanTime(_)
728            | ZmanPrimitive::CandleLighting
729            | ZmanPrimitive::ZmanisOffset(_, _)
730            | ZmanPrimitive::HalfDayBasedOffset(_, _, _)
731            | ZmanPrimitive::Shema(_, _, _)
732            | ZmanPrimitive::MinchaGedola(_, _, _)
733            | ZmanPrimitive::SamuchLeMinchaKetana(_, _, _)
734            | ZmanPrimitive::MinchaKetana(_, _, _)
735            | ZmanPrimitive::Tefila(_, _, _)
736            | ZmanPrimitive::PlagHamincha(_, _, _)
737            | ZmanPrimitive::SofZmanBiurChametz(_, _, _)
738            | ZmanPrimitive::SofZmanAchilasChametz(_, _, _)
739            | ZmanPrimitive::TzaisAteretTorah
740            | ZmanPrimitive::BainHashmashosRt2Stars
741            | ZmanPrimitive::MinchaGedolaGraGreaterThan30
742            | ZmanPrimitive::SofZmanKidushLevana15Days
743            | ZmanPrimitive::SofZmanKidushLevanaBetweenMoldos
744            | ZmanPrimitive::TchilasZmanKidushLevana3Days
745            | ZmanPrimitive::TchilasZmanKidushLevana7Days
746            | ZmanPrimitive::MinchaGedolaAhavatShalom
747            | ZmanPrimitive::MinchaKetanaAhavatShalom
748            | ZmanPrimitive::PlagAhavatShalom
749            | ZmanPrimitive::BeginCivilTwilight
750            | ZmanPrimitive::EndCivilTwilight
751            | ZmanPrimitive::BeginNauticalTwilight
752            | ZmanPrimitive::EndNauticalTwilight
753            | ZmanPrimitive::BeginAstronomicalTwilight
754            | ZmanPrimitive::EndAstronomicalTwilight
755            | ZmanPrimitive::SunsetOrWesternmostSolarAzimuth
756            | ZmanPrimitive::SunriseOrEasternmostSolarAzimuth => false,
757        }
758    }
759
760    pub(crate) fn uses_elevation_from_config(&self) -> bool {
761        match self {
762            ZmanPrimitive::ConfiguredSunrise | ZmanPrimitive::ConfiguredSunset => true,
763            ZmanPrimitive::Offset(event, _) => event.uses_elevation_from_config(),
764            ZmanPrimitive::ZmanisOffset(_, _) => true,
765            ZmanPrimitive::HalfDayBasedOffset(start, end, _)
766            | ZmanPrimitive::Shema(start, end, _)
767            | ZmanPrimitive::MinchaGedola(start, end, _)
768            | ZmanPrimitive::SamuchLeMinchaKetana(start, end, _)
769            | ZmanPrimitive::MinchaKetana(start, end, _)
770            | ZmanPrimitive::Tefila(start, end, _)
771            | ZmanPrimitive::PlagHamincha(start, end, _)
772            | ZmanPrimitive::SofZmanBiurChametz(start, end, _)
773            | ZmanPrimitive::SofZmanAchilasChametz(start, end, _) => {
774                start.uses_elevation_from_config() || end.uses_elevation_from_config()
775            }
776            ZmanPrimitive::TzaisAteretTorah
777            | ZmanPrimitive::BainHashmashosRt2Stars
778            | ZmanPrimitive::MinchaGedolaGraGreaterThan30
779            | ZmanPrimitive::SunsetOrWesternmostSolarAzimuth
780            | ZmanPrimitive::SunriseOrEasternmostSolarAzimuth => true,
781            ZmanPrimitive::ElevationAdjustedSunrise
782            | ZmanPrimitive::SeaLevelSunrise
783            | ZmanPrimitive::SolarTransit
784            | ZmanPrimitive::SolarMidnight
785            | ZmanPrimitive::ChatzosHayomAsHalfDay
786            | ZmanPrimitive::ChatzosHayom
787            | ZmanPrimitive::ChatzosHalaylaAsHalfDay
788            | ZmanPrimitive::ChatzosHalayla
789            | ZmanPrimitive::ElevationAdjustedSunset
790            | ZmanPrimitive::SeaLevelSunset
791            | ZmanPrimitive::SunriseOffsetByDegrees(_)
792            | ZmanPrimitive::SunsetOffsetByDegrees(_)
793            | ZmanPrimitive::LocalMeanTime(_)
794            | ZmanPrimitive::CandleLighting
795            | ZmanPrimitive::SofZmanKidushLevana15Days
796            | ZmanPrimitive::SofZmanKidushLevanaBetweenMoldos
797            | ZmanPrimitive::TchilasZmanKidushLevana3Days
798            | ZmanPrimitive::TchilasZmanKidushLevana7Days
799            | ZmanPrimitive::MinchaGedolaAhavatShalom
800            | ZmanPrimitive::MinchaKetanaAhavatShalom
801            | ZmanPrimitive::PlagAhavatShalom
802            | ZmanPrimitive::BeginCivilTwilight
803            | ZmanPrimitive::EndCivilTwilight
804            | ZmanPrimitive::BeginNauticalTwilight
805            | ZmanPrimitive::EndNauticalTwilight
806            | ZmanPrimitive::BeginAstronomicalTwilight
807            | ZmanPrimitive::EndAstronomicalTwilight => false,
808        }
809    }
810}