Skip to content

Commit

Permalink
WIP: attempt to support non-gregorian calendars
Browse files Browse the repository at this point in the history
See #20
  • Loading branch information
javier-godoy committed Oct 12, 2022
1 parent 9553ed1 commit 207b8b2
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 15 deletions.
134 changes: 121 additions & 13 deletions src/main/java/com/flowingcode/addons/ycalendar/MonthCalendar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
Expand All @@ -39,7 +48,20 @@
@SuppressWarnings("serial")
public class MonthCalendar extends AbstractCalendarComponent<MonthCalendar> 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<LocalDate, String> classNameGenerator;

Expand All @@ -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}.
* <p>
* 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
Expand All @@ -91,10 +170,11 @@ public void setClassNameGenerator(ValueProvider<LocalDate, String> 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;
}
Expand All @@ -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;
}
Expand All @@ -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<ValueRange> 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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export class MonthCalendarMixin extends ShadowFocusMixin(ThemableMixin(PolymerEl
value: false
},

_chronology: {
type: Object
},

i18n: {
type: Object
},
Expand Down Expand Up @@ -123,6 +127,9 @@ export class MonthCalendarMixin extends ShadowFocusMixin(ThemableMixin(PolymerEl
</vaadin-month-calendar>
`;}

ready() {
super.ready();
}

}

Original file line number Diff line number Diff line change
@@ -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);
}

}
23 changes: 21 additions & 2 deletions src/test/java/com/flowingcode/addons/ycalendar/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}

}

0 comments on commit 207b8b2

Please sign in to comment.