Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for reading error cells and non-numeric formula cells #33

Merged
merged 5 commits into from
Mar 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# CHANGELOG for Docjure

## Version 1.9.0

* `read-cell` now works on error cells and non-numeric formula cells without throwing an exception. (All cell types now handled safely).

Error cells return keyword of the error type:

```
:VALUE :DIV0 :CIRCULAR_REF :REF :NUM :NULL :FUNCTION_NOT_IMPLEMENTED :NAME :NA
```

## Version 1.8.0
* Upgraded to use Clojure 1.6 as default Clojure version.
* Upgraded to Apache POI v3.11.
Expand Down Expand Up @@ -48,20 +58,17 @@
* Added named ranges functions add-name! and select-name (contributed by cbaatz).
* Added row style functions set-row-style! and get-row-styles for styling rows (contributed by cbaatz).

## Version 1.4
## Version 1.4
* Introduces cell styling (font control, background colour).
* A more flexible cell-seq (supports sheet, row or collections of these).

## Version 1.3
* Updated semantics for reading blank cells: now they are read as nil (formerly read as empty strings).

## Version 1.2
## Version 1.2

First public release.

## Earlier versions

Earlier versions used internally for projects in Ative in 2009 and 2010.



57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ Docjure makes reading and writing Office documents in Clojure easy.

### Example: Read a Price List spreadsheet

