Skip to content

Commit

Permalink
add preliminary stylesheet functionality for cell datetime writing (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
HLWeil committed Oct 18, 2023
1 parent 5f6c8da commit 47615d7
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 108 deletions.
62 changes: 31 additions & 31 deletions src/FsSpreadsheet.ExcelIO/Cell.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,6 @@ module Cell =
/// </summary>
let setValue (value : string) (cellValue : CellValue) = cellValue.Text <- value

/// <summary>
/// Takes a DataType and returns the appropriate CellValue.
/// </summary>
/// <remarks>DataType is the FsSpreadsheet representation of the CellValue enum in OpenXml.</remarks>
let cellValuesFromDataType (dataType : DataType) =
match dataType with
| String -> CellValues.String
| Boolean -> CellValues.Boolean
| Number -> CellValues.Number
| Date -> CellValues.Date
| Empty -> CellValues.Error

/// <summary>
/// Takes a CellValue and returns the appropriate DataType.
/// </summary>
Expand Down Expand Up @@ -97,6 +85,15 @@ module Cell =
let create (dataType : CellValues) (reference : string) (value : CellValue) =
Cell(CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value)

/// <summary>
/// Creates a Cell from a CellValues type case, a "A1" style reference, and a CellValue containing the value string.
/// </summary>
let createWithFormat doc (dataType : CellValues) (reference : string) (cellFormat : CellFormat) (value : CellValue) =
let styleSheet = Stylesheet.getOrInit doc
let i = Stylesheet.CellFormat.count styleSheet
Stylesheet.CellFormat.append cellFormat styleSheet |> ignore
Cell(StyleIndex = UInt32Value(uint32 i),CellReference = StringValue.FromString reference, DataType = EnumValue(dataType), CellValue = value)

/// <summary>
/// Sets the preserve attribute of a Cell.
/// </summary>
Expand Down Expand Up @@ -143,20 +140,15 @@ module Cell =
setSpacePreserveAttribute c
else c

/// <summary>
/// Create a cell using a shared string table, also returns the updated shared string table.
/// </summary>
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
Expand All @@ -165,22 +157,30 @@ module Cell =
updatedSharedStringTable
|> SharedStringTable.count
|> string
|> CellValue.create
|> create CellValues.SharedString reference
|> fun c ->
if value.EndsWith " " then
setSpacePreserveAttribute c
else c
|> fun v -> {|DataType = CellValues.SharedString; Value = v; Format = None|}
| DataType.String -> {|DataType = CellValues.String; Value = value; Format = None|}
| DataType.Boolean -> {|DataType = CellValues.Boolean; Value = value; Format = None|}
| DataType.Number -> {|DataType = CellValues.Number; Value = value; Format = None|}
| DataType.Date ->
let cellFormat = CellFormat(NumberFormatId = UInt32Value 19u, ApplyNumberFormat = BooleanValue true)
let value = System.DateTime.Parse(value).ToOADate() |> string
{|DataType = CellValues.Number; Value = value; Format = Some cellFormat|}
| DataType.Empty -> {|DataType = CellValues.Number; Value = value; Format = None|}

| _ ->
let valType = cellValuesFromDataType dataType
let reference = CellReference.ofIndices columnIndex (rowIndex)
create valType reference (CellValue.create value)
|> fun c ->
/// <summary>
/// Create a cell using a shared string table, also returns the updated shared string table.
/// </summary>
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

/// <summary>
/// Gets "A1"-style Cell reference.
/// </summary>
Expand Down
103 changes: 56 additions & 47 deletions src/FsSpreadsheet.ExcelIO/FsExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,53 @@ module FsExtensions =
/// <summary>
/// Converts a given CellValues to the respective DataType.
/// </summary>
static member ofXlsxCellValues (cellValues : CellValues) =
match cellValues with
| CellValues.Number -> DataType.Number
| CellValues.Boolean -> DataType.Boolean
| CellValues.Date -> DataType.Date
| CellValues.Error -> DataType.Empty
| CellValues.InlineString
| CellValues.SharedString
| CellValues.String
| _ -> DataType.String
static member ofXlsXCell (doc : Packaging.SpreadsheetDocument) (cell : Cell) =

//https://stackoverflow.com/a/13178043/12858021
//https://stackoverflow.com/a/55425719/12858021
// if styleindex is not null and datatype is null we propably have a DateTime field.
// if datatype would not be null it could also be boolean, as far as i tested it ~Kevin F 13.10.2023
if cell.StyleIndex <> null && cell.DataType = null then
try
let styleSheet = Stylesheet.get doc
let cellFormat : CellFormat = Stylesheet.CellFormat.getAt (int cell.StyleIndex.InnerText) styleSheet
if cellFormat <> null then
// if numberformatid is between 14 and 18 it is standard date time format.
// custom formats are given in the range of 164 to 180, all none default date time formats fall in there.
let dateTimeFormats = [14..22]@[164 .. 180] |> List.map (uint32 >> UInt32Value)
if List.contains cellFormat.NumberFormatId dateTimeFormats then
DataType.Date
else
DataType.Number
else
DataType.Number
with
| _ -> DataType.Number
else
let cellValues = cell.DataType.Value
match cellValues with
| CellValues.Number -> DataType.Number
| CellValues.Boolean -> DataType.Boolean
| CellValues.Date -> DataType.Date
| CellValues.Error -> DataType.Empty
| CellValues.InlineString
| CellValues.SharedString
| CellValues.String -> DataType.String
| _ -> DataType.Number


type FsCell with
//member self.ofXlsxCell (sst : Spreadsheet.SharedStringTable option) (xlsxCell:Spreadsheet.Cell) =
// let v = Cell.getValue sst xlsxCell
// let row,col = xlsxCell.CellReference.Value |> CellReference.toIndices
// FsCell.create (int row) (int col) v

/// <summary>
/// Creates an FsCell on the basis of an XlsxCell. Uses a SharedStringTable if present to get the XlsxCell's value.
/// </summary>
static member ofXlsxCell (sst : SharedStringTable option) (xlsxCell : Cell) =
/// </summary>
static member ofXlsxCell (doc : Packaging.SpreadsheetDocument) (xlsxCell : Cell) =
let sst = Spreadsheet.tryGetSharedStringTable doc
let mutable v = Cell.getValue sst xlsxCell
let setValue x = v <- x
let col, row = xlsxCell.CellReference.Value |> CellReference.toIndices
let dt =
try DataType.ofXlsxCellValues xlsxCell.DataType.Value
try DataType.ofXlsXCell doc xlsxCell
with _ -> DataType.Number // default is number
match dt with
| Date ->
Expand All @@ -62,6 +82,9 @@ module FsExtensions =
| _ -> ()
FsCell.createWithDataType dt (int row) (int col) v

static member toXlsxCell (doc : Packaging.SpreadsheetDocument) (cell : FsCell) =
Cell.fromValueWithDataType doc (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) cell.Value cell.DataType

type FsTable with

/// <summary>
Expand Down Expand Up @@ -118,7 +141,7 @@ module FsExtensions =
/// <summary>
/// Returns the FsWorksheet in the form of an XlsxSpreadsheet.
/// </summary>
member self.ToXlsxWorksheet() =
member self.ToXlsxWorksheet(doc) =
self.RescanRows()
let sheet = Worksheet.empty()
let sheetData =
Expand All @@ -134,7 +157,7 @@ module FsExtensions =
let cells =
cells
|> List.map (fun cell ->
Cell.fromValueWithDataType None (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.Value) (cell.DataType)
Cell.fromValueWithDataType doc (uint32 cell.ColumnNumber) (uint32 cell.RowNumber) (cell.Value) (cell.DataType)
)
let row = Row.create (uint32 row.Index) (Row.Spans.fromBoundaries min max) cells
SheetData.appendRow row sd |> ignore
Expand All @@ -144,8 +167,8 @@ module FsExtensions =
/// <summary>
/// Returns an FsWorksheet in the form of an XlsxSpreadsheet.
/// </summary>
static member toXlsxWorksheet (fsWorksheet : FsWorksheet) =
fsWorksheet.ToXlsxWorksheet()
static member toXlsxWorksheet (fsWorksheet : FsWorksheet, doc) =
fsWorksheet.ToXlsxWorksheet(doc)

/// <summary>
/// Appends the FsTables of this FsWorksheet to a given OpenXmlWorksheetPart in an XlsxWorkbookPart.
Expand Down Expand Up @@ -198,25 +221,7 @@ module FsExtensions =
let sheetId = Sheet.getID xlsxSheet
let xlsxCells =
Spreadsheet.getCellsBySheetID sheetId doc
|> Seq.map (fun c ->
//https://stackoverflow.com/a/13178043/12858021
//https://stackoverflow.com/a/55425719/12858021
// if styleindex is not null and datatype is null we propably have a DateTime field.
// if datatype would not be null it could also be boolean, as far as i tested it ~Kevin F 13.10.2023
if c.StyleIndex <> null && c.DataType = null then
try
// get cellformat from stylesheet
let cellFormat : CellFormat = xlsxWorkbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements.GetItem (int c.StyleIndex.InnerText) :?> CellFormat
if cellFormat <> null then
// if numberformatid is between 14 and 18 it is standard date time format.
// custom formats are given in the range of 164 to 180, all none default date time formats fall in there.
let dateTimeFormats = [14..22]@[164 .. 180] |> List.map (uint32 >> UInt32Value)
if List.contains cellFormat.NumberFormatId dateTimeFormats then
c.DataType <- CellValues.Date
with
| _ -> ()
FsCell.ofXlsxCell sst c
)
|> Seq.map (FsCell.ofXlsxCell doc)
let assocXlsxTables =
xlsxTables
|> Seq.tryPick (fun (sid,ts) -> if sid = sheetId then Some ts else None)
Expand Down Expand Up @@ -267,20 +272,24 @@ module FsExtensions =
sr.Close()
wb

/// <summary>
/// Writes the FsWorkbook into a given MemoryStream.
/// </summary>
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)

