Roman-style date formatting

7

2

Today (March 15) is the Ides of March, best known for being the date that Julius Caesar was assassinated.

What's “Ides”? Well, the ancient Romans didn't simply number the days of month from 1 to 31 like we do. Instead, they had a more complex system of counting backwards to the Kalends, Nones, or Ides.

Challenge

Write, in as few bytes as possible, a program that will display the current date (on the Gregorian calendar, in the local timezone) using the abbreviated Roman-style convention described below. Your program should work until at least the end of 2037.

Roman dates

Months of the year

These should look familiar to you: Ianuarius, Februarius, Martius, Aprilis, Maius, Iunius, Iulius (Quintilis), Augustus (Sextilis), September, October, November, December.

Your program is to abbreviate all months to three letters, using the classical Latin alphabet (all caps, with J=I and U=V). That is, IAN, FEB, MAR, APR, MAI, IVN, IVL, AVG, SEP, OCT, NOV, DEC.

Special dates of the month

  • Kalendis, or the Kalends (abbr. KAL) is the first day of each month.
  • Idibus, or the Ides (abbr. ID) is the 15th day of March, May, July, or October; but the 13th day of the other eight months.
  • Nonas, or the Nones (abbr. NON) is 8 days before the Ides (5th or 7th of the month)

Other days of the month

Other days of the month are numbered by counting backwards and inclusively to the next reference date (Kalends, Ides, or Nones). Notation is AD (abbreviation for ante diem, no relation to Jesus of Nazareth) followed by the appropriate Roman numeral.

Exception: Write PR (for pridie) instead of AD II for the day before Kalends, Nones, or Ides.

The leap day

In leap years, two days (February 24 and 25 in modern reckoning) are given the date AD VI KAL MAR. (You do not need to distinguish the two days in your program.) February 14-23 are AD XVI KAL MAR through AD VII KAL MAR as in non-leap years. Feb 26 = AD V KAL MAR, Feb 27 = AD IV KAL MAR, Feb 28 = AD III KAL MAR, and Feb 29 = PR KAL MAR, offset by one from the corresponding dates in non-leap years.

Example

In March, the output of your program should be:

  • On March 1: KAL MAR
  • On March 2: AD VI NON MAR
  • On March 3: AD V NON MAR
  • On March 4: AD IV NON MAR
  • On March 5: AD III NON MAR
  • On March 6: PR NON MAR
  • On March 7: NON MAR
  • On March 8: AD VIII ID MAR
  • ...
  • On March 13: AD III ID MAR
  • On March 14: PR ID MAR
  • On March 15: ID MAR
  • On March 16: AD XVII KAL APR
  • ...
  • On March 30: AD III KAL APR
  • On March 31: PR KAL APR

dan04

Posted 2016-03-16T00:45:00.423

Reputation: 6 319

Answers

6

Tcl/Tk, 675 bytes

Well, I suppose I could crunch it more, but this is it for now...

set a {y m d}
rename proc p
p r {n {s ""}} {foreach {v d} {10 X 5 V 1 I} {while {$n>=$v} {set s $s$d;incr n -$v}};string map {VIIII IX IIII IV} $s}
p y y {expr !($y%100?$y%4:$y%400)}
p i $a {expr {($m in {3 5 7 10})?15:13}}
p n $a {expr [i $y $m $d]-8}
p k $a {expr $m==2?29+($d>24&&[y $y]):$m<8?31+$m%2:32-$m%2}
p N {} {clock f [clock se] -f {%Y %N %e}}
p M m {lindex {IAN FEB MAR APR MAI IVN IVL AVG SEP OCT NOV DEC IAN} $m-1}
p d $a {foreach f {n i k} n {NON ID KAL} mo {0 0 1} {if {$d<=[$f $y $m $d]} break}
string map {{AD I } {} {AD II } {PR }} "AD [r [expr [$f $y $m $d]+1-$d]] $n [M [expr $m+$mo]]"}
p p $a {if {$d==1} {puts "KAL [M $m]"} {puts [d $y $m $d]}}
p {*}[N]

How it works

The first thing is to collect today's date as a list: year, month, day. Everything works over that. The first of the month is a simple special case; we handle that separately.

All remaining cases work like this:

  1. Find the future reference day's date in (5,7,13,15,days-in-month)
  2. Get the abbreviation corresponding to that reference (NON, ID, KAL)
  3. For Kalends we increment the month by one
  4. Assemble the date string as:
    "AD <days-in-roman-numerals> <abbreviated-reference-day> <abbreviated-month-name>"
  5. Apply a text replace to correct AD I and AD II.

