jdcal

package module
v0.0.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2024 License: MPL-2.0 Imports: 5 Imported by: 0

README

jdcal

Go library and CLI utility to convert to-and-fro between Julian and Gregorian dates, aimed at Christian calendars and holidays, offering:

  • Conversions between Julian and Gregorian calendars for any date between 500BC and 2100AD
  • When is a year a leap year, on either of the calendars
  • Zones: When did which regions switch calendars
  • Visualization: How did a calendar look in a given zone around the switch-over date
  • Holidays: When did certain holidays occur in a given year, taking into account the full moon dates in that year
  • And more. Table of Contents

But why?

46 BC
Julius Caesar
In the year that we refer to as 46 BC the Roman consul Julius Caesar proposed a reform to the previously used lunisolar calendar (which, unsuccessfully, tried to combine both the solar and lunar cycles). The new calendar would be solely based on Solar timing, Lunar cycles would be computed separately. This Julian calendar would have 365.25 days per year: every February would have 28 days, except for years evenly divisible by four. Then the year would be a leap year and Februrary would have 29 days. This calendar took effect on January 1st 45 BC by edict. JC was after all "Dictator Perpetuo" and "Pontifex Maximus". That year needed 445 days (!) to synchronize the previous (and hopelessly out of sync) calendar with the Solar cycle.

