Happy Repdigit Day!

Today being February 2, 2022, I got to thinking about dates. Since most people will write today as “2/2/22”, today is one of the few days that can be written all using the same digit. I thought about how to find all such dates in a century, and the first thing I needed was a name for such dates. I came up with “uninumeric”, but a Google search turned up this Wikipedia article, which calls them repdigits, as in “repeated digits”.

SPOILER ALERT: the solution is shown at the end of this post. Try to come up with your own guess as to how many such dates there are before continuing.

We have to set some ground rules on this exercise first. Should January 1, 2001 count as a repdigit date? I’m going to say that leading zeroes can be dropped when finding repdigit dates, so this date, when written as “1/1/1”, would be a repdigit. Trailing zeroes cannot be dropped, so January 10, 2001 would not be a repdigit date.

I have some other date filters in mind, so I decided to create a CSV table of all the dates in a century, truncating the year to modulo 100 so that the list would apply to any modern century. (Leap days are always the fly in the ointment in such tables, and if I use the current century to make this table, it would omit February 29 from the 0 year. Fortunately, leap days could not qualify as repdigit dates, so I’m not going to worry about it today.)

I pulled up my trusty Python IDE and imported my simple table package littletable, plus some names from the Python datetime package:

from datetime import datetime, timedelta
import littletable as lt

I need something to give me all the days in a century, but unfortunately, Python’s range() builtin won’t allow this:

dates = range(datetime(2000, 1,1), datetime(2100, 1,1), timedelta(1))

range() only accepts int arguments.

So I needed to write a daterange function (I have said this so many times, that it might qualify as McGuire’s Law – Before you can ever do anything, you always have to do something else first. Unfortunately, McGuire’s Law is recursive.)

def daterange(from_: datetime, to_: datetime, step_: timedelta = timedelta(days=1)):
    cur = from_
    while cur < to_:
        yield cur
        cur += step_

This generator function follows the same convention as Python’s range(): it is inclusive of the starting value, but exclusive of the ending value.

With daterange, I can now create my littletable Table of dates in the current century:

dates = lt.Table()
dates.insert_many({"year": date.year % 100, "month": date.month, "day": date.day}
                  for date in daterange(datetime(2000, 1, 1), datetime(2100, 1, 1)))

For nice presentation, we can also add a str field (I’m showing year/month/day for the format, to avoid any discussions about why Americans write month/day/year while the rest of the world writes day/month/year – fortunately, when looking for repdigit dates, either style works the same):

dates.add_field("str",
                lambda rec: f"{rec.year:02d}/{rec.month:02d}/{rec.day:02d}")

Now to detect which dates are repdigit dates, we need a predicate function that takes a record from our table, and returns whether the date is a repdigit. A function to detect a repdigit string is just:

def is_repdigit(intstr: str):
    return len(set(intstr)) == 1

And for easy filtering, add a “YMD” field to all the records in our table, which contains the digits used to write each date, dropping leading zeroes:

dates.add_field("YMD", lambda rec: f"{rec.year}{rec.month}{rec.day}")

Getting and presenting the matching dates is now just:

repdigit_dates = dates.where(YMD=is_repdigit)
repdigit_dates.present(caption=f"{len(repdigit_dates)} dates")

Which shows these dates:

  Year   Month   Day   Str        Ymd     
 ──────────────────────────────────────── 
     1       1     1   01/01/01   111     
     1       1    11   01/01/11   1111    
     1      11     1   01/11/01   1111    
     1      11    11   01/11/11   11111   
     2       2     2   02/02/02   222     
     2       2    22   02/02/22   2222    
     3       3     3   03/03/03   333     
     4       4     4   04/04/04   444     
     5       5     5   05/05/05   555     
     6       6     6   06/06/06   666     
     7       7     7   07/07/07   777     
     8       8     8   08/08/08   888     
     9       9     9   09/09/09   999     
    11       1     1   11/01/01   1111    
    11       1    11   11/01/11   11111
    11      11     1   11/11/01   11111
    11      11    11   11/11/11   111111
    22       2     2   22/02/02   2222
    22       2    22   22/02/22   22222
    33       3     3   33/03/03   3333
    44       4     4   44/04/04   4444
    55       5     5   55/05/05   5555
    66       6     6   66/06/06   6666
    77       7     7   77/07/07   7777
    88       8     8   88/08/08   8888
    99       9     9   99/09/09   9999

26 dates

With this table, we can now try some other what-ifs (left as exercises for the reader):

  • dates that are repdigits if you don’t drop the leading zeroes
  • dates that are binary dates (written with just 0’s and 1’s)
  • dates that are palindromes (now you have to decide on month/day/year vs. day/month/year)

Here is all the code from this post, including code that, rather than regenerating the table every time, first tries loading the table from a CSV:

from datetime import datetime, timedelta
import littletable as lt

def daterange(from_: datetime, to_: datetime, step_: timedelta = timedelta(days=1)):
    cur = from_
    while cur < to_:
        yield cur
        cur += step_


def is_repdigit(intstr: str) -> bool:
    return len(set(intstr)) == 1


try:
    dates = lt.Table().csv_import("dates_in_century.csv",
                                  transforms={}.fromkeys("year month day".split(), int))
except FileNotFoundError:
    dates = lt.Table()
    dates.insert_many({"year": date.year % 100, "month": date.month, "day": date.day}
                      for date in daterange(datetime(2000, 1, 1), datetime(2100, 1, 1)))
    dates.add_field("str",
                    lambda rec: f"{rec.year:02d}/{rec.month:02d}/{rec.day:02d}")
    dates.add_field("YMD", lambda rec: f"{rec.year}{rec.month}{rec.day}")
    dates.csv_export("dates_in_century.csv")


repdigit_dates = dates.where(YMD=is_repdigit)
repdigit_dates.present(caption=f"{len(repdigit_dates)} dates")

  1. Leave a comment

Leave a comment