midrange.com code scratchpad
Name:
cpytoxlsf.py
Scriptlanguage:
Python
Tabwidth:
4
Date:
05/25/2012 05:45:40 pm
IP:
Logged
Description:
iSeries Python program to convert any arbitrary DDS physical file to Excel binary file. Requires third-party xlwt package.
Code:
  1. '''Copy data from a physical file to an Excel binary file in the IFS.
  2.  
  3. Written by John Yeung.  Last modified 2012-05-21.
  4.  
  5. Usage (from CL):
  6.     python233/python '/util/cpytoxlsf.py' parm(&pf &xls [&A1text &A2text ...])
  7.  
  8. (The above assumes this program is located in '/util', and that iSeries
  9. Python 2.3.3 is installed.  If you are at V5R3 or later, you should be using
  10. iSeries Python 2.7 instead.)
  11.  
  12. Some features/caveats:
  13.  
  14. -  Column headings come from the COLHDG values in the DDS.  Multiple
  15.     values for a single field are joined by spaces, not newlines.  For
  16.     any fields without a COLHDG, or with only blanks in the COLHDG (these
  17.     two situations are indistinguishable), the field name is used as the
  18.     heading (the TEXT keyword is not checked).  To specify a blank column
  19.     heading rather than the field name, use COLHDG('*BLANK').
  20. -  Column headings wrap and are displayed in bold.
  21. -  Each column is sized approximately according to its longest data,
  22.     assuming that the default font is Arial 10, unless a width is
  23.     specified in the field text (using 'width=<number>').  [For this
  24.     purpose, the length of numeric data is assumed to always include
  25.     commas and fixed decimal places.]
  26. -  Each column may be formatted using an Excel format string in the
  27.     field text (using 'format="<string>"').
  28. -  Character fields with no format string are set to Excel text format.
  29. -  Columns with a supported EDTCDE value but no format string are
  30.     formatted according to the edit code.
  31. -  Columns may specify 'zero=blank' anywhere in the field text to leave
  32.     a cell empty when its value is zero.  (This is different than using
  33.     a format string or edit code to hide zero values.  See the ISBLANK
  34.     and ISNUMBER functions in Excel.)
  35. -  Columns may be skipped entirely by specifying COLHDG('*SKIP')
  36. -  Numeric fields that are 8 digits long with no decimal places are
  37.     automatically converted to dates if they have a suitable edit word.
  38. -  Numeric fields that are 6 digits long with no decimal places are
  39.     automatically converted to times if they have a suitable edit word.
  40. -  Free-form data may be inserted at the top using additional parameters,
  41.     one parameter for each row.  The data will be in bold.  Any number of
  42.     parameters may be specified, up to the limits of the operating system.
  43.  
  44. The motivation for this program is to provide a tool for easy generation
  45. of formatted spreadsheets.
  46.  
  47. Nice-to-have features not yet implemented include general edit word
  48. support (not just for date detection), more available edit codes, more
  49. comprehensive date support, the ability to choose fonts, and automatic
  50. population of multiple sheets given multiple file members.
  51.  
  52. Also, it would be nice to wrap this in a command for even greater ease of
  53. use, including meaningful promptability.
  54. '''
  55.  
  56. import sys
  57. import re
  58. from os import system
  59. from datetime import date, time
  60.  
  61. # Third-party package available at <http://pypi.python.org/pypi/xlwt>
  62. import xlwt
  63. # If running iSeries Python 2.3.3, xlwt needs to be modified to work with
  64. # EBCDIC.  This is not required for iSeries Python 2.5 or 2.7.
  65. # Table of empirically determined character widths in Arial 10, the default
  66. # font used by xlwt.  Note that font rendering is somewhat dependent on the
  67. # configuration of the PC that is used to open the resulting file, so these
  68. # widths are not necessarily exact for other people's PCs.  Also note that
  69. # only characters which are different in width than '0' are needed here.
  70. charwidths = {
  71.     '0': 262.637,
  72.     'f': 146.015,
  73.     'i': 117.096,
  74.     'j': 88.178,
  75.     'k': 233.244,
  76.     'l': 88.178,
  77.     'm': 379.259,
  78.     'r': 175.407,
  79.     's': 233.244,
  80.     't': 117.096,
  81.     'v': 203.852,
  82.     'w': 321.422,
  83.     'x': 203.852,
  84.     'z': 233.244,
  85.     'A': 321.422,
  86.     'B': 321.422,
  87.     'C': 350.341,
  88.     'D': 350.341,
  89.     'E': 321.422,
  90.     'F': 291.556,
  91.     'G': 350.341,
  92.     'H': 321.422,
  93.     'I': 146.015,
  94.     'K': 321.422,
  95.     'M': 379.259,
  96.     'N': 321.422,
  97.     'O': 350.341,
  98.     'P': 321.422,
  99.     'Q': 350.341,
  100.     'R': 321.422,
  101.     'S': 321.422,
  102.     'U': 321.422,
  103.     'V': 321.422,
  104.     'W': 496.356,
  105.     'X': 321.422,
  106.     'Y': 321.422,
  107.     ' ': 146.015,
  108.     '!': 146.015,
  109.     '"': 175.407,
  110.     '%': 438.044,
  111.     '&': 321.422,
  112.     '\'': 88.178,
  113.     '(': 175.407,
  114.     ')': 175.407,
  115.     '*': 203.852,
  116.     '+': 291.556,
  117.     ',': 146.015,
  118.     '-': 175.407,
  119.     '.': 146.015,
  120.     '/': 146.015,
  121.     ':': 146.015,
  122.     ';': 146.015,
  123.     '<': 291.556,
  124.     '=': 291.556,
  125.     '>': 291.556,
  126.     '@': 496.356,
  127.     '[': 146.015,
  128.     '\\': 146.015,
  129.     ']': 146.015,
  130.     '^': 203.852,
  131.     '`': 175.407,
  132.     '{': 175.407,
  133.     '|': 146.015,
  134.     '}': 175.407,
  135.     '~': 291.556}
  136.  
  137. ezxf = xlwt.easyxf
  138.  
  139. # I have a custom SNDMSG wrapper that I use to receive immediate messages
  140. # from iSeries Python, but for basic use, simply printing the message works.
  141. # iSeries Python also comes with os400.sndmsg, but I have not been able to
  142. # get that to work.
  143. def sndmsg(msg):
  144.     print msg
  145.  
  146. def _integer_digits(n):
  147.     '''Return the number of digits in a positive integer'''
  148.     if n == 0:
  149.         return 1
  150.     digits = 0
  151.     while n:
  152.         digits += 1
  153.         n //= 10
  154.     return digits
  155.  
  156. def number_analysis(n, dp=0):
  157.     '''Return a 4-tuple of (digits, thousands, points, signs)'''
  158.     digits, thousands, points, signs = 0, 0, 0, 0
  159.     if n < 0:
  160.         signs = 1
  161.         n = -n
  162.     if dp > 0:
  163.         points = 1
  164.     if isinstance(n, float):
  165.         idigits = _integer_digits(int(n) + 1)
  166.     elif isinstance(n, (int, long)):
  167.         idigits = _integer_digits(n)
  168.     else:
  169.         return None
  170.     digits = idigits + dp
  171.     thousands = (idigits - 1) // 3
  172.     return digits, thousands, points, signs
  173.  
  174. def colwidth(n):
  175.     '''Translate human-readable units to BIFF column width units'''
  176.     if n <= 0:
  177.         return 0
  178.     if n <= 1:
  179.         return n * 456
  180.     return 200 + n * 256
  181.  
  182. def fitwidth(data, bold=False):
  183.     '''Try to autofit Arial 10'''
  184.     units = 220
  185.     for char in str(data):
  186.         if char in charwidths:
  187.             units += charwidths[char]
  188.         else:
  189.             units += charwidths['0']
  190.     if bold:
  191.         units *= 1.1
  192.     return max(units, 700) # Don't go smaller than a reported width of 2
  193. def numwidth(data, dp, use_commas=False):
  194.     '''Try to autofit a number in Arial 10'''
  195.     units = 220
  196.     digits, commas, points, signs = number_analysis(data, dp)
  197.     units += digits * charwidths['0']
  198.     if use_commas:
  199.         units += commas * charwidths[',']
  200.     units += points * charwidths['.']
  201.     units += signs * charwidths['-']
  202.     return max(units, 700) # Don't go smaller than a reported width of 2
  203. def datewidth():
  204.     return 220 + 8 * charwidths['0'] + 2 * charwidths['/']
  205.  
  206. def timewidth():
  207.     digits_width = 6 * charwidths['0']
  208.     separators_width = 2 * charwidths[':']
  209.     space_width = charwidths[' ']
  210.     am_pm_width = max(charwidths['A'], charwidths['P']) + charwidths['M']
  211.     return 220 + digits_width + separators_width + space_width + am_pm_width
  212.  
  213. def default_numformat(dp=0, use_commas=False):
  214.     '''Generate a style object for Excel fixed number format'''
  215.     integers, decimals = '0'''
  216.     if use_commas:
  217.         integers = '#,##0'
  218.     if dp > 0:
  219.         decimals = '.' + '0' * dp
  220.     combined = integers + decimals
  221.     return ezxf(num_format_str=combined)
  222.  
  223. def editcode(code, dp=0):
  224.     '''Generate a style object corresponding to an edit code'''
  225.     code = code.lower()
  226.     if len(code) != 1 or code not in ('1234nopq'):
  227.         return default_numformat(dp)
  228.     sign, integers, decimals, zero = '''#'''''
  229.     if code in 'nopq':
  230.         sign = '-'
  231.     if code in '12no':
  232.         integers = '#,###'
  233.     if dp > 0:
  234.         decimals = '.' + '0' * dp
  235.     positive = integers + decimals
  236.     negative = sign + positive
  237.     if code in '13np':
  238.         zero = positive[:-1] + '0'
  239.     return ezxf(num_format_str=';'.join((positive, negative, zero)))
  240.  
  241. def is_numeric_date(size, editword):
  242.     return size == (8, 0) and editword in ("'    -  -  '", "'    /  /  '")
  243.  
  244. def is_numeric_time(size, editword):
  245.     return size == (6, 0) and editword in ("'  .  .  '", "'  :  :  '")
  246.  
  247. # Check parameters
  248. parameters = len(sys.argv) - 1
  249. if parameters < 2:
  250.     sndmsg('Program needs at least 2 parameters; received %d.' % parameters)
  251.     sys.exit(2)
  252. pf = sys.argv[1].split('/')
  253. if len(pf) == 1:
  254.     libname = '*LIBL'
  255.     filename = pf[0].upper()
  256. elif len(pf) == 2:
  257.     libname = pf[0].upper()
  258.     filename = pf[1].upper()
  259. else:
  260.     sndmsg('Could not parse file name.')
  261.     sys.exit(2)
  262. sndmsg('Parameters checked.')
  263.  
  264. infile = File400(filename, 'r', lib=libname)
  265. if libname.startswith('*'):
  266.     libname = infile.libName()
  267. sndmsg('Opened ' + libname + '/' + filename + ' for reading.')
  268.  
  269. # Get column headings and formatting information from the DDS
  270. fieldlist = []
  271. headings = {}
  272. numformats = {}
  273. dateflags = {}
  274. timeflags = {}
  275. commaflags = {}
  276. decplaces = {}
  277. colwidths = {}
  278. blankzeros = {}
  279. template = "dspffd %s/%s output(*outfile) outfile(qtemp/dspffdpf)"
  280. system(template % (libname, filename))
  281. ddsfile = File400('DSPFFDPF''r', lib='QTEMP')
  282.  
  283. ddsfile.posf()
  284. while not ddsfile.readn():
  285.     fieldname = ddsfile['WHFLDE']
  286.     fieldtext = ddsfile['WHFTXT']
  287.  
  288.     # Set heading
  289.     headertuple = (ddsfile['WHCHD1'], ddsfile['WHCHD2'], ddsfile['WHCHD3'])
  290.     text = ' '.join(headertuple).strip()
  291.     if not text:
  292.         text = fieldname
  293.     elif text.upper() in ('*BLANK''*BLANKS'):
  294.         text = ''
  295.     elif text.upper() == '*SKIP':
  296.         continue
  297.     fieldlist.append(fieldname)
  298.     headings[fieldname] = text
  299.  
  300.     # Get field size and type
  301.     if ddsfile['WHFLDD']:
  302.         fieldsize = (ddsfile['WHFLDD'], ddsfile['WHFLDP'])
  303.         decplaces[fieldname] = fieldsize[1]
  304.         numeric = True
  305.     else:
  306.         fieldsize = ddsfile['WHFLDB']
  307.         numeric = False
  308.  
  309.     # Look for number format string
  310.     match = re.search(r'format="(.*)"', fieldtext, re.IGNORECASE)
  311.     if match:
  312.         numformat = ezxf(num_format_str=match.group(1))
  313.     elif numeric:
  314.         numformat = editcode(ddsfile['WHECDE'], ddsfile['WHFLDP'])
  315.     else:
  316.         numformat = None
  317.     if numformat:
  318.         numformats[fieldname] = numformat
  319.         commaflags[fieldname] = ',' in numformat.num_format_str
  320.  
  321.     # Check whether it looks like a numeric date or time
  322.     dateflags[fieldname] = is_numeric_date(fieldsize, ddsfile['WHEWRD'])
  323.     timeflags[fieldname] = is_numeric_time(fieldsize, ddsfile['WHEWRD'])
  324.  
  325.     # Look for fixed column width
  326.     match = re.search(r'width=([1-9][0-9]*)', fieldtext, re.IGNORECASE)
  327.     if match:
  328.         colwidths[fieldname] = colwidth(int(match.group(1)))
  329.  
  330.     # Look for zero-suppression flag
  331.     match = re.search(r'zero(s|es)?=blanks?', fieldtext, re.IGNORECASE)
  332.     if match:
  333.         blankzeros[fieldname] = True
  334.  
  335. ddsfile.close()
  336.  
  337. # Create a workbook with one sheet
  338. wb = xlwt.Workbook()
  339. ws = wb.add_sheet(infile.fileName())
  340. row = 0
  341.  
  342. title_style = ezxf('font: bold on')
  343. header_style = ezxf('font: bold on; align: wrap on')
  344. date_style = ezxf(num_format_str='m/d/yyyy')
  345. time_style = ezxf(num_format_str='h:mm:ss AM/PM')
  346. text_style = ezxf(num_format_str='@')
  347.  
  348. # Populate first few rows using additional parameters, if provided.
  349. # Typically, these rows would be used for report ID, date, and title.
  350. for arg in sys.argv[3:]:
  351.     ws.write(row, 0, arg, title_style)
  352.     row += 1
  353.  
  354. # Keep track of the widest data in each column
  355. maxwidths = [0] * len(fieldlist)
  356.  
  357. # If there were top-row parameters, skip a row before starting the
  358. # column headings.
  359. if parameters > 2:
  360.     row += 1
  361.  
  362. # Create a row for column headings
  363. for col, name in enumerate(fieldlist):
  364.     desc = headings[name]
  365.     ws.write(row, col, desc, header_style)
  366.     if name not in colwidths:
  367.         maxwidths[col] = fitwidth(desc, bold=True)
  368.  
  369. infile.posf()
  370. while not infile.readn():
  371.     row += 1
  372.     for col, data in enumerate(infile.get(fieldlist)):
  373.         fieldname = fieldlist[col]
  374.         nativedate = False
  375.         nativetime = False
  376.         if infile.fieldType(fieldname) == 'DATE':
  377.             year, month, day = [int(x) for x in data.split('-')]
  378.             if year > 1904:
  379.                 ws.write(row, col, date(year, month, day), date_style)
  380.             nativedate = True
  381.         elif infile.fieldType(fieldname) == 'TIME':
  382.             hour, minute, second = [int(x) for x in data.split('.')]
  383.             ws.write(row, col, time(hour, minute, second), time_style)
  384.             nativetime = True
  385.         elif dateflags[fieldname]:
  386.             if data:
  387.                 year, md = divmod(data, 10000)
  388.                 month, day = divmod(md, 100)
  389.                 ws.write(row, col, date(year, month, day), date_style)
  390.         elif timeflags[fieldname]:
  391.             if data:
  392.                 hour, minsec = divmod(data, 10000)
  393.                 minute, second = divmod(minsec, 100)
  394.                 ws.write(row, col, time(hour, minute, second), time_style)
  395.         elif data == 0 and fieldname in blankzeros:
  396.             pass
  397.         elif fieldname in numformats:
  398.             ws.write(row, col, data, numformats[fieldname])
  399.         elif infile.fieldType(fieldname) == 'CHAR':
  400.             ws.write(row, col, data, text_style)
  401.         else:
  402.             ws.write(row, col, data)
  403.         if fieldname not in colwidths:
  404.             if nativedate or dateflags[fieldname]:
  405.                 maxwidths[col] = datewidth()
  406.             elif nativetime or timeflags[fieldname]:
  407.                 maxwidths[col] = timewidth()
  408.             if fieldname in decplaces:
  409.                 dp = decplaces[fieldname]
  410.                 cf = commaflags[fieldname]
  411.                 maxwidths[col] = max(maxwidths[col], numwidth(data, dp, cf))
  412.             else:
  413.                 maxwidths[col] = max(maxwidths[col], fitwidth(data))
  414. infile.close()
  415.  
  416. # Set column widths
  417. for col in range(len(fieldlist)):
  418.     if fieldlist[col] in colwidths:
  419.         ws.col(col).width = colwidths[fieldlist[col]]
  420.     else:
  421.         ws.col(col).width = maxwidths[col]
  422.  
  423. wb.save(sys.argv[2])
  424. sndmsg('File copied to ' + sys.argv[2] + '.')
  425.  
© 2004-2019 by midrange.com generated in 0.097s valid xhtml & css