Skip to content

Instantly share code, notes, and snippets.

@williame
Last active August 29, 2015 14:06
Show Gist options
  • Save williame/2e93260c918e4bc1434e to your computer and use it in GitHub Desktop.
Save williame/2e93260c918e4bc1434e to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
versions = [version.strip() for version in (
"""
def t(b):
L=len;w,n=L(b[0])+3,-1;b=list("@"*w+"@@".join(b)+"@"*w);w+=n;I=b.index
for i in range(L(b)):
c=b[i];d={"^":-w,"<":n,">":1,"v":w}.get(c)
if d:
while c!='@':
i+=d;c=b[i]
if c=='*':b[i]='.'
elif c in "123456789":i+=I(c)-i or I(c,i+1)-i
elif c in '/\\\\':d={-w:n,w:1,1:w,n:-w}[d]*(-1 if c=='/' else 1)
return "*" not in b
""",
"""
def t(b):
L=len;w=L(b[0])+3;b=list("@"*w+"@@".join(b)+"@"*w);w-=1;I=b.index
for i in range(L(b)):
c=b[i];d={"^":-w,"<":-1,">":1,"v":w}.get(c)
if d:
while c!='@':
i+=d;c=b[i]
if c=='*':b[i]='.'
elif c in "123456789":i+=I(c)-i or I(c,i+1)-i
elif c in '/\\\\':d={-w:-1,w:1,1:w,-1:-w}[d]*(-1 if c=='/' else 1)
return "*" not in b
""",
"""
def t(b):
L=len;w=L(b[0])+3;b=list("@"*w+"@@".join(b)+"@"*w);w-=1;I=b.index
for i in range(L(b)):
c=b[i];d={"^":-w,"<":-1,">":1,"v":w}.get(c)
if d:
while c!='@':
i+=d;c=b[i]
if c=='*':b[i]='.'
elif c in '/\\\\':d={-w:-1,w:1,1:w,-1:-w}[d]*(-1 if c=='/' else 1)
elif c>'0':i+=I(c)-i or I(c,i+1)-i
return "*" not in b
""",
"""
def t(b):
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);C="<>v^";D=[-1,1,w,-w];i=0
while i<l:
d=C.find(B[i])
if-1<d:
j=i;n=l*4
while('@'!=B[j])*n:
c=B[j]
if'+'>c:B[j]=''
if'/'==c:d=(d+2)%4
if'\\\\'==c:d=3-d
if c.isdigit():j=(B*2).index(c,j+1)%l
j+=D[d];n-=1
i+=1
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0
while i<l:
d="<>v^".find(B[i])
if-1<d:
j=i;n=l*4
while(B[j]!='@')*n:
c=B[j]
if'+'>c:B[j]=''
if'/'==c:d=(d+2)%4
if'\\\\'==c:d=3-d
if c.isdigit():j=(B*2).index(c,j+1)%l
j+=[-1,1,w,-w][d];n-=1
i+=1
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0
while i<l:
d="<>v^".find(B[i])
if-1<d:
j=i;c='.'
while c not in"@<>v^":
if'+'>c:B[j]=''
if'/'==c:d^=2
if'\\\\'==c:d^=3
elif'0'<c:j=(B*2).index(c,j+1)%l
j+=[-1,1,w,-w][d];c=B[j]
i+=1
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0
while i<l:
d="<>v^".find(B[i]);j=i;c='.'
while c not in"@<>v^":
if'+'>c:B[j]=''
if'/'==c:d^=2
if'\\\\'==c:d^=3
elif'0'<c:j=(B*2).index(c,j+1)%l
j+=[-1,1,w,-w,-i][d];c=B[j]
i+=1
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+1;B=list('@'*w+'@'.join(b)+'@'*w);l=len(B);i=0
while i<l:
d="<>v^".find(B[i]);j=i;c='.'
while c not in"@<>v^":
if'+'>c:B[j]=''
if'/'==c:d^=2
if'\\\\'==c:d^=3
elif'0'<c:j=(B*2).index(c,j+1)%l
j+=[-1,1,w,-w,-j][d];c=B[j]
i+=1
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+1;B=list('@'*w+'@'.join(b));i=l=len(B);C="<>^v@"
while i:
j=l-i;i-=1;d=C.find(B[j]);c='.'
while c not in C:
if'+'>c:B[j]='.'
if'0'<c<C:j=(B*2).index(c,j+1)%l
elif'.'<c:d^=2+(c<C)
j-=[1,-1,w,-w,j][d];c=B[j%l]
return'*'not in B
""",
"""
def t(b):
w=len(b[0])+1;B=list('@'*w+'@'.join(b));i=l=len(B);C="<>^v@"
while i:
i-=1;d,j,c=C.find(B[i]),i,'.'
while(c in C)-1:
if'+'>c:B[j]='.'
if'0'<c<C:j=(B*2).index(c,j+1)%l
elif'.'<c:d^=2+(c<C)
j-=[1,-1,w,-w,j][d];c=B[j%l]
return('*'in B)-1
""")]
import sys
def ndiff(src,dest):
# compute edit distance
src_len, dest_len = len(src), len(dest)
d = {(-1, -1): -1}
for i in xrange(src_len):
d[i, -1] = i
for j in xrange(dest_len):
d[-1, j] = j
for i in xrange(src_len):
for j in xrange(dest_len):
d[i, j] = min(
d[i-1, j] + 1, # delete
d[i, j-1] + 1, # insert
d[i-1, j-1] + (src[i] != dest[j])) # substitute
# backtrack to recover cheapest path
path = 'S'
while i or j:
i, j, s = min(
(i-1, j, 'D'),
(i, j-1, 'I'),
(i-1, j-1, 'S'),
key = lambda(i, j, s): d.get((i, j), sys.maxint))
path += s
# yield in standard ndiff format
src_ofs = dest_ofs = 0
for i, s in enumerate(path[::-1]):
c, d = src[i + src_ofs], dest[i + dest_ofs]
if s == 'S':
if c == d:
yield ' ', d
else:
yield '-', c
yield '+', d
elif s == 'D':
dest_ofs -= 1
yield '-', c
else:
src_ofs -= 1
yield '+', d
width = max(len(line) for version in versions for line in version.split("\n"))
height = max(len(version.split("\n")) for version in versions)
import difflib
from PIL import Image, ImageFont, ImageDraw, ImageChops, GifImagePlugin
font = ImageFont.truetype("/Library/Fonts/OsakaMono.ttf", 15)
em, lineheight = 9, 14
anim_frames = 1
theme = {
"identifier": "blue",
"keyword": "black",
"string": "maroon",
"number": "magenta",
"punctuation": "gray",
"whitespace": None,
}
def clike_highlighter(src, keywords, theme=theme):
import string
x = y = i = 0
out = []
def emit(stop, typ):
colour = theme.get(typ, "black")
# print "(%s \"%s\" %s)" % (typ, src[start:stop].replace("\n","¶"), colour)
for c in src[start:stop]:
out.append((c, colour))
while i < len(src):
start = i
ch = src[i]
if ch in "\'\"":
while True:
i += 1
if src[i] == '\\':
i += 1
elif src[i] == ch:
i += 1
emit(i, "string")
break
elif ch in string.punctuation:
i += 1
emit(i, "punctuation")
elif ch in string.whitespace:
i += 1
emit(i, "whitespace")
elif ch in string.digits:
while True:
i += 1
if i == len(src) or (src[i] not in string.digits and src[i] not in "e."):
emit(i, "number")
break
else:
while True:
i += 1
if i == len(src) or src[i] in "\'\"" or src[i] in string.punctuation or src[i] in string.whitespace:
emit(i, "keyword" if src[start:i] in keywords else "identifier")
break
return out
from keyword import kwlist as python_keywords
versions = [clike_highlighter(version, python_keywords) for version in versions]
def render(text, ofs, diff, diff_ch, label):
frames = []
for frame in range(anim_frames if ofs is not None else 1):
image = Image.new("RGB", (width * em + int(em * 1.5), height * lineheight + int(lineheight/2)), "white")
frames.append(image)
draw = ImageDraw.Draw(image)
x, y = 0, 0
for i, (ch, colour) in enumerate(text):
if ch == '\n':
x = 0
y += lineheight
else:
if i == ofs:
draw.rectangle((x, y, x + em, y + lineheight),
fill='red' if diff=='+' else 'blue')
scale = 1.0 / 3 * frame
if diff == '-':
scale = 1.0 - scale
draw.text((x, y), ch, font=font,
fill='yellow')
draw.rectangle((x + em, y, x + em + em, y + lineheight),
fill='white')
x += int(round(float(em) * scale))
if colour:
draw.text((x, y), ch, font=font, fill=colour)
x += em
if label:
label = str(label)
x = width * em - len(label) * em
draw.rectangle((x, 0, width * em, lineheight), fill='yellow')
draw.text((x, 0), label, font=font, fill='red')
del draw
return frames
def compute_frames(prev, curr):
adjust = 0
for ofs, diff in enumerate(ndiff(prev, curr)):
diff, ch = diff[0], diff[-1]
if diff in "+-":
yield prev, ofs-adjust, diff, ch
if diff == "+":
prev = prev[:ofs-adjust] + [ch] + prev[ofs-adjust:]
else:
assert prev[ofs-adjust] == ch, (ofs, prev[:ofs+1], ch)
prev = prev[:ofs-adjust] + prev[ofs-adjust+1:]
adjust += 1
assert prev == curr, ["%d= %s != %s" % (i, prev[i].replace('\n','¶').replace(' ','¢'),
curr[i].replace('\n','¶').replace(' ','¢')) for i in range(min(len(prev),len(curr))) if prev[i] != curr[i]]
frames = render(versions[0], None, None, None, len(versions[0]))
prev = versions[0]
for i in range(3, len(versions)):
curr = versions[i]
print "==== %d (%d %d) ====" % (i, len(curr), len(prev)-len(curr))
for frame in compute_frames(prev, curr):
frames += render(*frame, label="%d -> %d" % (len(prev), len(curr)))
frames += render(versions[-1], None, None, None, len(versions[-1]))
frames = frames[:2000] ### max frames to keep demo small enough to upload to stackoverflow
with open("anim.gif", "wb") as f:
prev = None
for frame in frames:
frame = frame.convert('P', palette=Image.WEB)
if not prev:
for s in GifImagePlugin.getheader(frame)[0]:
f.write(s)
prev = frame.copy()
frame = GifImagePlugin.getdata(frame)
else:
delta = ImageChops.subtract_modulo(frame, prev)
prev = frame.copy()
bbox = delta.getbbox()
if bbox:
frame = GifImagePlugin.getdata(frame.crop(bbox), offset = bbox[:2])
else:
continue
for s in frame:
f.write(s)
f.write(";")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment