github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/calendar.go (about) 1 // Copyright © 2021. All rights reserved. 2 // Author: Ilya Stroy. 3 // Contacts: iyuryevich@pm.me, https://github.com/qioalice 4 // License: https://opensource.org/licenses/MIT 5 6 package ekatime 7 8 import ( 9 "encoding/base64" 10 "encoding/binary" 11 "errors" 12 13 "github.com/qioalice/ekago/v3/ekamath" 14 ) 15 16 type ( 17 // Calendar is a RAM friendly data structure that allows you to keep 18 // 365 days of some year with flags whether day is day off or a workday, 19 // store a reason of that and also binary/text encoding/decoding. 20 // 21 // WARNING! 22 // You MUST use NewCalendar() constructor to construct this object. 23 // If you just instantiate an object it will be considered as invalid, 24 // and almost all methods will return you an unexpected, bad result. 25 // 26 // WARNING! 27 // Encode/decode operations DOES NOT SUPPORT causing feature (for now). 28 // It will be fixed in the future. 29 Calendar struct { 30 31 // ------------------------------------------------------------- 32 // Binary encoding/decoding protocol. 33 // Version: 1.0 34 // 35 // [0..3] bytes: Reserved. 36 // [4..5] bytes: Field `year`, big endian. 37 // [6] byte: Field `isLeap`. 38 // [7] byte: Reserved. 39 // [8..] bytes: Field `dayOff`. BitSet as binary encoded. 40 // ------------------------------------------------------------- 41 42 // TODO: Add support of causing feature for encode/decode operations. 43 44 // The year this calendar of. 45 year Year 46 47 // Flag whether the current year is leap or not. 48 // Fewer computations, more RAM consumption. 49 isLeap bool 50 51 // Bitset of days in calendar. 52 // 0 means work day, 1 means day off. 53 // The index of bit is a day of year. 54 dayOff *ekamath.BitSet 55 56 // A set of Event that is used to overwrite default values of calendar. 57 // Nil if `enableCause` is false at the NewCalendar() call. 58 cause map[uint]EventID 59 60 // A map of EventID's descriptions. 61 // Nil if `enableCause` is false at the NewCalendar() call. 62 eventDescriptions map[EventID]string 63 } 64 ) 65 66 var ( 67 ErrCalendarInvalid = errors.New("invalid Calendar") 68 ErrCalendarInvalidDataToDecode = errors.New("invalid data to decode to Calendar") 69 ) 70 71 // ---------------------------------------------------------------------------- // 72 73 // IsValid reports whether current Calendar is valid and not malformed. 74 func (wc *Calendar) IsValid() bool { 75 return wc != nil && wc.dayOff != nil 76 } 77 78 // Clear clears the current Calendar marking ALL days as a workdays 79 // and removing all events. 80 // Does nothing if current Calendar is nil or malformed. 81 func (wc *Calendar) Clear() { 82 if wc.IsValid() { 83 if wc.cause != nil { 84 wc.cause = make(map[uint]EventID, _CALENDAR2_CAUSE_DEFAULT_CAPACITY) 85 wc.eventDescriptions = make(map[EventID]string, _CALENDAR2_EVENT_DESCRIPTIONS_DEFAULT_CAPACITY) 86 } 87 wc.dayOff.Clear() 88 } 89 } 90 91 // Clone returns a full-copy of the current Calendar. 92 // Returns nil if current Calendar is nil or malformed. 93 func (wc *Calendar) Clone() *Calendar { 94 95 if !wc.IsValid() { 96 return nil 97 } 98 99 cloned := Calendar{ 100 year: wc.year, 101 dayOff: wc.dayOff.Clone(), 102 } 103 104 if wc.cause != nil { 105 cloned.cause = make(map[uint]EventID, len(wc.cause)) 106 for k, v := range wc.cause { 107 cloned.cause[k] = v 108 } 109 cloned.eventDescriptions = make(map[EventID]string, len(wc.eventDescriptions)) 110 for k, v := range wc.eventDescriptions { 111 cloned.eventDescriptions[k] = v 112 } 113 } 114 115 return &cloned 116 } 117 118 // Year returns a Year this calendar of. 119 // Returns 0 if current Calendar is nil or malformed. 120 func (wc *Calendar) Year() Year { 121 if !wc.IsValid() { 122 return 0 123 } 124 return wc.year 125 } 126 127 // OverrideDate allows you to change the default (or previous defined) 128 // day type (day-off / workday) of the provided Date `dd` in the current Calendar. 129 func (wc *Calendar) OverrideDate(dd Date, isDayOff bool) { 130 wc.overrideDate(dd, 0, isDayOff, false) 131 } 132 133 // AddEvent adds a new Event to the Calendar. 134 // It's the same as just OverrideDate() but provides an ability to set an EventID 135 // of such overwriting rule. 136 // 137 // Requirements: 138 // - Calendar is valid and not malformed object, 139 // - Causing feature is enabled (`enableCause` being `true` at the NewCalendar() call), 140 // - Provided Event (`e`) is valid and belongs to Year, this Calendar of. 141 // 142 // Does nothing if any of requirements is failed. 143 func (wc *Calendar) AddEvent(e Event) { 144 if e.IsValid() { 145 eventID, dd, isDayOff := e.Split() 146 wc.overrideDate(dd, eventID, isDayOff, true) // contains all checks 147 } 148 } 149 150 // AddEventDescription adds a new EventID's description to the Calendar. 151 // Using that you can describe your EventID and figure out event's name. 152 // 153 // Requirements: 154 // - Calendar is valid and not malformed object, 155 // - Causing feature is enabled (`enableCause` being `true` at the NewCalendar() call), 156 // - Provided EventID's name (`desc`) is not empty 157 // 158 // Does nothing if any of requirements is failed. 159 func (wc *Calendar) AddEventDescription(eid EventID, desc string) { 160 if wc.IsValid() && wc.cause != nil && desc != "" { 161 wc.eventDescriptions[eid] = desc 162 } 163 } 164 165 // IsDayOff reports whether required day is day off. 166 // If you have a Date object just call Date.Days() method. 167 // 168 // Requirements: 169 // - Calendar is valid and not malformed object, 170 // - Requested Date is valid and belongs to Year, this Calendar of. 171 // 172 // Returns false if requested day is workday or if any of requirements is failed. 173 func (wc *Calendar) IsDayOff(dd Date) bool { 174 return wc.IsValid() && 175 dd.IsValid() && 176 dd.Year() == wc.year && 177 wc.dayOff.IsSetUnsafe(wc.dateToIndex(dd)) 178 } 179 180 // NextWorkDay returns a next work day followed by provided day of year. 181 // 182 // Requirements: 183 // - Calendar is valid and not malformed object, 184 // - Requested Date is valid and belongs to Year, this Calendar of. 185 // 186 // Returns an invalid date if there's no remaining workdays after requested 187 // or if any of requirements is failed. 188 func (wc *Calendar) NextWorkDay(dd Date) Date { 189 return wc.nextDay(dd, false) 190 } 191 192 // NextDayOff returns a next day off followed by provided day of year. 193 // 194 // Requirements: 195 // - Calendar is valid and not malformed object, 196 // - Requested Date is valid and belongs to Year, this Calendar of. 197 // 198 // Returns an invalid date if there's no remaining days off after requested 199 // or if any of requirements is failed. 200 func (wc *Calendar) NextDayOff(dd Date) Date { 201 return wc.nextDay(dd, true) 202 } 203 204 // EventOfDate returns an Event because of which the type of the current date is changed. 205 // 206 // Requirements: 207 // - Calendar is valid and not malformed object, 208 // - Causing feature is enabled (`enableCause` being `true` at the NewCalendar() call), 209 // - Requested date (`dayOfYear`) is valid and belongs the year, this calendar belongs also to. 210 // 211 // Returns an invalid event if there's no registered event with passed date, 212 // or if any of requirements is failed. 213 func (wc *Calendar) EventOfDate(dd Date) Event { 214 215 if !(wc.IsValid() && wc.cause != nil && dd.IsValid() && dd.Year() == wc.year) { 216 return _EVENT_INVALID 217 } 218 219 idx := wc.dateToIndex(dd) 220 eventID, ok := wc.cause[idx] 221 if !ok { 222 return _EVENT_INVALID 223 } 224 225 isDayOff := wc.dayOff.IsSetUnsafe(idx) 226 227 return NewEvent(dd, eventID, isDayOff) 228 } 229 230 // DescriptionOfEvent returns an EventID's description (name). 231 // 232 // Requirements: 233 // - Calendar is valid and not malformed object, 234 // - Causing feature is enabled (`enableCause` being `true` at the NewCalendar() call), 235 // - Calendar has at least one Event with requested EventID, 236 // 237 // Returns an empty string if there's no registered such EventID, 238 // or if any of requirements is failed. 239 func (wc *Calendar) DescriptionOfEvent(eid EventID) string { 240 241 if !(wc.IsValid() && wc.cause != nil) { 242 return "" 243 } 244 245 return wc.eventDescriptions[eid] 246 } 247 248 // WorkDays returns an array of work days of the provided Month. 249 // 250 // Requirements: 251 // - Calendar is valid and not malformed object, 252 // - Requested Month (`m`) is valid. 253 // 254 // WARNING: 255 // It takes quite a lot of time to prepare return data, because of the way 256 // the data is stored internally. So, if you need to access it often, 257 // consider caching data (generate output data once per time its still valid 258 // and then use it later). 259 // 260 // Returns an empty set if any of requirements is failed. 261 func (wc *Calendar) WorkDays(m Month) []Day { 262 return wc.daysIn(m, false) 263 } 264 265 // DaysOff returns an array of days off of the provided Month. 266 // 267 // See requirements, warnings and return section of WorkDays(). 268 // It works the same way here. 269 func (wc *Calendar) DaysOff(m Month) []Day { 270 return wc.daysIn(m, true) 271 } 272 273 // WorkDaysCount returns a number of working days in the provided Month. 274 // 275 // Requirements: 276 // - Calendar is valid and not malformed object, 277 // - Requested Month (`m`) is valid. 278 // 279 // Returns 0 if any of requirements is failed. 280 func (wc *Calendar) WorkDaysCount(m Month) Days { 281 return wc.daysInCount(m, false) 282 } 283 284 // DaysOffCount returns a number of days off in the provided Month. 285 // 286 // See requirements, warnings and return section of WorkDaysCount(). 287 // It works the same way here. 288 func (wc *Calendar) DaysOffCount(m Month) Days { 289 return wc.daysInCount(m, true) 290 } 291 292 // ---------------------------------------------------------------------------- // 293 294 // MarshalBinary implements BinaryMarshaler interface encoding current Calendar 295 // in binary form. 296 // 297 // It guarantees that if Calendar is valid, the MarshalBinary() cannot fail. 298 // There's no guarantees about algorithm that will be used to encode/decode. 299 // 300 // Requirements: 301 // - Calendar is valid and not malformed object, 302 // - User MUST NOT modify returned data. If you need it, clone it firstly. 303 // 304 // Limitations: 305 // - Encode/decode operations DOES NOT SUPPORT causing feature. 306 func (wc *Calendar) MarshalBinary() ([]byte, error) { 307 308 if !wc.IsValid() { 309 return nil, ErrCalendarInvalid 310 } 311 312 dayOffEncoded, err := wc.dayOff.MarshalBinary() 313 if err != nil { 314 return nil, ErrCalendarInvalidDataToDecode 315 } 316 317 // For more info about binary encode/decode protocol, 318 // see Calendar's internal docs (at the Calendar struct declaration). 319 320 buf := make([]byte, len(dayOffEncoded)+8) 321 binary.BigEndian.PutUint16(buf[4:], uint16(wc.year)) 322 if wc.isLeap { 323 buf[6] = 1 324 } 325 copy(buf[8:], dayOffEncoded) 326 327 return buf, nil 328 } 329 330 // UnmarshalBinary implements BinaryUnmarshaler interface decoding provided `data` 331 // from binary form. 332 // 333 // The current Calendar's data will be overwritten by the decoded one 334 // if decoding operation has been completed successfully. 335 // 336 // There's no guarantees about algorithm that will be used to encode/decode. 337 // Does nothing (and returns nil) if provided `data` is empty. 338 // 339 // Requirements: 340 // - Provided `data` MUST BE obtained by calling Calendar.MarshalBinary() method. 341 // - Provided `data` MUST BE valid, ErrCalendarInvalidDataToDecode returned otherwise. 342 // - User MUST NOT use provided `data` after passing to this method. UB otherwise. 343 // 344 // Limitations: 345 // - Encode/decode operations DOES NOT SUPPORT causing feature. 346 func (wc *Calendar) UnmarshalBinary(data []byte) error { 347 348 // It's ok for Calendar to be invalid - it will be overwritten anyway. 349 // But it must be not nil. 350 351 switch { 352 case len(data) == 0: 353 return nil 354 355 case len(data) < 9: 356 return ErrCalendarInvalidDataToDecode 357 358 case wc == nil: 359 return ErrCalendarInvalid 360 } 361 362 // For more info about binary encode/decode protocol, 363 // see Calendar's internal docs (at the Calendar struct declaration). 364 365 wc.cause = nil 366 wc.eventDescriptions = nil 367 wc.year = Year(binary.BigEndian.Uint16(data[4:])) 368 wc.isLeap = data[6] == 1 369 370 if wc.dayOff == nil { 371 wc.dayOff = new(ekamath.BitSet) 372 } 373 374 return wc.dayOff.UnmarshalBinary(data[8:]) 375 } 376 377 // MarshalText implements TextMarshaler interface encoding current Calendar 378 // in text form. 379 // 380 // It guarantees that if Calendar is valid, the MarshalText() cannot fail. 381 // MarshalText guarantees that output data will be base64 encoded. 382 // 383 // Requirements: 384 // - Calendar is valid and not malformed object, 385 // - User MUST NOT modify returned data. If you need it, clone it firstly. 386 // 387 // Limitations: 388 // - Encode/decode operations DOES NOT SUPPORT causing feature. 389 // - Provided base64 data is NO URL FRIENDLY! 390 func (wc *Calendar) MarshalText() ([]byte, error) { 391 392 binaryEncodedData, err := wc.MarshalBinary() 393 if err != nil { 394 return nil, err 395 } 396 397 buf := make([]byte, base64.StdEncoding.EncodedLen(len(binaryEncodedData))) 398 base64.StdEncoding.Encode(buf, binaryEncodedData) 399 400 return buf, nil 401 } 402 403 // UnmarshalText implements TextUnmarshaler interface decoding provided `data` 404 // from text form. 405 // 406 // The current Calendar's data will be overwritten by the decoded one 407 // if decoding operation has been completed successfully. 408 // 409 // Does nothing (and returns nil) if provided `data` is empty. 410 // 411 // Requirements: 412 // - Provided `data` MUST BE obtained by calling Calendar.MarshalBinary() method. 413 // - Provided `data` MUST BE valid, ErrCalendarInvalidDataToDecode returned otherwise. 414 // - User MUST NOT use provided `data` after passing to this method. UB otherwise. 415 // 416 // Limitations: 417 // - Encode/decode operations DOES NOT SUPPORT causing feature. 418 func (wc *Calendar) UnmarshalText(data []byte) error { 419 420 switch { 421 case len(data) == 0: 422 return nil 423 424 case wc == nil: 425 return ErrCalendarInvalid 426 } 427 428 buf := make([]byte, base64.StdEncoding.DecodedLen(len(data))) 429 n, err := base64.StdEncoding.Decode(buf, data) 430 if err != nil { 431 return err 432 } 433 434 return wc.UnmarshalBinary(buf[:n]) 435 } 436 437 // ---------------------------------------------------------------------------- // 438 439 // NewCalendar is a Calendar constructor. 440 // Returns an initialized, ready to use object. 441 // 442 // You MUST specify a valid Year, otherwise nil is returned. 443 // 444 // It's allowed to pass Year < 1900 or > 4095 445 // (that Year for which Year.IsValid() method will return false). 446 // 447 // If `saturdayAndSunday` is true, these days will be marked as days off. 448 // Keep in mind, that marking all saturdays and sundays as days off is quite heavy op. 449 // It takes 425ns for i7-9750H CPU @ 2.60GHz. 450 // Maybe it will be better for you to generate once a "template" of that 451 // and then just call Calendar.Clone() if you need many Calendar objects 452 // for the same year. 453 // For configuration above the cloning without `enableCause` feature (read later) 454 // it takes just 95ns. Its faster than filling each object up to x9 times. 455 // 456 // If `enableCause` is true, it also pre-allocates ~512 bytes to be able to keep 457 // 64+ reasons of when the default type of specific day is changed. 458 // If you don't need that (and EventOfDay(), EventOfDate() methods), just pass `false`. 459 // 460 // WARNING! 461 // Encode/decode operations DOES NOT SUPPORT causing feature. 462 func NewCalendar(y Year, saturdayAndSunday, enableCause bool) *Calendar { 463 464 if !IsValidDate(y, MONTH_JANUARY, 1) { 465 return nil 466 } 467 468 wc := Calendar{ 469 year: y, 470 isLeap: y.IsLeap(), 471 dayOff: ekamath.NewBitSet(_CALENDAR2_DEFAULT_CAPACITY), 472 } 473 474 if enableCause { 475 wc.cause = make(map[uint]EventID, _CALENDAR2_CAUSE_DEFAULT_CAPACITY) 476 wc.eventDescriptions = make(map[EventID]string, _CALENDAR2_EVENT_DESCRIPTIONS_DEFAULT_CAPACITY) 477 } 478 479 if saturdayAndSunday { 480 wc.doSaturdayAndSundayDayOff() 481 } 482 483 return &wc 484 }