Write a program to convert dates between the Gregorian calendar and the French Republican calendar.

The year 1 of the Republican calendar began on 22 September 1792. There were twelve months (Vendémiaire, Brumaire, Frimaire, Nivôse, Pluviôse, Ventôse, Germinal, Floréal, Prairial, Messidor, Thermidor, and Fructidor) of 30 days each, followed by five intercalary days or Sansculottides (Fête de la vertu / Virtue Day, Fête du génie / Talent Day, Fête du travail / Labour Day, Fête de l'opinion / Opinion Day, and Fête des récompenses / Honours Day). In leap years (the years 3, 7, and 11) a sixth Sansculottide was added: Fête de la Révolution / Revolution Day.

As a minimum, your program should give correct results for dates in the range from 1 Vendémiaire 1 = 22 September 1792 to 10 Nivôse 14 = 31 December 1805 (the last day when the Republican calendar was officially in use). If you choose to accept later dates, be aware that there are several different methods (described on the Wikipedia page) about how to determine leap years after the year 14. You should indicate which method you are using. (Because of these different methods, correct programs may sometimes give different results for dates after 1805.)

Test your program by converting the following dates both from Gregorian to Republican and from Republican to Gregorian:

• 1 Vendémiaire 1 = 22 September 1792

• 1 Prairial 3 = 20 May 1795

• 27 Messidor 7 = 15 July 1799 (Rosetta Stone discovered)

• Fête de la Révolution 11 = 23 September 1803

• 10 Nivôse 14 = 31 December 1805

require('DateTime')
 
var month_names = %w(
  Vendémiaire Brumaire Frimaire  Nivôse   Pluviôse  Ventôse
  Germinal    Floréal  Prairial  Messidor Thermidor Fructidor
)
 
var intercalary = [
                   'Fête de la vertu',
                   'Fête du génie',
                   'Fête du travail',
                   "Fête de l'opinion",
                   'Fête des récompenses',
                   'Fête de la Révolution',
                  ]
 
var i_cal_month = 13
var epoch = %O<DateTime>.new(year => 1792, month => 9, day => 22)
 
var month_nums = Hash(month_names.kv.map {|p| [p[1], p[0]+1] }.flat...)
var i_cal_nums = Hash(intercalary.kv.map {|p| [p[1], p[0]+1] }.flat...)
 
func is_republican_leap_year(Number year) -> Bool {
    var y = (year + 1)
    !!(4.divides(y) && (!100.divides(y) || 400.divides(y)))
}
 
func Republican_to_Gregorian(String rep_date) -> String {
    static months   = month_names.map { .escape }.join('|')
    static intercal = intercalary.map { .escape }.join('|')
    static re       = Regex("^
        \\s* (?:
                (?<ic> #{intercal})
              | (?<day> \\d+) \\s+ (?<month> #{months})
            )
        \\s+ (?<year> \\d+)
        \\s*
      \\z", 'x')
 
    var m = (rep_date =~ re)
    m || die "Republican date not recognized: '#{rep_date}'"
 
    var ncap = m.named_captures
 
    var day1   = Number(ncap{:ic}    ? i_cal_nums{ncap{:ic}}    : ncap{:day})
    var month1 = Number(ncap{:month} ? month_nums{ncap{:month}} : i_cal_month)
    var year1  = Number(ncap{:year})
 
    var days_since_epoch = (365*(year1 - 1) + 30*(month1 - 1) + (day1 - 1))
 
    var leap_days = (1 ..^ year1 -> grep { is_republican_leap_year(_) })
    epoch.clone.add(days => (days_since_epoch + leap_days)).strftime("%Y-%m-%d")
}
 
func Gregorian_to_Republican(String greg_date) -> String {
    var m = (greg_date =~ /^(\d{4})-(\d{2})-(\d{2})\z/)
    m || die "Gregorian date not recognized: '#{greg_date}'"
 
    var g = %O<DateTime>.new(year => m[0], month => m[1], day => m[2])
    var days_since_epoch = epoch.delta_days(g).in_units('days')
    days_since_epoch < 0 && die "unexpected error"
    var (year, days) = (1, days_since_epoch)
 
    loop {
        var year_length = (365 + (is_republican_leap_year(year) ? 1 : 0))
        days < year_length && break
        days -= year_length
        year += 1;
    }
 
    var day0   = (days % 30)
    var month0 = (days - day0)/30
 
    var (day1, month1) = (day0 + 1, month0 + 1)
 
    (month1 == i_cal_month
        ? "#{intercalary[day0]} #{year}"
        : "#{day1} #{month_names[month0]} #{year}")
}
 
for line in DATA {
 
    line.sub!(/\s*\#.+\R?\z/, '')
 
    var m = (line =~ /^(\d{4})-(\d{2})-(\d{2})\s+(\S.+?\S)\s*$/)
    m || die "error for: #{line.dump}"
 
    var g = "#{m[0]}-#{m[1]}-#{m[2]}"
    var r = m[3]
 
    var r2g = Republican_to_Gregorian(r)
    var g2r = Gregorian_to_Republican(g)
 
    #say "#{r} -> #{r2g}"
    #say "#{g} -> #{g2r}"
 
    if ((g2r != r) || (r2g != g)) {
        die "1-way error"
    }
 
    if ((Gregorian_to_Republican(r2g) != r) ||
        (Republican_to_Gregorian(g2r) != g)
    ) {
        die "2-way error"
    }
}
 
say 'All tests successful.'
 
__DATA__
1792-09-22  1 Vendémiaire 1
1795-05-20  1 Prairial 3
1799-07-15  27 Messidor 7
1803-09-23  Fête de la Révolution 11
1805-12-31  10 Nivôse 14
1871-03-18  27 Ventôse 79
1944-08-25  7 Fructidor 152
2016-09-19  Fête du travail 224
1871-05-06  16 Floréal 79   # Paris Commune begins
1871-05-23  3 Prairial 79   # Paris Commune ends
1799-11-09  18 Brumaire 8   # Revolution ends by Napoléon coup
1804-12-02  11 Frimaire 13  # Republic   ends by Napoléon coronation
1794-10-30  9 Brumaire 3    # École Normale Supérieure established
1794-07-27  9 Thermidor 2   # Robespierre falls
1799-05-27  8 Prairial 7    # Fromental Halévy born
1792-09-22  1 Vendémiaire 1
1793-09-22  1 Vendémiaire 2
1794-09-22  1 Vendémiaire 3
1795-09-23  1 Vendémiaire 4
1796-09-22  1 Vendémiaire 5
1797-09-22  1 Vendémiaire 6
1798-09-22  1 Vendémiaire 7
1799-09-23  1 Vendémiaire 8
1800-09-23  1 Vendémiaire 9
1801-09-23  1 Vendémiaire 10
1802-09-23  1 Vendémiaire 11
1803-09-24  1 Vendémiaire 12
1804-09-23  1 Vendémiaire 13
1805-09-23  1 Vendémiaire 14
1806-09-23  1 Vendémiaire 15
1807-09-24  1 Vendémiaire 16
1808-09-23  1 Vendémiaire 17
1809-09-23  1 Vendémiaire 18
1810-09-23  1 Vendémiaire 19
1811-09-24  1 Vendémiaire 20
2015-09-23  1 Vendémiaire 224
2016-09-22  1 Vendémiaire 225
2017-09-22  1 Vendémiaire 226