using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using EnVisage.Code; using System.Globalization; using System.Web.Script.Serialization; namespace EnVisage.Models { #region Holiday item models public class HolidayModel : IBaseModel { #region Enums /// /// Status of the Calendar Settings record /// public enum HolidayStatus { /// /// NoValue = 0. /// [DisplayValue("No Value")] NoValue = 0, /// /// Inactive = 0. /// [DisplayValue("Inactive")] Inactive = 1, /// /// Active = 1. /// [DisplayValue("Active")] Active = 2, /// /// Inactive = 2. /// [DisplayValue("Future")] Future = 3, } public enum HolidayOccurrence { [DisplayValue("Each")] SameDayEveryYear = 0, [DisplayValue("First")] FirstDayOfWeek = 1, [DisplayValue("Second")] SecondDayOfWeek = 2, [DisplayValue("Third")] ThirdDayOfWeek = 3, [DisplayValue("Fourth")] FourthDayOfWeek = 4, [DisplayValue("Last")] LastDayOfWeek = 5 } #endregion #region Constructors public HolidayModel() { Id = Guid.Empty; HolidayGroupId = Guid.Empty; CompanyImpactAllResources = true; Status = HolidayStatus.NoValue; CreatedAt = DateTime.UtcNow; Teams = new List(); Resources = new List(); ExpenditureCategories = new List(); } #endregion #region Properties public Guid Id { get; set; } public Guid HolidayGroupId { get; set; } public string Options { get; set; } /// /// Gets or sets a name of the holiday. /// [Required] [DataType(DataType.Text)] [StringLength(50)] [Display(Name = "Name")] public string Name { get; set; } [Display(Name = "Start Date")] [DataType(DataType.Date), DisplayFormat(DataFormatString = "{0:M/d/yyyy}", ApplyFormatInEditMode = true)] [Required] public DateTime? StartDate { get; set; } [Display(Name = "End Date")] [DataType(DataType.Date), DisplayFormat(DataFormatString = "{0:M/d/yyyy}", ApplyFormatInEditMode = true)] [RequiredIf("MultipleDaysHoliday", true, ErrorMessage = "The End Date field is required.")] [DateGreaterThanOrEqual("StartDate", "End Date must be later or equal to Start Date", ValidateIfEmpty = false)] public DateTime? EndDate { get; set; } public bool MultipleDaysHoliday { get; set; } [Display(Name = "Business Unit Impact")] public bool CompanyImpactAllResources { get; set; } [Display(Name = "From Team(s)")] public List Teams { get; set; } [Display(Name = "Resource Names")] public List Resources { get; set; } [Display(Name = "Expenditure Categories")] public List ExpenditureCategories { get; set; } /// /// Gets or sets a value indicating whether a holiday is a working day or not. /// [Display(Name = "Working Days")] public bool WorkingDays { get; set; } [Required] [DataType(DataType.Date), DisplayFormat(DataFormatString = "{0:MM/dd/yy}", ApplyFormatInEditMode = true)] [Display(Name = "Effective Date of Change")] public DateTime? EffectiveChangeDate { get; set; } [DataType(DataType.Date), DisplayFormat(DataFormatString = "{0:MM/dd/yy}", ApplyFormatInEditMode = true)] [Display(Name = "Valid To")] public DateTime? ValidTo { get; set; } public DateTime CreatedAt { get; set; } [Display(Name = "Status")] public HolidayStatus Status { get; set; } public bool IsInclude { get; set; } public bool IncludexpenditureCategories { get; set; } protected HolidayBaseRecurrenceModel recurrences = null; public HolidayBaseRecurrenceModel Recurrences { get { if ((recurrences == null) && !String.IsNullOrEmpty(this.Options)) { JavaScriptSerializer serializer = new JavaScriptSerializer(); HolidayRecurrenceJsonModel jsonModel = (HolidayRecurrenceJsonModel)serializer.Deserialize(this.Options, typeof(HolidayRecurrenceJsonModel)); recurrences = jsonModel.Convert(); // Assign number of days in the Holiday recurrences.NumberOfDays = this.MultipleDaysHoliday && this.StartDate.HasValue && this.EndDate.HasValue ? Convert.ToInt32((this.EndDate.Value - this.StartDate.Value).TotalDays) + 1 : 1; } return recurrences; } } public List GetHolidayDates() { List result = new List(); if (this.StartDate.HasValue && this.EffectiveChangeDate.HasValue) { int daysCount = this.MultipleDaysHoliday ? Convert.ToInt32((this.EndDate.Value - this.StartDate.Value).TotalDays) + 1 : 1; if (String.IsNullOrEmpty(this.Options)) { // Non recurrentive holiday for (int index = 0; index < daysCount; index++) { DateTime currentDate = this.StartDate.Value.AddDays(index); if (currentDate >= this.EffectiveChangeDate.Value) result.Add(currentDate); } } else { // Recurrentive holiday. Calculate dates through recurrence result = this.Recurrences.GetDates(this.EffectiveChangeDate.Value, Constants.FISCAL_CALENDAR_MAX_DATE); } } return result; } #endregion #region Methods /// /// Determines whether the specified object is valid. /// /// The validation context. /// A collection that holds failed-validation information. public IEnumerable Validate(ValidationContext validationContext) { if (StartDate.HasValue && EndDate.HasValue && MultipleDaysHoliday && (StartDate > EndDate.Value)) { yield return new ValidationResult("End Date must be later or equal to Start Date", new[] { "EndDate" }); } } /// /// Casts a obect to the object of type . /// /// A object. /// A object filled with data from db. public static explicit operator HolidayModel(Holiday obj) { if (obj == null) return null; var model = new HolidayModel { Id = obj.Id, HolidayGroupId = obj.HolidayGroupId, Name = obj.Name, WorkingDays = obj.WorkingDays, EffectiveChangeDate = obj.EffectiveChangeDate, CreatedAt = obj.CreatedAt, Options = obj.Options ?? string.Empty, StartDate = obj.StartDate, EndDate = obj.EndDate, MultipleDaysHoliday = obj.EndDate.HasValue && (obj.StartDate != obj.EndDate.Value), CompanyImpactAllResources = obj.CompanyImpact, Teams = obj.Holiday2Team.Select(x => x.TeamId).ToList(), Resources = obj.Holiday2PeopleResource.Select(x => x.ResourceId).ToList(), ExpenditureCategories = obj.Holiday2ExpenditureCategory.Select(x => x.ExpenditureCategoryId).ToList(), IsInclude = obj.IsInclude }; model.TrimStringProperties(); return model; } /// /// Copies data from model to DAL object. /// /// A target DAL object. public void CopyTo(Holiday holiday) { if (holiday == null) throw new ArgumentNullException("holiday"); if (!EffectiveChangeDate.HasValue) throw new Exception("Effective Date of Change must be set"); holiday.Id = Id; holiday.HolidayGroupId = HolidayGroupId; holiday.Name = Name; holiday.WorkingDays = WorkingDays; holiday.EffectiveChangeDate = EffectiveChangeDate.Value; holiday.CreatedAt = CreatedAt; holiday.Options = string.IsNullOrEmpty(Options) ? null : Options; holiday.StartDate = StartDate.Value; holiday.EndDate = EndDate; holiday.CompanyImpact = CompanyImpactAllResources; holiday.IsInclude = IsInclude; } #endregion } /// /// Model for displaying Holidays in Grids /// public class HolidayDisplayModel { #region Properties public Guid Id { get; set; } public Guid HolidayGroupId { get; set; } /// /// Gets or sets a name of the holiday. /// [DataType(DataType.Text)] [Display(Name = "Name")] public string Name { get; set; } public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } [Display(Name = "Start Date")] public string StartDateText { get; set; } [Display(Name = "End Date")] public string EndDateText { get; set; } /// /// Gets or sets a value indicating whether a holiday is a working day or not. /// [Display(Name = "Working Days")] public bool WorkingDays { get; set; } /// /// Text description of the holiday recurrency rules /// [Display(Name = "Recurrences")] public string Recurrences { get; set; } public DateTime EffectiveChangeDate { get; set; } [Display(Name = "Effective Date of Change")] public string EffectiveChangeDateAsText { get; set; } public DateTime? ValidTo { get; set; } [Display(Name = "Valid To")] public string ValidToAsText { get; set; } public HolidayModel.HolidayStatus Status { get; set; } [Display(Name = "Status")] public string StatusText { get; set; } public bool IsMultidaysHoliday { get { return StartDate != EndDate; } } public bool IsInclude { get; set; } #endregion public static explicit operator HolidayDisplayModel(HolidayModel obj) { if (obj == null) return null; var model = new HolidayDisplayModel { Id = obj.Id, HolidayGroupId = obj.HolidayGroupId, Name = obj.Name, StartDate = obj.StartDate, StartDateText = obj.StartDate.HasValue ? obj.StartDate.Value.ToShortDateString() : String.Empty, EndDate = obj.EndDate, EndDateText = obj.EndDate.HasValue ? obj.EndDate.Value.ToShortDateString() : String.Empty, WorkingDays = obj.WorkingDays, EffectiveChangeDate = obj.EffectiveChangeDate.Value, EffectiveChangeDateAsText = obj.EffectiveChangeDate.Value.ToShortDateString(), ValidTo = obj.ValidTo, ValidToAsText = obj.ValidTo.HasValue ? obj.ValidTo.Value.ToShortDateString() : String.Empty, Status = obj.Status, StatusText = obj.Status.ToDisplayValue(), Recurrences = obj.Options, IsInclude = obj.IsInclude }; return model; } } public class HolidayListModel { public HolidayListModel() { Items = new List(); } public List Items { get; set; } } #endregion #region Holiday recurrence models /// /// Internal model for serialization / deserialization of recurrences JSON text /// /// Use Convert methods to convert it to suitable for data manipulation model public class HolidayRecurrenceJsonModel { public int periodOption; public int? dailyOption; public int? dailyDay; public int? weeklyWeeks; public bool? Sunday; public bool? Monday; public bool? Tuesday; public bool? Wednesday; public bool? Thursday; public bool? Friday; public bool? Saturday; public int? monthlyOption; public int? monthlyDate; public int? monthlyMonths; public string monthlyOccurrence; public string monthlyWeek; public int? monthlyMonth; public int? yearlyOption; public int? yearlyYear; public int? yearlyMonth; public int? yearlyMonthNumber; public string yearlyOccurrence; public string yearlyWeek; public int? yearlyMonth2; public string range_start_picker; public int? rangeOption; public int? Occurrences; public string range_picker; public long? StartDate; public long? EndDate; public HolidayBaseRecurrenceModel Convert() { // Create object of the appropriate type HolidayBaseRecurrenceModel result = null; switch (this.periodOption) { case 1: result = new HolidayDailyRecurrenceModel(); break; case 2: result = new HolidayWeeklyRecurrenceModel(); break; case 3: result = new HolidayMonthlyRecurrenceModel(); break; case 4: result = new HolidayYearlyRecurrenceModel(); break; } if (result != null) // Assign values to created object result.Assign(this); return result; } } /// /// Base model for different types of the Holiday recurrences /// public abstract class HolidayBaseRecurrenceModel { public enum RecurrencePeriod { None = 0, Daily = 1, Weekly = 2, Monthly = 3, Yearly = 4 } public enum RecurrenceLimits { NotLimited = 1, AmountOfRepeat = 2, EndDate = 3 } protected const string C_JSON_PARSE_ERROR_MESSAGE = "Error parsing of the holiday recurrence json: {0}"; protected const string C_DATES_CALCULATION_ERROR_MESSAGE = "Unable to calculate recurrence dates: {0}"; private static Dictionary periods = new Dictionary() { {1, RecurrencePeriod.Daily }, {2, RecurrencePeriod.Weekly }, {3, RecurrencePeriod.Monthly }, {4, RecurrencePeriod.Yearly } }; public int NumberOfDays = 1; public RecurrencePeriod Period = RecurrencePeriod.None; public RecurrenceLimits LimitType = RecurrenceLimits.NotLimited; public DateTime? StartDate; public DateTime? EndDate; public int? AmountOfRepeats; public virtual void Assign(HolidayRecurrenceJsonModel source) { string message; if (periods.ContainsKey(source.periodOption)) { this.Clear(); this.Period = periods[source.periodOption]; } else { message = String.Format("Recurrence type {0} is not supported", source.periodOption); message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, message); throw new NotSupportedException(message); } if (source.StartDate.HasValue) { this.StartDate = Utils.ConvertFromUnixDate(source.StartDate.Value).Date; } else { if (!String.IsNullOrEmpty(source.range_start_picker)) { DateTime parsedDate; if (DateTime.TryParseExact(source.range_start_picker, "M/d/yyyy", CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDate)) { this.StartDate = parsedDate; } } } // Parse iteration limits if (source.rangeOption.HasValue) { RecurrenceLimits limits; if (!Enum.TryParse(source.rangeOption.ToString(), out limits)) { message = String.Format("Type of the recurrence range limit {0} is not supported", source.rangeOption); message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, message); throw new NotSupportedException(message); } this.LimitType = limits; switch (this.LimitType) { case RecurrenceLimits.AmountOfRepeat: // Iterations limited by count if (source.Occurrences.HasValue) { this.AmountOfRepeats = source.Occurrences.Value; } break; case RecurrenceLimits.EndDate: // Iterations limited by dates if (source.EndDate.HasValue) { this.EndDate = Utils.ConvertFromUnixDate(source.EndDate.Value).Date; } else { if (!String.IsNullOrEmpty(source.range_picker)) { DateTime parsedDate; if (DateTime.TryParseExact(source.range_picker, "M/d/yyyy", CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDate)) { this.EndDate = parsedDate; } } } break; } } } public virtual void Clear() { this.Period = RecurrencePeriod.None; this.LimitType = RecurrenceLimits.NotLimited; this.StartDate = null; this.EndDate = null; this.AmountOfRepeats = null; } public abstract List GetDates(DateTime periodStart, DateTime periodEnd); protected void ValidateDates(DateTime periodStart, DateTime periodEnd) { if (periodStart > periodEnd) throw new ArgumentException("Period Starting date must be less or equal to Period end date"); if (!this.StartDate.HasValue) throw new Exception("Starting date of the recurrence must have value"); } protected DateTime GetNthDayInMonth(int year, int month, HolidayModel.HolidayOccurrence occurence, DayOfWeek dw) { string message; bool itemFound = false; DateTime occurenceDate = new DateTime(year, month, 1); if ((occurence == HolidayModel.HolidayOccurrence.FirstDayOfWeek) || (occurence == HolidayModel.HolidayOccurrence.SecondDayOfWeek) || (occurence == HolidayModel.HolidayOccurrence.ThirdDayOfWeek) || (occurence == HolidayModel.HolidayOccurrence.FourthDayOfWeek)) { // Looking Nth day of week in current month int counter = 0; while (!itemFound && (occurenceDate.Month == month)) { if (occurenceDate.DayOfWeek == dw) counter++; if (Convert.ToInt32(occurence) == counter) // Nth day of week found itemFound = true; else // Go to the next day to check occurenceDate = occurenceDate.AddDays(1); } } if (occurence == HolidayModel.HolidayOccurrence.LastDayOfWeek) { // Looking Nth day of week in current month occurenceDate = new DateTime(year, month, 1).AddMonths(1).AddDays(-1); while (!itemFound) { while (!itemFound && (occurenceDate.Month == month)) { itemFound = (occurenceDate.DayOfWeek == dw); if (!itemFound) // Go to the prev day to check occurenceDate = occurenceDate.AddDays(-1); } } } if (!itemFound) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Occurence of the holiday not found. Check the parameter 'EveryNthDayOfWeek' has correct value"); throw new Exception(message); } return occurenceDate; } /// /// Returns DateTime object by specified YMD. If params are not valid, returns the last day ot the month. /// E.g. for 2016, 04, 31 returns 2016-04-30 /// /// protected DateTime GetValidDateByParts(int year, int month, int day) { if ((year < Constants.FISCAL_CALENDAR_MIN_DATE.Year) || (year > (Constants.FISCAL_CALENDAR_MAX_DATE.Year + 1))) throw new ArgumentException("year"); if ((month < 1) || (month > 12)) throw new ArgumentException("month"); if ((day < 1) || (day > 31)) throw new ArgumentException("day"); if (day < 29) return new DateTime(year, month, day); DateTime tmpDate = new DateTime(year, month, 1); int daysInMonths = tmpDate.AddMonths(1).AddDays(-1).Day; if (day > daysInMonths) return new DateTime(year, month, daysInMonths); else return new DateTime(year, month, day); } /// /// Adds specified amount of months to date and force sets the specified Day part of the date /// protected DateTime AddMonths(DateTime srcDate, int monthCount, int forceSetDay) { DateTime newDate = srcDate.AddMonths(monthCount); return GetValidDateByParts(newDate.Year, newDate.Month, forceSetDay); } /// /// Adds specified amount of years to date and force sets the specified Day part of the date /// protected DateTime AddYears(DateTime srcDate, int yearCount, int forceSetDay) { DateTime newDate = srcDate.AddYears(yearCount); return GetValidDateByParts(newDate.Year, newDate.Month, forceSetDay); } /// /// Returns date, which iterations are limited in future /// protected DateTime GetEndRecurrencesDate(DateTime calcRangeEndDate) { DateTime limit = calcRangeEndDate; if (this.LimitType == RecurrenceLimits.EndDate) { if (!this.EndDate.HasValue) { string message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Parameter 'EndDate' must have value"); throw new Exception(message); } if (this.EndDate.Value < calcRangeEndDate) limit = this.EndDate.Value; } return limit; } protected int GetMaxRecurrencesAmount() { string message; if (this.LimitType == RecurrenceLimits.AmountOfRepeat) { if (!this.AmountOfRepeats.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Parameter 'AmountOfRepeats' must have value"); throw new Exception(message); } if (this.AmountOfRepeats.Value < 1) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Parameter 'AmountOfRepeats' must have a positive value"); throw new Exception(message); } return this.AmountOfRepeats.Value; } else return Int32.MaxValue; } protected void AddDatesToResult(List result, DateTime date, int daysInHoliday) { for (int index = 0; index < daysInHoliday; index++) { DateTime dateToAdd = date.AddDays(index); result.Add(dateToAdd); } } } /// /// Model for daily recurrence of a Holiday /// public class HolidayDailyRecurrenceModel : HolidayBaseRecurrenceModel { public int? EveryNdays; public bool EveryWeekday; public override void Clear() { base.Clear(); this.EveryNdays = null; this.EveryWeekday = false; } public override void Assign(HolidayRecurrenceJsonModel source) { string message; base.Assign(source); if (!source.dailyOption.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'dailyOption' must have value"); throw new Exception(message); } switch (source.dailyOption.Value) { case 1: // Repeat daily, every N days if (!source.dailyDay.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'dailyDay' must have value"); throw new Exception(message); } this.EveryNdays = source.dailyDay.Value; this.EveryWeekday = false; break; case 2: // Repeat daily, every weekday this.EveryNdays = 1; this.EveryWeekday = true; break; default: message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'dailyDay' has unsupported value"); throw new NotSupportedException(message); } } public override List GetDates(DateTime periodStart, DateTime periodEnd) { // Note: The method calculates holiday occurence dates from startingPoint and to the future // up to periodEnd. // If startingPoint > periodStart, dates are calculated from startingPoint to periodEnd. // If startingPoint < periodStart, dates are calculated from periodStart to periodEnd. string message; List result = new List(); this.ValidateDates(periodStart, periodEnd); if (this.EveryWeekday) { // SA: Option in the UI form is hidden. Code left here for the case, that option may be set visible later throw new NotImplementedException("Daily weekday recurrence is not implemented"); } else { if (!this.EveryNdays.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNdays' is not set"); throw new Exception(message); } if (this.EveryNdays.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNdays' must be a positive number"); throw new Exception(message); } int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; DateTime baseDate = this.StartDate.Value; DateTime periodStartCorrected = periodStart.Date; DateTime periodEndCorrected = this.GetEndRecurrencesDate(periodEnd.Date).AddDays(1); DateTime currentDate = baseDate; while ((counter < iterationsCount) && (currentDate < periodEndCorrected)) { if (currentDate >= periodStartCorrected) { this.AddDatesToResult(result, currentDate, this.NumberOfDays); counter++; } currentDate = currentDate.AddDays(this.EveryNdays.Value); } } return result.Distinct().ToList(); } } /// /// Model for weekly recurrence of a Holiday /// public class HolidayWeeklyRecurrenceModel : HolidayBaseRecurrenceModel { public int? EveryNweeks; public List WeekDays; public override void Clear() { base.Clear(); this.EveryNweeks = null; this.WeekDays = null; } public override void Assign(HolidayRecurrenceJsonModel source) { string message; base.Assign(source); if (!source.weeklyWeeks.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'weeklyWeeks' must have value"); throw new Exception(message); } this.EveryNweeks = source.weeklyWeeks.Value; this.WeekDays = new List(); if (source.Sunday.HasValue && source.Sunday.Value) this.WeekDays.Add(DayOfWeek.Sunday); if (source.Monday.HasValue && source.Monday.Value) this.WeekDays.Add(DayOfWeek.Monday); if (source.Tuesday.HasValue && source.Tuesday.Value) this.WeekDays.Add(DayOfWeek.Tuesday); if (source.Wednesday.HasValue && source.Wednesday.Value) this.WeekDays.Add(DayOfWeek.Wednesday); if (source.Thursday.HasValue && source.Thursday.Value) this.WeekDays.Add(DayOfWeek.Thursday); if (source.Friday.HasValue && source.Friday.Value) this.WeekDays.Add(DayOfWeek.Friday); if (source.Saturday.HasValue && source.Saturday.Value) this.WeekDays.Add(DayOfWeek.Saturday); } public override List GetDates(DateTime periodStart, DateTime periodEnd) { // Note: The method calculates holiday occurence dates from startingPoint and to the future // up to periodEnd. // If startingPoint > periodStart, dates are calculated from startingPoint to periodEnd. // If startingPoint < periodStart, dates are calculated from periodStart to periodEnd. string message; List result = new List(); this.ValidateDates(periodStart, periodEnd); if (!this.EveryNweeks.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNweeks' is not set"); throw new Exception(message); } if (this.EveryNweeks.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNweeks' must be a positive number"); throw new Exception(message); } if ((this.WeekDays == null) || (this.WeekDays.Count < 1)) // Week days for recurrence are not set return result; int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; DateTime baseDate = this.StartDate.Value; DateTime periodStartCorrected = periodStart.Date; DateTime periodEndCorrected = this.GetEndRecurrencesDate(periodEnd.Date).AddDays(1); DateTime currentDate; DayOfWeek currentDayOfWeek; // Add holiday in the specified Start and End Dates for (int index = 0; index < this.NumberOfDays; index++) { currentDate = baseDate.AddDays(index); if ((currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) result.Add(currentDate); } // Get first day (Sun) of the week, Starting date belongs to currentDate = baseDate; while (currentDate.DayOfWeek != DayOfWeek.Sunday) currentDate = currentDate.AddDays(-1); // Skip specified amount of weeks after initial holiday (Start to End Dates) DateTime firstWeekDay = currentDate.AddDays(7 * this.EveryNweeks.Value); // Calculate other holiday dates through recurrence while ((counter < iterationsCount) && (currentDate < periodEndCorrected)) { // Get holidays from current week by specified days of week currentDate = firstWeekDay; currentDayOfWeek = currentDate.DayOfWeek; while ((counter < iterationsCount) && ((currentDate == firstWeekDay) || (currentDayOfWeek != DayOfWeek.Sunday))) { if (this.WeekDays.Contains(currentDayOfWeek) && (currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) { this.AddDatesToResult(result, currentDate, this.NumberOfDays); counter++; } currentDate = currentDate.AddDays(1); currentDayOfWeek = currentDate.DayOfWeek; } // Skip specified amount of weeks firstWeekDay = firstWeekDay.AddDays(7 * this.EveryNweeks.Value); } return result.Distinct().ToList(); } } /// /// Model for monthly recurrence of a Holiday /// public class HolidayMonthlyRecurrenceModel : HolidayBaseRecurrenceModel { public bool EveryNdateOfNmonths; public bool EveryXweekdayOfNmonths; public byte? EveryNdate; public byte? EveryNmonth; public HolidayModel.HolidayOccurrence? EveryNthDayOfWeek; public DayOfWeek? EveryDayOfWeek; public override void Assign(HolidayRecurrenceJsonModel source) { string message; base.Assign(source); if (!source.monthlyOption.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyOption' must have value"); throw new Exception(message); } switch (source.monthlyOption.Value) { case 1: // Repeat monthly, date N of every N month(s) if (!source.monthlyDate.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyDate' must have value"); throw new Exception(message); } if (!source.monthlyMonths.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyMonths' must have value"); throw new Exception(message); } this.EveryNdateOfNmonths = true; this.EveryNdate = (byte)source.monthlyDate.Value; this.EveryNmonth = (byte)source.monthlyMonths.Value; break; case 2: // Repeat monthly, the First Monday of every N month if (String.IsNullOrEmpty(source.monthlyOccurrence)) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyOccurrence' must have value"); throw new Exception(message); } if (String.IsNullOrEmpty(source.monthlyWeek)) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyWeek' must have value"); throw new Exception(message); } if (!source.monthlyMonth.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyMonth' must have value"); throw new Exception(message); } this.EveryXweekdayOfNmonths = true; try { HolidayModel.HolidayOccurrence monthlyOccurrenceParsed = (HolidayModel.HolidayOccurrence)Enum.Parse(typeof(HolidayModel.HolidayOccurrence), source.monthlyOccurrence, true); this.EveryNthDayOfWeek = monthlyOccurrenceParsed; } catch { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyOccurrence' has unsupported value"); throw new NotSupportedException(message); } try { DayOfWeek monthlyWeekParsed = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), source.monthlyWeek, true); this.EveryDayOfWeek = monthlyWeekParsed; } catch { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyWeek' has unsupported value"); throw new NotSupportedException(message); } this.EveryNmonth = (byte)source.monthlyMonth.Value; break; default: message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'monthlyOption' has unsupported value"); throw new NotSupportedException(message); } } public override List GetDates(DateTime periodStart, DateTime periodEnd) { // Note: The method calculates holiday occurence dates from startingPoint and to the future // up to periodEnd. // If startingPoint > periodStart, dates are calculated from startingPoint to periodEnd. // If startingPoint < periodStart, dates are calculated from periodStart to periodEnd. this.ValidateDates(periodStart, periodEnd); List result = new List(); DateTime periodStartCorrected = periodStart.Date; DateTime periodEndCorrected = this.GetEndRecurrencesDate(periodEnd.Date).AddDays(1); DateTime baseDate = this.StartDate.Value; // Add holiday in the specified Start and End Dates for (int index = 0; index < this.NumberOfDays; index++) { DateTime currentDate = baseDate.AddDays(index); if ((currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) result.Add(currentDate); } if (this.EveryNdateOfNmonths) { GetDatesForNdateNMonths(periodStartCorrected, periodEndCorrected, result); return result.Distinct().ToList(); } if (this.EveryXweekdayOfNmonths) { GetDatesForXweekdayNMonths(periodStartCorrected, periodEndCorrected, result); } return result.Distinct().ToList(); } protected void GetDatesForNdateNMonths(DateTime periodStartCorrected, DateTime periodEndCorrected, List result) { #region Validation string message; if (!this.EveryNdate.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNdate' is not set"); throw new Exception(message); } if (!this.EveryNmonth.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNmonth' is not set"); throw new Exception(message); } if (this.EveryNdate.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNdate' must be a positive number"); throw new Exception(message); } if (this.EveryNmonth.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNmonth' must be a positive number"); throw new Exception(message); } #endregion int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; // Calculate start date for recurrence DateTime currentDate = this.StartDate.Value; // Skip specified amount of months currentDate = this.AddMonths(currentDate, this.EveryNmonth.Value, this.EveryNdate.Value); //if (this.StartDate.Value.Day > this.EveryNdate.Value) // currentDate = this.AddMonths(currentDate, 1, this.EveryNdate.Value); // Calculate other holiday dates through recurrence while ((counter < iterationsCount) && (currentDate < periodEndCorrected)) { if ((currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) { this.AddDatesToResult(result, currentDate, this.NumberOfDays); counter++; } currentDate = this.AddMonths(currentDate, this.EveryNmonth.Value, this.EveryNdate.Value); } } protected void GetDatesForXweekdayNMonths(DateTime periodStartCorrected, DateTime periodEndCorrected, List result) { #region Validation string message; if (!this.EveryNthDayOfWeek.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNthDayOfWeek' is not set"); throw new Exception(message); } if (!this.EveryDayOfWeek.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryDayOfWeek' is not set"); throw new Exception(message); } if (!this.EveryNmonth.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNmonth' is not set"); throw new Exception(message); } if (this.EveryNmonth.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNmonth' must be a positive number"); throw new Exception(message); } #endregion int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; // Calculate start date for recurrence DateTime currentDate = this.StartDate.Value; //DateTime currentDate = // this.GetValidDateByParts(this.StartDate.Value.Year, this.StartDate.Value.Month, this.EveryNdate.Value); // Skip specified amount of months currentDate = this.AddMonths(currentDate, this.EveryNmonth.Value, 1); // Go through other dates via recurrence // Looking for the first holiday occurence in month, the starting point belongs to DateTime occurenceDate = this.GetNthDayInMonth(currentDate.Year, currentDate.Month, this.EveryNthDayOfWeek.Value, this.EveryDayOfWeek.Value); while ((counter < iterationsCount) && (occurenceDate < periodEndCorrected)) { if (occurenceDate >= periodStartCorrected) { this.AddDatesToResult(result, occurenceDate, this.NumberOfDays); counter++; } DateTime nextOccurenceDate = this.AddMonths(occurenceDate, this.EveryNmonth.Value, occurenceDate.Day); occurenceDate = this.GetNthDayInMonth(nextOccurenceDate.Year, nextOccurenceDate.Month, this.EveryNthDayOfWeek.Value, this.EveryDayOfWeek.Value); } } } /// /// Model for yearly recurrence of a Holiday /// public class HolidayYearlyRecurrenceModel : HolidayBaseRecurrenceModel { public int EveryNyears; public byte? EveryDateOrdinalNum; public byte? EveryMonthOrdinalNum; public HolidayModel.HolidayOccurrence? EveryNthDayOfWeek; public DayOfWeek? EveryDayOfWeek; public override void Clear() { base.Clear(); this.EveryNyears = 0; this.EveryDateOrdinalNum = null; this.EveryMonthOrdinalNum = null; this.EveryNthDayOfWeek = null; this.EveryDayOfWeek = null; } public override void Assign(HolidayRecurrenceJsonModel source) { string message; base.Assign(source); if (!source.yearlyYear.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyYear' must have value"); throw new Exception(message); } if (!source.yearlyOption.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyOption' must have value"); throw new Exception(message); } this.EveryNyears = source.yearlyYear.Value; switch (source.yearlyOption.Value) { case 1: if (!source.yearlyMonth.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyMonth' must have value"); throw new Exception(message); } if (!source.yearlyMonthNumber.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyMonthNumber' must have value"); throw new Exception(message); } this.EveryMonthOrdinalNum = (byte)source.yearlyMonth.Value; this.EveryDateOrdinalNum = (byte)source.yearlyMonthNumber.Value; break; case 2: if (String.IsNullOrEmpty(source.yearlyOccurrence)) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyOccurrence' must have value"); throw new Exception(message); } if (String.IsNullOrEmpty(source.yearlyWeek)) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyWeek' must have value"); throw new Exception(message); } if (!source.yearlyMonth2.HasValue) { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyMonth2' must have value"); throw new Exception(message); } try { HolidayModel.HolidayOccurrence yearlyOccurrenceParsed = (HolidayModel.HolidayOccurrence)Enum.Parse(typeof(HolidayModel.HolidayOccurrence), source.yearlyOccurrence, true); this.EveryNthDayOfWeek = yearlyOccurrenceParsed; } catch { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyOccurrence' has unsupported value"); throw new NotSupportedException(message); } try { DayOfWeek yearlyWeekParsed = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), source.yearlyWeek, true); this.EveryDayOfWeek = yearlyWeekParsed; } catch { message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyWeek' has unsupported value"); throw new NotSupportedException(message); } this.EveryMonthOrdinalNum = (byte)source.yearlyMonth2.Value; break; default: message = String.Format(C_JSON_PARSE_ERROR_MESSAGE, "Key 'yearlyOption' has unsupported value"); throw new NotSupportedException(message); } } public override List GetDates(DateTime periodStart, DateTime periodEnd) { // Note: The method calculates holiday occurence dates from startingPoint and to the future // up to periodEnd. // If startingPoint > periodStart, dates are calculated from startingPoint to periodEnd. // If startingPoint < periodStart, dates are calculated from periodStart to periodEnd. this.ValidateDates(periodStart, periodEnd); string message; if (this.EveryNyears <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNyears' must be a positive number"); throw new Exception(message); } List result = new List(); DateTime periodStartCorrected = periodStart.Date; DateTime periodEndCorrected = this.GetEndRecurrencesDate(periodEnd.Date).AddDays(1); DateTime baseDate = this.StartDate.Value; // Add holiday in the specified Start and End Dates for (int index = 0; index < this.NumberOfDays; index++) { DateTime currentDate = baseDate.AddDays(index); if ((currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) result.Add(currentDate); } if (this.EveryDateOrdinalNum.HasValue && EveryMonthOrdinalNum.HasValue) { GetDatesForNdateInMonth(periodStartCorrected, periodEndCorrected, result); return result.Distinct().ToList(); } if (this.EveryNthDayOfWeek.HasValue && EveryDayOfWeek.HasValue && EveryMonthOrdinalNum.HasValue) { GetDatesForXweekdayInMonth(periodStartCorrected, periodEndCorrected, result); } return result.Distinct().ToList(); } protected void GetDatesForNdateInMonth(DateTime periodStartCorrected, DateTime periodEndCorrected, List result) { #region Validation string message; if (!this.EveryDateOrdinalNum.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryDateOrdinalNum' is not set"); throw new Exception(message); } if (!this.EveryMonthOrdinalNum.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryMonthOrdinalNum' is not set"); throw new Exception(message); } if (this.EveryDateOrdinalNum.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryDateOrdinalNum' must be a positive number"); throw new Exception(message); } if (this.EveryMonthOrdinalNum.Value <= 0) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryMonthOrdinalNum' must be a positive number"); throw new Exception(message); } #endregion int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; // Skip specified number of months form start date DateTime currentDate = this.GetValidDateByParts(this.StartDate.Value.Year, this.EveryMonthOrdinalNum.Value, this.EveryDateOrdinalNum.Value); currentDate = this.AddYears(currentDate, this.EveryNyears, this.EveryDateOrdinalNum.Value); // Calculate other holidays through recurrence reules while ((counter < iterationsCount) && (currentDate < periodEndCorrected)) { if ((currentDate >= periodStartCorrected) && (currentDate < periodEndCorrected)) { this.AddDatesToResult(result, currentDate, this.NumberOfDays); counter++; } currentDate = this.AddYears(currentDate, this.EveryNyears, this.EveryDateOrdinalNum.Value); } } protected void GetDatesForXweekdayInMonth(DateTime periodStartCorrected, DateTime periodEndCorrected, List result) { #region Validation string message; if (!this.EveryNthDayOfWeek.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryNthDayOfWeek' is not set"); throw new Exception(message); } if (!this.EveryDayOfWeek.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryDayOfWeek' is not set"); throw new Exception(message); } if (!this.EveryMonthOrdinalNum.HasValue) { message = String.Format(C_DATES_CALCULATION_ERROR_MESSAGE, "Parameter 'EveryMonthOrdinalNum' is not set"); throw new Exception(message); } #endregion int iterationsCount = this.GetMaxRecurrencesAmount(); int counter = 0; // Skip time from Starting Date for Holiday DateTime occurenceDate = this.StartDate.Value; occurenceDate = occurenceDate.AddYears(this.EveryNyears); // Looking for the first holiday occurence in month, the starting point belongs to occurenceDate = this.GetNthDayInMonth(occurenceDate.Year, this.EveryMonthOrdinalNum.Value, this.EveryNthDayOfWeek.Value, this.EveryDayOfWeek.Value); while ((counter < iterationsCount) && (occurenceDate < periodEndCorrected)) { if (occurenceDate >= periodStartCorrected) { this.AddDatesToResult(result, occurenceDate, this.NumberOfDays); counter++; } DateTime nextOccurenceDate = occurenceDate.AddYears(this.EveryNyears); occurenceDate = this.GetNthDayInMonth(nextOccurenceDate.Year, nextOccurenceDate.Month, this.EveryNthDayOfWeek.Value, this.EveryDayOfWeek.Value); } } } #endregion }