From 4cf3bd7898347726f46f91b65f09c56a1f3c84c7 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Mon, 10 Nov 2025 22:07:24 -0500 Subject: [PATCH 01/13] improve naive timezone conversion --- .../zmanim/util/NOAACalculator.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java index ec9a0453..cc477fbe 100644 --- a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java @@ -16,6 +16,7 @@ package com.kosherjava.zmanim.util; import java.util.Calendar; +import java.util.TimeZone; /** * Implementation of sunrise and sunset methods to calculate astronomical times based on the Universal Coordinated Time (UTC) From 657b60c2528e4a48f4256baf9a4b81581b3f70c6 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 11 Nov 2025 17:31:39 -0500 Subject: [PATCH 02/13] use java.time for improved accuracy for distant dates --- .../zmanim/AstronomicalCalendar.java | 27 +++++++---- .../zmanim/hebrewcalendar/JewishCalendar.java | 4 +- .../kosherjava/zmanim/util/GeoLocation.java | 38 ++++++++++----- .../zmanim/util/NOAACalculator.java | 11 +++-- .../kosherjava/zmanim/util/TimeZoneUtils.java | 48 +++++++++++++++++++ 5 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java diff --git a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java index 04f829d6..08298977 100644 --- a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java @@ -16,12 +16,15 @@ package com.kosherjava.zmanim; import java.math.BigDecimal; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import com.kosherjava.zmanim.util.AstronomicalCalculator; import com.kosherjava.zmanim.util.GeoLocation; +import com.kosherjava.zmanim.util.TimeZoneUtils; import com.kosherjava.zmanim.util.ZmanimFormatter; /** @@ -626,21 +629,26 @@ protected Date getDateFromTime(double time, SolarEvent solarEvent) { return null; } double calculatedTime = time; - + Calendar adjustedCalendar = getAdjustedCalendar(); + + // Convert Calendar to java.time for accurate date extraction, especially for distant future dates + ZoneId zoneId = adjustedCalendar.getTimeZone().toZoneId(); + ZonedDateTime adjustedZdt = adjustedCalendar.toInstant().atZone(zoneId); + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); cal.clear();// clear all fields - cal.set(Calendar.YEAR, adjustedCalendar.get(Calendar.YEAR)); - cal.set(Calendar.MONTH, adjustedCalendar.get(Calendar.MONTH)); - cal.set(Calendar.DAY_OF_MONTH, adjustedCalendar.get(Calendar.DAY_OF_MONTH)); + cal.set(Calendar.YEAR, adjustedZdt.getYear()); + cal.set(Calendar.MONTH, adjustedZdt.getMonthValue() - 1); // Calendar months are 0-based + cal.set(Calendar.DAY_OF_MONTH, adjustedZdt.getDayOfMonth()); int hours = (int) calculatedTime; // retain only the hours calculatedTime -= hours; int minutes = (int) (calculatedTime *= 60); // retain only the minutes - calculatedTime -= minutes; + calculatedTime -= minutes; int seconds = (int) (calculatedTime *= 60); // retain only the seconds calculatedTime -= seconds; // remaining milliseconds - + // Check if a date transition has occurred, or is about to occur - this indicates the date of the event is // actually not the target date, but the day prior or after int localTimeHours = (int)getGeoLocation().getLongitude() / 15; @@ -758,8 +766,9 @@ public Date getLocalMeanTime(double hours) { if (hours < 0 || hours >= 24) { throw new IllegalArgumentException("Hours must between 0 and 23.9999..."); } - return getTimeOffset(getDateFromTime(hours - getGeoLocation().getTimeZone().getRawOffset() - / (double) HOUR_MILLIS, SolarEvent.SUNRISE), -getGeoLocation().getLocalMeanTimeOffset()); + long timezoneOffsetMillis = TimeZoneUtils.getTimezoneOffsetAt(getCalendar()); + return getTimeOffset(getDateFromTime(hours - timezoneOffsetMillis + / (double) HOUR_MILLIS, SolarEvent.SUNRISE), -getGeoLocation().getLocalMeanTimeOffset(calendar)); } /** @@ -769,7 +778,7 @@ public Date getLocalMeanTime(double hours) { * @return the adjusted Calendar */ private Calendar getAdjustedCalendar(){ - int offset = getGeoLocation().getAntimeridianAdjustment(); + int offset = getGeoLocation().getAntimeridianAdjustment(getCalendar()); if (offset == 0) { return getCalendar(); } diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java index acc987f2..5cb1c678 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java @@ -1249,7 +1249,7 @@ public Date getMoladAsDate() { molad.getMoladHours(), molad.getMoladMinutes(), (int) moladSeconds); cal.set(Calendar.MILLISECOND, (int) (1000 * (moladSeconds - (int) moladSeconds))); // subtract local time difference of 20.94 minutes (20 minutes and 56.496 seconds) to get to Standard time - cal.add(Calendar.MILLISECOND, -1 * (int) geo.getLocalMeanTimeOffset()); + cal.add(Calendar.MILLISECOND, -1 * (int) geo.getLocalMeanTimeOffset(cal)); return cal.getTime(); } @@ -1290,7 +1290,7 @@ public Date getTchilasZmanKidushLevana7Days() { cal.add(Calendar.HOUR, 168); // 7 days after the molad return cal.getTime(); } - + /** * Returns the latest time of Kiddush Levana according to the Maharil's opinion that it is calculated as diff --git a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java index b2888a33..b3c53b4c 100644 --- a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java +++ b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java @@ -15,6 +15,7 @@ */ package com.kosherjava.zmanim.util; +import java.util.Calendar; import java.util.Objects; import java.util.TimeZone; @@ -329,15 +330,17 @@ public void setTimeZone(TimeZone timeZone) { * so a user who is 1° west of this will have noon at 4 minutes after standard time noon, and conversely, a user * who is 1° east of the 15° longitude will have noon at 11:56 AM. Lakewood, N.J., whose longitude is * -74.222, is 0.778 away from the closest multiple of 15 at -75°. This is multiplied by 4 to yield 3 minutes - * and 10 seconds earlier than standard time. The offset returned does not account for the Daylight saving time offset since this class is - * unaware of dates. + * and 10 seconds earlier than standard time. The offset returned uses the actual timezone offset at the specific + * date/time from the Calendar, accounting for Daylight + * saving time. * - * @return the offset in milliseconds not accounting for Daylight saving time. A positive value will be returned - * East of the 15° timezone line, and a negative value West of it. + * @param calendar the Calendar containing the date/time to calculate the offset for + * @return the offset in milliseconds. A positive value will be returned East of the 15° timezone line, and a + * negative value West of it. */ - public long getLocalMeanTimeOffset() { - return (long) (getLongitude() * 4 * MINUTE_MILLIS - getTimeZone().getRawOffset()); + public long getLocalMeanTimeOffset(Calendar calendar) { + long timezoneOffsetMillis = TimeZoneUtils.getTimezoneOffsetAt(calendar); + return (long) (getLongitude() * 4 * MINUTE_MILLIS - timezoneOffsetMillis); } /** @@ -355,10 +358,11 @@ public long getLocalMeanTimeOffset() { * UTC time, the local DST offset of UTC+14:00 should be applied * to bring the date back to 2018-02-03. * + * @param calendar the Calendar containing the date/time to calculate the adjustment for * @return the number of days to adjust the date This will typically be 0 unless the date crosses the antimeridian */ - public int getAntimeridianAdjustment() { - double localHoursOffset = getLocalMeanTimeOffset() / (double)HOUR_MILLIS; + public int getAntimeridianAdjustment(Calendar calendar) { + double localHoursOffset = getLocalMeanTimeOffset(calendar) / (double)HOUR_MILLIS; if (localHoursOffset >= 20){// if the offset is 20 hours or more in the future (never expected anywhere other // than a location using a timezone across the antimeridian to the east such as Samoa) @@ -565,6 +569,10 @@ public double getRhumbLineDistance(GeoLocation location) { * @return The XML formatted String. */ public String toXML() { + Calendar cal = Calendar.getInstance(getTimeZone()); + long gmtOffsetMillis = TimeZoneUtils.getTimezoneOffsetAt(cal); + long dstOffsetMillis = getTimeZone().getDSTSavings(); + return "\n" + "\t" + getLocationName() + "\n" + "\t" + getLatitude() + "\n" + @@ -572,9 +580,9 @@ public String toXML() { "\t" + getElevation() + " Meters" + "\n" + "\t" + getTimeZone().getID() + "\n" + "\t" + getTimeZone().getDisplayName() + "\n" + - "\t" + getTimeZone().getRawOffset() / HOUR_MILLIS + + "\t" + gmtOffsetMillis / HOUR_MILLIS + "\n" + - "\t" + getTimeZone().getDSTSavings() / HOUR_MILLIS + + "\t" + dstOffsetMillis / HOUR_MILLIS + "\n" + ""; } @@ -620,6 +628,10 @@ public int hashCode() { * @see java.lang.Object#toString() */ public String toString() { + Calendar cal = Calendar.getInstance(getTimeZone()); + long gmtOffsetMillis = TimeZoneUtils.getTimezoneOffsetAt(cal); + long dstOffsetMillis = getTimeZone().getDSTSavings(); + return "\nLocation Name:\t\t\t" + getLocationName() + "\nLatitude:\t\t\t" + getLatitude() + "\u00B0" + "\nLongitude:\t\t\t" + getLongitude() + "\u00B0" + @@ -627,8 +639,8 @@ public String toString() { "\nTimezone ID:\t\t\t" + getTimeZone().getID() + "\nTimezone Display Name:\t\t" + getTimeZone().getDisplayName() + " (" + getTimeZone().getDisplayName(false, TimeZone.SHORT) + ")" + - "\nTimezone GMT Offset:\t\t" + getTimeZone().getRawOffset() / HOUR_MILLIS + - "\nTimezone DST Offset:\t\t" + getTimeZone().getDSTSavings() / HOUR_MILLIS; + "\nTimezone GMT Offset:\t\t" + gmtOffsetMillis / HOUR_MILLIS + + "\nTimezone DST Offset:\t\t" + dstOffsetMillis / HOUR_MILLIS; } /** diff --git a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java index cc477fbe..f60d29cd 100644 --- a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java @@ -15,6 +15,8 @@ */ package com.kosherjava.zmanim.util; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.TimeZone; @@ -101,9 +103,12 @@ public double getUTCSunset(Calendar calendar, GeoLocation geoLocation, double ze * day. Fractional days / time should be added later. */ private static double getJulianDay(Calendar calendar) { - int year = calendar.get(Calendar.YEAR); - int month = calendar.get(Calendar.MONTH) + 1; - int day = calendar.get(Calendar.DAY_OF_MONTH); + // Convert Calendar to java.time for accurate date extraction, especially for distant future dates + ZoneId zoneId = calendar.getTimeZone().toZoneId(); + ZonedDateTime zdt = calendar.toInstant().atZone(zoneId); + int year = zdt.getYear(); + int month = zdt.getMonthValue(); // Already 1-12, no need to add 1 + int day = zdt.getDayOfMonth(); if (month <= 2) { year -= 1; month += 12; diff --git a/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java new file mode 100644 index 00000000..31b6a194 --- /dev/null +++ b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java @@ -0,0 +1,48 @@ +/* + * Zmanim Java API + * Copyright (C) 2004-2025 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Utility class for timezone-related calculations using java.time APIs. + * + * @author © Eliyahu Hershfeld 2004 - 2025 + */ +public class TimeZoneUtils { + + /** + * Gets the timezone offset in milliseconds at a specific instant using java.time APIs. + * This method correctly handles Daylight Saving Time (DST) changes by calculating the offset + * at the specific date/time represented by the Calendar. + * + * @param calendar the Calendar containing the date/time and timezone to calculate the offset for + * @return the timezone offset in milliseconds + */ + public static long getTimezoneOffsetAt(Calendar calendar) { + TimeZone timeZone = calendar.getTimeZone(); + long unixTimestampMillis = calendar.getTimeInMillis(); + ZoneId zoneId = timeZone.toZoneId(); + Instant instant = Instant.ofEpochMilli(unixTimestampMillis); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId); + return zonedDateTime.getOffset().getTotalSeconds() * 1000; + } +} + From 05a983c6eba2f830178ffd886c1d5abb75024b97 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 11 Nov 2025 23:14:23 -0500 Subject: [PATCH 03/13] additional java.time conversions --- .../zmanim/AstronomicalCalendar.java | 12 +++++++---- .../kosherjava/zmanim/util/TimeZoneUtils.java | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java index 08298977..26abcba7 100644 --- a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java @@ -18,6 +18,7 @@ import java.math.BigDecimal; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; @@ -353,6 +354,7 @@ public Date getSunriseOffsetByDegrees(double offsetZenith) { */ public Date getSunsetOffsetByDegrees(double offsetZenith) { double sunset = getUTCSunset(offsetZenith); + // System.out.println("Jsunset: " + sunset); if (Double.isNaN(sunset)) { return null; } else { @@ -633,8 +635,11 @@ protected Date getDateFromTime(double time, SolarEvent solarEvent) { Calendar adjustedCalendar = getAdjustedCalendar(); // Convert Calendar to java.time for accurate date extraction, especially for distant future dates - ZoneId zoneId = adjustedCalendar.getTimeZone().toZoneId(); - ZonedDateTime adjustedZdt = adjustedCalendar.toInstant().atZone(zoneId); + long milliseconds = adjustedCalendar.getTimeInMillis(); + Instant instant = Instant.ofEpochMilli(milliseconds); + TimeZone timeZone = adjustedCalendar.getTimeZone(); + ZoneId zoneId = timeZone.toZoneId(); + ZonedDateTime adjustedZdt = instant.atZone(zoneId); Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); cal.clear();// clear all fields @@ -782,8 +787,7 @@ private Calendar getAdjustedCalendar(){ if (offset == 0) { return getCalendar(); } - Calendar adjustedCalendar = (Calendar) getCalendar().clone(); - adjustedCalendar.add(Calendar.DAY_OF_MONTH, offset); + Calendar adjustedCalendar = TimeZoneUtils.addDay(getCalendar()); return adjustedCalendar; } diff --git a/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java index 31b6a194..f206bae6 100644 --- a/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java +++ b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java @@ -44,5 +44,26 @@ public static long getTimezoneOffsetAt(Calendar calendar) { ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId); return zonedDateTime.getOffset().getTotalSeconds() * 1000; } + + /** + * Adds one day to the given Calendar by converting it to a ZonedDateTime, + * adding a day, and converting it back to a Calendar. + * The returned Calendar will have the same TimeZone as the input Calendar. + * + * @param calendar the Calendar to add a day to + * @return a new Calendar with one day added, preserving the original TimeZone + */ + public static Calendar addDay(Calendar calendar) { + TimeZone timeZone = calendar.getTimeZone(); + long unixTimestampMillis = calendar.getTimeInMillis(); + ZoneId zoneId = timeZone.toZoneId(); + Instant instant = Instant.ofEpochMilli(unixTimestampMillis); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId); + ZonedDateTime nextDay = zonedDateTime.plusDays(1); + Instant nextDayInstant = nextDay.toInstant(); + Calendar result = Calendar.getInstance(timeZone); + result.setTimeInMillis(nextDayInstant.toEpochMilli()); + return result; + } } From 81e56cd62735593aa0148094e87efacfd8293e16 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Thu, 4 Dec 2025 18:42:26 -0500 Subject: [PATCH 04/13] validate length of hebrew months --- .../zmanim/hebrewcalendar/JewishDate.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java index eb47d309..dccf0bed 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java @@ -567,8 +567,8 @@ private static int getJewishMonthOfYear(int year, int month) { * @param month * the Jewish month to validate. It will reject a month < 1 or > 12 (or 13 on a leap year) . * @param dayOfMonth - * the day of the Jewish month to validate. It will reject any value < 1 or > 30 TODO: check calling - * methods to see if there is any reason that the class can validate that 30 is invalid for some months. + * the day of the Jewish month to validate. It will reject any value < 1 or > the number of days in the month + * for that year. * @param hours * the hours (for molad calculations). It will reject an hour < 0 or > 23 * @param minutes @@ -590,9 +590,12 @@ private static void validateJewishDate(int year, int month, int dayOfMonth, int throw new IllegalArgumentException("The Jewish month has to be between 1 and 12 (or 13 on a leap year). " + month + " is invalid for the year " + year + "."); } - if (dayOfMonth < 1 || dayOfMonth > 30) { - throw new IllegalArgumentException("The Jewish day of month can't be < 1 or > 30. " + dayOfMonth - + " is invalid."); + + int maxDaysInMonth = getDaysInJewishMonth(month, year); + + if (dayOfMonth < 1 || dayOfMonth > maxDaysInMonth) { + throw new IllegalArgumentException("The Jewish day of month can't be < 1 or > " + maxDaysInMonth + ". " + dayOfMonth + + " is invalid for the month " + month + " of the year " + year + "."); } // reject dates prior to 18 Teves, 3761 (1/1/1 AD). This restriction can be relaxed if the date coding is // changed/corrected From f15204e1206460af9eaea058fef85a22ef174fe5 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Thu, 4 Dec 2025 20:07:49 -0500 Subject: [PATCH 05/13] apply offset correctly --- src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java | 2 +- src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java index 26abcba7..358619ae 100644 --- a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java @@ -787,7 +787,7 @@ private Calendar getAdjustedCalendar(){ if (offset == 0) { return getCalendar(); } - Calendar adjustedCalendar = TimeZoneUtils.addDay(getCalendar()); + Calendar adjustedCalendar = TimeZoneUtils.addDay(getCalendar(), offset); return adjustedCalendar; } diff --git a/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java index f206bae6..82fa787a 100644 --- a/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java +++ b/src/main/java/com/kosherjava/zmanim/util/TimeZoneUtils.java @@ -53,13 +53,13 @@ public static long getTimezoneOffsetAt(Calendar calendar) { * @param calendar the Calendar to add a day to * @return a new Calendar with one day added, preserving the original TimeZone */ - public static Calendar addDay(Calendar calendar) { + public static Calendar addDay(Calendar calendar, int days) { TimeZone timeZone = calendar.getTimeZone(); long unixTimestampMillis = calendar.getTimeInMillis(); ZoneId zoneId = timeZone.toZoneId(); Instant instant = Instant.ofEpochMilli(unixTimestampMillis); ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, zoneId); - ZonedDateTime nextDay = zonedDateTime.plusDays(1); + ZonedDateTime nextDay = zonedDateTime.plusDays(days); Instant nextDayInstant = nextDay.toInstant(); Calendar result = Calendar.getInstance(timeZone); result.setTimeInMillis(nextDayInstant.toEpochMilli()); From 931f23929b9f7839501cea545f131bb1df8b01b2 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 7 Dec 2025 00:32:55 -0500 Subject: [PATCH 06/13] fix null error in getMoladBasedTime --- .../zmanim/ComplexZmanimCalendar.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/ComplexZmanimCalendar.java b/src/main/java/com/kosherjava/zmanim/ComplexZmanimCalendar.java index 4a0e2961..b7f17377 100644 --- a/src/main/java/com/kosherjava/zmanim/ComplexZmanimCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/ComplexZmanimCalendar.java @@ -3486,18 +3486,22 @@ public Date getSofZmanKidushLevanaBetweenMoldos(Date alos, Date tzais) { */ private Date getMoladBasedTime(Date moladBasedTime, Date alos, Date tzais, boolean techila) { Date lastMidnight = getMidnightLastNight(); - Date midnightTonight = getMidnightTonight(); - if (!(moladBasedTime.before(lastMidnight) || moladBasedTime.after(midnightTonight))){ - if (alos != null || tzais != null) { - if (techila && !(moladBasedTime.before(tzais) || moladBasedTime.after(alos))){ + Date midnightTonight = getMidnightTonight(); + if(moladBasedTime.before(lastMidnight) || moladBasedTime.after(midnightTonight)){ // Invalid time, bailout + return null; + } else if (alos == null || tzais == null){ // Not enough info to adjust + return moladBasedTime; + } else { // It's the daytime, get the next/prev night + if (moladBasedTime.after(alos) && moladBasedTime.before(tzais)) { + if (techila) { return tzais; } else { return alos; } - } - return moladBasedTime; - } - return null; + } else { // It's the night, the provided time is valid + return moladBasedTime; + } + } } /** From 9b74e47b61176b4adf8733c7bc7c6be0c9f68859 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 7 Dec 2025 09:21:59 -0500 Subject: [PATCH 07/13] use UTC in yerushalmi calc --- .../kosherjava/zmanim/hebrewcalendar/JewishDate.java | 1 + .../hebrewcalendar/YerushalmiYomiCalculator.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java index dccf0bed..db7361e2 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java @@ -1183,6 +1183,7 @@ public void setJewishDate(int year, int month, int dayOfMonth, int hours, int mi */ public Calendar getGregorianCalendar() { Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.set(getGregorianYear(), getGregorianMonth(), getGregorianDayOfMonth()); return calendar; } diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java index a5603e9b..41941c88 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java @@ -17,6 +17,7 @@ import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.TimeZone; /** @@ -31,7 +32,12 @@ public class YerushalmiYomiCalculator { /** * The start date of the first Daf Yomi Yerushalmi cycle of February 2, 1980 / 15 Shevat, 5740. */ - private final static Calendar DAF_YOMI_START_DAY = new GregorianCalendar(1980, Calendar.FEBRUARY, 2); + private final static Calendar DAF_YOMI_START_DAY; + static { + DAF_YOMI_START_DAY = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + DAF_YOMI_START_DAY.set(1980, Calendar.FEBRUARY, 2, 0, 0, 0); + DAF_YOMI_START_DAY.set(Calendar.MILLISECOND, 0); + } /** The number of milliseconds in a day. */ private final static int DAY_MILIS = 1000 * 60 * 60 * 24; /** The number of pages in the Talmud Yerushalmi.*/ @@ -64,8 +70,8 @@ public YerushalmiYomiCalculator() { */ public static Daf getDafYomiYerushalmi(JewishCalendar calendar) { - Calendar nextCycle = new GregorianCalendar(); - Calendar prevCycle = new GregorianCalendar(); + Calendar nextCycle = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + Calendar prevCycle = new GregorianCalendar(TimeZone.getTimeZone("UTC")); Calendar requested = calendar.getGregorianCalendar(); int masechta = 0; Daf dafYomi = null; From e4dc75550eb9bc524910c69e36d40e1bb8b2f8e9 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 7 Dec 2025 09:39:10 -0500 Subject: [PATCH 08/13] fix missing import --- .../java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java index db7361e2..7cd8a92e 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.TimeZone; /** * The JewishDate is the base calendar class, that supports maintenance of a {@link java.util.GregorianCalendar} From 0179cab599da53179bc8cbd13f0cbe36ead45f62 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 7 Dec 2025 14:56:55 -0500 Subject: [PATCH 09/13] some more yerushalmi changes --- .../kosherjava/zmanim/AstronomicalCalendar.java | 8 ++++---- .../zmanim/hebrewcalendar/JewishDate.java | 4 ++++ .../hebrewcalendar/YerushalmiYomiCalculator.java | 14 +++++++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java index 358619ae..f891f35f 100644 --- a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java @@ -658,13 +658,13 @@ protected Date getDateFromTime(double time, SolarEvent solarEvent) { // actually not the target date, but the day prior or after int localTimeHours = (int)getGeoLocation().getLongitude() / 15; if (solarEvent == SolarEvent.SUNRISE && localTimeHours + hours > 18) { - cal.add(Calendar.DAY_OF_MONTH, -1); + cal = TimeZoneUtils.addDay(cal, -1); } else if (solarEvent == SolarEvent.SUNSET && localTimeHours + hours < 6) { - cal.add(Calendar.DAY_OF_MONTH, 1); + cal = TimeZoneUtils.addDay(cal, 1); } else if (solarEvent == SolarEvent.MIDNIGHT && localTimeHours + hours < 12) { - cal.add(Calendar.DAY_OF_MONTH, 1); + cal = TimeZoneUtils.addDay(cal, 1); } else if (solarEvent == SolarEvent.NOON && localTimeHours + hours > 24) { - cal.add(Calendar.DAY_OF_MONTH, -1); + cal = TimeZoneUtils.addDay(cal, -1); } cal.set(Calendar.HOUR_OF_DAY, hours); diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java index 7cd8a92e..b617065c 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java @@ -1186,6 +1186,10 @@ public Calendar getGregorianCalendar() { Calendar calendar = Calendar.getInstance(); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); calendar.set(getGregorianYear(), getGregorianMonth(), getGregorianDayOfMonth()); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); return calendar; } diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java index 41941c88..f85d01f1 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java @@ -19,6 +19,7 @@ import java.util.GregorianCalendar; import java.util.TimeZone; +import com.kosherjava.zmanim.util.TimeZoneUtils; /** * This class calculates the Talmud Yerusalmi Date: Sun, 7 Dec 2025 15:00:10 -0500 Subject: [PATCH 10/13] Fix formatting and spacing in JewishCalendar.java --- .../com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java index 5cb1c678..bc612758 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendar.java @@ -1290,7 +1290,7 @@ public Date getTchilasZmanKidushLevana7Days() { cal.add(Calendar.HOUR, 168); // 7 days after the molad return cal.getTime(); } - + /** * Returns the latest time of Kiddush Levana according to the Maharil's opinion that it is calculated as From c944303966f10e7520fef986fa6f35d877b42de1 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 7 Dec 2025 19:44:08 -0500 Subject: [PATCH 11/13] fix yomi for transition date --- .../hebrewcalendar/YerushalmiYomiCalculator.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java index f85d01f1..d330476e 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YerushalmiYomiCalculator.java @@ -89,13 +89,19 @@ public static Daf getDafYomiYerushalmi(JewishCalendar calendar) { } // Start to calculate current cycle. init the start day + prevCycle.setTime(DAF_YOMI_START_DAY.getTime()); nextCycle.setTime(DAF_YOMI_START_DAY.getTime()); + // Move the nextCycle to the last day of the current cycle + nextCycle = TimeZoneUtils.addDay(nextCycle, WHOLE_SHAS_DAFS - 1); + nextCycle = TimeZoneUtils.addDay(nextCycle, getNumOfSpecialDays(prevCycle, nextCycle)); // Go cycle by cycle, until we get the next cycle while (requested.after(nextCycle)) { + // Move the prevCycle from the 1st day of the current cycle to the 1st day of the next cycle prevCycle.setTime(nextCycle.getTime()); - - // Adds the number of whole shas dafs. and the number of days that not have daf. + prevCycle = TimeZoneUtils.addDay(prevCycle, 1); + + // Move the nextCycle from the last day of the current cycle to the last day of the next cycle nextCycle = TimeZoneUtils.addDay(nextCycle, WHOLE_SHAS_DAFS); nextCycle = TimeZoneUtils.addDay(nextCycle, getNumOfSpecialDays(prevCycle, nextCycle)); } @@ -116,6 +122,7 @@ public static Daf getDafYomiYerushalmi(JewishCalendar calendar) { total -= i; masechta++; } + return dafYomi; } From 0f8f92bb18253cc428eb5520ac8d5cf8c847f144 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 9 Dec 2025 20:54:36 -0500 Subject: [PATCH 12/13] check for valid dates with forwarding dates --- .../zmanim/hebrewcalendar/JewishDate.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java index b617065c..71b68ec2 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/JewishDate.java @@ -1301,28 +1301,36 @@ public void forward(int field, int amount) { } /** - * Forward the Jewish date by the number of months passed in. - * FIXME: Deal with forwarding a date such as 30 Nissan by a month. 30 Iyar does not exist. This should be dealt with similar to - * the way that the Java Calendar behaves (not that simple since there is a difference between add() or roll(). + * Advances the Jewish date forward by the specified number of months. + * If the day doesn't exist in the target month (e.g., 30 Iyar), it adjusts to the last day of that month (29 Iyar). * * @throws IllegalArgumentException if the amount is less than 1 - * @param amount the number of months to roll the month forward + * @param amount the number of months to advance (must be at least 1) */ private void forwardJewishMonth(int amount) { if (amount < 1) { throw new IllegalArgumentException("the amount of months to forward has to be greater than zero."); } + int currentMonth = getJewishMonth(); + int currentYear = getJewishYear(); + int currentDay = getJewishDayOfMonth(); for (int i = 0; i < amount; i++) { - if (getJewishMonth() == ELUL) { - setJewishMonth(TISHREI); - setJewishYear(getJewishYear() + 1); - } else if ((! isJewishLeapYear() && getJewishMonth() == ADAR) - || (isJewishLeapYear() && getJewishMonth() == ADAR_II)){ - setJewishMonth(NISSAN); + boolean isLeapYear = JewishDate.isJewishLeapYear(currentYear); + if (currentMonth == ELUL) { + currentMonth = TISHREI; + currentYear = currentYear + 1; + } else if ((!isLeapYear && currentMonth == ADAR) + || (isLeapYear && currentMonth == ADAR_II)){ + currentMonth = NISSAN; } else { - setJewishMonth(getJewishMonth() + 1); + currentMonth = currentMonth + 1; } } + int maxDaysInMonth = JewishDate.getDaysInJewishMonth(currentMonth, currentYear); + if (currentDay > maxDaysInMonth) { + currentDay = maxDaysInMonth; + } + setJewishDate(currentYear, currentMonth, currentDay); } /** From 85130ca4b27ce8baaadcabe8cfcec217c2652fc4 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Thu, 11 Dec 2025 17:12:59 -0500 Subject: [PATCH 13/13] fix issue with daf yomi calculation on start date --- .../zmanim/hebrewcalendar/YomiCalculator.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YomiCalculator.java b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YomiCalculator.java index 90ba0b8c..c17ed396 100644 --- a/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YomiCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/hebrewcalendar/YomiCalculator.java @@ -17,6 +17,7 @@ import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.TimeZone; /** * This class calculates the Daf Yomi Bavli page (daf) for a given date. To calculate Daf Yomi Yerushalmi @@ -30,19 +31,27 @@ public class YomiCalculator { /** * The start date of the first Daf Yomi Bavli cycle of September 11, 1923 / Rosh Hashana 5684. */ - private static final Calendar dafYomiStartDay = new GregorianCalendar(1923, Calendar.SEPTEMBER, 11); + private static final Calendar DAF_YOMI_START_DAY; + static { + DAF_YOMI_START_DAY = new GregorianCalendar(1923, Calendar.SEPTEMBER, 11,0,0,0); + DAF_YOMI_START_DAY.setTimeZone(TimeZone.getTimeZone("UTC")); + } /** The start date of the first Daf Yomi Bavli cycle in the Julian calendar. Used internally for calculations.*/ - private static final int dafYomiJulianStartDay = getJulianDay(dafYomiStartDay); + private static final int DAF_YOMI_JULIAN_START_DAY = getJulianDay(DAF_YOMI_START_DAY); /** * The date that the pagination for the Daf Yomi Maseches Shekalim changed to use the commonly used Vilna * Shas pagination from the no longer commonly available Zhitomir / Slavuta Shas used by Rabbi Meir Shapiro. */ - private static final Calendar shekalimChangeDay = new GregorianCalendar(1975, Calendar.JUNE, 24); + private static final Calendar SHEKALIM_CHANGE_DAY; + static { + SHEKALIM_CHANGE_DAY = new GregorianCalendar(1975, Calendar.JUNE, 24,0,0,0); + SHEKALIM_CHANGE_DAY.setTimeZone(TimeZone.getTimeZone("UTC")); + } /** The Julian date that the cycle for Shekalim changed. * @see #getDafYomiBavli(JewishCalendar) for details. */ - private static final int shekalimJulianChangeDay = getJulianDay(shekalimChangeDay); + private static final int SHEKALIM_JULIAN_CHANGE_DAY = getJulianDay(SHEKALIM_CHANGE_DAY); /** * Default constructor. @@ -89,17 +98,17 @@ public static Daf getDafYomiBavli(JewishCalendar jewishCalendar) { int julianDay = getJulianDay(calendar); int cycleNo; int dafNo; - if (calendar.before(dafYomiStartDay)) { + if (calendar.before(DAF_YOMI_START_DAY)) { // TODO: should we return a null or throw an IllegalArgumentException? throw new IllegalArgumentException(calendar + " is prior to organized Daf Yomi Bavli cycles that started on " - + dafYomiStartDay); + + DAF_YOMI_START_DAY); } - if (calendar.equals(shekalimChangeDay) || calendar.after(shekalimChangeDay)) { - cycleNo = 8 + ((julianDay - shekalimJulianChangeDay) / 2711); - dafNo = ((julianDay - shekalimJulianChangeDay) % 2711); + if (calendar.equals(SHEKALIM_CHANGE_DAY) || calendar.after(SHEKALIM_CHANGE_DAY)) { + cycleNo = 8 + ((julianDay - SHEKALIM_JULIAN_CHANGE_DAY) / 2711); + dafNo = ((julianDay - SHEKALIM_JULIAN_CHANGE_DAY) % 2711); } else { - cycleNo = 1 + ((julianDay - dafYomiJulianStartDay) / 2702); - dafNo = ((julianDay - dafYomiJulianStartDay) % 2702); + cycleNo = 1 + ((julianDay - DAF_YOMI_JULIAN_START_DAY) / 2702); + dafNo = ((julianDay - DAF_YOMI_JULIAN_START_DAY) % 2702); } int total = 0;