package upc import ( "time" lp "mgit.msbls.de/m/paliad/pkg/litigationplanner" ) // SnapshotHoliday is the embedded holiday row shape. Mirrors // paliad.holidays + the generator's output. Country and Regime are // optional pointers — at least one of them is non-empty on every // row (matches paliad's CHECK). type SnapshotHoliday struct { Date string `json:"date"` // YYYY-MM-DD Name string `json:"name"` Country *string `json:"country,omitempty"` Regime *string `json:"regime,omitempty"` State *string `json:"state,omitempty"` HolidayType string `json:"holiday_type"` } func (h SnapshotHoliday) appliesTo(country, regime string) bool { if h.Country != nil && country != "" && *h.Country == country { return true } if h.Regime != nil && regime != "" && *h.Regime == regime { return true } return false } func (h SnapshotHoliday) isVacation() bool { return h.HolidayType == "vacation" } func (h SnapshotHoliday) isClosure() bool { return h.HolidayType == "closure" } // SnapshotHolidayCalendar serves HolidayCalendar against the embedded // holiday slice. The semantics mirror paliad's HolidayService: // // - IsNonWorkingDay = weekend OR a closure/vacation row matching // the (country, regime) pair // - AdjustForNonWorkingDays = walk forward day-by-day until // IsNonWorkingDay returns false (bounded at 60 iters) // - AdjustForNonWorkingDaysBackward = same but stepping -1 day // - AdjustForNonWorkingDaysWithReason = forward walk + structured // reason payload (vacation > public_holiday > weekend) type SnapshotHolidayCalendar struct { byDate map[string][]SnapshotHoliday // keyed by YYYY-MM-DD } // NewHolidayCalendar parses the embedded holidays.json and returns a // ready-to-use calendar. func NewHolidayCalendar() (*SnapshotHolidayCalendar, error) { var holidays []SnapshotHoliday if err := readJSON("holidays.json", &holidays); err != nil { return nil, err } cal := &SnapshotHolidayCalendar{byDate: make(map[string][]SnapshotHoliday, len(holidays))} for _, h := range holidays { cal.byDate[h.Date] = append(cal.byDate[h.Date], h) } return cal, nil } // IsNonWorkingDay returns true on weekends or closure/vacation // holidays applicable to the given country/regime. func (c *SnapshotHolidayCalendar) IsNonWorkingDay(date time.Time, country, regime string) bool { if wd := date.Weekday(); wd == time.Saturday || wd == time.Sunday { return true } key := date.Format("2006-01-02") for _, h := range c.byDate[key] { if !h.appliesTo(country, regime) { continue } if h.isClosure() || h.isVacation() { return true } } return false } func (c *SnapshotHolidayCalendar) holidayMatch(date time.Time, country, regime string) *SnapshotHoliday { key := date.Format("2006-01-02") for _, h := range c.byDate[key] { if !h.appliesTo(country, regime) { continue } hh := h return &hh } return nil } // AdjustForNonWorkingDays walks forward until the date lands on a // working day. Bound = 60 iters (same as paliad — generous safety // margin past any vacation run). func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDays(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool) { original = date adjusted = date for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ { adjusted = adjusted.AddDate(0, 0, 1) wasAdjusted = true } return adjusted, original, wasAdjusted } // AdjustForNonWorkingDaysBackward walks backward until the date lands // on a working day. Same bound. func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDaysBackward(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool) { original = date adjusted = date for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ { adjusted = adjusted.AddDate(0, 0, -1) wasAdjusted = true } return adjusted, original, wasAdjusted } // AdjustForNonWorkingDaysWithReason is the structured-explanation // counterpart to AdjustForNonWorkingDays. Reason kind precedence // (longest cause wins): vacation > public_holiday > weekend. Reason // is nil when wasAdjusted is false. func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDaysWithReason(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool, reason *lp.AdjustmentReason) { original = date adjusted = date var holidaysHit []lp.HolidayDTO seen := map[string]bool{} var sawWeekend, sawVacation, sawPublicHoliday bool var vacationName string for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ { if wd := adjusted.Weekday(); wd == time.Saturday || wd == time.Sunday { sawWeekend = true } if h := c.holidayMatch(adjusted, country, regime); h != nil { if h.isVacation() { sawVacation = true if vacationName == "" { vacationName = h.Name } } else if h.isClosure() { sawPublicHoliday = true } key := h.Date + "|" + h.Name if !seen[key] { holidaysHit = append(holidaysHit, lp.HolidayDTO{ Date: h.Date, Name: h.Name, IsVacation: h.isVacation(), IsClosure: h.isClosure(), }) seen[key] = true } } adjusted = adjusted.AddDate(0, 0, 1) wasAdjusted = true } if !wasAdjusted { return adjusted, original, false, nil } r := &lp.AdjustmentReason{Holidays: holidaysHit} switch { case sawVacation: r.Kind = "vacation" r.VacationName = vacationName if vs, ve, ok := c.findVacationBlock(original, country, regime); ok { r.VacationStart = vs.Format("2006-01-02") r.VacationEnd = ve.Format("2006-01-02") } case sawPublicHoliday: r.Kind = "public_holiday" default: r.Kind = "weekend" } if sawWeekend && r.Kind == "weekend" { r.OriginalWeekday = original.Weekday().String() } return adjusted, original, true, r } // findVacationBlock scans outward from date through non-working days // to locate the first/last IsVacation entries. Weekends inside the // run are traversed but don't extend the reported span — start/end // are always real vacation entries. func (c *SnapshotHolidayCalendar) findVacationBlock(date time.Time, country, regime string) (start, end time.Time, ok bool) { cur := date for i := 0; i < 60; i++ { if !c.IsNonWorkingDay(cur, country, regime) { break } if h := c.holidayMatch(cur, country, regime); h != nil && h.isVacation() { start = cur ok = true break } cur = cur.AddDate(0, 0, -1) } if !ok { return } cur = date for i := 0; i < 60; i++ { if !c.IsNonWorkingDay(cur, country, regime) { break } if h := c.holidayMatch(cur, country, regime); h != nil && h.isVacation() { end = cur } cur = cur.AddDate(0, 0, 1) } return start, end, true } // Compile-time assertion that SnapshotHolidayCalendar satisfies // lp.HolidayCalendar. var _ lp.HolidayCalendar = (*SnapshotHolidayCalendar)(nil)