(use 'dk.ative.docjure.spreadsheet)
(use 'dk.ative.docjure.spreadsheet)

;; Load a spreadsheet and read the first two columns from the
;; Load a spreadsheet and read the first two columns from the
;; price list sheet:
(->> (load-workbook "spreadsheet.xlsx")
(select-sheet "Price List")
(select-columns {:A :name, :B :price}))

;=> [{:name "Foo Widget", :price 100}, {:name "Bar Widget", :price 200}]

### Example: Create a spreadsheet
### Example: Create a spreadsheet
This example creates a spreadsheet with a single sheet named "Price List".
It has three rows. We apply a style of yellow background colour and bold font
to the top header row, then save the spreadsheet.

(use 'dk.ative.docjure.spreadsheet)
(use 'dk.ative.docjure.spreadsheet)

;; Create a spreadsheet and save it
(let [wb (create-workbook "Price List"
[["Name" "Price"]
Expand All @@ -34,16 +34,54 @@ to the top header row, then save the spreadsheet.
(set-row-style! header-row (create-cell-style! wb {:background :yellow,
:font {:bold true}}))
(save-workbook! "spreadsheet.xlsx" wb)))



### Example: Handling Error Cells

Given a list of cells in a spreadsheet that may result in errors.

(use 'dk.ative.docjure.spreadsheet)

(def sample-cells (->> (load-workbook "spreadsheet.xlsx")
(sheet-seq)
(mapcat cell-seq)))

sample-cells

;=> (#<XSSFCell 15.0> #<XSSFCell NA()> #<XSSFCell 35.0> #<XSSFCell 13/0> #<XSSFCell 33.0> #<XSSFCell 96.0>)

Reading error cells, or cells that evaluate to an error (e.g. divide by
zero) returns a keyword representing the type of error from
`read-cell`.

(->> sample-cells
(map read-cell))

;=> (15.0 :NA 35.0 :DIV0 33.0 96.0)

How you handle errors will depend on your application. You may want to
replace specific errors with a defualt value and remove others for
example:

(->> sample-cells
(map read-cell)
(map #(get {:DIV0 0.0} % %))
(remove keyword?))

;=> (15.0 35.0 0.0 33.0 96.0)

The following is a list of all possible [error values](https://poi.apache.org/apidocs/org/apache/poi/ss/usermodel/FormulaError.html#enum_constant_summary):

#{:VALUE :DIV0 :CIRCULAR_REF :REF :NUM :NULL :FUNCTION_NOT_IMPLEMENTED :NAME :NA}

### Automatically get the Docjure jar from Clojars

The Docjure jar is distributed on [Clojars](http://clojars.org/dk.ative/docjure).
The Docjure jar is distributed on [Clojars](http://clojars.org/dk.ative/docjure).

If you are using the Leiningen build tool just add this line to the
:dependencies list in project.clj to use it:

[dk.ative/docjure "1.8.0"]
[dk.ative/docjure "1.8.0"]

Remember to issue the 'lein deps' command to download it.

Expand All @@ -56,7 +94,6 @@ Remember to issue the 'lein deps' command to download it.


## Installation

You need to install the Leiningen build tool to build the library.
You can get it here: [Leiningen](http://github.com/technomancy/leiningen)

Expand Down Expand Up @@ -92,7 +129,7 @@ Martin Jul

* Email: [email protected]
* Twitter: mjul
* GitHub: [mjul](https://github.com/mjul)
* GitHub: [mjul](https://github.com/mjul)


## Contributors
Expand Down
40 changes: 23 additions & 17 deletions src/dk/ative/docjure/spreadsheet.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
(org.apache.poi.xssf.usermodel XSSFWorkbook)
(org.apache.poi.hssf.usermodel HSSFWorkbook)
(org.apache.poi.ss.usermodel Workbook Sheet Cell Row
FormulaError
WorkbookFactory DateUtil
IndexedColors CellStyle Font
CellValue Drawing CreationHelper)
(org.apache.poi.ss.util CellReference AreaReference)))

(defmacro assert-type [value expected-type]
`(when-not (isa? (class ~value) ~expected-type)
(throw (IllegalArgumentException.
(throw (IllegalArgumentException.
(format "%s is invalid. Expected %s. Actual type %s, value: %s"
(str '~value) ~expected-type (class ~value) ~value)))))

Expand All @@ -27,29 +28,34 @@
(if date-format?
(DateUtil/getJavaDate (.getNumberValue cv))
(.getNumberValue cv)))
(defmethod read-cell-value Cell/CELL_TYPE_ERROR [^CellValue cv _]
(keyword (.name (FormulaError/forInt (.getErrorValue cv)))))

(defmulti read-cell #(.getCellType ^Cell %))
(defmethod read-cell Cell/CELL_TYPE_BLANK [_] nil)
(defmethod read-cell Cell/CELL_TYPE_STRING [^Cell cell] (.getStringCellValue cell))
(defmethod read-cell Cell/CELL_TYPE_FORMULA [^Cell cell]
(if (DateUtil/isCellDateFormatted cell)
(.getDateCellValue cell)
(let [evaluator (.. cell getSheet getWorkbook
getCreationHelper createFormulaEvaluator)
cv (.evaluate evaluator cell)]
(let [evaluator (.. cell getSheet getWorkbook
getCreationHelper createFormulaEvaluator)
cv (.evaluate evaluator cell)]
(if (and (= Cell/CELL_TYPE_NUMERIC (.getCellType cv))
(DateUtil/isCellDateFormatted cell))
(.getDateCellValue cell)
(read-cell-value cv false))))
(defmethod read-cell Cell/CELL_TYPE_BOOLEAN [^Cell cell] (.getBooleanCellValue cell))
(defmethod read-cell Cell/CELL_TYPE_NUMERIC [^Cell cell]
(if (DateUtil/isCellDateFormatted cell)
(.getDateCellValue cell)
(.getNumericCellValue cell)))
(defmethod read-cell Cell/CELL_TYPE_ERROR [^Cell cell]
(keyword (.name (FormulaError/forInt (.getErrorCellValue cell)))))

(defn load-workbook
"Load an Excel .xls or .xlsx workbook from a file."
[^String filename]
(with-open [stream (FileInputStream. filename)]
(WorkbookFactory/create stream)))

(defn save-workbook!
"Save the workbook into a file."
[^String filename ^Workbook workbook]
Expand Down Expand Up @@ -134,9 +140,9 @@
{new-key (read-cell cell)})))

(defn select-columns [column-map ^Sheet sheet]
"Takes two arguments: column hashmap and a sheet. The column hashmap
"Takes two arguments: column hashmap and a sheet. The column hashmap
specifies the mapping from spreadsheet columns dictionary keys:
its keys are the spreadsheet column names and the values represent
its keys are the spreadsheet column names and the values represent
the names they are mapped to in the result.

For example, to select columns A and C as :first and :third from the sheet
Expand Down Expand Up @@ -234,7 +240,7 @@
workbook))

;******************************************************
; helpers for font and style creation
; helpers for font and style creation


(defn color-index
Expand Down Expand Up @@ -328,12 +334,12 @@
clojure.lang.PersistentArrayMap
(set-font [this ^CellStyle style workbook]
(.setFont style (create-font! workbook this)))
(as-font [this workbook] (create-font! workbook this))
(as-font [this workbook] (create-font! workbook this))
org.apache.poi.ss.usermodel.Font
(set-font [this ^CellStyle style _] (.setFont style this))
(as-font [this _] this)
(as-font [this _] this)
org.apache.poi.xssf.usermodel.XSSFCellStyle
(get-font [this _] (.getFont this))
(get-font [this _] (.getFont this))
org.apache.poi.hssf.usermodel.HSSFCellStyle
(get-font [this workbook] (.getFont this workbook)))

Expand Down Expand Up @@ -383,8 +389,8 @@
border-left (.setBorderLeft cs (border border-left))
border-right (.setBorderRight cs (border border-right))
border-top (.setBorderTop cs (border border-top))
border-bottom (.setBorderBottom cs (border border-bottom)))
cs)))
border-bottom (.setBorderBottom cs (border border-bottom)))
cs)))

(defn set-cell-style!
"Apply a style to a cell.
Expand All @@ -398,7 +404,7 @@

(defn set-cell-comment!
"Creates a cell comment-box that displays a comment string
when the cell is hovered over. Returns the cell.
when the cell is hovered over. Returns the cell.

Options:

Expand All @@ -407,7 +413,7 @@
:height (int - height of comment-box in rows; default 2 rows)

Example:

(set-cell-comment! acell \"This comment should\nspan two lines.\"
:width 2 :font {:bold true :size 12 :color blue})
"
Expand Down
30 changes: 15 additions & 15 deletions test/dk/ative/docjure/spreadsheet_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (add-sheet! "not-a-workbook" "sheet-name"))))))

(deftest create-workbook-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1" "B1" "C1"]
["A2" "B2" "C2"]]
workbook (create-workbook sheet-name sheet-data)]
Expand Down Expand Up @@ -61,7 +61,7 @@
(is (thrown-with-msg? IllegalArgumentException #"sheet.*" (add-rows! "not-a-sheet" [[1 2 3]])))))

(deftest remove-row!-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1" "B1" "C1"]
["A2" "B2" "C2"]]
workbook (create-workbook sheet-name sheet-data)
Expand All @@ -71,13 +71,13 @@
(is (thrown-with-msg? IllegalArgumentException #"sheet.*" (remove-row! "not-a-sheet" (first (row-seq sheet)))))
(is (thrown-with-msg? IllegalArgumentException #"row.*" (remove-row! sheet "not-a-row"))))
(testing "Should remove row."
(do
(do
(is (= sheet (remove-row! sheet first-row)))
(is (= 1 (.getPhysicalNumberOfRows sheet)))
(is (= [{:A "A2", :B "B2", :C "C2"}] (select-columns {:A :A, :B :B :C :C} sheet)))))))

(deftest remove-all-row!-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1" "B1" "C1"]
["A2" "B2" "C2"]]
workbook (create-workbook sheet-name sheet-data)
Expand Down Expand Up @@ -127,7 +127,7 @@


(deftest set-cell!-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1"]]
workbook (create-workbook sheet-name sheet-data)
a1 (-> workbook (.getSheetAt 0) (.getRow 0) (.getCell 0))]
Expand Down Expand Up @@ -157,7 +157,7 @@


(deftest sheet-seq-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["foo" "bar"]]]
(testing "Empty workbook"
(let [workbook (XSSFWorkbook.)
Expand All @@ -180,7 +180,7 @@
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (sheet-seq "not-a-workbook"))))))

(deftest row-seq-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1" "B1"] ["A2" "B2"]]
workbook (create-workbook sheet-name sheet-data)
sheet (select-sheet sheet-name workbook)]
Expand All @@ -189,7 +189,7 @@
(is (= 2 (count actual)))))))

(deftest cell-seq-test
(let [sheet-name "Sheet 1"
(let [sheet-name "Sheet 1"
sheet-data [["A1" "B1"] ["A2" "B2"]]
workbook (create-workbook sheet-name sheet-data)
sheet (select-sheet sheet-name workbook)]
Expand All @@ -216,7 +216,7 @@


(deftest sheet-name-test
(let [name "Sheet 1"
(let [name "Sheet 1"
data [["foo" "bar"]]
workbook (create-workbook name data)
sheet (first (sheet-seq workbook))]
Expand Down Expand Up @@ -256,8 +256,8 @@
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (select-sheet (constantly true) "not-a-workbook")))))

(deftest select-columns-test
(let [data [["Name" "Quantity" "Price" "On Sale"]
["foo" 1.0 42 true]
(let [data [["Name" "Quantity" "Price" "On Sale"]
["foo" 1.0 42 true]
["bar" 2.0 108 false]]
workbook (create-workbook "Sheet 1" data)
sheet (first (sheet-seq workbook))]
Expand All @@ -278,7 +278,7 @@
(testing "Should support many datatypes."
(let [rows (select-columns {:A :string, :B :number, :D :boolean} sheet)
data-rows (rest rows)]
(are [actual expected] (= actual (let [[a b c d] expected]
(are [actual expected] (= actual (let [[a b c d] expected]
{:string a, :number b, :boolean d}))
(first data-rows) (data 1)
(second data-rows) (data 2))))
Expand Down Expand Up @@ -407,7 +407,7 @@
(is (= Font/BOLDWEIGHT_BOLD (.getBoldweight f-bold)))))
(is (thrown-with-msg? IllegalArgumentException #"^workbook.*"
(create-font! "not-a-workbook" {})))))


(deftest set-cell-style!-test
(testing "Should apply style to cell."
Expand Down Expand Up @@ -584,7 +584,7 @@


(defn- datatypes-rows [file]
(->> (load-workbook file)
(->> (load-workbook file)
sheet-seq
first
(select-columns datatypes-map)))
Expand All @@ -595,7 +595,7 @@
(map column)
(remove nil?)))

(defn- date? [date]
(defn- date? [date]
(isa? (class date) Date))

(deftest select-columns-integration-test
Expand Down
Loading