18
\$\begingroup\$

A quote from MO.SE answer:

Although it is well known that Conway was able to quickly calculate the day of the week of any given date, it is less well known that one part of the algorithm is easy to remember and useful in practice: In any given year, the following dates all fall on the same day of the week: 4/4, 6/6, 8/8, 10/10, 12/12, 5/9, 9/5, 7/11, 11/7, and the last day of February. For example, in 2020, all these dates fall on a Saturday. Conway, in his characteristically colorful way, would say that the Doomsday of 2020 is Saturday. Knowing this fact allows you to calculate fairly quickly in your head, with no special training, the day of the week for any date in 2020.

Well, it sounds easy, but then we need to check which doomsday is the closest from the given date in order to quickly calculate the day of week. Now that sounds hard.

Practically, we'd just compare a given date with the Doomsday in the same month (or adjacent month in case of January and March).

Task

Given a date consisting of full year, month, and day, output the closest Conway's Doomsday (i.e. one of 4/4, 6/6, 8/8, 10/10, 12/12, 5/9, 9/5, 7/11, 11/7, and the last day of February) from the given date.

The closest Doomsday can be in the same month, a different month, or even a different year. If the given date has two nearest Doomsdays, output any one or both of them. Also note that the last day of February can be either 28th or 29th (depending on leap-year-ness).

You can take input and produce output in any suitable format, e.g. three integers, a formatted string, or even a built-in Date object (if your language has one). You can assume the given date is valid, and the input year is between 1901 and 2099 inclusive. Gregorian calendar is assumed in this challenge.

Test cases

YYYY-MM-DD => YYYY-MM-DD, ...
-------------------------------
2020-05-18 => 2020-05-09
2020-05-30 => 2020-06-06
2020-10-31 => 2020-11-07
2020-10-24 => 2020-10-10 or 2020-11-07
2020-01-20 => 2019-12-12
2020-01-21 => 2020-02-29
2019-01-20 => 2018-12-12 or 2019-02-28

Reference implementation in Python.

\$\endgroup\$
9
  • \$\begingroup\$ Nice challenge! I think you probably either need a test case for the exception to the 'multiple of 4' leap year rule - i.e. except for years evenly divisible by 100, unless also divisible by 400, or limit the range of inputs to avoid needing to handle it \$\endgroup\$
    – Jarmex
    Apr 27, 2020 at 9:16
  • 1
    \$\begingroup\$ I thought the Doomsday for 2020, for instance, was "Saturday", so that any Saturday in 2020 would be considered a Doomsday. Are you looking for the closest Doomsday that happens to be on Conway's easy-to-remember list (4/4, 6/6, etc.?) \$\endgroup\$ Apr 27, 2020 at 9:46
  • \$\begingroup\$ If I don't have a built-in date type, what counts as a leap year? \$\endgroup\$
    – Neil
    Apr 27, 2020 at 13:28
  • \$\begingroup\$ @Neil A leap year (where Feb 29th exists) is a year divisible by 4. Unless it's divisible by 100, then it isn't. But if it's divisible by 400 it is. So 2020 is divisible by 4 and thus a leap year. 1700, 1800, 1900, 2100 are not leap years, but 1600, 2000 and 2400 is. \$\endgroup\$
    – Kjetil S
    Apr 27, 2020 at 15:43
  • 3
    \$\begingroup\$ I think this is likely unclear without you specifying what calendar to use, and an acceptable date range. Even your reference implementation will give errors for things I would assume should be valid, such as year 1. \$\endgroup\$ Apr 27, 2020 at 16:16

7 Answers 7

4
\$\begingroup\$

Charcoal, 138 110 98 bytes

