SCI.XLSX.RECALL
Since Office 2010, the internal format used for Office documents has switched to a zipped collection of XML files. Since these are "just" text files, it's reasonable to attempt to produce them directly from a PICK Basic program, and this is certainly feasible. Trying to reverse-engineer the xlsx XML schema can be challenging - Excel throws everything and the kitchen sink into the zip file, and small errors lead to Excel rejecting the entire file. Polymath Programmer has some excellent resources to explain what's going on at just the right level of detail. Well worth buying the book, and the free code if you're using DotNet.
The code below is an example of creating the XML files and zipping them to produce a file that can be opened natively in Excel. If the wiki formatting causes any problems, the latest version can be retrieved from: https://github.com/ianmcgowan/SCI.BP/blob/master/SCI.XLSX.RECALL
***************************************************************************
* Program: SCI.XLS.RECALL
* Author : MCGOWJ01
* Date : 2017-02-10
* Edited :
* Comment: Convert a recall into an excel sheet
***************************************************************************
* LOG DATE BY CHANGE
* ---------- ------------ -------------------------------------------------
* SEE: https://blogs.msdn.microsoft.com/brian_jones/2006/11/02/simple-spreadsheetml-file-part-1-of-3/
* http://polymathprogrammer.com/2010/01/11/custom-column-widths-in-excel-open-xml/
*
DEBUG=0
S=@SENTENCE
FILE.NAME=FIELD(S,' ',2)
IF INDEX(FILE.NAME,'/',1) = 0 THEN STOP FILE.NAME:' is not a directory'
OPENSEQ FILE.NAME TO INPUT.F ELSE STOP 201,FILE.NAME
OUTPUT.DIR=FILE.NAME:'.TMP'
EXECUTE '!rm -rf ':OUTPUT.DIR ;* Maybe check the filename isn't ./.. before this?
EXECUTE '!mkdir ':OUTPUT.DIR
EXECUTE '!mkdir ':OUTPUT.DIR:'/_rels'
EXECUTE '!mkdir ':OUTPUT.DIR:'/xl'
EXECUTE '!mkdir ':OUTPUT.DIR:'/xl/_rels'
EXECUTE '!mkdir ':OUTPUT.DIR:'/xl/worksheets'
* This next command unzips an empty excel sheet for us to work with
*EXECUTE '!cd ':OUTPUT.DIR:' ; unzip /info/local/bin/template.xlsx' CAPTURING DUMMY
OPEN 'RECALLS' TO RECALLS ELSE STOP 201,'RECALLS'
GOSUB GET.FIELD.NAMES
IF DEBUG THEN
PRINT 'FIELD.MAP'
FOR F=1 TO DCOUNT(FIELD.MAP<1>,@VM)
PRINT F'R#2':' ':FIELD.MAP<6,F>'L#20':' ':FIELD.MAP<2,F>'L#35':' ':FIELD.MAP<3,F>
NEXT F
END
CONTROL.CHARS=''
FOR CHARACTER = 127 TO 249
CONTROL.CHARS:=CHAR(CHARACTER)
NEXT CHARACTER
*
GOSUB XLS.HEADER
ROW.COUNT=0
LOOP
READSEQ ROW FROM INPUT.F ELSE EXIT
ROW.COUNT+=1
GOSUB XLS.BODY
REPEAT
*GOSUB XLS.TOTALS
GOSUB XLS.FOOTER
CLOSE OUTPUT.F
*
EXCEL.NAME=FIELD(FILE.NAME,'/',DCOUNT(FILE.NAME,'/')):'.xlsx'
EXECUTE '!cd ':OUTPUT.DIR:' ; zip -r ':EXCEL.NAME:' *' CAPTURING DUMMY
RECIP='' ; FROM.USER='' ; CC.USER='' ;* Let the SUBR figure it out
MSG='Please find your report attached'
SUBJECT='REPORT:':EXCEL.NAME
ATTACH=OUTPUT.DIR:'/':EXCEL.NAME
OPTIONS=''
CALL SCI.MAIL.SUB(RECIP, FROM.USER, CC.USER, MSG, ATTACH, SUBJECT, OPTIONS)
STOP
*
XLS.BODY:
L='<x:row>' ; GOSUB WRITE.LINE
CONVERT '^' TO @VM IN ROW
FOR C=1 TO DCOUNT(ROW<1>,@VM)
CELL=ROW<1,C>
GOSUB URL.ENCODE.CELL
FORMAT=FIELD.MAP<3,C>
CELL.NUM=CELL ; CONVERT ',' TO '' IN CELL.NUM
BEGIN CASE
CASE FORMAT[1,1]='D' AND ICONV(CELL,'D') # '' ;* Date
CELL=ICONV(CELL,'D')+24837 ;* 12/31/1967-12/31/1899=24837
L='<x:c s="1" t="Date"><x:v>':CELL:'</x:v></x:c>'
CASE FORMAT[1,2]='MD' OR FORMAT[1,2]='MR' ;* Number
CONVERT ',' TO '' IN CELL
IF NUM(CELL) THEN
L='<x:c s="3"><x:v>':CELL:'</x:v></x:c>'
END ELSE
L='<x:c t="str"><x:v>':CELL:'</x:v></x:c>'
END
CASE 1 ;* String is default type
IF NUM(CELL.NUM) AND CELL.NUM#'' THEN
* It's a number, but leave is unstyled so zip 90210 doesn't become 90210.00
L='<x:c><x:v>':CELL:'</x:v></x:c>'
END ELSE
L='<x:c t="str"><x:v>':CELL:'</x:v></x:c>'
END
END CASE
GOSUB WRITE.LINE
NEXT C
L='</x:row>'
GOSUB WRITE.LINE
RETURN
*
XLS.TOTALS:
L='<Row>'
GOSUB WRITE.LINE
FOR C=1 TO DCOUNT(FIELD.MAP<1>,@VM)
FORMAT=FIELD.MAP<3,C>
IF FORMAT[1,2]='MD' OR FORMAT[1,2]='MR' THEN
* Number col, add total to bottom row
L=' <Cell ss:Index="':C:'" ss:Formula="=SUM(R[-':ROW.COUNT:']C:R[-1]C)"><Data ss:Type="Number">0</Data></Cell>'
GOSUB WRITE.LINE
END
NEXT C
L='</Row>'
GOSUB WRITE.LINE
RETURN
*
URL.ENCODE.CELL:
* Certain characters not allowed in XML - escape them
CONVERT CONTROL.CHARS TO '' IN CELL ;* Zap unicode/other code pages
SWAP '&' WITH '&' IN CELL
SWAP '<' WITH '<' IN CELL
SWAP '>' WITH '>' IN CELL
CELL=TRIM(CELL);* No benefit to leading or trailing spaces
RETURN
*
XLS.HEADER:
* A minimal XLSX sheet consists of 6 files
* [Content_Types].xml
* _rels/.rels
* xl/_rels/workbook.xml.rels
* xl/styles.xml
* xl/workbook.xml
* xl/worksheets/sheet.xml
*
OUTPUT.FILE.NAME=OUTPUT.DIR:'/[Content_Types].xml'
GOSUB OPEN.FILE
L='<?xml version="1.0" encoding="utf-8"?>'
GOSUB WRITE.LINE
L='<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
GOSUB WRITE.LINE
L=' <Default ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" Extension="xml"/>'
GOSUB WRITE.LINE
L=' <Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>'
GOSUB WRITE.LINE
L=' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet.xml"/>'
GOSUB WRITE.LINE
L=' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>'
GOSUB WRITE.LINE
L='</Types>'
GOSUB WRITE.LINE
*
OUTPUT.FILE.NAME=OUTPUT.DIR:'/_rels/.rels'
GOSUB OPEN.FILE
L='<?xml version="1.0" encoding="utf-8"?>'
GOSUB WRITE.LINE
L='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
GOSUB WRITE.LINE
L=' <Relationship Id="Rb5834f0a9fe74ac0" Target="/xl/workbook.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"/>'
GOSUB WRITE.LINE
L='</Relationships>'
GOSUB WRITE.LINE
*
OUTPUT.FILE.NAME=OUTPUT.DIR:'/xl/_rels/workbook.xml.rels'
GOSUB OPEN.FILE
L='<?xml version="1.0" encoding="utf-8"?>'
GOSUB WRITE.LINE
L='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
GOSUB WRITE.LINE
L=' <Relationship Id="R203d98ce4bbc4619" Target="/xl/worksheets/sheet.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>'
GOSUB WRITE.LINE
L=' <Relationship Id="R8dd86d2508e64fce" Target="/xl/styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>'
GOSUB WRITE.LINE
L='</Relationships>'
GOSUB WRITE.LINE
*
* This one controls styles and formats. It's, complicated.
OUTPUT.FILE.NAME=OUTPUT.DIR:'/xl/styles.xml'
GOSUB OPEN.FILE
L='<?xml version="1.0" encoding="utf-8"?>'
GOSUB WRITE.LINE
L='<x:styleSheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
GOSUB WRITE.LINE
* This section controls number and date formats, and is refered below by the styles. 164=0, 165=1, etc
L=' <x:numFmts count="5">'
GOSUB WRITE.LINE
L=' <x:numFmt formatCode="dd/mm/yyyy" numFmtId="164"/>'
GOSUB WRITE.LINE
L=' <x:numFmt formatCode="#,##0.0000" numFmtId="165"/>'
GOSUB WRITE.LINE
L=' <x:numFmt formatCode="#,##0.00" numFmtId="166"/>'
GOSUB WRITE.LINE
L=' <x:numFmt formatCode="@" numFmtId="167"/>'
GOSUB WRITE.LINE
L=' <x:numFmt formatCode="mm/dd/yyyy" numFmtId="168"/>'
GOSUB WRITE.LINE
L=' </x:numFmts>'
GOSUB WRITE.LINE
L=' <x:fonts count="2">'
GOSUB WRITE.LINE
L=' <x:font>'
GOSUB WRITE.LINE
L=' <x:sz val="11"/>'
GOSUB WRITE.LINE
L=' <x:name val="Calibri"/>'
GOSUB WRITE.LINE
L=' </x:font>'
GOSUB WRITE.LINE
L=' <x:font>'
GOSUB WRITE.LINE
L=' <x:sz val="11"/>'
GOSUB WRITE.LINE
L=' <x:color tint="-0.499984740745262"/>'
GOSUB WRITE.LINE
L=' <x:name val="Calibri"/>'
GOSUB WRITE.LINE
L=' </x:font>'
GOSUB WRITE.LINE
L=' </x:fonts>'
GOSUB WRITE.LINE
L=' <x:fills count="2">'
GOSUB WRITE.LINE
L=' <x:fill>'
GOSUB WRITE.LINE
L=' <x:patternFill patternType="none"/>'
GOSUB WRITE.LINE
L=' </x:fill>'
GOSUB WRITE.LINE
L=' <x:fill>'
GOSUB WRITE.LINE
L=' <x:patternFill patternType="gray125"/>'
GOSUB WRITE.LINE
L=' </x:fill>'
GOSUB WRITE.LINE
L=' <x:fill>'
GOSUB WRITE.LINE
L=' <x:patternFill patternType="solid">'
GOSUB WRITE.LINE
L=' <x:fgColor tint="0.79998168889431442"/>'
GOSUB WRITE.LINE
L=' <x:bgColor indexed="64"/>'
GOSUB WRITE.LINE
L=' </x:patternFill>'
GOSUB WRITE.LINE
L=' </x:fill>'
GOSUB WRITE.LINE
L=' </x:fills>'
GOSUB WRITE.LINE
L=' <x:borders count="1">'
GOSUB WRITE.LINE
L=' <x:border>'
GOSUB WRITE.LINE
L=' <x:left/>'
GOSUB WRITE.LINE
L=' <x:right/>'
GOSUB WRITE.LINE
L=' <x:top/>'
GOSUB WRITE.LINE
L=' <x:bottom/>'
GOSUB WRITE.LINE
L=' <x:diagonal/>'
GOSUB WRITE.LINE
L=' </x:border>'
GOSUB WRITE.LINE
L=' </x:borders>'
GOSUB WRITE.LINE
L=' <x:cellStyleXfs count="1">'
GOSUB WRITE.LINE
L=' <x:xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>'
GOSUB WRITE.LINE
L=' </x:cellStyleXfs>'
GOSUB WRITE.LINE
L=' <x:cellXfs count="5">'
GOSUB WRITE.LINE
* These styles are indexed by number from the cell references with <c s="1"> picking numFmtId="165"
L=' <x:xf borderId="0" fillId="0" fontId="0" numFmtId="0" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="1" borderId="0" fillId="0" fontId="0" numFmtId="14" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="1" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="1" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="1" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="1" borderId="0" fillId="0" fontId="0" numFmtId="168" xfId="0"/>'
GOSUB WRITE.LINE
L=' <x:xf applyNumberFormat="0" borderId="0" fillId="2" fontId="1" xfId="0"/>'
GOSUB WRITE.LINE
L=' </x:cellXfs>'
GOSUB WRITE.LINE
L=' <x:cellStyles count="1">'
GOSUB WRITE.LINE
L=' <x:cellStyle builtinId="0" name="Normal" xfId="0"/>'
GOSUB WRITE.LINE
L=' </x:cellStyles>'
GOSUB WRITE.LINE
L=' <x:dxfs count="0"/>'
GOSUB WRITE.LINE
L=' <x:tableStyles count="0" defaultPivotStyle="PivotStyleLight16" defaultTableStyle="TableStyleMedium9"/>'
GOSUB WRITE.LINE
L='</x:styleSheet>'
GOSUB WRITE.LINE
*
OUTPUT.FILE.NAME=OUTPUT.DIR:'/xl/workbook.xml'
GOSUB OPEN.FILE
L='<?xml version="1.0" encoding="utf-8"?>'
GOSUB WRITE.LINE
L='<x:workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
GOSUB WRITE.LINE
L=' <x:fileVersion appName="Microsoft Office Excel"/>'
GOSUB WRITE.LINE
L=' <x:sheets>'
GOSUB WRITE.LINE
L=' <x:sheet name="Sheet1" r:id="R203d98ce4bbc4619" sheetId="1"/>'
GOSUB WRITE.LINE
L=' </x:sheets>'
GOSUB WRITE.LINE
L='</x:workbook>'
GOSUB WRITE.LINE
*
OUTPUT.FILE.NAME=OUTPUT.DIR:'/xl/worksheets/sheet.xml'
GOSUB OPEN.FILE
L='<?xml version="1.0"?>'
GOSUB WRITE.LINE
L='<x:worksheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
GOSUB WRITE.LINE
L=' <x:sheetData>'
GOSUB WRITE.LINE
*
* Now heading values in row 1
*
* Column definitions
FOR C=1 TO DCOUNT(FIELD.MAP<1>,@VM)
FORMAT=FIELD.MAP<3,C>
MASK=FIELD.MAP<5,C>
MASK=MASK[1,LEN(MASK)-1]
IF NOT(NUM(MASK)) THEN MASK=10
BEGIN CASE
CASE FORMAT[1,1]='D' ;* Date
STYLE='s102'
CASE FORMAT[1,2]='MD' OR FORMAT[1,2]='MR' ;* Number
IF FORMAT[3,1]='0' THEN
STYLE='s103'
END ELSE
STYLE='s101'
END
CASE 1 ;* String is default type
STYLE='Default'
END CASE
L=' <Column ss:Index="':C:'" ss:StyleID="':STYLE:'" ss:Width="':MASK*10:'"/>'
*GOSUB WRITE.LINE
NEXT C
*
* Now write a header row
*
L='<x:row>'
GOSUB WRITE.LINE
FOR C=1 TO DCOUNT(FIELD.MAP<1>,@VM)
L=' <x:c s="6" t="str"><x:v>'
*CELL=TRIM(FIELD.MAP<4,C>)
*IF CELL='' THEN CELL=FIELD.MAP<6,C>
CELL=FIELD.MAP<6,C>
GOSUB URL.ENCODE.CELL
L:=CELL
L:='</x:v></x:c>'
GOSUB WRITE.LINE
NEXT C
L='</x:row>'
GOSUB WRITE.LINE
RETURN
*
XLS.FOOTER:
L=' </x:sheetData>'
GOSUB WRITE.LINE
L='</x:worksheet>'
GOSUB WRITE.LINE
RETURN
*
OPEN.FILE:
EXECUTE '!rm -f ':OUTPUT.FILE.NAME
EXECUTE '!touch ':OUTPUT.FILE.NAME
OPENSEQ OUTPUT.FILE.NAME TO OUTPUT.F ELSE STOP 'CANNOT OPEN ':OUTPUT.FILE.NAME
RETURN
*
WRITE.LINE:
*IF DEBUG THEN PRINT L
WRITESEQ L:CHAR(13) APPEND ON OUTPUT.F ELSE STOP 'ERROR WRITING ':FILE.NAME
RETURN
*
GET.FIELD.NAMES:
FIELD.MAP=''
RECALL.NAME=FIELD(FILE.NAME,'/',DCOUNT(FILE.NAME,'/'))
READV R FROM RECALLS, RECALL.NAME, 1 ELSE STOP 'CANNOT READ RECALLS:':RECALL.NAME
DATA.FILE=''
FOR F=1 TO DCOUNT(R,@VM)
L=R<1,F>
IF FIELD(L,' ',1) = 'list' OR FIELD(L,' ',1) = 'sort' THEN
IF DATA.FILE # '' THEN STOP 'CAN ONLY PROCESS ONE list OR sort PER RECALL'
DATA.FILE=FIELD(L,' ',2)
OPEN 'DICT',DATA.FILE TO DICT ELSE STOP 'CANNOT OPEN DICT ':DATA.FILE
END
IF DATA.FILE # '' THEN
FOR WC=1 TO DCOUNT(L,' ')
* list LS.MASTER FIELD1 FIELD2 ETC.. Skip the first two
ATB=FIELD(L,' ',WC)
IF ATB='list' OR ATB='sort' THEN CONTINUE
IF ATB=DATA.FILE THEN CONTINUE
IF ATB='TO' THEN
* Stop when we get to the TO DELIM etc...
RETURN
END
READ DICT.REC FROM DICT, ATB THEN
TYPE=DICT.REC<1>
CORR=DICT.REC<2>
CONV=DICT.REC<3>
HEAD=DICT.REC<4>
MASK=DICT.REC<5>
N=DCOUNT(FIELD.MAP<1>,@VM)+1 ;* Always nervous of NULL with <1,-1>
FIELD.MAP<1,N>=TYPE
FIELD.MAP<2,N>=CORR
FIELD.MAP<3,N>=CONV
FIELD.MAP<4,N>=HEAD
FIELD.MAP<5,N>=MASK
FIELD.MAP<6,N>=ATB
END ELSE
IF ATB # 'BY' AND ATB # 'WITH' THEN
PRINT 'ATB ':ATB:' NOT FOUND IN DICT ':DATA.FILE
END
END
NEXT WC
END
NEXT F
RETURN