/*
* Copyright (C) 2017 Wouter Dullaert
*
* 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.
*/
package com.wdullaer.materialdatetimepicker.date;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.wdullaer.materialdatetimepicker.Utils;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.TimeZone;
import java.util.TreeSet;
class DefaultDateRangeLimiter implements DateRangeLimiter {
private static final int DEFAULT_START_YEAR = 1900;
private static final int DEFAULT_END_YEAR = 2100;
private transient DatePickerController mController;
private int mMinYear = DEFAULT_START_YEAR;
private int mMaxYear = DEFAULT_END_YEAR;
private Calendar mMinDate;
private Calendar mMaxDate;
private TreeSet<Calendar> selectableDays = new TreeSet<>();
private HashSet<Calendar> disabledDays = new HashSet<>();
DefaultDateRangeLimiter() {}
@SuppressWarnings({"unchecked", "WeakerAccess"})
public DefaultDateRangeLimiter(Parcel in) {
mMinYear = in.readInt();
mMaxYear = in.readInt();
mMinDate = (Calendar) in.readSerializable();
mMaxDate = (Calendar) in.readSerializable();
selectableDays = (TreeSet<Calendar>) in.readSerializable();
disabledDays = (HashSet<Calendar>) in.readSerializable();
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mMinYear);
out.writeInt(mMaxYear);
out.writeSerializable(mMinDate);
out.writeSerializable(mMaxDate);
out.writeSerializable(selectableDays);
out.writeSerializable(disabledDays);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<DefaultDateRangeLimiter> CREATOR
= new Parcelable.Creator<DefaultDateRangeLimiter>() {
public DefaultDateRangeLimiter createFromParcel(Parcel in) {
return new DefaultDateRangeLimiter(in);
}
public DefaultDateRangeLimiter[] newArray(int size) {
return new DefaultDateRangeLimiter[size];
}
};
void setSelectableDays(@NonNull Calendar[] days) {
for (Calendar selectableDay : days) Utils.trimToMidnight(selectableDay);
this.selectableDays.addAll(Arrays.asList(days));
}
void setDisabledDays(@NonNull Calendar[] days) {
for (Calendar disabledDay : days) Utils.trimToMidnight(disabledDay);
this.disabledDays.addAll(Arrays.asList(days));
}
void setMinDate(@NonNull Calendar calendar) {
mMinDate = Utils.trimToMidnight((Calendar) calendar.clone());
}
void setMaxDate(@NonNull Calendar calendar) {
mMaxDate = Utils.trimToMidnight((Calendar) calendar.clone());
}
void setController(@NonNull DatePickerController controller) {
mController = controller;
}
void setYearRange(int startYear, int endYear) {
if (endYear < startYear) {
throw new IllegalArgumentException("Year end must be larger than or equal to year start");
}
mMinYear = startYear;
mMaxYear = endYear;
}
@Nullable Calendar getMinDate() {
return mMinDate;
}
@Nullable Calendar getMaxDate() {
return mMaxDate;
}
@Nullable Calendar[] getSelectableDays() {
return selectableDays.isEmpty() ? null : selectableDays.toArray(new Calendar[0]);
}
@Nullable Calendar[] getDisabledDays() {
return disabledDays.isEmpty() ? null : disabledDays.toArray(new Calendar[0]);
}
@Override
public int getMinYear() {
if (!selectableDays.isEmpty()) return selectableDays.first().get(Calendar.YEAR);
// Ensure no years can be selected outside of the given minimum date
return mMinDate != null && mMinDate.get(Calendar.YEAR) > mMinYear ? mMinDate.get(Calendar.YEAR) : mMinYear;
}
@Override
public int getMaxYear() {
if (!selectableDays.isEmpty()) return selectableDays.last().get(Calendar.YEAR);
// Ensure no years can be selected outside of the given maximum date
return mMaxDate != null && mMaxDate.get(Calendar.YEAR) < mMaxYear ? mMaxDate.get(Calendar.YEAR) : mMaxYear;
}
@Override
public @NonNull Calendar getStartDate() {
if (!selectableDays.isEmpty()) return (Calendar) selectableDays.first().clone();
if (mMinDate != null) return (Calendar) mMinDate.clone();
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
Calendar output = Calendar.getInstance(timeZone);
output.set(Calendar.YEAR, mMinYear);
output.set(Calendar.DAY_OF_MONTH, 1);
output.set(Calendar.MONTH, Calendar.JANUARY);
return output;
}
@Override
public @NonNull Calendar getEndDate() {
if (!selectableDays.isEmpty()) return (Calendar) selectableDays.last().clone();
if (mMaxDate != null) return (Calendar) mMaxDate.clone();
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
Calendar output = Calendar.getInstance(timeZone);
output.set(Calendar.YEAR, mMaxYear);
output.set(Calendar.DAY_OF_MONTH, 31);
output.set(Calendar.MONTH, Calendar.DECEMBER);
return output;
}
/**
* @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate.
* If one or either have not been set, they are considered as Integer.MIN_VALUE and
* Integer.MAX_VALUE.
*/
@Override
public boolean isOutOfRange(int year, int month, int day) {
Calendar date = Calendar.getInstance();
date.set(Calendar.YEAR, year);
date.set(Calendar.MONTH, month);
date.set(Calendar.DAY_OF_MONTH, day);
return isOutOfRange(date);
}
private boolean isOutOfRange(@NonNull Calendar calendar) {
Utils.trimToMidnight(calendar);
return isDisabled(calendar) || !isSelectable(calendar);
}
private boolean isDisabled(@NonNull Calendar c) {
return disabledDays.contains(Utils.trimToMidnight(c)) || isBeforeMin(c) || isAfterMax(c);
}
private boolean isSelectable(@NonNull Calendar c) {
return selectableDays.isEmpty() || selectableDays.contains(Utils.trimToMidnight(c));
}
private boolean isBeforeMin(@NonNull Calendar calendar) {
return mMinDate != null && calendar.before(mMinDate) || calendar.get(Calendar.YEAR) < mMinYear;
}
private boolean isAfterMax(@NonNull Calendar calendar) {
return mMaxDate != null && calendar.after(mMaxDate) || calendar.get(Calendar.YEAR) > mMaxYear;
}
public @NonNull Calendar setToNearestDate(@NonNull Calendar calendar) {
if (!selectableDays.isEmpty()) {
Calendar newCalendar = null;
Calendar higher = selectableDays.ceiling(calendar);
Calendar lower = selectableDays.lower(calendar);
if (higher == null && lower != null) newCalendar = lower;
else if (lower == null && higher != null) newCalendar = higher;
if (newCalendar != null || higher == null) {
newCalendar = newCalendar == null ? calendar : newCalendar;
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
newCalendar.setTimeZone(timeZone);
return (Calendar) newCalendar.clone();
}
long highDistance = Math.abs(higher.getTimeInMillis() - calendar.getTimeInMillis());
long lowDistance = Math.abs(calendar.getTimeInMillis() - lower.getTimeInMillis());
if (lowDistance < highDistance) return (Calendar) lower.clone();
else return (Calendar) higher.clone();
}
if (!disabledDays.isEmpty()) {
Calendar forwardDate = isBeforeMin(calendar) ? getStartDate() : (Calendar) calendar.clone();
Calendar backwardDate = isAfterMax(calendar) ? getEndDate() : (Calendar) calendar.clone();
while (isDisabled(forwardDate) && isDisabled(backwardDate)) {
forwardDate.add(Calendar.DAY_OF_MONTH, 1);
backwardDate.add(Calendar.DAY_OF_MONTH, -1);
}
if (!isDisabled(backwardDate)) {
return backwardDate;
}
if (!isDisabled(forwardDate)) {
return forwardDate;
}
}
if (mMinDate != null && isBeforeMin(calendar)) {
return (Calendar) mMinDate.clone();
}
if (mMaxDate != null && isAfterMax(calendar)) {
return (Calendar) mMaxDate.clone();
}
return calendar;
}
}