diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/util/TLVTradeCalendarTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/util/TLVTradeCalendarTest.java new file mode 100644 index 0000000000..8fcb9c5888 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/util/TLVTradeCalendarTest.java @@ -0,0 +1,200 @@ +package name.abuchen.portfolio.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; + +import java.time.LocalDate; + +import org.junit.Test; + +/** + * Test for Tel Aviv Stock Exchange (TASE) trading calendar. Based on official + * TASE trading vacation schedule starting 2026. + * https://www.tase.co.il/en/content/knowledge_center/trading_vacation_schedule#vacations + */ +public class TLVTradeCalendarTest +{ + + @Test + public void Regular_Lunar_Calendar_Holidays_should_be_a_holiday() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // Passover I Evening and Day, Passover II Evening and Day + assertThat(calendar.isHoliday(LocalDate.parse("2024-04-22")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2024-04-23")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2024-04-28")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2024-04-29")), is(true)); + + assertThat(calendar.isHoliday(LocalDate.parse("2025-04-12")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-04-13")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-04-18")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-04-19")), is(true)); + + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-01")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-02")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-07")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-08")), is(true)); + + // Jewish New Years Eve + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-02")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-09-22")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-11")), is(true)); + + // Jewish New Years Day I + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-03")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-09-23")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-12")), is(true)); + + // Jewish New Years Day II + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-04")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-09-24")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-13")), is(true)); + + // YOM_KIPUR_EVE, if (hebDay == 9 && hebMonth == 7) + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-11")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-01")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-20")), is(true)); + + // YOM_KIPUR_DAY + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-12")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-02")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-21")), is(true)); + + // FAST_DAY Tisha B'Av + assertThat(calendar.isHoliday(LocalDate.parse("2024-08-13")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-08-03")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-07-23")), is(true)); + + // SIMCHAT_TORA_EVE, if (hebDay == 22 && hebMonth == 7) + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-23")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-13")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-10-02")), is(true)); + + // SIMCHAT_TORA; if (hebDay == 23 & hebMonth == 7) + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-24")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-14")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-10-03")), is(true)); + + // SHAVUOT_EVE and SHAVUOT + assertThat(calendar.isHoliday(LocalDate.parse("2024-06-11")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2024-06-12")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-06-01")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-06-02")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-05-21")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-05-22")), is(true)); + + // SUKKOTH_EVE, if (hebDay == 14 && hebMonth == 7) + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-16")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-06")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-25")), is(true)); + + // SUKKOTH, if (hebDay == 15 && hebMonth == 7) + assertThat(calendar.isHoliday(LocalDate.parse("2024-10-17")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-10-07")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-09-26")), is(true)); + } + + @Test + public void Special_Jewish_Calendar_Holidays_should_be_a_holiday() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // PURIM + assertThat(calendar.isHoliday(LocalDate.parse("2024-03-24")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-03-14")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-03-03")), is(true)); + } + + @Test + public void Israeli_Memorial_Day_should_be_a_holiday() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // Memorial Day (Yom HaZikaron) + assertThat(calendar.isHoliday(LocalDate.parse("2024-05-13")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-04-30")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-21")), is(true)); + } + + @Test + public void Israeli_Independence_Day_should_be_a_holiday() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // Independence Day (Yom Ha'atzmaut) + assertThat(calendar.isHoliday(LocalDate.parse("2024-05-14")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2025-05-01")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-04-22")), is(true)); + } + + @Test + public void testWeekendsAfter2016() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + assertFalse(calendar == null); + assertThat(calendar.isHoliday(LocalDate.parse("2026-01-10")), is(true)); + assertThat(calendar.isHoliday(LocalDate.parse("2026-01-11")), is(true)); + } + + @Test + public void Regular_working_days_should_not_be_holidays() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // Regular weekdays that are not holidays + assertThat(calendar.isHoliday(LocalDate.parse("2024-01-02")), is(false)); // Tuesday + assertThat(calendar.isHoliday(LocalDate.parse("2024-01-03")), is(false)); // Wednesday + assertThat(calendar.isHoliday(LocalDate.parse("2024-01-04")), is(false)); // Thursday + + assertThat(calendar.isHoliday(LocalDate.parse("2025-01-06")), is(false)); // Monday + assertThat(calendar.isHoliday(LocalDate.parse("2025-01-07")), is(false)); // Tuesday + + assertThat(calendar.isHoliday(LocalDate.parse("2026-01-06")), is(false)); // Monday + assertThat(calendar.isHoliday(LocalDate.parse("2026-01-07")), is(false)); // Tuesday + assertThat(calendar.isHoliday(LocalDate.parse("2026-01-08")), is(false)); // Wednesday + } + + @Test + public void Edge_cases_with_Sabbath_adjustments() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + assertThat(calendar.isHoliday(LocalDate.parse("2024-05-13")), is(true)); // Memorial Day adjusted + assertThat(calendar.isHoliday(LocalDate.parse("2024-05-14")), is(true)); // Independence Day adjusted + } + + @Test + public void Multi_year_consistency_test() + { + var calendar = TradeCalendarManager.getInstance("tlv"); + + // Test multiple years to ensure calendar consistency + int[] testYears = { 2024, 2025, 2026 }; + + for (int year : testYears) + { + // Each year should have Rosh Hashanah (2 days) + var roshHashanahStart = LocalDate.of(year, 9, 1); // approximate + var foundRoshHashanah = false; + + // Search for Rosh Hashanah in September/October + for (var day = 1; day <= 60; day++) + { + var testDate = roshHashanahStart.plusDays(day); + if (testDate.getYear() > year) + break; + + if (calendar.isHoliday(testDate) && calendar.isHoliday(testDate.plusDays(1))) + { + foundRoshHashanah = true; + break; + } + } + + assertThat("Rosh Hashanah not found for year " + year, foundRoshHashanah, is(true)); + } + } +} diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java index 69ca892cad..00a5c2438f 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java @@ -224,6 +224,7 @@ public class Messages extends NLS public static String LabelTradeCalendarSSE; public static String LabelTradeCalendarSix; public static String LabelTradeCalendarTARGET2; + public static String LabelTradeCalendarTLV; public static String LabelTradeCalendarTSX; public static String LabelTradeCalendarUseDefault; public static String LabelTradeCalendarVSE; diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties index 955daf7e7b..dc7f4eb935 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties @@ -438,6 +438,8 @@ LabelTradeCalendarSix = Swiss Exchange (SIX) LabelTradeCalendarTARGET2 = TARGET2 (Eurozone banking day) +LabelTradeCalendarTLV = Tel Aviv Stock Exchange Trade + LabelTradeCalendarTSX = Toronto Stock Exchange LabelTradeCalendarUseDefault = (Use globally configured calendar: {0}) diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/currencies.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/currencies.properties index 4612a5b593..4cda4dc064 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/currencies.properties +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/currencies.properties @@ -303,7 +303,7 @@ ILA = Israeli agora ILA.symbol = agorot -ILS = Israeli new sheqel +ILS = Israeli new shekel ILS.symbol = \u20AA diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayName.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayName.java index 7de48a5df0..edeb2865ee 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayName.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayName.java @@ -32,32 +32,49 @@ FAMILY_DAY, FIRST_CHRISTMAS_DAY, GOOD_FRIDAY, + HOLOCAUST_REMEMBERENCE_DAY, HURRICANE_SANDY, INDEPENDENCE, + INDEPENDENCE_DAY, INDIGENOUS_PEOPLE, INMACULATE_CONCEPTION, INTERNATION_WOMENS_DAY, + JEWISH_FAST_DAY, + JEWISH_NEW_YEAR_DAY_I, + JEWISH_NEW_YEAR_DAY_II, + JEWISH_NEW_YEAR_EVE, JUNETEENTH, KINGS_BIRTHDAY, LABOUR_DAY, MARTIN_LUTHER_KING, MEMORIAL, + MEMORIAL_DAY, MILLENNIUM, NATION_DAY, NEW_YEAR, - NEW_YEARS_EVE, NEW_YEAR_HOLIDAY, + NEW_YEARS_EVE, + PASSOVER_I_EVE, + PASSOVER_I, + PASSOVER_II_EVE, + PASSOVER_II, PATRON_DAY, + PURIM, REFORMATION_DAY, REPENTANCE_AND_PRAYER, REPUBLIC_PROCLAMATION_DAY, ROYAL_JUBILEE, ROYAL_WEDDING, - SAINT_STEPHEN, SAINT_PETER_PAUL, + SAINT_STEPHEN, + SAVHUOT_EVE, SECOND_CHRISTMAS_DAY, + SHAVUOT_DAY, + SIMCHAT_TORA, + SIMCHAT_TORA_EVE, SPRING_MAY_BANK_HOLIDAY, - STATE_FUNERAL, + STATE_FUNERAL, SUKKOTH_DAY, + SUKKOTH_EVE, SUMMER_BANK_HOLIDAY, TERRORIST_ATTACKS, THANKSGIVING, @@ -68,7 +85,9 @@ VICTORY_DAY, VIRGIN_OF_CARMEN, WASHINGTONS_BIRTHDAY, - WHIT_MONDAY; + WHIT_MONDAY, + YOM_KIPUR, + YOM_KIPUR_EVE; private static final ResourceBundle RESOURCES = ResourceBundle .getBundle("name.abuchen.portfolio.util.holiday-names"); //$NON-NLS-1$ diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayType.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayType.java index f7103145a1..5c60c4335b 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayType.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/HolidayType.java @@ -11,8 +11,110 @@ import java.util.List; import java.util.Set; +import name.abuchen.portfolio.util.JewishCalendar.CalendarImpl; +import name.abuchen.portfolio.util.JewishCalendar.JewishCalendarDate; + +/** + * Abstract base class for different types of holidays. Supports fixed dates, + * weekday-based dates, Easter-relative dates, and Jewish calendar dates. + */ /* package */ abstract class HolidayType { + + /** + * Holiday type for Israeli Independence Day (Yom Ha'atzmaut). Date varies + * based on day of week and follows Memorial Day. + */ + private static class IsraeliIndependenceCalendarHolidayType extends HolidayType + { + private final CalendarImpl calendar = new CalendarImpl(); + + public IsraeliIndependenceCalendarHolidayType(HolidayName name) + { + super(name); + } + + @Override + protected Holiday doGetHoliday(int gregorianYear) + { + var startOfYearAbsolute = calendar.absoluteFromGregorianDate(new JewishCalendarDate(1, 1, gregorianYear)); + var hebrewYearStart = calendar.hebrewDateFromAbsolute(startOfYearAbsolute); + + var independenceDate = calculateIndependenceDate(hebrewYearStart.getYear()); + var gregorianDate = calendar + .gregorianDateFromAbsolute(calendar.absoluteFromHebrewDate(independenceDate)); + + var date = LocalDate.of(gregorianDate.getYear(), gregorianDate.getMonth(), gregorianDate.getDay()); + return new Holiday(getName(), date); + } + + /** + * Calculates the Hebrew date for Independence Day. Usually Iyyar 5, but + * moved to follow Memorial Day rules and avoid Sabbath conflicts. + */ + private JewishCalendarDate calculateIndependenceDate(int hebrewYear) + { + // Independence Day follows Memorial Day, so we need to calculate + // Memorial Day first + var memorialDate = calculateMemorialDate(hebrewYear); + + // Independence Day is the day after Memorial Day + var independenceDay = memorialDate.getDay() + 1; + var independenceMonth = memorialDate.getMonth(); + var independenceYear = memorialDate.getYear(); + + // Handle month overflow (though unlikely with Iyyar) + var maxDayInMonth = calendar.getLastDayOfHebrewMonth(independenceMonth, independenceYear); + if (independenceDay > maxDayInMonth) + { + independenceDay = 1; + independenceMonth++; + if (independenceMonth > calendar.getLastMonthOfHebrewYear(independenceYear)) + { + independenceMonth = 1; + independenceYear++; + } + } + + return new JewishCalendarDate(independenceDay, independenceMonth, independenceYear); + } + + /** + * Calculates the Hebrew date for Memorial Day (shared logic). Iyyar 4, + * but moved to avoid Thursday/Friday/Saturday. + */ + private JewishCalendarDate calculateMemorialDate(int hebrewYear) + { + var weekday = getWeekdayOfHebrewDate(4, 2, hebrewYear); // Iyyar 4 + + if (weekday == 5) + { // Friday - move to Wednesday + return new JewishCalendarDate(2, 2, hebrewYear); + } + else if (weekday == 4) + { // Thursday - move to Wednesday + return new JewishCalendarDate(3, 2, hebrewYear); + } + else if (hebrewYear >= 5764 && weekday == 0) + { // Saturday after 2004 - move to Sunday + return new JewishCalendarDate(5, 2, hebrewYear); + } + else + { + return new JewishCalendarDate(4, 2, hebrewYear); // Default date + } + } + + private int getWeekdayOfHebrewDate(int day, int month, int year) + { + var absoluteDate = calendar.absoluteFromHebrewDate(new JewishCalendarDate(day, month, year)); + return absoluteDate % 7; + } + } + + /** + * Helper class for conditional date movement based on day of week. + */ private static class MoveIf { private final DayOfWeek dayOfWeek; @@ -30,6 +132,9 @@ public LocalDate apply(LocalDate date) } } + /** + * Holiday type for fixed calendar dates (e.g., Christmas on December 25). + */ private static class FixedHolidayType extends HolidayType { private final Month month; @@ -45,11 +150,14 @@ public FixedHolidayType(HolidayName name, Month month, int dayOfMonth) @Override protected Holiday doGetHoliday(int year) { - LocalDate date = LocalDate.of(year, month.getValue(), dayOfMonth); + var date = LocalDate.of(year, month.getValue(), dayOfMonth); return new Holiday(getName(), date); } } + /** + * Holiday type for weekday-based dates (e.g., first Monday in September). + */ private static class FixedWeekdayHolidayType extends HolidayType { private final int which; @@ -67,11 +175,15 @@ public FixedWeekdayHolidayType(HolidayName name, int which, DayOfWeek weekday, M @Override protected Holiday doGetHoliday(int year) { - LocalDate date = LocalDate.of(year, month, 1); + var date = LocalDate.of(year, month, 1); return new Holiday(getName(), date.with(dayOfWeekInMonth(which, weekday))); } } + /** + * Holiday type for Easter-relative dates (e.g., Good Friday = Easter - 2 + * days). + */ private static class RelativeToEasterHolidayType extends HolidayType { private final int daysToAdd; @@ -85,106 +197,205 @@ public RelativeToEasterHolidayType(HolidayName name, int daysToAdd) @Override protected Holiday doGetHoliday(int year) { - LocalDate easterSunday = calculateEasterSunday(year); - + var easterSunday = calculateEasterSunday(year); return new Holiday(getName(), easterSunday.plusDays(daysToAdd)); } - private LocalDate calculateEasterSunday(int nYear) + /** + * Calculates Easter Sunday using the ecclesiastical algorithm. + */ + private LocalDate calculateEasterSunday(int year) { - // @formatter:off - - // Taken from - // http://www.java2s.com/Code/Java/Data-Type/CalculateHolidays.htm - // and published under the - // GNU General Public License version 2 - - /* Calculate Easter Sunday - Written by Gregory N. Mirsky - Source: 2nd Edition by Peter Duffett-Smith. It was originally from - Butcher's Ecclesiastical Calendar, published in 1876. This - algorithm has also been published in the 1922 book General - Astronomy by Spencer Jones; in The Journal of the British - Astronomical Association (Vol.88, page 91, December 1977); and in - Astronomical Algorithms (1991) by Jean Meeus. - This algorithm holds for any year in the Gregorian Calendar, which - (of course) means years including and after 1583. - a=year%19 - b=year/100 - c=year%100 - d=b/4 - e=b%4 - f=(b+8)/25 - g=(b-f+1)/3 - h=(19*a+b-d-g+15)%30 - i=c/4 - k=c%4 - l=(32+2*e+2*i-h-k)%7 - m=(a+11*h+22*l)/451 - Easter Month =(h+l-7*m+114)/31 [3=March, 4=April] - p=(h+l-7*m+114)%31 - Easter Date=p+1 (date in Easter Month) - Note: Integer truncation is already factored into the - calculations. Using higher percision variables will cause - inaccurate calculations. - */ - // @formatter:on - - int nA = 0; - int nB = 0; - int nC = 0; - int nD = 0; - int nE = 0; - int nF = 0; - int nG = 0; - int nH = 0; - int nI = 0; - int nK = 0; - int nL = 0; - int nM = 0; - int nP = 0; - int nEasterMonth = 0; - int nEasterDay = 0; - - nA = nYear % 19; - nB = nYear / 100; - nC = nYear % 100; - nD = nB / 4; - nE = nB % 4; - nF = (nB + 8) / 25; - nG = (nB - nF + 1) / 3; - nH = (19 * nA + nB - nD - nG + 15) % 30; - nI = nC / 4; - nK = nC % 4; - nL = (32 + 2 * nE + 2 * nI - nH - nK) % 7; - nM = (nA + 11 * nH + 22 * nL) / 451; - - // [3=March, 4=April] - nEasterMonth = (nH + nL - 7 * nM + 114) / 31; - nP = (nH + nL - 7 * nM + 114) % 31; - - // Date in Easter Month. - nEasterDay = nP + 1; - - // Populate the date object... - return LocalDate.of(nYear, nEasterMonth, nEasterDay); + // Easter calculation algorithm from Butcher's Ecclesiastical + // Calendar (1876) + // Valid for Gregorian Calendar (years >= 1583) + + var a = year % 19; + var b = year / 100; + var c = year % 100; + var d = b / 4; + var e = b % 4; + var f = (b + 8) / 25; + var g = (b - f + 1) / 3; + var h = (19 * a + b - d - g + 15) % 30; + var i = c / 4; + var k = c % 4; + var l = (32 + 2 * e + 2 * i - h - k) % 7; + var m = (a + 11 * h + 22 * l) / 451; + + var easterMonth = (h + l - 7 * m + 114) / 31; // 3=March, 4=April + var easterDay = ((h + l - 7 * m + 114) % 31) + 1; + + return LocalDate.of(year, easterMonth, easterDay); } } - private final HolidayName name; + /** + * Holiday type for fixed Jewish calendar dates. + */ + private static class FixedJewishCalendarHolidayType extends HolidayType + { + private final int hebrewMonth; + private final int hebrewDayOfMonth; + private final int daysToAdd; + + public FixedJewishCalendarHolidayType(HolidayName name, int hebrewMonth, int hebrewDayOfMonth, int daysToAdd) + { + super(name); + this.hebrewMonth = hebrewMonth; + this.hebrewDayOfMonth = hebrewDayOfMonth; + this.daysToAdd = daysToAdd; + } + + @Override + protected Holiday doGetHoliday(int year) + { + var gregorianDate = calculateHebrewHolidayInGregorianYear(year, hebrewMonth, + hebrewDayOfMonth, daysToAdd); + var date = LocalDate.of(gregorianDate.getYear(), gregorianDate.getMonth(), gregorianDate.getDay()); + return new Holiday(getName(), date); + } + } + + + + /** + * Holiday type for Purim, which falls on different Hebrew months in leap + * years. + */ + private static class JewishPurimCalendarHolidayType extends HolidayType + { + private final CalendarImpl calendar = new CalendarImpl(); + + public JewishPurimCalendarHolidayType(HolidayName name) + { + super(name); + } + + @Override + protected Holiday doGetHoliday(int gregorianYear) + { + var startOfYearAbsolute = calendar.absoluteFromGregorianDate(new JewishCalendarDate(1, 1, gregorianYear)); + var hebrewYearStart = calendar.hebrewDateFromAbsolute(startOfYearAbsolute); + + var purimDate = calculatePurimDate(hebrewYearStart.getYear()); + var gregorianDate = calendar + .gregorianDateFromAbsolute(calendar.absoluteFromHebrewDate(purimDate)); + + var date = LocalDate.of(gregorianDate.getYear(), gregorianDate.getMonth(), gregorianDate.getDay()); + return new Holiday(getName(), date); + } + + /** + * Calculates the Hebrew date for Purim. Adar 14 (or Adar II 14 in leap + * years). + */ + private JewishCalendarDate calculatePurimDate(int hebrewYear) + { + // Adar II or Adar + var purimMonth = calendar.isHebrewLeapYear(hebrewYear) ? 13 : 12; + return new JewishCalendarDate(14, purimMonth, hebrewYear); + } + } + + /** + * Holiday type for Israeli Memorial/Independence Days. Dates vary based on + * day of week to avoid Sabbath conflicts. + */ + private static class IsraeliMemorialCalendarHolidayType extends HolidayType + { + private final int daysToAdd; + private final CalendarImpl calendar = new CalendarImpl(); + public IsraeliMemorialCalendarHolidayType(HolidayName name, int daysToAdd) + { + super(name); + this.daysToAdd = daysToAdd; + } + + @Override + protected Holiday doGetHoliday(int gregorianYear) + { + var startOfYearAbsolute = calendar.absoluteFromGregorianDate(new JewishCalendarDate(1, 1, gregorianYear)); + var hebrewYearStart = calendar.hebrewDateFromAbsolute(startOfYearAbsolute); + + var memorialDate = calculateMemorialDate(hebrewYearStart.getYear()); + var gregorianDate = calendar + .gregorianDateFromAbsolute(calendar.absoluteFromHebrewDate(memorialDate) + daysToAdd); + + var date = LocalDate.of(gregorianDate.getYear(), gregorianDate.getMonth(), gregorianDate.getDay()); + return new Holiday(getName(), date); + } + + /** + * Calculates the Hebrew date for Memorial Day. Iyyar 4, but moved to + * avoid Thursday/Friday/Saturday. + */ + private JewishCalendarDate calculateMemorialDate(int hebrewYear) + { + var weekday = getWeekdayOfHebrewDate(4, 2, hebrewYear); // Iyyar 4 + + if (weekday == 5) + { // Friday - move to Wednesday + return new JewishCalendarDate(2, 2, hebrewYear); + } + else if (weekday == 4) + { // Thursday - move to Wednesday + return new JewishCalendarDate(3, 2, hebrewYear); + } + else if (hebrewYear >= 5764 && weekday == 0) + { // Saturday after 2004 - move to Sunday + return new JewishCalendarDate(5, 2, hebrewYear); + } + else + { + return new JewishCalendarDate(4, 2, hebrewYear); // Default date + } + } + + private int getWeekdayOfHebrewDate(int day, int month, int year) + { + var absoluteDate = calendar.absoluteFromHebrewDate(new JewishCalendarDate(day, month, year)); + return absoluteDate % 7; + } + } + + // Instance variables + private final HolidayName name; private int validFrom = -1; private int validTo = -1; private final Set exceptIn = new HashSet<>(); - private final List moveIf = new ArrayList<>(); private DayOfWeek moveTo = null; - public HolidayType(HolidayName name) + protected HolidayType(HolidayName name) { this.name = name; } + // Factory methods + public static HolidayType fixedJewishCalendar(HolidayName name, int hebrewMonth, int hebrewDayOfMonth, + int daysToAdd) + { + return new FixedJewishCalendarHolidayType(name, hebrewMonth, hebrewDayOfMonth, daysToAdd); + } + + public static HolidayType jewishPurimCalendar(HolidayName name, int daysToAdd) + { + return new JewishPurimCalendarHolidayType(name); + } + + + public static HolidayType israeliMemorialCalendar(HolidayName name) + { + return new IsraeliMemorialCalendarHolidayType(name, 0); + } + + public static HolidayType israeliIndependenceCalendar(HolidayName name) + { + return new IsraeliIndependenceCalendarHolidayType(name); + } + public static HolidayType fixed(HolidayName name, Month month, int dayOfMonth) { return new FixedHolidayType(name, month, dayOfMonth); @@ -200,6 +411,7 @@ public static HolidayType easter(HolidayName name, int daysToAdd) return new RelativeToEasterHolidayType(name, daysToAdd); } + // Configuration methods public HolidayName getName() { return name; @@ -242,32 +454,83 @@ public HolidayType moveTo(DayOfWeek dayOfWeek) return this; } + // Main holiday calculation method public Holiday getHoliday(int year) { + // Check validity period if (validFrom != -1 && year < validFrom) return null; - if (validTo != -1 && year > validTo) return null; - if (exceptIn.contains(year)) return null; - Holiday answer = doGetHoliday(year); + var holiday = doGetHoliday(year); + // Apply date movements if configured if (moveIf.isEmpty() && moveTo == null) - return answer; + return holiday; - LocalDate date = answer.getDate(); + var date = holiday.getDate(); - for (MoveIf mv : moveIf) - date = mv.apply(date); + // Apply conditional movements + for (MoveIf movement : moveIf) + { + date = movement.apply(date); + } + // Apply absolute movement to specific weekday if (moveTo != null) + { date = date.with(nextOrSame(moveTo)); + } - return new Holiday(answer.getName(), date); + return new Holiday(holiday.getName(), date); } protected abstract Holiday doGetHoliday(int year); + + /** + * Utility method to calculate Hebrew holidays that fall within a Gregorian + * year. Handles the complexity of Hebrew calendar spanning Gregorian years. + */ + public JewishCalendarDate calculateHebrewHolidayInGregorianYear(int gregorianYear, int hebrewMonth, int hebrewDay, + int additionalDays) + { + var calendar = new CalendarImpl(); + + // Get absolute dates for start and end of Gregorian year + var yearStartAbsolute = calendar.absoluteFromGregorianDate(new JewishCalendarDate(1, 1, gregorianYear)); + var yearEndAbsolute = calendar.absoluteFromGregorianDate(new JewishCalendarDate(31, 12, gregorianYear)); + + // Get corresponding Hebrew dates + var hebrewYearStart = calendar.hebrewDateFromAbsolute(yearStartAbsolute); + var hebrewYearEnd = calendar.hebrewDateFromAbsolute(yearEndAbsolute); + + var holidayDay = hebrewDay + additionalDays; + + // Try the Hebrew holiday in the same Hebrew year as start of Gregorian + // year + var holidayCurrentYear = new JewishCalendarDate(holidayDay, hebrewMonth, + hebrewYearStart.getYear()); + var gregorianCurrentYear = calendar + .gregorianDateFromAbsolute(calendar.absoluteFromHebrewDate(holidayCurrentYear)); + + if (gregorianCurrentYear.getYear() == gregorianYear) + return gregorianCurrentYear; + + // Try the Hebrew holiday in the next Hebrew year + var holidayNextYear = new JewishCalendarDate(holidayDay, hebrewMonth, hebrewYearEnd.getYear()); + var gregorianNextYear = calendar + .gregorianDateFromAbsolute(calendar.absoluteFromHebrewDate(holidayNextYear)); + + return gregorianNextYear; + } + + // Legacy method for backward compatibility + @Deprecated + public JewishCalendarDate jewishHoliday(int gregorianYear, int hebrewMonth, int hebrewDay, int additionalDays) + { + return calculateHebrewHolidayInGregorianYear(gregorianYear, hebrewMonth, hebrewDay, additionalDays); + } } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/JewishCalendar.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/JewishCalendar.java new file mode 100644 index 0000000000..a64ed3ef16 --- /dev/null +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/JewishCalendar.java @@ -0,0 +1,414 @@ +package name.abuchen.portfolio.util; + +import java.util.Objects; + +/** + * Jewish Calendar implementation for date conversion between Gregorian and + * Hebrew calendars. Based on algorithms from David Greve's implementation. + * Copyright notice: The code is freely usable for non-profit purposes. + */ +public class JewishCalendar +{ + + /** + * Calendar implementation with optimized algorithms and improved + * readability. + */ + public static class CalendarImpl + { + + // Constants for better maintainability + private static final int[] GREGORIAN_MONTH_DAYS = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + // Days elapsed before absolute date 1 + private static final int HEBREW_EPOCH = 1373429; + // Months in 19-year Metonic cycle + private static final int METONIC_CYCLE_MONTHS = 235; + private static final int METONIC_CYCLE_YEARS = 19; + private static final int PARTS_PER_HOUR = 1080; + private static final int HOURS_PER_DAY = 24; + + /** + * Returns the last day of a Gregorian month, accounting for leap years. + */ + public int getLastDayOfGregorianMonth(int month, int year) + { + if (month == 2 && isGregorianLeapYear(year)) + return 29; + return GREGORIAN_MONTH_DAYS[month - 1]; + } + + /** + * Checks if a Gregorian year is a leap year. + */ + private boolean isGregorianLeapYear(int year) + { + return (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0); + } + + /** + * Converts a Gregorian date to absolute day number. + */ + public int absoluteFromGregorianDate(JewishCalendarDate date) + { + var absoluteDay = date.getDay(); + + // Add days from previous months in current year + for (var month = 1; month < date.getMonth(); month++) + { + absoluteDay += getLastDayOfGregorianMonth(month, date.getYear()); + } + + var year = date.getYear(); + var priorYears = year - 1; + + // Add days from previous years + absoluteDay += 365 * priorYears; + absoluteDay += priorYears / 4; // Julian leap days + absoluteDay -= priorYears / 100; // Century years (not leap) + absoluteDay += priorYears / 400; // 400-year leap years + + return absoluteDay; + } + + /** + * Converts absolute day number to Gregorian date. + */ + public JewishCalendarDate gregorianDateFromAbsolute(int absoluteDate) + { + // Approximate year + var year = absoluteDate / 366; + + // Find exact year + while (absoluteFromGregorianDate(new JewishCalendarDate(1, 1, year + 1)) <= absoluteDate) + { + year++; + } + + // Find month + var month = 1; + while (absoluteFromGregorianDate(new JewishCalendarDate(getLastDayOfGregorianMonth(month, year), month, + year)) < absoluteDate) + { + month++; + } + + // Calculate day + var day = absoluteDate - absoluteFromGregorianDate(new JewishCalendarDate(1, month, year)) + 1; + + return new JewishCalendarDate(day, month, year); + } + + /** + * Determines if a Hebrew year is a leap year. + */ + public boolean isHebrewLeapYear(int year) + { + return ((year * 7 + 1) % METONIC_CYCLE_YEARS) < 7; + } + + /** + * Returns the last month of a Hebrew year (12 or 13). + */ + public int getLastMonthOfHebrewYear(int year) + { + return isHebrewLeapYear(year) ? 13 : 12; + } + + /** + * Returns the number of days in a Hebrew month. + */ + public int getLastDayOfHebrewMonth(int month, int year) + { + // Months with 29 days: Iyyar(2), Tammuz(4), Elul(6), Tevet(10), + // Adar II(13) + if (month == 2 || month == 4 || month == 6 || month == 10 || month == 13) + return 29; + + // Adar in non-leap year has 29 days + if (month == 12 && !isHebrewLeapYear(year)) + return 29; + + // Heshvan can have 29 days (short year) + if (month == 8 && !isLongHeshvan(year)) + return 29; + + // Kislev can have 29 days (deficient year) + if (month == 9 && isShortKislev(year)) + return 29; + + return 30; // Default month length + } + + /** + * Calculates days elapsed since Hebrew calendar epoch for given year. + */ + private int hebrewCalendarElapsedDays(int year) + { + var priorYear = year - 1; + + // Calculate months elapsed until start of given year + var monthsElapsed = METONIC_CYCLE_MONTHS * (priorYear / METONIC_CYCLE_YEARS); + monthsElapsed += 12 * (priorYear % METONIC_CYCLE_YEARS); + monthsElapsed += ((priorYear % METONIC_CYCLE_YEARS) * 7 + 1) / METONIC_CYCLE_YEARS; + + // Calculate conjunction time + var partsElapsed = (monthsElapsed % PARTS_PER_HOUR) * 793 + 204; + var hoursElapsed = 5 + monthsElapsed * 12 + (monthsElapsed / PARTS_PER_HOUR) * 793 + + (partsElapsed / PARTS_PER_HOUR); + + var conjunctionDay = 1 + 29 * monthsElapsed + hoursElapsed / HOURS_PER_DAY; + var conjunctionParts = (hoursElapsed % HOURS_PER_DAY) * PARTS_PER_HOUR + (partsElapsed % PARTS_PER_HOUR); + + // Apply Rosh Hashanah postponement rules + var roshHashanah = conjunctionDay; + + // Rule 1: If molad is at or after midday + if (conjunctionParts >= 19440 || + // Rule 2: Tuesday molad in common year after 9:204 + (conjunctionDay % 7 == 2 && conjunctionParts >= 9924 && !isHebrewLeapYear(year)) || + // Rule 3: Monday molad after leap year after 15:589 + (conjunctionDay % 7 == 1 && conjunctionParts >= 16789 && isHebrewLeapYear(year - 1))) + { + roshHashanah++; + } + + // Rule 4: Avoid Sunday, Wednesday, Friday + var dayOfWeek = roshHashanah % 7; + if (dayOfWeek == 0 || dayOfWeek == 3 || dayOfWeek == 5) + { + roshHashanah++; + } + + return roshHashanah; + } + + /** + * Returns the number of days in a Hebrew year. + */ + private int getDaysInHebrewYear(int year) + { + return hebrewCalendarElapsedDays(year + 1) - hebrewCalendarElapsedDays(year); + } + + /** + * Determines if Heshvan has 30 days (long year). + */ + private boolean isLongHeshvan(int year) + { + return getDaysInHebrewYear(year) % 10 == 5; + } + + /** + * Determines if Kislev has 29 days (deficient year). + */ + private boolean isShortKislev(int year) + { + return getDaysInHebrewYear(year) % 10 == 3; + } + + /** + * Converts Hebrew date to absolute day number. + */ + public int absoluteFromHebrewDate(JewishCalendarDate date) + { + var absoluteDay = date.getDay(); + + // Add days from previous months in current year + if (date.getMonth() < 7) + { + // Before Tishri: add months from Tishri to end of year, then + // Nisan to current month + for (var month = 7; month <= getLastMonthOfHebrewYear(date.getYear()); month++) + { + absoluteDay += getLastDayOfHebrewMonth(month, date.getYear()); + } + for (var month = 1; month < date.getMonth(); month++) + { + absoluteDay += getLastDayOfHebrewMonth(month, date.getYear()); + } + } + else + { + // After/including Tishri: add months from Tishri to current + // month + for (var month = 7; month < date.getMonth(); month++) + { + absoluteDay += getLastDayOfHebrewMonth(month, date.getYear()); + } + } + + // Add days from previous years and adjust for epoch + absoluteDay += hebrewCalendarElapsedDays(date.getYear()) - HEBREW_EPOCH; + + return absoluteDay; + } + + /** + * Converts absolute day number to Hebrew date. + */ + public JewishCalendarDate hebrewDateFromAbsolute(int absoluteDate) + { + // Approximate year + var year = (absoluteDate + HEBREW_EPOCH) / 366; + + // Find exact year + while (absoluteFromHebrewDate(new JewishCalendarDate(1, 7, year + 1)) <= absoluteDate) + { + year++; + } + + // Determine starting month for search + var startMonth = absoluteFromHebrewDate(new JewishCalendarDate(1, 1, year)) <= absoluteDate ? 1 : 7; + + // Find month with protection against infinite loops + var month = startMonth; + var monthsChecked = 0; + var maxMonths = getLastMonthOfHebrewYear(year); + + while (absoluteFromHebrewDate( + new JewishCalendarDate(getLastDayOfHebrewMonth(month, year), month, year)) < absoluteDate) + { + month++; + monthsChecked++; + + // Wrap around if necessary + if (month > maxMonths) + { + month = 1; + } + + // Prevent infinite loop - if we've checked all months twice, + // break + if (monthsChecked > maxMonths * 2) + { + throw new IllegalArgumentException( + "Unable to find valid Hebrew month for absolute date: " + absoluteDate); //$NON-NLS-1$ + } + } + + // Calculate day + var day = absoluteDate - absoluteFromHebrewDate(new JewishCalendarDate(1, month, year)) + 1; + + // Validate the calculated day + if (day < 1 || day > getLastDayOfHebrewMonth(month, year)) + { + throw new IllegalArgumentException( + "Calculated day " + day + " is invalid for Hebrew month " + month + " in year " + year); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + return new JewishCalendarDate(day, month, year); + } + + // Legacy method names for backward compatibility - kept without + // deprecation + public boolean hebrewLeapYear(int year) + { + return isHebrewLeapYear(year); + } + + public int absoluteFromJewishDate(JewishCalendarDate date) + { + return absoluteFromHebrewDate(date); + } + + public JewishCalendarDate jewishDateFromAbsolute(int absoluteDate) + { + return hebrewDateFromAbsolute(absoluteDate); + } + } + + /** + * Immutable date representation for both Gregorian and Hebrew calendars. + */ + public static class JewishCalendarDate + { + private final int day; + private final int month; + private final int year; + + /** + * Creates a new calendar date. + * + * @param day + * Day of month (1-31) + * @param month + * Month (1-12 for Gregorian, 1-13 for Hebrew) + * @param year + * Year + */ + public JewishCalendarDate(int day, int month, int year) + { + if (day < 1 || day > 31) + { + throw new IllegalArgumentException("Day must be between 1 and 31: " + day); //$NON-NLS-1$ + } + if (month < 1 || month > 13) + { + throw new IllegalArgumentException("Month must be between 1 and 13: " + month); //$NON-NLS-1$ + } + if (year < 1) + { + throw new IllegalArgumentException("Year must be positive: " + year); //$NON-NLS-1$ + } + + this.day = day; + this.month = month; + this.year = year; + } + + public int getDay() + { + return day; + } + + public int getMonth() + { + return month; + } + + public int getYear() + { + return year; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + var that = (JewishCalendarDate) obj; + return day == that.day && month == that.month && year == that.year; + } + + @Override + public int hashCode() + { + return Objects.hash(day, month, year); + } + + @Override + public String toString() + { + return String.format("%d.%d.%d", day, month, year); //$NON-NLS-1$ + } + + /** + * Returns a formatted string representation. + */ + public String format(String pattern) + { + return pattern.replace("dd", String.format("%02d", day)) //$NON-NLS-1$ //$NON-NLS-2$ + .replace("MM", String.format("%02d", month)) //$NON-NLS-1$ //$NON-NLS-2$ + .replace("yyyy", String.valueOf(year)) //$NON-NLS-1$ + .replace("yy", String.valueOf(year % 100)); //$NON-NLS-1$ + } + + @SuppressWarnings("unused") + public int getHashCode() + { + return hashCode(); + } + } +} diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/TradeCalendarManager.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/TradeCalendarManager.java index ece94b5985..bb365d2933 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/TradeCalendarManager.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/TradeCalendarManager.java @@ -32,6 +32,10 @@ import static name.abuchen.portfolio.util.HolidayName.INDIGENOUS_PEOPLE; import static name.abuchen.portfolio.util.HolidayName.INMACULATE_CONCEPTION; import static name.abuchen.portfolio.util.HolidayName.INTERNATION_WOMENS_DAY; +import static name.abuchen.portfolio.util.HolidayName.JEWISH_FAST_DAY; +import static name.abuchen.portfolio.util.HolidayName.JEWISH_NEW_YEAR_DAY_I; +import static name.abuchen.portfolio.util.HolidayName.JEWISH_NEW_YEAR_DAY_II; +import static name.abuchen.portfolio.util.HolidayName.JEWISH_NEW_YEAR_EVE; import static name.abuchen.portfolio.util.HolidayName.JUNETEENTH; import static name.abuchen.portfolio.util.HolidayName.KINGS_BIRTHDAY; import static name.abuchen.portfolio.util.HolidayName.LABOUR_DAY; @@ -42,7 +46,12 @@ import static name.abuchen.portfolio.util.HolidayName.NEW_YEAR; import static name.abuchen.portfolio.util.HolidayName.NEW_YEARS_EVE; import static name.abuchen.portfolio.util.HolidayName.NEW_YEAR_HOLIDAY; +import static name.abuchen.portfolio.util.HolidayName.PASSOVER_I; +import static name.abuchen.portfolio.util.HolidayName.PASSOVER_II; +import static name.abuchen.portfolio.util.HolidayName.PASSOVER_II_EVE; +import static name.abuchen.portfolio.util.HolidayName.PASSOVER_I_EVE; import static name.abuchen.portfolio.util.HolidayName.PATRON_DAY; +import static name.abuchen.portfolio.util.HolidayName.PURIM; import static name.abuchen.portfolio.util.HolidayName.REFORMATION_DAY; import static name.abuchen.portfolio.util.HolidayName.REPENTANCE_AND_PRAYER; import static name.abuchen.portfolio.util.HolidayName.REPUBLIC_PROCLAMATION_DAY; @@ -50,9 +59,15 @@ import static name.abuchen.portfolio.util.HolidayName.ROYAL_WEDDING; import static name.abuchen.portfolio.util.HolidayName.SAINT_PETER_PAUL; import static name.abuchen.portfolio.util.HolidayName.SAINT_STEPHEN; +import static name.abuchen.portfolio.util.HolidayName.SAVHUOT_EVE; import static name.abuchen.portfolio.util.HolidayName.SECOND_CHRISTMAS_DAY; +import static name.abuchen.portfolio.util.HolidayName.SHAVUOT_DAY; +import static name.abuchen.portfolio.util.HolidayName.SIMCHAT_TORA; +import static name.abuchen.portfolio.util.HolidayName.SIMCHAT_TORA_EVE; import static name.abuchen.portfolio.util.HolidayName.SPRING_MAY_BANK_HOLIDAY; import static name.abuchen.portfolio.util.HolidayName.STATE_FUNERAL; +import static name.abuchen.portfolio.util.HolidayName.SUKKOTH_DAY; +import static name.abuchen.portfolio.util.HolidayName.SUKKOTH_EVE; import static name.abuchen.portfolio.util.HolidayName.SUMMER_BANK_HOLIDAY; import static name.abuchen.portfolio.util.HolidayName.TERRORIST_ATTACKS; import static name.abuchen.portfolio.util.HolidayName.THANKSGIVING; @@ -64,8 +79,14 @@ import static name.abuchen.portfolio.util.HolidayName.VIRGIN_OF_CARMEN; import static name.abuchen.portfolio.util.HolidayName.WASHINGTONS_BIRTHDAY; import static name.abuchen.portfolio.util.HolidayName.WHIT_MONDAY; +import static name.abuchen.portfolio.util.HolidayName.YOM_KIPUR; +import static name.abuchen.portfolio.util.HolidayName.YOM_KIPUR_EVE; import static name.abuchen.portfolio.util.HolidayType.easter; import static name.abuchen.portfolio.util.HolidayType.fixed; +import static name.abuchen.portfolio.util.HolidayType.fixedJewishCalendar; +import static name.abuchen.portfolio.util.HolidayType.israeliIndependenceCalendar; +import static name.abuchen.portfolio.util.HolidayType.israeliMemorialCalendar; +import static name.abuchen.portfolio.util.HolidayType.jewishPurimCalendar; import static name.abuchen.portfolio.util.HolidayType.weekday; import java.text.MessageFormat; @@ -98,7 +119,7 @@ public class TradeCalendarManager static { - TradeCalendar tc = new TradeCalendar(MINIMAL_CALENDAR_CODE, Messages.LabelTradeCalendarDefault, + var tc = new TradeCalendar(MINIMAL_CALENDAR_CODE, Messages.LabelTradeCalendarDefault, STANDARD_WEEKEND, false); tc.add(fixed(NEW_YEAR, Month.JANUARY, 1)); tc.add(easter(GOOD_FRIDAY, -2)); @@ -151,7 +172,7 @@ public class TradeCalendarManager // one-time closings since 1990; see https://www.bcm-news.de/wp-content/uploads/closings-nyse.pdf // for a complete list from 1885 to 2011 tc.add(fixed(STATE_FUNERAL, Month.APRIL, 27).onlyIn(1994)); // funeral of former president Nixon - for (int d = 11; d <= 14; d++) + for (var d = 11; d <= 14; d++) tc.add(fixed(TERRORIST_ATTACKS, Month.SEPTEMBER, d).onlyIn(2001)); tc.add(fixed(STATE_FUNERAL, Month.JUNE, 11).onlyIn(2004)); // funeral of former president Reagan tc.add(fixed(STATE_FUNERAL, Month.JANUARY, 2).onlyIn(2007)); // funeral of former president Ford @@ -345,6 +366,39 @@ public class TradeCalendarManager tc.add(fixed(SECOND_CHRISTMAS_DAY, Month.DECEMBER, 26)); CACHE.put(tc.getCode(), tc); + + // Tel Aviv Stock Exchange starting 2026 + // https://www.tase.co.il/en/content/knowledge_center/trading_vacation_schedule#vacations + tc = new TradeCalendar("tlv", Messages.LabelTradeCalendarTLV, STANDARD_WEEKEND); //$NON-NLS-1$ + tc.add(fixedJewishCalendar(PASSOVER_I_EVE, 1, 14, 0)); + tc.add(fixedJewishCalendar(PASSOVER_I, 1, 15, 0)); + tc.add(fixedJewishCalendar(PASSOVER_II_EVE, 1, 20, 0)); + tc.add(fixedJewishCalendar(PASSOVER_II, 1, 21, 0)); + + tc.add(fixedJewishCalendar(JEWISH_NEW_YEAR_EVE, 6, 29, 0)); + tc.add(fixedJewishCalendar(JEWISH_NEW_YEAR_DAY_I, 6, 29, 1)); + tc.add(fixedJewishCalendar(JEWISH_NEW_YEAR_DAY_II, 6, 29, 2)); + + tc.add(fixedJewishCalendar(YOM_KIPUR_EVE, 7, 9, 0)); + tc.add(fixedJewishCalendar(YOM_KIPUR, 7, 9, 1)); + + tc.add(fixedJewishCalendar(JEWISH_FAST_DAY, 5, 9, 0)); + + tc.add(fixedJewishCalendar(SUKKOTH_EVE, 7, 14, 0)); + tc.add(fixedJewishCalendar(SUKKOTH_DAY, 7, 15, 0)); + + tc.add(fixedJewishCalendar(SIMCHAT_TORA_EVE, 7, 21, 0)); + tc.add(fixedJewishCalendar(SIMCHAT_TORA, 7, 22, 0)); + + tc.add(fixedJewishCalendar(SAVHUOT_EVE, 3, 5, 0)); + tc.add(fixedJewishCalendar(SHAVUOT_DAY, 3, 5, 1)); + + tc.add(israeliMemorialCalendar(MEMORIAL)); + tc.add(israeliIndependenceCalendar(INDEPENDENCE)); + tc.add(jewishPurimCalendar(PURIM, 0)); + + CACHE.put(tc.getCode(), tc); + tc = new TradeCalendar(FIRST_OF_THE_MONTH_CODE, Messages.LabelTradeCalendarFirstOfTheMonth, EnumSet.noneOf(DayOfWeek.class)) { @Override @@ -400,7 +454,7 @@ public static TradeCalendar getInstance(String calendarCode) public static TradeCalendar createInheritDefaultOption() { - String description = MessageFormat.format(Messages.LabelTradeCalendarUseDefault, + var description = MessageFormat.format(Messages.LabelTradeCalendarUseDefault, getDefaultInstance().getDescription()); return new TradeCalendar("", description, STANDARD_WEEKEND); //$NON-NLS-1$ } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names.properties index 85359cf308..fb3eb845d7 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names.properties +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names.properties @@ -64,6 +64,14 @@ INMACULATE_CONCEPTION = Immaculate Conception INTERNATION_WOMENS_DAY = International Women's Day +JEWISH_FAST_DAY = Tisha B'Av Fast + +JEWISH_NEW_YEAR_DAY_I = First Day of Rosh Hashanah + +JEWISH_NEW_YEAR_DAY_II = Second Day of Rosh Hashanah + +JEWISH_NEW_YEAR_EVE = Rosh Hashanah Eve + JUNETEENTH = Juneteenth KINGS_BIRTHDAY = King\u2019s Birthday @@ -84,8 +92,16 @@ NEW_YEARS_EVE = New Year's Eve NEW_YEAR_HOLIDAY = New Year Holiday +PASSOVER_EVE = Passover Eve + +PASSOVER_I = First Day of Passover + +PASSOVER_II = Second Day of Passover + PATRON_DAY = Patron's Day +PURIM = Purim + REFORMATION_DAY = Reformation Day REPENTANCE_AND_PRAYER = Day of Repentance and Prayer @@ -100,12 +116,24 @@ SAINT_PETER_PAUL = Saints Peter and Paul SAINT_STEPHEN = Saint Stephen's Day +SAVHUOT_EVE = Shavuot Eve + SECOND_CHRISTMAS_DAY = 2nd Christmas Day +SHAVUOT_DAY = Shavuot Day + +SIMCHAT_TORA = Shimchat Tora Day + +SIMCHAT_TORA_EVE = Simchat Tora Eve + SPRING_MAY_BANK_HOLIDAY = Spring May Bank Holiday STATE_FUNERAL = State funeral +SUKKOTH_DAY = Sukkot Day + +SUKKOTH_EVE = Sukkot Eve + SUMMER_BANK_HOLIDAY = Summer Bank Holiday TERRORIST_ATTACKS = September 11 attacks @@ -127,3 +155,7 @@ VIRGIN_OF_CARMEN = Virgen del Carmen WASHINGTONS_BIRTHDAY = Presidents' Day WHIT_MONDAY = Whit Monday + +YOM_KIPUR = Yum Kippur + +YOM_KIPUR_EVE = Yum Kippur Eve diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names_de.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names_de.properties index 94dfba9e5c..23b90f8b9f 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names_de.properties +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/util/holiday-names_de.properties @@ -102,6 +102,8 @@ SAINT_STEPHEN = Stephanstag SECOND_CHRISTMAS_DAY = 2. Weihnachtsfeiertag +SIMCHAT_TORA = Simchat Tora + SPRING_MAY_BANK_HOLIDAY = Fr\u00FChlingsfeiertag STATE_FUNERAL = Staatsbegr\u00E4bnis