/*
* Copyright (c) 2015 Ngewi Fet <ngewif@gmail.com>
*
* 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 org.gnucash.android.model;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.NonNull;
import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.ui.util.RecurrenceParser;
import org.joda.time.Days;
import org.joda.time.Hours;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.Months;
import org.joda.time.ReadablePeriod;
import org.joda.time.Weeks;
import org.joda.time.Years;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Model for recurrences in the database
* <p>Basically a wrapper around {@link PeriodType}</p>
*/
public class Recurrence extends BaseModel {
private PeriodType mPeriodType;
/**
* Start time of the recurrence
*/
private Timestamp mPeriodStart;
/**
* End time of this recurrence
* <p>This value is not persisted to the database</p>
*/
private Timestamp mPeriodEnd;
/**
* Days of week on which to run the recurrence
*/
private List<Integer> mByDays = Collections.emptyList();
private int mMultiplier = 1; //multiplier for the period type
public Recurrence(@NonNull PeriodType periodType){
setPeriodType(periodType);
mPeriodStart = new Timestamp(System.currentTimeMillis());
}
/**
* Return the PeriodType for this recurrence
* @return PeriodType for the recurrence
*/
public PeriodType getPeriodType() {
return mPeriodType;
}
/**
* Sets the period type for the recurrence
* @param periodType PeriodType
*/
public void setPeriodType(PeriodType periodType) {
this.mPeriodType = periodType;
}
/**
* Return the start time for this recurrence
* @return Timestamp of start of recurrence
*/
public Timestamp getPeriodStart() {
return mPeriodStart;
}
/**
* Set the start time of this recurrence
* @param periodStart {@link Timestamp} of recurrence
*/
public void setPeriodStart(Timestamp periodStart) {
this.mPeriodStart = periodStart;
}
/**
* Returns an approximate period for this recurrence
* <p>The period is approximate because months do not all have the same number of days,
* but that is assumed</p>
* @return Milliseconds since Epoch representing the period
* @deprecated Do not use in new code. Uses fixed period values for months and years (which have variable units of time)
*/
public long getPeriod(){
long baseMillis = 0;
switch (mPeriodType){
case HOUR:
baseMillis = RecurrenceParser.HOUR_MILLIS;
break;
case DAY:
baseMillis = RecurrenceParser.DAY_MILLIS;
break;
case WEEK:
baseMillis = RecurrenceParser.WEEK_MILLIS;
break;
case MONTH:
baseMillis = RecurrenceParser.MONTH_MILLIS;
break;
case YEAR:
baseMillis = RecurrenceParser.YEAR_MILLIS;
break;
}
return mMultiplier * baseMillis;
}
/**
* Returns the event schedule (start, end and recurrence)
* @return String description of repeat schedule
*/
public String getRepeatString(){
StringBuilder repeatBuilder = new StringBuilder(getFrequencyRepeatString());
Context context = GnuCashApplication.getAppContext();
String dayOfWeek = new SimpleDateFormat("EEEE", GnuCashApplication.getDefaultLocale())
.format(new Date(mPeriodStart.getTime()));
if (mPeriodType == PeriodType.WEEK) {
repeatBuilder.append(" ").
append(context.getString(R.string.repeat_on_weekday, dayOfWeek));
}
if (mPeriodEnd != null){
String endDateString = SimpleDateFormat.getDateInstance().format(new Date(mPeriodEnd.getTime()));
repeatBuilder.append(", ").append(context.getString(R.string.repeat_until_date, endDateString));
}
return repeatBuilder.toString();
}
/**
* Creates an RFC 2445 string which describes this recurring event.
* <p>See http://recurrance.sourceforge.net/</p>
* <p>The output of this method is not meant for human consumption</p>
* @return String describing event
*/
public String getRuleString(){
String separator = ";";
StringBuilder ruleBuilder = new StringBuilder();
// =======================================================================
//This section complies with the formal rules, but the betterpickers library doesn't like/need it
// SimpleDateFormat startDateFormat = new SimpleDateFormat("'TZID'=zzzz':'yyyyMMdd'T'HHmmss", Locale.US);
// ruleBuilder.append("DTSTART;");
// ruleBuilder.append(startDateFormat.format(new Date(mStartDate)));
// ruleBuilder.append("\n");
// ruleBuilder.append("RRULE:");
// ========================================================================
ruleBuilder.append("FREQ=").append(mPeriodType.getFrequencyDescription()).append(separator);
ruleBuilder.append("INTERVAL=").append(mMultiplier).append(separator);
if (getCount() > 0)
ruleBuilder.append("COUNT=").append(getCount()).append(separator);
ruleBuilder.append(mPeriodType.getByParts(mPeriodStart.getTime())).append(separator);
return ruleBuilder.toString();
}
/**
* Return the number of days left in this period
* @return Number of days left in period
*/
public int getDaysLeftInCurrentPeriod(){
LocalDateTime startDate = new LocalDateTime(System.currentTimeMillis());
int interval = mMultiplier - 1;
LocalDateTime endDate = null;
switch (mPeriodType){
case HOUR:
endDate = new LocalDateTime(System.currentTimeMillis()).plusHours(interval);
break;
case DAY:
endDate = new LocalDateTime(System.currentTimeMillis()).plusDays(interval);
break;
case WEEK:
endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(interval);
break;
case MONTH:
endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(interval);
break;
case YEAR:
endDate = startDate.dayOfYear().withMaximumValue().plusYears(interval);
break;
}
return Days.daysBetween(startDate, endDate).getDays();
}
/**
* Returns the number of periods from the start date of this recurrence until the end of the
* interval multiplier specified in the {@link PeriodType}
* //fixme: Improve the documentation
* @return Number of periods in this recurrence
*/
public int getNumberOfPeriods(int numberOfPeriods) {
LocalDateTime startDate = new LocalDateTime(mPeriodStart.getTime());
LocalDateTime endDate;
int interval = mMultiplier;
//// TODO: 15.08.2016 Why do we add the number of periods. maybe rename method or param
switch (mPeriodType){
case HOUR: //this is not the droid you are looking for
endDate = startDate.plusHours(numberOfPeriods);
return Hours.hoursBetween(startDate, endDate).getHours();
case DAY:
endDate = startDate.plusDays(numberOfPeriods);
return Days.daysBetween(startDate, endDate).getDays();
case WEEK:
endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(numberOfPeriods);
return Weeks.weeksBetween(startDate, endDate).getWeeks() / interval;
case MONTH:
endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(numberOfPeriods);
return Months.monthsBetween(startDate, endDate).getMonths() / interval;
case YEAR:
endDate = startDate.dayOfYear().withMaximumValue().plusYears(numberOfPeriods);
return Years.yearsBetween(startDate, endDate).getYears() / interval;
}
return 0;
}
/**
* Return the name of the current period
* @return String of current period
*/
public String getTextOfCurrentPeriod(int periodNum){
LocalDate startDate = new LocalDate(mPeriodStart.getTime());
switch (mPeriodType){
case HOUR:
//nothing to see here. Just use default period designation
break;
case DAY:
return startDate.dayOfWeek().getAsText();
case WEEK:
return startDate.weekOfWeekyear().getAsText();
case MONTH:
return startDate.monthOfYear().getAsText();
case YEAR:
return startDate.year().getAsText();
}
return "Period " + periodNum;
}
/**
* Return the days of week on which to run the recurrence.
*
* <p>Days are expressed as defined in {@link java.util.Calendar}.
* For example, Calendar.MONDAY</p>
*
* @return list of days of week on which to run the recurrence.
*/
public @NonNull List<Integer> getByDays(){
return Collections.unmodifiableList(mByDays);
}
/**
* Sets the days on which to run the recurrence.
*
* <p>Days must be expressed as defined in {@link java.util.Calendar}.
* For example, Calendar.MONDAY</p>
*
* @param byDays list of days of week on which to run the recurrence.
*/
public void setByDays(@NonNull List<Integer> byDays){
mByDays = new ArrayList<>(byDays);
}
/**
* Computes the number of occurrences of this recurrences between start and end date
* <p>If there is no end date or the PeriodType is unknown, it returns -1</p>
* @return Number of occurrences, or -1 if there is no end date
*/
public int getCount(){
if (mPeriodEnd == null)
return -1;
int multiple = mMultiplier;
ReadablePeriod jodaPeriod;
switch (mPeriodType){
case HOUR:
jodaPeriod = Hours.hours(multiple);
break;
case DAY:
jodaPeriod = Days.days(multiple);
break;
case WEEK:
jodaPeriod = Weeks.weeks(multiple);
break;
case MONTH:
jodaPeriod = Months.months(multiple);
break;
case YEAR:
jodaPeriod = Years.years(multiple);
break;
default:
jodaPeriod = Months.months(multiple);
}
int count = 0;
LocalDateTime startTime = new LocalDateTime(mPeriodStart.getTime());
while (startTime.toDateTime().getMillis() < mPeriodEnd.getTime()){
++count;
startTime = startTime.plus(jodaPeriod);
}
return count;
/*
//this solution does not use looping, but is not very accurate
int multiplier = mMultiplier;
LocalDateTime startDate = new LocalDateTime(mPeriodStart.getTime());
LocalDateTime endDate = new LocalDateTime(mPeriodEnd.getTime());
switch (mPeriodType){
case DAY:
return Days.daysBetween(startDate, endDate).dividedBy(multiplier).getDays();
case WEEK:
return Weeks.weeksBetween(startDate, endDate).dividedBy(multiplier).getWeeks();
case MONTH:
return Months.monthsBetween(startDate, endDate).dividedBy(multiplier).getMonths();
case YEAR:
return Years.yearsBetween(startDate, endDate).dividedBy(multiplier).getYears();
default:
return -1;
}
*/
}
/**
* Sets the end time of this recurrence by specifying the number of occurences
* @param numberOfOccurences Number of occurences from the start time
*/
public void setPeriodEnd(int numberOfOccurences){
LocalDateTime localDate = new LocalDateTime(mPeriodStart.getTime());
LocalDateTime endDate;
int occurrenceDuration = numberOfOccurences * mMultiplier;
switch (mPeriodType){
case HOUR:
endDate = localDate.plusHours(occurrenceDuration);
break;
case DAY:
endDate = localDate.plusDays(occurrenceDuration);
break;
case WEEK:
endDate = localDate.plusWeeks(occurrenceDuration);
break;
default:
case MONTH:
endDate = localDate.plusMonths(occurrenceDuration);
break;
case YEAR:
endDate = localDate.plusYears(occurrenceDuration);
break;
}
mPeriodEnd = new Timestamp(endDate.toDateTime().getMillis());
}
/**
* Return the end date of the period in milliseconds
* @return End date of the recurrence period
*/
public Timestamp getPeriodEnd(){
return mPeriodEnd;
}
/**
* Set period end date
* @param endTimestamp End time in milliseconds
*/
public void setPeriodEnd(Timestamp endTimestamp){
mPeriodEnd = endTimestamp;
}
/**
* Returns the multiplier for the period type. The default multiplier is 1.
* e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2.
*
* @return Multiplier for the period type
*/
public int getMultiplier(){
return mMultiplier;
}
/**
* Sets the multiplier for the period type.
* e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2.
*
* @param multiplier Multiplier for the period type
*/
public void setMultiplier(int multiplier){
mMultiplier = multiplier;
}
/**
* Returns a localized string describing the period type's frequency.
*
* @return String describing the period type
*/
private String getFrequencyRepeatString(){
Resources res = GnuCashApplication.getAppContext().getResources();
switch (mPeriodType) {
case HOUR:
return res.getQuantityString(R.plurals.label_every_x_hours, mMultiplier, mMultiplier);
case DAY:
return res.getQuantityString(R.plurals.label_every_x_days, mMultiplier, mMultiplier);
case WEEK:
return res.getQuantityString(R.plurals.label_every_x_weeks, mMultiplier, mMultiplier);
case MONTH:
return res.getQuantityString(R.plurals.label_every_x_months, mMultiplier, mMultiplier);
case YEAR:
return res.getQuantityString(R.plurals.label_every_x_years, mMultiplier, mMultiplier);
default:
return "";
}
}
/**
* Returns a new {@link Recurrence} with the {@link PeriodType} specified in the old format.
*
* @param period Period in milliseconds since Epoch (old format to define a period)
* @return Recurrence with the specified period.
*/
public static Recurrence fromLegacyPeriod(long period) {
int result = (int) (period/RecurrenceParser.YEAR_MILLIS);
if (result > 0) {
Recurrence recurrence = new Recurrence(PeriodType.YEAR);
recurrence.setMultiplier(result);
return recurrence;
}
result = (int) (period/RecurrenceParser.MONTH_MILLIS);
if (result > 0) {
Recurrence recurrence = new Recurrence(PeriodType.MONTH);
recurrence.setMultiplier(result);
return recurrence;
}
result = (int) (period/RecurrenceParser.WEEK_MILLIS);
if (result > 0) {
Recurrence recurrence = new Recurrence(PeriodType.WEEK);
recurrence.setMultiplier(result);
return recurrence;
}
result = (int) (period/RecurrenceParser.DAY_MILLIS);
if (result > 0) {
Recurrence recurrence = new Recurrence(PeriodType.DAY);
recurrence.setMultiplier(result);
return recurrence;
}
result = (int) (period/RecurrenceParser.HOUR_MILLIS);
if (result > 0) {
Recurrence recurrence = new Recurrence(PeriodType.HOUR);
recurrence.setMultiplier(result);
return recurrence;
}
return new Recurrence(PeriodType.DAY);
}
}