Changeset - 686166c7d5bc
[Not reviewed]
default
0 13 0
Laman - 3 years ago 2022-03-05 18:08:48

added more whitespace
10 files changed with 123 insertions and 80 deletions:
0 comments (0 inline, 0 general)
src/diana/diana.py
Show inline comments
 
import os
 
import re
 

	
 
from . import config as cfg
 
from . import go
 
from .go import BLACK,WHITE,EMPTY
 
from .sgfParser import ParserError
 
from .sgfParser.collection import Collection
 
from .drawer.svg import Svg
 
from .drawer.tikz import Tikz
 

	
 

	
 
def collectMoves(root):
 
	node=root
 
	while len(node.children)>0:
 
		b=node.getProp("B")
 
		w=node.getProp("W")
 
		if b is not None: yield ("b",b)
 
		elif w is not None: yield ("w",w)
 
		if b is not None:
 
			yield ("b", b)
 
		elif w is not None:
 
			yield ("w", w)
 
		# else: yield None # !! not really robust
 

	
 
		node=node.children[0]
 

	
 

	
 
class SourceFile:
 
	def __init__(self,fileName):
 
		self.fileName=fileName
 
		self._shortName= "".join(re.split(r'[/\\]', fileName)[-1].split('.')[:-1])
 
		self._game=go.Go()
 

	
 
		with open(self.fileName, 'r', encoding=cfg.encoding) as f:
 
			games=Collection(f.read()).listGames()
 
		self._record=list(games)[0]
 
		self._moves=list(collectMoves(self._record.root))
 

	
 
	def process(self):
 
		print("{0}... ".format(self.fileName), end="")
 

	
 
		i=1
 
		for k in range(0,len(self._moves),cfg.movesPerDiagram):
 
			filename=os.path.join(cfg.outputDir,"{0}-{1}".format(self._shortName,i))
 
			self.createDiagram(k,k+cfg.movesPerDiagram).save(filename,"templ-pleb.svg")
 
			i+=1
 

	
 
		infoStr="""{GN}
 
B: {PB} {BR}
 
W: {PW} {WR}
 
{DT}
 
{RE}""".format(**self.fetchGameInfo(["GN","PB","BR","PW","WR","DT","RE"],""))
 
		notes=open(os.path.join(cfg.outputDir,"{0}.txt".format(self._shortName)), 'w')
 
		notes.write(infoStr)
 
		notes.close()
 
		print("done")
 

	
 
	def createDiagram(self,start,end):
 
		# initialize the diagram
 
		template=Svg()
 

	
 
		self._setMove(start)
 

	
 
		# draw current state
 
		for lineNumber,line in enumerate(self._game.board):
 
			for itemNumber,item in enumerate(line):
 
				if item!=EMPTY: template.addStone(itemNumber,lineNumber,"b" if item==BLACK else "w")
 
				if item != EMPTY:
 
					template.addStone(itemNumber, lineNumber, "b" if item==BLACK else "w")
 

	
 
		# draw the moves
 
		for k in range(start,end):
 
			if k>=len(self._moves): break
 
			if k >= len(self._moves):
 
				break
 

	
 
			color,move=self._moves[k]
 
			if move==tuple():
 
				template.overlays.append((k,"pass")) # !!
 
				continue
 
			else:
 
				(c,r)=move
 

	
 
			if not self._move(color,c,r):
 
				if cfg.keepBroken: continue
 
				else: return False
 
				if cfg.keepBroken:
 
					continue
 
				else:
 
					return False
 

	
 
			# draw the move
 
			template.addMove(c,r,color,k+1)
 

	
 
		return template
 

	
 
	def fetchGameInfo(self,fieldNames,default=None):
 
		return {k:self._record.get(k,default) for k in fieldNames}
 

	
 
	def _setMove(self,k):
 
		self._game=go.Go()
 

	
 
		blackStones=self._record.root.getProp("AB")
 
		whiteStones=self._record.root.getProp("AW")
 
		if blackStones:
 
			for p in blackStones:
 
				self._game.board[p.r][p.c]=BLACK
 
		if whiteStones:
 
			for p in whiteStones:
 
				self._game.board[p.r][p.c]=WHITE
 

	
 
		for i in range(k):
 
			(color,move)=self._moves[i]
 
			if move==tuple(): continue # pass
 
			if move == tuple():
 
				continue # pass
 
			self._move(color,*move)
 

	
 
	def _move(self,color,c,r):
 
		if not self._game.move(BLACK if color=='b' else WHITE, c,r):
 
			# !! we do not honor http://red-bean.com/sgf/ff5/m_vs_ax.htm at the moment
 
			msg="illegal move: {0} at {1},{2}".format(self._game.moveCount+1,c,r)
 
			if cfg.keepBroken:
 
				print(msg)
 
			else:
 
				msg+=". aborted"
 
				print(msg)
 
				return False
 
		return True
 

	
 

	
 
def main():
 
	cfg.parseArgs()
 
	print("processing:")
 
	files=cfg.inputFiles[:]
 

	
 
	for item in files:
 
		if os.path.isfile(item):
 
			try:
 
				f=SourceFile(item)
 
				f.process()
 
			except ParserError as e:
 
				print("Couldn't parse {0}, following error occured: {1}".format(item,e))
 
		elif os.path.isdir(item):
 
			files+=[os.path.join(item,child) for child in os.listdir(item)]
 
			print("contents of the '{0}' directory added to the queue".format(item))
 
		else: print("the '{0}' path could not be resolved to either a file nor a directory".format(item))
 
		else:
 
			print("the '{0}' path could not be resolved to either a file nor a directory".format(item))
src/diana/drawer/svg.py
Show inline comments
 
from .base import Base
 

	
 

	
 
def adjustFont(base,text):
 
	text=str(text)
 
	if len(text)<2: return round(0.7*base)
 
	elif len(text)<3: return round(0.55*base)
 
	else: return round(0.4*base)
 

	
 

	
 
class DiagramPoint:
 
	def __init__(self,x,y,color="",label=""):
 
		self.x=x
 
		self.y=y
 
		self.color=color
 
		self.label=label
 

	
 
	def __repr__(self):
 
		return 'DiagramPoint({0},{1},"{2}","{3}")'.format(self.x,self.y,self.color,self.label)
 
	if len(text) < 2:
 
		return round(0.7*base)
 
	elif len(text) < 3:
 
		return round(0.55*base)
 
	else:
 
		return round(0.4*base)
 

	
 

	
 
class Svg(Base):
 
	extension="svg"
 

	
 
	padding=15
 
	highNumbers=True
 
	
 
	def __init__(self,start=0):
 
		super().__init__(start)
 
		self.boardSize=480
 
		self.padding=30
 

	
 
	def render(self,templateName,bgcolor=""):
 
		points = [p for (i,p) in sorted(self._index.values(), key=lambda x: x[0])]
 

	
 
		stones = [p for p in points if p.color and p.label==""]
 
		moves = [p for p in points if p.color and p.label]
 
		labels = [p for p in points if not p.color and p.label]
 

	
 
		params = {
 
			"boardSize":self.boardSize, "padding":self.padding, "stones":stones, "moves":moves,
 
			"labels":labels, "adjustFont":adjustFont, "bgcolor":bgcolor}
 

	
 
		return self._env.get_template(templateName).render(params)
 

	
 
	def save(self,filename,template="templ.svg",bgcolor=""):
 
		file=open(filename+".svg",'w')
 
		file.write(self.render(template,bgcolor))
 
		file.close()
 

	
 
		super().save(filename)
src/diana/drawer/tikz.py
Show inline comments
 
class Tikz:
 
	content=""
 
	footer=""
 
	extension="tex"
 

	
 
	highNumbers=True
 

	
 
	def __init__(self):
 
		self.content=r'''\begin{tikzpicture}
 
	\draw[step=\boardSquare,gray,very thin] (0,0) grid (18\boardSquare,18\boardSquare);
 
	\draw (0,0) rectangle (18\boardSquare,18\boardSquare);
 

	
 
	'''
 

	
 
		# hvězdy
 
		# stars
 
		for i in range(3):
 
			for j in range(3):
 
				self.content+=r'''  \filldraw[fill=black] ({0}\boardSquare,{1}\boardSquare) circle[radius=0.04];'''.format(6*i+3, 6*j+3)+'\n'
 
			self.content+='\n'
 

	
 
		self.footer=r'\end{tikzpicture}' '\n'
 

	
 
	def __str__(self):
 
		return self.content+self.footer
 

	
 
	def drawStone(self,x,y,color):
 
		fill="black" if color=="b" else "white"
 
		self.content+=r'  \filldraw[draw=black,fill={0}] ({1}\boardSquare,{2}\boardSquare) circle[radius=0.5\boardSquare];'.format(fill,x,18-y)+'\n'
 

	
 
	def drawMove(self,x,y,label,color):
 
		fill="black" if color=="b" else "white"
 
		labelColor="white" if color=="b" else "black"
 
		if (not self.highNumbers) and isinstance(label,int) and label%100!=0: label=label%100 # dost neobratná logika
 
		if (not self.highNumbers) and isinstance(label, int) and label%100 != 0:
 
			label = label%100
 

	
 
		self.content+=r'  \filldraw[draw=black,fill={0}] ({1}\boardSquare,{2}\boardSquare) circle[radius=0.5\boardSquare] node[color={3}]{{{4}}};'.format(fill,x,18-y,labelColor,label)+'\n'
 

	
 
	def getContent(self):
 
		return self.content+self.footer
src/diana/go.py
Show inline comments
 
BLACK=1
 
WHITE=-1
 
EMPTY=0
 

	
 

	
 
class Go:
 
	board=[[EMPTY]*19 for i in range(19)]
 
	
 
	def __init__(self):
 
		self.board=[[EMPTY]*19 for i in range(19)]
 
		self.moveCount=0
 
	
 
	def move(self,color,y,x):
 
		if self.board[x][y]!=EMPTY: return False
 
		if self.board[x][y] != EMPTY:
 
			return False
 

	
 
		self.board[x][y]=color
 

	
 
		for i,j in ((-1,0),(1,0),(0,-1),(0,1)):
 
			self.temp=[[False]*19 for i in range(19)]
 
			if not self._floodFill(-color,x+i,y+j): self._remove()
 
			if not self._floodFill(-color, x+i, y+j):
 
				self._remove()
 
		self.temp=[[False]*19 for i in range(19)]
 
		if not self._floodFill(color,x,y):
 
			self.board[x][y]=EMPTY
 
			return False
 
		self.moveCount+=1
 
		return True
 

	
 
	def _floodFill(self,color,x,y):
 
		if x<0 or x>18 or y<0 or y>18: return False
 
		if self.temp[x][y]: return False
 
		if self.board[x][y]==EMPTY: return True
 
		if self.board[x][y]!=color: return False
 
		if x < 0 or x > 18 or y < 0 or y > 18:
 
			return False
 
		if self.temp[x][y]:
 
			return False
 
		if self.board[x][y] == EMPTY:
 
			return True
 
		if self.board[x][y] != color:
 
			return False
 
		self.temp[x][y]=True
 
		return self._floodFill(color,x-1,y) or self._floodFill(color,x+1,y) or self._floodFill(color,x,y-1) or self._floodFill(color,x,y+1)
 

	
 
		return self._floodFill(color, x-1, y) or \
 
			self._floodFill(color, x+1, y) or \
 
			self._floodFill(color, x, y-1) or \
 
			self._floodFill(color, x, y+1)
 
	
 
	def _remove(self):
 
		for i in range(19):
 
			for j in range(19):
 
				if self.temp[i][j]: self.board[i][j]=EMPTY
 
				if self.temp[i][j]:
 
					self.board[i][j] = EMPTY
src/diana/sgfParser/__init__.py
Show inline comments
 
def skipWhitespace(s,start):
 
	i=start
 
	while i<len(s) and s[i].isspace(): i+=1
 
	while i < len(s) and s[i].isspace():
 
		i+=1
 

	
 
	return i
 

	
 

	
 
def strRowCol(s, i):
 
	k=0
 
	r,c=0,0
 
	(r, c) = (0, 0)
 
	for (r,line) in enumerate(s.splitlines(True)):
 
		c=i-k
 
		if k+len(line)>i:
 
			break
 
		else:
 
			k+=len(line)
 
	return (r+1, c+1)
 

	
 

	
 
class ParserError(Exception):
 
	def __init__(self,msg,s,i):
 
		self.msg=msg
 
		self.row,self.col=strRowCol(s,i)
 
		(self.row, self.col) = strRowCol(s, i)
 
		self.context=s[i:i+16]
 

	
 
	def __str__(self):
 
		return "{0} at row {1}, col {2}, got '{3}...' instead".format(self.msg,self.row,self.col,self.context)
 

	
 

	
 
class ParserWarning(ParserError):
 
	pass
src/diana/sgfParser/collection.py
Show inline comments
 
from .node import Node
 
from . import skipWhitespace, ParserError
 
from .gameRecord import GameRecord
 

	
 

	
 
class Collection:
 
	def __init__(self,s):
 
		self.gameTrees=[]
 
		i=skipWhitespace(s,0)
 
		if i>=len(s): return
 
		if i >= len(s):
 
			return
 
		elif not GameTree.fits(s,i):
 
			raise ParserError("expected a GameTree starting with '('",s,i)
 
		while GameTree.fits(s,i):
 
			i,x=GameTree.create(s,i)
 
			(i, x) = GameTree.create(s, i)
 
			self.gameTrees.append(x)
 
		if i<len(s):
 
			raise ParserError("expected EOF",s,i)
 

	
 
	def listGames(self):
 
		for tree in self.gameTrees:
 
			for game in tree.listGames(): yield game
 
			for game in tree.listGames():
 
				yield game
 

	
 

	
 
class GameTree:
 
	def __init__(self):
 
		self.nodes=[]
 
		self.branches=[]
 

	
 
	@staticmethod
 
	def fits(s,i):
 
		return i<len(s) and s[i]=="("
 

	
 
	@staticmethod
 
	def create(s,start):
 
		assert GameTree.fits(s,start)
 
		res=GameTree()
 

	
 
		i=skipWhitespace(s,start+1)
 
		if not Node.fits(s,i):
 
			raise ParserError("expected a Node starting with ';'",s,i)
 

	
 
		y=None
 
		while Node.fits(s,i):
 
			i,x=Node.create(s,i)
 
			(i, x) = Node.create(s, i)
 
			res.nodes.append(x)
 
			if y: y.addChild(x)
 
			if y:
 
				y.addChild(x)
 
			x.parent=y
 
			y=x
 
			i=skipWhitespace(s,i)
 

	
 
		while GameTree.fits(s,i):
 
			i,x=GameTree.create(s,i)
 
			(i, x) = GameTree.create(s, i)
 
			res.branches.append(x)
 
			subroot=x.getNode(0)
 
			subroot.parent=y
 
			if y: y.addChild(subroot)
 
			if y:
 
				y.addChild(subroot)
 
			i=skipWhitespace(s,i)
 

	
 
		if i>=len(s) or s[i]!=")":
 
			raise ParserError("expected end of a GameTree marked by ')'",s,i)
 
		i=skipWhitespace(s,i+1)
 
		return (i,res)
 

	
 
	## Expand multiple games into distinct GameTrees and yield each.
 
	def listGames(self):
 
		if len(self.nodes)==0: return None
 
		if len(self.nodes) == 0:
 
			return None
 
		for node in self.nodes[0].listGINodes():
 
			yield GameRecord(self._buildSubtree(node))
 

	
 
	def getNode(self,i):
 
		if 0<=i<len(self.nodes):
 
			return self.nodes[i]
 
		return None
 

	
 
	## Create and return a new game tree containing the provided Node.
 
	#
 
	# Ancestor nodes are copied, descendants are moved from the seedNode.
 
	def _buildSubtree(self,seedNode):
 
		node=seedNode.copy()
 
		node.setChildren(seedNode.children)
 
		seedNode.children=[]
 

	
 
		while node.parent:
 
			newNode=node.parent.copy()
 
			newNode.addChild(node)
 
			node=newNode
 

	
 
		return node
src/diana/sgfParser/node.py
Show inline comments
 
from collections import deque
 
import logging as log
 

	
 
from . import skipWhitespace, ParserWarning
 
from .property import Property, GAME_INFO
 

	
 

	
 
class Node:
 
	def __init__(self):
 
		self.properties=dict()
 
		self.parent=None
 
		self.children=[]
 

	
 
	@staticmethod
 
	def fits(s,i):
 
		return i<len(s) and s[i]==";"
 

	
 
	@staticmethod
 
	def create(s,start):
 
		assert Node.fits(s,start)
 
		res=Node()
 

	
 
		i=skipWhitespace(s,start+1)
 
		while Property.fits(s,i):
 
			i,x=Property.create(s,i)
 
			(i, x) = Property.create(s, i)
 
			if x.name in res.properties:
 
				log.warning(ParserWarning('duplicate "{0}" property in a node. second value ignored'.format(x.name),s,i))
 
			else:
 
				res.properties[x.name]=x
 
			i=skipWhitespace(s,i)
 
		return (i,res)
 

	
 
	def listGINodes(self):
 
		if self.isGINode(): yield self
 
		if self.isGINode():
 
			yield self
 
		empty=not self.isGINode()
 

	
 
		node=self
 
		while node.parent:
 
			node=node.parent
 
			if node.isGINode():
 
				empty=False
 
				yield node
 

	
 
		queue=deque(self.children)
 
		while len(queue)>0:
 
			node=queue.popleft()
 
			if node.isGINode():
 
				empty=False
 
				yield node
 
			queue.extend(node.children)
 
		if empty: yield self # always yield at least self, can work as GINode as well as any other
 
		if empty:
 
			yield self  # always yield at least self, can work as GINode as well as any other
 

	
 
	def isGINode(self):
 
		return any(prop.type==GAME_INFO for prop in self.properties.values())
 

	
 
	def setProp(self,name,value):
 
		self.properties[name]=value
 
		# zkontrolovat typ value
 
		# check value type
 

	
 
	def setChildren(self,children):
 
		self.children=children
 
		for child in children: child.parent=self
 
		for child in children:
 
			child.parent = self
 

	
 
	def addChild(self,node):
 
		if node in self.children: return node
 
		if node in self.children:
 
			return node
 
		node.parent=self
 
		self.children.append(node)
 
		return node
 

	
 
	def removeChild(self,node):
 
		if node not in self.children:
 
			return None
 
		del self.children[self.children.index(node)]
 
		node.parent=None
 
		return node
 

	
 
	def removeChildAt(self,i):
 
		if -len(self.children)<i<len(self.children):
 
			res=self.children[i]
 
			del self.children[i]
 
			res.parent=None
 
			return res
 
		return None
 

	
 
	## Create a copy of the Node, with the same parent and deep copied properties, no copied children.
 
	def copy(self):
 
		res=Node()
 
		res.properties={k: v.copy() for (k,v) in self.properties.items()}
 
		res.parent=self.parent
 
		return res
 

	
 
	def getProp(self,name,default=None):
 
		if name in self.properties: return self.properties[name].value
 
		else: return default
 
		if name in self.properties:
 
			return self.properties[name].value
 
		else:
 
			return default
 

	
 
	## Returns textual representation of the Node itself, but disregards its children.
 
	def __str__(self):
 
		return ";" + "".join(str(p) for p in self.properties.values())
 

	
 
	def export(self):
 
		# there is a beautiful recursive solution, which this stack is too narrow to contain
 
		stack=[(self,1,1)]
 
		output=[]
 

	
 
		while len(stack)>0:
 
			node,left,right=stack.pop()
 
			if left>0: output.append("("*left)
 
			(node, left, right) = stack.pop()
 
			if left > 0:
 
				output.append("("*left)
 
			output.append(str(node)+"\n")
 

	
 
			childCount=len(node.children)
 
			if childCount==0: # a leaf
 
				output.append(")"*right+"\n")
 
			elif childCount==1: # a line
 
				stack.append((node.children[0],0,right))
 
			else: # a branching node
 
				# first child pops first, last child closes parent's parentheses
 
				children=zip(node.children,[1]*childCount,[1]*(childCount-1)+[1+right])
 
				stack.extend(reversed(children))
 
				stack.extend(reversed(list(children)))
 

	
 
		return "".join(output)
src/diana/sgfParser/propValues.py
Show inline comments
 
@@ -3,176 +3,181 @@ import re
 
from . import ParserError, skipWhitespace
 

	
 

	
 
class Regexp:
 
	number=re.compile(r"(\+|-|)\d+")
 
	real=re.compile(r"(\+|-|)\d+(\.\d+)?")
 
	point=re.compile(r"[a-zA-Z]{2}|")
 
	text=re.compile(r"(?:.*?[^\\])??(?:\\\\)*(?=])", re.DOTALL)
 
	composedText=re.compile(r"(?:.*?[^\\])??(?:\\\\)*(?=]|:)", re.DOTALL)
 

	
 
	class Text:
 
		softBreaks=re.compile(r"(^|[^\\])((\\\\)*)\\((\n\r)|(\r\n)|\r|\n)")
 
		whitespace=re.compile(r"[\t\f\v]")
 
		simpleWhitespace=re.compile(r"[\t\f\v\n\r]")
 
		removeSlashes=re.compile(r"(^|[^\\])((\\\\)*)\\($|[^\\])")
 
		unescapeSlashes=re.compile(r"\\\\")
 

	
 

	
 
class Composed:
 
	def __init__(self,a=None,b=None):
 
		self.a=a
 
		self.b=b
 

	
 
	def __str__(self):
 
		return "{0}:{1}".format(self.a,self.b)
 

	
 

	
 
class Point:
 
	def __init__(self,c,r):
 
		self.r=r
 
		self.c=c
 

	
 
	def __iter__(self):
 
		yield self.c
 
		yield self.r
 

	
 
	def __str__(self):
 
		a=ord("a")
 
		return chr(a+self.c)+chr(a+self.r)
 

	
 

	
 
## Metatype matching one of the provided types.
 
#
 
# Returns the first match, so the order is important.
 
def choose(*vTypes):
 
	def f(s,start):
 
		for vType in vTypes:
 
			try:
 
				i,x=vType(s,start)
 
				(i, x) = vType(s, start)
 
				return (i,x)
 
			except ParserError: pass
 
			except ParserError:
 
				pass
 
		raise ParserError("no variant of a 'choose' property value matched",s,start)
 
	return f
 

	
 

	
 
def singletonFits(s,i):
 
	return i<len(s) and s[i]=="["
 

	
 

	
 
def singletonEnds(s,i):
 
	return i<len(s) and s[i]=="]"
 

	
 

	
 
def singleton(vType):
 
	def f(s,start):
 
		if not singletonFits(s,start):
 
			raise ParserError("expected a property value starting with '['",s,start)
 
		i,x=vType(s,start+1)
 
		(i, x) = vType(s, start+1)
 
		if not singletonEnds(s,i):
 
			raise ParserError("expected a property value ending with ']'",s,i)
 
		i=skipWhitespace(s,i+1)
 
		return (i,x)
 
	return f
 

	
 

	
 
def listOf(vType,allowEmpty=False):
 
	def f(s,start):
 
		i=start
 
		if not singletonFits(s,i):
 
			raise ParserError("expected a property value starting with '['",s,i)
 
		if singletonEnds(s,i+1) and allowEmpty:
 
			i=skipWhitespace(s,i+2)
 
			return (i,[])
 
		single=singleton(vType)
 
		i,x=single(s,i)
 
		(i, x) = single(s, i)
 
		res=[x]
 
		while singletonFits(s,i):
 
			i,x=single(s,i)
 
			(i, x) = single(s, i)
 
			res.append(x)
 
		return (i,res)
 
	return f
 

	
 

	
 
def compose(vTypeA,vTypeB):
 
	def f(s,start):
 
		i,a=vTypeA(s,start)
 
		(i, a) = vTypeA(s, start)
 
		if i>=len(s) or s[i]!=":":
 
			raise ParserError("expected a composed property value separated by ':'",s,i)
 
		i,b=vTypeB(s,i+1)
 
		(i, b) = vTypeB(s, i+1)
 
		return (i,Composed(a,b))
 
	return f
 

	
 

	
 
def number(s,start):
 
	m=Regexp.number.match(s,start)
 
	if m is None: raise ParserError("expected a number matching '{0}'".format(Regexp.number.pattern),s,start)
 
	if m is None:
 
		raise ParserError("expected a number matching '{0}'".format(Regexp.number.pattern), s, start)
 
	res=int(m.group(0))
 
	return (m.end(),res)
 

	
 

	
 
def real(s,start):
 
	m=Regexp.real.match(s,start)
 
	if m is None: raise ParserError("expected a real number matching '{0}'".format(Regexp.real.pattern),s,start)
 
	if m is None:
 
		raise ParserError("expected a real number matching '{0}'".format(Regexp.real.pattern), s, start)
 
	res=float(m.group(0))
 
	return (m.end(),res)
 

	
 

	
 
def double(s,start):
 
	c=s[start]
 
	if c not in ("1", "2"):
 
		raise ParserError("expected a double value, either '1' or '2'",s,start)
 
	return (start+1,c)
 

	
 

	
 
def color(s,start):
 
	c=s[start]
 
	if c not in ("B", "W"):
 
		raise ParserError("expected a color value, either 'B' or 'W'",s,start)
 
	return (start+1,c)
 

	
 

	
 
def text(simple=True,composed=False):
 
	def f(s,start):
 
		regexps=Regexp.Text
 
		m=Regexp.composedText.match(s,start) if composed else Regexp.text.match(s,start)
 
		res=m.group(0)
 
		res=regexps.softBreaks.sub(r"\1\2",res) # remove soft line breaks
 
		if simple:
 
			res=regexps.simpleWhitespace.sub(" ",res) # convert whitespace to spaces, no escapes
 
		else:
 
			res=regexps.whitespace.sub(" ",res) # convert whitespace to spaces, no escapes
 
		res=regexps.removeSlashes.sub(r"\1\2\4",res)
 
		res=regexps.unescapeSlashes.sub(r"\\",res) # unescape slashes
 

	
 
		return (m.end(),res)
 
	return f
 

	
 

	
 
def empty(s,start): return (start,"")
 
def empty(s, start):
 
	return (start, "")
 

	
 

	
 
def anything(s,start):
 
	esc=False
 
	i=start
 
	for i,c in enumerate(s[start:],start):
 
	for (i, c) in enumerate(s[start:], start):
 
		if esc: esc=False
 
		elif c=="\\": esc=True
 
		elif c=="]": break
 
	return (i,s[start:i])
 

	
 

	
 
# go specific
 
def point(s,start):
 
	m=Regexp.point.match(s,start) # !! limit to board size
 
	if m is None: raise ParserError("expected a point value matching '{0}'".format(Regexp.point.pattern),s,start)
 
	if m is None:
 
		raise ParserError("expected a point value matching '{0}'".format(Regexp.point.pattern), s, start)
 
	if m.group(0)=="": # pass, !! tt
 
		return (m.end(),tuple())
 
	col=m.group(0)[0]
 
	row=m.group(0)[1]
 
	col=ord(col)-(ord("a") if "a"<=col<="z" else ord("A")-26)
 
	row=ord(row)-(ord("a") if "a"<=row<="z" else ord("A")-26)
 
	return (m.end(),Point(col,row))
 

	
 

	
 
move=point
 
stone=point
src/diana/sgfParser/property.py
Show inline comments
 
import re
 
from datetime import date
 
import logging as log
 

	
 
from .propValues import choose, singleton, listOf, compose, number, real, double, color, text, empty, anything, point, move, stone
 
from . import skipWhitespace, ParserError
 

	
 
GAME_INFO=1
 
UNKNOWN=99
 

	
 

	
 
class DateException(Exception):
 
	pass
 

	
 

	
 
