diff --git a/.gitignore b/.gitignore index a34d9223..0d7f3130 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,4 @@ tmp/ /tests/JS/FsSpreadsheet.Exceljs /tests/FsSpreadsheet.Exceljs.Tests/js /dist +/tests/TestUtils/TestFiles/Scripts/fable diff --git a/FsSpreadsheet.sln b/FsSpreadsheet.sln index 0f2312dd..556dfc60 100644 --- a/FsSpreadsheet.sln +++ b/FsSpreadsheet.sln @@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JS", "JS", "{ADCF7D08-F2EE- tests\JS\Exceljs.js = tests\JS\Exceljs.js EndProjectSection EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TestUtils", "tests\TestUtils\TestUtils.fsproj", "{60678E53-EDC4-4ADE-A9EE-B194BDC76B37}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -177,6 +179,18 @@ Global {96E12F19-B25A-415E-B965-F9DE8D713C67}.Release|x64.Build.0 = Release|Any CPU {96E12F19-B25A-415E-B965-F9DE8D713C67}.Release|x86.ActiveCfg = Release|Any CPU {96E12F19-B25A-415E-B965-F9DE8D713C67}.Release|x86.Build.0 = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|x64.ActiveCfg = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|x64.Build.0 = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|x86.ActiveCfg = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Debug|x86.Build.0 = Debug|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|Any CPU.Build.0 = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|x64.ActiveCfg = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|x64.Build.0 = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|x86.ActiveCfg = Release|Any CPU + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +207,7 @@ Global {E72A14FF-5026-463B-B0FA-2DA104D67B0C} = {1CF1274C-DE28-4446-9B4E-5884E797B87B} {96E12F19-B25A-415E-B965-F9DE8D713C67} = {F77AD108-C6B4-46BB-B7BC-13573F45F876} {ADCF7D08-F2EE-4DFD-A96A-7E0134A1546F} = {F77AD108-C6B4-46BB-B7BC-13573F45F876} + {60678E53-EDC4-4ADE-A9EE-B194BDC76B37} = {F77AD108-C6B4-46BB-B7BC-13573F45F876} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0EDE6697-0F13-4DB1-AC56-12C15A72D395} diff --git a/build/TestTasks.fs b/build/TestTasks.fs index 3003374a..3a16a5d3 100644 --- a/build/TestTasks.fs +++ b/build/TestTasks.fs @@ -11,6 +11,28 @@ let FableTestPath_input = "tests/FsSpreadsheet.Tests" module RunTests = + open Fake.Core + + //let createFreshTestFiles = BuildTask.create "createFreshTestFiles" [] { + // let testFilesPath = "./tests/TestUtils/TestFiles" + // let source = System.IO.FileInfo(testFilesPath + @"/TestWorkbook_Excel.xlsx") + // let scriptsFolder = "/Scripts" + // let testFiles = + // [| + // @"/TestWorkbook_FsSpreadsheet.net.xlsx", @".\runFsSpreadsheet.fsx.cmd" + // @"/TestWorkbook_FsSpreadsheet.js.xlsx", @".\runFsSpreadsheet.js.cmd" + // @"/TestWorkbook_FableExceljs.xlsx", @".\runFableExceljs" + // @"/TestWorkbook_ClosedXML.xlsx", @".\runClosedXml" + // |] + + // for testFile, script in testFiles do + // let target = System.IO.FileInfo(testFilesPath + testFile) + // if source.LastWriteTimeUtc > target.LastWriteTimeUtc then + // let scriptFolderPath = testFilesPath + scriptsFolder + // Trace.traceImportant $"Update `{testFile}` with `{script}`, as source file was updated since last transpilation." + // run (createProcess script) "" scriptFolderPath + //} + /// runs `npm test` in root. /// npm test consists of `test` and `pretest` /// check package.json in root for behavior diff --git a/src/FsSpreadsheet.CsvIO/FsExtension.fs b/src/FsSpreadsheet.CsvIO/FsExtension.fs index 020849e7..752df39a 100644 --- a/src/FsSpreadsheet.CsvIO/FsExtension.fs +++ b/src/FsSpreadsheet.CsvIO/FsExtension.fs @@ -16,7 +16,7 @@ module FsExtensions = cells |> Seq.tryPick (fun cell -> if cell.ColumnNumber = i then - Option.Some cell.Value + cell.ValueAsString() |> Some else None ) diff --git a/src/FsSpreadsheet.ExcelIO/Cell.fs b/src/FsSpreadsheet.ExcelIO/Cell.fs index 01ac9e97..06870a6c 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. /// @@ -94,8 +82,20 @@ module Cell = /// /// Creates a Cell from a CellValues type case, a "A1" style reference, and a CellValue containing the value string. /// - let create (dataType : CellValues) (reference : string) (value : CellValue) = - Cell(CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value) + let create (dataType : CellValues option) (reference : string) (value : CellValue) = + match dataType with + | Some dataType -> Cell(CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value) + | None -> Cell(CellReference = StringValue.FromString reference, 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 option) (reference : string) (cellFormat : CellFormat) (value : CellValue) = + let styleSheet = Stylesheet.getOrInit doc + let i = Stylesheet.CellFormat.appendOrGetIndex cellFormat styleSheet + match dataType with + | Some dataType -> Cell(StyleIndex = UInt32Value(uint32 i),CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value) + | None -> Cell(StyleIndex = UInt32Value(uint32 i),CellReference = StringValue.FromString reference, CellValue = value) /// /// Sets the preserve attribute of a Cell. @@ -118,7 +118,7 @@ module Cell = i |> string |> CellValue.create - |> create CellValues.SharedString reference + |> create (Some CellValues.SharedString) reference | None -> let updatedSharedStringTable = sharedStringTable @@ -128,7 +128,7 @@ module Cell = |> SharedStringTable.count |> string |> CellValue.create - |> create CellValues.SharedString reference + |> create (Some CellValues.SharedString) reference |> fun c -> if s.EndsWith " " then setSpacePreserveAttribute c @@ -137,26 +137,21 @@ module Cell = | _ -> let valType,value = inferCellValue value let reference = CellReference.ofIndices columnIndex (rowIndex) - create valType reference (CellValue.create value) + create (Some valType) reference (CellValue.create value) |> fun c -> if value.EndsWith " " then 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 +160,38 @@ 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 = Some CellValues.SharedString; Value = v; Format = None|} + | DataType.String -> + {|DataType = Some CellValues.String; Value = value; Format = None|} + | DataType.Boolean -> + {|DataType = Some CellValues.Boolean; Value = System.Boolean.Parse value |> FsCellAux.boolConverter; Format = None|} + | DataType.Number -> + {|DataType = Some 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 + let cellFormat = + if value.Contains(".") then + Stylesheet.CellFormat.getDefaultDateTime() + else + Stylesheet.CellFormat.getDefaultDate() + {|DataType = None; Value = value; Format = Some cellFormat|} + | DataType.Empty -> {|DataType = None; 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. /// @@ -256,7 +267,6 @@ module Cell = match cell |> tryGetType with | Some (CellValues.SharedString) when sharedStringTable.IsSome-> let sharedStringTable = sharedStringTable.Value - let sharedStringTableIndex = cell |> getCellValue diff --git a/src/FsSpreadsheet.ExcelIO/FsExtensions.fs b/src/FsSpreadsheet.ExcelIO/FsExtensions.fs index cca37629..70e717b6 100644 --- a/src/FsSpreadsheet.ExcelIO/FsExtensions.fs +++ b/src/FsSpreadsheet.ExcelIO/FsExtensions.fs @@ -17,35 +17,81 @@ 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) = + + if cell.CellFormula <> null then + // LibreOffice annotates boolean values as formulas instead of boolean datatypes + if cell.CellFormula.InnerText = "TRUE()" || cell.CellFormula.InnerText = "FALSE()" then + DataType.Boolean + else + DataType.Number + //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 + elif cell.StyleIndex <> null && (cell.DataType = null || cell.DataType.Value = CellValues.Number) then + try + let stylesheet = Stylesheet.get doc + let cellFormat : CellFormat = Stylesheet.CellFormat.getAt (int cell.StyleIndex.InnerText) stylesheet + if cellFormat <> null && Stylesheet.CellFormat.isDateTime stylesheet cellFormat then + DataType.Date + + 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) = - let v = Cell.getValue sst xlsxCell + /// + static member ofXlsxCell (doc : Packaging.SpreadsheetDocument) (xlsxCell : Cell) = + let sst = Spreadsheet.tryGetSharedStringTable doc + let cellValueString = Cell.getValue sst xlsxCell let col, row = xlsxCell.CellReference.Value |> CellReference.toIndices let dt = - try DataType.ofXlsxCellValues xlsxCell.DataType.Value - with _ -> DataType.Empty - FsCell.createWithDataType dt (int row) (int col) v + try DataType.ofXlsXCell doc xlsxCell + with _ -> DataType.Number // default is number + let mutable cellValue : obj = cellValueString + match dt with + | Date -> + try + // datetime is written as float counting days since 1900. + // We use the .NET helper because we really do not want to deal with datetime issues. + cellValue <- System.DateTime.FromOADate(float cellValueString) + with + | _ -> () + | Boolean -> + // boolean is written as int/float either 0 or null + match cellValueString.ToLower() with + | "1" | "true" -> cellValue <- true + | "0" | "false" -> cellValue <- false + | _ -> () + | Number -> + try + cellValue <- float cellValueString + with + | _ -> + () + | Empty | String -> () + //let dt, v = DataType.InferCellValue v + FsCell.createWithDataType dt (int row) (int col) (cellValue) + static member toXlsxCell (doc : Packaging.SpreadsheetDocument) (cell : FsCell) = + Cell.fromValueWithDataType doc (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.ValueAsString()) cell.DataType type FsTable with @@ -103,7 +149,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 = @@ -119,7 +165,7 @@ module FsExtensions = let cells = cells |> List.map (fun cell -> - Cell.fromValueWithDataType None (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.Value) (cell.DataType) + FsCell.toXlsxCell doc cell ) let row = Row.create (uint32 row.Index) (Row.Spans.fromBoundaries min max) cells SheetData.appendRow row sd |> ignore @@ -129,8 +175,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. @@ -179,11 +225,11 @@ module FsExtensions = xlsxSheets |> Seq.map ( fun xlsxSheet -> - let sheetIndex = Sheet.getSheetIndex xlsxSheet + let sheetIndex = Sheet.getSheetIndex xlsxSheet //unused? let sheetId = Sheet.getID xlsxSheet let xlsxCells = Spreadsheet.getCellsBySheetID sheetId doc - |> Seq.map (FsCell.ofXlsxCell sst) + |> Seq.map (FsCell.ofXlsxCell doc) let assocXlsxTables = xlsxTables |> Seq.tryPick (fun (sid,ts) -> if sid = sheetId then Some ts else None) @@ -215,39 +261,48 @@ module FsExtensions = /// Creates an FsWorkbook from a given Stream to an XlsxFile. /// static member fromXlsxStream (stream : Stream) = - let doc = Spreadsheet.fromStream stream false - FsWorkbook.fromSpreadsheetDocument doc + if stream.CanWrite && stream.CanSeek then + let package = Packaging.Package.Open(stream,FileMode.Open,FileAccess.ReadWrite) + if Package.isLibrePackage package then + Package.fixLibrePackage package + FsWorkbook.fromPackage package + else + let package = Packaging.Package.Open(stream) + FsWorkbook.fromPackage package /// /// Creates an FsWorkbook from a given Stream to an XlsxFile. /// static member fromBytes (bytes : byte []) = - let stream = new MemoryStream(bytes) + let stream = new MemoryStream(bytes,writable = true) FsWorkbook.fromXlsxStream stream /// /// Takes the path to an Xlsx file and returns the FsWorkbook based on its content. /// static member fromXlsxFile (filePath : string) = - let sr = new StreamReader(filePath) - let wb = FsWorkbook.fromXlsxStream sr.BaseStream - sr.Close() - wb + let bytes = File.ReadAllBytes filePath + FsWorkbook.fromBytes bytes - /// - /// 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..d2302bcc 100644 --- a/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj +++ b/src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj @@ -17,6 +17,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/src/FsSpreadsheet.ExcelIO/Package.fs b/src/FsSpreadsheet.ExcelIO/Package.fs new file mode 100644 index 00000000..e42ae9cc --- /dev/null +++ b/src/FsSpreadsheet.ExcelIO/Package.fs @@ -0,0 +1,35 @@ +namespace FsSpreadsheet.ExcelIO + +open DocumentFormat.OpenXml +open System.IO + +module Package = + + let tryGetApplication (package : Packaging.Package) = + let uri = new System.Uri("/docProps/app.xml", System.UriKind.Relative); + if package.PartExists(uri) then + let part = package.GetPart(uri) + use stream = part.GetStream() + use reader = System.Xml.XmlReader.Create(stream) + let ns = System.Xml.Linq.XNamespace.Get("http://schemas.openxmlformats.org/officeDocument/2006/extended-properties") + let root = System.Xml.Linq.XElement.Load(reader) + let app = root.Element(ns + "Application") + if app <> null then + Some app.Value + else None + else + None + + let fixLibrePackage (package : Packaging.Package) = + + let uri = new System.Uri("/xl/webextensions/taskpanes.xml", System.UriKind.Relative); + + package.DeletePart(uri) + package.CreatePart(uri,contentType = "application/vnd.ms-office.webextensiontaskpanes+xml") + |> ignore + + + let isLibrePackage (package : Packaging.Package) = + match tryGetApplication package with + | Some app -> app.Contains "LibreOffice" + | None -> false \ No newline at end of file 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..ed286dd0 --- /dev/null +++ b/src/FsSpreadsheet.ExcelIO/Stylesheet.fs @@ -0,0 +1,215 @@ +namespace FsSpreadsheet.ExcelIO + +open DocumentFormat.OpenXml.Spreadsheet +open DocumentFormat.OpenXml.Packaging +open DocumentFormat.OpenXml + +module Stylesheet = + + module Font = + + let getDefault() = + + Font( + FontSize = FontSize(Val = DoubleValue(11.)), + Color = Color(Theme = UInt32Value(uint32 1)), + FontName = FontName(Val = StringValue("Calibri")), + FontFamilyNumbering = FontFamilyNumbering(Val = Int32Value(int32 2)), + FontScheme = FontScheme(Val = EnumValue(FontSchemeValues.Minor)) + ) + + let updateCount (stylesheet : Stylesheet) = + let newCount = stylesheet.Fonts.Elements() |> Seq.length + stylesheet.Fonts.Count <- UInt32Value(uint32 newCount) + + let initDefaultFonts() = + let f = Fonts(Count = UInt32Value(1ul)) + f.AppendChild(getDefault()) |> ignore + f + + module Fill = + + let getDefault() = + Fill( + PatternFill = PatternFill(PatternType = EnumValue(PatternValues.None)) + ) + + let updateCount (stylesheet : Stylesheet) = + let newCount = stylesheet.Fills.Elements() |> Seq.length + stylesheet.Fills.Count <- UInt32Value(uint32 newCount) + + let initDefaultFills() = + let f = Fills(Count = UInt32Value(1ul)) + f.AppendChild(getDefault()) |> ignore + f + + module Border = + + let getDefault() = + Border( + LeftBorder = LeftBorder(), + RightBorder = RightBorder(), + TopBorder = TopBorder(), + BottomBorder = BottomBorder(), + DiagonalBorder = DiagonalBorder() + ) + + let updateCount (stylesheet : Stylesheet) = + let newCount = stylesheet.Borders.Elements() |> Seq.length + stylesheet.Borders.Count <- UInt32Value(uint32 newCount) + + let initDefaultBorders() = + let f = Borders(Count = UInt32Value(1ul)) + f.AppendChild(getDefault()) |> ignore + f + + module NumberingFormat = + + let get (id : int) (stylesheet : Stylesheet) = + stylesheet.NumberingFormats.Elements() + |> Seq.find (fun nf -> nf.NumberFormatId.Value = uint32 id) + + let tryGet (id : int) (stylesheet : Stylesheet) = + try + get id stylesheet + |> Some + with + | _ -> None + + let getFormatCode (nf : NumberingFormat) = + nf.FormatCode.Value + + // Libre does set default numbers to "General" custom format code, so we need to check for that. + // Floats for example are set to "0.00", so we need a whitelist for isDateTime instead of a blacklist for any else. + // https://stackoverflow.com/a/72012646/12858021 + let isDateTime (nf : NumberingFormat) = + let format = getFormatCode nf + let input = System.DateTime.Now.ToString(format, System.Globalization.CultureInfo.InvariantCulture) + let dt = System.DateTime.ParseExact( + input, + format, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.NoCurrentDateDefault + ) + dt <> Unchecked.defaultof + + module CellFormat = + + let isDateTime (stylesheet : Stylesheet) (cf : CellFormat) = + // 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] |> List.map (uint32 >> UInt32Value) + let customFormats = [164 .. 180] |> List.map (uint32 >> UInt32Value) + if List.contains cf.NumberFormatId dateTimeFormats then + true + elif List.contains cf.NumberFormatId customFormats then + NumberingFormat.tryGet (cf.NumberFormatId.Value |> int) stylesheet + |> Option.map NumberingFormat.isDateTime + |> Option.defaultValue true + else + false + + let structurallyEquals (cf1 : CellFormat) (cf2 : CellFormat) = + cf1.BorderId = cf2.BorderId + && cf1.FillId = cf2.FillId + && cf1.FontId = cf2.FontId + && cf1.NumberFormatId = cf2.NumberFormatId + && cf1.ApplyNumberFormat = cf2.ApplyNumberFormat + + 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 tryGetIndex (cellFormat : CellFormat) (stylesheet : Stylesheet) = + if stylesheet.CellFormats = null then None + else + stylesheet.CellFormats.Elements() + |> Seq.tryFindIndex (structurallyEquals cellFormat) + + 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 + + let appendOrGetIndex (cf : CellFormat) (stylesheet : Stylesheet) = + match tryGetIndex cf stylesheet with + | Some i -> i + | None -> + append cf stylesheet + updateCount stylesheet + (count stylesheet) - 1 + + let getDefault () = + CellFormat( + NumberFormatId = UInt32Value(0ul), + FontId = UInt32Value(0ul), + FillId = UInt32Value(0ul), + BorderId = UInt32Value(0ul) + //FormatId = UInt32Value(0ul) + ) + + let getDefaultDate () = + CellFormat( + NumberFormatId = UInt32Value(14ul), + FontId = UInt32Value(0ul), + FillId = UInt32Value(0ul), + BorderId = UInt32Value(0ul), + //FormatId = UInt32Value(0ul), + ApplyNumberFormat = BooleanValue(true) + ) + + let getDefaultDateTime () = + CellFormat( + NumberFormatId = UInt32Value(22ul), + FontId = UInt32Value(0ul), + FillId = UInt32Value(0ul), + BorderId = UInt32Value(0ul), + //FormatId = UInt32Value(0ul), + ApplyNumberFormat = BooleanValue(true) + ) + + let initDefaultCellFormats() = + let f = CellFormats(Count = UInt32Value(1ul)) + f.AppendChild(getDefault()) |> ignore + f + + 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 <- CellFormat.initDefaultCellFormats() + ssp.Stylesheet.Borders <- Border.initDefaultBorders() + ssp.Stylesheet.Fills <- Fill.initDefaultFills() + ssp.Stylesheet.Fonts <- Font.initDefaultFonts() + ssp.Stylesheet + | ssp -> ssp.Stylesheet + + let tryGet (doc : SpreadsheetDocument) = + match doc.WorkbookPart.WorkbookStylesPart with + | null -> None + | ssp -> Some(ssp.Stylesheet) \ No newline at end of file diff --git a/src/FsSpreadsheet.Exceljs/Cell.fs b/src/FsSpreadsheet.Exceljs/Cell.fs new file mode 100644 index 00000000..6fec03d2 --- /dev/null +++ b/src/FsSpreadsheet.Exceljs/Cell.fs @@ -0,0 +1,88 @@ +namespace FsSpreadsheet.Exceljs + + +module JsCell = + + open Fable.Core + open Fable.Core.JsInterop + open FsSpreadsheet + open Fable.ExcelJs + + [] + let private log (obj:obj) = jsNative + + let writeFromFsCell (fsCell: FsCell) = + match fsCell.DataType with + | Boolean -> + fsCell.ValueAsBool() |> box |> Some + | Number -> + fsCell.ValueAsFloat() |> box |> Some + | Date -> + /// Here it will actually show the correct DateTime. But when writing, exceljs will apply local offset. + let dt = fsCell.ValueAsDateTime() |> System.DateTimeOffset + /// Therefore we add offset and it should work. + let dt = dt + dt.Offset |> box |> Some + dt + | String -> + fsCell.Value |> Some + | anyElse -> + let msg = sprintf "ValueType '%A' is not fully implemented in FsSpreadsheet and is handled as string input." anyElse + #if FABLE_COMPILER_JAVASCRIPT + log msg + #else + printfn "%s" msg + #endif + fsCell.Value |> box |> Some + + /// + /// `worksheetName`, `rowIndex` and `columnIndex` are only used for debugging. + /// + /// + /// + /// + /// + let readToFsCell worksheetName rowIndex columnIndex (jsCell: Cell) = + let t = enum(jsCell.``type``) + let fsadress = FsAddress(jsCell.address) + let createFscell = fun dt v -> FsCell(v,dt,address = fsadress) + let vTemp = string jsCell.value.Value + let fscell = + match t with + | ValueType.Boolean -> + let b = System.Boolean.Parse vTemp + createFscell DataType.Boolean b + | ValueType.Number -> float vTemp |> createFscell DataType.Number + | ValueType.Date -> + let dt = System.DateTime.Parse(vTemp).ToUniversalTime() + /// Without this step universal time get changed to local time? Exceljs tests will hit this. + /// + /// Expected item (from test object): C2 : Sat Oct 14 2023 00:00:00 GMT+0200 (Mitteleuropäische Sommerzeit) | Date + /// + /// Actual item (created here): C2 : Sat Oct 14 2023 02:00:00 GMT+0200 (Mitteleuropäische Sommerzeit) | Date + /// + /// But logging hour minute showed that the values were given correctly and needed to be reinitialized. + let dt = System.DateTime(dt.Year,dt.Month,dt.Day,dt.Hour,dt.Minute, dt.Second) + dt |> createFscell DataType.Date + | ValueType.String -> vTemp |> createFscell DataType.String + | ValueType.Formula -> + match jsCell.formula with + | "TRUE()" -> + let b = true + createFscell DataType.Boolean b + | "FALSE()" -> + let b = false + createFscell DataType.Boolean b + | anyElse -> + let msg = sprintf "ValueType 'Format' (%s) is not fully implemented in FsSpreadsheet and is handled as string input. In %s: (%i,%i)" anyElse worksheetName rowIndex columnIndex + log msg + anyElse |> createFscell DataType.String + | ValueType.Hyperlink -> + //log (c.value.Value?text) + jsCell.value.Value?hyperlink |> createFscell DataType.String + | anyElse -> + let msg = sprintf "ValueType `%A` (%s) is not fully implemented in FsSpreadsheet and is handled as string input. In %s: (%i,%i)" anyElse vTemp worksheetName rowIndex columnIndex + log msg + vTemp |> createFscell DataType.String + fscell + + diff --git a/src/FsSpreadsheet.Exceljs/FsSpreadsheet.Exceljs.fsproj b/src/FsSpreadsheet.Exceljs/FsSpreadsheet.Exceljs.fsproj index 6fd50397..a568732c 100644 --- a/src/FsSpreadsheet.Exceljs/FsSpreadsheet.Exceljs.fsproj +++ b/src/FsSpreadsheet.Exceljs/FsSpreadsheet.Exceljs.fsproj @@ -1,52 +1,51 @@ - - - - Kevin Frey, Heinrich Lukas Weil, Oliver Maus, Kevin Schneider, Timo Mühlhaus - Excel IO Extensions for the FsSpreadsheet Datamodel in js environments using exceljs. - Spreadsheet creation and manipulation in FSharp - MIT - logo.png - F# FSharp spreadsheet Excel xlsx datascience fable fable-library fable-javascript - https://github.com/CSBiology/FsSpreadsheet - git - - - - net6.0 - true - - - + + + + Kevin Frey, Heinrich Lukas Weil, Oliver Maus, Kevin Schneider, Timo Mühlhaus + Excel IO Extensions for the FsSpreadsheet Datamodel in js environments using exceljs. + Spreadsheet creation and manipulation in FSharp + MIT + logo.png + F# FSharp spreadsheet Excel xlsx datascience fable fable-library fable-javascript + https://github.com/CSBiology/FsSpreadsheet + git + + + + net6.0 + true + + + \ true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FsSpreadsheet.Exceljs/Table.fs b/src/FsSpreadsheet.Exceljs/Table.fs index ad206b86..58359f08 100644 --- a/src/FsSpreadsheet.Exceljs/Table.fs +++ b/src/FsSpreadsheet.Exceljs/Table.fs @@ -10,12 +10,12 @@ module JsTable = [] let private log (obj:obj) = jsNative - let fromFsTable (fscellcollection: FsCellsCollection) (fsTable: FsTable) : Table = + let writeFromFsTable (fscellcollection: FsCellsCollection) (fsTable: FsTable) : Table = let fsColumns = fsTable.GetColumns fscellcollection let columns = if fsTable.ShowHeaderRow then - [| for headerCell in fsTable.HeadersRow().Cells(fscellcollection) do - yield TableColumn(headerCell.Value) |] + [| for headerCell in fsTable.GetHeaderRow(fscellcollection) do + yield TableColumn(headerCell.ValueAsString()) |] else [| for i in 1 .. Seq.length fsColumns do yield TableColumn(string i) @@ -24,31 +24,27 @@ module JsTable = [| for col in fsColumns do let cells = if fsTable.ShowHeaderRow then col.Cells |> Seq.tail else col.Cells - yield! cells |> Seq.mapi (fun i c -> - let rowValue = - match c.DataType with - | Boolean -> c.ValueAsBool() |> box - | Number -> c.ValueAsFloat() |> box - | Date -> c.ValueAsDateTime() |> box - | String -> c.Value |> box - | anyElse -> - let msg = sprintf "ValueType '%A' is not fully implemented in FsSpreadsheet and is handled as string input." anyElse - #if FABLE_COMPILER_JAVASCRIPT - log msg - #else - printfn "%s" msg - #endif - c.Value |> box - i+1, rowValue + yield! cells |> Seq.map (fun c -> + let rowValue = JsCell.writeFromFsCell c |> Option.get + c.Address.RowNumber, (c.Address.ColumnNumber, rowValue) ) |] |> Array.groupBy fst |> Array.sortBy fst |> Array.map (fun (_,arr) -> - arr |> Array.map snd + let m = arr |> Array.map snd |> Map + let row = [|fsTable.RangeAddress.FirstAddress.ColumnNumber .. fsTable.RangeAddress.FirstAddress.ColumnNumber + (columns.Length-1)|] + let row = row |> Array.map (fun i -> m.TryFind i |> box) + row ) let defaultStyle = {| - theme = "TableStyleMedium7" showRowStripes = true |} - Table(fsTable.Name,fsTable.RangeAddress.Range,columns,rows,fsTable.Name,headerRow = fsTable.ShowHeaderRow, style = defaultStyle) \ No newline at end of file + Table(fsTable.Name,fsTable.RangeAddress.Range,columns,rows,fsTable.Name,headerRow = fsTable.ShowHeaderRow, style = defaultStyle) + + let readToFsTable(table:ITableRef) = + let table = table.table.Value + let tableRef = table.tableRef |> FsRangeAddress + let tableName = if isNull table.displayName then table.name else table.displayName + let table = FsTable(tableName, tableRef, table.totalsRow, table.headerRow) + table \ No newline at end of file diff --git a/src/FsSpreadsheet.Exceljs/Workbook.fs b/src/FsSpreadsheet.Exceljs/Workbook.fs index 6c569e03..a53c3d2e 100644 --- a/src/FsSpreadsheet.Exceljs/Workbook.fs +++ b/src/FsSpreadsheet.Exceljs/Workbook.fs @@ -13,15 +13,15 @@ module JsWorkbook = [] let private log (obj:obj) = jsNative - let toFsWorkbook (jswb: Workbook) = - let fswb = new FsWorkbook() - for jsws in jswb.worksheets do - JsWorksheet.addJsWorksheet fswb jsws - fswb - - let fromFsWorkbook (fswb: FsWorkbook) = + let writeFromFsWorkbook (fswb: FsWorkbook) = let jswb = ExcelJs.Excel.Workbook() jswb?_themes <- Aux.theme1 for fsws in fswb.GetWorksheets() do - JsWorksheet.addFsWorksheet jswb fsws + JsWorksheet.writeFromFsWorksheet jswb fsws jswb + + let readToFsWorkbook (jswb: Workbook) = + let fswb = new FsWorkbook() + for jsws in jswb.worksheets do + JsWorksheet.readToFsWorksheet fswb jsws + fswb diff --git a/src/FsSpreadsheet.Exceljs/Worksheet.fs b/src/FsSpreadsheet.Exceljs/Worksheet.fs index 54f50ff4..ecbcfb09 100644 --- a/src/FsSpreadsheet.Exceljs/Worksheet.fs +++ b/src/FsSpreadsheet.Exceljs/Worksheet.fs @@ -10,68 +10,32 @@ module JsWorksheet = open FsSpreadsheet open Fable.ExcelJs - open Fable.Core.JsInterop - let addFsWorksheet (wb: Workbook) (fsws:FsWorksheet) : unit = + let writeFromFsWorksheet (wb: Workbook) (fsws:FsWorksheet) : unit = fsws.RescanRows() let rows = fsws.Rows |> Seq.map (fun x -> x.Cells) let ws = wb.addWorksheet(fsws.Name) // due to the design of fsspreadsheet this might overwrite some of the stuff from tables, // but as it should be the same, this is only a performance sink. for row in rows do - for cell in row do - let c = ws.getCell(cell.Address.Address) - match cell.DataType with - | Boolean -> - c.value <- cell.ValueAsBool() |> box |> Some - | Number -> - c.value <- cell.ValueAsFloat() |> box |> Some - | Date -> - c.value <- cell.ValueAsDateTime() |> box |> Some - | String -> - c.value <- cell.Value |> box |> Some - | anyElse -> - let msg = sprintf "ValueType '%A' is not fully implemented in FsSpreadsheet and is handled as string input." anyElse - #if FABLE_COMPILER_JAVASCRIPT - log msg - #else - printfn "%s" msg - #endif - c.value <- cell.Value |> box |> Some - let tables = fsws.Tables |> Seq.map (fun table -> JsTable.fromFsTable fsws.CellCollection table) + for fsCell in row do + let jsCell = ws.getCell(fsCell.Address.Address) + jsCell.value <- JsCell.writeFromFsCell fsCell + let tables = fsws.Tables |> Seq.map (fun table -> JsTable.writeFromFsTable fsws.CellCollection table) for table in tables do ws.addTable(table) |> ignore - let addJsWorksheet (wb: FsWorkbook) (jsws: Worksheet) : unit = + let readToFsWorksheet (wb: FsWorkbook) (jsws: Worksheet) : unit = let fsws = FsWorksheet(jsws.name) jsws.eachRow(fun (row, rowIndex) -> - row.eachCell(fun (c, rowIndex) -> + row.eachCell(fun (c, columnIndex) -> if c.value.IsSome then - let t = enum(c.``type``) - let fsadress = FsAddress(c.address) - let createFscell = fun dt v -> FsCell(v,dt,address = fsadress) - let vTemp = string c.value.Value - let fscell = - match t with - | ValueType.Boolean -> System.Boolean.Parse(vTemp) |> createFscell DataType.Boolean - | ValueType.Number -> float vTemp |> createFscell DataType.Number - | ValueType.Date -> System.DateTime.Parse(vTemp) |> createFscell DataType.Date - | ValueType.String -> vTemp |> createFscell DataType.String - | anyElse -> - let msg = sprintf "ValueType '%A' is not fully implemented in FsSpreadsheet and is handled as string input." anyElse - #if FABLE_COMPILER_JAVASCRIPT - log msg - #else - printfn "%s" msg - #endif - vTemp |> createFscell DataType.String - fsws.AddCell(fscell) |> ignore + let fsCell = JsCell.readToFsCell jsws.name rowIndex columnIndex c + fsws.AddCell(fsCell) |> ignore ) ) for jstableref in jsws.getTables() do - let table = jstableref.table.Value - let tableRef = table.tableRef |> FsRangeAddress - let table = FsTable(table.name, tableRef, table.totalsRow, table.headerRow) + let table = JsTable.readToFsTable jstableref fsws.AddTable table |> ignore fsws.RescanRows() wb.AddWorksheet(fsws) \ No newline at end of file diff --git a/src/FsSpreadsheet.Exceljs/Xlsx.fs b/src/FsSpreadsheet.Exceljs/Xlsx.fs index 850733de..aa5af836 100644 --- a/src/FsSpreadsheet.Exceljs/Xlsx.fs +++ b/src/FsSpreadsheet.Exceljs/Xlsx.fs @@ -14,7 +14,7 @@ type Xlsx = promise { let wb = ExcelJs.Excel.Workbook() do! wb.xlsx.readFile(path) - let fswb = JsWorkbook.toFsWorkbook wb + let fswb = JsWorkbook.readToFsWorkbook wb return fswb } @@ -22,7 +22,7 @@ type Xlsx = promise { let wb = ExcelJs.Excel.Workbook() do! wb.xlsx.read stream - return JsWorkbook.toFsWorkbook wb + return JsWorkbook.readToFsWorkbook wb } static member fromBytes (bytes: byte []) : Promise = @@ -30,20 +30,20 @@ type Xlsx = let wb = ExcelJs.Excel.Workbook() let uint8 = Fable.Core.JS.Constructors.Uint8Array.Create bytes do! wb.xlsx.load(uint8.buffer) - return JsWorkbook.toFsWorkbook wb + return JsWorkbook.readToFsWorkbook wb } static member toFile (path: string) (wb:FsWorkbook) : Promise = - let jswb = JsWorkbook.fromFsWorkbook wb + let jswb = JsWorkbook.writeFromFsWorkbook wb jswb.xlsx.writeFile(path) static member toStream (stream: System.IO.Stream) (wb:FsWorkbook) : Promise = - let jswb = JsWorkbook.fromFsWorkbook wb + let jswb = JsWorkbook.writeFromFsWorkbook wb jswb.xlsx.write(stream) static member toBytes (wb:FsWorkbook) : Promise = promise { - let jswb = JsWorkbook.fromFsWorkbook wb + let jswb = JsWorkbook.writeFromFsWorkbook wb let buffer = jswb.xlsx.writeBuffer() return !!buffer } diff --git a/src/FsSpreadsheet.Interactive/Formatters.fs b/src/FsSpreadsheet.Interactive/Formatters.fs index 8c1fc96d..2d419364 100644 --- a/src/FsSpreadsheet.Interactive/Formatters.fs +++ b/src/FsSpreadsheet.Interactive/Formatters.fs @@ -34,7 +34,7 @@ module Formatters = let cells = worksheet.CellCollection.GetCells() |> Seq.map (fun c -> - c.RowNumber - 1, c.ColumnNumber - 1, c.Value + c.RowNumber - 1, c.ColumnNumber - 1, c.ValueAsString() ) let matrix = FsSparseMatrix.init diff --git a/src/FsSpreadsheet/Cells/FsCell.fs b/src/FsSpreadsheet/Cells/FsCell.fs index 1165a465..0bf90834 100644 --- a/src/FsSpreadsheet/Cells/FsCell.fs +++ b/src/FsSpreadsheet/Cells/FsCell.fs @@ -4,6 +4,15 @@ open System open Fable.Core +//type Hyperlink = { +// Text: string +// Hyperlink: string +//} with +// static member create(text, hyperlink) = { +// Text = text +// Hyperlink = hyperlink +// } + /// /// Possible DataTypes used in a FsCell. /// @@ -12,6 +21,7 @@ type DataType = | Boolean | Number | Date + //| Hyperlink | Empty /// @@ -20,34 +30,40 @@ type DataType = static member inline InferCellValue (value : 'T) = let value = box value match value with - | :? char as c -> DataType.String,c.ToString() - | :? bool as true -> DataType.Boolean, "True" - | :? bool as false -> DataType.Boolean, "False" - | :? byte as i -> DataType.Number,i.ToString() - | :? sbyte as i -> DataType.Number,i.ToString() - | :? int as i -> DataType.Number,i.ToString() - | :? int16 as i -> DataType.Number,i.ToString() - | :? int64 as i -> DataType.Number,i.ToString() - | :? uint as i -> DataType.Number,i.ToString() - | :? uint16 as i -> DataType.Number,i.ToString() - | :? uint64 as i -> DataType.Number,i.ToString() - | :? single as i -> DataType.Number,i.ToString() - | :? float as i -> DataType.Number,i.ToString() - | :? decimal as i -> DataType.Number,i.ToString() - | :? System.DateTime as d -> DataType.Date,d.ToString() - | :? string as s -> DataType.String,s.ToString() - | _ -> DataType.String,value.ToString() + //| :? Hyperlink as hpl -> DataType.Hyperlink, value + | :? char as c -> DataType.String, value + | :? bool as true -> DataType.Boolean, true + | :? bool as false -> DataType.Boolean, false + | :? byte as i -> DataType.Number, value + | :? sbyte as i -> DataType.Number, value + | :? int as i -> DataType.Number, value + | :? int16 as i -> DataType.Number, value + | :? int64 as i -> DataType.Number, value + | :? uint as i -> DataType.Number, value + | :? uint16 as i -> DataType.Number,value + | :? uint64 as i -> DataType.Number,value + | :? single as i -> DataType.Number,value + | :? float as i -> DataType.Number,value + | :? decimal as i -> DataType.Number,value + | :? System.DateTime as d -> DataType.Date,value + | :? string as s -> DataType.String,value + | _ -> DataType.String,value // Type based on the type XLCell used in ClosedXml /// /// Creates an FsCell of `DataType` dataType, with value of type `string`, and `FsAddress` address. /// +module FsCellAux = + + let boolConverter (bool:bool) = + match bool with | true -> "1" | false -> "0" + [] -type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = +type FsCell (value : obj, ?dataType : DataType, ?address : FsAddress) = // TODO: Maybe save as IConvertible - let mutable _cellValue = string value + let mutable _cellValue : obj = value let mutable _dataType = dataType |> Option.defaultValue DataType.String let mutable _comment = "" let mutable _hyperlink = "" @@ -58,9 +74,9 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = let mutable _rowIndex : int = address |> Option.map (fun a -> a.RowNumber) |> Option.defaultValue 0 let mutable _columnIndex : int = address |> Option.map (fun a -> a.ColumnNumber) |> Option.defaultValue 0 - + new(value: IConvertible, ?dataType : DataType, ?address : FsAddress) = FsCell(box value, ?dataType = dataType, ?address = address) /// Creates an empty FsCell, set at row 0, column 0 (1-based). - static member empty () = FsCell ("", DataType.Empty, FsAddress(0,0)) + static member inline empty () = FsCell ("", DataType.Empty, FsAddress(0,0)) ///// Creates an FsCell of `DataType` `Number`, with the given value, set at row 1, column 1 (1-based). //new (value : int) = FsCell (string value, DataType.Number, FsAddress(0,0)) @@ -158,8 +174,8 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// /// Creates an FsCell with the given DataType, rowNumber, colNumber, and value. /// - static member createWithDataType (dataType : DataType) (rowNumber : int) (colNumber : int) value = - FsCell(value, dataType, FsAddress(rowNumber, colNumber)) + static member createWithDataType (dataType : DataType) (rowNumber : int) (colNumber : int) (value: obj) = + FsCell(value, dataType, FsAddress(rowNumber, colNumber)) //how 2: //return (format.ToUpper()) switch @@ -222,8 +238,17 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// /// Gets the value as string /// - member self.ValueAsString() = - self.Value + member self.ValueAsString() : string = + let v = self.Value + match self.DataType with + | DataType.String | DataType.Date | DataType.Boolean | DataType.Empty -> + v.ToString() + | Number -> + // Example: 4.123: + // - (4.123)ToString() will parse floats in germany to "4,123" which is not allowed by Excel. + // - string(4.123) will parse floats in germany to "4.123" which is allowed by Excel. + // TODO: Maybe swap to (90.213).ToString(new Globalization.CultureInfo("en-US") ) // val it: string = "90.213" + string v /// /// Gets the value as string @@ -235,7 +260,10 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as bool /// member self.ValueAsBool() = - bool.Parse (self.Value) + match (string self.Value).ToLower() with + | "1" | "true" | "true()" -> true + | "0" | "false" | "false()" -> false + | anyElse -> raise (System.FormatException($"String '{anyElse}' was not recognized as a valid Boolean")) /// /// Gets the value as bool @@ -247,7 +275,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as float /// member self.ValueAsFloat() = - Double.Parse (self.Value) + Double.Parse (string self.Value) /// /// Gets the value as float @@ -259,7 +287,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as int /// member self.ValueAsInt() = - Int32.Parse (self.Value) + Int32.Parse (string self.Value) /// /// Gets the value as int @@ -271,7 +299,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as uint /// member self.ValueAsUInt() = - UInt32.Parse (self.Value) + UInt32.Parse (string self.Value) /// /// Gets the value as uint @@ -283,7 +311,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as long /// member self.ValueAsLong() = - Int64.Parse (self.Value) + Int64.Parse (string self.Value) /// /// Gets the value as long @@ -295,7 +323,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as ulong /// member self.ValueAsULong() = - UInt64.Parse (self.Value) + UInt64.Parse (string self.Value) /// /// Gets the value as ulong @@ -307,7 +335,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as double /// member self.ValueAsDouble() = - Double.Parse (self.Value) + Double.Parse (string self.Value) /// /// Gets the value as double @@ -319,7 +347,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as decimal /// member self.ValueAsDecimal() = - Decimal.Parse (self.Value) + Decimal.Parse (string self.Value) /// /// Gets the value as decimal @@ -331,7 +359,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as DateTime /// member self.ValueAsDateTime() = - DateTime.Parse (self.Value) + DateTime.Parse (string self.Value) /// /// Gets the value as DateTime @@ -343,7 +371,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as Guid /// member self.ValueAsGuid() = - Guid.Parse (self.Value) + Guid.Parse (string self.Value) /// /// Gets the value as Guid @@ -355,7 +383,7 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = /// Gets the value as char /// member self.ValueAsChar() = - Char.Parse (self.Value) + Char.Parse (string self.Value) /// /// Gets the value as char @@ -385,3 +413,24 @@ type FsCell (value : IConvertible, ?dataType : DataType, ?address : FsAddress) = static member setValueAs<'T> value (cell : FsCell)= cell.SetValueAs<'T>(value) cell + + member this.StructurallyEquals (other: FsCell) = + let r = + [| + this.Value = other.Value + this.DataType = other.DataType + [| + this.Address.Address = other.Address.Address + this.Address.ColumnNumber = other.Address.ColumnNumber + this.Address.RowNumber = other.Address.RowNumber + this.Address.FixedColumn = other.Address.FixedColumn + this.Address.FixedRow = other.Address.FixedRow + |] + |> Seq.forall (fun x -> x=true) + this.ColumnNumber = other.ColumnNumber + this.RowNumber = other.RowNumber + |] + |> Seq.forall (fun x -> x=true) + r + + diff --git a/src/FsSpreadsheet/DSL/CellBuilder.fs b/src/FsSpreadsheet/DSL/CellBuilder.fs index 061e7857..d540ae86 100644 --- a/src/FsSpreadsheet/DSL/CellBuilder.fs +++ b/src/FsSpreadsheet/DSL/CellBuilder.fs @@ -14,6 +14,7 @@ type ReduceOperation = values |> List.map (snd >> string) |> List.reduce (fun a b -> $"{a}{separator}{b}") + |> box type CellBuilder() = diff --git a/src/FsSpreadsheet/DSL/Transform.fs b/src/FsSpreadsheet/DSL/Transform.fs index df175acf..bdcce510 100644 --- a/src/FsSpreadsheet/DSL/Transform.fs +++ b/src/FsSpreadsheet/DSL/Transform.fs @@ -72,7 +72,7 @@ module Transform = match col with | [] -> failwith "Empty column" | header :: fields -> - let field = table.Field(snd header, cellCollection) + let field = table.Field(snd >> string <| header, cellCollection) fields |> List.iteri (fun i (dataType,value) -> let cell = field.Column.Cell(i + 2,cellCollection) diff --git a/src/FsSpreadsheet/DSL/Types.fs b/src/FsSpreadsheet/DSL/Types.fs index 63f16906..47d90a62 100644 --- a/src/FsSpreadsheet/DSL/Types.fs +++ b/src/FsSpreadsheet/DSL/Types.fs @@ -95,7 +95,7 @@ module SheetEntityExtensions = failwith $"SheetEntity of type {typeof<'T>.Name} does not contain Value: \n\t{appendedMessages}" #endif -type Value = DataType * string +type Value = DataType * obj type CellElement = Value * int option diff --git a/src/FsSpreadsheet/FsColumn.fs b/src/FsSpreadsheet/FsColumn.fs index c4092547..0ba979ec 100644 --- a/src/FsSpreadsheet/FsColumn.fs +++ b/src/FsSpreadsheet/FsColumn.fs @@ -114,7 +114,8 @@ type FsColumn (rangeAddress : FsRangeAddress, cells : FsCellsCollection)= /// /// Checks if there is an FsCell at given row index of a given FsColumn. /// - /// The number of the row where the presence of an FsCell shall be checked. + /// The number of the row where the presence of an FsCell shall be checked. + /// static member hasCellAt rowIndex (column : FsColumn) = column.HasCellAt rowIndex @@ -143,6 +144,7 @@ type FsColumn (rangeAddress : FsRangeAddress, cells : FsCellsCollection)= /// Returns the FsCell at the given rowIndex if it exists in the given FsColumn. Else returns None. /// /// The number of the column where the FsCell shall be retrieved. + /// static member tryItem rowIndex (column : FsColumn) = column.TryItem rowIndex diff --git a/src/FsSpreadsheet/FsRow.fs b/src/FsSpreadsheet/FsRow.fs index ce713817..35f8b6cd 100644 --- a/src/FsSpreadsheet/FsRow.fs +++ b/src/FsSpreadsheet/FsRow.fs @@ -117,6 +117,7 @@ type FsRow (rangeAddress : FsRangeAddress, cells : FsCellsCollection)= /// Checks if there is an FsCell at given column index of a given FsRow. /// /// The number of the column where the presence of an FsCell shall be checked. + /// static member hasCellAt colIndex (row : FsRow) = row.HasCellAt colIndex @@ -145,6 +146,7 @@ type FsRow (rangeAddress : FsRangeAddress, cells : FsCellsCollection)= /// Returns the FsCell at the given columnIndex if it exists in the given FsRow. Else returns None. /// /// The number of the column where the FsCell shall be retrieved. + /// static member tryItem colIndex (row : FsRow) = row.TryItem colIndex diff --git a/src/FsSpreadsheet/FsWorksheet.fs b/src/FsSpreadsheet/FsWorksheet.fs index 33001626..154eba89 100644 --- a/src/FsSpreadsheet/FsWorksheet.fs +++ b/src/FsSpreadsheet/FsWorksheet.fs @@ -517,7 +517,16 @@ type FsWorksheet (name, ?fsRows, ?fsTables, ?fsCellsCollection) = /// /// If a cell exists at the given postion, it is shoved to the right. /// - member self.InsertValueAt(value : 'a, rowIndex, colIndex)= + member self.InsertValueAt(value : System.IConvertible, rowIndex, colIndex)= + let cell = FsCell(value) + self.CellCollection.Add(int32 rowIndex, int32 colIndex, cell) + + /// + /// Adds a value at the given row- and columnIndex to the FsWorksheet. + /// + /// If a cell exists at the given postion, it is shoved to the right. + /// + member self.InsertValueAt(value : obj, rowIndex, colIndex)= let cell = FsCell(value) self.CellCollection.Add(int32 rowIndex, int32 colIndex, cell) diff --git a/src/FsSpreadsheet/SheetBuilder.fs b/src/FsSpreadsheet/SheetBuilder.fs index 799e14f5..56506936 100644 --- a/src/FsSpreadsheet/SheetBuilder.fs +++ b/src/FsSpreadsheet/SheetBuilder.fs @@ -91,11 +91,11 @@ module SheetBuilder = let headerCell = FsCell.createEmpty() for header in field.HeaderTransformers do ignore (header row headerCell) - let headerString = - if headerCell.Value = "" then + if headerCell.ValueAsString() = "" then field.Hash - else headerCell.Value + else + headerCell.ValueAsString() let tableField = self.Field(headerString,cells) @@ -152,7 +152,7 @@ module SheetBuilder = let hasHeader, headerString = if headerCell.Value = "" then false, field.Hash - else true, headerCell.Value + else true, headerCell.ValueAsString() match Dictionary.tryGetValue (headerString) headers with | Some int -> int diff --git a/src/FsSpreadsheet/Tables/FsTable.fs b/src/FsSpreadsheet/Tables/FsTable.fs index 6d48109f..c61cee0c 100644 --- a/src/FsSpreadsheet/Tables/FsTable.fs +++ b/src/FsSpreadsheet/Tables/FsTable.fs @@ -59,10 +59,26 @@ type FsTable (name : string, rangeAddress : FsRangeAddress, ?showTotalsRow : boo /// /// Returns the header row as FsRangeRow. Scans for new fieldnames. /// + [] member this.HeadersRow() = if (not this.ShowHeaderRow) then null; else - FsRange(base.RangeAddress).FirstRow(); + FsRange(base.RangeAddress).FirstRow() + + member this.TryGetHeaderRow(cellsCollection) = + match this.ShowHeaderRow with + | false -> None + | true -> + let rowIndex = this.RangeAddress.FirstAddress.RowNumber + let firstAddress = FsAddress(rowIndex, this.RangeAddress.FirstAddress.ColumnNumber) + let lastAddress = FsAddress(rowIndex, this.RangeAddress.LastAddress.ColumnNumber) + let range = FsRangeAddress (firstAddress, lastAddress) + FsRow(range, cellsCollection) |> Some + + member this.GetHeaderRow(cellsCollection) = + match this.TryGetHeaderRow(cellsCollection) with + | Some hr -> hr + | None -> failwith $"""Error. Unable to get header row for table "{this.Name}" as `ShowHeaderRow` is set to `false`.""" /// /// Returns the FsColumns from the FsTable. @@ -420,10 +436,10 @@ type FsTable (name : string, rangeAddress : FsRangeAddress, ?showTotalsRow : boo if this.ShowHeaderRow then let oldFieldNames = _fieldNames _fieldNames <- new Dictionary() - let headersRow = this.HeadersRow(); + let headersRow = this.GetHeaderRow(cellsCollection); let mutable cellPos = 0 - for cell in headersRow.Cells(cellsCollection) do - let mutable name = cell.Value //GetString(); + for cell in headersRow do + let mutable name = cell.ValueAsString() //GetString(); match Dictionary.tryGet name oldFieldNames with | Some tableField -> tableField.Index <- cellPos diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/DefaultIO.Tests.fs b/tests/FsSpreadsheet.ExcelIO.Tests/DefaultIO.Tests.fs new file mode 100644 index 00000000..6285cd8f --- /dev/null +++ b/tests/FsSpreadsheet.ExcelIO.Tests/DefaultIO.Tests.fs @@ -0,0 +1,53 @@ +module DefaultIO + +open Expecto +open TestingUtils +open FsSpreadsheet +open FsSpreadsheet.ExcelIO + +let tests_Read = testList "Read" [ + let readFromTestFile (testFile: DefaultTestObject.TestFiles) = + try + FsWorkbook.fromXlsxFile(testFile.asRelativePath) + with + | _ -> FsWorkbook.fromXlsxFile($"{DefaultTestObject.testFolder}/{testFile.asFileName}") + + testCase "FsCell equality" <| fun _ -> + let c1 = FsCell(1, DataType.Number, FsAddress("A2")) + let c2 = FsCell(1, DataType.Number, FsAddress("A2")) + let isStructEqual = c1.StructurallyEquals(c2) + Expect.isTrue isStructEqual "" + testCase "Excel" <| fun _ -> + let wb = readFromTestFile DefaultTestObject.TestFiles.Excel + Expect.isDefaultTestObject wb + testCase "Libre" <| fun _ -> + let wb = readFromTestFile DefaultTestObject.TestFiles.Libre + Expect.isDefaultTestObject wb + testCase "FableExceljs" <| fun _ -> + let wb = readFromTestFile DefaultTestObject.TestFiles.FableExceljs + Expect.isDefaultTestObject wb + testCase "ClosedXML" <| fun _ -> + let wb = readFromTestFile DefaultTestObject.TestFiles.ClosedXML + Expect.isDefaultTestObject wb + testCase "FsSpreadsheet" <| fun _ -> + let wb = readFromTestFile DefaultTestObject.TestFiles.FsSpreadsheetNET + wb.GetWorksheets().[0].GetCellAt(5,1) |> fun x -> (x.Value, x.DataType) |> printfn "%A" + Expect.isDefaultTestObject wb +] + +let private tests_Write = testList "Write" [ + testCase "default" <| fun _ -> + let wb = DefaultTestObject.defaultTestObject() + let p = DefaultTestObject.WriteTestFiles.FsSpreadsheetNET.asRelativePath + wb.ToFile(p) + let wb_read = FsWorkbook.fromXlsxFile p + Expect.isDefaultTestObject wb_read + +] + +[] +let main = testList "DefaultIO" [ + tests_Read + tests_Write +] + diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj b/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj index 81f9a2bb..d904c7ba 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj +++ b/tests/FsSpreadsheet.ExcelIO.Tests/FsSpreadsheet.ExcelIO.Tests.fsproj @@ -7,7 +7,8 @@ - + + @@ -20,14 +21,18 @@ - - - + - + + + + + + + \ No newline at end of file diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/Cell.fs b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/Cell.fs index 1f6dc97d..22ae6f3c 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/Cell.fs +++ b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/Cell.fs @@ -14,10 +14,9 @@ let sstFox = sstpFox.SharedStringTable let sstFoxInnerText = sstFox.InnerText let wsp1Fox = (wbpFox.WorksheetParts |> Array.ofSeq)[0] let cbsi1Fox = wsp1Fox.Worksheet.Descendants() |> Array.ofSeq -let nullCell = Cell.create Spreadsheet.CellValues.Error "A1" (Cell.CellValue.create "") +let nullCell = Cell.create (Some Spreadsheet.CellValues.Error) "A1" (Cell.CellValue.create "") nullCell.CellValue.Text <- null - [] let cellTests = testList "Cell" [ diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs index 5c3dce4b..5ffc6d5c 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs +++ b/tests/FsSpreadsheet.ExcelIO.Tests/OpenXml/FsExtensions.fs @@ -14,7 +14,7 @@ let dummyDtBoolean = DataType.Boolean let dummyDtDate = DataType.Date let dummyDtEmpty = DataType.Empty -let dummyXlsxCell = Cell.create CellValues.Number "A1" (CellValue(1.337)) +let dummyXlsxCell = Cell.create (Some CellValues.Number) "A1" (CellValue(1.337)) //let testFilePath = @"C:\Repos\CSBiology\FsSpreadsheet\tests\FsSpreadsheet.ExcelIO.Tests\data\testUnit.xlsx" let testFilePath = Path.Combine(__SOURCE_DIRECTORY__, "../data", "testUnit.xlsx") @@ -24,7 +24,7 @@ let dummyFsCells = [ FsCell.create 1 1 "A1" // for sheet1 (StringSheet) FsCell.create 7 3 "7" // for sheet2 (NumericSheet) FsCell.create 2 10 "B10" // for sheet3 (TableSheet) - FsCell.create 2 1 "True" // for sheet4 (DataTypeSheet), DataType.Boolean + FsCell.create 2 1 "1" // for sheet4 (DataTypeSheet), DataType.Boolean FsCell.create 5 1 "03.13.2023" // for sheet4 (DataTypeSheet), DataType.DateTime ] let dummyFsCellsCollection1 = FsCellsCollection() @@ -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" + Expect.equal (testCell.ValueAsString()) 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" ] ] @@ -116,7 +120,7 @@ let fsExtensionTests = Expect.equal d DataType.String "DataType is not DataType.String" testCase "is equal to dummyFsWorkbook in sheet2, cellC7 value" <| fun _ -> let v = (FsWorksheet.getCellAt 7 3 fsWorksheet2FromStream).Value - Expect.equal v "7" "value is not equal" + Expect.equal v 7. "value is not equal" testCase "is equal to dummyFsWorkbook in sheet2, cellC7 address" <| fun _ -> let a = (FsWorksheet.getCellAt 7 3 fsWorksheet2FromStream).Address.Address Expect.equal a "C7" "address is not equal" @@ -135,7 +139,7 @@ let fsExtensionTests = Expect.equal d DataType.String "DataType is not DataType.String" testCase "is equal to dummyFsWorkbook in sheet4, cellA2 value" <| fun _ -> let v = (FsWorksheet.getCellAt 2 1 fsWorksheet4FromStream).Value - Expect.equal v "1" "value is not equal" // should be "True"... why is it not? Maybe bc. it's stored as "1" in the XML and only Excel converts it to "TRUE" on the screen... TO DO: check that. + Expect.equal v true "value is not equal" testCase "is equal to dummyFsWorkbook in sheet4, cellA2 address" <| fun _ -> let a = (FsWorksheet.getCellAt 2 1 fsWorksheet4FromStream).Address.Address Expect.equal a "A2" "address is not equal" diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/Stylesheet.Tests.fs b/tests/FsSpreadsheet.ExcelIO.Tests/Stylesheet.Tests.fs new file mode 100644 index 00000000..0e762f27 --- /dev/null +++ b/tests/FsSpreadsheet.ExcelIO.Tests/Stylesheet.Tests.fs @@ -0,0 +1,35 @@ +module Stylesheet + +open Expecto +open FsSpreadsheet +open FsSpreadsheet.ExcelIO +open DocumentFormat.OpenXml.Spreadsheet +open DocumentFormat.OpenXml.Packaging +open DocumentFormat.OpenXml + +let private tests_NumberingFormat = testList "NumberingFormat" [ + testList "isDateTime" [ + let testFormat (input: bool*string) = + let expected, format = input + testCase format <| fun _ -> + let numberingFormat = new NumberingFormat() + numberingFormat.FormatCode <- format + let isDateTime = Stylesheet.NumberingFormat.isDateTime numberingFormat + Expect.equal isDateTime expected format + let formats = [| + false, "General" + false, "aaaa" + true, "dd/mm/yyyy" + true, "d/m/yy\ h:mm;@" + true, "m/d/yyyy" + false, "0.00" + |] + for format in formats do + yield testFormat format + ] +] + +[] +let main = testList "Stylesheet" [ + tests_NumberingFormat +] \ No newline at end of file diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.net.xlsx b/tests/FsSpreadsheet.ExcelIO.Tests/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.net.xlsx new file mode 100644 index 00000000..755e23dc Binary files /dev/null and b/tests/FsSpreadsheet.ExcelIO.Tests/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.net.xlsx differ diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/TestObjects.fs b/tests/FsSpreadsheet.ExcelIO.Tests/TestObjects.fs index 0be08089..41c08507 100644 --- a/tests/FsSpreadsheet.ExcelIO.Tests/TestObjects.fs +++ b/tests/FsSpreadsheet.ExcelIO.Tests/TestObjects.fs @@ -26,15 +26,15 @@ let sheet1() = let sheet2() = let ws = new FsWorksheet(sheet2Name) [ - FsCell.createWithDataType DataType.Number 1 1 1 - FsCell.createWithDataType DataType.Number 1 2 2 - FsCell.createWithDataType DataType.Number 1 3 3 - FsCell.createWithDataType DataType.Number 1 4 4 + FsCell.createWithDataType DataType.Number 1 1 1. + FsCell.createWithDataType DataType.Number 1 2 2. + FsCell.createWithDataType DataType.Number 1 3 3. + FsCell.createWithDataType DataType.Number 1 4 4. - FsCell.createWithDataType DataType.Number 2 1 5 - FsCell.createWithDataType DataType.Number 2 2 6 - FsCell.createWithDataType DataType.Number 2 3 7 - FsCell.createWithDataType DataType.Number 2 4 8 + FsCell.createWithDataType DataType.Number 2 1 5. + FsCell.createWithDataType DataType.Number 2 2 6. + FsCell.createWithDataType DataType.Number 2 3 7. + FsCell.createWithDataType DataType.Number 2 4 8. ] |> List.iter (fun c -> ws.Row(c.RowNumber).[c.ColumnNumber].SetValueAs c.Value) ws \ No newline at end of file diff --git a/tests/FsSpreadsheet.ExcelIO.Tests/Utils.fs b/tests/FsSpreadsheet.ExcelIO.Tests/Utils.fs deleted file mode 100644 index b7a01998..00000000 --- a/tests/FsSpreadsheet.ExcelIO.Tests/Utils.fs +++ /dev/null @@ -1,21 +0,0 @@ -module TestingUtils - -open FsSpreadsheet -open Expecto - -module Expect = - - let workSheetEqual (actual : FsWorksheet) (expected : FsWorksheet) message = - let f (ws : FsWorksheet) = - ws.RescanRows() - ws.Rows - |> Seq.map (fun r -> r.Cells |> Seq.map (fun c -> c.Value) |> Seq.reduce (fun a b -> a + b)) - if actual.Name <> expected.Name then - failwithf $"{message}. Worksheet names do not match. Expected {expected.Name} but got {actual.Name}" - Expect.sequenceEqual (f actual) (f expected) $"{message}. Worksheet does not match" - - let columnsEqual (actual : FsCell seq seq) (expected : FsCell seq seq) message = - let f (cols : FsCell seq seq) = - cols - |> Seq.map (fun r -> r |> Seq.map (fun c -> c.Value) |> Seq.reduce (fun a b -> a + b)) - Expect.sequenceEqual (f actual) (f expected) $"{message}. Columns do not match" \ No newline at end of file diff --git a/tests/FsSpreadsheet.Exceljs.Tests/DefaultIO.Tests.fs b/tests/FsSpreadsheet.Exceljs.Tests/DefaultIO.Tests.fs new file mode 100644 index 00000000..6b6ea3bd --- /dev/null +++ b/tests/FsSpreadsheet.Exceljs.Tests/DefaultIO.Tests.fs @@ -0,0 +1,52 @@ +module DefaultIO.Tests + +open TestingUtils +open FsSpreadsheet +open FsSpreadsheet.Exceljs +open Fable.Core + +let private readFromTestFile (testFile: DefaultTestObject.TestFiles) = + FsWorkbook.fromXlsxFile(testFile.asRelativePathNode) + +let private tests_Read = testList "Read" [ + + testCaseAsync "Excel" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.Excel |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } + testCaseAsync "Libre" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.Libre |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } + testCaseAsync "FableExceljs" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.FableExceljs |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } + ptestCaseAsync "ClosedXML" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.ClosedXML |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } + testCaseAsync "FsSpreadsheetNET" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.FsSpreadsheetNET |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } + testCaseAsync "FsSpreadsheetJS" <| async { + let! wb = readFromTestFile DefaultTestObject.TestFiles.FsSpreadsheetJS |> Async.AwaitPromise + Expect.isDefaultTestObject wb + } +] + +let private tests_Write = testList "Write" [ + testCaseAsync "default" (Async.AwaitPromise <| promise { + let wb = DefaultTestObject.defaultTestObject() + let p = DefaultTestObject.WriteTestFiles.FsSpreadsheetJS.asRelativePathNode + do! FsWorkbook.toFile p wb + let! wb_read = FsWorkbook.fromXlsxFile p + Expect.isDefaultTestObject wb_read + }) +] + +let main = testList "DefaultIO" [ + tests_Read + tests_Write +] \ No newline at end of file diff --git a/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.csproj b/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.csproj new file mode 100644 index 00000000..7851ed66 --- /dev/null +++ b/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + false + + + + + + + + + + + + + + + + diff --git a/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.fsproj b/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.fsproj index a79f7f85..7851ed66 100644 --- a/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.fsproj +++ b/tests/FsSpreadsheet.Exceljs.Tests/FsSpreadsheet.Exceljs.Tests.fsproj @@ -7,19 +7,16 @@ + - - - - - + - + diff --git a/tests/FsSpreadsheet.Exceljs.Tests/Main.fs b/tests/FsSpreadsheet.Exceljs.Tests/Main.fs index dc5b823b..647a6da2 100644 --- a/tests/FsSpreadsheet.Exceljs.Tests/Main.fs +++ b/tests/FsSpreadsheet.Exceljs.Tests/Main.fs @@ -1,12 +1,14 @@ module FsSpreadsheet.Exceljs.Tests +open Fable.Core.JsInterop open Fable.Mocha - +open TestingUtils let all = testList "All" [ Workbook.Tests.main + DefaultIO.Tests.main ] [] @@ -14,4 +16,4 @@ let main argv = #if !FABLE_COMPILER failwith "The test repo FsSpreadsheet.Exceljs.Tests can only be executed in js environment!" #endif - Mocha.runTests all \ No newline at end of file + Mocha.runTests !!all \ No newline at end of file diff --git a/tests/FsSpreadsheet.Exceljs.Tests/Workbook.Tests.fs b/tests/FsSpreadsheet.Exceljs.Tests/Workbook.Tests.fs index e5a051d5..7ab1e3c6 100644 --- a/tests/FsSpreadsheet.Exceljs.Tests/Workbook.Tests.fs +++ b/tests/FsSpreadsheet.Exceljs.Tests/Workbook.Tests.fs @@ -1,10 +1,12 @@ module Workbook.Tests -open Fable.Mocha +open TestingUtils open Fable.ExcelJs open FsSpreadsheet.Exceljs open FsSpreadsheet +open Fable.Core + module Helper = open Fable.Core @@ -18,7 +20,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ testCase "empty" <| fun _ -> let jswb = ExcelJs.Excel.Workbook() Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb Expect.passWithMsg "Convert to fswb" let fswsList = fswb.GetWorksheets() let jswsList = jswb.worksheets @@ -27,7 +29,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ let jswb = ExcelJs.Excel.Workbook() let _ = jswb.addWorksheet("My Awesome Worksheet") Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb Expect.passWithMsg "Convert to fswb" let jswsList = jswb.worksheets let fswsList = fswb.GetWorksheets() @@ -39,7 +41,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ let _ = jswb.addWorksheet("My Best Worksheet") let _ = jswb.addWorksheet("My Nice Worksheet") Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb Expect.passWithMsg "Convert to fswb" let jswsList = jswb.worksheets let fswsList = fswb.GetWorksheets() @@ -57,7 +59,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ let table = Table("My_Awesome_Table", "B1", tableColumns, [||]) let _ = jsws.addTable(table) Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb Expect.passWithMsg "Convert to fswb" let fsTables_a = fswb.GetWorksheets().[0].Tables let fsTables_b = fswb.GetTables() @@ -87,7 +89,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ let table = Table("My_Awesome_Table", "B1", tableColumns, rows) let _ = jsws.addTable(table) Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb Expect.passWithMsg "Convert to fswb" let fsTables = fswb.GetTables() Expect.hasLength (jsws.getTables()) 1 "js table count" @@ -107,7 +109,7 @@ let private tests_toFsWorkbook = testList "toFsWorkbook" [ let table = Table("My_Awesome_Table", "B1", tableColumns, rows) let _ = jsws.addTable(table) Expect.passWithMsg "Create jswb" - let fswb = JsWorkbook.toFsWorkbook jswb + let fswb = JsWorkbook.readToFsWorkbook jswb let getCellValue (address: string) = let ws = fswb.GetWorksheetAt 1 let range = FsAddress(address) @@ -132,7 +134,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ testCase "empty" <| fun _ -> let fswb = new FsWorkbook() Expect.passWithMsg "Create fswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb Expect.passWithMsg "Convert to jswb" let fswsList = fswb.GetWorksheets() let jswsList = jswb.worksheets @@ -141,7 +143,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ let fswb = new FsWorkbook() let _ = fswb.InitWorksheet("My Awesome Worksheet") Expect.passWithMsg "Create fswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb Expect.passWithMsg "Convert to jswb" let fswsList = fswb.GetWorksheets() let jswsList = jswb.worksheets @@ -154,7 +156,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ let _ = fswb.InitWorksheet("My cool Worksheet") let _ = fswb.InitWorksheet("My wow Worksheet") Expect.passWithMsg "Create fswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb Expect.passWithMsg "Convert to jswb" let fswsList = fswb.GetWorksheets() let jswsList = jswb.worksheets @@ -171,7 +173,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ let t = FsTable("My_New_Table", FsRangeAddress("B1:C1")) let _ = fsws.AddTable(t) Expect.passWithMsg "Create jswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb Expect.passWithMsg "Convert to fswb" let jsws = jswb.worksheets.[0] Expect.equal jsws.name "My Awesome Worksheet" "ws name" @@ -198,7 +200,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ let t = FsTable("My_New_Table", FsRangeAddress("B1:D3")) let _ = fsws.AddTable(t) Expect.passWithMsg "Create jswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb Expect.passWithMsg "Convert to fswb" let jsws = jswb.worksheets.[0] Expect.equal jsws.name "My Awesome Worksheet" "ws name" @@ -227,7 +229,7 @@ let tests_toJsWorkbook = testList "toJsWorkbook" [ let _ = fsws.AddTable(t) fsws.RescanRows() Expect.passWithMsg "Create jswb" - let jswb = JsWorkbook.fromFsWorkbook fswb + let jswb = JsWorkbook.writeFromFsWorkbook fswb let jstable = jswb.worksheets.[0].getTables().[0].table.Value let row0 = jstable.rows.[0] let row1 = jstable.rows.[1] @@ -279,7 +281,7 @@ open Fable.Core let tests_xlsx = testList "xlsx" [ testList "read" [ - testAsync "isa.assay.xlsx" { + testCaseAsync "isa.assay.xlsx" <| async { let! fswb = Xlsx.fromXlsxFile("./tests/JS/TestFiles/isa.assay.xlsx") |> Async.AwaitPromise Expect.equal (fswb.GetWorksheets().Count) 5 "Count" } diff --git a/tests/FsSpreadsheet.Tests/DSL/CellBuilderTests.fs b/tests/FsSpreadsheet.Tests/DSL/CellBuilderTests.fs index f3fd8c89..f327d279 100644 --- a/tests/FsSpreadsheet.Tests/DSL/CellBuilderTests.fs +++ b/tests/FsSpreadsheet.Tests/DSL/CellBuilderTests.fs @@ -17,7 +17,7 @@ let main = cell { 1 } - let expected = Value(DataType.Number,"1"),None + let expected = Value(DataType.Number,1),None Expect.equal cell expected "Cell differs" testCase "simple string" <| fun _ -> let cell = diff --git a/tests/FsSpreadsheet.Tests/FsCellTests.fs b/tests/FsSpreadsheet.Tests/FsCellTests.fs index 8e467c78..73261c44 100644 --- a/tests/FsSpreadsheet.Tests/FsCellTests.fs +++ b/tests/FsSpreadsheet.Tests/FsCellTests.fs @@ -14,8 +14,8 @@ let dataType = let resultDtTrue, resultStrTrue = DataType.InferCellValue boolValTrue testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTrue = DataType.Boolean) "is not the expected DataType.Boolean" - testCase "Correct string" <| fun _ -> - let expected = "True" + testCase "Correct value" <| fun _ -> + let expected = true Expect.equal resultStrTrue expected $"resulting string is not correct: {resultStrTrue}" ] testList "InferCellValue bool = false" [ @@ -23,8 +23,8 @@ let dataType = let resultDtFalse, resultStrFalse = DataType.InferCellValue boolValFalse testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtFalse = DataType.Boolean) "is not the expected DataType.Boolean" - testCase "Correct string" <| fun _ -> - let expected = "False" + testCase "Correct value" <| fun _ -> + let expected = false Expect.equal resultStrFalse expected "resulting string is not correct" ] testList "InferCellValue string = \"test\"" [ @@ -32,7 +32,7 @@ let dataType = let resultDtTest, resultStrTest = DataType.InferCellValue stringValTest testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTest = DataType.String) "is not the expected DataType.String" - testCase "Correct string" <| fun _ -> + testCase "Correct value" <| fun _ -> Expect.isTrue (resultStrTest = "test") "resulting string is not correct" ] testList "InferCellValue string = \"\"" [ @@ -49,15 +49,15 @@ let dataType = testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTest = DataType.String) "is not the expected DataType.String" testCase "Correct string" <| fun _ -> - Expect.isTrue (resultChrTest = "1") "resulting string is not correct" + Expect.isTrue (resultChrTest = '1') "resulting string is not correct" ] testList "InferCellValue byte = 255uy" [ let byteValTest = 255uy let resultDtTest, resultBytTest = DataType.InferCellValue byteValTest testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTest = DataType.Number) "is not the expected DataType.Number" - testCase "Correct string" <| fun _ -> - Expect.equal "255" resultBytTest "resulting string is not correct" + testCase "Correct value" <| fun _ -> + Expect.equal (box byteValTest) resultBytTest "resulting value is not correct" ] testList "InferCellValue sbyte = -10y" [ let sbyteValTest = -10y @@ -65,7 +65,7 @@ let dataType = testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTest = DataType.Number) "is not the expected DataType.Number" testCase "Correct string" <| fun _ -> - Expect.equal "-10" resultSbyTest "resulting string is not correct" + Expect.equal (box -10y) resultSbyTest "resulting is not correct" ] testList "InferCellValue int = 0" [ let intValTest = 0 @@ -73,7 +73,7 @@ let dataType = testCase "Correct DataType" <| fun _ -> Expect.isTrue (resultDtTest = DataType.Number) "is not the expected DataType.Number" testCase "Correct string" <| fun _ -> - Expect.equal "0" resultIntTest "resulting string is not correct" + Expect.equal (box 0) resultIntTest "resulting is not correct" ] ] @@ -95,17 +95,17 @@ let fsCellData = testCase "Value: A1" <| fun _ -> Expect.equal fscellA1_string.Value "A1" "resulting value is not A1" testCase "Value: 1" <| fun _ -> - Expect.equal fscellB1_num.Value "1" "resulting value is not 1" - testCase "Value: True" <| fun _ -> - Expect.equal fscellA2_bool.Value "True" "resulting value is not True" + Expect.equal fscellB1_num.Value 1 "resulting value is not 1" + testCase "Value: true" <| fun _ -> + Expect.equal fscellA2_bool.Value true "resulting value is not true" testCase "Value as string : A1" <| fun _ -> Expect.equal (fscellA1_string.ValueAsString()) "A1" "resulting value is not A1 as string" testCase "Value as integer: 1 " <| fun _ -> Expect.equal (fscellB1_num.ValueAsInt()) 1 "resulting value is not 1 as integer" - testCase "Value as bool: True" <| fun _ -> - Expect.equal (fscellA2_bool.ValueAsBool()) true "resulting value is not True as bool" + testCase "Value as bool: true" <| fun _ -> + Expect.equal (fscellA2_bool.ValueAsBool()) true "resulting value is not true as bool" testCase "RowNumber: 1 " <| fun _ -> diff --git a/tests/FsSpreadsheet.Tests/FsTableTests.fs b/tests/FsSpreadsheet.Tests/FsTableTests.fs index 2e4dd034..79a5bc40 100644 --- a/tests/FsSpreadsheet.Tests/FsTableTests.fs +++ b/tests/FsSpreadsheet.Tests/FsTableTests.fs @@ -66,7 +66,7 @@ let dummyFsTableFields = if fsc.RowNumber = headerRowIndex then i <- i + 1 FsTableField( - fsc.Value, + fsc.ValueAsString(), i, dummyFsRangeColumns |> Seq.find (fun t -> t.RangeAddress.FirstAddress.ColumnNumber = fsc.ColumnNumber), obj, diff --git a/tests/JS/Exceljs.js b/tests/JS/Exceljs.js index 49ec9388..3463ef35 100644 --- a/tests/JS/Exceljs.js +++ b/tests/JS/Exceljs.js @@ -3,7 +3,7 @@ import { Xlsx } from './FsSpreadsheet.Exceljs/Xlsx.js'; import { FsWorkbook } from "./FsSpreadsheet.Exceljs/FsSpreadsheet/FsWorkbook.js"; import { FsRangeAddress_$ctor_Z721C83C5, FsRangeAddress__get_Range } from "./FsSpreadsheet.Exceljs/FsSpreadsheet/Ranges/FsRangeAddress.js"; import { FsTable } from "./FsSpreadsheet.Exceljs/FsSpreadsheet/Tables/FsTable.js"; -import { fromFsWorkbook, toFsWorkbook } from "./FsSpreadsheet.Exceljs/Workbook.js"; +import { writeFromFsWorkbook, readToFsWorkbook } from "./FsSpreadsheet.Exceljs/Workbook.js"; describe('FsSpreadsheet.Exceljs', function () { describe('read', function () { diff --git a/tests/TestUtils/DefaultTestObjects.fs b/tests/TestUtils/DefaultTestObjects.fs new file mode 100644 index 00000000..5c51fd86 --- /dev/null +++ b/tests/TestUtils/DefaultTestObjects.fs @@ -0,0 +1,143 @@ +module DefaultTestObject + +open FsSpreadsheet +open Fable.Core +#if FABLE_COMPILER +open Fable.Mocha +#else +open Expecto +#endif + +let [] testFolder = "TestFiles" + +[] +type TestFiles = +| Excel +| Libre +| FableExceljs +| ClosedXML +| FsSpreadsheetNET +| FsSpreadsheetJS + + member this.asFileName = + match this with + | Excel -> "TestWorkbook_Excel.xlsx" + | Libre -> "TestWorkbook_Libre.xlsx" + | FableExceljs -> "TestWorkbook_FableExceljs.xlsx" + | ClosedXML -> "TestWorkbook_ClosedXML.xlsx" + | FsSpreadsheetNET -> "TestWorkbook_FsSpreadsheet.net.xlsx" + | FsSpreadsheetJS -> "TestWorkbook_FsSpreadsheet.js.xlsx" + + member this.asRelativePath = $"../TestUtils/{testFolder}/{this.asFileName}" + member this.asRelativePathNode = $"./tests/TestUtils/{testFolder}/{this.asFileName}" + +[] +type WriteTestFiles = +| FsSpreadsheetNET +| FsSpreadsheetJS + + member this.asFileName = + match this with + | FsSpreadsheetNET -> "TestWorkbook_FsSpreadsheet_WRITE.net.xlsx" + | FsSpreadsheetJS -> "TestWorkbook_FsSpreadsheet_WRITE.js.xlsx" + + member this.asRelativePath = $"{testFolder}/{this.asFileName}" + member this.asRelativePathNode = $"./tests/TestUtils/{testFolder}/{this.asFileName}" + +module ExpectedRows = + let headerRow (range:string) cc = + let row = FsRow(FsRangeAddress(range),cc) + row[1].SetValueAs "Numbers" + row[2].SetValueAs "Strings" + row[3].SetValueAs "DateTime" + row[4].SetValueAs "Boolean" + row[5].SetValueAs "ARCtrl Column" + row[6].SetValueAs "ARCtrl Column " + row + let firstRow(range: string) cc = + let row = FsRow(FsRangeAddress(range),cc) + row[1].SetValueAs 1. + row[2].SetValueAs "Hello" + row[3].SetValueAs (System.DateTime(2023,10,14,0,0,0)) + row[4].SetValueAs true + row[5].SetValueAs "(A) This is part 1 of 2" + row[6].SetValueAs "(A) This is part 2 of 2" + row + let secondRow(range:string) cc = + let row = FsRow(FsRangeAddress(range),cc) + row[1].SetValueAs 2. + row[2].SetValueAs "World" + row[3].SetValueAs (System.DateTime(2023,10,15, 18,0,0)) + row[4].SetValueAs false + row[6].SetValueAs "Tests if column names with whitespace at end can be unique" + row + let thirdRow(range:string) cc = + let row = FsRow(FsRangeAddress(range),cc) + row[1].SetValueAs 3. + row[2].SetValueAs "Bye" + row[3].SetValueAs (System.DateTime(2023,10,16, 20,0,0)) + row[4].SetValueAs true + row + let fourthRow(range:string) cc = + let row = FsRow(FsRangeAddress(range),cc) + row[1].SetValueAs 4.269 + row[2].SetValueAs "Outer Space" + row[3].SetValueAs (System.DateTime(2023,10,17,0,0,0)) + row[4].SetValueAs false + row + + let rowCollectionA1 = + let cells = FsCellsCollection() + [|headerRow("A1:F1") ;firstRow("A2:F2") ;secondRow("A3:F3");thirdRow("A4:F4");fourthRow("A5:F5")|] + |> Array.map (fun x -> x cells) + let rowCollectionB4 = + let cells = FsCellsCollection() + [|headerRow("B4:G4");firstRow("B5:G5");secondRow("B6:G6");thirdRow("B7:G7");fourthRow("B8:G8")|] + |> Array.map (fun x -> x cells) + +module Sheet1 = + + [] + let sheetName = "WithTable" + [] + let tableName = "MyTable" + +module Sheet2 = + + [] + let sheetName = "Tableless" + +module Sheet3 = + + [] + let sheetName = "WithTable_Duplicate" + [] + let tableName = "MyOtherTable" + +let defaultTestObject() = + let wb = new FsWorkbook() + let table1 = new FsTable(Sheet1.tableName, FsRangeAddress(FsAddress("A1"),FsAddress("F1"))) + let sheet1 = wb.InitWorksheet(Sheet1.sheetName) + for row in ExpectedRows.rowCollectionA1 do + for c in row do + sheet1.AddCell c |> ignore + sheet1.AddTable table1 |> ignore + let sheet2 = wb.InitWorksheet(Sheet2.sheetName) + for row in ExpectedRows.rowCollectionA1 do + for c in row do + sheet2.AddCell c |> ignore + let table2 = new FsTable(Sheet3.tableName, FsRangeAddress(FsAddress("B4"),FsAddress("G8"))) + let sheet3 = wb.InitWorksheet(Sheet3.sheetName) + for row in ExpectedRows.rowCollectionB4 do + for c in row do + sheet3.AddCell c |> ignore + sheet3.AddTable(table2) |> ignore + wb + +let valueMap = + [ + Sheet1.sheetName, (Some Sheet1.tableName, ExpectedRows.rowCollectionA1); + Sheet2.sheetName, (None, ExpectedRows.rowCollectionA1); + Sheet3.sheetName, (Some Sheet3.tableName, ExpectedRows.rowCollectionB4) + ] + |> Map.ofList diff --git a/tests/TestUtils/TestFiles/Scripts/closedXml.fsx b/tests/TestUtils/TestFiles/Scripts/closedXml.fsx new file mode 100644 index 00000000..33319512 --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/closedXml.fsx @@ -0,0 +1,12 @@ +#r "nuget: ClosedXML, 0.102.1" + +open ClosedXML +open ClosedXML.Excel + +let inputPath = @"../TestWorkbook_Excel.xlsx" + +let outputPath = @"../TestWorkbook_ClosedXML.xlsx" + +let wb = new XLWorkbook(inputPath) + +wb.SaveAs(outputPath) diff --git a/tests/TestUtils/TestFiles/Scripts/fableExceljs.fs.js b/tests/TestUtils/TestFiles/Scripts/fableExceljs.fs.js new file mode 100644 index 00000000..52c584dc --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/fableExceljs.fs.js @@ -0,0 +1,17 @@ +import { PromiseBuilder__Delay_62FBFDE1, PromiseBuilder__Run_212F1D4B } from "./fable_modules/Fable.Promise.3.2.0/Promise.fs.js"; +import { Excel } from "./fable_modules/Fable.Exceljs.1.6.0/ExcelJs.fs.js"; +import { promise } from "./fable_modules/Fable.Promise.3.2.0/PromiseImpl.fs.js"; + +export const inputPath = "../TestWorkbook_Excel.xlsx"; + +export const outputPath = "../TestWorkbook_FableExceljs.xlsx"; + +export function run() { + return PromiseBuilder__Run_212F1D4B(promise, PromiseBuilder__Delay_62FBFDE1(promise, () => { + const wb = new Excel.Workbook(); + return wb.xlsx.readFile(inputPath).then(() => (wb.xlsx.writeFile(outputPath))); + })); +} + +run(); + diff --git a/tests/TestUtils/TestFiles/Scripts/fableExceljs.fsx b/tests/TestUtils/TestFiles/Scripts/fableExceljs.fsx new file mode 100644 index 00000000..1e6618b7 --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/fableExceljs.fsx @@ -0,0 +1,20 @@ +#r "nuget: Fable.Exceljs, 1.6.0" +#r "nuget: Fable.Promise, 3.2.0" + +open Fable.ExcelJs + +let inputPath = @"../TestWorkbook_Excel.xlsx" + +let outputPath = @"../TestWorkbook_FableExceljs.xlsx" + + +let run() = + promise { + let wb = ExcelJs.Excel.Workbook() + // Read + do! wb.xlsx.readFile(inputPath) + // Write + return! wb.xlsx.writeFile(outputPath) + } + +run() diff --git a/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.fsx b/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.fsx new file mode 100644 index 00000000..81513451 --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.fsx @@ -0,0 +1,25 @@ +#r @"..\..\..\FsSpreadsheet.ExcelIO.Tests\bin\Debug\net6.0\DocumentFormat.OpenXml.dll" +#r @"..\..\..\FsSpreadsheet.ExcelIO.Tests\bin\Debug\net6.0\FsSpreadsheet.dll" +#r @"..\..\..\FsSpreadsheet.ExcelIO.Tests\bin\Debug\net6.0\FsSpreadsheet.ExcelIO.dll" +#r @"..\..\..\FsSpreadsheet.ExcelIO.Tests\bin\Debug\net6.0\System.IO.Packaging.dll" + + +open FsSpreadsheet +open FsSpreadsheet.ExcelIO +open DocumentFormat.OpenXml.Spreadsheet +open DocumentFormat.OpenXml + +let inputPath = @"../TestWorkbook_Excel.xlsx" + +let outputPath = @"../TestWorkbook_FsSpreadsheet.net.xlsx" + +let wb = FsWorkbook.fromXlsxFile (inputPath) +// wb.GetWorksheets().[0].GetCellAt(5,1) |> fun x -> (x.Value, x.DataType) |> printfn "%A" + +// let r = wb.GetWorksheets().[0].GetCellAt(5,1).Value |> string + +// Cell(DataType = EnumValue(CellValues.Number), CellValue = CellValue(r)).InnerText + +// for i in r do printfn "%A" i + +wb.ToFile(outputPath) diff --git a/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.js b/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.js new file mode 100644 index 00000000..12b9264a --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/fsSpreadsheet.js @@ -0,0 +1,14 @@ +import { Xlsx } from "./fable/Xlsx.js" + +export const inputPath = "../TestWorkbook_Excel.xlsx"; + +export const outputPath = "../TestWorkbook_FsSpreadsheet.js.xlsx"; + +async function run() { + let wb = await Xlsx.fromXlsxFile(inputPath) + // console.log(wb) + Xlsx.toFile(outputPath, wb) +} + +run(); + diff --git a/tests/TestUtils/TestFiles/Scripts/runClosedXml.cmd b/tests/TestUtils/TestFiles/Scripts/runClosedXml.cmd new file mode 100644 index 00000000..f97f86aa --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/runClosedXml.cmd @@ -0,0 +1 @@ +dotnet fsi .\closedXml.fsx \ No newline at end of file diff --git a/tests/TestUtils/TestFiles/Scripts/runFableExceljs.cmd b/tests/TestUtils/TestFiles/Scripts/runFableExceljs.cmd new file mode 100644 index 00000000..2862035e --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/runFableExceljs.cmd @@ -0,0 +1,3 @@ +dotnet fable ./fableExceljs.fsx + +node ./fableExceljs.fs.js \ No newline at end of file diff --git a/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.fsx.cmd b/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.fsx.cmd new file mode 100644 index 00000000..9da59c94 --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.fsx.cmd @@ -0,0 +1 @@ +dotnet fsi .\fsSpreadsheet.fsx \ No newline at end of file diff --git a/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.js.cmd b/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.js.cmd new file mode 100644 index 00000000..7d02c3cd --- /dev/null +++ b/tests/TestUtils/TestFiles/Scripts/runFsSpreadsheet.js.cmd @@ -0,0 +1,3 @@ +dotnet fable ../../../../src/FsSpreadsheet.Exceljs -o ./fable --noCache + +node ./fsSpreadsheet.js \ No newline at end of file diff --git a/tests/TestUtils/TestFiles/TestWorkbook_ClosedXML.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_ClosedXML.xlsx new file mode 100644 index 00000000..09270a7e Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_ClosedXML.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_Excel.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_Excel.xlsx new file mode 100644 index 00000000..064dd6db Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_Excel.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_FableExceljs.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_FableExceljs.xlsx new file mode 100644 index 00000000..85dbdad5 Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_FableExceljs.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.js.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.js.xlsx new file mode 100644 index 00000000..77e4df3b Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.js.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.net.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.net.xlsx new file mode 100644 index 00000000..f5135fc5 Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet.net.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.js.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.js.xlsx new file mode 100644 index 00000000..f11de524 Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.js.xlsx differ diff --git a/tests/TestUtils/TestFiles/TestWorkbook_Libre.xlsx b/tests/TestUtils/TestFiles/TestWorkbook_Libre.xlsx new file mode 100644 index 00000000..fc3ef7ec Binary files /dev/null and b/tests/TestUtils/TestFiles/TestWorkbook_Libre.xlsx differ diff --git a/tests/TestUtils/TestUtils.fsproj b/tests/TestUtils/TestUtils.fsproj new file mode 100644 index 00000000..476f1794 --- /dev/null +++ b/tests/TestUtils/TestUtils.fsproj @@ -0,0 +1,46 @@ + + + + net6.0 + true + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/TestUtils/TestingUtils.fs b/tests/TestUtils/TestingUtils.fs new file mode 100644 index 00000000..dc2ce95c --- /dev/null +++ b/tests/TestUtils/TestingUtils.fs @@ -0,0 +1,142 @@ +module TestingUtils + +open FsSpreadsheet +#if FABLE_COMPILER +open Fable.Mocha +#else +open Expecto +#endif + +[] +module Utils = + + let extractWords (json:string) = + json.Split([|'{';'}';'[';']';',';':'|]) + |> Array.map (fun s -> s.Trim()) + |> Array.filter ((<>) "") + + let wordFrequency (json:string) = + json + |> extractWords + |> Array.countBy id + |> Array.sortBy fst + + let inline firstDiff s1 s2 = + let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None)) + let s2 = Seq.append (Seq.map Some s2) (Seq.initInfinite (fun _ -> None)) + Seq.mapi2 (fun i s p -> i,s,p) s1 s2 + |> Seq.find (function |_,Some s,Some p when s=p -> false |_-> true) + +/// Fable compatible Expecto/Mocha unification +module Expect = + + /// Expects the `actual` sequence to equal the `expected` one. + let inline private _sequenceEqual message (comparison: int * 'a option * 'a option) = + match comparison with + | _,None,None -> () + | i,Some a, Some e -> + let msg = + sprintf "%s. Sequence does not match at position %i.\n" message i + + sprintf "Expected item: %A\n" e + + sprintf "Actual item: %A\n" a + failwith msg + | i,None,Some e -> + let msg = + sprintf "%s. Sequence actual shorter than expected, at pos %i for expected item: \n" message i + + sprintf "%A" e + failwith msg + | i,Some a,None -> + let msg = + sprintf "%s. Sequence actual longer than expected, at pos %i found item: \n" message i + + sprintf "%A" a + failwith msg + + let inline sequenceEqual actual expected message = + let comp = Utils.firstDiff actual expected + _sequenceEqual message comp + + let cellSequenceEquals (actual: FsCell seq) (expected: FsCell seq) message = + let cellDiff (s1: FsCell seq) (s2: FsCell seq) = + let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None)) + let s2 = Seq.append (Seq.map Some s2) (Seq.initInfinite (fun _ -> None)) + Seq.mapi2 (fun i s p -> i,s,p) s1 s2 + |> Seq.find (function |_,Some s,Some p when s.StructurallyEquals(p) -> false |_-> true) + let comp = cellDiff actual expected + _sequenceEqual message comp + + let columnsEqual (actual : FsCell seq seq) (expected : FsCell seq seq) message = + Seq.iteri2 (fun i s1 s2 -> + cellSequenceEquals s1 s2 $"{message}. Columns do not match in row {i}." + ) actual expected + + let workSheetEqual (actual : FsWorksheet) (expected : FsWorksheet) message = + let f (ws : FsWorksheet) = + ws.RescanRows() + ws.Rows |> Seq.map (fun r -> r.Cells) + if actual.Name <> expected.Name then + failwithf $"{message}. Worksheet names do not match. Expected {expected.Name} but got {actual.Name}" + columnsEqual (f actual) (f expected) $"{message}. Worksheet does not match" + + let isDefaultTestObject (wb: FsWorkbook) = + let worksheets = wb.GetWorksheets() + for ws in worksheets do + let isTable, expectedRows = Expect.wantSome (DefaultTestObject.valueMap |> Map.tryFind ws.Name) $"ExpectError: Unable to get info for worksheet: {ws.Name}" + match isTable with + | Some expectedTableName -> + let actualTable = Expect.wantSome (ws.Tables |> Seq.tryFind (fun t -> t.Name = expectedTableName)) $"ExpectError: Unable to get info for worksheet->table: {ws.Name}->{expectedTableName}" + let headerRow = Expect.wantSome (actualTable.TryGetHeaderRow(ws.CellCollection)) $"ExpectError: ShowHeaderRow is false for worksheet->table: {ws.Name}->{expectedTableName}" + let actualRows = actualTable.GetRows(ws.CellCollection) |> Seq.tail //Seq.tail skips HeaderRow + cellSequenceEquals headerRow expectedRows.[0] $"ExpectError: HeaderRow is not equal for worksheet->table: {ws.Name}->{expectedTableName}" + for actualRow, expectedRow in Seq.zip actualRows expectedRows.[1..] do + cellSequenceEquals actualRow expectedRow $"ExpectError: Table body rows are not equal for worksheet->table: {ws.Name}->{expectedTableName}" + | None -> + let actualRows = ws.Rows + for actualRow, expectedRow in Seq.zip actualRows expectedRows do + cellSequenceEquals actualRow expectedRow $"ExpectError: Worksheet rows are not equal for worksheet: {ws.Name}" + + let inline equal actual expected message = Expect.equal actual expected message + let notEqual actual expected message = Expect.notEqual actual expected message + + let isNull actual message = Expect.isNull actual message + let isNotNull actual message = Expect.isNotNull actual message + + let isSome actual message = Expect.isSome actual message + let isNone actual message = Expect.isNone actual message + let wantSome actual message = Expect.wantSome actual message + + let isEmpty actual message = Expect.isEmpty actual message + let hasLength actual expectedLength message = Expect.hasLength actual expectedLength message + + let isTrue actual message = Expect.isTrue actual message + let isFalse actual message = Expect.isFalse actual message + + let wantError actual message = Expect.wantError actual message + let wantOk actual message = Expect.wantOk actual message + let isOk actual message = Expect.isOk actual message + let isError actual message = Expect.isError actual message + + let throws actual message = Expect.throws actual message + let throwsC actual message = Expect.throwsC actual message + + let exists actual asserter message = Expect.exists actual asserter message + let containsAll actual expected message = Expect.containsAll actual expected message + + let passWithMsg (message: string) = equal true true message + +/// Fable compatible Expecto/Mocha unification +[] +module Test = + + let test = test + let testAsync = testAsync + let testSequenced = testSequenced + + let testCase = testCase + let ptestCase = ptestCase + let ftestCase = ftestCase + let testCaseAsync = testCaseAsync + let ptestCaseAsync = ptestCaseAsync + let ftestCaseAsync = ftestCaseAsync + + + let testList = testList \ No newline at end of file