diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ced82..6da835c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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. - - - diff --git a/README.md b/README.md index 38f69c3..039ad58 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ 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") @@ -16,13 +16,13 @@ Docjure makes reading and writing Office documents in Clojure easy. ;=> [{: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"] @@ -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 + + ;=> (# # # # # #) + +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. @@ -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) @@ -92,7 +129,7 @@ Martin Jul * Email: martin@.....com * Twitter: mjul -* GitHub: [mjul](https://github.com/mjul) +* GitHub: [mjul](https://github.com/mjul) ## Contributors diff --git a/src/dk/ative/docjure/spreadsheet.clj b/src/dk/ative/docjure/spreadsheet.clj index 3ee3b55..e18b2bd 100644 --- a/src/dk/ative/docjure/spreadsheet.clj +++ b/src/dk/ative/docjure/spreadsheet.clj @@ -5,6 +5,7 @@ (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) @@ -12,7 +13,7 @@ (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))))) @@ -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] @@ -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 @@ -234,7 +240,7 @@ workbook)) ;****************************************************** -; helpers for font and style creation +; helpers for font and style creation (defn color-index @@ -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))) @@ -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. @@ -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: @@ -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}) " diff --git a/test/dk/ative/docjure/spreadsheet_test.clj b/test/dk/ative/docjure/spreadsheet_test.clj index 54a9c14..5e4d987 100644 --- a/test/dk/ative/docjure/spreadsheet_test.clj +++ b/test/dk/ative/docjure/spreadsheet_test.clj @@ -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)] @@ -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) @@ -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) @@ -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))] @@ -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.) @@ -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)] @@ -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)] @@ -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))] @@ -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))] @@ -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)))) @@ -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." @@ -584,7 +584,7 @@ (defn- datatypes-rows [file] - (->> (load-workbook file) + (->> (load-workbook file) sheet-seq first (select-columns datatypes-map))) @@ -595,7 +595,7 @@ (map column) (remove nil?))) -(defn- date? [date] +(defn- date? [date] (isa? (class date) Date)) (deftest select-columns-integration-test diff --git a/test/dk/ative/docjure/xls_test.clj b/test/dk/ative/docjure/xls_test.clj index e9d7178..849f5d6 100644 --- a/test/dk/ative/docjure/xls_test.clj +++ b/test/dk/ative/docjure/xls_test.clj @@ -21,7 +21,7 @@ (is (thrown-with-msg? IllegalArgumentException #"workbook.*" (add-sheet! "not-a-workbook" "sheet-name")))))) (deftest create-xls-workbook-test - (let [sheet-name "Sheet 1" + (let [sheet-name "Sheet 1" sheet-data [["A1" "B1" "C1"] ["A2" "B2" "C2"]] workbook (create-xls-workbook sheet-name sheet-data)] @@ -41,7 +41,7 @@ (.getCell (second rows) 1) (second (second sheet-data))))))) (deftest remove-row!-test - (let [sheet-name "Sheet 1" + (let [sheet-name "Sheet 1" sheet-data [["A1" "B1" "C1"] ["A2" "B2" "C2"]] workbook (create-xls-workbook sheet-name sheet-data) @@ -51,13 +51,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-xls-workbook sheet-name sheet-data) @@ -106,7 +106,7 @@ (is (= 42.0 (read-cell number-cell)))))) (deftest set-cell!-test - (let [sheet-name "Sheet 1" + (let [sheet-name "Sheet 1" sheet-data [["A1"]] workbook (create-xls-workbook sheet-name sheet-data) a1 (-> workbook (.getSheetAt 0) (.getRow 0) (.getCell 0))] @@ -136,7 +136,7 @@ (deftest sheet-seq-test - (let [sheet-name "Sheet 1" + (let [sheet-name "Sheet 1" sheet-data [["foo" "bar"]]] (testing "Empty workbook" (let [workbook (HSSFWorkbook.) @@ -159,7 +159,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-xls-workbook sheet-name sheet-data) sheet (select-sheet sheet-name workbook)] @@ -168,7 +168,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-xls-workbook sheet-name sheet-data) sheet (select-sheet sheet-name workbook)] @@ -195,7 +195,7 @@ (deftest sheet-name-test - (let [name "Sheet 1" + (let [name "Sheet 1" data [["foo" "bar"]] workbook (create-xls-workbook name data) sheet (first (sheet-seq workbook))] @@ -234,8 +234,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-xls-workbook "Sheet 1" data) sheet (first (sheet-seq workbook))] @@ -256,7 +256,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)))) @@ -376,7 +376,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." @@ -553,7 +553,7 @@ ;; ---------------------------------------------------------------- (defn- datatypes-rows [file] - (->> (load-workbook file) + (->> (load-workbook file) sheet-seq first (select-columns datatypes-map))) @@ -564,7 +564,7 @@ (map column) (remove nil?))) -(defn- date? [date] +(defn- date? [date] (isa? (class date) Date)) (deftest select-columns-integration-test @@ -604,4 +604,3 @@ (is (= (reduce concat (map (fn [[_ a b]] [a b]) data)) (map read-cell (select-name workbook "ten")))) (is (nil? (select-name workbook "bill")))))) -