Code:
import java.io.{FileWriter, BufferedWriter}
import scala.io.Source
object DateUtil {
def isLeapYear(y: Int) = y % 4 == 0 && y % 100 != 0 || y % 400 == 0
def gregorianMonthLengths(y: Int) = List(31, 28 + (if(isLeapYear(y)) 1 else 0), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
}
object FRDate {
//for some reason, after 1992 and before the 1800s the new year is the 12th of nivose, not the 11th. Also, on the years 1804 and 1808, the gregorian new year was on the 10th of nivose.
def gregorianNewYear(a: Int) = if(a == 1804 || a == 1808) FRDate(10,04,a-1792) else if(a > 1992 || a < 1800) FRDate(12,04,a-1792) else FRDate(11,04,a-1792)
//converts unicode characters into non-unicode ones
private def deUnicode(c: Char) = {
c.toLower match {
case 'é'|'ê' => 'e'
case 'ô' => 'o'
case x => x
}
}
//An extension method class for ints, all ints now have a romanize function that turns them into a roman numeral
implicit class RomanInts(val i: Int) extends AnyVal {
private def romanize(level: Int, ones: String, fives: String, tens: String) = {
val s = ((i / level) % 10) match {
case 4 => ones + fives
case x if x > 4 & x < 9 => fives
case 9 => ones + tens
case _ => ""
}
s + (ones*((i / level) % 10 % 5 % 4))
}
def toRomanNumeral = "M" * (i / 1000) + romanize(100, "C", "D", "M") + romanize(10, "X", "L", "C") + romanize(1, "I", "V", "X")
}
//used to match for input parsing, as well as pretty printing the date
val monthMap = IndexedSeq("Vendémiaire", "Brumaire", "Frimaire",
"Nivôse", "Pluviôse", "Ventôse",
"Germinal", "Floréal", "Prairial",
"Messidor", "Thermidor", "Fructidor", "Sans-culottide")
//lazy values are only calculated when they are needed, these won't be calculated until the parser tries to parse ascii versions of the month
lazy val nonUnicodeMonthMap = monthMap map (_ map (deUnicode))
val dayMap = IndexedSeq("Primidi", "Duodi", "Tridi", "Quartidi", "Quintidi", "Sextidi", "Septidi", "Octidi", "Nonidi", "Décadi")
lazy val nonUnicodeDayMap = dayMap map (_ map (deUnicode))
//days for the "13th" month
val sansCulottides = IndexedSeq("La Fête de la Vertu", "La Fête du Génie", "La Fête du Travail", "La Fête de l'Opinion",
"La Fête des Récompenses", "La Fête de la Revolution")
lazy val nonUnicodeSansCulottides = sansCulottides map (_ map (deUnicode))
//checks both unicode and non-unicode versions of the months, followed by an attempt to parse a roman numeral
// => IndexedSeq[String] means that it doesn't evaluate the argument until it's used in the function. This is to keep lazy vals such as nonUnicodeMonthMap from being calculated until absolutely necessary.
private def getIndexOrConvertNumeral(s: String, nuL: => IndexedSeq[String], l: => IndexedSeq[String]) = {
(l.map(_.toLowerCase).indexOf(s.toLowerCase) match {
case -1 => nuL.indexOf(s.toLowerCase)
case i => i
}) match {
case -1 => s match {
case romanRegex(s) => fromRomanNumeral(s)
case _ => throw new Exception("String doesn't match any form of valid input!")
}
case i => i + 1
}
}
//takes a string matching the format of either regex.
def parse(s: String) = {
val regex = """D[eé]cade\s+([MCDXLIV]*),\s*(\p{L}+|[XVI]+)\s+de\s+(\p{L}+|[XVI]+)\s+de\s+l'Ann[ée]e\s+([MCDXLIV]+)\s+de\s+la\s+R[ée]volution\.?""".r
val sCulottRegex = """(La F[êe]te .+)\s+de\s+l'Ann[ée]e\s+([MCDXLIV]+)\s+de\s+la\s+R[ée]volution""".r
s match {
case regex(dec, jour, mois, annee) => {
val week = if(dec.isEmpty) 0 else fromRomanNumeral(dec)
val day = getIndexOrConvertNumeral(jour, nonUnicodeDayMap, dayMap)
val month = getIndexOrConvertNumeral(mois, nonUnicodeMonthMap, monthMap)
val year = fromRomanNumeral(annee)
FRDate(day+ (week - 1) * 10, month, year)
}
case sCulottRegex(jour, annee) => {
val day = (sansCulottides map (_.toLowerCase) indexOf (jour.toLowerCase) match {
case -1 => nonUnicodeSansCulottides map (_.toLowerCase) indexOf (jour.toLowerCase)
case i => i
}) match {
case -1 => throw new Exception("String doesn't match any form of valid input!")
case i => i + 1
}
val year = fromRomanNumeral(annee)
FRDate(day, 13, year)
}
case _ => throw new Exception("Invalid date!!")
}
}
//converts a roman numeral into an int
private def fromRomanNumeral(s: String) = {
val startCount = s.count(_ == 'M')*1000 + s.count(_ == 'C')*100 + s.count(_ == 'D')*500 +
s.count(_ == 'X') * 10 + s.count(_ == 'L') * 50 + s.count(_ == 'V') * 5 + s.count(_ == 'I') * 1
//turns a string like "abc" into a list like List(('a','b'),('b','c'))
val pairs = s zip s.tail
//removes the extra stuff added by startcount interpreting things like IV as 6
startCount - pairs.count{case (a,b) => a == 'I' && (b == 'X' || b == 'V')} * 2 -
pairs.count{case (a,b) => a == 'X' && (b == 'L' || b == 'C')} * 20 -
pairs.count{case (a,b) => a == 'C' && (b == 'D' || b == 'M')} * 200
}
val romanRegex = """([MCDXLIV]+)""".r
//takes a date of DD/MM/YYYY gregorian format and makes it into a FRDate.
def fromGregorianDate(d: Int, m: Int, y: Int) = {
val newYear = gregorianNewYear(y)
//days since 1/4/y in the French Revolutionary Calendar
val days = ((m-1,0) /: DateUtil.gregorianMonthLengths(y)) {case ((remMonths, totalDays), daysInMonth) => if(remMonths > 0) (remMonths-1, totalDays + daysInMonth) else (0, totalDays)}._2 + (d-1)
val moisPt1 = 4 + ((days + newYear.jour - 1)/30)
val frNewYearPassed = moisPt1 > 13 || moisPt1 == 13 && (days+newYear.jour) % 30 > (if(DateUtil.isLeapYear(y)) 6 else 5)
//if the number of months reported by moisPt1 is 13 and the numbers of days in the month is greater than 6 or 5
// (depending on whether or not it's a leap year), remove the gregorianNewYear - French revolutionary new year of
// the next year days from days to make adjusted days, otherwise, adjusted days == days
val adjustedDays = if(frNewYearPassed) days - FRDate(01,01, y+1-1792).daysBetween(newYear) else days + newYear.jour - 1
// calculation of the day of the month in the french calendar
val jour = adjustedDays % 30 + 1
//calculation of the month of the year in the french calendar
val mois = if(frNewYearPassed) adjustedDays / 30 + 1 else moisPt1
//calculation of the year in the french calendar
val année = y - 1792 + (if(frNewYearPassed) 1 else 0)
FRDate(jour,mois,année)
}
}
//A date on the French revolutionary calendar
// jour, mois, and année are the french words for day, month, and year
case class FRDate(jour: Int, mois: Int, année: Int) {
require(jour <= 30 && jour >= 1, s"day out of range: $jour")
require(mois <= 13 && mois >= 1, "month out of range")
require(année >= 1, s"year before 1: $année")
import FRDate.RomanInts
// lazy values are not calculated until requested.
lazy val leapYear = DateUtil.isLeapYear(année)
lazy val leapDay: Option[FRDate] = if(leapYear) Some(FRDate(06, 13, année)) else None
lazy val gregorianNewYear = FRDate.gregorianNewYear(année+1792)
def avecAnnée(a: Int) = FRDate(jour, mois, a)
def avecMois(m: Int) = FRDate(jour, m, année)
def avecJour(j: Int) = FRDate(j, mois, année)
//used to determine if one date is before another
def before(other: FRDate) = if(année < other.année) true else if (année == other.année) {
if(mois < other.mois) true else if (mois == other.mois) {
if(jour < other.jour) true else false
} else false
} else false
//calculates the number of days between two dates
def daysBetween(other: FRDate) = {
//calculates the number of days from years between these dates. if there is no difference in year, this returns the calculated day difference
def dayHelper(m: FRDate, s: FRDate): Int = {
if(m.année != s.année) 365 + (if(m.leapYear) 1 else 0) + dayHelper(m avecAnnée(m.année - 1), s)
else _monthAdjuster(m,s) + _dayAdjuster(m avecMois s.mois, s)
}
//calculates the days from months
def _monthAdjuster(m: FRDate, s: FRDate): Int = m.mois * 30 - s.mois * 30
def _dayAdjuster(m: FRDate, s: FRDate): Int = m.jour - s.jour
val (m,s) = if(this before other) (other, this) else (this, other)
dayHelper(m,s) - (m.leapDay map (lD => if((m before lD) && s.année - m.année >= 1) 1 else 0) getOrElse(0))
}
//the year of this FRDate in the gregorian calendar
lazy val gregorianYear = 1791 + année + (if(this before gregorianNewYear) 0 else 1)
//the month of this FRDate in the gregorian calendar
lazy val gregorianMonth = {
val refPoint = if(this before gregorianNewYear) gregorianNewYear avecAnnée(année - 1) else gregorianNewYear
val daysSince = (refPoint daysBetween this) + 1
val m = DateUtil.gregorianMonthLengths(gregorianYear)
(m foldLeft (0 -> daysSince)) {case (tup, days) => (tup._1 + (if(tup._2 > 0) 1 else 0), tup._2 - days) }._1
}
//the day of this FRDate in the gregorian calendar
lazy val gregorianDay = {
val refPoint = if(this before gregorianNewYear) gregorianNewYear avecAnnée(année - 1) else gregorianNewYear
val daysSince = (refPoint daysBetween this) + 1
(DateUtil.gregorianMonthLengths(gregorianYear).take(gregorianMonth - 1) foldLeft daysSince) (_-_)
}
def gregorianDateString = s"$gregorianDay/$gregorianMonth/$gregorianYear"
override def toString = if(mois != 13)
s"Décade ${((jour - 1)/ 10 + 1).toRomanNumeral}, ${FRDate.dayMap((jour - 1) % 10)} de ${FRDate.monthMap(mois-1)} de l'Année ${année.toRomanNumeral} de la Révolution"
else
s"${FRDate.sansCulottides(jour - 1)} de l'Année ${année.toRomanNumeral} de la Révolution"
}
object Cal {
def main(args: Array[String]) {
//converts either gregorian dates of the format DD/MM/YYYY or french republic dates of the format listed in the challenge into the corresponding French republic date or Gregorian date
def interpreter(l: String) = {
val r = """\s*([0-3]?\d)/([01]?\d)/(\d{4})""".r
l match {
case "exit" => System.exit(0); ??? //??? is there as a return type of Nothing. If it's accessed, an error will occur, which shouldn't happen because the program should have exited already
case r(day,month,year) => try {
FRDate.fromGregorianDate(day.toInt,month.toInt,year.toInt).toString
} catch { case e: Exception => e.getMessage }
case _ => try {
FRDate.parse(l).gregorianDateString
}
}
}
args match {
case Array("-i") => {
Source.fromInputStream(System.in)(io.Codec.UTF8).getLines() foreach {l => println(interpreter(l))}
}
case Array(file) => Source.fromFile(file)(io.Codec.UTF8).getLines() foreach {l => println(interpreter(l))}
case Array(file, "-o", outFile) => {
val res = Source.fromFile(file)(io.Codec.UTF8).getLines() map interpreter
val os = new BufferedWriter(new FileWriter(outFile))
res.foreach {l => os.write(l + "\n")}
os.flush()
}
case _ => println("no match" + args.fold("") (_ + _))
}
}
}
This code does not use standard lib calendars or any other kind of calendar. As far as I can tell so far, it's completely accurate, but calendars are tough so it may not be.
Bookmarks