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 )