github.com/go-chrono/chrono@v0.0.0-20240102183611-532f0d0d7c34/date.go (about)

     1  package chrono
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"math/big"
     7  )
     8  
     9  func getISOWeek(v int64) (isoYear, isoWeek int, err error) {
    10  	year, month, day, err := fromDate(v)
    11  	if err != nil {
    12  		return 0, 0, err
    13  	}
    14  
    15  	isoYear = year
    16  	isoWeek = int((10 + getOrdinalDate(isoYear, int(month), day) - getWeekday(int32(v))) / 7)
    17  	if isoWeek == 0 {
    18  		if isLeapYear(isoYear - 1) {
    19  			return isoYear - 1, 53, nil
    20  		}
    21  		return isoYear - 1, 52, nil
    22  	}
    23  
    24  	if isoWeek == 53 && !isLeapYear(year) {
    25  		return isoYear + 1, 1, nil
    26  	}
    27  
    28  	return isoYear, isoWeek, nil
    29  }
    30  
    31  var daysInMonths = [12]int{
    32  	January - 1:   31,
    33  	February - 1:  28,
    34  	March - 1:     31,
    35  	April - 1:     30,
    36  	May - 1:       31,
    37  	June - 1:      30,
    38  	July - 1:      31,
    39  	August - 1:    31,
    40  	September - 1: 30,
    41  	October - 1:   31,
    42  	November - 1:  30,
    43  	December - 1:  31,
    44  }
    45  
    46  const (
    47  	// unixEpochJDN is the JDN that corresponds to 1st January 1970 (Gregorian).
    48  	unixEpochJDN = 2440588
    49  
    50  	// The minimum representable date is JDN 0.
    51  	minYear  = -4713
    52  	minMonth = int(November)
    53  	minDay   = 24
    54  	minJDN   = -unixEpochJDN
    55  
    56  	// The maximum representable date must fit into an int32.
    57  	maxYear  = 5874898
    58  	maxMonth = int(June)
    59  	maxDay   = 3
    60  	maxJDN   = math.MaxInt32 - unixEpochJDN
    61  )
    62  
    63  func getWeekday(ordinal int32) int {
    64  	return int((ordinal+int32(unixEpochJDN))%7) + 1
    65  }
    66  
    67  func isLeapYear(year int) bool {
    68  	return (year%4 == 0 && year%100 != 0) || year%400 == 0
    69  }
    70  
    71  func getOrdinalDate(year, month, day int) int {
    72  	var out int
    73  	for i := int(January); i <= month; i++ {
    74  		if i == month {
    75  			out += int(day)
    76  		} else {
    77  			out += int(daysInMonths[i-1])
    78  		}
    79  	}
    80  
    81  	if isLeapYear(year) && month > int(February) {
    82  		out++
    83  	}
    84  	return out
    85  }
    86  
    87  func isDateInBounds(year, month, day int) bool {
    88  	if year < minYear {
    89  		return false
    90  	} else if year == minYear {
    91  		if month < minMonth {
    92  			return false
    93  		} else if month == minMonth && day < minDay {
    94  			return false
    95  		}
    96  	}
    97  
    98  	if year > maxYear {
    99  		return false
   100  	} else if year == maxYear {
   101  		if month > maxMonth {
   102  			return false
   103  		} else if month == maxMonth && day > maxDay {
   104  			return false
   105  		}
   106  	}
   107  
   108  	return true
   109  }
   110  
   111  func isDateValid(year, month, date int) bool {
   112  	if month < int(January) || month > int(December) {
   113  		return false
   114  	}
   115  
   116  	if isLeapYear(year) && month == int(February) {
   117  		return date > 0 && date <= 29
   118  	}
   119  	return date > 0 && date <= daysInMonths[month-1]
   120  }
   121  
   122  func fromDate(v int64) (year, month, day int, err error) {
   123  	if v < minJDN || v > maxJDN {
   124  		return 0, 0, 0, fmt.Errorf("invalid date")
   125  	}
   126  
   127  	dd := int64(v + unixEpochJDN)
   128  
   129  	f := dd + 1401 + ((((4*dd + 274277) / 146097) * 3) / 4) - 38
   130  	e := 4*f + 3
   131  	g := (e % 1461) / 4
   132  	h := 5*g + 2
   133  
   134  	day = int((h%153)/5) + 1
   135  	month = int((h/153+2)%12) + 1
   136  	year = int(e/1461 - 4716 + (14-int64(month))/12)
   137  	return
   138  }
   139  
   140  func getYearDay(v int64) (int, error) {
   141  	year, month, day, err := fromDate(v)
   142  	if err != nil {
   143  		return 0, err
   144  	}
   145  	return getOrdinalDate(year, int(month), day), nil
   146  }
   147  
   148  func getDaysInYear(year int) int {
   149  	if isLeapYear(year) {
   150  		return 366
   151  	}
   152  	return 365
   153  }
   154  
   155  func makeDate(year, month, day int) (int64, error) {
   156  	if !isDateInBounds(year, month, day) {
   157  		return 0, fmt.Errorf("date out of bounds")
   158  	}
   159  	return makeJDN(int64(year), int64(month), int64(day)), nil
   160  }
   161  
   162  func makeJDN(y, m, d int64) int64 {
   163  	return (1461*(y+4800+(m-14)/12))/4 + (367*(m-2-12*((m-14)/12)))/12 - (3*((y+4900+(m-14)/12)/100))/4 + d - 32075 - unixEpochJDN
   164  }
   165  
   166  func ofDayOfYear(year, day int) (int64, error) {
   167  	isLeap := isLeapYear(year)
   168  	if (!isLeap && day > 365) || day > 366 {
   169  		return 0, fmt.Errorf("invalid date")
   170  	}
   171  
   172  	var month Month
   173  
   174  	var total int
   175  	for m, n := range daysInMonths {
   176  		if isLeap && m == 1 {
   177  			n = 29
   178  		}
   179  
   180  		if total+n >= day {
   181  			day = day - total
   182  			month = Month(m + 1)
   183  			break
   184  		}
   185  		total += n
   186  	}
   187  
   188  	return makeDate(year, int(month), day)
   189  }
   190  
   191  func ofISOWeek(year, week, day int) (int64, error) {
   192  	if week < 1 || week > 53 {
   193  		return 0, fmt.Errorf("invalid week number")
   194  	}
   195  
   196  	jan4th, err := makeDate(year, int(January), 4)
   197  	if err != nil {
   198  		return 0, err
   199  	}
   200  
   201  	v := week*7 + int(day) - int(getWeekday(int32(jan4th))+3)
   202  
   203  	daysThisYear := getDaysInYear(year)
   204  	switch {
   205  	case v <= 0: // Date is in previous year.
   206  		return ofDayOfYear(year-1, v+getDaysInYear(year-1))
   207  	case v > daysThisYear: // Date is in next year.
   208  		return ofDayOfYear(year+1, v-daysThisYear)
   209  	default: // Date is in this year.
   210  		return ofDayOfYear(year, v)
   211  	}
   212  }
   213  
   214  func addDateToDate(d int64, years, months, days int) (int64, error) {
   215  	year, month, day, err := fromDate(d)
   216  	if err != nil {
   217  		return 0, err
   218  	}
   219  
   220  	out, err := makeDate(year+years, int(month)+months, day+days)
   221  	return out, err
   222  }
   223  
   224  func simpleDateStr(year, month, day int) string {
   225  	return fmt.Sprintf("%04d-%02d-%02d", year, month, day)
   226  }
   227  
   228  func getISODateSimpleStr(year, week, day int) string {
   229  	return fmt.Sprintf("%04d-W%02d-%d", year, week, day)
   230  }
   231  
   232  func makeDateTime(date, time int64) big.Int {
   233  	out := big.NewInt(date)
   234  	out.Mul(out, bigIntDayExtent)
   235  	out.Add(out, big.NewInt(time))
   236  	return *out
   237  }
   238  
   239  func addDurationToBigDate(d big.Int, v Duration) (big.Int, error) {
   240  	out := new(big.Int).Set(&d)
   241  	out.Add(out, &v.v)
   242  
   243  	if out.Cmp(&minLocalDateTime.v) == -1 || out.Cmp(&maxLocalDateTime.v) == 1 {
   244  		return big.Int{}, fmt.Errorf("datetime out of range")
   245  	}
   246  	return *out, nil
   247  }
   248  
   249  func bigDateToOffset(d big.Int, o1, o2 int64) big.Int {
   250  	out := new(big.Int).Set(&d)
   251  	out.Sub(out, big.NewInt(o1))
   252  	out.Add(out, big.NewInt(o2))
   253  	return *out
   254  }
   255  
   256  func addDateToBigDate(d big.Int, years, months, days int) (big.Int, error) {
   257  	date, _ := splitDateAndTime(d)
   258  
   259  	added, err := addDateToDate(date, years, months, days)
   260  	if err != nil {
   261  		return big.Int{}, err
   262  	}
   263  
   264  	if added < minJDN || added > maxJDN {
   265  		return big.Int{}, fmt.Errorf("date out of bounds")
   266  	}
   267  
   268  	diff := big.NewInt(int64(added - date))
   269  	diff.Mul(diff, bigIntDayExtent)
   270  
   271  	out := new(big.Int).Set(&d)
   272  	out.Add(out, diff)
   273  
   274  	return *out, nil
   275  }
   276  
   277  func splitDateAndTime(v big.Int) (date, time int64) {
   278  	vv := new(big.Int).Set(&v)
   279  
   280  	var _time big.Int
   281  	_date, _ := vv.DivMod(vv, bigIntDayExtent, &_time)
   282  	return _date.Int64(), _time.Int64()
   283  }
   284  
   285  var (
   286  	bigIntDayExtent = big.NewInt(24 * int64(Hour))
   287  
   288  	minLocalDateTime = OfLocalDateTime(MinLocalDate(), LocalTimeOf(0, 0, 0, 0))
   289  	maxLocalDateTime = OfLocalDateTime(MaxLocalDate(), LocalTimeOf(99, 59, 59, 999999999))
   290  )