The first step is done with a function lookup table for each of the Nones, Ides, and Kalends. Nones and Ides are straight-forward subtraction and lookup, respectively. Computing the Kalends is only a little more complicated (notice the trick with leap-February's day number.):

  • February → 28 days + (it's a leap year AND day > 24)
  • January..July → 30 days + (it's January, March, May or July)
  • August..December → 31 days - (it's September or November)

The days is a simple difference: (reference_day - todays_day + 1)

Computing the leap year is done with a somewhat unusual arrangement of the standard formula so that it could be crunched better (removing braces and parentheses from the expr command).

Converting decimal to Roman numerals is a simple table-driven approach, stripped to use only ones, fives, and tens since we don't need to print the year in Roman numerals (domain is [1,39]).

Almost everything else is application of lookup tables.

A note on Tcl and reducing bytes

The first two lines of the script eat bytes by simply being shorter than spelling out proc f {y m d} for all instances.

Single-letter proc names and variables, of course:

    r := decimal→roman(n)    i := ides(y,m,d)        M := month-abbreviation(month)
    y := is-leap(year)       n := nones(y,m,d)       d := date→roman(y,m,d) [d ≠ 1]
    N := today → (y,m,d)     k := kalends(y,m,d)     p := date→roman(y,m,d) [∀d]

Testing for dates other than today

Change the very last line from p {*}[N] to

p {*}$argv

and run the program with the date desired:

% tclsh roman.tcl 2016 2 25
AD VI KAL MAR

Enjoy!

Dúthomhas

Posted 2016-03-16T00:45:00.423

Reputation: 541

2

(I will not give myself the checkmark, even though I have the shortest submission so far.)

Python 3: 390 bytes

import time
y,m,d=time.localtime()[:3]
d-=(y%4==0)*(m==2)*(d>24)
i=13+2*(m in (3,5,7,10))
n=i-8
k=29+(0,3,0,3,2,3,2,3,3,2,3,2,3)[m]
a,R={1:(0,'KAL'),d>1:(n-d,'NON'),d>n:(i-d,'ID'),d>i:(k-d,'KAL')}[1]
a+=1
A={1:'',2:'PR '}.get(a,'AD '+a//10*'X'+('','I','II','III','IV','V','VI','VII','VIII','IX')[a%10]+' ')
if d>i:m=m%12+1
M='IFMAMIIASONDAEAPAVVVECOENBRRINLGPTVC'[m-1::12]
print(A+R+' '+M)

Explanation

y,m,d=time.localtime()[:3]

Gets the current date: y=year, m=month, and d=day.

To test the program with dates other than the current date, simply replace this line with something else.

d-=(y%4==0)*(m==2)*(d>24)

If this is February 25-29 of a leap year, subtract 1 from the date. From this point on, we can treat February as if it's a non-leap year.

Note that, because the challenge rules don't require correct behavior in the year 2100, I used the simple 4-year Julian leap-year rule instead of the Gregorian 400-year cycle.

i=13+2*(m in (3,5,7,10))

Ides of the current month.

n=i-8

Nones of the current month.

k=29+(0,3,0,3,2,3,2,3,3,2,3,2,3)[m]

One more than the number of days in the current month, equivalent to the Kalends of the next month.

a,R={1:(0,'KAL'),d>1:(n-d,'NON'),d>n:(i-d,'ID'),d>i:(k-d,'KAL')}[1]

a is the number of days until the next reference date and R is its abbreviation. This expression takes advantage of the fact that if the same key is specified multiple times in the same dict literal, only the last one counts.

a+=1

Adjustment for inclusive counting.

A={1:'',2:'PR '}.get(a,'AD '+a//10*'X'+('','I','II','III','IV','V','VI','VII','VIII','IX')[a%10]+' ')

Stringify the count a to the next reference date.

  • If 1, use no prefix
  • If 2, use the prefix PR
  • If 3 or higher, use the prefix AD + Roman numeral + space

There's probably a shorter way to do the Roman numerals, but I'm too lazy to find it.

if d>i:m=m%12+1

If after the Ides, increment the month for display.

M='IFMAMIIASONDAEAPAVVVECOENBRRINLGPTVC'[m-1::12]

Get the month name.

print(A+R+' '+M)

Finally, print the date.

dan04

Posted 2016-03-16T00:45:00.423

Reputation: 6 319