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  }