/// <summary>
/// Writes the FsWorkbook into a given MemoryStream.
/// </summary>
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

Expand Down
1 change: 1 addition & 0 deletions src/FsSpreadsheet.ExcelIO/FsSpreadsheet.ExcelIO.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Stylesheet.fs" />
<Compile Include="SharedStringTable.fs" />
<Compile Include="Cell.fs" />
<Compile Include="CellData.fs" />
Expand Down
8 changes: 7 additions & 1 deletion src/FsSpreadsheet.ExcelIO/SharedStringTable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ module SharedStringTable =
else
index


/// <summary>
/// Gets the sharedStringTable of the spreadsheet if it exists, else returns None.
/// </summary>
let tryGet (spreadsheetDocument : SpreadsheetDocument) =
try spreadsheetDocument.WorkbookPart.SharedStringTablePart.SharedStringTable |> Some
with | _ -> None




61 changes: 61 additions & 0 deletions src/FsSpreadsheet.ExcelIO/Stylesheet.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace FsSpreadsheet.ExcelIO

open DocumentFormat.OpenXml.Spreadsheet
open DocumentFormat.OpenXml.Packaging
open DocumentFormat.OpenXml

module Stylesheet =

//module Font =

// let getDefault() = Font().Color


let get (doc : SpreadsheetDocument) =

