From fd156c48380c85cdcd987827fa54dd872fdcef12 Mon Sep 17 00:00:00 2001 From: armirveliaj Date: Wed, 6 Nov 2024 10:34:48 -0500 Subject: [PATCH] Revise GetNextStartTime method --- engine/action_plan.go | 55 +++++++++++++-------- engine/action_plan_test.go | 98 ++++++++++++++++++++++++++++++++------ utils/coreutils_test.go | 95 ++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 35 deletions(-) diff --git a/engine/action_plan.go b/engine/action_plan.go index b890954c5..407024584 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -117,35 +117,48 @@ func getDayOrEndOfMonth(day int, t1 time.Time) int { return day } -func (at *ActionTiming) GetNextStartTime(t1 time.Time) (t time.Time) { +func (at *ActionTiming) GetNextStartTime(refTime time.Time) time.Time { if !at.stCache.IsZero() { return at.stCache } - i := at.Timing - if i == nil || i.Timing == nil { - return + rateIvl := at.Timing + if rateIvl == nil || rateIvl.Timing == nil { + return time.Time{} } // Normalize - if i.Timing.StartTime == "" { - i.Timing.StartTime = "00:00:00" + if rateIvl.Timing.StartTime == "" { + rateIvl.Timing.StartTime = "00:00:00" } - if len(i.Timing.Years) > 0 && len(i.Timing.Months) == 0 { - i.Timing.Months = append(i.Timing.Months, 1) + if len(rateIvl.Timing.Years) > 0 && len(rateIvl.Timing.Months) == 0 { + rateIvl.Timing.Months = append(rateIvl.Timing.Months, 1) } - if len(i.Timing.Months) > 0 && len(i.Timing.MonthDays) == 0 { - i.Timing.MonthDays = append(i.Timing.MonthDays, 1) + if len(rateIvl.Timing.Months) > 0 && len(rateIvl.Timing.MonthDays) == 0 { + rateIvl.Timing.MonthDays = append(rateIvl.Timing.MonthDays, 1) } - at.stCache = cronexpr.MustParse(i.Timing.CronString()).Next(t1) - if i.Timing.ID == utils.MetaMonthlyEstimated { - // substract a month from at.stCache only if we skip 2 months - // or we skip a month because mentioned MonthDay is after the last day of the current month - if at.stCache.Month() == t1.Month()+2 || - (utils.GetEndOfMonth(t1).Day() < at.Timing.Timing.MonthDays[0] && - at.stCache.Month() == t1.Month()+1) { - lastDay := utils.GetEndOfMonth(at.stCache).Day() - // only change the time if the new one is after t1 - if tmp := at.stCache.AddDate(0, 0, -lastDay); tmp.After(t1) { - at.stCache = tmp + at.stCache = cronexpr.MustParse(rateIvl.Timing.CronString()).Next(refTime) + if rateIvl.Timing.ID == utils.MetaMonthlyEstimated { + // When target day doesn't exist in a month, fall back to that month's last day + // instead of skipping to next occurrence. + currentMonth := refTime.Month() + targetMonthDay := rateIvl.Timing.MonthDays[0] + oneMonthSkip := utils.GetEndOfMonth(refTime).Day() < targetMonthDay && + at.stCache.Month() == currentMonth+1 + twoMonthSkip := at.stCache.Month() == currentMonth+2 + if oneMonthSkip || twoMonthSkip { + daysToSubtract := utils.GetEndOfMonth(at.stCache).Day() + + // When transitioning from Jan to Feb, subtract the + // actual desired day instead of Mar's last day. + // This fixes cases like: + // - Jan 29 -> Mar 29 (should be Feb 28 in non-leap years) + // - Jan 30 -> Mar 30 (should be Feb 28/29) + if currentMonth == time.January { + daysToSubtract = targetMonthDay + } + + adjustedTime := at.stCache.AddDate(0, 0, -daysToSubtract) + if adjustedTime.After(refTime) { + at.stCache = adjustedTime } } } diff --git a/engine/action_plan_test.go b/engine/action_plan_test.go index 7003037d5..3de1f53e4 100644 --- a/engine/action_plan_test.go +++ b/engine/action_plan_test.go @@ -516,20 +516,19 @@ func TestActionTimingGetNextStartTimesMonthlyEstimated(t *testing.T) { }, expected: time.Date(2021, 1, 31, 14, 25, 0, 0, time.UTC), }, - // { - // name: "Non-Leap Year: January 29 to Feb 28", - // t1: time.Date(2021, 1, 29, 0, 0, 0, 0, time.UTC), - // at: &ActionTiming{ - // Timing: &RateInterval{ - // Timing: &RITiming{ - // ID: utils.MetaMonthlyEstimated, - // MonthDays: utils.MonthDays{29}, - // }, - // }, - // }, - // expected: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), - // }, - + { + name: "Non-Leap Year: January 29 to Feb 28", + t1: time.Date(2021, 1, 29, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{29}, + }, + }, + }, + expected: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), + }, { name: "Non-Leap Year: February 28 to March 28", t1: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), @@ -622,6 +621,77 @@ func TestActionTimingGetNextStartTimesMonthlyEstimated(t *testing.T) { }, expected: time.Date(2021, 9, 30, 0, 0, 0, 0, time.UTC), }, + + { + name: "Non-Leap Year: Jan 29 to Feb 28", + t1: time.Date(2021, 1, 29, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{29}, + StartTime: "00:00:00", + }, + }, + }, + expected: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), + }, + { + name: "Leap Year: Jan 30 to Feb 29 ", + t1: time.Date(2020, 1, 30, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{30}, + StartTime: "00:00:00", + }, + }, + }, + expected: time.Date(2020, 2, 29, 0, 0, 0, 0, time.UTC), + }, + { + name: "Non-Leap Year: Jan 30 to Feb 28 ", + t1: time.Date(2021, 1, 30, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{30}, + StartTime: "00:00:00", + }, + }, + }, + expected: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), + }, + { + name: "Non-Leap Year: Jan 15 to Feb 15 ", + t1: time.Date(2021, 1, 15, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{15}, + StartTime: "00:00:00", + }, + }, + }, + expected: time.Date(2021, 2, 15, 0, 0, 0, 0, time.UTC), + }, + { + name: "Jan 14 to Jan 15 ", + t1: time.Date(2021, 1, 14, 0, 0, 0, 0, time.UTC), + at: &ActionTiming{ + Timing: &RateInterval{ + Timing: &RITiming{ + ID: utils.MetaMonthlyEstimated, + MonthDays: utils.MonthDays{15}, + StartTime: "00:00:00", + }, + }, + }, + expected: time.Date(2021, 1, 15, 0, 0, 0, 0, time.UTC), + }, } for _, tt := range tests { diff --git a/utils/coreutils_test.go b/utils/coreutils_test.go index 35429d13d..5422b53b8 100644 --- a/utils/coreutils_test.go +++ b/utils/coreutils_test.go @@ -1915,3 +1915,98 @@ func TestCoreutilsParseHierarchyPath(t *testing.T) { } } } + +func TestMonthlyEstimatedCases(t *testing.T) { + tests := []struct { + name string + t1 time.Time + expected time.Time + }{ + { + name: "Non-Leap Year: January 31 to February 28", + t1: time.Date(2021, 1, 31, 0, 0, 0, 0, time.UTC), + expected: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), + }, + { + name: "Leap Year: January 29 to February 29", + t1: time.Date(2020, 1, 29, 0, 0, 0, 0, time.UTC), + expected: time.Date(2020, 2, 29, 0, 0, 0, 0, time.UTC), + }, + + { + name: "Non-Leap Year: February 28 to March 28", + t1: time.Date(2021, 2, 28, 0, 0, 0, 0, time.UTC), + expected: time.Date(2021, 3, 28, 0, 0, 0, 0, time.UTC), + }, + { + name: "October 31 to November 30", + t1: time.Date(2021, 10, 31, 0, 0, 0, 0, time.UTC), + expected: time.Date(2021, 11, 30, 0, 0, 0, 0, time.UTC), + }, + { + name: "November 30 to December 30", + t1: time.Date(2021, 11, 30, 0, 0, 0, 0, time.UTC), + expected: time.Date(2021, 12, 30, 0, 0, 0, 0, time.UTC), + }, + { + name: "November 25 to December 25", + t1: time.Date(2021, 11, 25, 0, 0, 0, 0, time.UTC), + expected: time.Date(2021, 12, 25, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := monthlyEstimated(tt.t1) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !got.Equal(tt.expected) { + t.Errorf("Expected: %v, got: %v", tt.expected, got) + } + }) + } +} + +func TestGetEndOfMonth(t *testing.T) { + tests := []struct { + name string + ref time.Time + expected time.Time + }{ + { + name: "End of January (non-leap year)", + ref: time.Date(2023, time.January, 15, 0, 0, 0, 0, time.UTC), + expected: time.Date(2023, time.January, 31, 23, 59, 59, 0, time.UTC), + }, + { + name: "End of February (non-leap year)", + ref: time.Date(2023, time.February, 10, 0, 0, 0, 0, time.UTC), + expected: time.Date(2023, time.February, 28, 23, 59, 59, 0, time.UTC), + }, + { + name: "End of February (leap year)", + ref: time.Date(2024, time.February, 10, 0, 0, 0, 0, time.UTC), + expected: time.Date(2024, time.February, 29, 23, 59, 59, 0, time.UTC), + }, + { + name: "End of December", + ref: time.Date(2023, time.December, 25, 0, 0, 0, 0, time.UTC), + expected: time.Date(2023, time.December, 31, 23, 59, 59, 0, time.UTC), + }, + { + name: "End of April ", + ref: time.Date(2023, time.April, 15, 0, 0, 0, 0, time.UTC), + expected: time.Date(2023, time.April, 30, 23, 59, 59, 0, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetEndOfMonth(tt.ref) + if !result.Equal(tt.expected) { + t.Errorf("GetEndOfMonth(%v) = %v, want %v", tt.ref, result, tt.expected) + } + }) + } +}