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