Forgotten Realms date calculator

18

In an effort to level the playing field between languages with built-in date libraries and those without, let's work with a fictional calendar. The Forgotten Realms are a (the?) campaign setting for Dungeons & Dragons. Of course, each have their own calendar.

The Calendar of Harptos

Conveniently, a year in the Forgotten Realms also has 365 days. Furthermore, the calendar also has 12 months. However, this is where it gets interesting. Each month is exactly 30 days long. The remaining 5 days are holidays which fall between the months. Here are the months and holidays in order (with holidays indented):

1   Deepwinter
        Midwinter
2   The Claw of Winter
3   The Claw of the Sunsets
4   The Claw of the Storms
        Greengrass
5   The Melting
6   The Time of Flowers
7   Summertide
        Midsummer
        [Shieldmeet]
8   Highsun
9   The Fading
        Highharvestide
10  Leaffall
11  The Rotting
        The Feast of the Moon
12  The Drawing Down

Notice that I've inserted a sixth holiday in brackets. This is the leap day which only occurs every four years (yes, that's it — there are no additional shenanigans with the centuries).

Side note about month names: each month has a formal and a common name. The above are the common names. I've picked those because I think they allow for more interesting compression.

There are several numberings of the years, but the most widespread one is Dalereckoning, shortened to DR. (Also, every year has one or more names, but we're not going to bother with that.)

The components of a date should be separated by a comma and a space. All in all, a valid date might look like:

4, The Melting, 1491 DR

or

Shieldmeet, 1464 DR

Note that there is no day number for the holidays. (I suppose 4th of The Melting would be nicer for the days of the months, but I don't want to drag ordinal numbers into this.)

Footnote: I came up with this when xnor complained that every single date challenge needs the leap year computation. I've failed in eliminating it completely, but at least it's only a single modulo in this calendar.

The Challenge

Given a valid date of the Calendar of Harptos, as well as an integer D, output the date D days later. Note that D may be negative, in which case you should return the date D days earlier.

You may write a program or function, taking input via STDIN (or closest alternative), command-line argument or function argument and outputting the result via STDOUT (or closest alternative), function return value or function (out) parameter.

You may assume that the year is positive and less than 2000.

Standard rules apply.

Test Cases

The first dozen or so test cases should test all the edge cases surrounding holidays and leap years. The next set are to test that ranges across multiple years work and all months and holidays have been implemented. The second half is all the same test cases again but with negative offsets.

"30, Summertide, 1491 DR" 1                 => "Midsummer, 1491 DR"
"30, Summertide, 1491 DR" 2                 => "1, Highsun, 1491 DR"
"Midsummer, 1491 DR" 1                      => "1, Highsun, 1491 DR"
"30, Summertide, 1492 DR" 1                 => "Midsummer, 1492 DR"
"30, Summertide, 1492 DR" 2                 => "Shieldmeet, 1492 DR"
"30, Summertide, 1492 DR" 3                 => "1, Highsun, 1492 DR"
"Midsummer, 1492 DR" 1                      => "Shieldmeet, 1492 DR"
"Midsummer, 1492 DR" 2                      => "1, Highsun, 1492 DR"
"Shieldmeet, 1492 DR" 1                     => "1, Highsun, 1492 DR"
"1, Highsun, 1490 DR" 365                   => "1, Highsun, 1491 DR"
"1, Highsun, 1491 DR" 365                   => "Shieldmeet, 1492 DR"
"Shieldmeet, 1492 DR" 365                   => "Midsummer, 1493 DR"
"Midsummer, 1493 DR" 365                    => "Midsummer, 1494 DR"
"Shieldmeet, 1500 DR" 365                   => "Midsummer, 1501 DR"

"14, Deepwinter, 654 DR" 5069               => "The Feast of the Moon, 667 DR"
"Midwinter, 17 DR" 7897                     => "15, The Fading, 38 DR"
"3, The Claw of Winter, 1000 DR" 813        => "25, The Claw of the Storms, 1002 DR"
"Greengrass, 5 DR" 26246                    => "9, The Claw of the Sunsets, 77 DR"
"30, The Melting, 321 DR" 394               => "29, The Time of Flowers, 322 DR"
"17, The Time of Flowers, 867 DR" 13579     => "20, Highsun, 904 DR"
"Highharvestide, 1814 DR" 456               => "30, The Drawing Down, 1815 DR"
"23, The Rotting, 1814 DR" 3616             => "16, Leaffall, 1824 DR"
"1, Deepwinter, 1 DR" 730499                => "30, The Drawing Down, 2000 DR"

"Midsummer, 1491 DR" -1                     => "30, Summertide, 1491 DR"
"1, Highsun, 1491 DR" -2                    => "30, Summertide, 1491 DR"
"1, Highsun, 1491 DR" -1                    => "Midsummer, 1491 DR"
"Midsummer, 1492 DR" -1                     => "30, Summertide, 1492 DR"
"Shieldmeet, 1492 DR" -2                    => "30, Summertide, 1492 DR"
"1, Highsun, 1492 DR" -3                    => "30, Summertide, 1492 DR"
"Shieldmeet, 1492 DR" -1                    => "Midsummer, 1492 DR"
"1, Highsun, 1492 DR" -2                    => "Midsummer, 1492 DR"
"1, Highsun, 1492 DR" -1                    => "Shieldmeet, 1492 DR"
"1, Highsun, 1491 DR" -365                  => "1, Highsun, 1490 DR"
"Shieldmeet, 1492 DR" -365                  => "1, Highsun, 1491 DR"
"Midsummer, 1493 DR" -365                   => "Shieldmeet, 1492 DR"
"Midsummer, 1494 DR" -365                   => "Midsummer, 1493 DR"
"Midsummer, 1501 DR" -365                   => "Shieldmeet, 1500 DR"

"The Feast of the Moon, 667 DR" -5069       => "14, Deepwinter, 654 DR"
"15, The Fading, 38 DR" -7897               => "Midwinter, 17 DR"
"25, The Claw of the Storms, 1002 DR" -813  => "3, The Claw of Winter, 1000 DR"
"9, The Claw of the Sunsets, 77 DR" -26246  => "Greengrass, 5 DR"
"29, The Time of Flowers, 322 DR" -394      => "30, The Melting, 321 DR"
"20, Highsun, 904 DR" -13579                => "17, The Time of Flowers, 867 DR"
"30, The Drawing Down, 1815 DR" -456        => "Highharvestide, 1814 DR"
"16, Leaffall, 1824 DR" -3616               => "23, The Rotting, 1814 DR"
"30, The Drawing Down, 2000 DR" -730499     => "1, Deepwinter, 1 DR"

Martin Ender

Posted 2015-12-22T16:51:16.597

Reputation: 184 808

1DragonLance is another major D&D campaign setting. I can't remember much about their calendar except for their three moons, the orbits of which were explained in detail in some reference book. – CJ Dennis – 2016-01-06T12:47:41.950

Answers

5

Ruby, 543 523 521 498 511 509 bytes

To encourage more answers to this question, I'm going to post a Ruby version of my Python answer, since I figured it would be shorter. This answer is shorter but not by much. Can you do better?

Edit: With thanks to Martin Büttner and his suggestion here.

Edit: I golfed the "number of days in a month" list down considerably.

Edit: While golfing down how I handled d[10]=r%4<1?1:0 to d[10]=0**(r%4) for a byte, I noticed I'd introduced a bug while golfing down d, the number of days list, so that Shieldmeet had 30 days by accident. And so, the byte count has come back up. I will also edit the Python answer to fix this bug there.

Edit: I forgot that functions don't need to be named in this question.

->s,n{x=s[0..-4].split(", ");x=x[2]?x:[1,*x];t=(["Deepwinter,Midwinter","Winter","Sunsets","the Storms,Greengrass,The Melting,The Time of Flowers,Summertide,Midsummer,Shieldmeet,Highsun,The Fading,Highharvestide,Leaffall,The Rotting,The Feast of the Moon,The Drawing Down"]*',The Claw of ').split(?,);p,q,r=x[0].to_i+n,t.index(x[1]),x[2].to_i;d=[30,1,30,30]*4+[1,30];d[10]=0**(r%4);(a=p<1?1:-1;q=(q-a)%18;p+=a*d[a<0?q-1:q];r-=a*0**q;d[10]=0**(r%4))until(1..d[q])===p;z=d[q]<2?[t[q],r]:[p,t[q],r];z*", "+" DR"}

Ungolfed:

def h(s,n)
  x=s[0..-4].split(", ")
  x=x[2]?x:[1,*x]
  t=["Deepwinter,Midwinter","Winter","Sunsets","the Storms,Greengrass,The Melting,The Time of Flowers,Summertide,Midsummer,Shieldmeet,Highsun,The Fading,Highharvestide,Leaffall,The Rotting,The Feast of the Moon,The Drawing Down"]
  t=t*',The Claw of '           # turns the above array into a string with "Claw"s inserted
  t=t.split(?,)                 # then splits that string back up again by ","
  p=x[0].to_i+n
  q=t.index(x[1])
  r=x[2].to_i
  d=[30,1,30,30]*4+[1,30]
  d[10]=0**(r%4)
  until(1..d[q])===p
    a=p<1?1:-1
    q=(q-a)%18
    p+=a*d[a<0?q-1:q]
    r-=a*0**q
    d[10]=0**(r%4)
  end
  z=d[q]<2?[t[q],r]:[p,t[q],r]  # putting z=[t[q],r] on another line saved me no bytes
  z*", "+" DR"
end

Sherlock9

Posted 2015-12-22T16:51:16.597

Reputation: 11 664

5

Python 3, 712 652 636 567 563 552 550 548 529 540 bytes

At last, I found time to write an answer for this excellent question. It isn't very golfed yet (the month names list and the number of days list are is particularly egregious in this case, and the fact that handling negative D requires a separate while loop) but at least it's an answer.

Edit: Fixing a bug

def h(s,n):
 x=s[:-3].split(", ");x=[1]*(len(x)<3)+x;t="Deepwinter,Midwinter,The Claw of Winter,The Claw of the Sunsets,The Claw of the Storms,Greengrass,The Melting,The Time of Flowers,Summertide,Midsummer,Shieldmeet,Highsun,The Fading,Highharvestide,Leaffall,The Rotting,The Feast of the Moon,The Drawing Down".split(",");p,q,r=int(x[0])+n,t.index(x[1]),int(x[2]);d=[30,1,30,30]*4+[1,30];d[10]=r%4<1
 while p>d[q]or p<1:a=[-1,1][p<1];q=(q-a)%18;p+=a*d[q-(a<0)];r-=a*0**q;d[10]=r%4<1
 return', '.join([str(p)]*(d[q]>2)+[t[q],str(r)])+" DR"

Ungolfed:

def harptos(date, num):
    t = "Deepwinter,Midwinter,The Claw of Winter,The Claw of the Sunsets,The Claw of the Storms,Greengrass,The Melting,The Time of Flowers,Summertide,Midsummer,Shieldmeet,Highsun,The Fading,Highharvestide,Leaffall,The Rotting,The Feast of the Moon,The Drawing Down"
    t = t.split(",")        # split up the names of the months
    x = date[:-3]           # removes " DR"
    x = x.split(", ")
    if len(x) < 3:
        x = [1] + x         # if we have two items (holiday), append a "day of the month"
    p = int(x[0]) + num     # initialize the "date" by adding num to it
    q = t.index(x[1])
    r = int(x[2])
    d=[30,1,30,30]*4+[1,30] # all the month lengths
    d[10] = r%4 < 1         # leap year toggle
    while p > d[q]:         # while the "date" > the number of days in the current month
        p -= d[q]           # decrement by number of days in current month
        q = (q+1)%18        # increment month
        r += 0**q           # increment year if the incremented month == the first month
        d[10] = r%4 < 1     # leap year toggle
    while p < 1:            # while the "date" is negative
        q = (q-1)%18        # decrement month first
        p += d[q]           # add the number of days in the decremented month
        r -= 0**q            # decrement year if the decremented month == the first month
        d[10] = r%4 < 1     # leap year toggle
    m = [t[q],str(r)]       # start the result array
    if d[q] > 2:
        m = [str(p)] + m    # if the month is NOT a holiday, add the day
    return ", ".join(m) + " DR"

Sherlock9

Posted 2015-12-22T16:51:16.597

Reputation: 11 664