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