├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .ocp-indent ├── CHANGES.md ├── LICENSE.md ├── Makefile ├── README.md ├── dune-project ├── easy_xlsx.descr ├── easy_xlsx.opam ├── easy_xlsx ├── src │ ├── dune │ ├── easy_xlsx.ml │ └── easy_xlsx.mli └── test │ ├── dune │ ├── files │ ├── autofilter_import_xml_12.xlsx │ ├── autofilter_import_xml_12_Advanced1.csv │ ├── autofilter_import_xml_12_Advanced2.csv │ ├── autofilter_import_xml_12_Custom.csv │ ├── autofilter_import_xml_12_Discrete.csv │ ├── autofilter_import_xml_12_Top10.csv │ ├── chart_3dsettings_import_xml_12.xlsx │ ├── chart_3dsettings_import_xml_12_Elevation.csv │ ├── chart_3dsettings_import_xml_12_Perspective.csv │ ├── chart_3dsettings_import_xml_12_Rotation.csv │ ├── chart_3dsettings_import_xml_12_Settings.csv │ ├── chart_3dsettings_import_xml_12_SourceData.csv │ ├── chart_axis_import_xml_12.xlsx │ ├── chart_axis_import_xml_12_Axis Labels.csv │ ├── chart_axis_import_xml_12_Axis Line.csv │ ├── chart_axis_import_xml_12_Gridlines.csv │ ├── chart_axis_import_xml_12_SourceData.csv │ ├── chart_axis_import_xml_12_Tick Marks.csv │ ├── chart_bitmaps_import_xml_12.xlsx │ ├── chart_bitmaps_import_xml_12_General.csv │ ├── chart_bitmaps_import_xml_12_Series 2D.csv │ ├── chart_bitmaps_import_xml_12_Series 3D.csv │ ├── chart_bitmaps_import_xml_12_SourceData.csv │ ├── convert.py │ ├── formats.xlsx │ ├── hyperlink_import_xml_12.xlsx │ ├── hyperlink_import_xml_12_Sheet'!.csv │ ├── hyperlink_import_xml_12_Sheet1.csv │ ├── hyperlink_import_xml_12_Sheet2.csv │ ├── oleobject_import_xml_12.xlsx │ ├── oleobject_import_xml_12_OLE.csv │ ├── oleobject_import_xml_12_SourceData.csv │ ├── scenarios_import_xml_12.xlsx │ ├── scenarios_import_xml_12_Name.csv │ ├── scenarios_import_xml_12_Ranges.csv │ ├── scenarios_import_xml_12_RefCheck.csv │ ├── scenarios_import_xml_12_Settings.csv │ ├── sheetprotection_import_xml_12.xlsx │ ├── sheetprotection_import_xml_12_Sheet1.csv │ ├── sheetprotection_import_xml_12_Sheet2.csv │ ├── sheetprotection_import_xml_12_Sheet3.csv │ └── sheetprotection_import_xml_12_Sheet4.csv │ ├── test_easy_xlsx_docs.ml │ └── test_easy_xlsx_formats.ml ├── open_packaging.descr ├── open_packaging.opam ├── open_packaging └── src │ ├── dune │ ├── open_packaging.ml │ ├── relationship.ml │ ├── relationship.mli │ ├── relationships.ml │ ├── relationships.mli │ └── utils.ml ├── pkg └── pkg.ml ├── spreadsheetml.descr ├── spreadsheetml.opam └── spreadsheetml └── src ├── dune ├── shared_string_table.ml ├── shared_string_table.mli ├── spreadsheetml.ml ├── styles.ml ├── styles.mli ├── workbook.ml ├── workbook.mli └── worksheet.ml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: ocaml/opam2:alpine-3.8-ocaml-4.04 6 | environment: 7 | TERM: xterm 8 | steps: 9 | - run: 10 | name: Update opam 11 | command: | 12 | opam remote remove --all default 13 | opam remote add default https://opam.ocaml.org 14 | - checkout 15 | - run: 16 | name: Pin packages 17 | command: | 18 | opam pin add -y -n easy_xlsx . 19 | opam pin add -y -n open_packaging . 20 | opam pin add -y -n spreadsheetml . 21 | - run: 22 | name: Install system dependencies 23 | command: opam depext -y easy_xlsx open_packaging spreadsheetml 24 | - run: 25 | name: Install OCaml dependencies 26 | command: opam install --deps-only -y easy_xlsx open_packaging spreadsheetml 27 | - run: 28 | name: Build 29 | command: opam config exec -- make 30 | - run: 31 | name: Install OCaml test dependencies 32 | # Not using opam install -y -t because it tries to run csv's tests 33 | command: opam install -y csv oUnit 34 | - run: 35 | name: Test 36 | command: opam config exec -- make test 37 | - run: 38 | name: Coverage 39 | command: opam config exec -- make coverage 40 | - run: 41 | name: Install ocveralls 42 | command: opam install -y ocveralls 43 | - run: 44 | name: Upload coverage report 45 | command: | 46 | cd _build/default 47 | shopt -s globstar 48 | opam config exec -- ocveralls **/bisect*.out --send --repo_token $COVERALLS_REPO_TOKEN --git 49 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 80 12 | 13 | # Makefiles only support tab indents 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | _build 3 | _coverage 4 | *.install 5 | bisect*.out 6 | .merlin 7 | -------------------------------------------------------------------------------- /.ocp-indent: -------------------------------------------------------------------------------- 1 | JaneStreet 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 1.0 (2018-05-31) 2 | 3 | Initial release 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @dune build @install 3 | 4 | clean: 5 | @dune clean 6 | 7 | coverage: clean 8 | @BISECT_ENABLE=YES dune runtest --force 9 | @bisect-ppx-report -I _build/default/ -html _coverage/ \ 10 | `find . -name 'bisect*.out'` 11 | 12 | test: 13 | @dune runtest --force 14 | 15 | # until we have https://github.com/ocaml/opam-publish/issues/38 16 | REPO=../opam-repository 17 | PACKAGES=$(REPO)/packages 18 | 19 | pkg-%: 20 | topkg opam pkg -n $* 21 | mkdir -p $(PACKAGES)/$* 22 | cp -r _build/$*.* $(PACKAGES)/$*/ 23 | rm -f $(PACKAGES)/$*/$*.opam 24 | cd $(PACKAGES) && git add $* 25 | 26 | PKGS=$(basename $(wildcard *.opam)) 27 | opam-pkg: 28 | $(MAKE) $(PKGS:%=pkg-%) 29 | 30 | .PHONY: all build clean coverage opam-pkg test 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/brendanlong/ocaml-ooxml.svg?style=shield)](https://circleci.com/gh/brendanlong/ocaml-ooxml) 2 | [![Coverage Status](https://coveralls.io/repos/github/brendanlong/ocaml-ooxml/badge.svg?branch=master)](https://coveralls.io/github/brendanlong/ocaml-ooxml?branch=master) 3 | 4 | The repo contains three libraries for reading data from Microsoft's document 5 | formats ("Office Open XML"). 6 | 7 | - `open_packaging` parses Office Open XML's "Open Packaging Conventions" 8 | (the container format for all Microsoft Office documents) 9 | - `spreadsheetml` parses the XML data in SpreadsheetML (i.e. Excel's XLSX 10 | format) 11 | - `easy_xlsx` reads XLSX documents, applies the formatting in the document, 12 | and returns the result as a list of sheets (with a sheet name and then 13 | a `string list list` of data). The goal of this library is to give the 14 | same results as if you exported the XLSX file to CSV and then read it with 15 | a standard CSV reader. 16 | 17 | `open_packaging` and `spreadsheetml` are relatively safe to use but incomplete 18 | (it should be obvious what they're mising -- if a field doesn't exist, I 19 | haven't got to it yet). Everything that does exist should be parsed properly. 20 | 21 | `easy_xlsx` is in very early stages. It should properly give read XLSX files 22 | and output correct types, but the SpreadsheetML spec doesn't list all of the 23 | built-in format strings, so some types may not be handled correctly. At the 24 | moment, `easy_xlsx` will bail out in any case where it can't understand the 25 | formatting, although I'd be open to patches to make this optional. 26 | 27 | [API documentation](https://brendanlong.github.io/ocaml-ooxml/doc/) 28 | 29 | ## Building 30 | 31 | Install dependencies: 32 | 33 | ``` 34 | opam pin add -n easy_xlsx . 35 | opam depext easy_xlsx 36 | opam install --deps-only easy_xlsx 37 | ``` 38 | 39 | Then build: 40 | 41 | ``` 42 | make 43 | ``` 44 | 45 | You can run the tests if you want: 46 | 47 | ``` 48 | make test 49 | ``` 50 | 51 | The `Makefile` is just a thin wrapper around jbuilder, so you can use 52 | jbuilder commands too if you prefer. 53 | 54 | ## Helping 55 | 56 | If you want to help with this, create an issue with what you'd like to work 57 | on, and mention it if you need me to help with anything (point you at the 58 | relevant spec, give my opinion on approaches, etc.). 59 | 60 | Some things I could use help with: 61 | 62 | - Implement more of the specs. See [ECMA 376](https://www.ecma-international.org/publications/standards/Ecma-376.htm). 63 | The editions only contain changes, so most of what's interesting is in 64 | the 1st edition. Part 2 is the most interesting for the Open Packaging 65 | Conventions and Part 4 has specifics for SpreadsheetML (or the other office 66 | formats if you want to start a library for them). 67 | - Add more tests. Right now there's a set of extremely high level tests 68 | that we get the sound output as OpenOffice's CSV export, but being so high 69 | level means a lot of things are completely untested until we're 100% 70 | finished, which isn't a good situation to be in. It's easy to have a typo 71 | when implementing this spec, so I'd like to aim for 100% test coverage. 72 | - The cell formatting needs a lot of work. It's what I'm likely to work on 73 | next, but if you think you can do it first I'd be happy to let someone 74 | else do it (let me know if you start working on this though, so we don't 75 | duplicate effort). 76 | - [Make CamlZip support a string or bigstring interface](https://github.com/xavierleroy/camlzip/pull/7). 77 | Right now we can only read actual files on the filesystem since that's the 78 | interface CamlZip gives us. I'd like to be able to read data from memory. 79 | - Make a pure-OCaml ZIP library. Then we won't need any system dependencies 80 | and should work with `js_of_ocaml` too. 81 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 1.4) 2 | -------------------------------------------------------------------------------- /easy_xlsx.descr: -------------------------------------------------------------------------------- 1 | A library to easily read XLSX files into a simpler format 2 | -------------------------------------------------------------------------------- /easy_xlsx.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | authors: ["Brendan Long "] 3 | maintainer: "self@brendanlong.com" 4 | homepage: "https://github.com/brendanlong/ocaml-ooxml" 5 | dev-repo: "git+https://github.com/brendanlong/ocaml-ooxml.git" 6 | bug-reports: "https://github.com/brendanlong/ocaml-ooxml/issues" 7 | doc: "https://brendanlong.github.io/ocaml-ooxml/doc" 8 | 9 | build: [ 10 | ["dune" "build" "-p" name "-j" jobs] 11 | ["dune" "runtest" "-p" name "-j" jobs] {with-test} 12 | ] 13 | 14 | depends: [ 15 | "camlzip" 16 | "ocaml" {>= "4.04.2"} 17 | "ppx_jane" 18 | "ptime" 19 | "spreadsheetml" 20 | 21 | "bisect_ppx" {build & >= "1.3.0"} 22 | "dune" {build} 23 | 24 | "csv" {test} 25 | "ounit" {test} 26 | ] 27 | -------------------------------------------------------------------------------- /easy_xlsx/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name easy_xlsx) 3 | (public_name easy_xlsx) 4 | (libraries camlzip ptime spreadsheetml str) 5 | (preprocess (pps ppx_jane bisect_ppx -conditional))) 6 | -------------------------------------------------------------------------------- /easy_xlsx/src/easy_xlsx.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open Stdint 4 | open Spreadsheetml 5 | 6 | module Value = struct 7 | type t = 8 | | Date of Ptime.date 9 | | Datetime of Ptime.t 10 | | Number of float 11 | | String of string 12 | | Time of Ptime.time 13 | 14 | let to_string = function 15 | | Date (y, m, d) -> sprintf "%d-%d-%d" y m d 16 | | Datetime t -> Ptime.to_rfc3339 t 17 | | Number f -> 18 | Float.to_string f 19 | |> String.rstrip ~drop:(function '.' -> true | _ -> false) 20 | | String s -> s 21 | | Time ((h, m, s), _tz) -> sprintf "%d:%d:%d" h m s 22 | 23 | let sexp_of_t = function 24 | | Number n -> Float.sexp_of_t n 25 | | String s -> String.sexp_of_t s 26 | | _ as t -> to_string t |> String.sexp_of_t 27 | 28 | let built_in_formats = 29 | (* FIXME: This is only the English format codes. There are 4 Asian format 30 | codes (Chinese Simplified, Chinese Traditional, Japanese, Korean) and it's 31 | not clear to me how we're supposed to pick between them. *) 32 | [ 0, "general" 33 | ; 1, "0" 34 | ; 2, "0.00" 35 | ; 3, "#,##0" 36 | ; 4, "#,##0,00" 37 | ; 9, "0%" 38 | ; 10, "0.00%" 39 | ; 11, "0.00E+00" 40 | ; 12, "# ?/?" 41 | ; 13, "# ??/??" 42 | ; 14, "mm-dd-yy" 43 | ; 15, "d-mmm-yy" 44 | ; 16, "d-mmm" 45 | ; 17, "mmm-yy" 46 | ; 18, "h:mm AM/PM" 47 | ; 19, "h:mm:ss AM/PM" 48 | ; 20, "h:mm" 49 | ; 21, "h:mm:ss" 50 | ; 22, "m/d/yy h:mm" 51 | ; 37, "#,##0 ;(#,##0)" 52 | ; 38, "#,##0 ;[Red](#,##0)" 53 | ; 39, "#,##0.00;(#,##0.00)" 54 | ; 40, "#,##0.00;[Red](#,##0.00)" 55 | ; 45, "mm:ss" 56 | ; 46, "[h]:mm:ss" 57 | ; 47, "mmss.0" 58 | ; 48, "##0.0E+0" 59 | ; 49, "@" ] 60 | |> Map.of_alist_exn (module Int) 61 | 62 | let classify_format str = 63 | (* FIXME: This is really slow. We should really use Menhir for this. *) 64 | let str = String.lowercase str in 65 | let remove_strings = Str.regexp "\\[[^\\[]*\\]\\|\"[^\"]*\"" in 66 | let str = Str.global_replace remove_strings "" str in 67 | if String.(str = "general" || str = "") then 68 | `Number 69 | else 70 | let is_string = String.contains str '@' in 71 | let is_date = String.contains str 'y' || String.contains str 'd' 72 | (* QQ is quarter, NN is day of the week *) 73 | || String.contains str 'q' || String.contains str 'n' 74 | (* WW is week number *) 75 | || String.contains str 'w' 76 | || String.is_substring str ~substring:"mmm" in 77 | let is_time = String.contains str 'h' || String.contains str 's' 78 | || String.is_substring str ~substring:"am/pm" 79 | || String.is_substring str ~substring:"a/p" in 80 | if [ is_string ; is_date || is_time ] 81 | |> List.filter ~f:Fn.id 82 | |> List.length > 1 then 83 | failwithf "Ambiguous format string '%s'" str () 84 | else if is_string then 85 | `String 86 | else if is_date && is_time then 87 | `Datetime 88 | else if is_date then 89 | `Date 90 | else if is_time then 91 | `Time 92 | else 93 | `Number 94 | 95 | let of_cell ~styles ~formats ~shared_strings 96 | ({ Worksheet.Cell.style_index ; data_type ; _ } as cell) = 97 | let formats = Map.merge built_in_formats formats ~f:(fun ~key -> 98 | function 99 | | `Left v -> Some v 100 | | `Right v -> Some v 101 | | `Both (a, b) -> 102 | if String.(a = b) then Some a 103 | else 104 | failwithf "Got format string with ID %d, \"%s\", but there is a \ 105 | built-in format string with the same ID, \"%s\"" 106 | key b a ()) 107 | in 108 | let str = Worksheet.Cell.to_string ~shared_strings cell in 109 | match data_type with 110 | | Number when String.(str <> "") -> 111 | styles.(Uint32.to_int style_index) 112 | |> Spreadsheetml.Styles.Format.number_format_id 113 | |> Option.map ~f:Uint32.to_int 114 | |> Option.value ~default:0 115 | |> (fun num_fmt_id -> 116 | match Map.find formats num_fmt_id with 117 | | Some format -> format 118 | | None -> 119 | failwithf "Cell referenced numFmtId %d but it's not listed in the \ 120 | XLSX file and isn't a known built-in format ID" 121 | num_fmt_id ()) 122 | |> classify_format 123 | |> ( 124 | let xlsx_epoch = Option.value_exn ~here:[%here] 125 | (Ptime.(of_date (1899, 12, 30))) in 126 | let date_of_float n = 127 | Float.iround_exn ~dir:`Down n 128 | |> (fun days -> Ptime.Span.of_d_ps (days, Int64.zero)) 129 | |> Option.value_exn ~here:[%here] 130 | |> Ptime.add_span xlsx_epoch 131 | |> Option.value_exn ~here:[%here] 132 | |> Ptime.to_date 133 | in 134 | let time_of_float n = 135 | Float.((modf n |> Parts.fractional) * 24. * 60. * 60.) 136 | |> Ptime.Span.of_float_s 137 | |> Option.value_exn ~here:[%here] 138 | |> Ptime.of_span 139 | |> Option.value_exn ~here:[%here] 140 | |> Ptime.to_date_time 141 | |> snd 142 | in 143 | let n = Float.of_string str in 144 | function 145 | | `Number -> Number n 146 | | `Date -> Date (date_of_float n) 147 | | `Datetime -> 148 | let date = date_of_float n in 149 | let time = time_of_float n in 150 | Datetime (Ptime.of_date_time (date, time) 151 | |> Option.value_exn ~here:[%here]) 152 | | `String -> String str 153 | | `Time -> 154 | Time (time_of_float n)) 155 | | _ -> String str 156 | 157 | let is_empty = function 158 | | String "" -> true 159 | | _ -> false 160 | end 161 | 162 | type sheet = 163 | { name : string 164 | ; rows : Value.t list list } 165 | [@@deriving fields, sexp_of] 166 | 167 | type t = sheet list [@@deriving sexp_of] 168 | 169 | let read_file filename = 170 | let zip_entry_to_xml zip name = 171 | Zip.find_entry zip name 172 | |> Zip.read_entry zip 173 | |> Xml.parse_string 174 | in 175 | let zip = Zip.open_in filename in 176 | Exn.protect ~f:(fun () -> 177 | let shared_strings = 178 | zip_entry_to_xml zip "xl/sharedStrings.xml" 179 | |> Shared_string_table.of_xml 180 | |> List.to_array 181 | in 182 | let sheets = 183 | zip_entry_to_xml zip "xl/workbook.xml" 184 | |> Workbook.of_xml 185 | |> Workbook.sheets 186 | in 187 | let stylesheet = 188 | zip_entry_to_xml zip "xl/styles.xml" 189 | |> Styles.of_xml 190 | in 191 | let styles = 192 | Styles.cell_formats stylesheet 193 | |> Array.of_list 194 | in 195 | let formats = 196 | Styles.number_formats stylesheet 197 | |> List.map ~f:(fun { Styles.Number_format.id ; format } -> 198 | Uint32.to_int id, format) 199 | |> Map.of_alist_exn (module Int) 200 | in 201 | let rel_map = 202 | zip_entry_to_xml zip "xl/_rels/workbook.xml.rels" 203 | |> Open_packaging.Relationships.of_xml 204 | |> List.map ~f:(fun { Open_packaging.Relationship.id ; target ; _ } -> 205 | id, target) 206 | |> Map.of_alist_exn (module String) 207 | in 208 | List.map sheets ~f:(fun { Workbook.Sheet.name ; id ; _ } -> 209 | let rows = 210 | let target = Map.find_exn rel_map id in 211 | let path = sprintf "xl/%s" target in 212 | let { Worksheet.columns ; rows } = 213 | Zip.find_entry zip path 214 | |> Zip.read_entry zip 215 | |> Xml.parse_string 216 | |> Worksheet.of_xml 217 | in 218 | let num_cols = 219 | columns 220 | |> List.map ~f:Worksheet.Column.max 221 | |> List.map ~f:Uint32.to_int 222 | |> List.max_elt ~compare:Int.compare 223 | |> Option.value ~default:0 224 | in 225 | let row_map = 226 | rows 227 | |> List.map ~f:(fun { Worksheet.Row.row_index ; cells ; _ } -> 228 | let index = 229 | Option.value_exn ~here:[%here] row_index 230 | |> Uint32.to_int 231 | in 232 | let cell_map = 233 | List.map cells ~f:(fun cell -> 234 | Worksheet.Cell.column cell, cell) 235 | |> Map.of_alist_exn (module Int) 236 | in 237 | let cells = 238 | Map.max_elt cell_map 239 | |> Option.map ~f:(fun (max, _) -> 240 | List.init (max + 1) ~f:(fun i -> 241 | Map.find cell_map i 242 | |> Option.value ~default:Worksheet.Cell.default)) 243 | |> Option.value ~default:[] 244 | in 245 | index - 1, cells) 246 | |> Map.of_alist_exn (module Int) 247 | in 248 | let n = 249 | Map.keys row_map 250 | |> List.max_elt ~compare:Int.compare 251 | |> Option.map ~f:((+) 1) 252 | |> Option.value ~default:0 253 | in 254 | List.init n ~f:Fn.id 255 | |> List.map ~f:(fun i -> 256 | let row = 257 | Map.find row_map i 258 | |> Option.value ~default:[] 259 | in 260 | let missing_cols = num_cols - List.length row in 261 | if missing_cols > 0 then 262 | row @ List.init ~f:(Fn.const Worksheet.Cell.default) missing_cols 263 | else 264 | row) 265 | |> List.map ~f:(List.map ~f:(Value.of_cell ~styles ~formats ~shared_strings)) 266 | in 267 | { name ; rows })) 268 | ~finally:(fun () -> Zip.close_in zip) 269 | -------------------------------------------------------------------------------- /easy_xlsx/src/easy_xlsx.mli: -------------------------------------------------------------------------------- 1 | (** An easy read for XLSX files. *) 2 | module Value : sig 3 | type t = 4 | | Date of Ptime.date 5 | | Datetime of Ptime.t 6 | | Number of float 7 | | String of string 8 | | Time of Ptime.time 9 | [@@deriving sexp_of] 10 | (** A cell value. Note that a [Number] could be either an integer or 11 | a float; Excel stores them exactly the same and the only difference 12 | is formatting. *) 13 | 14 | val to_string : t -> string 15 | val is_empty : t -> bool 16 | end 17 | 18 | type sheet = 19 | { name : string 20 | ; rows : Value.t list list } 21 | [@@deriving fields, sexp_of] 22 | (** One sheet from a workbook. Empty rows will be returned, and empty or 23 | missing columns will be returned as [""]. *) 24 | 25 | type t = sheet list [@@deriving sexp_of] 26 | 27 | val read_file : string -> sheet list 28 | (** [read_file file_name] synchronously reads the .xlsx document at 29 | [file_name] and returns all of the sheets in the document. Will throw 30 | an exception if the file doesn't exist, can't be read for any reason, 31 | isn't a valid ZIP file, or isn't a valid XLSX file. *) 32 | -------------------------------------------------------------------------------- /easy_xlsx/test/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names test_easy_xlsx_docs) 3 | (modules test_easy_xlsx_docs) 4 | (libraries csv easy_xlsx oUnit) 5 | (preprocess (pps ppx_jane))) 6 | 7 | (executables 8 | (names test_easy_xlsx_formats) 9 | (modules test_easy_xlsx_formats) 10 | (libraries csv easy_xlsx oUnit) 11 | (preprocess (pps ppx_jane))) 12 | 13 | (alias 14 | (package easy_xlsx) 15 | (name runtest) 16 | (deps test_easy_xlsx_docs.exe (source_tree files)) 17 | (action (run ./test_easy_xlsx_docs.exe))) 18 | 19 | (alias 20 | (package easy_xlsx) 21 | (name runtest) 22 | (deps test_easy_xlsx_formats.exe (source_tree files)) 23 | (action (run ./test_easy_xlsx_formats.exe))) 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/autofilter_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12_Advanced1.csv: -------------------------------------------------------------------------------- 1 | AUTOFILTER TEST DOCUMENT,,,,, 2 | ,,,,, 3 | 4 - ADVANCED FILTERS,,,,, 4 | ,,,,, 5 | 4.1 - INPLACE FILTERING,,,,, 6 | Header A,Header B,Header C,,,"(C, D, E, F, G)" 7 | A,1,A,,, 8 | B,2,a,,, 9 | C,3,abc,,, 10 | D,4,ABC,,, 11 | E,5,abcd,,, 12 | F,6,DEF,,, 13 | G,7,defg,,, 14 | H,8,cdef,,, 15 | I,9,A,,, 16 | J,10,a,,, 17 | ,,,,, 18 | Header B,Header B,Header C,,, 19 | >=3,<8,,,, 20 | ,,=ABC,,, 21 | ,,DEF,,, 22 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12_Advanced2.csv: -------------------------------------------------------------------------------- 1 | AUTOFILTER TEST DOCUMENT,,,,, 2 | ,,,,, 3 | 4 - ADVANCED FILTERS,,,,, 4 | ,,,,, 5 | 4.2 - FILTERING TO OUTPUT RANGE,,,,, 6 | Header A,Header B,Header C,,, 7 | A,1,A,,, 8 | B,2,a,,, 9 | C,3,abc,,, 10 | D,4,ABC,,, 11 | E,5,abcd,,, 12 | F,6,DEF,,, 13 | G,7,defg,,, 14 | H,8,cdef,,, 15 | I,9,A,,, 16 | J,10,a,,, 17 | ,,,,, 18 | Header B,Header B,Header C,,, 19 | >=3,<8,,,, 20 | ,,=ABC,,, 21 | ,,DEF,,, 22 | ,,,,, 23 | Header A,Header B,Header C,,,"(C, D, E, F, G)" 24 | C,3,abc,,, 25 | D,4,ABC,,, 26 | E,5,abcd,,, 27 | F,6,DEF,,, 28 | G,7,defg,,, 29 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12_Custom.csv: -------------------------------------------------------------------------------- 1 | AUTOFILTER TEST DOCUMENT,,,,, 2 | ,,,,, 3 | 3 - CUSTOM FILTERS,,,,, 4 | ,,,,, 5 | "3.1 - NUMERIC FILTERS, GREATER, LESS-EQUAL, EQUAL (table filter)",,,,, 6 | Header A,Header B,Header C,Header D,,"(C, D, G, H)" 7 | A,1,1,1,, 8 | B,2,2,1,, 9 | C,3,3,1,, 10 | D,4,4,1,, 11 | E,5,5,0,, 12 | F,6,6,0,, 13 | G,7,7,1,, 14 | H,8,8,1,, 15 | I,9,9,1,, 16 | J,10,10,1,, 17 | ,,,,, 18 | "3.2 - TEXT FILTERS, NO WILDCARDS, BEGINS, CONTAINS, EQUAL (table filter)",,,,, 19 | Header A,Header B,Header C,Header D,,"(C, D, G, H)" 20 | A,ABCD,ABDE,ABC,, 21 | B,abcd,abde,abc,, 22 | C,ABCD,BCD,ABC,, 23 | D,abcd,bcd,abc,, 24 | E,ABCD,ABCD,ABCD,, 25 | F,abcd,AbCd,BCDE,, 26 | G,ABCD,aBcD,ABC,, 27 | H,abcd,abcd,abc,, 28 | I,ABD,ABC,ABC,, 29 | J,ab,abc,abc,, 30 | ,,,,, 31 | "3.3 - TEXT FILTERS, WILDCARDS, EQUAL (table filter)",,,,, 32 | Header A,Header B,Header C,Header D,,"(C, D, G, H)" 33 | A,CA,ABC,ACDE,, 34 | B,A,abc,ABCFE,, 35 | C,AC,ADC,ABBCGE,, 36 | D,ABC,adc,ABBBCHE,, 37 | E,ABBC,AEC,ABCE,, 38 | F,ABBBC,aec,abcdde,, 39 | G,abbbc,AFC,abbbche,, 40 | H,abbc,afc,abbcge,, 41 | I,abc,AC,abcfe,, 42 | J,ac,ABBC,acde,, 43 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12_Discrete.csv: -------------------------------------------------------------------------------- 1 | AUTOFILTER TEST DOCUMENT,,,,, 2 | ,,,,, 3 | 1 - DISCRETE FILTERS,,,,, 4 | ,,,,, 5 | "1.1 - SHOW TEXT ENTRIES, SHOW BLANKS (sheet filter)",,,,, 6 | Header A,Header B,,,, 7 | A,1,,,, 8 | B,2,,,, 9 | C,3,,,, 10 | D,4,,,, 11 | E,5,,,, 12 | ,6,,,, 13 | A,7,,,, 14 | B,8,,,, 15 | C,9,,,, 16 | D,10,,,, 17 | E,11,,,, 18 | ,,,,, 19 | "1.2 - SHOW NUMERIC ENTRIES, HIDE BLANKS (table filter)",,,,, 20 | Header A,Header B,,,, 21 | A,1,,,, 22 | B,2,,,, 23 | C,3,,,, 24 | D,4,,,, 25 | E,5,,,, 26 | F,,,,, 27 | G,1,,,, 28 | H,2,,,, 29 | I,3,,,, 30 | J,4,,,, 31 | K,5,,,, 32 | ,,,,, 33 | 1.3 - HIDE/SHOW BLANKS ONLY (table filter),,,,, 34 | Header A,Header B,Header C,,,"(B, C, H, I)" 35 | A,A,A,,, 36 | B,B,,,, 37 | C,C,,,, 38 | D,D,D,,, 39 | E,,,,, 40 | F,,F,,, 41 | G,G,G,,, 42 | H,H,,,, 43 | I,I,,,, 44 | J,J,J,,, 45 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/autofilter_import_xml_12_Top10.csv: -------------------------------------------------------------------------------- 1 | AUTOFILTER TEST DOCUMENT,,,,, 2 | ,,,,, 3 | 2 - TOP-10 FILTERS,,,,, 4 | ,,,,, 5 | 2.1 - SHOW TOP 3 ITEMS (sheet filter),,,,, 6 | Header A,Header B,,,,"(H, I, J)" 7 | 1,A,,,, 8 | 2,B,,,, 9 | 3,C,,,, 10 | 4,D,,,, 11 | 5,E,,,, 12 | 6,F,,,, 13 | 7,G,,,, 14 | 8,H,,,, 15 | 9,I,,,, 16 | 10,J,,,, 17 | ,,,,, 18 | 2.2 - SHOW BOTTOM 30 PERCENT (table filter),,,,, 19 | Header A,Header B,,,,"(A, B, C)" 20 | A,1,,,, 21 | B,2,,,, 22 | C,3,,,, 23 | D,4,,,, 24 | E,5,,,, 25 | F,6,,,, 26 | G,7,,,, 27 | H,8,,,, 28 | I,9,,,, 29 | J,10,,,, 30 | ,,,,, 31 | 2.3 - MULTIPLE COMBINED TOP-10 FILTERS (table filter),,,,, 32 | Header A,Header B,Header C,Header D,,"(C, D, E, H)" 33 | A,1,1,6,, 34 | B,2,2,7,, 35 | C,3,3,8,, 36 | D,4,4,9,, 37 | E,5,5,10,, 38 | F,6,6,1,, 39 | G,7,7,2,, 40 | H,8,8,3,, 41 | I,9,9,4,, 42 | J,10,10,5,, 43 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/chart_3dsettings_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12_Elevation.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - 3D SETTINGS,,,,,,,, 2 | ,,,,,,,, 3 | 2 - ELEVATION,,,,,,,, 4 | ,,,,,,,, 5 | 2.1 - BAR CHART (-180° ... 180°),,,,,,,, 6 | Elevation = -90°,,,,Elevation = -50°,,,,Elevation = -10° 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | Elevation = 10°,,,,Elevation = 50°,,,,Elevation = 90° 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | 2.2 - PIE CHART (0° ... 90°),,,,,,,, 18 | Elevation = 0°,,,,Elevation = 10°,,,,Elevation = 20° 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | Elevation = 40°,,,,Elevation = 60°,,,,Elevation = 90° 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12_Perspective.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - 3D SETTINGS,,,,,,,, 2 | ,,,,,,,, 3 | 3 - PERSPECTIVE,,,,,,,, 4 | ,,,,,,,, 5 | 3.1 - BAR CHART (0% ... 100%),,,,,,,, 6 | Perspective = 0%,,,,Perspective = 20%,,,,Perspective = 40% 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | Perspective = 60%,,,,Perspective = 80%,,,,Perspective = 100% 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | 3.2 - PIE CHART (0% ... 100%),,,,,,,, 18 | Perspective = 0%,,,,Perspective = 20%,,,,Perspective = 40% 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | Perspective = 60%,,,,Perspective = 80%,,,,Perspective = 100% 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12_Rotation.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - 3D SETTINGS,,,,,,,, 2 | ,,,,,,,, 3 | 1 - ROTATION,,,,,,,, 4 | ,,,,,,,, 5 | 1.1 - BAR CHART (0° ... 359°),,,,,,,, 6 | Rotation = 0°,,,,Rotation = 60°,,,,Rotation = 120° 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | Rotation = 180°,,,,Rotation = 240°,,,,Rotation = 300° 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | 1.2 - PIE CHART (0° ... 359°),,,,,,,, 18 | Rotation = 0°,,,,Rotation = 60°,,,,Rotation = 120° 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | Rotation = 180°,,,,Rotation = 240°,,,,Rotation = 300° 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12_Settings.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - 3D SETTINGS,,,,,,,, 2 | ,,,,,,,, 3 | 4 - OTHER 3D SETTINGS,,,,,,,, 4 | ,,,,,,,, 5 | 4.1 - RIGHT-ANGLED AXES,,,,,,,, 6 | RightAngledAxes = off,,,,RightAngledAxes = on,,,, 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | 4.2 - BAR CHART SCALING (PERSPECTIVE),,,,,,,, 13 | "AutoScaling = on, RelDepth = 30%",,,,"AutoScaling = on, RelDepth = 100%",,,,"AutoScaling = on, RelDepth = 300%" 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | ,,,,,,,, 18 | "AutoScaling = off, RelHeight = 30%",,,,"AutoScaling = off, RelHeight = 100%",,,,"AutoScaling = off, RelHeight = 300%" 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,,,,,,,, 24 | 4.3 - BAR CHART SCALING (RIGHT-ANGLED),,,,,,,, 25 | "AutoScaling = on, RelDepth = 30%",,,,"AutoScaling = on, RelDepth = 100%",,,,"AutoScaling = on, RelDepth = 300%" 26 | ,,,,,,,, 27 | ,,,,,,,, 28 | ,,,,,,,, 29 | ,,,,,,,, 30 | "AutoScaling = off, RelHeight = 30%",,,,"AutoScaling = off, RelHeight = 100%",,,,"AutoScaling = off, RelHeight = 300%" 31 | ,,,,,,,, 32 | ,,,,,,,, 33 | ,,,,,,,, 34 | ,,,,,,,, 35 | ,,,,,,,, 36 | 4.4 - PIE CHART SCALING,,,,,,,, 37 | "AutoScaling = off, RelHeight = 30%",,,,"AutoScaling = off, RelHeight = 100%",,,,"AutoScaling = off, RelHeight = 300%" 38 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_3dsettings_import_xml_12_SourceData.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - 3D SETTINGS,,, 2 | ,,, 3 | 5 - SOURCE DATA,,, 4 | ,Series 1,Series 2,Series 3 5 | Point 1,2,4,2 6 | Point 2,4,3,1 7 | Point 3,6,2,-1 8 | Point 4,8,1,-2 9 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/chart_axis_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12_Axis Labels.csv: -------------------------------------------------------------------------------- 1 | CHART AXES TEST DOCUMENT,,,,,,,,,,,, 2 | ,,,,,,,,,,,, 3 | 2 - AXIS LABELS,,,,,,,,,,,, 4 | ,,,,,,,,,,,, 5 | 2.1 - 2-DIMENSIONAL LINE CHARTS,,,,,,,,,,,, 6 | Invisible,,,,Next to axis,,,,High,,,,Low 7 | ,,,,,,,,,,,, 8 | ,,,,,,,,,,,, 9 | ,,,,,,,,,,,, 10 | ,,,,,,,,,,,, 11 | ,,,,,,,,,,,, 12 | 2.2 - 2-DIMENSIONAL RADAR CHARTS,,,,,,,,,,,, 13 | Invisible,,,,Next to axis,,,,,,,, 14 | ,,,,,,,,,,,, 15 | ,,,,,,,,,,,, 16 | ,,,,,,,,,,,, 17 | ,,,,,,,,,,,, 18 | ,,,,,,,,,,,, 19 | 2.3 - 2-DIMENSIONAL SCATTER CHARTS,,,,,,,,,,,, 20 | Invisible,,,,Next to axis,,,,High,,,,Low 21 | ,,,,,,,,,,,, 22 | ,,,,,,,,,,,, 23 | ,,,,,,,,,,,, 24 | ,,,,,,,,,,,, 25 | ,,,,,,,,,,,, 26 | 2.4 - 3-DIMENSIONAL DEEP BAR CHARTS,,,,,,,,,,,, 27 | Invisible,,,,Next to axis,,,,High,,,,Low 28 | ,,,,,,,,,,,, 29 | ,,,,,,,,,,,, 30 | ,,,,,,,,,,,, 31 | ,,,,,,,,,,,, 32 | ,,,,,,,,,,,, 33 | 2.5 - LABEL TEXT FORMATTING,,,,,,,,,,,, 34 | "Font size X = 8, Y = 12",,,,"Weight X = bold, posture Y = italic, color X = red, Y = blue",,,,"Underline X = single, strikeout Y = single",,,,"Rotation X = 20° cw, Y =20° ccw" 35 | ,,,,,,,,,,,, 36 | ,,,,,,,,,,,, 37 | ,,,,,,,,,,,, 38 | ,,,,,,,,,,,, 39 | ,,,,,,,,,,,, 40 | 2.6 - LABEL NUMBER FORMAT,,,,,,,,,,,, 41 | Linked to source,,,,0.000,,,,0.000;[Red]-0.000,,,,0% 42 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12_Axis Line.csv: -------------------------------------------------------------------------------- 1 | CHART AXES TEST DOCUMENT,,,,,,,, 2 | ,,,,,,,, 3 | 1 - AXIS LINE FORMATTING,,,,,,,, 4 | ,,,,,,,, 5 | 1.1 - AXIS VISIBILITY,,,,,,,, 6 | Invisible line,,,,Default line,,,,Deleted axes 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | 1.2 - AXIS LINE FORMATTING,,,,,,,, 13 | "X = solid thin red, Y = solid thin blue",,,,"X = dashed thick red 50% transp., Y = dotted thick blue",,,,"X = black-red gradient arrow, Y = transp. gradient arrow" 14 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12_Gridlines.csv: -------------------------------------------------------------------------------- 1 | CHART GRIDLINES TEST DOCUMENT,,,,,,,, 2 | ,,,,,,,, 3 | 4 - GRIDLINES,,,,,,,, 4 | ,,,,,,,, 5 | 4.1 - MAJOR GRIDLINES IN 2-DIMENSIONAL LINE CHARTS,,,,,,,, 6 | Invisible,,,,Automatic,,,,Manual formatting 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | 4.2 - MAJOR GRIDLINES IN 2-DIMENSIONAL RADAR CHARTS,,,,,,,, 13 | Invisible,,,,Automatic,,,,Manual formatting 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | ,,,,,,,, 18 | ,,,,,,,, 19 | 4.3 - MAJOR GRIDLINES IN 2-DIMENSIONAL SCATTER CHARTS,,,,,,,, 20 | Invisible,,,,Automatic,,,,Manual formatting 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,,,,,,,, 24 | ,,,,,,,, 25 | ,,,,,,,, 26 | 4.4 - MAJOR GRIDLINES IN 3-DIMENSIONAL DEEP BAR CHARTS,,,,,,,, 27 | Invisible,,,,Automatic,,,,Manual formatting 28 | ,,,,,,,, 29 | ,,,,,,,, 30 | ,,,,,,,, 31 | ,,,,,,,, 32 | Vertical only,,,,Horizontal only,,,,Deep only 33 | ,,,,,,,, 34 | ,,,,,,,, 35 | ,,,,,,,, 36 | ,,,,,,,, 37 | ,,,,,,,, 38 | 4.5 - MAJOR AND MINOR GRIDLINES,,,,,,,, 39 | Invisible,,,,Automatic,,,,Manual formatting 40 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12_SourceData.csv: -------------------------------------------------------------------------------- 1 | CHART AXES TEST DOCUMENT,,,, 2 | ,,,, 3 | 5 - SOURCE DATA,,,, 4 | ,Series 1,Series 2,Series 3,X values 5 | Point 1,2,4,1,-2 6 | Point 2,6,2,2,-1 7 | Point 3,4,3,-1,1 8 | Point 4,8,1,-2,2 9 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_axis_import_xml_12_Tick Marks.csv: -------------------------------------------------------------------------------- 1 | CHART AXES TEST DOCUMENT,,,,,,,,,,,, 2 | ,,,,,,,,,,,, 3 | 3 - AXIS TICK MARKS,,,,,,,,,,,, 4 | ,,,,,,,,,,,, 5 | 3.1 - MAJOR TICKMARKS,,,,,,,,,,,, 6 | None,,,,Inside,,,,Outside,,,,Crossing 7 | ,,,,,,,,,,,, 8 | ,,,,,,,,,,,, 9 | ,,,,,,,,,,,, 10 | ,,,,,,,,,,,, 11 | ,,,,,,,,,,,, 12 | 3.2 - MINOR TICKMARKS,,,,,,,,,,,, 13 | None,,,,Inside,,,,Outside,,,,Crossing 14 | ,,,,,,,,,,,, 15 | ,,,,,,,,,,,, 16 | ,,,,,,,,,,,, 17 | ,,,,,,,,,,,, 18 | ,,,,,,,,,,,, 19 | 3.3 - MAJOR AND MINOR TICKMARKS,,,,,,,,,,,, 20 | None,,,,Inside,,,,Outside,,,,Crossing 21 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_bitmaps_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/chart_bitmaps_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_bitmaps_import_xml_12_General.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - BITMAP FILLS,,,,,,,,,,, 2 | ,,,,,,,,,,, 3 | 1 - GENERAL CHART ELEMENTS,,,,,,,,,,,Image source: http://en.wikipedia.org/wiki/File:Smiley.svg 4 | ,,,,,,,,,,, 5 | 1.1 - CHART BACKGROUND,,,,,,,,,,, 6 | Stretched,,,,"Stretched, left = 30%, top = 10%, right = bottom = -10%",,,,"Stretched, transparency = 50%",,, 7 | ,,,,,,,,,,, 8 | ,,,,,,,,,,, 9 | ,,,,,,,,,,, 10 | ,,,,,,,,,,, 11 | Tiled,,,,"Tiled, scale = 50%, align = bottom-right, mirror = vertical",,,,"Tiled, offsetX = offsetY = 20pt, transparency = 50%",,, 12 | ,,,,,,,,,,, 13 | ,,,,,,,,,,, 14 | ,,,,,,,,,,, 15 | ,,,,,,,,,,, 16 | ,,,,,,,,,,, 17 | "1.2 - PLOT AREA, LEGEND, CHART TITLE, AXIS TITLES",,,,,,,,,,, 18 | Stretched,,,,Tiled (legend and title scale = 50%),,,,,,, 19 | ,,,,,,,,,,, 20 | ,,,,,,,,,,, 21 | ,,,,,,,,,,, 22 | ,,,,,,,,,,, 23 | ,,,,,,,,,,, 24 | "1.3 - FLOOR, BACK WALL, SIDE WALL",,,,,,,,,,, 25 | Stretched,,,,Stacked,,,,,,, 26 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_bitmaps_import_xml_12_Series 2D.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - BITMAP FILLS,,,,,,,, 2 | ,,,,,,,, 3 | 2 - 2-DIMENSIONAL DATA SERIES,,,,,,,, 4 | ,,,,,,,, 5 | "2.1 - BAR CHARTS (Series1, Series2 Point2)",,,,,,,, 6 | Stretched,,,,Stacked,,,,"Stacked with unit, Series1 unit = 10, Series2 unit = 5" 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | 2.2 - OTHER CHART TYPES,,,,,,,, 13 | "Area, Series1 = stretched, Series2 = tiled",,,,"Pie, Point1 = stretched, Point2 = tiled",,,,"Filled radar, Series1 = stretched, Series2 = tiled" 14 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_bitmaps_import_xml_12_Series 3D.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - BITMAP FILLS,,,,,,,, 2 | ,,,,,,,, 3 | 3 - 3-DIMENSIONAL DATA SERIES,,,,,,,, 4 | ,,,,,,,, 5 | "2.2 - BAR CHARTS (Series1, Series2 Point2)",,,,,,,, 6 | "Series1, Series2/Point2 = stretched",,,,"Series1, Series2/Point2 = stacked",,,,"Stacked with unit, Series1 unit = 10, Series2 unit = 5" 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | "Series1 = front only, Series2 = side+top",,,,"Series1 = side only, Series2 = front+top",,,,"Series1 = top only, Series2 = front+side" 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | 3.2 - OTHER CHART TYPES,,,,,,,, 18 | "Area, Series1 = stretched, Series2 = tiled",,,,"Pie, Point1 = stretched, Point2 = tiled",,,,"Line, Series1 = stretched, Series2 = tiled" 19 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/chart_bitmaps_import_xml_12_SourceData.csv: -------------------------------------------------------------------------------- 1 | CHART TEST DOCUMENT - BITMAP FILLS,, 2 | ,, 3 | 4 - SOURCE DATA,, 4 | ,Series 1,Series 2 5 | Point 1,50.0,30.0 6 | Point 2,40.0,25.0 7 | Point 3,30.0,20.0 8 | Point 4,30.0,20.0 9 | Point 5,40.0,25.0 10 | Point 6,50.0,30.0 11 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import csv 3 | import os 4 | import subprocess 5 | import sys 6 | 7 | import xlrd 8 | 9 | 10 | files = sys.argv[1:] 11 | for file in files: 12 | name, _ = file.rsplit(".", maxsplit=1) 13 | workbook = xlrd.open_workbook(file) 14 | for sheet in workbook.sheets(): 15 | with open("%s_%s.csv" % (name, sheet.name), "w") as f: 16 | writer = csv.writer(f) 17 | for row in sheet.get_rows(): 18 | values = [cell.value for cell in row] 19 | writer.writerow(values) 20 | print("; `Skip, \"%s\", [ %s ]" % (name, "; ".join(["\"%s\"" % sheet.name for sheet in workbook.sheets()]))) 21 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/formats.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/formats.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/hyperlink_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/hyperlink_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/hyperlink_import_xml_12_Sheet'!.csv: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/hyperlink_import_xml_12_Sheet1.csv: -------------------------------------------------------------------------------- 1 | CELL FORMAT TEST DOCUMENT,,, 2 | ,,, 3 | 1 - INTERNAL LINKS,,, 4 | ,,, 5 | 1.1 - INTERNAL LINKS TO OWN SHEET (cell A1),,, 6 | URL,Hyperlink cell,, 7 | #A1,#A1,, 8 | #Sheet1!A1,#Sheet1!A1,, 9 | #name1,#name1,, 10 | #Sheet1!name1,#Sheet1!name1,, 11 | #name2,#name2,, 12 | #name3,#name3,, 13 | ,,, 14 | 1.2 - INTERNAL LINKS TO OTHER SHEETS (cell A2),,, 15 | URL,Hyperlink cell,, 16 | #Sheet2!A1,#Sheet2!A1,, 17 | #Sheet2!localname,#Sheet2!localname,, 18 | #'Sheet''!'!A1,#'Sheet''!'!A1,, 19 | #'Sheet''!'!localname,#'Sheet''!'!localname,, 20 | ,,, 21 | 2 - FILE LINKS,,, 22 | ,,, 23 | 2.1 - LOCAL FILE LINKS,,, 24 | URL,Hyperlink cell,, 25 | EXTDATA12.XLSX,EXTDATA12.XLSX,, 26 | EXTDATA12.XLSX#A1,EXTDATA12.XLSX#A1,, 27 | long_unicode_filename_€.xls,long_unicode_filename_€.xls,, 28 | long_unicode_filename_€.xls#A1,long_unicode_filename_€.xls#A1,, 29 | PATH\EXTP12_1.XLSX,PATH\EXTP12_1.XLSX,, 30 | PATH\EXTP12_1.XLSX#A1,PATH\EXTP12_1.XLSX#A1,, 31 | \PATH\EXTP12_2.XLSX,\PATH\EXTP12_2.XLSX,, 32 | \PATH\EXTP12_2.XLSX#A1,\PATH\EXTP12_2.XLSX#A1,, 33 | C:\PATH\EXTP12_3.XLSX,C:\PATH\EXTP12_3.XLSX,, 34 | C:\PATH\EXTP12_3.XLSX#A1,C:\PATH\EXTP12_3.XLSX#A1,, 35 | ,,, 36 | 2.2 - UNC PATHS,,, 37 | URL,Hyperlink cell,, 38 | \\127.0.0.1\PATH\EXTP12_4.XLSX,\\127.0.0.1\PATH\EXTP12_4.XLSX,, 39 | \\127.0.0.1\PATH\EXTP12_4.XLSX#A1,\\127.0.0.1\PATH\EXTP12_4.XLSX#A1,, 40 | ,,, 41 | 3 - URLS,,, 42 | ,,, 43 | 3.1 - HTTP LINKS,,, 44 | URL,Hyperlink cell,, 45 | http://www.example.org/,http://www.example.org/,, 46 | http://127.0.0.1/PATH/EXTP12_5.XLSX,http://127.0.0.1/PATH/EXTP12_5.XLSX,, 47 | http://127.0.0.1/PATH/EXTP12_5.XLSX#A1,http://127.0.0.1/PATH/EXTP12_5.XLSX#A1,, 48 | ,,, 49 | 3.2 - MAIL LINKS,,, 50 | URL,Hyperlink cell,, 51 | mailto:test@example.org,mailto:test@example.org,, 52 | mailto:test@example.org?subject=subject,mailto:test@example.org?subject=subject,, 53 | ,,, 54 | 4 - OTHER FEATURES,,, 55 | ,,, 56 | 4.1 - TOOLTIPS,,, 57 | Tooltip,Hyperlink cell,, 58 | This is a tooltip.,http://www.example.org/,, 59 | This is another tooltip.,http://www.example.org/,, 60 | ,,, 61 | "4.2 - CELL TYPES, TEXT FORMATTING",,, 62 | Cell type,Cell without hyperlink,Cell with hyperlink,With text formatting 63 | String,STRING,STRING,STRING 64 | Rich string,RICH STRING,RICH STRING,RICH STRING 65 | Number,12345,12345,12345 66 | Boolean,1,1,1 67 | Error,#DIV/0!,#DIV/0!,#DIV/0! 68 | String formula,STRING,STRING,STRING 69 | Number formula,12345,12345,12345 70 | Boolean formula,1,1,1 71 | Error formula,#DIV/0!,#DIV/0!,#DIV/0! 72 | String formula with HYPERLINK function,STRING,STRING,STRING 73 | Number formula with HYPERLINK function,12345,12345,12345 74 | Boolean formula with HYPERLINK function,1,1,1 75 | Error formula with HYPERLINK function,#DIV/0!,#DIV/0!,#DIV/0! 76 | Single-cell array formulas,STRING,STRING,STRING 77 | Array formula,STRING,STRING,STRING 78 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/hyperlink_import_xml_12_Sheet2.csv: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/oleobject_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/oleobject_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/oleobject_import_xml_12_OLE.csv: -------------------------------------------------------------------------------- 1 | OLE OBJECT TEST DOCUMENT,,,,,, 2 | ,,,,,, 3 | 1 - LINKED AND EMBEDDED OBJECTS,,,,,, 4 | ,,,,,, 5 | 1.1 - LINKED PICTURES,,,,,, 6 | Internal cell,Internal name,Sheet cell,Sheet name,External cell,External name,Linked cell 7 | ,,,,,, 8 | External DDE,External OLE,,,,, 9 | ,,,,,, 10 | ,,,,,, 11 | 1.2 - EMBEDDED OBJECTS,,,,,, 12 | Picture,... as symbol,... own symbol,Text,Spreadsheet,, 13 | ,,,,,, 14 | ,,,,,, 15 | 1.3 - LINKED OBJECTS,,,,,, 16 | Picture,Text (manual update),,,Spreadsheet,, 17 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/oleobject_import_xml_12_SourceData.csv: -------------------------------------------------------------------------------- 1 | Linked sheet 2 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/scenarios_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/scenarios_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/scenarios_import_xml_12_Name.csv: -------------------------------------------------------------------------------- 1 | SCENARIOS TEST DOCUMENT, 2 | , 3 | 3 - SCENARIO NAMES, 4 | , 5 | "3.1 - NAMES ALREADY USED IN SHEET ""Ranges""", 6 | "cells = A8:B9, shown = Scenario3, selected = Scenario2", 7 | , 8 | Item 3.1,Item 3.2 9 | Item 3.3,Item 3.4 10 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/scenarios_import_xml_12_Ranges.csv: -------------------------------------------------------------------------------- 1 | SCENARIOS TEST DOCUMENT, 2 | , 3 | 1 - SCENARIO RANGES, 4 | , 5 | 1.1 - 3 SCENARIOS IN SINGLE RANGE, 6 | "cells = A8:B9, shown = Scenario2, selected = Scenario3", 7 | , 8 | Item 2.1,Item 2.2 9 | Item 2.3,Item 2.4 10 | , 11 | 1.2 - 2 SCENARIOS IN SINGLE CELLS, 12 | cells = A14;B15, 13 | , 14 | Item 5.1, 15 | ,Item 5.2 16 | , 17 | 1.3 - REFERENCE CHECK, 18 | "formula = ""=RefCheck!$A$7"", value = ""linked cell""", 19 | linked cell, 20 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/scenarios_import_xml_12_RefCheck.csv: -------------------------------------------------------------------------------- 1 | SCENARIOS TEST DOCUMENT 2 | 3 | 4 - LINKED DATA 4 | 5 | 4.1 - REFERENCE TARGET 6 | 7 | linked cell 8 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/scenarios_import_xml_12_Settings.csv: -------------------------------------------------------------------------------- 1 | SCENARIOS TEST DOCUMENT, 2 | , 3 | 2 - SCENARIO SETTINGS, 4 | , 5 | "2.1 - PROTECTION, VISIBILITY", 6 | "Scenario10 = not protected/visible, Scenario11 = protected/hidden", 7 | , 8 | 1,1 9 | , 10 | 2.2 - COMMENTS, 11 | "Scenario12 = ""Comment for Scenario12""", 12 | , 13 | 1,1 14 | , 15 | 2.3 - REFERENCE CHECK, 16 | "formula = ""=RefCheck!$A$7"", value = ""linked cell""", 17 | linked cell, 18 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/sheetprotection_import_xml_12.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanlong/ocaml-ooxml/dd6b8496d22670cc0fa0b50990414f955267ded9/easy_xlsx/test/files/sheetprotection_import_xml_12.xlsx -------------------------------------------------------------------------------- /easy_xlsx/test/files/sheetprotection_import_xml_12_Sheet1.csv: -------------------------------------------------------------------------------- 1 | PROTECTION TEST DOCUMENT, 2 | , 3 | 1 - WORKSHEET PROTECTION, 4 | , 5 | 1.1 - NO WORKSHEET PROTECTION, 6 | Setting,Value 7 | Password,none 8 | Protect locked cells,off 9 | Edit objects,on 10 | Edit scenarios,off 11 | Format cells,off 12 | Format columns,off 13 | Format rows,off 14 | Insert columns,on 15 | Insert rows,on 16 | Insert hyperlinks,on 17 | Delete columns,on 18 | Delete rows,on 19 | Select locked cells,on 20 | Use sort,on 21 | Use autofilter,on 22 | Pivot tables reports,on 23 | Select unlocked cells,on 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/sheetprotection_import_xml_12_Sheet2.csv: -------------------------------------------------------------------------------- 1 | PROTECTION TEST DOCUMENT, 2 | , 3 | 1 - WORKSHEET PROTECTION, 4 | , 5 | 1.2 - WORKSHEET PROTECTION WITHOUT PASSWORD, 6 | Setting,Value 7 | Password,none 8 | Protect locked cells,on 9 | Edit objects,off 10 | Edit scenarios,off 11 | Format cells,off 12 | Format columns,off 13 | Format rows,off 14 | Insert columns,off 15 | Insert rows,off 16 | Insert hyperlinks,off 17 | Delete columns,off 18 | Delete rows,off 19 | Select locked cells,on 20 | Use sort,off 21 | Use autofilter,off 22 | Pivot tables reports,off 23 | Select unlocked cells,on 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/sheetprotection_import_xml_12_Sheet3.csv: -------------------------------------------------------------------------------- 1 | PROTECTION TEST DOCUMENT, 2 | , 3 | 1 - WORKSHEET PROTECTION, 4 | , 5 | 1.3 - WORKSHEET PROTECTION WITH PASSWORD, 6 | Setting,Value 7 | Password,"""password""" 8 | Protect locked cells,on 9 | Edit objects,off 10 | Edit scenarios,off 11 | Format cells,off 12 | Format columns,off 13 | Format rows,off 14 | Insert columns,off 15 | Insert rows,off 16 | Insert hyperlinks,off 17 | Delete columns,off 18 | Delete rows,off 19 | Select locked cells,on 20 | Use sort,off 21 | Use autofilter,off 22 | Pivot tables reports,off 23 | Select unlocked cells,on 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/files/sheetprotection_import_xml_12_Sheet4.csv: -------------------------------------------------------------------------------- 1 | PROTECTION TEST DOCUMENT, 2 | , 3 | 1 - WORKSHEET PROTECTION, 4 | , 5 | 1.4 - WORKSHEET PROTECTION WITH PASSWORD, 6 | Setting,Value 7 | Password,"""password""" 8 | Protect locked cells,on 9 | Edit objects,on 10 | Edit scenarios,on 11 | Format cells,on 12 | Format columns,on 13 | Format rows,on 14 | Insert columns,on 15 | Insert rows,on 16 | Insert hyperlinks,on 17 | Delete columns,on 18 | Delete rows,on 19 | Select locked cells,off 20 | Use sort,on 21 | Use autofilter,on 22 | Pivot tables reports,on 23 | Select unlocked cells,off 24 | -------------------------------------------------------------------------------- /easy_xlsx/test/test_easy_xlsx_docs.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open OUnit2 3 | open Printf 4 | 5 | (* Test documents come from OpenOffice's test suite: 6 | https://www.openoffice.org/sc/testdocs/ 7 | 8 | I downloaded the "XML" versions of each document, then converted them to 9 | CSV using OpenOffice. The tests then compare our input of the XLSX 10 | document to the CSV. *) 11 | type t = 12 | { name : string 13 | ; rows : string list list } 14 | 15 | let pp_diff fmt (exp, real) = 16 | if String.(exp.name <> real.name) then 17 | Caml.Format.fprintf fmt "Expected sheet %s but got sheet %s" exp.name real.name 18 | else 19 | Caml.Format.fprintf fmt "Sheet: %s\n" exp.name; 20 | let print_opt_row diff_mark row = 21 | Option.iter row ~f:(fun row -> 22 | String.concat ~sep:"," row 23 | |> Caml.Format.fprintf fmt "%c %s\n" diff_mark) 24 | in 25 | let rec diff_lists = function 26 | | [], [] -> () 27 | | exp, real -> 28 | let exp_row = List.hd exp in 29 | let real_row = List.hd real in 30 | if Option.equal (List.equal ~equal:String.equal) exp_row real_row then 31 | print_opt_row ' ' exp_row 32 | else begin 33 | print_opt_row '-' exp_row; 34 | print_opt_row '+' real_row 35 | end; 36 | diff_lists 37 | (List.tl exp |> Option.value ~default:[], 38 | List.tl real |> Option.value ~default:[]) 39 | in 40 | diff_lists (exp.rows, real.rows) 41 | 42 | let sort_sheets = 43 | List.sort ~compare:(fun a b -> 44 | String.compare a.name b.name) 45 | 46 | let make_test (file_name, sheet_names) = 47 | let normalize_sheets sheets = 48 | (* Fix sheet order and remove whitespace at the end of lines or sheets, 49 | since we don't care about either of these things *) 50 | sort_sheets sheets 51 | |> List.map ~f:(fun { name ; rows } -> 52 | let rows = 53 | List.rev_map rows ~f:(fun row -> 54 | List.rev row 55 | |> List.drop_while ~f:String.is_empty 56 | |> List.rev) 57 | |> List.drop_while ~f:List.is_empty 58 | |> List.rev 59 | in 60 | { name ; rows }) 61 | in 62 | let sheet_names = List.sort ~compare:String.compare sheet_names in 63 | file_name >:: 64 | fun _ -> 65 | let expect = 66 | List.map sheet_names ~f:(fun sheet_name -> 67 | let rows = 68 | sprintf "files/%s_%s.csv" file_name sheet_name 69 | |> Csv.load ~strip:false 70 | |> Csv.to_array 71 | |> Array.map ~f:Array.to_list 72 | |> Array.to_list 73 | in 74 | { name = sheet_name ; rows }) 75 | |> normalize_sheets 76 | in 77 | Easy_xlsx.read_file (sprintf "files/%s.xlsx" file_name) 78 | |> List.map ~f:(fun { Easy_xlsx.name ; rows } -> 79 | let rows = 80 | List.map rows ~f:(List.map ~f:Easy_xlsx.Value.to_string) in 81 | { name ; rows }) 82 | |> normalize_sheets 83 | |> List.iter2_exn expect ~f:(fun expect actual -> 84 | assert_equal ~pp_diff expect actual) 85 | 86 | let () = 87 | [ "autofilter_import_xml_12", [ "Discrete"; "Top10"; "Custom"; "Advanced1"; "Advanced2" ] 88 | ; "chart_3dsettings_import_xml_12", [ "Rotation"; "Elevation"; "Perspective"; "Settings"; "SourceData" ] 89 | ; "chart_axis_import_xml_12", [ "Axis Line"; "Axis Labels"; "Tick Marks"; "Gridlines"; "SourceData" ] 90 | ; "hyperlink_import_xml_12", [ "Sheet1"; "Sheet2"; "Sheet'!" ] 91 | ; "oleobject_import_xml_12", [ "OLE"; "SourceData" ] 92 | ; "scenarios_import_xml_12", [ "Ranges"; "Settings"; "Name"; "RefCheck" ] 93 | ; "sheetprotection_import_xml_12", [ "Sheet1"; "Sheet2"; "Sheet3"; "Sheet4" ] ] 94 | |> List.map ~f:make_test 95 | |> test_list 96 | |> run_test_tt_main 97 | -------------------------------------------------------------------------------- /easy_xlsx/test/test_easy_xlsx_formats.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open OUnit2 4 | 5 | (* Test that we properly classify formats from OpenOffice *) 6 | 7 | let test_easy_xlsx_formats = 8 | let open Easy_xlsx.Value in 9 | let expect_number = Number (-12345.6789) in 10 | let expect_date = Date (1866, 03, 12) in 11 | let expect_datetime = 12 | let date = 1866, 03, 12 in 13 | let time = (7, 42, 23), 0 in 14 | Datetime (Ptime.of_date_time (date, time) |> Option.value_exn) in 15 | let expect_time = Time ((16, 17, 37), 0) in 16 | let expect_string = String "-12345.6789" in 17 | Easy_xlsx.read_file "files/formats.xlsx" 18 | |> List.hd_exn 19 | |> Easy_xlsx.rows 20 | |> List.tl_exn 21 | |> List.concat_mapi ~f:( 22 | fun row -> 23 | let row = row + 2 in 24 | let open Easy_xlsx.Value in 25 | function 26 | | _boolean :: number :: date :: datetime :: string :: time :: _ -> 27 | let test type_ value expected = 28 | if is_empty value then 29 | None 30 | else 31 | (sprintf "%s on row %d" type_ row 32 | >:: fun _ -> 33 | assert_equal ~printer:Easy_xlsx.Value.to_string expected value) 34 | |> Option.some 35 | in 36 | [ test "Number" number expect_number 37 | ; test "Date" date expect_date 38 | ; test "Datetime" datetime expect_datetime 39 | ; test "String" string expect_string 40 | ; test "Time" time expect_time ] 41 | | _ -> assert false) 42 | |> List.filter_opt 43 | |> List.rev 44 | 45 | let () = 46 | test_easy_xlsx_formats 47 | |> test_list 48 | |> run_test_tt_main 49 | -------------------------------------------------------------------------------- /open_packaging.descr: -------------------------------------------------------------------------------- 1 | A library for parsing Microsoft's Open Packaging Specification (most commonly used for Microsoft 2 | Office's "Office Open XML") 3 | -------------------------------------------------------------------------------- /open_packaging.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | authors: ["Brendan Long "] 3 | maintainer: "self@brendanlong.com" 4 | homepage: "https://github.com/brendanlong/ocaml-ooxml" 5 | dev-repo: "git+https://github.com/brendanlong/ocaml-ooxml.git" 6 | bug-reports: "https://github.com/brendanlong/ocaml-ooxml/issues" 7 | doc: "https://brendanlong.github.io/ocaml-ooxml/doc" 8 | 9 | build: [ 10 | ["dune" "build" "-p" name "-j" jobs] 11 | ["dune" "runtest" "-p" name "-j" jobs] {with-test} 12 | ] 13 | 14 | depends: [ 15 | "base" {= "v0.11.1"} 16 | "ocaml" {>= "4.04.2"} 17 | "ppx_jane" 18 | "sexplib" 19 | "stdint" 20 | "xml-light" 21 | 22 | "bisect_ppx" {build & >= "1.3.0"} 23 | "dune" {build} 24 | 25 | "ounit" {test} 26 | ] 27 | -------------------------------------------------------------------------------- /open_packaging/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name open_packaging) 3 | (public_name open_packaging) 4 | (libraries base sexplib stdint xml-light) 5 | (flags (:standard -w -47)) 6 | (preprocess (pps ppx_jane bisect_ppx -conditional))) 7 | -------------------------------------------------------------------------------- /open_packaging/src/open_packaging.ml: -------------------------------------------------------------------------------- 1 | (* See ECMA 376 Part 2 - "Open Packaging Conventions" *) 2 | 3 | module Relationship = Relationship 4 | module Relationships = Relationships 5 | module Utils = Utils 6 | -------------------------------------------------------------------------------- /open_packaging/src/relationship.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open Utils 4 | 5 | module Target_mode = struct 6 | type t = 7 | | Internal 8 | | External 9 | [@@deriving sexp_of] 10 | 11 | let of_string = function 12 | | "Internal" -> Internal 13 | | "External" -> External 14 | | str -> failwithf "Expected ST_TargetMode but got '%s'" str () 15 | end 16 | 17 | (* 8.3.3.2 Relationship Element *) 18 | type t = 19 | { target_mode : Target_mode.t 20 | ; target : string 21 | ; type_ : string 22 | ; id : string } 23 | [@@deriving fields, sexp_of] 24 | 25 | let of_xml = function 26 | | Xml.Element ("Relationship", attrs, _) -> 27 | let target_mode = ref Target_mode.Internal in 28 | let target = ref None in 29 | let type_ = ref None in 30 | let id = ref None in 31 | List.iter attrs ~f:(function 32 | | "TargetMode", v -> target_mode := Target_mode.of_string v 33 | | "Target", v -> target := Some v 34 | | "Type", v -> type_ := Some v 35 | | "Id", v -> id := Some v 36 | | _ -> ()); 37 | let require_attribute = require_attribute "Relationship" in 38 | let target = require_attribute "Target" !target in 39 | let type_ = require_attribute "Type" !type_ in 40 | let id = require_attribute "Id" !id in 41 | Some { target_mode = !target_mode ; target ; type_ ; id } 42 | | _ -> None 43 | -------------------------------------------------------------------------------- /open_packaging/src/relationship.mli: -------------------------------------------------------------------------------- 1 | module Target_mode : sig 2 | type t = 3 | | Internal 4 | | External 5 | [@@deriving sexp_of] 6 | 7 | val of_string : string -> t 8 | end 9 | 10 | type t = 11 | { target_mode : Target_mode.t 12 | ; target : string (** xsd:anyURI *) 13 | ; type_ : string (** xsd:anyURI *) 14 | ; id : string (** xsd:ID *) } 15 | [@@deriving fields, sexp_of] 16 | 17 | val of_xml : Xml.xml -> t option 18 | -------------------------------------------------------------------------------- /open_packaging/src/relationships.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Utils 3 | 4 | (* 8.3.3.1 Relationships Element *) 5 | type t = Relationship.t list 6 | [@@deriving sexp_of] 7 | 8 | let of_xml = 9 | expect_element "Relationships" (fun _ -> 10 | List.filter_map ~f:Relationship.of_xml) 11 | -------------------------------------------------------------------------------- /open_packaging/src/relationships.mli: -------------------------------------------------------------------------------- 1 | type t = Relationship.t list 2 | [@@deriving sexp_of] 3 | 4 | val of_xml : Xml.xml -> Relationship.t list 5 | -------------------------------------------------------------------------------- /open_packaging/src/utils.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open Sexplib 4 | open Stdint 5 | 6 | let sexp_of_uint8 t = 7 | Uint8.to_string t 8 | |> Sexp.of_string 9 | 10 | let sexp_of_uint32 t = 11 | Uint32.to_string t 12 | |> Sexp.of_string 13 | 14 | let expect_element expect_name f = 15 | let open Xml in 16 | function 17 | | Element (name, attrs, children) when String.(name = expect_name) -> 18 | f attrs children 19 | | Element (name, _, _) -> 20 | failwithf "Expected <%s> element but saw <%s>" expect_name name () 21 | | PCData str -> 22 | failwithf "Expected <%s> element but saw '%s'" expect_name str () 23 | 24 | let expect_pcdata children = 25 | List.find_map children ~f:( 26 | let open Xml in 27 | function 28 | | PCData str -> Some str 29 | | Element (name, _, _) -> failwithf "Expected PCData but got <%s>" name ()) 30 | |> Option.value ~default:"" 31 | 32 | let require_attribute element name = 33 | function 34 | | Some v -> v 35 | | None -> failwithf "<%s> missing required attribute '%s'" element name () 36 | 37 | let bool_of_xsd_boolean = 38 | (* See http://books.xmlschemata.org/relaxng/ch19-77025.html *) 39 | function 40 | | "true" | "1" -> true 41 | | "false" | "0" -> false 42 | | str -> failwithf "Expected xsd:boolean but got '%s'" str () 43 | -------------------------------------------------------------------------------- /pkg/pkg.ml: -------------------------------------------------------------------------------- 1 | #use "topfind" 2 | #require "topkg-jbuilder" 3 | 4 | open Topkg 5 | 6 | let () = 7 | Topkg_jbuilder.describe ~name:"easy_xlsx" (); 8 | Topkg_jbuilder.describe ~name:"open_packaging" (); 9 | Topkg_jbuilder.describe ~name:"spreadsheetml" (); 10 | -------------------------------------------------------------------------------- /spreadsheetml.descr: -------------------------------------------------------------------------------- 1 | A library to parsing SpreadsheetML (used in Microsoft Excel files) 2 | -------------------------------------------------------------------------------- /spreadsheetml.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | authors: ["Brendan Long "] 3 | maintainer: "self@brendanlong.com" 4 | homepage: "https://github.com/brendanlong/ocaml-ooxml" 5 | dev-repo: "git+https://github.com/brendanlong/ocaml-ooxml.git" 6 | bug-reports: "https://github.com/brendanlong/ocaml-ooxml/issues" 7 | doc: "https://brendanlong.github.io/ocaml-ooxml/doc" 8 | 9 | build: [ 10 | ["dune" "build" "-p" name "-j" jobs] 11 | ["dune" "runtest" "-p" name "-j" jobs] {with-test} 12 | ] 13 | 14 | depends: [ 15 | "ocaml" {>= "4.04.2"} 16 | "open_packaging" 17 | "ppx_jane" 18 | 19 | "bisect_ppx" {build & >= "1.3.0"} 20 | "dune" {build} 21 | 22 | "ounit" {test} 23 | ] 24 | -------------------------------------------------------------------------------- /spreadsheetml/src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name spreadsheetml) 3 | (public_name spreadsheetml) 4 | (libraries open_packaging) 5 | (flags (:standard -w -47)) 6 | (preprocess (pps ppx_jane bisect_ppx -conditional))) 7 | -------------------------------------------------------------------------------- /spreadsheetml/src/shared_string_table.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open Stdint 4 | open Open_packaging.Utils 5 | 6 | module Rich_text = struct 7 | (* Note: We represent text and rich text the same internally. Unformatted 8 | text is just rich text with empty formatting options *) 9 | 10 | module Underline = struct 11 | (* http://www.datypic.com/sc/ooxml/t-ssml_ST_UnderlineValues.html *) 12 | type t = 13 | | Single 14 | | Double 15 | | Single_accounting 16 | | Double_accounting 17 | | None 18 | [@@deriving sexp_of] 19 | 20 | let of_string = function 21 | | "single" -> Single 22 | | "double" -> Double 23 | | "singleAccounting" -> Single_accounting 24 | | "doubleAccounting" -> Double_accounting 25 | | "none" -> None 26 | | str -> failwithf "Expected ST_Underline but got '%s'" str () 27 | end 28 | 29 | module Vertical_align = struct 30 | (* http://www.datypic.com/sc/ooxml/t-ssml_ST_VerticalAlignRun.html *) 31 | type t = 32 | | Baseline 33 | | Superscript 34 | | Subscript 35 | [@@deriving sexp_of] 36 | 37 | let of_string = function 38 | | "baseline" -> Baseline 39 | | "superscript" -> Superscript 40 | | "subscript" -> Subscript 41 | | str -> failwithf "Expected ST_VerticalAlign but got '%s'" str () 42 | end 43 | 44 | type t = 45 | { text : string 46 | ; bold : bool option 47 | ; charset : int32 option 48 | (* ; color : CT_Color *) 49 | ; condense : bool option 50 | ; extend : bool option 51 | ; font : string option 52 | ; font_family : int32 option 53 | ; font_size : float option (* CT_FontSize *) 54 | (* ; font_scheme : CT_FontScheme *) 55 | ; italic : bool option 56 | ; outline : bool option 57 | ; shadow : bool option 58 | ; strikethrough : bool option 59 | ; underline : Underline.t option 60 | ; vertical_align : Vertical_align.t option } 61 | [@@deriving fields, sexp_of] 62 | 63 | let empty = 64 | { text = "" 65 | ; bold = None 66 | ; charset = None 67 | ; condense = None 68 | ; extend = None 69 | ; font = None 70 | ; font_family = None 71 | ; font_size = None 72 | ; italic = None 73 | ; outline = None 74 | ; shadow = None 75 | ; strikethrough = None 76 | ; underline = None 77 | ; vertical_align = None } 78 | 79 | let of_xml = 80 | let open Xml in 81 | function 82 | | Element ("r", _, children) -> 83 | List.fold children ~init:empty ~f:(fun acc -> function 84 | | Element ("rPr", attrs, _) -> 85 | List.fold attrs ~init:acc ~f:(fun acc -> function 86 | | "b", v -> 87 | { acc with bold = Some (bool_of_xsd_boolean v) } 88 | | "charset", v -> 89 | { acc with charset = Some (Int32.of_string v) } 90 | | "condense", v -> 91 | { acc with condense = Some (bool_of_xsd_boolean v) } 92 | | "extend", v -> 93 | { acc with extend = Some (bool_of_xsd_boolean v) } 94 | | "family", v -> 95 | { acc with font_family = Some (Int32.of_string v) } 96 | | "i", v -> 97 | { acc with italic = Some (bool_of_xsd_boolean v) } 98 | | "outline", v -> 99 | { acc with outline = Some (bool_of_xsd_boolean v) } 100 | | "rFont", v -> 101 | { acc with font = Some v } 102 | | "shadow", v -> 103 | { acc with shadow = Some (bool_of_xsd_boolean v) } 104 | | "stike", v -> 105 | { acc with strikethrough = Some (bool_of_xsd_boolean v) } 106 | | "sz", v -> 107 | { acc with font_size = Some (Float.of_string v) } 108 | | "u", v -> 109 | { acc with underline = Some (Underline.of_string v) } 110 | | "vertAlign", v -> 111 | { acc with vertical_align = Some (Vertical_align.of_string v) } 112 | | _ -> acc) 113 | | Element ("t", _, children) -> 114 | { acc with text = expect_pcdata children } 115 | | _ -> acc) 116 | |> Option.some 117 | | Element ("t", _, children) -> 118 | Some { empty with text = expect_pcdata children } 119 | | _ -> None 120 | end 121 | 122 | module String_item = struct 123 | type t = Rich_text.t list 124 | [@@deriving sexp_of] 125 | 126 | let to_string t = 127 | List.map t ~f:Rich_text.text 128 | |> String.concat ~sep:"" 129 | 130 | let of_xml = function 131 | | Xml.Element ("si", _, children) -> 132 | List.filter_map children ~f:Rich_text.of_xml 133 | |> Option.some 134 | | _ -> None 135 | end 136 | 137 | type t = String_item.t list 138 | [@@deriving sexp_of] 139 | 140 | let to_string_array t = 141 | List.map t ~f:String_item.to_string 142 | |> List.to_array 143 | 144 | let of_xml = 145 | expect_element "sst" (fun _ -> 146 | List.filter_map ~f:String_item.of_xml) 147 | -------------------------------------------------------------------------------- /spreadsheetml/src/shared_string_table.mli: -------------------------------------------------------------------------------- 1 | module Rich_text : sig 2 | module Underline : sig 3 | type t = 4 | | Single 5 | | Double 6 | | Single_accounting 7 | | Double_accounting 8 | | None 9 | [@@deriving sexp_of] 10 | 11 | val of_string : string -> t 12 | end 13 | 14 | module Vertical_align : sig 15 | type t = 16 | | Baseline 17 | | Superscript 18 | | Subscript 19 | [@@deriving sexp_of] 20 | 21 | val of_string : string -> t 22 | end 23 | 24 | type t = 25 | { text : string 26 | ; bold : bool option 27 | ; charset : int32 option 28 | ; condense : bool option 29 | ; extend : bool option 30 | ; font : string option 31 | ; font_family : int32 option 32 | ; font_size : float option 33 | ; italic : bool option 34 | ; outline : bool option 35 | ; shadow : bool option 36 | ; strikethrough : bool option 37 | ; underline : Underline.t option 38 | ; vertical_align : Vertical_align.t option } 39 | [@@deriving fields, sexp_of] 40 | 41 | val empty : t 42 | 43 | val of_xml : Xml.xml -> t option 44 | end 45 | 46 | module String_item : sig 47 | type t = Rich_text.t list 48 | [@@deriving sexp_of] 49 | 50 | val to_string : t -> string 51 | 52 | val of_xml : Xml.xml -> t option 53 | end 54 | 55 | type t = String_item.t list 56 | [@@deriving sexp_of] 57 | 58 | val to_string_array : t -> string array 59 | 60 | val of_xml : Xml.xml -> t 61 | -------------------------------------------------------------------------------- /spreadsheetml/src/spreadsheetml.ml: -------------------------------------------------------------------------------- 1 | (* See ECMA 376 Part 4, Section 3 - "SpreadsheetML Reference Material" *) 2 | 3 | module Shared_string_table = Shared_string_table 4 | module Styles = Styles 5 | module Workbook = Workbook 6 | module Worksheet = Worksheet 7 | -------------------------------------------------------------------------------- /spreadsheetml/src/styles.ml: -------------------------------------------------------------------------------- 1 | (* 3.8 Styles *) 2 | open Base 3 | open Stdint 4 | open Open_packaging.Utils 5 | 6 | module Format = struct 7 | (* 3.8.45 xf (Format) *) 8 | (* TODO: Add and children *) 9 | type t = 10 | { apply_alignment : bool option 11 | ; apply_border : bool option 12 | ; apply_fill : bool option 13 | ; apply_font : bool option 14 | ; apply_number_format : bool option 15 | ; apply_protection : bool option 16 | ; border_id : uint32 option 17 | ; fill_id : uint32 option 18 | ; font_id : uint32 option 19 | ; number_format_id : uint32 option 20 | ; pivot_button : bool 21 | ; quote_prefix : bool 22 | ; format_id : uint32 option } 23 | [@@deriving fields, sexp_of] 24 | 25 | let default = 26 | { apply_alignment = None 27 | ; apply_border = None 28 | ; apply_fill = None 29 | ; apply_font = None 30 | ; apply_number_format = None 31 | ; apply_protection = None 32 | ; border_id = None 33 | ; fill_id = None 34 | ; font_id = None 35 | ; number_format_id = None 36 | ; pivot_button = false 37 | ; quote_prefix = false 38 | ; format_id = None } 39 | 40 | let of_xml = function 41 | | Xml.Element ("xf", attrs, _) -> 42 | List.fold attrs ~init:default ~f:(fun acc -> function 43 | | "applyAlignment", v -> 44 | { acc with apply_alignment = Some (bool_of_xsd_boolean v) } 45 | | "applyBorder", v -> 46 | { acc with apply_border = Some (bool_of_xsd_boolean v) } 47 | | "applyFill", v -> 48 | { acc with apply_fill = Some (bool_of_xsd_boolean v) } 49 | | "applyFont", v -> 50 | { acc with apply_font = Some (bool_of_xsd_boolean v) } 51 | | "applyNumberFormat", v -> 52 | { acc with apply_number_format = Some (bool_of_xsd_boolean v) } 53 | | "applyProtection", v -> 54 | { acc with apply_protection = Some (bool_of_xsd_boolean v) } 55 | | "borderId", v -> 56 | { acc with border_id = Some (Uint32.of_string v) } 57 | | "fillId", v -> 58 | { acc with fill_id = Some (Uint32.of_string v) } 59 | | "fontId", v -> 60 | { acc with font_id = Some (Uint32.of_string v) } 61 | | "numFmtId", v -> 62 | { acc with number_format_id = Some (Uint32.of_string v) } 63 | | "pivotButton", v -> 64 | { acc with pivot_button = bool_of_xsd_boolean v } 65 | | "quotePrefix", v -> 66 | { acc with quote_prefix = bool_of_xsd_boolean v } 67 | | "xfId", v -> 68 | { acc with format_id = Some (Uint32.of_string v) } 69 | | _ -> acc) 70 | |> Option.some 71 | | _ -> None 72 | end 73 | 74 | module Number_format = struct 75 | (* 3.8.30 numFmt (Number Format) *) 76 | type t = 77 | { id : uint32 78 | ; format : string } 79 | [@@deriving fields, sexp_of] 80 | 81 | let of_xml = function 82 | | Xml.Element ("numFmt", attrs, _) -> 83 | let id = ref None in 84 | let format = ref None in 85 | List.iter attrs ~f:(function 86 | | "numFmtId", v -> id := Some (Uint32.of_string v) 87 | | "formatCode", v -> format := Some v 88 | | _ -> ()); 89 | let id = require_attribute "numFmt" "numFmtId" !id in 90 | let format = require_attribute "numFmt" "formatCode" !format in 91 | Some { id ; format } 92 | | _ -> None 93 | end 94 | 95 | type t = 96 | { cell_formats : Format.t list 97 | ; formatting_records : Format.t list 98 | ; number_formats : Number_format.t list } 99 | [@@deriving fields, sexp_of] 100 | 101 | let empty = 102 | { cell_formats = [] 103 | ; formatting_records = [] 104 | ; number_formats = [] } 105 | 106 | let of_xml = 107 | expect_element "styleSheet" (fun _ -> 108 | List.fold ~init:empty ~f:(fun acc -> 109 | let open Xml in 110 | function 111 | | Element ("cellStyleXfs", _, children) -> 112 | let formatting_records = List.filter_map children ~f:Format.of_xml in 113 | { acc with formatting_records } 114 | | Element ("cellXfs", _, children) -> 115 | let cell_formats = List.filter_map children ~f:Format.of_xml in 116 | { acc with cell_formats } 117 | | Element ("numFmts", _, children) -> 118 | let number_formats = 119 | List.filter_map children ~f:Number_format.of_xml in 120 | { acc with number_formats } 121 | | _ -> acc)) 122 | -------------------------------------------------------------------------------- /spreadsheetml/src/styles.mli: -------------------------------------------------------------------------------- 1 | open Stdint 2 | 3 | module Format : sig 4 | type t = 5 | { apply_alignment : bool option 6 | ; apply_border : bool option 7 | ; apply_fill : bool option 8 | ; apply_font : bool option 9 | ; apply_number_format : bool option 10 | ; apply_protection : bool option 11 | ; border_id : uint32 option 12 | ; fill_id : uint32 option 13 | ; font_id : uint32 option 14 | ; number_format_id : uint32 option 15 | ; pivot_button : bool 16 | ; quote_prefix : bool 17 | ; format_id : uint32 option } 18 | [@@deriving fields, sexp_of] 19 | 20 | val default : t 21 | 22 | val of_xml : Xml.xml -> t option 23 | end 24 | 25 | module Number_format : sig 26 | type t = 27 | { id : uint32 28 | ; format : string } 29 | [@@deriving fields, sexp_of] 30 | 31 | val of_xml : Xml.xml -> t option 32 | end 33 | 34 | type t = 35 | { cell_formats : Format.t list 36 | ; formatting_records : Format.t list 37 | ; number_formats : Number_format.t list } 38 | [@@deriving fields, sexp_of] 39 | 40 | val empty : t 41 | 42 | val of_xml : Xml.xml -> t 43 | -------------------------------------------------------------------------------- /spreadsheetml/src/workbook.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Base.Printf 3 | open Stdint 4 | open Open_packaging.Utils 5 | 6 | module Book_view = struct 7 | module Visibility = struct 8 | type t = 9 | | Hidden 10 | | Very_hidden 11 | | Visible 12 | [@@deriving sexp_of] 13 | 14 | let of_string = 15 | function 16 | | "hidden" -> Hidden 17 | | "veryHidden" -> Very_hidden 18 | | "visible" -> Visible 19 | | str -> failwithf "Expected ST_Visibility but got '%s'" str () 20 | end 21 | 22 | (* 3.2.30 workbookView *) 23 | type t = 24 | { active_tab : uint32 25 | ; autofilter_date_grouping : bool 26 | ; first_sheet : uint32 27 | ; minimized : bool 28 | ; show_horizontal_scroll : bool 29 | ; show_vertical_scroll : bool 30 | ; tab_ratio : uint32 31 | ; visibility : Visibility.t 32 | ; window_height : uint32 option 33 | ; window_width : uint32 option 34 | ; x_window : int32 option 35 | ; y_window : int32 option } 36 | [@@deriving fields, sexp_of] 37 | 38 | let default = 39 | { active_tab = Uint32.zero 40 | ; autofilter_date_grouping = true 41 | ; first_sheet = Uint32.zero 42 | ; minimized = false 43 | ; show_horizontal_scroll = true 44 | ; show_vertical_scroll = true 45 | ; tab_ratio = Uint32.of_int 600 46 | ; visibility = Visibility.Visible 47 | ; window_height = None 48 | ; window_width = None 49 | ; x_window = None 50 | ; y_window = None } 51 | 52 | let of_xml = function 53 | | Xml.Element ("bookView", attrs, _) -> 54 | List.fold attrs ~init:default ~f:(fun t -> 55 | function 56 | | "activeTab", v -> 57 | { t with active_tab = Uint32.of_string v } 58 | | "autoFilterDateGrouping", v -> 59 | { t with autofilter_date_grouping = bool_of_xsd_boolean v } 60 | | "firstSheet", v -> 61 | { t with first_sheet = Uint32.of_string v } 62 | | "minimized", v -> 63 | { t with minimized = bool_of_xsd_boolean v } 64 | | "showHorizontalScroll", v -> 65 | { t with show_horizontal_scroll = bool_of_xsd_boolean v } 66 | | "showVerticalScroll", v -> 67 | { t with show_vertical_scroll = bool_of_xsd_boolean v } 68 | | "tabRatio", v -> 69 | { t with tab_ratio = Uint32.of_string v } 70 | | "visibility", v -> 71 | { t with visibility = Visibility.of_string v } 72 | | "windowHeight", v -> 73 | { t with window_height = Some (Uint32.of_string v) } 74 | | "windowWidth", v -> 75 | { t with window_width = Some (Uint32.of_string v) } 76 | | "xWindow", v -> 77 | { t with x_window = Some (Int32.of_string v) } 78 | | "yWindow", v -> 79 | { t with y_window = Some (Int32.of_string v) } 80 | | _ -> t) 81 | |> Option.some 82 | | _ -> None 83 | end 84 | 85 | module Sheet = struct 86 | (* 3.2.19 sheet (Sheet information) *) 87 | 88 | module State = struct 89 | type t = 90 | | Hidden 91 | | Very_hidden 92 | | Visible 93 | [@@deriving sexp_of] 94 | 95 | let of_string = 96 | function 97 | | "hidden" -> Hidden 98 | | "veryHidden" -> Very_hidden 99 | | "visible" -> Visible 100 | | str -> failwithf "Expected ST_SheetState but got '%s'" str () 101 | end 102 | 103 | type t = 104 | { id : string (* ST_RelationshipId *) 105 | ; name : string 106 | ; sheet_id : uint32 107 | ; state : State.t } 108 | [@@deriving fields, sexp_of] 109 | 110 | let of_xml = function 111 | | Xml.Element ("sheet", attrs, _) -> 112 | let name = ref None in 113 | let sheet_id = ref None in 114 | let state = ref State.Visible in 115 | let id = ref None in 116 | List.iter attrs ~f:(function 117 | | "name", v -> name := Some v 118 | | "sheetId", v -> sheet_id := Some (Uint32.of_string v) 119 | | "state", v -> state := State.of_string v 120 | | "r:id", v -> id := Some v 121 | | _ -> ()); 122 | let name = require_attribute "sheet" "name" !name in 123 | let sheet_id = require_attribute "sheet" "sheetId" !sheet_id in 124 | let id = require_attribute "sheet" "r:id" !id in 125 | Some { name ; sheet_id ; state = !state ; id } 126 | | _ -> None 127 | end 128 | 129 | type t = 130 | { book_views : Book_view.t list 131 | (* 132 | ; calculation_properties : Calculation_property.t list 133 | ; custom_workbook_views : Custom_workbook_view.t list 134 | ; defined_names : Defined_name.t list 135 | ; external_references : External_reference.t list 136 | ( * TODO: extLst - Future Feature Storage Area * ) 137 | ; file_recovery_properties : File_recovery_property.t list 138 | ; file_sharing : File_sharing.t list 139 | ; file_version : File_version.t list 140 | ; function_groups : Function_group.t list 141 | ; embedded_object_sizes : Embedded_object_size.t list 142 | ; pivot_caches : Pivot_cache.t list *) 143 | ; sheets : Sheet.t list (* 3.2.20 sheets *) 144 | (* 145 | ; smart_tag_properties : Smart_tag_property.t list 146 | ; smart_tag_types : Smart_tag_type.t list 147 | ; web_publishing_properties : Web_publishing_properties.t list 148 | ; web_publish_objects : Web_publish_object.t list 149 | ; workbook_properties : Workbook_property.t list 150 | ; workbook_protection : Workbook_protection.t list *) } 151 | [@@deriving fields, sexp_of] 152 | 153 | let empty = 154 | { book_views = [] 155 | ; sheets = [] } 156 | 157 | let of_xml = 158 | expect_element "workbook" (fun _ -> 159 | List.fold ~init:empty ~f:(fun t -> 160 | let open Xml in 161 | function 162 | | Element ("bookViews", _, children) -> 163 | let book_views = List.filter_map children ~f:Book_view.of_xml in 164 | { t with book_views } 165 | | Element ("sheets", _, children) -> 166 | let sheets = List.filter_map children ~f:Sheet.of_xml in 167 | { t with sheets } 168 | | _ -> t)) 169 | -------------------------------------------------------------------------------- /spreadsheetml/src/workbook.mli: -------------------------------------------------------------------------------- 1 | open Stdint 2 | 3 | module Book_view : sig 4 | module Visibility : sig 5 | type t = 6 | | Hidden 7 | | Very_hidden 8 | | Visible 9 | [@@deriving sexp_of] 10 | 11 | val of_string : string -> t 12 | end 13 | 14 | type t = 15 | { active_tab : uint32 16 | ; autofilter_date_grouping : bool 17 | ; first_sheet : uint32 18 | ; minimized : bool 19 | ; show_horizontal_scroll : bool 20 | ; show_vertical_scroll : bool 21 | ; tab_ratio : uint32 22 | ; visibility : Visibility.t 23 | ; window_height : uint32 option 24 | ; window_width : uint32 option 25 | ; x_window : int32 option 26 | ; y_window : int32 option } 27 | [@@deriving fields, sexp_of] 28 | 29 | val default : t 30 | 31 | val of_xml : Xml.xml -> t option 32 | end 33 | 34 | module Sheet : sig 35 | 36 | module State : sig 37 | type t = 38 | | Hidden 39 | | Very_hidden 40 | | Visible 41 | [@@deriving sexp_of] 42 | 43 | val of_string : string -> t 44 | end 45 | 46 | type t = 47 | { id : string (** ST_RelationshipId *) 48 | ; name : string 49 | ; sheet_id : uint32 50 | ; state : State.t } 51 | [@@deriving fields, sexp_of] 52 | 53 | val of_xml : Xml.xml -> t option 54 | end 55 | 56 | type t = 57 | { book_views : Book_view.t list 58 | ; sheets : Sheet.t list (** 3.2.20 sheets *) } 59 | [@@deriving fields, sexp_of] 60 | 61 | val empty : t 62 | 63 | val of_xml : Xml.xml -> t 64 | -------------------------------------------------------------------------------- /spreadsheetml/src/worksheet.ml: -------------------------------------------------------------------------------- 1 | (* 3.3 Worksheets *) 2 | open Base 3 | open Base.Printf 4 | open Stdint 5 | open Open_packaging.Utils 6 | 7 | module Cell = struct 8 | module Type = struct 9 | (* 3.18.12 ST_CellType (Cell Type) *) 10 | type t = 11 | | Boolean 12 | | Error 13 | | Inline_string 14 | | Number 15 | | Shared_string 16 | | Formula_string 17 | [@@deriving sexp_of] 18 | 19 | let of_string = function 20 | | "b" -> Boolean 21 | | "e" -> Error 22 | | "inlineStr" -> Inline_string 23 | | "n" -> Number 24 | | "s" -> Shared_string 25 | | "str" -> Formula_string 26 | | str -> failwithf "Expected ST_CellType but got '%s'" str () 27 | end 28 | 29 | module Value = struct 30 | (* 3.3.1.93 v (Cell Value) *) 31 | type t = 32 | | Inline of string 33 | | Rich_inline of Shared_string_table.String_item.t 34 | | Shared of uint32 35 | [@@deriving sexp_of] 36 | 37 | let to_string ~shared_strings = function 38 | | Inline v -> v 39 | | Rich_inline v -> 40 | Shared_string_table.String_item.to_string v 41 | | Shared n -> 42 | let n = Uint32.to_int n in 43 | shared_strings.(n) 44 | |> Shared_string_table.String_item.to_string 45 | 46 | let of_xml ~data_type el = 47 | let open Xml in 48 | match data_type, el with 49 | | _, Element ("is", _, children) -> 50 | let value = 51 | List.filter_map children ~f:Shared_string_table.Rich_text.of_xml in 52 | Some (Rich_inline value) 53 | | Type.Shared_string, Element ("v", _, children) -> 54 | Some (Shared (Uint32.of_string (expect_pcdata children))) 55 | | _, Element ("v", _, children) -> 56 | Some (Inline (expect_pcdata children)) 57 | | _ -> None 58 | end 59 | 60 | (* 3.3.1.3 c (Cell) *) 61 | type t = 62 | { reference : string option 63 | ; formula : string option 64 | ; value : Value.t option 65 | ; cell_metadata_index : uint32 66 | ; show_phonetic : bool 67 | ; style_index : uint32 68 | ; data_type : Type.t 69 | ; value_metadata_index : uint32 } 70 | [@@deriving fields, sexp_of] 71 | 72 | let column { reference ; _ } = 73 | (Option.value_exn reference ~here:[%here] 74 | |> String.to_list 75 | |> List.take_while ~f:Char.is_alpha 76 | |> List.map ~f:Char.uppercase 77 | |> List.map ~f:(fun c -> Char.to_int c - Char.to_int 'A' + 1) 78 | |> List.fold ~init:0 ~f:(fun acc n -> acc * 26 + n)) 79 | - 1 80 | 81 | let to_string ~shared_strings { value ; _ } = 82 | Option.map value ~f:(Value.to_string ~shared_strings) 83 | |> Option.value ~default:"" 84 | 85 | let default = 86 | { reference = None 87 | ; formula = None 88 | ; value = None 89 | ; cell_metadata_index = Uint32.zero 90 | ; show_phonetic = false 91 | ; style_index = Uint32.zero 92 | ; data_type = Type.Number 93 | ; value_metadata_index = Uint32.zero } 94 | 95 | let of_xml = 96 | let open Xml in 97 | function 98 | | Element ("c", attrs, children) -> 99 | let with_attrs = 100 | List.fold attrs ~init:default ~f:(fun acc -> function 101 | | "r", v -> 102 | { acc with reference = Some v } 103 | | "s", v -> 104 | { acc with style_index = Uint32.of_string v } 105 | | "t", v -> 106 | { acc with data_type = Type.of_string v } 107 | | "cm", v -> 108 | { acc with cell_metadata_index = Uint32.of_string v } 109 | | "vm", v -> 110 | { acc with value_metadata_index = Uint32.of_string v } 111 | | "ph", v -> 112 | { acc with show_phonetic = bool_of_xsd_boolean v } 113 | | _ -> acc) 114 | in 115 | List.fold children ~init:with_attrs ~f:(fun acc -> function 116 | | Element ("f", _, children) -> 117 | { acc with formula = Some (expect_pcdata children) } 118 | | el -> 119 | let value = Value.of_xml ~data_type:with_attrs.data_type el in 120 | { acc with value = Option.first_some value acc.value }) 121 | |> Option.some 122 | | _ -> None 123 | end 124 | 125 | module Row = struct 126 | (* 3.3.1.71 row (Row) *) 127 | type t = 128 | { cells : Cell.t list 129 | ; collapsed : bool 130 | ; custom_format : bool 131 | ; custom_height : bool 132 | ; hidden : bool 133 | ; height : float option 134 | ; outline_level : uint8 135 | ; show_phonetic : bool 136 | ; row_index : uint32 option 137 | ; style_index : uint32 138 | ; thick_bottom_border : bool 139 | ; thick_top_border : bool } 140 | [@@deriving fields, sexp_of] 141 | 142 | let default = 143 | { cells = [] 144 | ; collapsed = false 145 | ; custom_format = false 146 | ; custom_height = false 147 | ; hidden = false 148 | ; height = None 149 | ; outline_level = Uint8.zero 150 | ; show_phonetic = false 151 | ; row_index = None 152 | ; style_index = Uint32.zero 153 | ; thick_bottom_border = false 154 | ; thick_top_border = false } 155 | 156 | let of_xml = function 157 | | Xml.Element ("row", attrs, children) -> 158 | let cells = List.filter_map children ~f:Cell.of_xml in 159 | List.fold attrs ~init:{ default with cells } ~f:(fun acc -> function 160 | | "collapsed", v -> 161 | { acc with collapsed = bool_of_xsd_boolean v } 162 | | "customFormat", v -> 163 | { acc with custom_format = bool_of_xsd_boolean v } 164 | | "customHeight", v -> 165 | { acc with custom_height = bool_of_xsd_boolean v } 166 | | "hidden", v -> 167 | { acc with hidden = bool_of_xsd_boolean v } 168 | | "ht", v -> 169 | { acc with height = Some (Float.of_string v) } 170 | | "outlineLevel", v -> 171 | { acc with outline_level = Uint8.of_string v } 172 | | "ph", v -> 173 | { acc with show_phonetic = bool_of_xsd_boolean v } 174 | | "r", v -> 175 | { acc with row_index = Some (Uint32.of_string v) } 176 | | "s", v -> 177 | { acc with style_index = Uint32.of_string v } 178 | | "thickBot", v -> 179 | { acc with thick_bottom_border = bool_of_xsd_boolean v } 180 | | "thickTop", v -> 181 | { acc with thick_top_border = bool_of_xsd_boolean v } 182 | | _ -> acc) 183 | |> Option.some 184 | | _ -> None 185 | end 186 | 187 | module Column = struct 188 | (* 3.3.1.12 col (Column Width & Formatting) *) 189 | type t = 190 | { best_fit : bool 191 | ; collapsed : bool 192 | ; custom_width : bool 193 | ; hidden : bool 194 | ; max : uint32 195 | ; min : uint32 196 | ; outline_level : uint8 197 | ; show_phonetic : bool 198 | ; default_style : uint32 199 | ; width : float option } 200 | [@@deriving fields, sexp_of] 201 | 202 | let of_xml = function 203 | | Xml.Element ("col", attrs, _) -> 204 | let best_fit = ref false in 205 | let collapsed = ref false in 206 | let custom_width = ref false in 207 | let hidden = ref false in 208 | let max = ref None in 209 | let min = ref None in 210 | let outline_level = ref Uint8.zero in 211 | let show_phonetic = ref false in 212 | let default_style = ref Uint32.zero in 213 | let width = ref None in 214 | List.iter attrs ~f:(function 215 | | "bestFit", v -> 216 | best_fit := bool_of_xsd_boolean v 217 | | "collapsed", v -> 218 | collapsed := bool_of_xsd_boolean v 219 | | "customWidth", v -> 220 | custom_width := bool_of_xsd_boolean v 221 | | "hidden", v -> 222 | hidden := bool_of_xsd_boolean v 223 | | "max", v -> 224 | max := Some (Uint32.of_string v) 225 | | "min", v -> 226 | min := Some (Uint32.of_string v) 227 | | "outlineLevel", v -> 228 | outline_level := Uint8.of_string v 229 | | "phonetic", v-> 230 | show_phonetic := bool_of_xsd_boolean v 231 | | "style", v -> 232 | default_style := Uint32.of_string v 233 | | "width", v -> 234 | width := Some (Float.of_string v) 235 | | _ -> ()); 236 | let max = require_attribute "col" "max" !max in 237 | let min = require_attribute "col" "min" !min in 238 | Some { best_fit = !best_fit 239 | ; collapsed = !collapsed 240 | ; custom_width = !custom_width 241 | ; hidden = !hidden 242 | ; max 243 | ; min 244 | ; outline_level = !outline_level 245 | ; show_phonetic = !show_phonetic 246 | ; default_style = !default_style 247 | ; width = !width } 248 | | _ -> None 249 | end 250 | 251 | type t = 252 | { columns : Column.t list 253 | ; rows : Row.t list } 254 | [@@deriving fields, sexp_of] 255 | 256 | let default = 257 | { columns = [] 258 | ; rows = [] } 259 | 260 | let of_xml = 261 | expect_element "worksheet" (fun _ -> 262 | let open Xml in 263 | List.fold ~init:default ~f:(fun acc -> function 264 | | Element ("cols", _, children) -> 265 | { acc with columns = List.filter_map children ~f:Column.of_xml } 266 | | Element ("sheetData", _, children) -> 267 | { acc with rows = List.filter_map children ~f:Row.of_xml } 268 | | _ -> acc)) 269 | --------------------------------------------------------------------------------