diff --git a/julian/julian.go b/julian/julian.go new file mode 100644 index 0000000..1116940 --- /dev/null +++ b/julian/julian.go @@ -0,0 +1,122 @@ +package julian + +import "time" + +// TODO: describe how time doesn't go past BCE, and therefore we don't either +// Keep in mind, Go's time.Time gregorian dates are `proleptic`, ie. there is +// no need to give special care to dates before 15. October 1582. +type Date struct { + year int + month time.Month + day int +} + +func greaterOrEqual(a, b Date) bool { + if a.year != b.year { + return a.year > b.year + } else if a.month != b.month { + return a.month > b.month + } else { + return a.day >= b.day + } +} + +func monthLength(year int, month time.Month) int { + lengths := map[time.Month]int{ + time.January: 31, + time.March: 31, + time.April: 30, + time.May: 31, + time.June: 30, + time.July: 31, + time.August: 31, + time.September: 30, + time.October: 31, + time.November: 30, + time.December: 31, + } + if _, ok := lengths[month]; ok { + return lengths[month] + } + // February + if year%4 == 0 { + return 29 + } + return 28 +} + +func dateDiff(d Date, days int) Date { + // keep in mind throughout this code that the original "days" value can + // be negative + var fourYearPeriods int + var periodLen int = (3 * 365) + 366 + // a difference which is an exact multiple of 4 years can be dealt with + // easily, so we only have to take greater care with the remainder + fourYearPeriods, days = days/periodLen, days%periodLen + // this `if' will happen if days was originally <0, AND is not an exact + // multiple of periodLen + if days < 0 { + fourYearPeriods -= 1 + days += periodLen + } + // now days must be 0 <= days < periodLen, so our job is easier + // first we shift in the big periods + d.year += fourYearPeriods * 4 + + // We shift the date to the first of the month to simplify our + // calculation + days += d.day - 1 + d.day = 1 + // since days < periodLen+31-1, we will go through this loop at most + // 4*12+1 = 49 times + for days >= monthLength(d.year, d.month) { + days -= monthLength(d.year, d.month) + d.year, d.month = nextMonth(d.year, d.month) + } + d.day += days + + return d +} + +func nextMonth(year int, month time.Month) (int, time.Month) { + if month == time.December { + return year + 1, time.January + } else { + return year, month + 1 + } +} + +func New(year int, month time.Month, day int) Date { + // TODO: deal with users giving negative dates + for day > monthLength(year, month) { + day -= monthLength(year, month) + year, month = nextMonth(year, month) + } + return Date{year, month, day} +} + +// The algorithm, in large part: follows this article: +// https://en.wikipedia.org/wiki/Conversion_between_Julian_and_Gregorian_calendars +func ToTime(d Date) time.Time { + offset := -2 + borderDate := Date{100, time.March, 2} + // We don't increase the offset for years divisible by 400, ie. every + // fourth loop, we keep track of that with skipYear, we can't simply + // check borderDate.year % 400, because when we reach 31. December + // 8299, the year will not be divisible by 100 anymore + skipYear := 1 + for greaterOrEqual(d, borderDate) { + // We can simply add 100 to the year because in the julian + // calendar, if a date exists in year y, it always exists in + // year y+100 + borderDate.year += 100 + if skipYear%4 != 0 { + borderDate = dateDiff(borderDate, -1) + offset++ + } + skipYear++ + } + + d = dateDiff(d, offset) + return time.Date(d.year, d.month, d.day, 0, 0, 0, 0, time.UTC) +}