# Convert a date to xkcd notation

In his xkcd about the ISO 8601 standard date format Randall snuck in a rather curious alternative notation:

The large numbers are all the digits that appear in the current date in their usual order, and the small numbers are 1-based indices of the occurrences of that digit. So the above example represents 2013-02-27.

Let's define an ASCII representation for such a date. The first line contains the indices 1 to 4. The second line contains the "large" digits. The third line contains the indices 5 to 8. If there are multiple indices in a single slot, they are listed next to each other from smallest to largest. If there are at most m indices in a single slot (i.e. on the same digit, and in the same row), then each column should have be m+1 characters wide and left-aligned:

2  3  1  4
0  1  2  3  7
5     67    8


See also the companion challenge for the opposite conversion.

## The Challenge

Given an ISO 8601 date (YYYY-MM-DD), output the corresponding xkcd date notation.

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.

Any year from 0000 to 9999 is valid input.

Trailing spaces are allowed, leading spaces are not. You may optionally output a single trailing newline.

Standard rules apply.

## Test Cases

2013-02-27
2  3  1  4
0  1  2  3  7
5     67    8

2015-12-24
2  3  1     4
0  1  2  4  5
5  67 8

2222-11-11
1234
1    2
5678

1878-02-08
1     3  24
0  1  2  7  8
57    6     8

2061-02-22
2   4   1   3
0   1   2   6
5       678

3564-10-28
1 4 2 3
0 1 2 3 4 5 6 8
6 5 7         8

1111-11-11
1234
1
5678

0123-12-30
1 2 3 4
0 1 2 3
8 5 6 7


# Pyth, 86 78 bytes

