15
\$\begingroup\$

Back in 1965, The Beatles released their hit song 'Eight Days a Week'. In this challenge we are going to reimagine dates of the 21st century as if there really were eight days a week.

Input

A Gregorian calendar date between 1 January 2001 and 31 December 2100 inclusive. You may take input in any convenient format (including built-in date objects).

Output

The weekday number, week number, and week-numbering year (all defined below) corresponding to the input date. You may use any format (to be specified in your answer) in which all three numbers are unambiguously identifiable.

Week date system

The week date system, based on the ISO week date system but modified for eight-day weeks, works like this:

  • Reckoning of dates begins on 1 January 2001 (Gregorian), which is weekday number 1 of week number 1 of week-numbering year 2001.
  • Weeks begin on weekday number 1 and end on weekday number 8. (So 9 January 2001 is weekday number 1 of week number 2 of week-numbering year 2001.)
  • Week number 1 of a week-numbering year is the week that contains 4 January.
  • Week-numbering years contain exactly 45 or 46 weeks. Thus a week-numbering year begins on weekday number 1 of week number 1 and ends on weekday number 8 of week number 45 or 46.

Given that the number of days in a Gregorian calendar year is never a multiple of 8, the above rules have two important consequences:

  • Week number 1 of a week-numbering year may begin in the last four days of December of the previous Gregorian year.
  • Week number 46 (if there is one) of a week-numbering year may end in the first three days of January of the next Gregorian year.

Test cases

Input (Gregorian yyyy-mm-dd) -> Output (week-numbering year, week number, weekday number)

2001-01-01 -> 2001, 1,1 
2001-01-08 -> 2001, 1,8
2001-01-09 -> 2001, 2,1
2001-12-31 -> 2001,46,5
2002-01-01 -> 2001,46,6
2002-01-04 -> 2002, 1,1
2002-12-29 -> 2002,45,8
2002-12-30 -> 2003, 1,1
2020-02-29 -> 2020, 8,7
2037-09-13 -> 2037,32,5
2061-04-23 -> 2061,15,4
2100-12-31 -> 2101, 1,4

Related, but that challenge involves standard 7-day weeks, has finicky I/O requirements, and bans date/time libraries and built-ins.

\$\endgroup\$
2
  • \$\begingroup\$ May the weekday be 0-indexed? \$\endgroup\$
    – Arnauld
    Jul 4, 2020 at 8:59
  • \$\begingroup\$ @Arnauld Yes that's fine. \$\endgroup\$
    – Dingus
    Jul 4, 2020 at 10:02

4 Answers 4

5
\$\begingroup\$

JavaScript (ES6),  129 ... 107  105 bytes

Expects a Date object. Returns [year, week, weekday].

d=>(g=n=>(w=/an 0[^9]/.test(x=new Date(99,24,n))?++y/y:w+1,q=(d-x)/864e5)>3?g(n+8):[y,w,5+q])(-3,w=y=2e3)

Try it online!

How?

We start a few days before January 1, 2001 and progressively advance in the future, adding 8 days at each iteration. We increment the year and reset the week number each time we reach the 4th of January. We stop as soon as the target date is passed.

The most important part in the code is:

/an 0[^9]/.test(x = new Date(99, 24, n))

When the year argument is less than 100, the Date() constructor interprets it as 19xx. So, new Date(99, 24, n) means 24 months and n-1 days after January 1, 1999, or n-1 days after January 1, 2001.

When passed to the .test() method, the date is implicitly turned into a string. For instance, new Date(99, 24, 5) is converted to:

"Fri Jan 05 2001 00:00:00 GMT+0000 (Coordinated Universal Time)"

January is the only month whose 3-letter abbreviation ends in -an. So /an 0[^9]/ is used to test if the date is between January 1 and January 8 (both included).

What we really want to know is whether we are between January 4 and January 11, but the corresponding regular expression would be significantly longer. It's shorter to do it that way and use an offset of -3 days instead. This is why n is initialized to -3.

Commented

