Calculate longest Stack Exchange streak

Introduction

This challenge is inspired by the Meta Stack Exchange question The longest consecutive days streak; it turns out that the URL https://codegolf.stackexchange.com/users/daily-site-access/[user id], where the last number is your user ID (found in the URL of your profile) contains information about which days you visited the site, in the following format:

var visited = {2015:{6:{3:1,8:1,12:1,13:1,18:1,19:1,21:1,22:1,23:1,24:1,26:1},7:{7:1,8:1,9:1,10:1,11:1,12:1,14:1,16:1,19:1,21:1,23:1,27:1,28:1,29:1,30:1},8:{1:1,2:1,3:1,5:1,7:1,17:1,19:1,23:1,26:1,28:1,30:1},9:{5:1,6:1,7:1,14:1,22:1,25:1,29:1,30:1},10: ...


(When viewed in a browser, the document seems empty; try viewing the source instead. Unless you are a ♦ moderator, you can only see this information for your own account.)

The string has a rather peculiar format but presumably it's an easy way to populate the calendar:

The information can be used to calculate the longest consecutive days streak, i.e. the one that determines if you get the Fanatic badge. The linked question has a working JavaScript example by user @ShadowWizard.

Standard rules apply: the shortest code wins.

Input

A string with the exact format provided by Stack Exchange, e.g. {2015:{6:{3:1}}}. Top level keys are years, second level keys are months, third level keys are days. All values are 1. You may assume the dates are not earlier than July 31st, 2008, which is when the first Stack Overflow question was posted. You may also assume there's at least one day in the input; the information is only provided to registered users, who must have visited the site at least once.

Output

The length of the longest streak, i.e. consecutive days in the input. You may choose to return it as a number or a string.

Test cases

(besides your own streak on CGCC of course; you should be able to verify if you're eligible for the Enthusiast and Fanatic badges or not)

Input Output
{2015:{6:{3:1}}} 1
{2015:{6:{3:1}},2016:{6:{3:1}}} 1
{2015:{6:{3:1},7:{3:1}}} 1
{2015:{6:{3:1,5:1}}} 1
{2015:{6:{3:1,4:1}}} 2
{2015:{2:{28:1},3:{1:1}}} 2
{2016:{2:{28:1},3:{1:1}}} 1
{2016:{1:{30:1},2:{1:1}}} 1
{2016:{4:{30:1},5:{1:1}}} 2
{2016:{4:{30:1},6:{1:1}}} 1
{2016:{12:{31:1}},2017:{1:{1:1}}} 2
{2016:{12:{31:1}},2018:{1:{1:1}}} 1
{2016:{11:{29:1},12:{30:1,31:1}},2017:{1:{1:1,2:1,4:1}}} 4
{2100:{2:{28:1},3:{1:1}}} 2
{2400:{2:{28:1},3:{1:1}}} 1
• Link to Sandbox entry Apr 4, 2021 at 18:34
• If the input is a string and we are to parse it with date logic then having the internet tag is incorrect ("This tag indicates that the challenge requires answers to use the Internet to retrieve information."). I believe you should describe the string in a little more detail (it seems to be calendar days) and the task at hand (it seems to be finding the longest consecutive streak of identified calendar days). Apr 4, 2021 at 18:45
• Not the person who downvoted. Strict input format is usually discouraged, which may contribute to the downvote here. However, for this challenge, since this isn't an arbitrary format and is actually the format given by SE, I think this is perfectly reasonable, and I don't know why it got downvoted especially so fast. Apr 4, 2021 at 18:50
• Nice first challenge! Apr 4, 2021 at 19:01
• Personally, I think the "strict" input format is part of the challenge, and is a good idea. It's not arbitrarily strict, and makes sense for the format (it's the format returned by the page, and is common JSON syntax), and parsing it shouldn't be any more complicated than navigating the equivalent JSON "dictionary" Apr 4, 2021 at 19:07

Python 2,  186 184 179 177 173 172  162 bytes

-10 thanks to ovs! (avoiding use of .items)

x=input()
from datetime import*
a=sorted(date(y,m,d)for y in x for m in x[y]for d in x[y][m])
print max(map(len,[d+timedelta(1)in a for d in a*2].split('s')))/6


Try it online! Or see the test-suite.

Huh, how?

In Python 2 input implicitly attempts an eval on the string which, in this case, will produce a nested dictionary.

a is then a list of the dates in order (sorted(...)), this is formed by inspecting the nested dictionaries in a nested for loop, and passing the resulting parts of the dates to date(year, month, day).

[d+timedelta(1)in a for d in a*2] forms a list of booleans which identify whether the next date is present for each date in the sorted dates repeated twice, a*2.

Note that the final value of a will always be False. The reason for repeating a is to avoid something later (see the final paragraph).

[...] gets the Python string representation (AKA repr) of that list - for example, something like "[False, True, True, False, False, False, True, True, False, False]" for an input with three consecutive dates between two others.

.split('s') splits the resulting string at occurrences of the 's' character - so for the above, that would be ['[Fal', 'e, True, True, Fal', 'e, Fal', 'e, Fal', 'e, True, True, Fal', 'e, Fal', 'e]'].

map(len,...) simply gives us a list of the lengths of the strings in the above list - so for the above, that would be [4, 18, 6, 6, 18, 6, 2].

max(...) gives the maximum value of that list - so for the above, that would be 18.

To find the number of consecutive days we want to count how many Trues there were in that longest section and then add one. In the above example, we can simply divide by six (the length of either 'True, ' or 'e, Fal'), hence the /6 at the end.

If the longest run of consecutive dates is at the left of the sorted dates we'd get something like '[True, True, Fal' (or '[Fal' when no dates at all are consecutive), which would be two shy of what we need, so if we were to perform all of this across a rather than a*2 we'd need to add two before performing the integer division (e.g. (...+2)/6). This wouldn't affect any other results but costs one more byte than simply repeating a with a*2, whereupon we'll get the longer string later on - this is guaranteed by the fact that d+timedelta(1) for the maximal d will give False.

• 162 bytes with indexing instead of .items().
– ovs
Apr 5, 2021 at 10:40
• Awesome, thanks @ovs! Seems I was so focused on the rest I neglected that. Apr 5, 2021 at 15:54

MATL, 45 43 bytes

Thanks to Jonathan Allan for pointing out a mistake, now corrected.

j':1?'0YXU2e"@Y:wXHx2e"H@Y:g3$YO]]vtf-FT#XM  How it works j % Input as (unevaluated) string ':1?' % Push this string. Will be interpreted as regexp 0 % Push 0. Will be interpreted as character 0 (equivalent to space) YX % Regexprep: replace ':' or ':1' by character 0 U % Evaluate string. Gives nested cell array 2e % Reshape as a 2D cell array with 2 rows (in column major order) " % For each column. Each column contains a year and a (nested) cell array @ % Push current column, as a cell array Y: % Unbox: pushes year, then the cell array w % Swap XHx % Copy to clipboard H and delete 2e % Reshape cell array as a 2D cell array with 2 rows " % For each column. Each column contains month and a cell array (of days) H % Push year from clipboard @ % Push current column, as a cell array Y: % Unbox: pushes month, then the cell array of days g % Convert cell array to numerical vector of days 3$YO  %     3-input datenum: converts year, month, days to serial date numbers
]       %   End
]         % End
v         % Concatenate stack into numerical vector of serial date numbers
tf        % Duplicate, find. Gives vector of the same size containing 1, 2, 3...
-         % Subtract, element-wise
FT#XM     % Second output of mode function: number of repetitions of the mode
% Implicit display

