# -*- coding: utf-8 -*- # This file is part of convertdate. # http://github.com/fitnr/convertdate # Licensed under the MIT license: # http://opensource.org/licenses/MIT # Copyright (c) 2016, fitnr from math import trunc from pymeeus.Sun import Sun from . import gregorian from .data.french_republican_days import french_republican_days # julian day (1792, 9, 22) EPOCH = 2375839.5 YEAR_EPOCH = 1791. DAYS_IN_YEAR = 365. MOIS = [ u'Vendémiaire', u'Brumaire', u'Frimaire', u'Nivôse', u'Pluviôse', u'Ventôse', u'Germinal', u'Floréal', u'Prairial', u'Messidor', u'Thermidor', u'Fructidor', u'Sansculottides' ] LEAP_CYCLE_DAYS = 1461. # 365 * 4 + 1 LEAP_CYCLE_YEARS = 4. def leap(year, method=None): ''' Determine if this is a leap year in the FR calendar using one of three methods: 4, 100, 128 (every 4th years, every 4th or 400th but not 100th, every 4th but not 128th) Methods: * 4 (concordance rule): leap every four years: 3, 7, 11, 15, ... etc * 100 (Romme's rule): leap every 4th and 400th year, but not 100th: 20, 24, ... 96, 104, ... 396, 400, 404 ... * 128 (von Mädler's rule): leap every 4th but not 128th: 20, 24, ... 124, 132, ... * equinox [default]: use calculation of the equinox to determine date, never returns a leap year ''' method = method or 'equinox' if year in (3, 7, 11): return True if year < 15: return False if method in (4, 'continuous') or (year <= 16 and method in (128, 'madler', 4, 'continuous')): return year % 4 == 3 if method in (100, 'romme'): return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 if method in (128, 'madler'): return year % 4 == 0 and year % 128 != 0 if method == 'equinox': # Is equinox on 366th day after (year, 1, 1) startjd = to_jd(year, 1, 1, method='equinox') if premier_da_la_annee(startjd + 367) - startjd == 366.0: return True else: raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox") return False def _previous_fall_equinox(jd): '''Return the julian day count of the previous fall equinox.''' y, _, _ = gregorian.from_jd(jd) eqx = Sun.get_equinox_solstice(y, "autumn").jde() if eqx > jd: eqx = Sun.get_equinox_solstice(y - 1, "autumn").jde() return eqx def _next_fall_equinox(jd): '''Return the julian day count of the previous fall equinox.''' y, _, _ = gregorian.from_jd(jd) eqx = Sun.get_equinox_solstice(y, "autumn").jde() if eqx < jd: eqx = Sun.get_equinox_solstice(y + 1, "autumn").jde() return eqx def premier_da_la_annee(jd): ''' Returns Julian day number containing fall equinox (first day of the FR year) of the current FR year. ''' previous = trunc(_previous_fall_equinox(jd) - 0.5) + 0.5 if previous + 364 < jd: # test if current day is the equinox if the previous equinox was a long time ago nxt = trunc(_next_fall_equinox(jd) - 0.5) + 0.5 if nxt <= jd: return nxt return previous def to_jd(year, month, day, method=None): '''Obtain Julian day from a given French Revolutionary calendar date.''' method = method or 'equinox' if day < 1 or day > 30: raise ValueError("Invalid day for this calendar") if month > 13: raise ValueError("Invalid month for this calendar") if month == 13 and day > 5 + leap(year, method=method): raise ValueError("Invalid day for this month in this calendar") if method == 'equinox': return _to_jd_equinox(year, month, day) return _to_jd_schematic(year, month, day, method) def _to_jd_schematic(year, month, day, method): '''Calculate JD using various leap-year calculation methods''' y0, y1, y2, y3, y4, y5 = 0, 0, 0, 0, 0, 0 intercal_cycle_yrs, over_cycle_yrs, leap_suppression_yrs = None, None, None # Use the every-four-years method below year 16 (madler) or below 15 (romme) if ((method in (100, 'romme') and year < 15) or (method in (128, 'madler') and year < 17)): method = 4 if method in (4, 'continuous'): # Leap years: 15, 19, 23, ... y5 = -365 elif method in (100, 'romme'): year = year - 13 y5 = DAYS_IN_YEAR * 12 + 3 leap_suppression_yrs = 100. leap_suppression_days = 36524 # leap_cycle_days * 25 - 1 intercal_cycle_yrs = 400. intercal_cycle_days = 146097 # leap_suppression_days * 4 + 1 over_cycle_yrs = 4000. over_cycle_days = 1460969 # intercal_cycle_days * 10 - 1 elif method in (128, 'madler'): year = year - 17 y5 = DAYS_IN_YEAR * 16 + 4 leap_suppression_days = 46751 # 32 * leap_cycle_days - 1 leap_suppression_yrs = 128 else: raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox") if over_cycle_yrs: y0 = trunc(year / over_cycle_yrs) * over_cycle_days year = year % over_cycle_yrs # count intercalary cycles in days (400 years long or None) if intercal_cycle_yrs: y1 = trunc(year / intercal_cycle_yrs) * intercal_cycle_days year = year % intercal_cycle_yrs # count leap suppresion cycles in days (100 or 128 years long) if leap_suppression_yrs: y2 = trunc(year / leap_suppression_yrs) * leap_suppression_days year = year % leap_suppression_yrs y3 = trunc(year / LEAP_CYCLE_YEARS) * LEAP_CYCLE_DAYS year = year % LEAP_CYCLE_YEARS # Adjust 'year' by one to account for lack of year 0 y4 = year * DAYS_IN_YEAR yj = y0 + y1 + y2 + y3 + y4 + y5 mj = (month - 1) * 30 return EPOCH + yj + mj + day - 1 def _to_jd_equinox(an, mois, jour): '''Return jd of this FR date, counting from the previous equinox.''' day_of_adr = (30 * (mois - 1)) + (jour - 1) equinoxe = _next_fall_equinox(gregorian.to_jd(an + YEAR_EPOCH, 1, 1)) return trunc(equinoxe - 0.5) + 0.5 + day_of_adr def from_jd(jd, method=None): '''Calculate date in the French Revolutionary calendar from Julian day. The five or six "sansculottides" are considered a thirteenth month in the results of this function.''' method = method or 'equinox' if method == 'equinox': return _from_jd_equinox(jd) else: return _from_jd_schematic(jd, method) def _from_jd_schematic(jd, method): '''Convert from JD using various leap-year calculation methods''' if jd < EPOCH: raise ValueError("Can't convert days before the French Revolution") # days since Epoch J = trunc(jd) + 0.5 - EPOCH y0, y1, y2, y3, y4, y5 = 0, 0, 0, 0, 0, 0 intercal_cycle_days = leap_suppression_days = over_cycle_days = None # Use the every-four-years method below year 17 if (J <= DAYS_IN_YEAR * 12 + 3 and method in (100, 'romme')) or (J <= DAYS_IN_YEAR * 17 + 4 and method in (128, 'madler')): method = 4 # set p and r in Hatcher algorithm if method in (4, 'continuous'): # Leap years: 15, 19, 23, ... # Reorganize so that leap day is last day of cycle J = J + 365 y5 = - 1 elif method in (100, 'romme'): # Year 15 is not a leap year # Year 16 is leap, then multiples of 4, not multiples of 100, yes multiples of 400 y5 = 12 J = J - DAYS_IN_YEAR * 12 - 3 leap_suppression_yrs = 100. leap_suppression_days = 36524 # LEAP_CYCLE_DAYS * 25 - 1 intercal_cycle_yrs = 400. intercal_cycle_days = 146097 # leap_suppression_days * 4 + 1 over_cycle_yrs = 4000. over_cycle_days = 1460969 # intercal_cycle_days * 10 - 1 elif method in (128, 'madler'): # Year 15 is a leap year, then year 20 and multiples of 4, not multiples of 128 y5 = 16 J = J - DAYS_IN_YEAR * 16 - 4 leap_suppression_yrs = 128 leap_suppression_days = 46751 # 32 * leap_cycle_days - 1 else: raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox") if over_cycle_days: y0 = trunc(J / over_cycle_days) * over_cycle_yrs J = J % over_cycle_days if intercal_cycle_days: y1 = trunc(J / intercal_cycle_days) * intercal_cycle_yrs J = J % intercal_cycle_days if leap_suppression_days: y2 = trunc(J / leap_suppression_days) * leap_suppression_yrs J = J % leap_suppression_days y3 = trunc(J / LEAP_CYCLE_DAYS) * LEAP_CYCLE_YEARS if J % LEAP_CYCLE_DAYS == LEAP_CYCLE_DAYS - 1: J = 1460 else: J = J % LEAP_CYCLE_DAYS # 0 <= J <= 1460 # J needs to be 365 here on leap days ONLY y4 = trunc(J / DAYS_IN_YEAR) if J == DAYS_IN_YEAR * 4: y4 = y4 - 1 J = 365.0 else: J = J % DAYS_IN_YEAR year = y0 + y1 + y2 + y3 + y4 + y5 month = trunc(J / 30.) J = J - month * 30 return year + 1, month + 1, trunc(J) + 1 def _from_jd_equinox(jd): '''Calculate the FR day using the equinox as day 1''' jd = trunc(jd) + 0.5 equinoxe = premier_da_la_annee(jd) an = gregorian.from_jd(equinoxe)[0] - YEAR_EPOCH mois = trunc((jd - equinoxe) / 30.) + 1 jour = int((jd - equinoxe) % 30) + 1 return (an, mois, jour) def decade(jour): return trunc(jour / 100.) + 1 def day_name(month, day): return french_republican_days[month][day - 1] def from_gregorian(year, month, day, method=None): return from_jd(gregorian.to_jd(year, month, day), method=method) def to_gregorian(an, mois, jour, method=None): return gregorian.from_jd(to_jd(an, mois, jour, method=method)) def format(an, mois, jour): return "{0} {1} {2}".format(jour, MOIS[mois - 1], an)