d => (                // d = input date
  g = n =>            // g is a recursive function taking a number of days n
    (                 //
      w =             // update w:
        /an 0[^9]/    //   if the following date x is between January 1 and
        .test(        //   January 8 (meaning that x + 3 days is within the
          x =         //   week including the 4th of January)
          new Date(   //     where x is defined as ...
            99, 24, n //       ... n-1 days after January 1, 2001
          )           //       (24 months and n-1 days after January 1, 1999)
        ) ?           //   then:
          ++y / y     //     increment the year y and set w to 1
        :             //   else:
          w + 1,      //     increment w
      q = (d - x)     // if x + 3 days is less than the target date d
          / 864e5     // i.e. the difference in days q between d and x
    ) > 3 ?           // is greater than 3:
      g(n + 8)        //   do a recursive call with n + 8,
                      //   i.e. one '8-day week' later
    :                 // else:
      [               //   return the result array:
        y,            //     year
        w,            //     week number
        5 + q         //     weekday: 8 + (q - 3)
      ]               //   end of array
)(-3, w = y = 2e3)    // initial call to g with n = -3 and y = 2000
\$\endgroup\$
4
\$\begingroup\$

T-SQL, 83 80 76 bytes

Saved 2 bytes by 0-indexing weekday.

Solved without looping

SELECT
year(z|7^1),(datepart(y,z|7^1)+7)/8,z%8FROM(SELECT
datediff(d,2,@)z)t

Try it online

In order to show this method provides the correct results, I have included a link to compare all test cases from the question. This link is not 0 indexed to allow easy comparison

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

JavaScript (Node.js), 166 bytes

d=>{w=(d-978336e6)/r+.5&7
for(D=0,e=new Date(d.getTime()+(7-w)*r);e.getMonth()!=0||e.getDate()!=4;D++)e.setTime(e.getTime()-r)
return[e.getYear(),8+D>>3,w+1]}
r=864e5

Takes input as a Javascript date object. Outputs as a 3-element list [week-year, week number, weekday number]. The week-numbering year is expressed as a 2-digit year (year minus 1900). If this is not acceptable, change e.getYear() to e.getFullYear() for +4 bytes.

Try it online!

Huh?

r=864e5 // milliseconds in a day
d=>{ // Take d as a date object
 w= // w is 1 less than the week number
  (d-978336e6) // milliseconds since Jan 1, 2001
  /r+  // Divide to get days
  .5&7 // Round (up or down) to nearest integer, and take mod 8.
       // This rounding smooths over DST and related variations
       // Variations over 12 hours do not occur, as far as I know
 for(
  D=0,  // D will be the number of days since the last Jan 4
  e=new Date(d.getTime()+(7-w)*r); // Initialize e to be the end of this week
  e.getMonth()!=0||e.getDate()!=4; // While e is not Jan 4 of any year:
   D++  // Increment D
  ) e.setTime(e.getTime()-r)  // Set e to the day before
 return [
  e.getYear(), // The week-numbering year of d is the same as the Gregorian year of the preceding Jan 4
  8+D>>3, // Convert days elapsed into weeks since Jan 4
  w+1 // the week number
 ]
}
\$\endgroup\$
3
\$\begingroup\$

Python 3, 181 bytes

def c(t):
 y,w,d=2001,1,1;D=type(t);O=D.toordinal
 for o in range(730487,O(t)+1):
  y,w,d=(y,w,d+1)if d<8 else(y,w+1,1)if O(D(y+1,1,4))not in range(o,o+8)else(y+1,1,1)
 return y,w,d

Try it online!

My approach counts years, weeks and days for every proleptic Gregorian ordinal between 2001-01-01 and the date. c expects a datetime.date instance of a date on or after 2001-01-01.

Every day the day counter is incremented unless a week has passed.
Every week the day counter is reset and the week counter is incremented, unless january 4th of the next year is in the next week.
Every year the week counter is reset and the year counter is incremented.

The magic number 730487 is the ordinal of 2001-01-02: 730487 == datetime.date(2001, 1, 2).toordinal()

c returns a tuple of integers containing the year, week number and day of the week in that order. week number and day of the week are >=1.

\$\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.