• @JonathanAllan Ah, of course. Thank you! Corrected now Apr 4, 2021 at 22:31
• Would Y'*QX> save the s? EDIT - guess not :p Apr 4, 2021 at 22:40
• @JonathanAllan Unfortunately no. The problem is that the vector of consecutive differences may be an empty vector, then the result of Y'* will be empty too, and Q applied to an empty array leaves it as it is Apr 4, 2021 at 22:44
• @JonathanAllan Got rid of it using the mode function :-) Apr 4, 2021 at 23:56

JavaScript (ES6), 122 bytes

Thanks to @tsh for a bug fix and some optimizations

o=>(D=k=1,M=g=(o,c)=>Object.keys(o).map(c))(o,y=>g(Y=o[y],m=>g(Y[m],d=>M>(D-(D=Date.UTC(y,m-1,d))<-1e8?k=1:++k)?0:M=k)))|M


Try it online!

Commented

o => (                  // o = input object
D =                   // D = previous day, as an integer
k = 1,                // k = streak length
M =                   // M = maximum streak length
g = (o, c) =>         // g is a helper function which applies a callback
Object.keys(o).map(c) // function c to each key of an object o
)(                      // 1st call to g with ...
o,                    //   ... the input object o
y =>                  //   for each year y,
g(                    //   do a 2nd call to g with ...
Y = o[y],           //     ... the object of months o[y]
m =>                //     for each month m,
g(                  //     do a 3rd call to g with ...
Y[m],             //       ... the object of days Y[m]
d =>              //       for each day d,
M > (             //       test whether M is greater than the updated k
D - (           //         subtract from D ...
D = Date.UTC( //         ... the new D corresponding to
y, m - 1, d //         this year, this month and this day
)             //
) < -1e8 ?      //         if the difference is less than -10**8 ms:
k = 1         //           reset k to 1
:               //         else:
++k           //           increment k
) ?               //       if M is greater than the new value of k:
0               //         do nothing
:                 //       else:
M = k           //         update M to k
)                   //     end of 3rd call
)                     //   end of 2nd call
) | M                   // end of 1st call; return M

• +1. I didn't know JavaScript could be THIS sacrilegious. Apr 5, 2021 at 5:09
• What should be the output for console.log(f({2015:{6:{3:1,4:1,5:1,8:1,9:1,10:1}}}))? Maybe 3?
– tsh
Apr 5, 2021 at 12:16
• +new Date(...) -> Date.UTC(...)
– tsh
Apr 5, 2021 at 12:19
• D=(D=...)-864e5 -> D-(D=...)>-1e8
– tsh
Apr 5, 2021 at 12:22
• M>(k=D-(D=Date.UTC(y,m-1,d-++k))?k:1)?0:M=k
– tsh
Apr 6, 2021 at 0:52