diff --git a/src/FsSpreadsheet.ExcelIO/Cell.fs b/src/FsSpreadsheet.ExcelIO/Cell.fs index 47641616..315e88e4 100644 --- a/src/FsSpreadsheet.ExcelIO/Cell.fs +++ b/src/FsSpreadsheet.ExcelIO/Cell.fs @@ -36,18 +36,6 @@ module Cell = /// let setValue (value : string) (cellValue : CellValue) = cellValue.Text <- value - /// - /// Takes a DataType and returns the appropriate CellValue. - /// - /// DataType is the FsSpreadsheet representation of the CellValue enum in OpenXml. - let cellValuesFromDataType (dataType : DataType) = - match dataType with - | String -> CellValues.String - | Boolean -> CellValues.Boolean - | Number -> CellValues.Number - | Date -> CellValues.Date - | Empty -> CellValues.Error - /// /// Takes a CellValue and returns the appropriate DataType. /// @@ -97,6 +85,15 @@ module Cell = let create (dataType : CellValues) (reference : string) (value : CellValue) = Cell(CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value) + /// + /// Creates a Cell from a CellValues type case, a "A1" style reference, and a CellValue containing the value string. + /// + let createWithFormat doc (dataType : CellValues) (reference : string) (cellFormat : CellFormat) (value : CellValue) = + let styleSheet = Stylesheet.getOrInit doc + let i = Stylesheet.CellFormat.count styleSheet + Stylesheet.CellFormat.append cellFormat styleSheet |> ignore + Cell(StyleIndex = UInt32Value(uint32 i),CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value) + /// /// Sets the preserve attribute of a Cell. /// @@ -143,20 +140,15 @@ module Cell = setSpacePreserveAttribute c else c - /// - /// Create a cell using a shared string table, also returns the updated shared string table. - /// - let fromValueWithDataType (sharedStringTable : SharedStringTable Option) columnIndex rowIndex (value : string) (dataType : DataType) = + let getCellContent (doc : Packaging.SpreadsheetDocument) (value : string) (dataType : DataType) = + let sharedStringTable = SharedStringTable.tryGet doc match dataType with | DataType.String when sharedStringTable.IsSome-> let sharedStringTable = sharedStringTable.Value - let reference = CellReference.ofIndices columnIndex (rowIndex) match SharedStringTable.tryGetIndexByString value sharedStringTable with | Some i -> i |> string - |> CellValue.create - |> create CellValues.SharedString reference | None -> let updatedSharedStringTable = sharedStringTable @@ -165,22 +157,30 @@ module Cell = updatedSharedStringTable |> SharedStringTable.count |> string - |> CellValue.create - |> create CellValues.SharedString reference - |> fun c -> - if value.EndsWith " " then - setSpacePreserveAttribute c - else c + |> fun v -> {|DataType = CellValues.SharedString; Value = v; Format = None|} + | DataType.String -> {|DataType = CellValues.String; Value = value; Format = None|} + | DataType.Boolean -> {|DataType = CellValues.Boolean; Value = value; Format = None|} + | DataType.Number -> {|DataType = CellValues.Number; Value = value; Format = None|} + | DataType.Date -> + let cellFormat = CellFormat(NumberFormatId = UInt32Value 19u, ApplyNumberFormat = BooleanValue true) + let value = System.DateTime.Parse(value).ToOADate() |> string + {|DataType = CellValues.Number; Value = value; Format = Some cellFormat|} + | DataType.Empty -> {|DataType = CellValues.Number; Value = value; Format = None|} - | _ -> - let valType = cellValuesFromDataType dataType - let reference = CellReference.ofIndices columnIndex (rowIndex) - create valType reference (CellValue.create value) - |> fun c -> + /// + /// Create a cell using a shared string table, also returns the updated shared string table. + /// + let fromValueWithDataType (doc : Packaging.SpreadsheetDocument) columnIndex rowIndex (value : string) (dataType : DataType) = + let reference = CellReference.ofIndices columnIndex (rowIndex) + let cellContent = getCellContent doc value dataType + if cellContent.Format.IsSome then + createWithFormat doc cellContent.DataType reference cellContent.Format.Value (CellValue.create cellContent.Value) + else + create cellContent.DataType reference (CellValue.create cellContent.Value) + |> fun c -> if value.EndsWith " " then setSpacePreserveAttribute c else c - /// /// Gets "A1"-style Cell reference. /// diff --git a/src/FsSpreadsheet.ExcelIO/FsExtensions.fs b/src/FsSpreadsheet.ExcelIO/FsExtensions.fs index 1e5a3f75..b9804e3c 100644 --- a/src/FsSpreadsheet.ExcelIO/FsExtensions.fs +++ b/src/FsSpreadsheet.ExcelIO/FsExtensions.fs @@ -17,33 +17,53 @@ module FsExtensions = /// /// Converts a given CellValues to the respective DataType. /// - static member ofXlsxCellValues (cellValues : CellValues) = - match cellValues with - | CellValues.Number -> DataType.Number - | CellValues.Boolean -> DataType.Boolean - | CellValues.Date -> DataType.Date - | CellValues.Error -> DataType.Empty - | CellValues.InlineString - | CellValues.SharedString - | CellValues.String - | _ -> DataType.String + static member ofXlsXCell (doc : Packaging.SpreadsheetDocument) (cell : Cell) = + + //https://stackoverflow.com/a/13178043/12858021 + //https://stackoverflow.com/a/55425719/12858021 + // if styleindex is not null and datatype is null we propably have a DateTime field. + // if datatype would not be null it could also be boolean, as far as i tested it ~Kevin F 13.10.2023 + if cell.StyleIndex <> null && cell.DataType = null then + try + let styleSheet = Stylesheet.get doc + let cellFormat : CellFormat = Stylesheet.CellFormat.getAt (int cell.StyleIndex.InnerText) styleSheet + if cellFormat <> null then + // if numberformatid is between 14 and 18 it is standard date time format. + // custom formats are given in the range of 164 to 180, all none default date time formats fall in there. + let dateTimeFormats = [14..22]@[164 .. 180] |> List.map (uint32 >> UInt32Value) + if List.contains cellFormat.NumberFormatId dateTimeFormats then + DataType.Date + else + DataType.Number + else + DataType.Number + with + | _ -> DataType.Number + else + let cellValues = cell.DataType.Value + match cellValues with + | CellValues.Number -> DataType.Number + | CellValues.Boolean -> DataType.Boolean + | CellValues.Date -> DataType.Date + | CellValues.Error -> DataType.Empty + | CellValues.InlineString + | CellValues.SharedString + | CellValues.String -> DataType.String + | _ -> DataType.Number type FsCell with - //member self.ofXlsxCell (sst : Spreadsheet.SharedStringTable option) (xlsxCell:Spreadsheet.Cell) = - // let v = Cell.getValue sst xlsxCell - // let row,col = xlsxCell.CellReference.Value |> CellReference.toIndices - // FsCell.create (int row) (int col) v /// /// Creates an FsCell on the basis of an XlsxCell. Uses a SharedStringTable if present to get the XlsxCell's value. - /// - static member ofXlsxCell (sst : SharedStringTable option) (xlsxCell : Cell) = + /// + static member ofXlsxCell (doc : Packaging.SpreadsheetDocument) (xlsxCell : Cell) = + let sst = Spreadsheet.tryGetSharedStringTable doc let mutable v = Cell.getValue sst xlsxCell let setValue x = v <- x let col, row = xlsxCell.CellReference.Value |> CellReference.toIndices let dt = - try DataType.ofXlsxCellValues xlsxCell.DataType.Value + try DataType.ofXlsXCell doc xlsxCell with _ -> DataType.Number // default is number match dt with | Date -> @@ -62,6 +82,9 @@ module FsExtensions = | _ -> () FsCell.createWithDataType dt (int row) (int col) v + static member toXlsxCell (doc : Packaging.SpreadsheetDocument) (cell : FsCell) = + Cell.fromValueWithDataType doc (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) cell.Value cell.DataType + type FsTable with /// @@ -118,7 +141,7 @@ module FsExtensions = /// /// Returns the FsWorksheet in the form of an XlsxSpreadsheet. /// - member self.ToXlsxWorksheet() = + member self.ToXlsxWorksheet(doc) = self.RescanRows() let sheet = Worksheet.empty() let sheetData = @@ -134,7 +157,7 @@ module FsExtensions = let cells = cells |> List.map (fun cell -> - Cell.fromValueWithDataType None (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.Value) (cell.DataType) + Cell.fromValueWithDataType doc (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.Value) (cell.DataType) ) let row = Row.create (uint32 row.Index) (Row.Spans.fromBoundaries min max) cells SheetData.appendRow row sd |> ignore @@ -144,8 +167,8 @@ module FsExtensions = /// /// Returns an FsWorksheet in the form of an XlsxSpreadsheet. /// - static member toXlsxWorksheet (fsWorksheet : FsWorksheet) = - fsWorksheet.ToXlsxWorksheet() + static member toXlsxWorksheet (fsWorksheet : FsWorksheet, doc) = + fsWorksheet.ToXlsxWorksheet(doc) /// /// Appends the FsTables of this FsWorksheet to a given OpenXmlWorksheetPart in an XlsxWorkbookPart. @@ -198,25 +221,7 @@ module FsExtensions = let sheetId = Sheet.getID xlsxSheet let xlsxCells = Spreadsheet.getCellsBySheetID sheetId doc - |> Seq.map (fun c -> - //https://stackoverflow.com/a/13178043/12858021 - //https://stackoverflow.com/a/55425719/12858021 - // if styleindex is not null and datatype is null we propably have a DateTime field. - // if datatype would not be null it could also be boolean, as far as i tested it ~Kevin F 13.10.2023 - if c.StyleIndex <> null && c.DataType = null then - try - // get cellformat from stylesheet - let cellFormat : CellFormat = xlsxWorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements.GetItem (int c.StyleIndex.InnerText) :?> CellFormat - if cellFormat <> null then - // if numberformatid is between 14 and 18 it is standard date time format. - // custom formats are given in the range of 164 to 180, all none default date time formats fall in there. - let dateTimeFormats = [14..22]@[164 .. 180] |> List.map (uint32 >> UInt32Value) - if List.contains cellFormat.NumberFormatId dateTimeFormats then - c.DataType <- CellValues.Date - with - | _ -> () - FsCell.ofXlsxCell sst c - ) + |> Seq.map (FsCell.ofXlsxCell doc) let assocXlsxTables = xlsxTables |> Seq.tryPick (fun (sid,ts) -> if sid = sheetId then Some ts else None) @@ -267,20 +272,24 @@ module FsExtensions = sr.Close() wb - /// - /// Writes the FsWorkbook into a given MemoryStream. - /// - member self.ToStream(stream : MemoryStream) = - let doc = Spreadsheet.initEmptyOnStream stream - + member self.ToEmptySpreadsheet(doc : Packaging.SpreadsheetDocument) = + let workbookPart = Spreadsheet.initWorkbookPart doc for worksheet in self.GetWorksheets() do let worksheetPart = - WorkbookPart.appendWorksheet worksheet.Name (worksheet.ToXlsxWorksheet()) workbookPart + WorkbookPart.appendWorksheet worksheet.Name (worksheet.ToXlsxWorksheet(doc)) workbookPart |> WorkbookPart.getOrInitWorksheetPartByName worksheet.Name worksheet.AppendTablesToWorksheetPart(workbookPart,worksheetPart) + + /// + /// Writes the FsWorkbook into a given MemoryStream. + /// + member self.ToStream(stream : MemoryStream) = + let doc = Spreadsheet.initEmptyOnStream stream + + self.ToEmptySpreadsheet(doc) //Worksheet.setSheetData sheetData sheet |> ignore //WorkbookPart.appendWorksheet worksheet.Name sheet workbookPart |> ignore diff --git a/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj b/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj index 47d37afc..41ae4878 100644 --- a/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj +++ b/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj @@ -17,6 +17,7 @@ + diff --git a/src/FsSpreadsheet.ExcelIO/SharedStringTable.fs b/src/FsSpreadsheet.ExcelIO/SharedStringTable.fs index fcfd2876..1359fe7a 100644 --- a/src/FsSpreadsheet.ExcelIO/SharedStringTable.fs +++ b/src/FsSpreadsheet.ExcelIO/SharedStringTable.fs @@ -92,7 +92,13 @@ module SharedStringTable = else index - + /// + /// Gets the sharedStringTable of the spreadsheet if it exists, else returns None. + /// + let tryGet (spreadsheetDocument : SpreadsheetDocument) = + try spreadsheetDocument.WorkbookPart.SharedStringTablePart.SharedStringTable |> Some + with | _ -> None + diff --git a/src/FsSpreadsheet.ExcelIO/Stylesheet.fs b/src/FsSpreadsheet.ExcelIO/Stylesheet.fs new file mode 100644 index 00000000..0a2503ae --- /dev/null +++ b/src/FsSpreadsheet.ExcelIO/Stylesheet.fs @@ -0,0 +1,61 @@ +namespace FsSpreadsheet.ExcelIO + +open DocumentFormat.OpenXml.Spreadsheet +open DocumentFormat.OpenXml.Packaging +open DocumentFormat.OpenXml + +module Stylesheet = + + //module Font = + + // let getDefault() = Font().Color + + + let get (doc : SpreadsheetDocument) = + + doc.WorkbookPart.WorkbookStylesPart.Stylesheet + + let getOrInit (doc : SpreadsheetDocument) = + + match doc.WorkbookPart.WorkbookStylesPart with + | null -> + let ssp = doc.WorkbookPart.AddNewPart() + ssp.Stylesheet <- new Stylesheet() + ssp.Stylesheet.CellFormats <- new CellFormats() + ssp.Stylesheet + | ssp -> ssp.Stylesheet + + let tryGet (doc : SpreadsheetDocument) = + match doc.WorkbookPart.WorkbookStylesPart with + | null -> None + | ssp -> Some(ssp.Stylesheet) + + module CellFormat = + + let updateCount (stylesheet : Stylesheet) = + let newCount = stylesheet.CellFormats.Elements() |> Seq.length + stylesheet.CellFormats.Count <- UInt32Value(uint32 newCount) + + let count (stylesheet : Stylesheet) = + if stylesheet.CellFormats = null then 0 + elif stylesheet.CellFormats.Count = null then 0 + else stylesheet.CellFormats.Count.Value |> int + + let getAt (index : int) (stylesheet : Stylesheet) = + stylesheet.CellFormats.Elements() |> Seq.item index + + let tryGetAt (index : int) (stylesheet : Stylesheet) = + stylesheet.CellFormats.Elements() |> Seq.tryItem index + + let setAt (index : int) (cf : CellFormat) (stylesheet : Stylesheet) = + if count stylesheet > index then + let previousChild = getAt index stylesheet + stylesheet.CellFormats.ReplaceChild(cf, previousChild) |> ignore + if count stylesheet = index then + stylesheet.CellFormats.AppendChild(cf) |> ignore + else failwith "Cannot insert style into stylesheet: Index out of range" + updateCount stylesheet + + let append (cf : CellFormat) (stylesheet : Stylesheet) = + stylesheet.CellFormats.AppendChild(cf) |> ignore + updateCount stylesheet \ No newline at end of file diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj b/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj index 59ca306f..30c829b6 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj +++ b/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj @@ -25,8 +25,13 @@ - - + + + + + + + \ No newline at end of file diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs index 7ed25ad2..3dba8572 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs +++ b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs @@ -53,40 +53,44 @@ let testFile2Path = Path.Combine(__SOURCE_DIRECTORY__, "../data", "2EXT02_Protei [] let fsExtensionTests = testList "FsExtensions" [ - testList "DataType" [ - testList "ofXlsxCellValues" [ - let testCvNumber = DataType.ofXlsxCellValues CellValues.Number - testCase "is correct DataTypeNumber from CellValuesNumber" <| fun _ -> - Expect.equal testCvNumber DataType.Number "is not the correct DataType" - let testCvString = DataType.ofXlsxCellValues CellValues.String - testCase "is correct DataTypeString from CellValuesString" <| fun _ -> - Expect.equal testCvString DataType.String "is not the correct DataType" - let testCvSharedString = DataType.ofXlsxCellValues CellValues.SharedString - testCase "is correct DataTypeString from CellValuesSharedString" <| fun _ -> - Expect.equal testCvSharedString DataType.String "is not the correct DataType" - let testCvInlineString = DataType.ofXlsxCellValues CellValues.InlineString - testCase "is correct DataTypeString from CellValuesInlineString" <| fun _ -> - Expect.equal testCvInlineString DataType.String "is not the correct DataType" - let testCvBoolean = DataType.ofXlsxCellValues CellValues.Boolean - testCase "is correct DataTypeBoolean from CellValuesBoolean" <| fun _ -> - Expect.equal testCvBoolean DataType.Boolean "is not the correct DataType" - let testCvDate = DataType.ofXlsxCellValues CellValues.Date - testCase "is correct DataTypeDate from CellValuesDate" <| fun _ -> - Expect.equal testCvDate DataType.Date "is not the correct DataType" - let testCvError = DataType.ofXlsxCellValues CellValues.Error - testCase "is correct DataTypeEmpty from CellValuesError" <| fun _ -> - Expect.equal testCvError DataType.Empty "is not the correct DataType" - ] - ] + //testList "DataType" [ + // testList "ofXlsxCellValues" [ + // let stream = new MemoryStream() + // let doc = Spreadsheet.initEmptyOnStream stream + // let testCvNumber = DataType.ofXlsxCellValues doc CellValues.Number + // testCase "is correct DataTypeNumber from CellValuesNumber" <| fun _ -> + // Expect.equal testCvNumber DataType.Number "is not the correct DataType" + // let testCvString = DataType.ofXlsxCellValues CellValues.String + // testCase "is correct DataTypeString from CellValuesString" <| fun _ -> + // Expect.equal testCvString DataType.String "is not the correct DataType" + // let testCvSharedString = DataType.ofXlsxCellValues CellValues.SharedString + // testCase "is correct DataTypeString from CellValuesSharedString" <| fun _ -> + // Expect.equal testCvSharedString DataType.String "is not the correct DataType" + // let testCvInlineString = DataType.ofXlsxCellValues CellValues.InlineString + // testCase "is correct DataTypeString from CellValuesInlineString" <| fun _ -> + // Expect.equal testCvInlineString DataType.String "is not the correct DataType" + // let testCvBoolean = DataType.ofXlsxCellValues CellValues.Boolean + // testCase "is correct DataTypeBoolean from CellValuesBoolean" <| fun _ -> + // Expect.equal testCvBoolean DataType.Boolean "is not the correct DataType" + // //let testCvDate = DataType.ofXlsxCellValues CellValues.Date + // //testCase "is correct DataTypeDate from CellValuesDate" <| fun _ -> + // // Expect.equal testCvDate DataType.Date "is not the correct DataType" + // let testCvError = DataType.ofXlsxCellValues CellValues.Error + // testCase "is correct DataTypeEmpty from CellValuesError" <| fun _ -> + // Expect.equal testCvError DataType.Empty "is not the correct DataType" + // ] + //] testList "FsCell" [ testList "ofXlsxCell" [ - let testCell = FsCell.ofXlsxCell None dummyXlsxCell + let stream = new MemoryStream() + let doc = Spreadsheet.initEmptyOnStream stream + let testCell = FsCell.ofXlsxCell doc dummyXlsxCell testCase "is equal in value" <| fun _ -> Expect.equal testCell.Value dummyXlsxCell.CellValue.Text "values are not equal" testCase "is equal in address/reference" <| fun _ -> Expect.equal testCell.Address.Address dummyXlsxCell.CellReference.Value "addresses/references are not equal" testCase "is equal in DataType/CellValues" <| fun _ -> - let dtOfCvs = DataType.ofXlsxCellValues dummyXlsxCell.DataType + let dtOfCvs = DataType.ofXlsXCell doc dummyXlsxCell Expect.equal testCell.DataType dtOfCvs "addresses/references are not equal" ] ]