≔§⪪”)¶⊟eΦO∨ü&-T[¿Q№i⧴⊕%⁰q”⁴LΦ⪪”)¶ »R≦PH↘{⎚″4χχβ´ΣP”⁴›ι✂θ⁴χ¹η¿⁼Iη¹²⁺⊖…θ⁴1212«…θ⁴¿⁻Ση³η«022§9888I…θ⁴

Try it online! Link is to verbose version of code. Edit: Saved 28 bytes by switching I/O to compact ISO format (yyyymmdd). Saved 12 bytes when the supported year range was restricted to 1901 to 2099. Explanation:

≔§⪪”)¶⊟eΦO∨ü&-T[¿Q№i⧴⊕%⁰q”⁴LΦ⪪”)¶ »R≦PH↘{⎚″4χχβ´ΣP”⁴›ι✂θ⁴χ¹η

Take the dates (in mmdd format) 0120, 0317, 0421, 0523, 0623, 0725, 0822, 0922, 1024, 1124. These represent the cutoff points above which the next Doomsday is nearer. (In some cases the Doomsday is equidistant but in particular for 0120 that is not true on leap years.) Work out which cutoff point applies by counting the number of dates that fall before the input date. Then look up the relevant Doomsday date from the list (in mmdd format) 1212, 1107, 1010, 0905, 0808, 0711, 0606, 0509, 0404, 0300, 0012.

If I had access to a date library I could then ask it to fix up my date, but unfortunately I have to do that manually:

¿⁼Iη¹²

Is this the 0012 date, meaning the 12th of month 0, i.e. last December?

⁺⊖…θ⁴1212«

If so then output the previous year and a month and day of 12.

…θ⁴

Otherwise the year is at least correct...

¿⁻Ση³

Is this the 0300 date, meaning the 0th of March, i.e. the last day of February?

η«

If not then this is the date we seek.

022

Output February the 2?th.

§9888I…θ⁴

Output 9 if the year is a multiple of 4, otherwise 8.

\$\endgroup\$
2
  • \$\begingroup\$ The input year range is now restricted to 1901-2099. \$\endgroup\$
    – Bubbler
    Apr 27, 2020 at 23:24
  • \$\begingroup\$ @Bubbler Thanks; there goes another 12 bytes! \$\endgroup\$
    – Neil
    Apr 27, 2020 at 23:34
3
\$\begingroup\$

Wolfram Language (Mathematica), 102 bytes

aMinimalBy[DateObject@{a[[1,1]],##}&@@@36^^3cx83c24e4aw06er~IntegerDigits~13~Partition~2,Abs[#-a]&]

Try it online! Pure function. Takes a DateObject as input and returns a list of DateObjects as output. The Unicode character is U+F4A1 (\[Function]). I'd use Nearest here, but its default DistanceFunction refuses to compare Quantity values. Note that the function emits a few warnings on TIO due to its sandboxing.

\$\endgroup\$
3
\$\begingroup\$

JavaScript (ES7),  125 ... 114  113 bytes

Takes and returns a Date object.

D=>[25,-15,...'108088080'].map(b=d=>(v=(q=new Date(D.getFullYear(x+=21-d),x>>4,x&15))-D)*v>b||(b=v*v,o=q),x=0)&&o

Try it online!

How?

The array [25,-15,...'108088080'] encodes the following pairs \$(m,d)\$ where \$m\$ is a 0-indexed month and \$d\$ is a day:

[-1,12], [2,0], [3,4], [4,9], [5,6], [6,11], [7,8], [8,5], [9,10], [10,7], [11,12]

Special cases:

  • [-1,12] is the 12th of December of the previous year
  • [2,0] (literally "March 0") is the last day of February

It is decoded as follows:

[25, -15, ...'108088080'] // array of delta values
.map(d =>                 // for each value d in this list:
  [                       //   build the pair (month, day):
    (x += 21 - d) >> 4,   //     add 21 - d to x; the month is floor(x / 16)
    x & 15                //     the day is (x + 16) mod 16
  ],                      //   end of pair
  x = 0                   //   start with x = 0
)                         // end of map()

Try it online!

For each pair \$(m,d)\$ we compute the square of the difference (in milliseconds) between the input date \$D\$ and a new date \$q\$ generated with this month and this day.

(v = (q = new Date(D.getFullYear(), m, d)) - D) * v

We eventually return the date that leads to the smallest squared difference.

\$\endgroup\$
3
+250
\$\begingroup\$

APL (Dyalog Unicode), 115 99 91 bytes

{u/⍨(⊢=⌊/)|(1∆DT⊂⍵)-1∘∆DT¨⊂¨u←(,y∘.,(2/¨2+2×⍳5),(⊢,⌽¨)0 2+⊂5 9),↓y,↑2∘,¨28+0=4|y←(2-⍳3)+⊃⍵}

Try it online!

Takes a date array yyyy mm dd, and returns all the closest doomsdays. If that is not allowed, then +1 byte.

-16 bytes from Bubbler(simplifying leap year check)

-8 bytes from Bubbler(compressing doomsdays)

⎕DT is polyfilled as ∆DT here(Courtesy of Adám) since tio's version doesn't allow it's usage.

Explanation

(2-⍳3) array (-1,0,1)

y←(2-⍳3)+⊃⍵ add that to the year and store it as y.

0=4| check if each of those is a leap year(This works due to the year range.)

28+ add 28 to those booleans to get the correct february dates

↓y,↑2∘,¨ add the respective years, and months(2) to them to get the appropriate dates

(...), concatenate with:

(⊢,⌽¨)0 2 generate array (5 9)(7 11)(9 5)(11 7)

(2/¨2+2×⍳5) and the numbers 4 6 8 10 12 repeated twice

,y∘., join with the years and flatten to a list to get all the dates

u← save all the dates together as u

1∘∆DT¨⊂¨ get each of their date numbers

(1∆DT⊂⍵) and subtract from the input's date number.

| convert to their absolute values:

(⊢=⌊/) tacit fn: array = minimum? (generates bitmask)

u/⍨ filter the dates by that (get the dates with minimum distance)

\$\endgroup\$
3
  • \$\begingroup\$ You can simplify the leap year test to (0=4|⊢) because of the year range. \$\endgroup\$
    – Bubbler
    Jan 19, 2021 at 6:16
  • 1
    \$\begingroup\$ (0=4|⊢)¨ → 0=4| because arithmetics are scalar. \$\endgroup\$
    – Bubbler
    Jan 19, 2021 at 6:22
  • \$\begingroup\$ ⊃,/(⊂,⊂∘⌽)¨(5 9)(7 11) → (⊢,⌽¨)(5 9)(7 11) → (⊢,⌽¨)0 2+⊂5 9. Your initial version is the same length as (5 9)(7 11)(9 5)(11 7) :D \$\endgroup\$
    – Bubbler
    Jan 19, 2021 at 6:48
2
\$\begingroup\$

Red, 234 182 176 bytes

func[n][y: n/2 t: to-date[y 3]second sort/skip
collect[foreach[d m]reduce[4 4 6 6 8 8 10 10 12 12 5 9 9 5 7 11 11 7 t/4 2 -19 1][a:
to-date[d m y]keep absolute a - n keep a]]2]

Try it online!

\$\endgroup\$
2
\$\begingroup\$

05AB1E, 84 bytes

4Ö¹тÖ_²3@PU•ΘÏF•5°X*+ºS₂+©²<£O+•23õ₄ƶ₃-¹å•60в.¥X+¦19(šs.xDVdi®ηODY‹O©£θYα®>¹)ë12D¹<)

Can definitely be golfed some more.. 05AB1E doesn't have any date builtins, so everything is done manually.

Input as three loose inputs in the order year,month,day, output as a triplet in the format [day,month,year].

Try it online.

Explanation:

Determine if the input is a leap year, and if the input-month is NOT January nor February:

4Ö            # Check if the (implicit) first input-year is divisible by 4
¹тÖ_          # Check that the first input-year is NOT divisible by 100
²3@           # Check that the second input-month is >= 3
    P         # Check if all three are truthy by taking the product of the stack
              # (1 if truthy; 0 if falsey)
     U        # Pop and store this in variable `X`

Convert the input to an integer \$n\$, being the (1-based) \$n^{th}\$ day of the year:

•ΘÏF•         # Push compressed integer 5254545
5°            # Push 10 to the power 5: 100000
  X*          # Multiply it by `X`
    +         # Add it to the integer (5354545 if `X` is truthy; 5254545 if falsey)
º             # Mirror it: 5354545454535 or 5254545454525
 S            # Convert it to a list of digits
  ₂+          # Add 26 to each: [31,28 or 29,31,30,31,30,31,31,30,31,30,31,28 or 29,31]
    ©         # Store this list in variable `®` (without popping)
²<            # Push the second input-month, and decrease it by 1
  £           # Leave that many leading values of the list
   O          # Sum them
    +         # And add them to the (implicit) third input-day

Create a list of values \$k\$, representing the \$k^{th}\$ day of the year for the dates [prevYear-12-12, year-02-28 or 29, year-04-04, year-05-09, year-06-06, year-07-11, year-08-08, year-09-05, year-10-10, year-11-07, year-12-12]:

•23õ₄ƶ₃-¹å•   # Push compressed integer 36033721893183342948
 60в          # Convert it to base-60 as list: [59,35,35,28,35,28,28,35,28,35,48]
    .¥        # Undelta it with leading 0: [0,59,94,129,157,192,220,248,283,311,346,394]
      X+      # Add `X` to each
        ¦     # Remove the leading 0 (or 1)
         19(š # And prepend -19 instead

Get \$k\$ closest to \$n\$:

s             # Swap so `n` is at the top of the stack
 .x           # And get the value of the list closest to it
   DV         # And store a copy in variable `Y`

And convert that result back into a date to output:

di            # If the result is non-negative (>=0):
  ®           #  Push the list from variable `®`
   η          #  Get its prefixes
    O         #  And sum each prefix: [31,60,91,121,152,182,213,244,274,305,335,366,395,426]
  D           #  Duplicate it
   Y‹         #  Check for each whether it's smaller than `Y` (1 if truthy; 0 if falsey)
     O        #  Sum those checks
      ©       #  Store it in variable `®` (without popping)
   £          #  Leave that many leading values from the list
    θ         #  Then only leave its last value
     Yα       #  And take its absolute difference with `Y`
       ®>     #  Push `®` + 1
         ¹    #  Push the first input-year
          )   #  And wrap all three values on the stack into a list
 ë            # Else:
  12D         #  Push two 12s
     ¹<       #  Push the first input-year - 1
       )      #  And wrap all three values on the stack into a list
              # (after which it is output implicitly as result)

See this 05AB1E tip of mine (sections How to compress large integers? and How to compress integer lists?) to understand why •ΘÏF• is 5254545; •23õ₄ƶ₃-¹å• is 36033721893183342948; and •23õ₄ƶ₃-¹å•60в is [59,35,35,28,35,28,28,35,28,35,48].

\$\endgroup\$
1
\$\begingroup\$

Bash + Core utilities, 173 169 bytes

y=${1%%-*}
for E in {4..12} {3/1/{$y,$[y+1]}-,1/2/$y-2}1day
{
((E))&&E+=/$[E%2?12^E:E]/$y
a=date\ -d;k=$[(`$a$1 +%s`-`$a$E +%s`)**2];((k<n|!n))&&{ n=$k;D=$E;}
}
$a$D +%F

Try the test cases online!

This is a full program. Input is passed as an argument in yyyy-mm-dd format. Output is on stdout in the same format.

This uses several tricks:

  • For each month E from April to December, the Doomsday day in that month is computed as E for even months and E xor 12 for odd months.
  • The last day of February is computed as 1 day before March 1. (This applies to both the current year and the next year.)
  • December 12 of the previous year is computed as 21 days before January 2 of the current year.
  • The last two computations (ending with -1day and -21day) are combined using bash's brace expansion).
  • The date command with first option -d is saved in a variable for use like a macro.
  • Some spurious error messages are written to stderr.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.