(Image: Julius Caesar, probably the only surviving sculpture created during his life. From https://en.wikipedia.org/wiki/Julius_Caesar)

Romans would generally avoid numbering years. Preferably they'd refer to a date as "in the year of the consuls so-and-so", or, if needed, they would refer to a year Ab Urbe Condita, from the founding of the City, which is probably 753 BC. Julius Caesar of course had no notion of "45 BC". A coordinate shift of the time axis and BC/AD counting were added much later, around 525 AD, when the West Roman empire collapsed. An interesting fact is that when introducing BC/AC and the new time axis, the number zero was not widely recognized. Calendars using BC/AD go from December 31st, 1 BC to January 1st, 1 AD. Dates going back to 500 BC were backdated to the Julian calendar (probably because dates before 500 BC were unknown anyway). So we can say that the Julian calendar started 500 BC in the sense that one may find documents referring to such dates -- though such documents will be written after the introduction of the Julian calendar, so after 45 BC.

Errors in days over years, Julian Calendar
The Julian calendar was unfortunately not entirely precise. It would postpone a Solar day too often, by too frequently scheduling February 29th. By the year 1500, the longest day (Midsummer night) was on June 11th, instead of the 21st. And the spring Equinox was equally far from March 21st, which gravely hampered the calculations of the Easter festivities. Interestingly, the equinoxes or solstices did **not** match their "right" dates at the introduction of the Julian calendar. By chance the Julian calendar got the dates right in the third century AD.

(Image: Drift of the Julian calendar. There are no data for year zero; that date doensn't exist in BC/AC.)

Around 1582 AD
Pope Gregory XIII
In 1582, Pope Gregory XIII ordered a modification so that a year would have 365.2425 days. (However, 365.2425 days per year is also not precise. We need leap seconds now.) The change meant that a year would be a leap year if the year was divisible by four, but not if it was a century mark (divisible by 100), except if it was a millenium (divisible by 1000). Hence, 1896 would be a leap year (divisble by 4), but 1900 would not (it's a century mark). Also, 1996 would be a leap year, and 2000 as well (it's a millenium). A further change was that 10 days were skipped in order to align the calendar with the solar cycle: the day following October 4th 1582 would be October 15th, and not the 5th.

(Image: Pope Gregory XIII, celebrating the introduction of his calendar. From https://en.wikipedia.org/wiki/Pope_Gregory_XIII.) The Papal edict was put in place and recognized by anyone under the authority of the Catholic Church. G13 was after all Vicarius Christi, Principis Apostolorum, Pontiff, etc.. Since it was ordered by the Catholics, others wouldn't accept the change; notably these were the followers of the Protestant Churches, the Eastern Orthodox Churches and the Oriental Orthodox churches.

Simplified map of switch over dates
So January 1st, 1600 may be a Gregorian date if it's in a historically Catholic zone, and it may be a Julian date if not. Or it may not exist at all if this area switched to the Gregorian calendar around that time. Or it may be both if this area switched back to the Julian calendar. Near the border of such zones, one could arrange to meet your inlaws on August 1st, and they'd show up on August 11th because that was August 1st on their calendar. One may still find remnants of how people avoided such confusion in phrases like "the market will be held on the the third Sunday after Winter Equinox".

(Image: Simplified map of switch over dates. From https://familytreemagazine.com/history/gregorian-calendar-adoption-map/)

Up to 1923 AD (!)

Eventually even the initial nay-sayers adopted the Gregorian calendar, probably simply because it's more precise and to avoid confusion. An example is USA. The Catholic parts (former French and Spanish colonies) switched to the Gregorian calendar in 1582 AD, as ordered by the Pope. The Protestant parts (formerly British, so Anglican/Protestant) switched in 1752. Alaska switched in 1867, since it was formerly part of the Russian empire and hence Orthodox -- they postponed switching for a loooong time. Though not as long as Greece, which only switched in 1923 AD. But, even disregarding different switch dates for different areas, this didn't always go smoothly. Some areas fell under Catholic rule (read: were conquered) and were forced to switch to the Gregorian calendar, only to be re-conquered by Protestants and to switch back to the Julian calendar, and finally to switch to the Gregorian calendar again. A nice example is Switzerland's Appenzell Ausserrhoden:

  • They adopted the Gregorian calendar in 1584 (due to Catholic rule),
  • Dumped that in favor of the Julian calendar in 1597,
  • Kept the Julian calendar for more than 200 years,
  • Finally switched to the (obviously better) Gregorian calendar in 1798.

So before 1585 they were "in sync" with other areas, and until 1798 you don't know -- it depends on their switching dates and on their neighbours.

Moon cycle based holidays

The fact that the Julian calendar was unprecise, was a problem because of typical moon-cycle based Christian holidays.

Ash Wednesday (start of fasting), Good Friday, Easter, Ascension Day and Pentecost are are defined by finding the full moon following the ecclesiastical Spring equinox and counting from there (forward or back). This isn't the astronomical equinox (official start of the spring), but instead is pinned at March 21st, Gregorian. Once Julian March 21st was too "far away" from the astronomical equinox, a correction needed to be made.

The software supports finding dates for these holidays, both on the Gregorian and on the Julian calendars.

2023 AD

Late 2023 my darling wife asked me to write some conversion software because she's working on a book that has to be historically precise. I dove into the rabbit hole.

Installation

To install the package, run:

go install github.com/KarelKubat/jdcal@latest

To install the program jdcal as a CLI interface, then run:

cd ~go/src/github.com/KarelKubat/jdcal  # replace ~/go/src with whatever works for you
make install                            # `jdcal` is installed into your ~/go/bin/ or $GOBIN

Short CLI synopsis

To install jdcal as a CLI tool, run make install or go install main/jdcal/jdcal.go. After this you can use the utility. Start jdcal without any arguments to see the usage information.

Conversions between calendars
# Take October 5th 1582 as a Julian date and convert
jdcal convert 1582/10/05              # default calendar is Julian
Julian 1582/10/05 is Gregorian 1582/10/15

# Take October 10th 1582 as a Gregorian and convert back
jdcal convert --gregorian 1582/10/15  # or use -g as a short flag
Gregorian 1582/10/15 is Julian 1582/10/05
Weekdays
# What day was January 1st 1600 on the Julian or Gregorian calendar?
jdcal weekday 1600/1/1
Julian 1600/01/01 is a Tuesday
jdcal weekday 1600/1/1 --gregorian  # or -g as shorthand
Gregorian 1600/01/01 is a Saturday
When did the world switch calendars
# Switch over dates for zones matching "america"
jdcal zones --zone america
United States of America (French & Spanish colonial empires)
  Started using the Julian    calendar   on   Gregorian -0500/02/28
  Switched to   the Gregorian calendar   on   Julian 1582/12/09
United States of America (British Empire)
  Started using the Julian    calendar   on   Gregorian -0500/02/28
  Switched to   the Gregorian calendar   on   Julian 1752/09/02
United States of America (Russian Empire: Alaska)
  Started using the Julian    calendar   on   Gregorian -0500/02/28
  Switched to   the Gregorian calendar   on   Julian 1867/10/06
# All known zones
jdcal zones
  # Shows a long list
Visualization of a timeline

jdcal timeline shows a visualization of the progression of dates. Example:

# Show the timeline around 1582/10/04
jdcal timeline 1582/10/01 --days 10  # note: --zone is unspecified
     Julian |   |   Gregorian
------------+---+------------
 1582/10/01 | M |  1582/10/11
         02 | T |          12
         03 | W |          13
         04 | T |          14
         05 | F |          15
         06 | S |          16
         07 | S |          17
         08 | M |          18
         09 | T |          19
         10 | W |          20

Spain switched over to the Gregorian calendar on October 4th 1582 (try it with jdcal zones --zone spain). So the Spanish people went to sleep on October 4th and woke up on the 15th. That year, valid October dates were 1, 2, 3, 4, 15, 16, 17, etc.. Dates like October 10th don't exist in that zone. One can argue whether October 14th is a valid date as it is the Gregorian version of the switch-over date October 4th. jdcal assumes that it is. The output can be further trimmed by using the flag --zone which leaves out unused dates:

# Show the timeline around 1582/10/04 for zone Spain, which switched over to
# the Gregorian calendar on 1582/10/04.
jdcal timeline 1582/10/01 --days 10 --zone spain
     Julian |   |   Gregorian
------------+---+------------
 1582/10/01 | M |
         02 | T |
         03 | W |
         04 | T |  1582/10/14
            | F |          15
            | S |          16
            | S |          17
            | M |          18
            | T |          19
            | W |          20

Zones that switch back from Gregorian to Julian offer interesting timelines. E.g., "Groningen City" temporarily switched back to Julian after being re-conquered by Protestants. This happened on (the Gregorian date) 1594/11/10, which became (Julian) 1594/10/31. (Again, one may argue whether the switch-over date Julian 1594/10/31 is valid. jdcal assumes it is.) A visualization:

jdcal zones -z 'groningen city'
Netherlands (Groningen City)
  Started using the Julian    calendar   on   Gregorian -0500/02/28
  Switched to   the Gregorian calendar   on   Julian 1583/01/01
  Switched to   the Julian    calendar   on   Gregorian 1594/11/10
  Switched to   the Gregorian calendar   on   Julian 1700/12/31
jdcal timeline 1594/11/01 --days 20 --zone 'groningen city' --gregorian
  Gregorian |   |      Julian
------------+---+------------
 1594/11/01 | T |
         02 | W |
         03 | T |
         04 | F |
         05 | S |
         06 | S |
         07 | M |
         08 | T |
         09 | W |
         10 | T |  1594/10/31
            | F |       11/01
            | S |          02
            | S |          03
            | M |          04
            | T |          05
            | W |          06
            | T |          07
            | F |          08
            | S |          09
            | S |          10

In this case a date like 1594/11/08 exists twice. Also;

  • The date 1594/10/30 must be Gregorian;
  • Dates 1594/10/31 until 1594/11/10 exist on both calendars;
  • The date 1594/11/11 must be Julian.
Holidays

jdcal holidays shows holiday dates for given years. The default output format is Julian:

jdcal holidays 1584
Ash Wednesday occurs on Wednesday Julian 1584/02/05
Good Friday   occurs on Friday    Julian 1584/03/20
Easter        occurs on Sunday    Julian 1584/03/22
Ascension Day occurs on Thursday  Julian 1584/04/30
Pentecost     occurs on Sunday    Julian 1584/05/10

The output format can be forced to Gregorian using flag --gregorian (shorthand: -g). Alternatively, the output format can be "guessed" when a zone is given using flag --zone (shorthand: -z). For example, Switzerland (Appenzell Ausserrhoden) switched to the Gregorian calendar in 1584:

jdcal holidays 1584 -z ausserrhoden
Ash Wednesday in Switzerland (Appenzell Ausserrhoden) occurs on Wednesday Gregorian 1584/02/15
Good Friday   in Switzerland (Appenzell Ausserrhoden) occurs on Friday    Gregorian 1584/03/30
Easter        in Switzerland (Appenzell Ausserrhoden) occurs on Sunday    Gregorian 1584/04/01
Ascension Day in Switzerland (Appenzell Ausserrhoden) occurs on Thursday  Gregorian 1584/05/10
Pentecost     in Switzerland (Appenzell Ausserrhoden) occurs on Sunday    Gregorian 1584/05/20

Some zones switched between the holiday dates. E.g., Georgia switched to the Gregorian calendar on the the Julian date 1918/04/17, which means that the Easter should be represented as Julian, but Ascension day and Pentecost as Gregorian:

jdcal holidays 1918 -z georgia
Ash Wednesday in Georgia occurs on Wednesday Julian 1918/01/31
Good Friday   in Georgia occurs on Friday    Julian 1918/03/16
Easter        in Georgia occurs on Sunday    Julian 1918/03/18
Ascension Day in Georgia occurs on Thursday  Gregorian 1918/05/09
Pentecost     in Georgia occurs on Sunday    Gregorian 1918/05/19

Short library synopsis

Conversions

The default conversion from one date format to another is jdcal.ByProgression, which uses day progression: there is a day zero at the start of epoch, the next day is 1, and so on. The progression table jdcal.YearProgression holds the sequences for the Greogorian and for the Julian progression.

A much slower, but more tested algorithm is jdcal.ByLookup, which uses a lookup table, derived from //en.wikipedia.org/wiki/Conversion_between_Julian_and_Gregorian_calendars. If you suspect an error, switch to this algorithm, retry, and let me know if you find a discrepancy. Switching is done using:

  • jdcal.Algorithm = jdcal.ByLookup (switch to lookup), and:
  • jdcal.Algorithm = jdcal.ByProgression (switch back to the default)

A benchmark for these two algorithms can be run using go test -bench . The order of magnitude is 10 (!) but in CLI invocations that's not even noticable. Computers are good at.. ehrm computing.

// main/demo1/demo1.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/KarelKubat/jdcal"
)

func main() {
	// October 5th (Julian) was the Papal announcement to skip 10 days.
	// The new date would be October 15 (Gregorian).
	jd0, err := jdcal.NewDate(1582, time.October, 5, jdcal.Julian)
	check(err)

	// To switch to a slower, but longer tested algorithm:
	// jdcal.ConvertByLookup()

	// to Gregorian
	gd, err := jd0.Convert()
	check(err)
	wd, err := gd.Weekday()
	check(err)
	fmt.Println("From Julian to Gregorian:", jd0, "is", gd, "and it's a", wd)

	// back to Julian
	jd1, err := gd.Convert()
	check(err)
	wd, err = jd1.Weekday()
	check(err)
	fmt.Println("And back again:", gd, "is", jd1, "and it's a", wd)

	// Output:
	// 	From Julian to Gregorian: Julian 1582/10/05 is Gregorian 1582/10/15 and it's a Friday
	// 	And back again: Gregorian 1582/10/15 is Julian 1582/10/05 and it's a Friday
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
Honoring leap years
// main/demo2/demo2.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/KarelKubat/jdcal"
)

func main() {
	// Advancing dates
	// ---------------
	jd, err := jdcal.NewDate(300, time.February, 27, jdcal.Julian)
	check(err)

	for i := 0; i < 6; i++ {
		gd, err := jd.Convert()
		check(err)
		fmt.Println(jd, "is", gd)
		jd = jd.Forward()
	}

	// Output:
	// Julian 0300/02/27 is Gregorian 0300/02/27
	// Julian 0300/02/28 is Gregorian 0300/02/28
	// Julian 0300/02/29 is Gregorian 0300/03/01  # BOOM, Julian is a day wrong
	// Julian 0300/03/01 is Gregorian 0300/03/02
	// Julian 0300/03/02 is Gregorian 0300/03/03
	// Julian 0300/03/03 is Gregorian 0300/03/04

	// Note that the Julian calendar knows a February 29th, the Gregorian one doesn't.
	// The two calendars diverge after February 28th. This is historically correct.

	// Going backward
	// --------------
	for i := 0; i < 6; i++ {
		gd, err := jd.Convert()
		check(err)
		fmt.Println(jd, "is", gd)
		jd = jd.Backward()
	}

	// Output:
	// Julian 0300/03/04 is Gregorian 0300/03/05
	// Julian 0300/03/03 is Gregorian 0300/03/04
	// Julian 0300/03/02 is Gregorian 0300/03/03
	// Julian 0300/03/01 is Gregorian 0300/03/02
	// Julian 0300/02/29 is Gregorian 0300/03/01
	// Julian 0300/02/28 is Gregorian 0300/02/28

	// Testing leap years
	// ------------------
	for _, yr := range []jdcal.Year{1796, 1797, 1800, 2000} {
		for _, tp := range []jdcal.Type{jdcal.Julian, jdcal.Gregorian} {
			cyr, err := jdcal.NewCalendarYear(yr, tp)
			check(err)
			fmt.Println(cyr, "is a leap year:", cyr.IsLeap())
		}
	}

	// Output:

	// Julian 1796 is a leap year: true        # Standard leap year (divisible by 4)
	// Gregorian 1796 is a leap year: true     # or standard non-leap: Julian and Gregorian
	// Julian 1797 is a leap year: false       # agree
	// Gregorian 1797 is a leap year: false

	// Julian 1800 is a leap year: true        # Century: IsLeap disagrees
	// Gregorian 1800 is a leap year: false

	// Julian 2000 is a leap year: true        # Millenium: Julian and Gregorian agree
	// Gregorian 2000 is a leap year: true
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
Days per month for leap or nonleap years

The number of days in February is 29 for leap years, 28 otherwise. Whether a year is a leap year in a full century (1700, 1800 etc.) depends on the calendar type.

// main/demo6/demo6.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/KarelKubat/jdcal"
)

func main() {
	jyr, err := jdcal.NewCalendarYear(1900, jdcal.Julian)
	check(err)
	gyr, err := jdcal.NewCalendarYear(1900, jdcal.Gregorian)
	check(err)

	jdpm := jyr.DaysPerMonth()
	gdpm := gyr.DaysPerMonth()

	for m := time.January; m <= time.December; m++ {
		fmt.Printf("%-10s: in Julian %2.2d, in Gregorian %2.2d days\n", m, jdpm[m], gdpm[m])
	}

	// Output:
	// January   : in Julian 31, in Gregorian 31 days
	// February  : in Julian 29, in Gregorian 28 days
	// March     : in Julian 31, in Gregorian 31 days
	// April     : in Julian 30, in Gregorian 30 days
	// May       : in Julian 31, in Gregorian 31 days
	// June      : in Julian 30, in Gregorian 30 days
	// July      : in Julian 31, in Gregorian 31 days
	// August    : in Julian 31, in Gregorian 31 days
	// September : in Julian 30, in Gregorian 30 days
	// October   : in Julian 31, in Gregorian 31 days
	// November  : in Julian 30, in Gregorian 30 days
	// December  : in Julian 31, in Gregorian 31 days
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
Zones

The package knows about zones, and on which dates these zones switched. Some zones even first switched from Julian to Gregorian, but then back again to Julian, and then forward. The below code would display zone information in a human readable way, tough a jdcal.ZoneEntry holds this as a struct that can be programmatically examined.

// main/demo3/demo3.go
package main

import (
	"fmt"

	"github.com/KarelKubat/jdcal"
)

func main() {
	for _, e := range jdcal.ZonesByName("netherlands") {
		fmt.Println(e)
	}

	// Output (actual string representations may differ):
	//   Belgium (Southern Netherlands)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1582/12/20
	//   Netherlands (Brabant)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1582/12/14
	//   Netherlands (Drenthe)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1701/04/30
	//   Netherlands (Frisia)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1701/12/31
	//   Netherlands (Gelderland)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1700/06/12
	//   Netherlands (Groningen City)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1583/01/01
	// 		Switched to   the Julian    calendar   on   Gregorian 1594/11/10
	// 		Switched to   the Gregorian calendar   on   Julian 1700/12/31
	//   Netherlands (Holland)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1583/01/01
	//   Netherlands (Utrecht, Overijssel)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1700/11/30
	//   Netherlands (Zeeland, States General)
	// 		Started using the Julian    calendar   on   Gregorian -0500/02/28
	// 		Switched to   the Gregorian calendar   on   Julian 1582/12/14
}
Testing whether a date exists in a zone
// main/demo4/demo4.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/KarelKubat/jdcal"
)

const (
	zoneName = "Netherlands (Groningen City)"
)

func main() {
	zone, err := jdcal.SingleZone(zoneName)
	check(err)
	fmt.Println(zone)
	// Netherlands (Groningen City)
	//   Started using the Julian    calendar   on   Gregorian -0500/02/28
	//   Switched to   the Gregorian calendar   on   Julian 1583/01/01
	//   Switched to   the Julian    calendar   on   Gregorian 1594/11/10
	//   Switched to   the Gregorian calendar   on   Julian 1700/12/31

	// Some dates that lie in between the cutovers.
	for _, year := range []jdcal.Year{1580, 1590, 1600, 1800} {
		test(year, time.January, 1, zone)
	}

	// Output:
	//   1580/01/01 is a Julian date
	//   1590/01/01 is a Gregorian date
	//   1600/01/01 is a Julian date
	//   1800/01/01 is a Gregorian date

	// Just around the exact cutover dates.
	for _, date := range []struct {
		year  jdcal.Year
		month time.Month
		day   int
	}{
		// Around the first switch from Julian into the Gregorian calendar
		{year: 1582, month: time.December, day: 31},
		{year: 1583, month: time.January, day: 1},
		{year: 1583, month: time.January, day: 2},
		// Output:
		//   1582/12/31 is a Julian date
		//   1583/01/01 is a Julian date
		//   1583/01/02 is neither a Julian nor a Gregorian date

		// Around the second switch from Gregorian back to Julian
		{year: 1594, month: time.November, day: 9},
		{year: 1594, month: time.November, day: 10},
		{year: 1594, month: time.November, day: 11},
		// Output:
		//   1594/11/09 can be both a Julian and a Gregorian date
		//   1594/11/10 can be both a Julian and a Gregorian date
		//   1594/11/11 is a Julian date

		// Around the third switch back to Gregorian
		{year: 1700, month: time.December, day: 30},
		{year: 1700, month: time.December, day: 31},
		{year: 1701, month: time.January, day: 1},
		// Output:
		//   1700/12/30 is a Julian date
		//   1700/12/31 is a Julian date
		//   1701/01/01 is neither a Julian nor a Gregorian date
	} {
		test(date.year, date.month, date.day, zone)
	}
}

func test(year jdcal.Year, month time.Month, day int, z jdcal.ZoneEntry) {
	d, err := jdcal.NewDate(year, month, day, jdcal.Julian)
	check(err)
	jdInZone, err := d.InZone(z)
	check(err)

	d, err = jdcal.NewDate(year, month, day, jdcal.Gregorian)
	check(err)
	gdInZone, err := d.InZone(z)
	check(err)

	fmt.Printf("%4.4d/%2.2d/%2.2d ", year, int(month), day)
	switch {
	case jdInZone && gdInZone:
		fmt.Println("can be both a Julian and a Gregorian date")
	case !jdInZone && !gdInZone:
		fmt.Println("is neither a Julian nor a Gregorian date")
	case jdInZone:
		fmt.Println("is a Julian date")
	default:
		fmt.Println("is a Gregorian date")
	}
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
Holidays
// main/demo5/demo5.go
package main

import (
	"fmt"
	"log"

	"github.com/KarelKubat/jdcal"
)

func main() {

	for _, yr := range []jdcal.Year{1369, 1370, 1371} {
		cyr, err := jdcal.NewCalendarYear(yr, jdcal.Julian)
		check(err)

		for h := jdcal.GoodFriday; h <= jdcal.Pentecost; h++ {
			dt, err := cyr.HolidayDate(h)
			check(err)
			wd, err := dt.Weekday()
			check(err)
			fmt.Println(h, "in", cyr, "falls on", wd, dt)
		}
		fmt.Println()
	}

	// Output:
	// Good Friday in Julian 1369 falls on Friday Julian 1369/03/23
	// Easter in Julian 1369 falls on Sunday Julian 1369/03/25
	// Ascension Day in Julian 1369 falls on Thursday Julian 1369/05/03
	// Pentecost in Julian 1369 falls on Sunday Julian 1369/05/13

	// Good Friday in Julian 1370 falls on Friday Julian 1370/04/12
	// Easter in Julian 1370 falls on Sunday Julian 1370/04/14
	// Ascension Day in Julian 1370 falls on Thursday Julian 1370/05/23
	// Pentecost in Julian 1370 falls on Sunday Julian 1370/06/02

	// Good Friday in Julian 1371 falls on Friday Julian 1371/04/04
	// Easter in Julian 1371 falls on Sunday Julian 1371/04/06
	// Ascension Day in Julian 1371 falls on Thursday Julian 1371/05/15
	// Pentecost in Julian 1371 falls on Sunday Julian 1371/05/25
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
Ordinal days

jdcal internally uses "ordinal days" for conversions. At the start of epoch it's day number zero, the next day is number 1 and so on. The appropriate dates for the ordinals differ for the types jdcal.Gregorian and jdcal.Julian, since the Julian calendar honors more often a February 29th.

Working with ordinals is probably not useful outside of the package, but here's a demo. The actual output (the numbers) may differ from what is shown here.

// main/demo7/demo7.go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/KarelKubat/jdcal"
)

func main() {
	for _, ymd := range []jdcal.YMD{
		// Near start of epoch
		{Year: -500, Month: time.January, Day: 1},

		// Around negative leap
		{Year: -301, Month: time.February, Day: 28},
		{Year: -301, Month: time.March, Day: 1},

		// Aroound positive leap
		{Year: 300, Month: time.February, Day: 28},
		{Year: 300, Month: time.March, Day: 1},

		// Somewhere in the 20th century
		{Year: 1962, Month: time.August, Day: 19},

		// Near end of epoch
		{Year: 2100, Month: time.January, Day: 1},
	} {
		for _, tp := range []jdcal.Type{jdcal.Gregorian, jdcal.Julian} {
			dt, err := jdcal.NewDate(ymd.Year, ymd.Month, ymd.Day, tp)
			check(err)
			ord, err := dt.Ordinal()
			check(err)
			back, err := ord.Date(dt.Type)
			check(err)

			fmt.Printf("%-22v --> %6d --> %v\n", dt, ord, back)

			eq, err := dt.Equal(back)
			check(err)
			if !eq {
				check(fmt.Errorf("%v and %v mismatch", dt, back))
			}

		}
	}

	// Output (actual oridnal number may differ):
	// Gregorian -0500/01/01  -->   2191 --> Gregorian -0500/01/01
	// Julian -0500/01/01     -->   2186 --> Julian -0500/01/01
	// Gregorian -0301/02/28  -->  74933 --> Gregorian -0301/02/28
	// Julian -0301/02/28     -->  74928 --> Julian -0301/02/28
	// Gregorian -0301/03/01  -->  74934 --> Gregorian -0301/03/01
	// Julian -0301/03/01     -->  74930 --> Julian -0301/03/01
	// Gregorian 0300/02/28   --> 294444 --> Gregorian 0300/02/28
	// Julian 0300/02/28      --> 294444 --> Julian 0300/02/28
	// Gregorian 0300/03/01   --> 294445 --> Gregorian 0300/03/01
	// Julian 0300/03/01      --> 294446 --> Julian 0300/03/01
	// Gregorian 1962/08/19   --> 901649 --> Gregorian 1962/08/19
	// Julian 1962/08/19      --> 901662 --> Julian 1962/08/19
	// Gregorian 2100/01/01   --> 951823 --> Gregorian 2100/01/01
	// Julian 2100/01/01      --> 951836 --> Julian 2100/01/01
}

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
More documentation

For more please see the generated docs at https://pkg.golang.ir/github.com/KarelKubat/jdcal.

Documentation

Overview

Package jdcal provides conversions from dates on the Julian calendar to the Gregorian calendar and vice versa, and knows about zones (geographical locations) where such switches occurred, and at which dates. Holidays that depend on moon phases (e.g., Easter) can be computed.

Furthermore helper functions are provided for small tasks, such as: date validation, is a year a leap year (which depends on the calendar type), comparisons between dates (before/after/equal), advancing a date while honoring leap years.

The distribution also contains a CLI main/jdcal/jdcal.go which provides a command line interface.

Index

Constants

View Source
const (
	StartProgressionYear = -500
	EndProgressionYear   = 2100
)

Variables

View Source
var ConversionTable = [...]ConversionEntry{

	{
		JDate: Date{Year: -501, Month: time.March, Day: 5, Type: Julian},
		GDate: Date{Year: -501, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: -501, Month: time.March, Day: 6, Type: Julian},
		GDate: Date{Year: -501, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: -301, Month: time.March, Day: 3, Type: Julian},
		GDate: Date{Year: -301, Month: time.February, Day: 27, Type: Gregorian},
	},
	{
		JDate: Date{Year: -301, Month: time.March, Day: 4, Type: Julian},
		GDate: Date{Year: -301, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: -301, Month: time.March, Day: 5, Type: Julian},
		GDate: Date{Year: -301, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: -201, Month: time.March, Day: 2, Type: Julian},
		GDate: Date{Year: -201, Month: time.February, Day: 27, Type: Gregorian},
	},
	{
		JDate: Date{Year: -201, Month: time.March, Day: 3, Type: Julian},
		GDate: Date{Year: -201, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: -201, Month: time.March, Day: 4, Type: Julian},
		GDate: Date{Year: -201, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: -101, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: -101, Month: time.February, Day: 27, Type: Gregorian},
	},
	{
		JDate: Date{Year: -101, Month: time.March, Day: 2, Type: Julian},
		GDate: Date{Year: -101, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: -101, Month: time.March, Day: 3, Type: Julian},
		GDate: Date{Year: -101, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 100, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 100, Month: time.February, Day: 27, Type: Gregorian},
	},
	{
		JDate: Date{Year: 100, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 100, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 100, Month: time.March, Day: 2, Type: Julian},
		GDate: Date{Year: 100, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 200, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 200, Month: time.February, Day: 27, Type: Gregorian},
	},
	{
		JDate: Date{Year: 200, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 200, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 200, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 200, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 300, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 300, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 300, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 300, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 300, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 300, Month: time.March, Day: 2, Type: Gregorian},
	},
	{
		JDate: Date{Year: 500, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 500, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 500, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 500, Month: time.March, Day: 2, Type: Gregorian},
	},
	{
		JDate: Date{Year: 500, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 500, Month: time.March, Day: 3, Type: Gregorian},
	},
	{
		JDate: Date{Year: 600, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 600, Month: time.March, Day: 2, Type: Gregorian},
	},
	{
		JDate: Date{Year: 600, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 600, Month: time.March, Day: 3, Type: Gregorian},
	},
	{
		JDate: Date{Year: 600, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 600, Month: time.March, Day: 4, Type: Gregorian},
	},
	{
		JDate: Date{Year: 700, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 700, Month: time.March, Day: 3, Type: Gregorian},
	},
	{
		JDate: Date{Year: 700, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 700, Month: time.March, Day: 4, Type: Gregorian},
	},
	{
		JDate: Date{Year: 700, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 700, Month: time.March, Day: 5, Type: Gregorian},
	},
	{
		JDate: Date{Year: 900, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 900, Month: time.March, Day: 4, Type: Gregorian},
	},
	{
		JDate: Date{Year: 900, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 900, Month: time.March, Day: 5, Type: Gregorian},
	},
	{
		JDate: Date{Year: 900, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 900, Month: time.March, Day: 6, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1000, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1000, Month: time.March, Day: 5, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1000, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1000, Month: time.March, Day: 6, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1000, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1000, Month: time.March, Day: 7, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1100, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1100, Month: time.March, Day: 6, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1100, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1100, Month: time.March, Day: 7, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1100, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1100, Month: time.March, Day: 8, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1300, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1300, Month: time.March, Day: 7, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1300, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1300, Month: time.March, Day: 8, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1300, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1300, Month: time.March, Day: 9, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1400, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1400, Month: time.March, Day: 8, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1400, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1400, Month: time.March, Day: 9, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1400, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1400, Month: time.March, Day: 10, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1500, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1500, Month: time.March, Day: 9, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1500, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1500, Month: time.March, Day: 10, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1500, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1500, Month: time.March, Day: 11, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1700, Month: time.February, Day: 18, Type: Julian},
		GDate: Date{Year: 1700, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1700, Month: time.February, Day: 19, Type: Julian},
		GDate: Date{Year: 1700, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1700, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1700, Month: time.March, Day: 10, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1700, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1700, Month: time.March, Day: 11, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1700, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1700, Month: time.March, Day: 12, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1800, Month: time.February, Day: 17, Type: Julian},
		GDate: Date{Year: 1800, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1800, Month: time.February, Day: 18, Type: Julian},
		GDate: Date{Year: 1800, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1800, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1800, Month: time.March, Day: 11, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1800, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1800, Month: time.March, Day: 12, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1800, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1800, Month: time.March, Day: 13, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1900, Month: time.February, Day: 16, Type: Julian},
		GDate: Date{Year: 1900, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1900, Month: time.February, Day: 17, Type: Julian},
		GDate: Date{Year: 1900, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1900, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 1900, Month: time.March, Day: 12, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1900, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 1900, Month: time.March, Day: 13, Type: Gregorian},
	},
	{
		JDate: Date{Year: 1900, Month: time.March, Day: 1, Type: Julian},
		GDate: Date{Year: 1900, Month: time.March, Day: 14, Type: Gregorian},
	},
	{
		JDate: Date{Year: 2100, Month: time.February, Day: 15, Type: Julian},
		GDate: Date{Year: 2100, Month: time.February, Day: 28, Type: Gregorian},
	},
	{
		JDate: Date{Year: 2100, Month: time.February, Day: 16, Type: Julian},
		GDate: Date{Year: 2100, Month: time.March, Day: 1, Type: Gregorian},
	},
	{
		JDate: Date{Year: 2100, Month: time.February, Day: 28, Type: Julian},
		GDate: Date{Year: 2100, Month: time.March, Day: 13, Type: Gregorian},
	},
	{
		JDate: Date{Year: 2100, Month: time.February, Day: 29, Type: Julian},
		GDate: Date{Year: 2100, Month: time.March, Day: 14, Type: Gregorian},
	},
}

ConversionTable defines matching Julian and Gregorian dates. It is consulted by, e.g., Date.Convert(). The contained ConversionEntry's won't normally be of use outside of this module, but they are coded as exportable (as uppercase symbols) so they can be inspected.

This table reflects https://en.wikipedia.org/wiki/Conversion_between_Julian_and_Gregorian_calendars, original source: the Nautical almanac of the United Kingdom and United States (1961). This table however knows that year zero doesn't exist (we go 2BC, 1BC, 1AD, 2AD), therefore, years before 0 are generated one-off relative to the above source refrence.

View Source
var FullMoons = map[Year]MD{}/* 2599 elements not displayed */

FullMoons maps years to a month and day (MD) representing the first full moon beyond March 21st. The date points are relative to the Georgian calendar. Example:

  md, ok := jdcal.FullMoons[1600]
  if !ok {
	log.Fatal("no full moon information for year 1600")
  }
  fmt.Println("first full moon beyond March 21st in the year 1600 is on", md)  // 03/29

The data were obtained from https://astropixels.com/ephemeris/phasescat/phasescat.html Moon Phases Table courtesy of Fred Espenak, www.Astropixels.com.

The data here differ from www.Astropixels.com in that:

- Years are stated as BC or AD - so -3, -2, -1, 1, 2, 3, etc.. There is no year zero. Astropixels.com knows a year zero and has -2, -1, 0, 1, 2, 3.

- All dates are relative to the Gregorian calendar. Astropixels.com uses Julian for pre-1582, this is converted to Gregorian for clarity.

View Source
var LeapMonthProgression = MonthProgression{
	time.January: {
		0,
		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
	},
	time.February: {
		0,
		31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
	},
	time.March: {
		0,
		60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
	},
	time.April: {
		0,
		91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
	},
	time.May: {
		0,
		121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151,
	},
	time.June: {
		0,
		152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181,
	},
	time.July: {
		0,
		182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212,
	},
	time.August: {
		0,
		213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243,
	},
	time.September: {
		0,
		244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273,
	},
	time.October: {
		0,
		274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304,
	},
	time.November: {
		0,
		305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334,
	},
	time.December: {
		0,
		335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365,
	},
}

LeapMonthProgression holds per month the ordinal of a day, January 1st being 0, December 31st being 365 (365 days in a year, plus February 29th is 366, so ordinal number 365).

View Source
var NonLeapMonthProgression = MonthProgression{
	time.January: {
		0,
		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
	},
	time.February: {
		0,
		31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
	},
	time.March: {
		0,
		59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
	},
	time.April: {
		0,
		90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
	},
	time.May: {
		0,
		120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150,
	},
	time.June: {
		0,
		151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180,
	},
	time.July: {
		0,
		181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
	},
	time.August: {
		0,
		212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242,
	},
	time.September: {
		0,
		243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272,
	},
	time.October: {
		0,
		273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303,
	},
	time.November: {
		0,
		304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333,
	},
	time.December: {
		0,
		334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364,
	},
}

NonLeapMonthProgression holds per month the ordinal of a day, January 1st being 0, December 31st being 364 (365 days in a year, so ordinal number 364).

View Source
var YearProgression = map[Year][2]Ordinal{}/* 2613 elements not displayed */

YearProgression holds the number of days since jdcal.StartProgressionYear. The first int in each entry is for jdcal.Gregorian, the second for jdcal.Julian.

View Source
var ZonesTable = [...]ZoneEntry{}/* 114 elements not displayed */

ZonesTable defines matching geographical zones to the date of adoption of the Gregorian calendar. Some zones temporarily reverted to the Julian calendar, these have multiple cutover dates.

This table reflects https://en.wikipedia.org/wiki/List_of_adoption_dates_of_the_Gregorian_calendar_by_country.

Note: The Cutovers entries are the dates where that period STOPPED. So: "{Year: 1918, Month: time.April, Day: 17, Type: Julian}" is the Julian date 1918/04/16, when the Julian calendar was abandoned in favor of "the other one", being the Gregorian.

Another note: All symbols of the table are exported (upper case). You can skip reading the table; rather, have a look at the "Zones*()" functions that can do the lifting.

Functions

This section is empty.

Types

type CalendarYear added in v0.0.4

type CalendarYear struct {
	Year Year
	Type Type
}

CalendarYear wraps a Year with a calendar Type (Julian or Gregorian).

func NewCalendarYear added in v0.0.12

func NewCalendarYear(y Year, tp Type) (CalendarYear, error)

NewCalendarYear returns a CalendarYear for a given year and calendar type.

func (CalendarYear) DaysPerMonth added in v0.0.8

func (c CalendarYear) DaysPerMonth() []int

DaysPerMonth returns for each time.Month the number of days, taking into account the calendar type (Julian or Gregorian) and leap years. Example:

cyr, err := NewCalendarYear(1900, jdcal.Julian)
if err != nil { ... }
dpm := cyr.DaysPerMonth()
fmt.Println(dpm[time.February])

func (CalendarYear) HolidayDate added in v0.0.6

func (cyr CalendarYear) HolidayDate(h Holiday) (Date, error)

HolidayDate returns for a given CalendarYear the Date of one of the Holidays. The type of the returned date matches the type of the calendar year. E.g., if the calendar year is Julian 1370, then the returned date is also on the Julian calendar.

Easter is defined as the next Sunday beyond the ecclesiastical spring equinox of March 21st. If this equinox is a Sunday, then the next one is taken.

Ascension Day is on the 40th day after Easter (so plus 39), making it a Thursday.

Pentecost is on the 50th day after Easter (so plus 49), making it a Sunday.

func (CalendarYear) IsLeap added in v0.0.4

func (cYear CalendarYear) IsLeap() bool

IsLeap is true when a year indicate that that year should be a leap year for the given calendar type. IsLeap implements the following definition:

https://en.wikipedia.org/wiki/Leap_year:

The historic Julian calendar has three common years of 365 days followed by a leap year of 366 days, by extending February to 29 days rather than the common 28.

The Gregorian calendar, the world's most widely used civil calendar, makes a further adjustment for the small error in the Julian algorithm. Again each leap year has 366 days instead of 365. This extra leap day occurs in each year that is an integer multiple of 4 (except for years evenly divisible by 100, but not by 400).

Example:

var cyr jdcal.CalendarYear

cyr = {
	Year: 1900,
	Type: jdcal.Julian,
}
fmt.Println(cyr.IsLeap())  // true
cyr.Type = jdcal.Gregorian
fmt.Println(cyr.IsLeap())  // false

func (CalendarYear) String added in v0.0.4

func (c CalendarYear) String() string

String returns the readable representation for a CalendarYear, e.g. "1600 Julian".

type ConversionAlgorithm added in v0.0.13

type ConversionAlgorithm int

ConversionAlgoritm (an int) represents the applicable conversion algorithm.

const (
	ByProgression ConversionAlgorithm // Default conversion algorithm, fast
	ByLookup                          // Slow conversion algorithm, but thoroughly tested

)

func (ConversionAlgorithm) String added in v0.0.13

func (c ConversionAlgorithm) String() string

type ConversionEntry

type ConversionEntry struct {
	JDate, GDate Date
}

ConversionEntry wraps one Julian and one Gregorian date. The ConversionTable is an array of such entries.

type Date

type Date struct {
	Year  Year
	Month time.Month
	Day   int
	Type  Type
}

Date wraps a year, month, day and calendar type (Julian or Gregorian); basically a YMD with a type.

func First

func First(tp Type) Date

First returns the first convertible day for a given type. Dates before this cannot be converted; Convert() would throw an error. This is a limitation of the ConversionTable. Example:

jd := jdcal.First(jdcal.Julian)  // First convertible date
gd, err := jd.Convert()          // err will be nil, jd can be converted

The first convertible dates point to the same day, despite being different day numbers on the Julian or Gregorian calendars.

func Last

func Last(tp Type) Date

Last returns the last convertible date for a given type. Dates after this cannot be converted; Convert() would throw an error. This is a limitation of the ConversionTable. Example:

gd := jdcal.Last(jdcal.Gregorian)  // Last convertible date
gd = gd.Forward()                  // Move 1 day forward
jd, err := gd.Convert()            // err will be set, gd cannot be converted

func NewDate added in v0.0.12

func NewDate(year Year, month time.Month, day int, tp Type) (dt Date, err error)

NewDate is a helper function to construct a Date from a year, month, day and calendar type. The two snippets are equivalent:

d, err := jdcal.New(1962, time.August, 19, jdcal.Gregorian)
if err != nil {...}

d := Date{Year: 1962, Month: time.August, Day: 19, Type: jdcal.Gregorian}
err := d.Valid()
if err {...}

func NewDateFromString added in v0.0.12

func NewDateFromString(arg string, tp Type) (dt Date, err error)

NewFromString is a helper function to convert a string in the format "YYYY/MM/DD" into a date of a given Type (Julian or Gregorian). It chaines StringToYMD() and New().

func (Date) Advance

func (d Date) Advance() Date

Advance returns a date that is one day later, honoring leap days for Julian or Gregorian calendars. The reference date (receiver) is not modified. Example:

jd0, err := jdcal.Date(1900, time.February, 28, jdcal.Julian)
if err != nil {...}
jd1 := jd0.Advance()  // Copy of jd but a day ahead
fmt.Println(jd1)      // February 29th

gd, err := jdcal.Date(1900, time.February, 28, jdcal.Gregorian)
if err != nil {...}
gd = gd.Advance()     // advance gd itself
fmt.Println(gd)       // March 1st

func (Date) After

func (d Date) After(other Date) (bool, error)

After is true when the date in question occurs later than the other date. Note that different date types raise an error. When comparing different date types, the caller must first convert:

jd, err := jdcal.New(1666, time.March, 13, jdcal.Julian) // a Julian date in 1666
if err != nil {...}

gd, err := jdcal.New(2023, time.December, 4, jdcal.Gregorian) // a Gregorian date in 2023
if err != nil {...}

gdTmp, err := jd.Convert() // jd as a Gregorian date
if err != nil {...}
fmt.Println(gd.After(gdTmp)) // true; the 2023 date comes after the 1666 date

func (Date) AfterOrEqual added in v0.0.1

func (d Date) AfterOrEqual(other Date) (bool, error)

AfterOrEqual is true when the date in question occurs later than the other date. Comparing dates of different types raises an error, see Equal().

func (Date) AsTime added in v0.0.2

func (d Date) AsTime() (tm time.Time, err error)

AsTime returns the jdcal.Date as a time.Time type, so that one can apply all standard functions of https://pkg.golang.ir/time#Time. The time coordinates are pinned at noon, UTC.

The date type (jdcal.Julian or jdcal.Gregorian) does not matter; the date is converted if needed (time.Time is, by definition, Gregorian).

func (Date) Backward added in v0.0.8

func (d Date) Backward() Date

Backward returns a date that is one day before, hononing leap years for Julian or Gregorian calendars. The reference date (receiver) is not modified. Example:

jd0, err := jdcal.Date(1900, time.March, 1, jdcal.Julian)
if err != nil {...}
jd1 := jd0.Backward()  // Copy of jd but a day before
fmt.Println(jd1)       // February 29th

gd, err := jdcal.Date(1900, time.March, 1, jdcal.Gregorian)
if err != nil {...}
gd = gd.Backward()    // Decrease gd itself
fmt.Println(gd)       // February 28th

func (Date) Before

func (d Date) Before(other Date) (bool, error)

Before is true when the date in question occurs earlier than the other date. Note that different date types raise an error, see After().

func (Date) BeforeOrEqual added in v0.0.1

func (d Date) BeforeOrEqual(other Date) (bool, error)

BeforeOrEqual is true when the date in question occurs earlier than the other date or exactly on that other date. Note that different date types raise an error, see After().

func (Date) Convert

func (d Date) Convert() (Date, error)

Convert converts a jdcal.Date to the "other" format: from Julian to Gregorian, or vv. Example:

jd, err := jdcal.New(1712, time.February, 19, jdcal.Julian)
if err != nil {...}
gd, err := jd.Convert()
if err != nil {...}
fmt.Println(gd) // Gregorian 1712/03/01

func (Date) Equal

func (d Date) Equal(other Date) (bool, error)

Equal is true when two dates point to the same day. Different date types cannot be compared, see After().

func (Date) Forward added in v0.0.8

func (d Date) Forward() Date

Forward returns a date that is one day later, honoring leap days for Julian or Gregorian calendars. The reference date (receiver) is not modified. Example:

jd0, err := jdcal.Date(1900, time.February, 28, jdcal.Julian)
if err != nil {...}
jd1 := jd0.Forward()  // Copy of jd but a day ahead
fmt.Println(jd1)      // February 29th

gd, err := jdcal.Date(1900, time.February, 28, jdcal.Gregorian)
if err != nil {...}
gd = gd.Forward()     // Advance gd itself
fmt.Println(gd)       // March 1st

func (Date) InZone added in v0.0.1

func (d Date) InZone(z ZoneEntry) (bool, error)

InZone returns true when the date in question matches in the calendar progression as defined by the ZoneEntry. For example, assuming that the ZoneEntry is:

{
	Name: "Netherlands (Groningen City)",
	Cutovers: []Date{
		{Year: -500, Month: time.February, Day: 28, Type: Gregorian}  // started using Julian
		{Year: 1583, Month: time.January, Day: 1, Type: Julian},      // switched to Gregorian
		{Year: 1594, Month: time.November, Day: 10, Type: Gregorian}, // switched to Julian
		{Year: 1700, Month: time.December, Day: 31, Type: Julian},    // switched to Gregorian
	},
}

Stated differently, the zone is defined as follows:

The zone calendar started on  Gregorian -500/02/28, the next day was Julian date
Julian    1583/01/01  became  Gregorian 1583/01/11, the next day was a Gregorian date
Gregorian 1594/11/10  became  Julian    1594/10/31, the next day was a Julian date
Julian    1700/12/31  became  Gregorian 1701/01/11, the next day was a Gregorian date

Regarding which dates are possible in the zone, the following applies:

1580/01/01 occurs in the zone as a Julian date, but not as as a Gregorian
1590/01/01 occurs in the zone as a Gregorian date, but not as a Julian
1600/01/01 occurs in the zone as a Julian date, but not as a Gregorian
1800/01/01 occurs in the zone as a Gregorian date, but not as a Julian

Example:

jd, err := jdcal.New(1580, 1, 1, jdcal.Julian)
if err != nil { ... }
in, err := jd.InZone(zoneEntry)  // obtained using jdcal.ZonesByname("Groningen City")
if err != nil { ... }
fmt.Println(in)                 // true

gd, err := jdcal.New(1580, 1, 1, jdcal.Gregorian)
if err != nil { ... }
in, err := gd.InZone(zoneEntry)
if err != nil { ... }
fmt.Println(in)                 // false

Just around the cutover dates, the following applies. Around the cutover from Gregorian 1594/11/10 to Julian 1594/10/31:

- 1594/11/09 can be both a Gregorian and a Julian date. Gregorian, because it's one day before the switch over. But it can also be a Julian date, when it points 9 days beyond this switch over.

- 1594/11/10 can be both a Julian and a Gregorian date. Gregorian, because it's the switch over date. Julian, because it points 10 days beyond the switch over.

- 1594/11/11 can only be a Julian date; it doesn't exist in the Gregorian calendar in this zone, but points to the Julian calendar 11 days beyond the switch over.

Around the cutover from Julian 1700/12/31 to Gregorian Gregorian 1701/01/11, the following applies:

- 1700/12/30 and 1700/12/31 must be a Julian dates, there is no Gregorian representation. Gregorian 1700/12/30 would mean Julian 1700/12/19, and that's before the cutover.

- 1701/01/01 can't be either, it's a lost date. In the zone, the Julian calendar ends on 1700/12/31 but the Gregorian only starts on 1594/10/31.

func (Date) IsSet added in v0.0.1

func (d Date) IsSet() bool

IsSet returns true when any of the fields Year, Month or Day of a given date have a non-zero value. IsSet is false when a date is not initialized. Example:

d := Date{}
fmt.Println(d.IsSet())  // false

d.Day = 1
fmt.Println(d.IsSet())  // true

func (Date) Ordinal added in v0.0.11

func (d Date) Ordinal() Ordinal

Ordinal returns the ordinal day for a given date. This is a daycount since "start of epoch", liberally defined as the constant StartProgressionYear. This is the reverse of OrdinalToDate.

func (Date) String

func (d Date) String() string

String returns a printable version of a date in the format "TYPE YYYY/MM/DD", e.g. "Julian 1234/12/27". The year may be prefixed by a - to indicate negative values.

func (Date) Valid

func (d Date) Valid() error

Valid returns an error when a date cannot be processed. The date must not exceed the maximum number of month days (e.g., April 31st is wrong, February 29th may only occur in leap years) and Convert() must be able to process it: it can't be outside of the range [Date.First() .. Date.Last()].

func (Date) Weekday added in v0.0.1

func (d Date) Weekday() (wd time.Weekday, err error)

Weekday returns the day of the week (time.Sunday, time.Monday etc.) for the given date. The date type (jdcal.Julian or jdcal.Gregorian) doesn't matter; if needed, the date is converted before determining the weekday.

The following two snippets are equivalent:

dt, err := jdcal.New(...)
if err != nil { ... }

// Alternative 1
wd, err := dt.Weekday()
if err != nil { ... }

// Alternative 2
tm, err := dt.AsTime()
if err != nil { ... }
wd := tm.Weekday()

type Holiday added in v0.0.4

type Holiday int

Holiday enumarates yearly holidays.

const (
	AshWednesday Holiday
	GoodFriday
	Easter
	Ascension
	Pentecost
)

func (Holiday) String added in v0.0.6

func (h Holiday) String() string

String returns the string representation for a Holiday: "Easter", "Ascension day", etc.

type MD added in v0.0.10

type MD struct {
	Month time.Month
	Day   int
}

MD is a representation of a month and a day; basically a YMD without a year.

func (MD) String added in v0.0.10

func (m MD) String() string

type MonthProgression added in v0.0.11

type MonthProgression map[time.Month][]Ordinal

MonthProgression maps arrays of ordinals to months. There is such a table for leap years (LeapMonthProgression) and for non-leap years (NonLeapMonthProgression).

type Ordinal added in v0.0.11

type Ordinal int

Ordinal (an int) represents the day number since epoch start.

func (Ordinal) Date added in v0.0.11

func (o Ordinal) Date(tp Type) (Date, error)

Date returns the jdcal.Date for a given ordinal day number. This is the reverse of Date.Ordinal.

func (Ordinal) Year added in v0.0.11

func (o Ordinal) Year(tp Type) Year

Year returns the best matching year for a given ordinal.

type Type

type Type int

Type (an int) defines the calendar type for a jdcal.Date or a jdcal.CalendarYear: jdcal.Gregorian or jdcal.Julian.

const (
	Gregorian Type = iota // jdcal.Gregorian indicates a date on the Gregorian calendar
	Julian                // jdcal.Julian indicates a date on the Julian calendar
)

func (Type) Other added in v0.0.11

func (tp Type) Other() Type

Other returns the "other" calendar type.

func (Type) String

func (t Type) String() string

String returns "Gregorian" for jdcal.Gregorian or "Julian" for jdcal.Julian.

type YMD added in v0.0.10

type YMD struct {
	Year  Year
	Month time.Month
	Day   int
}

YMD is a representation of a year, month and a day; basically a Date without a type.

func StringToYMD added in v0.0.1

func StringToYMD(arg string) (ymd YMD, err error)

StringToYMD is a simple string parser to convert a date in the format "YYYY/MM/DD" to a separate year, month and day. The separator is a slash, to allow for easier negative years (as in "-25/02/28"). An error occurs when the parts in the argument cannot be converted; the validity of the date is not checked (so "2020/12/100" would pass).

func (YMD) String added in v0.0.10

func (y YMD) String() string

type Year added in v0.0.4

type Year int

Year is an integer and the receiver type for some helper functions.

func (Year) String added in v0.0.4

func (y Year) String() string

String returns the string representation for a year, left-padded with zeroes over four positions, and if needed prefixed with a minus sign.

func (Year) Valid added in v0.0.4

func (y Year) Valid() error

Valid returns an error when a year points outside of the known dates of the conversion table, or when the year is zero (there is no zero in BC/AD).

type ZoneEntry added in v0.0.1

type ZoneEntry struct {
	Name     string // Zone name, e.g. "Denmark"
	Cutovers []Date // List of dates when the zone switched from a given calendar
}

ZoneEntry wraps a zone name with the dates where that zone switched to the Gregorian calendar, possibly later back to the Gregorian, etc. The ZoneTable is an array of such entries.

The CutOvers list is an array of Dates where this zone switched FROM a calendar TO another one. E.g., when CutOvers is:

Cutovers: []Date{
	// -500 to 1584: Julian calendar applies
	// 1584 to 1597: Switched to Gregorian
	// 1597 to 1798: Switched back to Julian
	// 1798 to now:  Ended up with Gregorian
	{Year: -500, Month: time.February, Day: 28, Type: Gregorian},
	{Year: 1584, Month: time.January, Day: 1, Type: Julian},
	{Year: 1597, Month: time.January, Day: 1, Type: Gregorian},
	{Year: 1798, Month: time.December, Day: 25, Type: Julian},
}

Then that means:

- The calendar for this zone starts at -500/02/28 Gregorian (or: -500 March 1st Julian, convert yourself if you think that is handy). On that day the zone stopped using the Gregorian, and started using the Julian calendar.

- On 1584/01/01 Julian, the zone switched to Gregorian ("the other one").

- On 1597/01/01 Gregorian, the zone switched to Julian. So they switched back.

- Finally, on 1798/12/25, they switched again. To Gregorian.

func SingleZone added in v0.0.9

func SingleZone(n string) (z ZoneEntry, err error)

SingleZone returns a ZoneEntry matching a name, or an error when nothing matches, or when multiple zones match.

Example:

var zn jdcal.ZoneEntry
var err error

zn, err = jdcal.SingleZone("netherlands")  // error: >1 matches
zn, err = jdcal.SingleZone("gelderland")   // nil error, zn is valid
zn, err = jdcal.SingleZone("xyzzy")        // error: 0 matches

func ZonesByName added in v0.0.1

func ZonesByName(n string) []ZoneEntry

ZonesByname returns a list of ZoneEntry's matching the input argument. The matching is done without regard to case. E.g., ZonesByName("netherlands") will return zones for "Belgium (Southern Netherlands)", "Netherlands (Brabant)" and ~7 more.

Example:

fmt.Println(jdcal.ZonesByName("netherlands"))

// Output:
// Belgium (Southern Netherlands)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1582/12/20
// Netherlands (Brabant)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1582/12/14
// Netherlands (Drenthe)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1701/04/30
// Netherlands (Frisia)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1701/12/31
// Netherlands (Gelderland)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1700/06/12
// Netherlands (Groningen City)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1583/01/01
//   Switched to   the Julian    calendar   on   Gregorian 1594/11/10
//   Switched to   the Gregorian calendar   on   Julian 1700/12/31
// Netherlands (Holland)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1583/01/01
// Netherlands (Utrecht, Overijssel)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1700/11/30
// Netherlands (Zeeland, States General)
//   Started using the Julian    calendar   on   Gregorian -0500/02/28
//   Switched to   the Gregorian calendar   on   Julian 1582/12/14

func (ZoneEntry) String added in v0.0.1

func (z ZoneEntry) String() string

String returns a human-readable representation of a ZoneEntry. The returned string is not usable for machine parsing, it is only meant for human consumption. Example:

Switzerland (Appenzell Ausserrhoden)
  Started using the Julian    calendar   on   Gregorian -0500/02/28
  Switched to   the Gregorian calendar   on   Julian 1584/01/01
  Switched to   the Julian    calendar   on   Gregorian 1597/01/01
  Switched to   the Gregorian calendar   on   Julian 1798/12/25

The first line is the start of the calendar. This will in most cases be the "start of time recording", except e.g., China where before any supported calendar was used, dates would be in a not-supported format. China will have "Started using the Gregorian calendar on ..." and nothing else:

China
  Started using the Gregorian calendar   on   Julian 1911/12/01

Next entries, when present, are switches. In the example of Switzerland (Appenzell Ausserrhoden):

- The zone switched to the Gregorian calendar on 1584/01/01 (which must be a Julian date, because they were switching).

- Then they switched back to Julian on 1597/01/01 (on the Gregorian calendar, because they were switching).

- Etc..

Directories

Path Synopsis
main
demo1
main/demo1/demo1.go
main/demo1/demo1.go
demo2
main/demo2/demo2.go
main/demo2/demo2.go
demo3
main/demo3/demo3.go
main/demo3/demo3.go
demo4
main/demo4/demo4.go
main/demo4/demo4.go
demo5
main/demo5/demo5.go
main/demo5/demo5.go
demo6
main/demo6/demo6.go
main/demo6/demo6.go
demo7
main/demo7/demo7.go
main/demo7/demo7.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL