Skip to content
This repository has been archived by the owner on Apr 16, 2022. It is now read-only.

Added support for "\@" formatting code for dates. #57

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 106 additions & 4 deletions mailmerge.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import re
import shlex
from datetime import date, time, datetime
from copy import deepcopy
import re
import warnings
Expand Down Expand Up @@ -51,7 +54,7 @@ def __init__(self, file, remove_empty_tables=False):
m = r.match(instr)
if m is None:
continue
parent[idx] = Element('MergeField', name=m.group(1))
parent[idx] = Element('MergeField', name=m.group(1), data=instr)

for parent in part.findall('.//{%(w)s}instrText/../..' % NAMESPACES):
children = list(parent)
Expand Down Expand Up @@ -83,7 +86,7 @@ def __init__(self, file, remove_empty_tables=False):
m = r.match(instr_text)
if m is None:
continue
parent[idx_begin] = Element('MergeField', name=m.group(1))
parent[idx_begin] = Element('MergeField', name=m.group(1), data=instr_text)

# use this so we know *where* to put the replacement
instr_elements[0].tag = 'MergeText'
Expand Down Expand Up @@ -115,7 +118,7 @@ def __get_tree_of_file(self, file):
def write(self, file):
# Replace all remaining merge fields with empty values
for field in self.get_merge_fields():
self.merge(**{field: ''})
self.merge(**{field: None})

with ZipFile(file, 'w', ZIP_DEFLATED) as output:
for zi in self.zip.filelist:
Expand Down Expand Up @@ -253,16 +256,115 @@ def merge(self, parts=None, **replacements):
for part in parts:
self.__merge_field(part, field, replacement)

@classmethod
def eval_strftime(cls, dt, fmt):
if dt is None:
return ''
elif not isinstance(dt, (datetime, date, time, )):
return str(dt)

def repl(m):
res = ''
pattern = m.group(0)
while pattern:
# Years
if pattern[:4] in ['yyyy', 'YYYY']:
res += '%Y'
pattern = pattern[4:]
elif pattern[:2] in ['yy', 'YY']:
res += '%y'
pattern = pattern[2:]
elif pattern[:1] in ['y', 'Y']:
res += '%Y'
pattern = pattern[1:]

# Months

elif pattern[:4] == 'MMMM':
res += '%B'
pattern = pattern[4:]
elif pattern[:2] == 'MM':
res += '%m'
pattern = pattern[2:]
elif pattern[:1] == 'M':
# Hack for non-zero padded month
res += str(dt.month)
pattern = pattern[1:]

# Days
elif pattern[:4] == 'dddd':
res += '%A'
pattern = pattern[4:]
elif pattern[:2] == 'dd':
res += '%d'
pattern = pattern[2:]
elif pattern[:1] == 'd':
# Hack for non-zero padded month
res += str(dt.day)
pattern = pattern[1:]
else:
break
return res

if fmt[0] == '"' and fmt[-1] == '"':
fmt = fmt[1:-1]

fmt = re.sub(r'[dmMyY]+', repl, fmt)
try:
return dt.strftime(fmt)
except AttributeError:
return str(dt)

@classmethod
def eval_star(cls, data):
# This is similiar to what Word is doing. It uses
# default formats based on locale. This should be
# consistent behaviour.
if isinstance(data, (datetime, )):
# i.e. 08/16/1988 13:42
return data.strftime('%x %X')
elif isinstance(data, (date, )):
# i.e. 08/16/1988
return data.strftime('%x')
elif isinstance(data, (time, )):
# i.e. 13:18:00
return data.strftime('%X')
elif data is None:
return ''
else:
return str(data)

@classmethod
def eval(cls, data, code):
params = shlex.split(code, posix=False)
params = params[2:]
evaluated = False
for i, param in enumerate(params):
if param == '\\@':
data = cls.eval_strftime(data, params[i + 1])
evaluated = True
elif param == '\\*':
data = cls.eval_star(data)
evaluated = True

# According to Word lack of "\* MERGEFORMAT" in MERGEFIELD is perfectly fine
# so we treat lack of evaluation as \* code.
if not evaluated:
data = cls.eval_star(data)
return data

def __merge_field(self, part, field, text):
for mf in part.findall('.//MergeField[@name="%s"]' % field):
children = list(mf)
# Original code is saved in "data" attribute
instr = mf.attrib['data']
mf.clear() # clear away the attributes
mf.tag = '{%(w)s}r' % NAMESPACES
mf.extend(children)

nodes = []
# preserve new lines in replacement text
text = text or '' # text might be None
text = self.eval(text, instr)
text_parts = text.replace('\r', '').split('\n')
for i, text_part in enumerate(text_parts):
text_node = Element('{%(w)s}t' % NAMESPACES)
Expand Down
48 changes: 48 additions & 0 deletions tests/test_eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from unittest import TestCase
from datetime import datetime
from mailmerge import MailMerge


class TestEval(TestCase):

def setUp(self):
self.today = datetime(2018, 7, 1, 10, 12, 54, 0)

def test_year(self):
self.assertEqual(MailMerge.eval_strftime(self.today, 'y'), '2018')
self.assertEqual(MailMerge.eval_strftime(self.today, 'Y'), '2018')
self.assertEqual(MailMerge.eval_strftime(self.today, 'yy'), '18')
self.assertEqual(MailMerge.eval_strftime(self.today, 'YY'), '18')
self.assertEqual(MailMerge.eval_strftime(self.today, 'yyyy'), '2018')
self.assertEqual(MailMerge.eval_strftime(self.today, 'YYYY'), '2018')
self.assertEqual(MailMerge.eval_strftime(self.today, 'yyyyy'), '20182018')
self.assertEqual(MailMerge.eval_strftime(self.today, 'YYYYY'), '20182018')

def test_month(self):
self.assertEqual(MailMerge.eval_strftime(self.today, 'M'), '7')
self.assertEqual(MailMerge.eval_strftime(self.today, 'MM'), '07')
self.assertEqual(MailMerge.eval_strftime(self.today, 'MMMM'), self.today.strftime('%B'))

def test_days(self):
self.assertEqual(MailMerge.eval_strftime(self.today, 'd'), '1')
self.assertEqual(MailMerge.eval_strftime(self.today, 'dd'), '01')
self.assertEqual(MailMerge.eval_strftime(self.today, 'dddd'), self.today.strftime('%A'))

def test_strftime_none(self):
self.assertEqual(MailMerge.eval_strftime(None, 'd'), '')

def test_strftime_str(self):
self.assertEqual(MailMerge.eval_strftime('foo', 'd'), 'foo')

def test_eval(self):
self.assertEqual(MailMerge.eval_star(self.today), self.today.strftime('%x %X'))
self.assertEqual(MailMerge.eval_star(self.today.date()), self.today.strftime('%x'))
self.assertEqual(MailMerge.eval_star(self.today.time()), self.today.strftime('%X'))

def test_eval_none(self):
self.assertEqual(MailMerge.eval(None, 'MAILMERGE Foo \\@ "y-MM-dd" MERGEFORMAT'), '')

def test_parse_code(self):
self.assertEqual(MailMerge.eval(self.today, 'MAILMERGE Foo \\* MERGEFORMAT'), self.today.strftime('%x %X'))
self.assertEqual(MailMerge.eval(self.today, 'MAILMERGE Foo'), self.today.strftime('%x %X'))
self.assertEqual(MailMerge.eval(self.today, 'MAILMERGE Foo \\@ "y-MM-dd" MERGEFORMAT'), '2018-07-01')