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

Allow users of the library to ignore missing workbooks #16

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 1 addition & 2 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject dk.ative/docjure "1.7.0-SNAPSHOT"
(defproject tgk/docjure "1.7.0-SNAPSHOT"
:description "Easily read and write Office documents from Clojure."
:url "http://github.com/ative/docjure"
:license {:name "MIT License"
Expand All @@ -12,4 +12,3 @@
:1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}}
:aliases {"all" ["with-profile" "1.3:1.4:1.5"]}
:global-vars {*warn-on-reflection* true})

55 changes: 46 additions & 9 deletions src/dk/ative/docjure/spreadsheet.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
(java.io FileOutputStream FileInputStream)
(java.util Date Calendar)
(org.apache.poi.xssf.usermodel XSSFWorkbook)
(org.apache.poi.hssf.usermodel HSSFFormulaEvaluator)
(org.apache.poi.ss.usermodel Workbook Sheet Cell Row
WorkbookFactory DateUtil
IndexedColors CellStyle Font
CellValue)
(org.apache.poi.ss.util CellReference AreaReference)))

(def ^:dynamic *ignore-missing-workbooks*
false)

(def ^:dynamic *ignore-formulas*
false)

(def ^:dynamic *ignore-hidden-sheets*
false)

(defmacro assert-type [value expected-type]
`(when-not (isa? (class ~value) ~expected-type)
(throw (IllegalArgumentException.
Expand All @@ -25,20 +35,36 @@
(if date-format?
(DateUtil/getJavaDate (.getNumberValue cv))
(.getNumberValue cv)))
(defmethod read-cell-value Cell/CELL_TYPE_ERROR [^CellValue cv _]
(str "Error cell: " 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]
(let [evaluator (.. cell getSheet getWorkbook
getCreationHelper createFormulaEvaluator)
cv (.evaluate evaluator cell)]
(read-cell-value cv false)))
(if *ignore-formulas*
(condp = (.getCachedFormulaResultType cell)
Cell/CELL_TYPE_BLANK nil
Cell/CELL_TYPE_STRING (.getStringCellValue cell)
Cell/CELL_TYPE_BOOLEAN (.getBooleanCellValue cell)
Cell/CELL_TYPE_NUMERIC (if (DateUtil/isCellDateFormatted cell)
(.getDateCellValue cell)
(.getNumericCellValue cell))
(throw (ex-info "Unable to read cell value when ignoring formulas"
{:cell cell
:cached-formula-result-type (.getCachedFormulaResultType cell)})))
(let [evaluator (.. cell getSheet getWorkbook
getCreationHelper createFormulaEvaluator)]
(when (instance? HSSFFormulaEvaluator evaluator)
(.setIgnoreMissingWorkbooks evaluator *ignore-missing-workbooks*))
(read-cell-value (.evaluate evaluator cell) 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]
"ERROR")

(defn load-workbook
"Load an Excel .xls or .xlsx workbook from a file."
Expand All @@ -53,12 +79,23 @@
(with-open [file-out (FileOutputStream. filename)]
(.write workbook file-out)))

(defn hidden?
"Returns true if the given sheet is hidden."
[^Sheet sheet]
(let [workbook (.getWorkbook sheet)
idx (.getSheetIndex workbook sheet)]
(or (.isSheetHidden workbook idx)
(.isSheetVeryHidden workbook idx))))

(defn sheet-seq
"Return a lazy seq of the sheets in a workbook."
"Return a lazy seq of the sheets in a workbook. Excludes hidden sheets if *ignore-hidden-sheets* is true."
[^Workbook workbook]
(assert-type workbook Workbook)
(for [idx (range (.getNumberOfSheets workbook))]
(.getSheetAt workbook idx)))
(let [sheets (for [idx (range (.getNumberOfSheets workbook))]
(.getSheetAt workbook idx))]
(if *ignore-hidden-sheets*
(filter (comp not hidden?) sheets)
sheets)))

(defn sheet-name
"Return the name of a sheet."
Expand Down Expand Up @@ -114,9 +151,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
75 changes: 55 additions & 20 deletions test/dk/ative/docjure/spreadsheet_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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 @@ -58,7 +58,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 @@ -68,13 +68,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 @@ -125,7 +125,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 @@ -153,9 +153,22 @@
(set-cell! a1 (double 1.2))
(is (= 1.2 (.getNumericCellValue a1)))))))

(deftest hidden?-test
(let [name "Sheet 1"
data [["foo" "bar"]]
workbook (create-workbook name data)
sheet2 (.createSheet workbook "Sheet 2")
sheet3 (.createSheet workbook "Sheet 3")
sheet4 (.createSheet workbook "Sheet 4")]
(testing "Can detect whether a sheet is hidden"
(.setSheetHidden workbook 1 1)
(.setSheetHidden workbook 3 2)
(is (hidden? sheet2))
(is (not (hidden? sheet3)))
(is (hidden? sheet4)))))

(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 @@ -175,10 +188,34 @@
(is (= 3 (count actual)))
(is (= [sheet2 sheet3] (rest actual)))))
(testing "Should fail on invalid type"
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (sheet-seq "not-a-workbook"))))))
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (sheet-seq "not-a-workbook"))))
(testing "Can exclude hidden sheets"
(binding [*ignore-hidden-sheets* true]
(let [name "Sheet 1"
data [["foo" "bar"]]
workbook (create-workbook name data)
sheet2 (.createSheet workbook "Sheet 2")
sheet3 (.createSheet workbook "Sheet 3")
sheet4 (.createSheet workbook "Sheet 4")
sheet5 (.createSheet workbook "Sheet 5")]
(.setSheetHidden workbook 1 1)
(.setSheetHidden workbook 3 2)
(is (= 3 (count (sheet-seq workbook)))))))
(testing "Can include hidden sheets"
(binding [*ignore-hidden-sheets* false]
(let [name "Sheet 1"
data [["foo" "bar"]]
workbook (create-workbook name data)
sheet2 (.createSheet workbook "Sheet 2")
sheet3 (.createSheet workbook "Sheet 3")
sheet4 (.createSheet workbook "Sheet 4")
sheet5 (.createSheet workbook "Sheet 5")]
(.setSheetHidden workbook 1 1)
(.setSheetHidden workbook 3 2)
(is (= 5 (count (sheet-seq 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 @@ -187,7 +224,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 @@ -214,7 +251,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 All @@ -223,7 +260,7 @@
(is (thrown-with-msg? IllegalArgumentException #"sheet.*" (sheet-name "not-a-sheet")))))

(deftest select-sheet-test
(let [name "Sheet 1"
(let [name "Sheet 1"
data [["foo" "bar"]]
workbook (create-workbook name data)
first-sheet (first (sheet-seq workbook))]
Expand All @@ -232,10 +269,9 @@
(testing "Should fail on invalid parameter type"
(is (thrown-with-msg? IllegalArgumentException #"workbook.*" (select-sheet "name" "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 @@ -256,7 +292,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 @@ -301,14 +337,14 @@
(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."
(let [wb (create-workbook "Dummy" [["foo"]])
cs (create-cell-style! wb {:background :yellow})
cell (-> (sheet-seq wb) first cell-seq first)]
(do
(do
(is (= cell (set-cell-style! cell cs)))
(is (= (.getCellStyle cell) cs))))))

Expand Down Expand Up @@ -370,7 +406,7 @@


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

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

(deftest select-columns-integration-test
Expand Down Expand Up @@ -421,4 +457,3 @@
(is (= (reduce concat (map (fn [[_ a b]] [a b]) data))
(map read-cell (select-name workbook "ten"))))
(is (nil? (select-name workbook "bill"))))))