diff --git a/src/main/java/com/flowingcode/addons/ycalendar/MonthCalendar.java b/src/main/java/com/flowingcode/addons/ycalendar/MonthCalendar.java index 4c147e5..016adb3 100644 --- a/src/main/java/com/flowingcode/addons/ycalendar/MonthCalendar.java +++ b/src/main/java/com/flowingcode/addons/ycalendar/MonthCalendar.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,16 @@ import java.time.LocalDate; import java.time.Month; import java.time.YearMonth; +import java.time.chrono.ChronoLocalDate; +import java.time.chrono.Chronology; +import java.time.chrono.IsoChronology; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.ValueRange; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.IntStream; @Tag("fc-month-calendar") @@ -39,7 +48,20 @@ @SuppressWarnings("serial") public class MonthCalendar extends AbstractCalendarComponent implements HasSize, HasTheme { - private YearMonth yearMonth; + // current yearMonth in ISO Chronology + // This is NOT the equivalent date in the ISO calendar, but a date in some month with the same + // "shape" (i.e. a month that starts on the same week-day and has the same number of days). + // For instance, Dhul Hijjah 1442 is a 29-day month starting on Sunday, 11 July 2021. + // The corresponding isoYearMonth is February 2004 (a 29-day month starting on Sunday) + private YearMonth isoYearMonth; + + // current yearMonth in the selected Chronology + private YearMonth chronoYearMonth; + + // start of the current month in the selected Chronology + private ChronoLocalDate chronoLocalDate; + + private Chronology chronology = IsoChronology.INSTANCE; private ValueProvider classNameGenerator; @@ -53,29 +75,86 @@ public MonthCalendar(YearMonth yearMonth) { setYearMonth(yearMonth); } + /** + * Creates a new instance of {@code MonthCalendar} for the year, month and calendar system given + * by a {@code ChronoLocalDate}. + *

+ * This implementation requires that the {@code chronology} has 12-month year and 7-day weeks, and + * that all months have between 28 and 31 days. + */ + public MonthCalendar(ChronoLocalDate chronoLocalDate) { + chronology = chronoLocalDate.getChronology(); + validateChronology(chronology); + setYearMonth(YearMonth.of(chronoLocalDate.get(ChronoField.YEAR), + chronoLocalDate.get(ChronoField.MONTH_OF_YEAR))); + } + public void setReadOnly(boolean readOnly) { getElement().setAttribute("readonly", readOnly); } /** Returns the year-and-month that is displayed in this calendar. */ public YearMonth getYearMonth() { - return yearMonth; + return chronoYearMonth; } /** Returns the month-of-year that is displayed in this calendar. */ public Month getMonth() { - return yearMonth.getMonth(); + return chronoYearMonth.getMonth(); } /** Sets the year-and-month that is displayed in this calendar. */ public void setYearMonth(YearMonth yearMonth) { - this.yearMonth = Objects.requireNonNull(yearMonth); - this.yearMonth = Objects.requireNonNull(yearMonth); + chronoYearMonth = Objects.requireNonNull(yearMonth); + String script = "this.month=new Date($0,$1-1,1);"; - getElement().executeJs(script, yearMonth.getYear(), yearMonth.getMonthValue()); + int year = chronoYearMonth.get(ChronoField.YEAR); + int month = chronoYearMonth.get(ChronoField.MONTH_OF_YEAR); + chronoLocalDate = chronology.date(year, month, 1); + + if (chronology instanceof IsoChronology) { + // for ISO chronology use the default implementation of vaadin-month-calendar + script = "this._chronology={}; " + script; + getElement().executeJs(script, year, month); + isoYearMonth = chronoYearMonth; + } else { + isoYearMonth = findIsoYearMonth(chronoLocalDate); + + int isoYear = isoYearMonth.get(ChronoField.YEAR); + int isoMonth = isoYearMonth.get(ChronoField.MONTH_OF_YEAR); + + script = "this._chronology={year:$2, month:$3}; " + script; + getElement().executeJs(script, isoYear, isoMonth, year, month); + } + refreshAll(); } - + + private static int getDaysInMonth(Temporal t) { + return t.with(TemporalAdjusters.lastDayOfMonth()).get(ChronoField.DAY_OF_MONTH); + } + + private static YearMonth findIsoYearMonth(ChronoLocalDate chronoDate) { + // find a month that starts in the same day of week as the chronoDate + // and has the same number of days in month + + Integer dayOfWeek = chronoDate.get(ChronoField.DAY_OF_WEEK); + Integer daysInMonth = (int) chronoDate.range(ChronoField.DAY_OF_MONTH).getMaximum(); + + LocalDate date = LocalDate.of(2000, 2, 1); + if (daysInMonth == 29) { + while (date.get(ChronoField.DAY_OF_WEEK) != dayOfWeek) { + date = date.plusYears(4); + } + } else { + while (date.get(ChronoField.DAY_OF_WEEK) != dayOfWeek + || getDaysInMonth(date) != daysInMonth) { + date = date.plusMonths(1); + } + } + return YearMonth.from(date); + } + /** * Sets the function that is used for generating CSS class names for rows in this calendar. * Returning {@code null} from the generator results in no custom class name being set. Multiple @@ -91,10 +170,11 @@ public void setClassNameGenerator(ValueProvider classNameGene */ @Override public void refreshAll() { - IntStream.rangeClosed(1, yearMonth.lengthOfMonth()).forEach(dayOfMonth -> { + IntStream.rangeClosed(1, chronoYearMonth.lengthOfMonth()).forEach(dayOfMonth -> { String className; if (classNameGenerator != null) { - className = classNameGenerator.apply(yearMonth.atDay(dayOfMonth)); + className = classNameGenerator + .apply(LocalDate.from(chronoLocalDate.plus(dayOfMonth - 1, ChronoUnit.DAYS))); } else { className = null; } @@ -108,10 +188,12 @@ public void refreshAll() { * @throws IllegalArgumentException if the displayed year-and-month does not contain {@code date}. */ public void refreshItem(LocalDate date) { - if (date.getYear() == yearMonth.getYear() && date.getMonth() == yearMonth.getMonth()) { + if (date.getYear() == chronoYearMonth.getYear() + && date.getMonth() == chronoYearMonth.getMonth()) { String className; if (classNameGenerator != null) { - className = classNameGenerator.apply(date); + LocalDate isoDate = isoYearMonth.atDay(date.getDayOfMonth()); + className = classNameGenerator.apply(isoDate); } else { className = null; } @@ -125,4 +207,30 @@ private void setStyleForDay(int dayOfMonth, String className) { getElement().executeJs("setTimeout(()=>{this._setStyleForDay($0,$1);})", dayOfMonth, className); } + + private void validate(Chronology chronology, ChronoField field, Predicate predicate) { + ValueRange range = chronology.range(field); + if (!predicate.test(range)) { + throw new IllegalArgumentException( + String.format("Unsupported Chronology %s (%s out of range)", chronology, field)); + } + } + + private void validateChronology(Chronology chronology) { + if (chronology != null) { + // Check that all weeks have 7 days + validate(chronology, ChronoField.DAY_OF_WEEK, range -> range.getMaximum() == 7); + validate(chronology, ChronoField.DAY_OF_WEEK, range -> range.getSmallestMaximum() == 7); + validate(chronology, ChronoField.DAY_OF_WEEK, range -> range.getLargestMinimum() == 1); + // Check that all years have 12 months + validate(chronology, ChronoField.MONTH_OF_YEAR, range -> range.getMaximum() == 12); + validate(chronology, ChronoField.MONTH_OF_YEAR, range -> range.getSmallestMaximum() == 12); + validate(chronology, ChronoField.MONTH_OF_YEAR, range -> range.getLargestMinimum() == 1); + // Check that all months have between 28 and 31 days + validate(chronology, ChronoField.DAY_OF_MONTH, range -> range.getMaximum() <= 31); + validate(chronology, ChronoField.DAY_OF_MONTH, range -> range.getSmallestMaximum() >= 28); + validate(chronology, ChronoField.DAY_OF_MONTH, range -> range.getLargestMinimum() == 1); + } + } + } diff --git a/src/main/resources/META-INF/frontend/fc-month-calendar/month-calendar-mixin.js b/src/main/resources/META-INF/frontend/fc-month-calendar/month-calendar-mixin.js index b2a5ebd..57656ed 100644 --- a/src/main/resources/META-INF/frontend/fc-month-calendar/month-calendar-mixin.js +++ b/src/main/resources/META-INF/frontend/fc-month-calendar/month-calendar-mixin.js @@ -56,6 +56,10 @@ export class MonthCalendarMixin extends ShadowFocusMixin(ThemableMixin(PolymerEl value: false }, + _chronology: { + type: Object + }, + i18n: { type: Object }, @@ -123,6 +127,9 @@ export class MonthCalendarMixin extends ShadowFocusMixin(ThemableMixin(PolymerEl `;} + ready() { + super.ready(); + } } diff --git a/src/test/java/com/flowingcode/addons/ycalendar/MonthDemoHijrah.java b/src/test/java/com/flowingcode/addons/ycalendar/MonthDemoHijrah.java new file mode 100644 index 0000000..d516bb7 --- /dev/null +++ b/src/test/java/com/flowingcode/addons/ycalendar/MonthDemoHijrah.java @@ -0,0 +1,69 @@ +/*- + * #%L + * Year Month Calendar Add-on + * %% + * Copyright (C) 2021 - 2022 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.addons.ycalendar; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.time.DayOfWeek; +import java.time.chrono.HijrahChronology; +import java.util.Arrays; + +@DemoSource +@PageTitle("Month") +@CssImport(value = "./styles/test_year-month-calendar.css", themeFor = "vaadin-month-calendar") +@Route(value = "year-month-calendar/month-hijrah", layout = YearMonthCalendarDemoView.class) +public class MonthDemoHijrah extends Div { + + public MonthDemoHijrah() { + + MonthCalendar calendar = new MonthCalendar(HijrahChronology.INSTANCE.dateNow()); + + calendar.setClassNameGenerator(date -> { + if (TestUtils.isHijrahHoliday(date)) { + return "holiday"; + } + if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { + return "weekend"; + } + return null; + }); + + DatePicker.DatePickerI18n i18n = new DatePicker.DatePickerI18n(); + i18n.setMonthNames(Arrays.asList("al-Muḥarram", "Ṣafar", "Rabīʿ al-ʾAwwal", "Rabīʿ ath-Thānī", + "Rabīʿ al-ʾĀkhir", "Jumādā al-ʾŪlā", "Jumādā ath-Thāniyah", "Jumādā al-ʾĀkhirah", "Rajab", + "Shaʿbān", "Ramaḍān", "Shawwāl", "Ḏū al-Qaʿdah", "Ḏū al-Ḥijjah")); + // calendar.setI18n(i18n); + + Span selectedDate = new Span(); + calendar.addDateSelectedListener(ev -> { + selectedDate.setText("Selected date: " + ev.getDate()); + }); + + Span instructions = new Span("Use arrow keys to move."); + add(new HorizontalLayout(instructions, selectedDate), calendar); + } + +} diff --git a/src/test/java/com/flowingcode/addons/ycalendar/TestUtils.java b/src/test/java/com/flowingcode/addons/ycalendar/TestUtils.java index 8da9929..0a7059e 100644 --- a/src/test/java/com/flowingcode/addons/ycalendar/TestUtils.java +++ b/src/test/java/com/flowingcode/addons/ycalendar/TestUtils.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,6 +20,8 @@ package com.flowingcode.addons.ycalendar; import java.time.LocalDate; +import java.time.chrono.HijrahDate; +import java.time.temporal.ChronoField; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -85,4 +87,21 @@ public static boolean isPublicHoliday(LocalDate date) { } } + public static boolean isHijrahHoliday(LocalDate date) { + HijrahDate hdate = HijrahDate.from(date); + switch (hdate.get(ChronoField.MONTH_OF_YEAR)) { + case 1: + // 1st Muharram + return hdate.get(ChronoField.DAY_OF_MONTH) == 1; + case 10: + // 1st Shawwal + return hdate.get(ChronoField.DAY_OF_MONTH) == 1; + case 12: + // 10 Dhul Hijjah + return hdate.get(ChronoField.DAY_OF_MONTH) == 10; + default: + return false; + } + } + }