class Property:
 
	identRegexp=re.compile(r"[A-Z]+")
 

	
 
	def __init__(self):
 
		self.name=""
 
		self.value=""
 

	
 
	@staticmethod
 
	def fits(s,i):
 
		return i<len(s) and s[i].isupper()
 

	
 
	@staticmethod
 
	def create(s,start):
 
		assert Property.fits(s,start)
 
		res=Property()
 
		i,res.name=Property.ident(s,start)
 
		(i, res.name) = Property.ident(s, start)
 
		i=skipWhitespace(s,i)
 
		try:
 
			i,x=Property.createValue(s,i,res.name)
 
		except ParserError as e: # malformed value
 
			(i, x) = Property.createValue(s, i, res.name)
 
		except ParserError as e:  # a malformed value
 
			log.warning(e)
 
			i,x=choose(listOf(anything), singleton(anything))(s,i)
 
			(i, x) = choose(listOf(anything), singleton(anything))(s, i)
 
			res.name="_"+res.name
 
		res.value=x
 
		if res.name=="DT":
 
			res=DateProperty(x)
 
		i=skipWhitespace(s,i)
 
		return (i,res)
 

	
 
	@staticmethod
 
	def ident(s,start):
 
		m=Property.identRegexp.match(s,start)
 
		if m is None: raise ParserError("expected a property identifier matching '[A-Z]+'",s,start)
 
		if m is None:
 
			raise ParserError("expected a property identifier matching '[A-Z]+'", s, start)
 
		return (m.end(),m.group())
 

	
 
	@staticmethod
 
	def createValue(s,start,name):
 
		if name in Property.patterns:
 
			return Property.patterns[name](s,start)
 
		else:
 
			log.info("unknown property %s at position %d",name,start)
 
			return choose(listOf(anything), singleton(anything))(s,start)
 

	
 
	@property
 
	def type(self):
 
		gameInfo={"AN","BR","BT","CP","DT","EV","GN","GC","ON","OT","PB","PC","PW","RE","RO","RU","SO","TM","US","WR","WT"}
 
		if self.name in gameInfo: return GAME_INFO
 
		else: return UNKNOWN
 
		if self.name in gameInfo:
 
			return GAME_INFO
 
		else:
 
			return UNKNOWN
 

	
 
	def copy(self):
 
		res=Property()
 
		res.name=self.name
 
		res.value=self.value if not isinstance(self.value,list) else self.value[:]
 
		return res
 

	
 
	def __str__(self):
 
		name=self.name.lstrip("_")
 
		val="[{0}]".format(self.value) if not isinstance(self.value,list) else "".join("[{0}]".format(x) for x in self.value)
 
		val = "[{0}]".format(self.value) \
 
			if not isinstance(self.value, list) \
 
			else "".join("[{0}]".format(x) for x in self.value)
 
		return "{0}{1}".format(name,val)
 

	
 
	patterns={
 
		"B":singleton(move),
 
		"KO":singleton(empty),
 
		"MN":singleton(number),
 
		"W":singleton(move),
 
		"AB":listOf(stone), #
 
		"AE":listOf(point), #
 
		"AW":listOf(stone), #
 
		"PL":singleton(color),
 
		"C":singleton(text(simple=False)),
 
		"DM":singleton(double),
 
		"GB":singleton(double),
 
		"GW":singleton(double),
 
		"HO":singleton(double),
 
		"N":singleton(text()),
 
		"UC":singleton(double),
 
		"V":singleton(real),
 
		"BM":singleton(double),
 
		"DO":singleton(empty),
 
		"IT":singleton(empty),
 
		"TE":singleton(double),
 
		"AR":listOf(compose(point,point)), #
 
		"CR":listOf(point), #
 
		"DD":listOf(point,allowEmpty=True), #
 
		"LB":listOf(compose(point,text())), #
 
		"LN":listOf(compose(point,point)), #
 
		"MA":listOf(point), #
 
		"SL":listOf(point), #
 
		"SQ":listOf(point), #
 
		"TR":listOf(point), #
 
		"AP":singleton(compose(text(composed=True),text())), #
 
		"CA":singleton(text()),
 
		"FF":singleton(number),
 
		"GM":singleton(number),
 
		"ST":singleton(number),
 
		"SZ":choose(singleton(number),singleton(compose(number,number))), #
 
		"AN":singleton(text()),
 
		"BR":singleton(text()),
 
		"BT":singleton(text()),
 
		"CP":singleton(text()),
 
		"DT":singleton(text()),
 
		"EV":singleton(text()),
 
		"GN":singleton(text()),
 
		"GC":singleton(text(simple=False)),
 
		"ON":singleton(text()),
 
		"OT":singleton(text()),
 
@@ -134,54 +139,59 @@ class Property:
 
		"OB":singleton(number),
 
		"OW":singleton(number),
 
		"WL":singleton(real),
 
		"FG":choose(singleton(empty),singleton(compose(number,text()))), #
 
		"PM":singleton(number),
 
		"VW":listOf(point,allowEmpty=True), #
 

	
 
		# go specific
 
		"HA":singleton(number),
 
		"KM":singleton(real),
 
		"TB":listOf(point,allowEmpty=True),
 
		"TW":listOf(point,allowEmpty=True)
 
	}
 

	
 

	
 
class DateProperty(Property):
 
	def __init__(self,value):
 
		super().__init__()
 
		self.name="DT"
 
		self.value=[]
 
		self.rawValue=value
 
		self.parse(value)
 

	
 
	def parse(self,s):
 
		regexp=re.compile(r"\d{4}(-\d\d){0,2}(,(\d{4}(-\d\d){0,2}|\d\d(-\d\d)?))*")
 
		match=re.search(regexp,s)
 
		if not match:
 
			raise DateException('Could not parse a DT value: "{0}"'.format(s))
 
		substr=match.group(0)
 
		dateStrs=substr.split(",")
 
		dates=[]
 
		prevFormat=None
 

	
 
		for s in dateStrs:
 
			try:
 
				(prevFormat,d)=DateProperty.parseSingle(s,prevFormat,dates[-1] if dates else None)
 
			except ValueError:
 
				raise DateException('Could not parse a DT value: "{0}"'.format(s))
 
			dates.append(d)
 
		self.value=dates
 

	
 
	@staticmethod
 
	def parseSingle(dateStr,prevFormat,prev=None):
 
		tokens=dateStr.split("-")
 
		num_tokens=list(map(int,tokens))
 
		if len(tokens)==3:
 
			return ("YMD",date(*num_tokens))
 
		elif len(tokens)==2:
 
			if len(tokens[0])==4: return ("YM",date(*num_tokens,1))
 
			else: return ("MD",date(prev.year,*num_tokens))
 
			if len(tokens[0]) == 4:
 
				return ("YM", date(*num_tokens, 1))
 
			else:
 
				return ("MD", date(prev.year, *num_tokens))
 
		else:
 
			if len(tokens[0])==4: return ("Y",date(*num_tokens,1,1))
 
			elif prevFormat in ("YM","M"): return ("M",date(prev.year,*num_tokens,1))
 
			else: return ("D",date(prev.year,prev.month,*num_tokens))
 
			if len(tokens[0]) == 4:
 
				return ("Y", date(*num_tokens, 1, 1))
 
			elif prevFormat in ("YM","M"):
 
				return ("M", date(prev.year, *num_tokens, 1))
 
			else:
 
				return ("D", date(prev.year, prev.month, *num_tokens))
src/diana/tests/testSgfParser.py
Show inline comments
 
from itertools import chain
 
from datetime import date
 
import unittest
 
from unittest import TestCase
 
import os
 

	
 
from diana.sgfParser import strRowCol
 
from diana.sgfParser.collection import Collection
 
