171 lines
5.3 KiB
Clojure
Vendored
171 lines
5.3 KiB
Clojure
Vendored
(ns tech.v3.libs.fastexcel
|
|
"Parse a dataset in xlsx format. This namespace auto-registers a handler for
|
|
the 'xlsx' file type so that when using ->dataset, `xlsx` will automatically map to
|
|
`(first (workbook->datasets))`.
|
|
|
|
Note that this namespace does **not** auto-register a handler for the `xls` file type.
|
|
`xls` is handled by the poi namespace.
|
|
|
|
|
|
If you have an xlsx file that contains multiple sheets and you want a dataset
|
|
out of each sheet you have to use `workbook->datasets` as opposed to the higher level
|
|
`->dataset` operator.
|
|
|
|
This dependency has a known conflict with cognitec's aws library - see [issue 283](https://github.com/techascent/tech.ml.dataset/issues/283).
|
|
|
|
Required Dependencies:
|
|
|
|
```clojure
|
|
[org.dhatim/fastexcel-reader \"0.12.8\" :exclusions [org.apache.poi/poi-ooxml]]
|
|
```"
|
|
(:require [tech.v3.io :as io]
|
|
[tech.v3.datatype.protocols :as dtype-proto]
|
|
[tech.v3.datatype :as dtype]
|
|
[tech.v3.datatype.errors :as errors]
|
|
[tech.v3.dataset.io.spreadsheet :as parse-spreadsheet]
|
|
[tech.v3.dataset.io :as ds-io])
|
|
(:import [org.dhatim.fastexcel.reader ReadableWorkbook
|
|
Sheet Row Cell CellType]
|
|
[tech.v3.dataset Spreadsheet$Workbook Spreadsheet$Sheet
|
|
Spreadsheet$Row Spreadsheet$Cell]))
|
|
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
|
|
(defn- cell-type->keyword
|
|
[^CellType cell-type]
|
|
(condp = cell-type
|
|
CellType/EMPTY :none
|
|
CellType/NUMBER :float64
|
|
CellType/BOOLEAN :boolean
|
|
CellType/STRING :string))
|
|
|
|
|
|
(defn- formula-type?
|
|
[cell-type]
|
|
(or (= cell-type CellType/FORMULA)
|
|
(= cell-type CellType/ERROR)))
|
|
|
|
|
|
(defn- try-parse-double
|
|
[^Cell cell]
|
|
(try
|
|
(Double/parseDouble (.getRawValue cell))
|
|
(catch Exception _e
|
|
(.getRawValue cell))))
|
|
|
|
|
|
(defn- wrap-cell
|
|
[^Cell cell]
|
|
(reify
|
|
dtype-proto/PElemwiseDatatype
|
|
(elemwise-datatype [this]
|
|
(let [cell-type (.getType cell)]
|
|
(if (formula-type? cell-type)
|
|
(if (number? (try-parse-double cell))
|
|
:float64
|
|
:string)
|
|
(cell-type->keyword cell-type))))
|
|
Spreadsheet$Cell
|
|
(getColumnNum [this] (.getColumnIndex cell))
|
|
(missing [this] (= :none (dtype/get-datatype this)))
|
|
(value [this]
|
|
(if (formula-type? (.getType cell))
|
|
(try-parse-double cell)
|
|
(case (dtype-proto/elemwise-datatype this)
|
|
:none nil
|
|
:string (.getRawValue cell)
|
|
:boolean (.asBoolean cell)
|
|
(double (.asNumber cell)))))
|
|
(doubleValue [this]
|
|
(if (formula-type? (.getType cell))
|
|
(Double/parseDouble (.getRawValue cell))
|
|
(double (.asNumber cell))))
|
|
(boolValue [this] (.asBoolean cell))))
|
|
|
|
|
|
(defn- wrap-row
|
|
[^Row row]
|
|
(reify
|
|
Spreadsheet$Row
|
|
(getRowNum [this] (dec (.getRowNum row)))
|
|
(iterator [this]
|
|
(let [iter (-> (.stream row)
|
|
(.filter (reify
|
|
java.util.function.Predicate
|
|
(test [this val] (not= val nil))))
|
|
(.iterator))]
|
|
(reify java.util.Iterator
|
|
;;In some cases hasNext throws.
|
|
(hasNext [this] (try (.hasNext iter)
|
|
(catch Exception _e false)))
|
|
(next [this] (wrap-cell (.next iter))))))))
|
|
|
|
(defn- wrap-sheet
|
|
[^Sheet sheet]
|
|
(reify
|
|
Spreadsheet$Sheet
|
|
(name [this] (.getName sheet))
|
|
(id [this] (.getId sheet))
|
|
(stableId [this] (.getStableId sheet))
|
|
(iterator [this]
|
|
(let [iter (.iterator (.openStream sheet))]
|
|
(reify java.util.Iterator
|
|
(hasNext [this] (.hasNext iter))
|
|
(next [this] (wrap-row (.next iter))))))))
|
|
|
|
|
|
(defn input->workbook
|
|
"Given an input data source, return an implementation of
|
|
`tech.v3.dataset/Spreadsheet$Workbook`. This interface allows you
|
|
to iterate through sheets without necessarily parsing them.
|
|
Once you have a spreadsheet, use `tech.v3.dataset.io.spreadsheet/sheet->dataset`
|
|
to get a dataset."
|
|
(^Spreadsheet$Workbook [input]
|
|
(input->workbook input {}))
|
|
(^Spreadsheet$Workbook [input _options]
|
|
(let [workbook (ReadableWorkbook. (io/input-stream input))]
|
|
(reify
|
|
Spreadsheet$Workbook
|
|
(close [this] (.close workbook))
|
|
(iterator [this]
|
|
(let [sheet-iter (.iterator (.getSheets workbook))]
|
|
(reify java.util.Iterator
|
|
(hasNext [this] (.hasNext sheet-iter))
|
|
(next [this] (wrap-sheet (.next sheet-iter))))))))))
|
|
|
|
|
|
(defn workbook->datasets
|
|
"Given a workbook, an string filename or an input stream return a sequence of
|
|
dataset named after the sheets.
|
|
|
|
Options are a subset of the arguments for tech.v3.dataset/->dataset:
|
|
|
|
* `:header-row?`
|
|
* `:num-rows`
|
|
* `:n-initial-skip-rows`
|
|
* `:parser-fn`"
|
|
([input options]
|
|
(let [workbook (input->workbook input options)]
|
|
(try
|
|
(mapv #(parse-spreadsheet/sheet->dataset % options) workbook)
|
|
(finally
|
|
(when-not (identical? input workbook)
|
|
(.close workbook))))))
|
|
([input]
|
|
(workbook->datasets input {})))
|
|
|
|
|
|
(defmethod ds-io/data->dataset :xlsx
|
|
[data options]
|
|
(let [datasets (workbook->datasets data options)
|
|
n-datasets (count datasets)]
|
|
(errors/when-not-errorf
|
|
(== 1 n-datasets)
|
|
(if (== 0 n-datasets)
|
|
"No (%d) datasets found in file"
|
|
"Multiple (%d) datasets found in file")
|
|
n-datasets)
|
|
(first datasets)))
|