JS{K-z\-=GheS.nmmlkd=N,mf<T5d=ZmmhkxdcK1Jmf>T4dZjbmjkmj"".[|k\ \ Gd[hNm]dcJ1eN


I will try to golf this down as soon as my brain has recovered. I was happy just to see it work.

# JavaScript (ES6), 168 173

As an anonymous function. Using template strings, there is a newline near the end that is significant and included in the byte count.

d=>d.replace(/\d/g,c=>m=(l=((o[c]=o[c]||[b,c,b])[p/2&~1]+=++p).length)>m?l:m,t=[m=p=b='',b,b],o=[])&o.map(x=>x&&x.map((x,i)=>t[i]+=(x+'     ').slice(0,m+1)))||t.join



Less golfed

f=d=>(
// get the indices in o and the columns width in m
m=0,
p=0,
o=[],
d.replace(/\d/g,c=>(
o[c] = o[c]||['',c,''], // for each found digit :array with top indices, digit, bottom indices
o[c][p/2 & ~1] += ++p, // (p/2 and not 1) maps 0..3 to 0, 4..7 to 2
l = o[c].length,
m = l>m ? l : m // max indices string length in m
)),
// build the output in t
t=['','',''],
o.map(x=> x && x.map(
(x,i) => t[i]+=(x+'     ').slice(0,m+1)) // left justify, max value of m is 4
),
t.join\n // return output as a newline separated string
)


Test snippet

f=d=>
d.replace(/\d/g,c=>m=(l=((o[c]=o[c]||[b,c,b])[p/2&~1]+=++p).length)>m?l:m,t=[m=p=b='',b,b],o=[])&
o.map(x=>x&&x.map((x,i)=>t[i]+=(x+'     ').slice(0,m+1)))
||t.join\n

console.log=x=>O.textContent+=x+'\n'

;['2013-02-27','2015-12-24','2222-11-11','1878-02-08','2061-02-22','3564-10-28','1111-11-11']
.forEach(t=>console.log(t+'\n'+f(t)+'\n'))
<pre id=O></pre>

• Oh, good additional test case. I'll add that to both challenges. – Martin Ender Dec 20 '15 at 10:20

# Ruby, 200195189178162 157 characters

(156 characters code + 1 character command line option)

o={}
i=0
$_.gsub(/\d/){o[$&]||=['','']
o[$&][i/4]+="#{i+=1}"} o=o.sort.map &:flatten puts [1,0,2].map{|i|o.map{|c|c[i].ljust o.flatten.map(&:size).max}*' '}  Sample run: bash-4.3$ ruby -n xkcd-date.rb <<< '2013-02-27'
2  3  1  4
0  1  2  3  7
5     67    8

bash-4.3$ruby -n xkcd-date.rb <<< '2222-11-11' 1234 1 2 5678 bash-4.3$ ruby -n xkcd-date.rb <<< '3564-10-28'
1 4 2 3
0 1 2 3 4 5 6 8
6 5 7         8


# Python 2.7, 308 310 bytes

i=raw_input().replace("-","")
s,w=sorted(set(i)),len
x,m={},0
for c in s:
q,v=i,[""]*2
while c in q:a=str(-~q.index(c)+(w(i)-w(q)));v[int(a)>4]+=a;q=q[q.index(c)+1:]
m,x[c]=max(m,max(map(w,v))),v
for l in[0,1]:print"".join((lambda x:x+(-~m-w(x))*" ")("".join(x[n][l]))for n in s)+"\n"+(" "*m).join(s)*(-l+1)


Wow, fixing it only costed 2 bytes!

The date doesn't have to be separated, the date can be any length, it doesn't have to be a date, it could be any string (but dashes are removed). The middle part looks pretty golfable to me.

## C#, 456

Golfed:

string  x(string p){string s=p.Replace("-", ""),a="",d="",e="";var u=new Dictionary<char,List<int>>();for(int i=0;i<s.Length;i++)if(u.ContainsKey(s[i]))u[s[i]].Add(i+1);else u.Add(s[i],new List<int>{i+1});foreach (var c in u.Keys.OrderBy(k=>k)){var t=String.Join("",u[c].Where(i=>i<5));var b=String.Join("",u[c].Where(i=>i>4));var l=Math.Max(t.Length,b.Length);var m=c+"".PadRight(l);a+=t.PadRight(l)+" ";e+=m;d+=b.PadRight(l)+" ";}return a+"\n"+e+"\n"+d;}


Ungolfed:

string  x(string p)
{
string s = p.Replace("-", ""),a = "", d = "", e = "";;
var u = new Dictionary<char, List<int>>();
for (int i = 0; i < s.Length; i++) if (u.ContainsKey(s[i])) u[s[i]].Add(i + 1); else u.Add(s[i], new List<int>{ i + 1 });
foreach (var c in u.Keys.OrderBy(k => k))
{
var t = String.Join("", u[c].Where(i => i < 5));
var b = String.Join("", u[c].Where(i => i > 4));
var l = Math.Max(t.Length, b.Length);
var m = c + "".PadRight(l);
a += t.PadRight(l) + " ";
e += m;
d += b.PadRight(l) + " ";
}
return a + "\n" + e + "\n" + d;
}


# Perl6, 265 bytes

Golfed

my$i=get;$i~~s:g/\-//;my@b=$i.comb.unique.sort;my$f={$i.comb[$_[1]-1]eq$_[0]??$_[1]!!''};my$g={[~] .map:$f};my$h={(@b X @^a).rotor(4).map:$g}my@a=$h(1..4);my@c=$h(5..8);my$s=max(|@a».chars,|@c».chars)+1;my$x='%-'~$s~'s';for @a,@b,@c {say [~] @_.map: *.fmt($x)}


Ungolfed (slightly)

my $i = get;$i ~~ s:g/\-//;
my @b = $i.comb.unique.sort; my$f = { $i.comb[$_[1]-1] eq $_[0] ??$_[1] !! '' };
my $g = { [~] .map:$f };
my $h = { (@b X @^a).rotor(4).map:$g }
my @a = $h(1..4); my @c =$h(5..8);
my $s = max(|@a».chars, |@c».chars)+1; my$x = '%-'~$s~'s'; for @a,@b,@c { say [~] @_.map: *.fmt($x) }


# Python 3, 306 bytes

I'm investigating ways to determine, before assembling the top and bottom lines, what the maximum width of any given column will be. Once I've got that, I should be able to build the spaces into the lines directly instead of using all those join functions.

j=''.join
def t(d):
c,*l={},;i,*n=0,
for e in d.replace('-',''):
i+=1
try:c[e]+=[i]
except:c[e]=i,
m=sorted(c)
for x in m:
l+=[j(str(p)for p in c[x]if p<5)]
n+=[j(str(p)for p in c[x]if p>4)]
f='<'+str(max(map(len,l+n)))
return'\n'.join(map(lambda o:' '.join(format(i,f)for i in o),(l,m,n)))

• Couldn't you save ''.join to a variable? – Dennis Dec 21 '15 at 4:29
• @Dennis: Huh. I wonder why I'd never thought of that. Maybe because I was subconsciously treating ''.join(...) as join('',...), which would need to be "saved" as a function instead. – Tim Pederick Dec 21 '15 at 4:32
• I think +=[i] can be +=i,. – Jonathan Frech Oct 19 '18 at 22:37
• return '\n' -> return'\n'. – Jonathan Frech Oct 19 '18 at 22:38
• @TimPederick Are you sure? (Note the comma!) – Jonathan Frech Oct 20 '18 at 10:58

# Powershell, 174170168 167 bytes

$a=@{}$args|% t*y|?{$_-45}|%{if(!$a.$_){$a.$_="","","$_"}$a.$_[++$i-gt4]+=$i}
0,2,1|%{$r=$_
-join($a.Keys|sort|%{$a.$_[$r]}|% p*ht(1+($a|% v*|%{$_|% l*h}|sort)[-1]))}


Less golfed test script:

$f = {$a=@{}                              # a hash table for a result
$args|% toCharArray|?{$_-45}|%{     # for each digit from argument strings except a '-'
if(!$a.$_){$a.$_="","","$_"} # create an array if there are no values for the current digit$a.$_[++$i-gt4]+=$i # append the character to the relative row (0 - top, 1 - bottom) and increment position number } # the result is a hash table in which the key is a char of a digit and the value is an array of string # for example, first lines for the first test case: # @{ # [char]48: ("2","5","0") # [char]49: ("3","","1") # [char]50: ("1","67","2") # ... # }$l=1+($a|% Values|%{$_|% Length}|sort)[-1]      # calc the maximum width of strings
0,2,1|%{                            # for each number 0,2,1
$r=$_                           # store it as row number
-join(
$a.Keys|sort| # for each keys (digit of the dates) in the sorted order %{$a.$_[$r]}|           # push to pipe the relative string
% padRight $l # for which to execute the 'padright' method to pad with spaces ) # and finally join the row } } @( ,("2013-02-27", "2 3 1 4 ", "0 1 2 3 7", "5 67 8") ,("2015-12-24", "2 3 1 4", "0 1 2 4 5", " 5 67 8 ") ,("2222-11-11", " 1234 ", "1 2 ", "5678 ") ,("1878-02-08", " 1 3 24 ", "0 1 2 7 8 ", "57 6 8 ") ,("2061-02-22", "2 4 1 3 ", "0 1 2 6 ", "5 678 ") ,("3564-10-28", " 1 4 2 3 ", "0 1 2 3 4 5 6 8 ", "6 5 7 8 ") ,("1111-11-11", "1234 ", "1 ", "5678 ") ,("0123-12-30", "1 2 3 4 ", "0 1 2 3 ", "8 5 6 7 ") ) | % {$d, $expected =$_
$result = &$f $d$d
$j=0$result|%{
"$($_.trimEnd() -eq $expected[$j].TrimEnd()): |$_|" # Vertical bars only to see trailing and leading spaces$j++
}
}


Output (vertical bars only to see trailing and leading spaces):

2013-02-27
True: |2  3  1  4     |
True: |0  1  2  3  7  |
True: |5     67    8  |
2015-12-24
True: |2  3  1     4  |
True: |0  1  2  4  5  |
True: |   5  67 8     |
2222-11-11
True: |     1234 |
True: |1    2    |
True: |5678      |
1878-02-08
True: |   1     3  24 |
True: |0  1  2  7  8  |
True: |57    6     8  |
2061-02-22
True: |2   4   1   3   |
True: |0   1   2   6   |
True: |5       678     |
3564-10-28
True: |      1 4 2 3   |
True: |0 1 2 3 4 5 6 8 |
True: |6 5 7         8 |
1111-11-11
True: |1234 |
True: |1    |
True: |5678 |
0123-12-30
True: |1 2 3 4 |
True: |0 1 2 3 |
True: |8 5 6 7 |