from diana.sgfParser.property import Property,DateProperty,DateException
 
from diana.sgfParser.propValues import text,compose
 

	
 

	
 
dataDir=os.path.join(os.path.dirname(__file__), "data")
 

	
 

	
 
class TestUtils(TestCase):
 
	def testTextPos(self):
 
		s="abc\ndef\rgh\r\nij\n\rklmn"
 
		rc=[
 
			[1,2,3,4],
 
			[1,2,3,4],
 
			[1,2,3,4],
 
			[1,2,3], [1], # don't care about LFCR, we unicode now
 
			[1,2,3,4]
 
		]
 
		res=chain((r+1,c) for (r,row) in enumerate(rc) for c in row)
 
		for (i,(r,c)) in zip(range(len(s)+1), res):
 
			self.assertEqual(strRowCol(s, i), (r, c))
 

	
 

	
 
class TestProperty(TestCase):
 
	def testName(self):
 
		with self.assertRaises(AssertionError):
 
			Property.create("[99]",0)
 
		with self.assertRaises(AssertionError):
 
			Property.create("99[99]",0)
 

	
 
		i,prop=Property.create("MN[99]",0)
 
		(i, prop) = Property.create("MN[99]", 0)
 
		self.assertNotEqual((i,prop), (0,None))
 
		self.assertEqual((i,prop.name), (6,"MN"))
 

	
 
	def testText(self):
 
		s=r"""[abc\
 
def
 
ghi]"""
 
		self.assertEqual(text()(s,1)[1], "abcdef ghi")
 
		self.assertEqual(text(False)(s,1)[1], "abcdef\nghi")
 

	
 
		s="""[m\\no\\\tpqr\\]\\\\]"""
 
		self.assertEqual(text()(s,1)[1], "mno pqr]\\")
 
		self.assertEqual(text(False)(s,1)[1], "mno pqr]\\")
 

	
 
		s="""[abc:def]"""
 
		parsed=compose(text(composed=True),text(composed=True))(s,1)
 
		self.assertEqual(str(parsed[1]), "abc:def")
 

	
 

	
 
class TestDateProperty(TestCase):
 
	def testSingle(self):
 
		self.assertEqual(DateProperty.parseSingle("2019","Y")[1], date(2019,1,1))
 
		self.assertEqual(DateProperty.parseSingle("2019-06","YM")[1], date(2019,6,1))
 
		self.assertEqual(DateProperty.parseSingle("2019-06-22","YMD")[1], date(2019,6,22))
 
		d=date(2019,6,21)
 
		self.assertEqual(DateProperty.parseSingle("22","D",d)[1], date(2019,6,22))
 
		self.assertEqual(DateProperty.parseSingle("07-22","MD",d)[1], date(2019,7,22))
 
		self.assertEqual(DateProperty.parseSingle("2020-07-22","YMD",d)[1], date(2020,7,22))
 
		with self.assertRaises(ValueError):
 
			DateProperty.parseSingle("2019-31","YMD")
 

	
 
	def testParse(self):
 
		self.assertEqual(DateProperty("1996-05,06").value, [date(1996,5,1),date(1996,6,1)])
 
		self.assertEqual(DateProperty("1996-05-06,07,08").value, [date(1996,5,6),date(1996,5,7),date(1996,5,8)])
 
		self.assertEqual(DateProperty("1996,1997").value, [date(1996,1,1),date(1997,1,1)])
 
		self.assertEqual(DateProperty("1996-12-27,28,1997-01-03,04").value, [date(1996,12,27),date(1996,12,28),date(1997,1,3),date(1997,1,4)])
 
		self.assertEqual(DateProperty("1997-05-05,1997-05-06").value, [date(1997,5,5),date(1997,5,6)])
 
		self.assertEqual(DateProperty("Published on 1997-05-06").value, [date(1997,5,6)])
 
		with self.assertRaises(DateException):
 
			DateProperty("unknown")
 

	
 

	
 
class TestCollection(TestCase):
 
	def testSubtrees(self):
 
		c=Collection("""
 
(;B[aa]
 
	(;W[ab]PB[Some Black]PW[Some White];B[ac])
 
	(;W[bb]PB[Other Black]PW[Other White])
 
)""")
 
		games=list(c.listGames())
 

	
 
		self.assertEqual(len(games),2)
 
		self.assertRegex(games[0].export(), r"^\(;B\[aa]\n;(PB\[Some Black]|PW\[Some White]|W\[ab]){3}\n;B\[ac]\n\)\n$")
 
		self.assertRegex(games[1].export(), r"^\(;B\[aa]\n;(PB\[Other Black]|PW\[Other White]|W\[bb]){3}\n\)\n$")
 

	
 
	def testEmptySgf(self):
 
		Collection("(;)")
 

	
 
	def testSimpleSgf(self):
 
		with open(os.path.join(dataDir, "simple.sgf")) as f:
 
			Collection(f.read())
 

	
 
	def testComplexSgf(self):
 
		with open(os.path.join(dataDir, "kogos.sgf")) as f:
 
			Collection(f.read())
 

	
 

	
 
if __name__ == '__main__':
 
	unittest.main()
0 comments (0 inline, 0 general)