doc.WorkbookPart.WorkbookStylesPart.Stylesheet

let getOrInit (doc : SpreadsheetDocument) =

match doc.WorkbookPart.WorkbookStylesPart with
| null ->
let ssp = doc.WorkbookPart.AddNewPart<WorkbookStylesPart>()
ssp.Stylesheet <- new Stylesheet()
ssp.Stylesheet.CellFormats <- new CellFormats()
ssp.Stylesheet
| ssp -> ssp.Stylesheet

let tryGet (doc : SpreadsheetDocument) =
match doc.WorkbookPart.WorkbookStylesPart with
| null -> None
| ssp -> Some(ssp.Stylesheet)

module CellFormat =

let updateCount (stylesheet : Stylesheet) =
let newCount = stylesheet.CellFormats.Elements<CellFormat>() |> Seq.length
stylesheet.CellFormats.Count <- UInt32Value(uint32 newCount)

let count (stylesheet : Stylesheet) =
if stylesheet.CellFormats = null then 0
elif stylesheet.CellFormats.Count = null then 0
else stylesheet.CellFormats.Count.Value |> int

let getAt (index : int) (stylesheet : Stylesheet) =
stylesheet.CellFormats.Elements<CellFormat>() |> Seq.item index

let tryGetAt (index : int) (stylesheet : Stylesheet) =
stylesheet.CellFormats.Elements<CellFormat>() |> 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
<PackageReference Update="FSharp.Core" Version="7.0.300" />
<PackageReference Include="Expecto" Version="[10.1.0]" />
<PackageReference Include="YoloDev.Expecto.TestSdk" Version="[0.14.1]" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="FSharp.Core" Version="7.0.400" />
</ItemGroup>

</Project>
Loading

0 comments on commit 47615d7

Please sign in to comment.