diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9416600 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +# 4 space indentation +[*.cs] +indent_style = space +indent_size = 4 + +# CA1820: Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = error diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..307f7d6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# Source https://help.github.com/articles/dealing-with-line-endings +# Set default behaviour, in case users don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files we want to always be normalized and converted +# to native line endings on checkout. +*.cs text diff=csharp eol=crlf + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf +*.csproj text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.dll binary +*.gif binary +*.ico binary +*.xlsx binary + +core.autocrlf=true diff --git a/.gitignore b/.gitignore index ed6d1d2..14bd0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,402 +1,32 @@ -# ---> VisualStudio -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore -# User-specific files -*.rsuser -*.suo +#ignore thumbnails created by windows +Thumbs.db +#Ignore files build by Visual Studio +*.obj +*.exe +*.pdb *.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio +*.aps +*.pch +*.vspscc *_i.c *_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr +*.ncb +*.suo *.tlb -*.tli *.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj +*.bak +*.cache +*.ilk *.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in +.vs/ +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ _ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - +[Tt]est[Rr]esult* +/packages +.idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..477b942 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Developer guidelines +The [OpenXML specification](https://www.ecma-international.org/publications/standards/Ecma-376.htm) is a large and complicated beast. In order for ClosedXML, the wrapper around OpenXML, to support all the features, we rely on community contributions. Before opening an issue to request a new feature, we'd like to urge you to try to implement it yourself and log a pull request. + +Here are some tips. + +* Before starting a large pull request, log an issue and outline the problem and a broad outline of your solution. The maintainers will discuss the issue with you and possibly propose some alternative approaches to align with the ClosedXML development conventions. +* Please submit pull requests that are based on the `develop` branch. +* Where possible, pull requests should include unit tests that cover as many uses cases as possible. +* We recommend Visual Studio 2019 or higher as the development environment. If you do use Visual Studio, please install these extensions: + * [CodeMaid](https://marketplace.visualstudio.com/items?itemName=SteveCadwallader.CodeMaid), for ensuring consistent code syntax, whitespace convention, etc. + * If you use a version of Visual Studio lower than 2017, you should install [editorconfig]() support. Read more about [EditorConfig](http://www.editorconfig.org). +* We use 4 spaces for code indentation. This is the default in Visual Studio. Don't leave any trailing white space at the end of lines or files. + + + +## Working with Excel file internals +Excel files (`.xlsx` and `.xlsm`) are zip packages. You can easily verify this by renaming the extension any Excel file to `.zip` and opening the file in your favourite `.zip` file editor. + +Internally, the file contains files (also known as parts) that represent different entities in the Excel framework, for example `workbook.xml` and `table1.xml`. The [OpenXML specification](https://www.ecma-international.org/publications/standards/Ecma-376.htm) documents all these parts and their contents. + +Making changes to the ClosedXML code may change the input or output of the package parts. For example if you add support for a currently unsupported element, you will have to ensure that you read the appropriate package part into the ClosedXML model and also support writing of the package parts to the file. + +### Comparing the internals of Excel files + +A ClosedXML developer will often want to compare the internals of 2 similar Excel files. For example if you want to compare the output of a specific package part before and after your code changes. The long, difficult way would be to extract the package parts of the 2 files and manually compare the relevant parts. To ease this, we recommend this tooling stack: + +- [Total Commander](https://www.ghisler.com/download.htm) +- [WinMerge](http://winmerge.org/downloads) version `2.14.0`, because subsequent versions for [some reason](https://bitbucket.org/winmerge/winmerge/issues/152/displayxmlfiles-plugin-not-included-with) excludes the required `DisplayXMLFiles.dll` plugin. +- Set Total Commander [to use WinMerge](https://superuser.com/questions/238039/can-i-replace-internal-diff-in-total-commander-with-a-custom-tool) as its compare tool. +- In WinMerge, enable `Plugins > Automatic Prediffer` + +Now, to compare 2 similar, but not exact Excel files: + +- In Total Commander, navigate to the 1st file in the left-hand pane and the 2nd file in the right-hand pane. +- Press `Ctrl+PageDown` to "enter" the package. You should see, among others, a `[Content_Types].xml` file in both panes. +- You can now compare all package parts by selecting `Commands > Synchronise Dirs...`. Press `Compare`. This will do a full, recursive comparison. You can filter out parts that are identical. +- You can select an item that differs and press `Ctrl+F3` to open the two parts in WinMerge and see the exact comparison of the part's contents. The XML files should automatically reformat/reindent to ease the comparison instead of showing the entire XML contents on a single line. This is the reason for requiring the `DisplayXMLFiles.dll` plugin. +- In Total Commander, you can also navigate to specific files in the left-hand and right-hand panes and select `File > Compare by Content...`. This will open WinMerge directly. +- Note that since WinMerge reformats the XML, it does so in a temporary file. If you make changes to the contents of any of the 2 panes in WinMerge and save the file, it will not be saved back into the Excel file. + +## Code conventions +ClosedXML has a fairly large codebase and we therefore want to keep code revisions as clean and tidy as possible. It is therefore important not to introduce unnecessary whitespace changes in your commits. + +To ensure you follow the coding conventions, please do the following steps before you commit your code: + +- In Visual Studio, run `CodeMaid > Cleanup Active Document` or `Ctrl+M, Space` on each file that you have altered. This will ensure the correct whitespace consistency. +- Some files, not all, have a header in the first line: `// Keep this file CodeMaid organised and cleaned`. For these files, also run `CodeMaid > Reorganize Active Document` or `Ctrl+M, Z`. This will reorder properties and methods alphabetically into a predetermined order. For example, public properties and methods will be organized before private properties and methods. Not all files require this yet. Please take note of the headers. diff --git a/ClosedXML.Examples/Examples/Created/AddingComments.xlsx b/ClosedXML.Examples/Examples/Created/AddingComments.xlsx new file mode 100644 index 0000000..41b438b Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AddingComments.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AddingDataSet.xlsx b/ClosedXML.Examples/Examples/Created/AddingDataSet.xlsx new file mode 100644 index 0000000..a7c8dcf Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AddingDataSet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AddingDataTableAsWorksheet.xlsx b/ClosedXML.Examples/Examples/Created/AddingDataTableAsWorksheet.xlsx new file mode 100644 index 0000000..b2a30e2 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AddingDataTableAsWorksheet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AddingRowToTables.xlsx b/ClosedXML.Examples/Examples/Created/AddingRowToTables.xlsx new file mode 100644 index 0000000..15aa2fa Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AddingRowToTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AdjustToContents.xlsx b/ClosedXML.Examples/Examples/Created/AdjustToContents.xlsx new file mode 100644 index 0000000..2be94e4 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AdjustToContents.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AdjustToContentsWithAutoFilter.xlsx b/ClosedXML.Examples/Examples/Created/AdjustToContentsWithAutoFilter.xlsx new file mode 100644 index 0000000..a0b6444 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AdjustToContentsWithAutoFilter.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/AutoFilter.xlsx b/ClosedXML.Examples/Examples/Created/AutoFilter.xlsx new file mode 100644 index 0000000..748fdce Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/AutoFilter.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/BasicTable.xlsx b/ClosedXML.Examples/Examples/Created/BasicTable.xlsx new file mode 100644 index 0000000..ee3c39f Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/BasicTable.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/BasicTable_Modified.xlsx b/ClosedXML.Examples/Examples/Created/BasicTable_Modified.xlsx new file mode 100644 index 0000000..6d51928 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/BasicTable_Modified.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/BlankCells.xlsx b/ClosedXML.Examples/Examples/Created/BlankCells.xlsx new file mode 100644 index 0000000..bf4348a Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/BlankCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/CellMoves.xlsx b/ClosedXML.Examples/Examples/Created/CellMoves.xlsx new file mode 100644 index 0000000..d011551 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/CellMoves.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/CellValues.xlsx b/ClosedXML.Examples/Examples/Created/CellValues.xlsx new file mode 100644 index 0000000..4339fbd Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/CellValues.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ClearingRanges.xlsx b/ClosedXML.Examples/Examples/Created/ClearingRanges.xlsx new file mode 100644 index 0000000..76a0346 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ClearingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Collections.xlsx b/ClosedXML.Examples/Examples/Created/Collections.xlsx new file mode 100644 index 0000000..8a57595 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Collections.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ColumnCells.xlsx b/ClosedXML.Examples/Examples/Created/ColumnCells.xlsx new file mode 100644 index 0000000..6338844 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ColumnCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ColumnCollection.xlsx b/ClosedXML.Examples/Examples/Created/ColumnCollection.xlsx new file mode 100644 index 0000000..daecff8 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ColumnCollection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ColumnSettings.xlsx b/ClosedXML.Examples/Examples/Created/ColumnSettings.xlsx new file mode 100644 index 0000000..1645c34 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ColumnSettings.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/CopyingRanges.xlsx b/ClosedXML.Examples/Examples/Created/CopyingRanges.xlsx new file mode 100644 index 0000000..b4e932f Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/CopyingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/CopyingRowsAndColumns.xlsx b/ClosedXML.Examples/Examples/Created/CopyingRowsAndColumns.xlsx new file mode 100644 index 0000000..51824a5 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/CopyingRowsAndColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/CopyingWorksheets.xlsx b/ClosedXML.Examples/Examples/Created/CopyingWorksheets.xlsx new file mode 100644 index 0000000..d4ed3d5 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/CopyingWorksheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DataTypes.xlsx b/ClosedXML.Examples/Examples/Created/DataTypes.xlsx new file mode 100644 index 0000000..e651ac7 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DataTypes.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DataTypesUnderDifferentCulture.xlsx b/ClosedXML.Examples/Examples/Created/DataTypesUnderDifferentCulture.xlsx new file mode 100644 index 0000000..9496b85 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DataTypesUnderDifferentCulture.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DataValidation.xlsx b/ClosedXML.Examples/Examples/Created/DataValidation.xlsx new file mode 100644 index 0000000..6d18dce Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DataValidation.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DefaultStyles.xlsx b/ClosedXML.Examples/Examples/Created/DefaultStyles.xlsx new file mode 100644 index 0000000..25ffad0 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DefaultStyles.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DefiningRanges.xlsx b/ClosedXML.Examples/Examples/Created/DefiningRanges.xlsx new file mode 100644 index 0000000..4b47900 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DefiningRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DeletingColumns.xlsx b/ClosedXML.Examples/Examples/Created/DeletingColumns.xlsx new file mode 100644 index 0000000..ba8f75e Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DeletingColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/DeletingRanges.xlsx b/ClosedXML.Examples/Examples/Created/DeletingRanges.xlsx new file mode 100644 index 0000000..d4eabf8 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/DeletingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Formulas.xlsx b/ClosedXML.Examples/Examples/Created/Formulas.xlsx new file mode 100644 index 0000000..18435e4 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Formulas.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/FreezePanes.xlsx b/ClosedXML.Examples/Examples/Created/FreezePanes.xlsx new file mode 100644 index 0000000..a6f429d Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/FreezePanes.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/HeaderFooters.xlsx b/ClosedXML.Examples/Examples/Created/HeaderFooters.xlsx new file mode 100644 index 0000000..c2e7a11 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/HeaderFooters.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/HelloWorld.xlsx b/ClosedXML.Examples/Examples/Created/HelloWorld.xlsx new file mode 100644 index 0000000..d72af13 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/HelloWorld.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/HideSheets.xlsx b/ClosedXML.Examples/Examples/Created/HideSheets.xlsx new file mode 100644 index 0000000..9f987cc Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/HideSheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/HideUnhide.xlsx b/ClosedXML.Examples/Examples/Created/HideUnhide.xlsx new file mode 100644 index 0000000..85e4e5f Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/HideUnhide.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Hyperlinks.xlsx b/ClosedXML.Examples/Examples/Created/Hyperlinks.xlsx new file mode 100644 index 0000000..e0e170a Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Hyperlinks.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertColumns.xlsx b/ClosedXML.Examples/Examples/Created/InsertColumns.xlsx new file mode 100644 index 0000000..163ffc0 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertRows.xlsx b/ClosedXML.Examples/Examples/Created/InsertRows.xlsx new file mode 100644 index 0000000..72f55ae Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertRows.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertingData.xlsx b/ClosedXML.Examples/Examples/Created/InsertingData.xlsx new file mode 100644 index 0000000..7a37e4d Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertingData.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertingDeletingColumns.xlsx b/ClosedXML.Examples/Examples/Created/InsertingDeletingColumns.xlsx new file mode 100644 index 0000000..2255083 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertingDeletingColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertingDeletingRows.xlsx b/ClosedXML.Examples/Examples/Created/InsertingDeletingRows.xlsx new file mode 100644 index 0000000..bea82f2 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertingDeletingRows.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/InsertingTables.xlsx b/ClosedXML.Examples/Examples/Created/InsertingTables.xlsx new file mode 100644 index 0000000..0be08ed Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/InsertingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/LambdaExpressions.xlsx b/ClosedXML.Examples/Examples/Created/LambdaExpressions.xlsx new file mode 100644 index 0000000..202beaf Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/LambdaExpressions.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Margins.xlsx b/ClosedXML.Examples/Examples/Created/Margins.xlsx new file mode 100644 index 0000000..dbaa51e Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Margins.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/MergedCells.xlsx b/ClosedXML.Examples/Examples/Created/MergedCells.xlsx new file mode 100644 index 0000000..ab58776 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/MergedCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/MergedMoves.xlsx b/ClosedXML.Examples/Examples/Created/MergedMoves.xlsx new file mode 100644 index 0000000..47cce41 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/MergedMoves.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/MultipleRanges.xlsx b/ClosedXML.Examples/Examples/Created/MultipleRanges.xlsx new file mode 100644 index 0000000..7e48c53 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/MultipleRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/MultipleSheets.xlsx b/ClosedXML.Examples/Examples/Created/MultipleSheets.xlsx new file mode 100644 index 0000000..7e59087 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/MultipleSheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/NamedRanges.xlsx b/ClosedXML.Examples/Examples/Created/NamedRanges.xlsx new file mode 100644 index 0000000..e533d11 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/NamedRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Outline.xlsx b/ClosedXML.Examples/Examples/Created/Outline.xlsx new file mode 100644 index 0000000..196a6e0 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Outline.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Page.xlsx b/ClosedXML.Examples/Examples/Created/Page.xlsx new file mode 100644 index 0000000..628781f Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Page.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/PivotTables.xlsx b/ClosedXML.Examples/Examples/Created/PivotTables.xlsx new file mode 100644 index 0000000..7bf64cd Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/PivotTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ResizingTables.xlsx b/ClosedXML.Examples/Examples/Created/ResizingTables.xlsx new file mode 100644 index 0000000..90aec87 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ResizingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/RightToLeft.xlsx b/ClosedXML.Examples/Examples/Created/RightToLeft.xlsx new file mode 100644 index 0000000..47a76d5 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/RightToLeft.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/RowCells.xlsx b/ClosedXML.Examples/Examples/Created/RowCells.xlsx new file mode 100644 index 0000000..9d15d2b Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/RowCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/RowCollection.xlsx b/ClosedXML.Examples/Examples/Created/RowCollection.xlsx new file mode 100644 index 0000000..9b067ac Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/RowCollection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/RowSettings.xlsx b/ClosedXML.Examples/Examples/Created/RowSettings.xlsx new file mode 100644 index 0000000..43f749e Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/RowSettings.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/SheetProtection.xlsx b/ClosedXML.Examples/Examples/Created/SheetProtection.xlsx new file mode 100644 index 0000000..71b2c3a Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/SheetProtection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/SheetTab.xlsx b/ClosedXML.Examples/Examples/Created/SheetTab.xlsx new file mode 100644 index 0000000..198a335 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/SheetTab.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/SheetViews.xlsx b/ClosedXML.Examples/Examples/Created/SheetViews.xlsx new file mode 100644 index 0000000..e63b8db Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/SheetViews.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Sheets.xlsx b/ClosedXML.Examples/Examples/Created/Sheets.xlsx new file mode 100644 index 0000000..49fa6ff Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Sheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ShiftingFormulas.xlsx b/ClosedXML.Examples/Examples/Created/ShiftingFormulas.xlsx new file mode 100644 index 0000000..22f5242 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ShiftingFormulas.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ShiftingRanges.xlsx b/ClosedXML.Examples/Examples/Created/ShiftingRanges.xlsx new file mode 100644 index 0000000..bcedd34 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ShiftingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/ShowCase.xlsx b/ClosedXML.Examples/Examples/Created/ShowCase.xlsx new file mode 100644 index 0000000..bf4035b Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/ShowCase.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/SortExample.xlsx b/ClosedXML.Examples/Examples/Created/SortExample.xlsx new file mode 100644 index 0000000..8628ce6 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/SortExample.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/Sorting.xlsx b/ClosedXML.Examples/Examples/Created/Sorting.xlsx new file mode 100644 index 0000000..c5d60ff Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/Sorting.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/StyleRowsColumns.xlsx b/ClosedXML.Examples/Examples/Created/StyleRowsColumns.xlsx new file mode 100644 index 0000000..f6e7608 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/StyleRowsColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/StyleWorksheet.xlsx b/ClosedXML.Examples/Examples/Created/StyleWorksheet.xlsx new file mode 100644 index 0000000..89b4c9e Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/StyleWorksheet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/TabColors.xlsx b/ClosedXML.Examples/Examples/Created/TabColors.xlsx new file mode 100644 index 0000000..cc12352 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/TabColors.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/TransposeRanges.xlsx b/ClosedXML.Examples/Examples/Created/TransposeRanges.xlsx new file mode 100644 index 0000000..0d533bf Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/TransposeRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/TransposeRangesPlus.xlsx b/ClosedXML.Examples/Examples/Created/TransposeRangesPlus.xlsx new file mode 100644 index 0000000..8777999 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/TransposeRangesPlus.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/TwoPages.xlsx b/ClosedXML.Examples/Examples/Created/TwoPages.xlsx new file mode 100644 index 0000000..7d4f1c6 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/TwoPages.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/UsingColors.xlsx b/ClosedXML.Examples/Examples/Created/UsingColors.xlsx new file mode 100644 index 0000000..812c129 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/UsingColors.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/UsingPhonetics.xlsx b/ClosedXML.Examples/Examples/Created/UsingPhonetics.xlsx new file mode 100644 index 0000000..cbd24ee Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/UsingPhonetics.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/UsingRichText.xlsx b/ClosedXML.Examples/Examples/Created/UsingRichText.xlsx new file mode 100644 index 0000000..77cf712 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/UsingRichText.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/UsingTables.xlsx b/ClosedXML.Examples/Examples/Created/UsingTables.xlsx new file mode 100644 index 0000000..a6ca79c Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/UsingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/WorkbookProperties.xlsx b/ClosedXML.Examples/Examples/Created/WorkbookProperties.xlsx new file mode 100644 index 0000000..260e30a Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/WorkbookProperties.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleAlignment.xlsx b/ClosedXML.Examples/Examples/Created/styleAlignment.xlsx new file mode 100644 index 0000000..8fdcc98 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleAlignment.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleBorder.xlsx b/ClosedXML.Examples/Examples/Created/styleBorder.xlsx new file mode 100644 index 0000000..aed649c Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleBorder.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleFill.xlsx b/ClosedXML.Examples/Examples/Created/styleFill.xlsx new file mode 100644 index 0000000..13577c3 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleFill.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleFont.xlsx b/ClosedXML.Examples/Examples/Created/styleFont.xlsx new file mode 100644 index 0000000..290958b Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleFont.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleIncludeQuotePrefix.xlsx b/ClosedXML.Examples/Examples/Created/styleIncludeQuotePrefix.xlsx new file mode 100644 index 0000000..e648105 Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleIncludeQuotePrefix.xlsx differ diff --git a/ClosedXML.Examples/Examples/Created/styleNumberFormat.xlsx b/ClosedXML.Examples/Examples/Created/styleNumberFormat.xlsx new file mode 100644 index 0000000..1a9ab0c Binary files /dev/null and b/ClosedXML.Examples/Examples/Created/styleNumberFormat.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AddingComments.xlsx b/ClosedXML.Examples/Examples/Modified/AddingComments.xlsx new file mode 100644 index 0000000..8995e5e Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AddingComments.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AddingDataSet.xlsx b/ClosedXML.Examples/Examples/Modified/AddingDataSet.xlsx new file mode 100644 index 0000000..a7c8dcf Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AddingDataSet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AddingDataTableAsWorksheet.xlsx b/ClosedXML.Examples/Examples/Modified/AddingDataTableAsWorksheet.xlsx new file mode 100644 index 0000000..b2a30e2 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AddingDataTableAsWorksheet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AddingRowToTables.xlsx b/ClosedXML.Examples/Examples/Modified/AddingRowToTables.xlsx new file mode 100644 index 0000000..15aa2fa Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AddingRowToTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AdjustToContents.xlsx b/ClosedXML.Examples/Examples/Modified/AdjustToContents.xlsx new file mode 100644 index 0000000..2be94e4 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AdjustToContents.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AdjustToContentsWithAutoFilter.xlsx b/ClosedXML.Examples/Examples/Modified/AdjustToContentsWithAutoFilter.xlsx new file mode 100644 index 0000000..1ff3667 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AdjustToContentsWithAutoFilter.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/AutoFilter.xlsx b/ClosedXML.Examples/Examples/Modified/AutoFilter.xlsx new file mode 100644 index 0000000..294a06c Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/AutoFilter.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/BasicTable.xlsx b/ClosedXML.Examples/Examples/Modified/BasicTable.xlsx new file mode 100644 index 0000000..747b056 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/BasicTable.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/BasicTable_Modified.xlsx b/ClosedXML.Examples/Examples/Modified/BasicTable_Modified.xlsx new file mode 100644 index 0000000..e4cab68 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/BasicTable_Modified.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/BlankCells.xlsx b/ClosedXML.Examples/Examples/Modified/BlankCells.xlsx new file mode 100644 index 0000000..bf4348a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/BlankCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/CellMoves.xlsx b/ClosedXML.Examples/Examples/Modified/CellMoves.xlsx new file mode 100644 index 0000000..d011551 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/CellMoves.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/CellValues.xlsx b/ClosedXML.Examples/Examples/Modified/CellValues.xlsx new file mode 100644 index 0000000..526a1ff Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/CellValues.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ClearingRanges.xlsx b/ClosedXML.Examples/Examples/Modified/ClearingRanges.xlsx new file mode 100644 index 0000000..457ac89 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ClearingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Collections.xlsx b/ClosedXML.Examples/Examples/Modified/Collections.xlsx new file mode 100644 index 0000000..8a57595 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Collections.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ColumnCells.xlsx b/ClosedXML.Examples/Examples/Modified/ColumnCells.xlsx new file mode 100644 index 0000000..6338844 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ColumnCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ColumnCollection.xlsx b/ClosedXML.Examples/Examples/Modified/ColumnCollection.xlsx new file mode 100644 index 0000000..b46824b Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ColumnCollection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ColumnSettings.xlsx b/ClosedXML.Examples/Examples/Modified/ColumnSettings.xlsx new file mode 100644 index 0000000..1802bab Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ColumnSettings.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/CopyingRanges.xlsx b/ClosedXML.Examples/Examples/Modified/CopyingRanges.xlsx new file mode 100644 index 0000000..b4e932f Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/CopyingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/CopyingRowsAndColumns.xlsx b/ClosedXML.Examples/Examples/Modified/CopyingRowsAndColumns.xlsx new file mode 100644 index 0000000..51824a5 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/CopyingRowsAndColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/CopyingWorksheets.xlsx b/ClosedXML.Examples/Examples/Modified/CopyingWorksheets.xlsx new file mode 100644 index 0000000..d4ed3d5 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/CopyingWorksheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DataTypes.xlsx b/ClosedXML.Examples/Examples/Modified/DataTypes.xlsx new file mode 100644 index 0000000..0a97ac3 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DataTypes.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DataTypesUnderDifferentCulture.xlsx b/ClosedXML.Examples/Examples/Modified/DataTypesUnderDifferentCulture.xlsx new file mode 100644 index 0000000..c7ae2c7 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DataTypesUnderDifferentCulture.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DataValidation.xlsx b/ClosedXML.Examples/Examples/Modified/DataValidation.xlsx new file mode 100644 index 0000000..a7de870 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DataValidation.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DefaultStyles.xlsx b/ClosedXML.Examples/Examples/Modified/DefaultStyles.xlsx new file mode 100644 index 0000000..7d77c32 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DefaultStyles.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DefiningRanges.xlsx b/ClosedXML.Examples/Examples/Modified/DefiningRanges.xlsx new file mode 100644 index 0000000..69b987b Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DefiningRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DeletingColumns.xlsx b/ClosedXML.Examples/Examples/Modified/DeletingColumns.xlsx new file mode 100644 index 0000000..fd0d760 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DeletingColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/DeletingRanges.xlsx b/ClosedXML.Examples/Examples/Modified/DeletingRanges.xlsx new file mode 100644 index 0000000..86cd60e Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/DeletingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Formulas.xlsx b/ClosedXML.Examples/Examples/Modified/Formulas.xlsx new file mode 100644 index 0000000..18435e4 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Formulas.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/FreezePanes.xlsx b/ClosedXML.Examples/Examples/Modified/FreezePanes.xlsx new file mode 100644 index 0000000..eceab4d Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/FreezePanes.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/HeaderFooters.xlsx b/ClosedXML.Examples/Examples/Modified/HeaderFooters.xlsx new file mode 100644 index 0000000..6a3af49 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/HeaderFooters.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/HelloWorld.xlsx b/ClosedXML.Examples/Examples/Modified/HelloWorld.xlsx new file mode 100644 index 0000000..995b547 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/HelloWorld.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/HideSheets.xlsx b/ClosedXML.Examples/Examples/Modified/HideSheets.xlsx new file mode 100644 index 0000000..e06f14d Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/HideSheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/HideUnhide.xlsx b/ClosedXML.Examples/Examples/Modified/HideUnhide.xlsx new file mode 100644 index 0000000..51ae5a3 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/HideUnhide.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Hyperlinks.xlsx b/ClosedXML.Examples/Examples/Modified/Hyperlinks.xlsx new file mode 100644 index 0000000..665d549 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Hyperlinks.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertColumns.xlsx b/ClosedXML.Examples/Examples/Modified/InsertColumns.xlsx new file mode 100644 index 0000000..12f9153 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertRows.xlsx b/ClosedXML.Examples/Examples/Modified/InsertRows.xlsx new file mode 100644 index 0000000..1108e54 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertRows.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertingData.xlsx b/ClosedXML.Examples/Examples/Modified/InsertingData.xlsx new file mode 100644 index 0000000..b9a071e Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertingData.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertingDeletingColumns.xlsx b/ClosedXML.Examples/Examples/Modified/InsertingDeletingColumns.xlsx new file mode 100644 index 0000000..c3d7db1 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertingDeletingColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertingDeletingRows.xlsx b/ClosedXML.Examples/Examples/Modified/InsertingDeletingRows.xlsx new file mode 100644 index 0000000..92c9408 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertingDeletingRows.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/InsertingTables.xlsx b/ClosedXML.Examples/Examples/Modified/InsertingTables.xlsx new file mode 100644 index 0000000..f08563a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/InsertingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/LambdaExpressions.xlsx b/ClosedXML.Examples/Examples/Modified/LambdaExpressions.xlsx new file mode 100644 index 0000000..ab5fd9e Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/LambdaExpressions.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Margins.xlsx b/ClosedXML.Examples/Examples/Modified/Margins.xlsx new file mode 100644 index 0000000..16ebabc Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Margins.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/MergedCells.xlsx b/ClosedXML.Examples/Examples/Modified/MergedCells.xlsx new file mode 100644 index 0000000..d18e97a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/MergedCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/MergedMoves.xlsx b/ClosedXML.Examples/Examples/Modified/MergedMoves.xlsx new file mode 100644 index 0000000..842049e Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/MergedMoves.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/MultipleRanges.xlsx b/ClosedXML.Examples/Examples/Modified/MultipleRanges.xlsx new file mode 100644 index 0000000..1b28483 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/MultipleRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/MultipleSheets.xlsx b/ClosedXML.Examples/Examples/Modified/MultipleSheets.xlsx new file mode 100644 index 0000000..3ee2f03 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/MultipleSheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/NamedRanges.xlsx b/ClosedXML.Examples/Examples/Modified/NamedRanges.xlsx new file mode 100644 index 0000000..e533d11 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/NamedRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Outline.xlsx b/ClosedXML.Examples/Examples/Modified/Outline.xlsx new file mode 100644 index 0000000..93417e1 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Outline.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Page.xlsx b/ClosedXML.Examples/Examples/Modified/Page.xlsx new file mode 100644 index 0000000..85733f1 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Page.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/PivotTables.xlsx b/ClosedXML.Examples/Examples/Modified/PivotTables.xlsx new file mode 100644 index 0000000..7fa2390 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/PivotTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ResizingTables.xlsx b/ClosedXML.Examples/Examples/Modified/ResizingTables.xlsx new file mode 100644 index 0000000..90aec87 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ResizingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/RightToLeft.xlsx b/ClosedXML.Examples/Examples/Modified/RightToLeft.xlsx new file mode 100644 index 0000000..cd7943b Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/RightToLeft.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/RowCells.xlsx b/ClosedXML.Examples/Examples/Modified/RowCells.xlsx new file mode 100644 index 0000000..def71bf Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/RowCells.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/RowCollection.xlsx b/ClosedXML.Examples/Examples/Modified/RowCollection.xlsx new file mode 100644 index 0000000..d4010ba Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/RowCollection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/RowSettings.xlsx b/ClosedXML.Examples/Examples/Modified/RowSettings.xlsx new file mode 100644 index 0000000..3d8052a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/RowSettings.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/SheetProtection.xlsx b/ClosedXML.Examples/Examples/Modified/SheetProtection.xlsx new file mode 100644 index 0000000..3e25afc Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/SheetProtection.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/SheetTab.xlsx b/ClosedXML.Examples/Examples/Modified/SheetTab.xlsx new file mode 100644 index 0000000..15fe42a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/SheetTab.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/SheetViews.xlsx b/ClosedXML.Examples/Examples/Modified/SheetViews.xlsx new file mode 100644 index 0000000..2b77aae Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/SheetViews.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Sheets.xlsx b/ClosedXML.Examples/Examples/Modified/Sheets.xlsx new file mode 100644 index 0000000..b7f9bbb Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Sheets.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ShiftingFormulas.xlsx b/ClosedXML.Examples/Examples/Modified/ShiftingFormulas.xlsx new file mode 100644 index 0000000..22f5242 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ShiftingFormulas.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ShiftingRanges.xlsx b/ClosedXML.Examples/Examples/Modified/ShiftingRanges.xlsx new file mode 100644 index 0000000..8062423 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ShiftingRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/ShowCase.xlsx b/ClosedXML.Examples/Examples/Modified/ShowCase.xlsx new file mode 100644 index 0000000..bf4035b Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/ShowCase.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/SortExample.xlsx b/ClosedXML.Examples/Examples/Modified/SortExample.xlsx new file mode 100644 index 0000000..d0db288 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/SortExample.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/Sorting.xlsx b/ClosedXML.Examples/Examples/Modified/Sorting.xlsx new file mode 100644 index 0000000..d13c679 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/Sorting.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/StyleRowsColumns.xlsx b/ClosedXML.Examples/Examples/Modified/StyleRowsColumns.xlsx new file mode 100644 index 0000000..8b300d0 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/StyleRowsColumns.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/StyleWorksheet.xlsx b/ClosedXML.Examples/Examples/Modified/StyleWorksheet.xlsx new file mode 100644 index 0000000..f834c59 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/StyleWorksheet.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/TabColors.xlsx b/ClosedXML.Examples/Examples/Modified/TabColors.xlsx new file mode 100644 index 0000000..c2fe6dd Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/TabColors.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/TransposeRanges.xlsx b/ClosedXML.Examples/Examples/Modified/TransposeRanges.xlsx new file mode 100644 index 0000000..3de344d Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/TransposeRanges.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/TransposeRangesPlus.xlsx b/ClosedXML.Examples/Examples/Modified/TransposeRangesPlus.xlsx new file mode 100644 index 0000000..70e0162 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/TransposeRangesPlus.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/TwoPages.xlsx b/ClosedXML.Examples/Examples/Modified/TwoPages.xlsx new file mode 100644 index 0000000..fb97e3a Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/TwoPages.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/UsingColors.xlsx b/ClosedXML.Examples/Examples/Modified/UsingColors.xlsx new file mode 100644 index 0000000..3451dd2 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/UsingColors.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/UsingPhonetics.xlsx b/ClosedXML.Examples/Examples/Modified/UsingPhonetics.xlsx new file mode 100644 index 0000000..7d0c0d6 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/UsingPhonetics.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/UsingRichText.xlsx b/ClosedXML.Examples/Examples/Modified/UsingRichText.xlsx new file mode 100644 index 0000000..b8d74da Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/UsingRichText.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/UsingTables.xlsx b/ClosedXML.Examples/Examples/Modified/UsingTables.xlsx new file mode 100644 index 0000000..a6ca79c Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/UsingTables.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/WorkbookProperties.xlsx b/ClosedXML.Examples/Examples/Modified/WorkbookProperties.xlsx new file mode 100644 index 0000000..3b017e4 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/WorkbookProperties.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleAlignment.xlsx b/ClosedXML.Examples/Examples/Modified/styleAlignment.xlsx new file mode 100644 index 0000000..5b3742f Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleAlignment.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleBorder.xlsx b/ClosedXML.Examples/Examples/Modified/styleBorder.xlsx new file mode 100644 index 0000000..e9bd318 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleBorder.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleFill.xlsx b/ClosedXML.Examples/Examples/Modified/styleFill.xlsx new file mode 100644 index 0000000..91f9a5c Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleFill.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleFont.xlsx b/ClosedXML.Examples/Examples/Modified/styleFont.xlsx new file mode 100644 index 0000000..f1a7b9f Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleFont.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleIncludeQuotePrefix.xlsx b/ClosedXML.Examples/Examples/Modified/styleIncludeQuotePrefix.xlsx new file mode 100644 index 0000000..c9ea419 Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleIncludeQuotePrefix.xlsx differ diff --git a/ClosedXML.Examples/Examples/Modified/styleNumberFormat.xlsx b/ClosedXML.Examples/Examples/Modified/styleNumberFormat.xlsx new file mode 100644 index 0000000..a2a087f Binary files /dev/null and b/ClosedXML.Examples/Examples/Modified/styleNumberFormat.xlsx differ diff --git a/ClosedXML.sln b/ClosedXML.sln new file mode 100644 index 0000000..c0bd8ee --- /dev/null +++ b/ClosedXML.sln @@ -0,0 +1,67 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML_Sandbox", "ClosedXML_Sandbox\ClosedXML_Sandbox.csproj", "{38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5C94E22C-85AA-48FD-B082-CF929FFC6C31}" + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + ClosedXML.vsmdi = ClosedXML.vsmdi + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML_Examples", "ClosedXML_Examples\ClosedXML_Examples.csproj", "{03A518D0-1CB7-488E-861C-C4E782B27A46}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML", "ClosedXML\ClosedXML.csproj", "{BD5E6BFE-E837-4A35-BCA9-39667D873A20}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML_Tests", "ClosedXML_Tests\ClosedXML_Tests.csproj", "{09B066ED-E4A7-4545-A1A4-FF03DD524BDF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{073CFB1C-43DC-4ADC-9D12-BB8D7B10C099}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md + .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release.Signed|Any CPU = Release.Signed|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Release.Signed|Any CPU.ActiveCfg = Release.Signed|Any CPU + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Release.Signed|Any CPU.Build.0 = Release.Signed|Any CPU + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38B882F0-E6F2-45C5-9BE9-CDC27FBEB4AB}.Release|Any CPU.Build.0 = Release|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Release.Signed|Any CPU.ActiveCfg = Release.Signed|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Release.Signed|Any CPU.Build.0 = Release.Signed|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03A518D0-1CB7-488E-861C-C4E782B27A46}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Release.Signed|Any CPU.ActiveCfg = Release.Signed|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Release.Signed|Any CPU.Build.0 = Release.Signed|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5E6BFE-E837-4A35-BCA9-39667D873A20}.Release|Any CPU.Build.0 = Release|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Release.Signed|Any CPU.ActiveCfg = Release.Signed|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Release.Signed|Any CPU.Build.0 = Release.Signed|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09B066ED-E4A7-4545-A1A4-FF03DD524BDF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E35B7D8-9690-408F-B52A-F4FC485A6B09} + EndGlobalSection + GlobalSection(TestCaseManagementSettings) = postSolution + CategoryFile = ClosedXML.vsmdi + EndGlobalSection +EndGlobal diff --git a/ClosedXML/Attributes/XLColumnAttribute.cs b/ClosedXML/Attributes/XLColumnAttribute.cs new file mode 100644 index 0000000..67ab26f --- /dev/null +++ b/ClosedXML/Attributes/XLColumnAttribute.cs @@ -0,0 +1,42 @@ +using ClosedXML.Excel; +using System; +using System.Linq; +using System.Reflection; + +namespace ClosedXML.Attributes +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public class XLColumnAttribute : Attribute + { + public String Header { get; set; } + public Boolean Ignore { get; set; } + public Int32 Order { get; set; } + + private static XLColumnAttribute GetXLColumnAttribute(MemberInfo mi) + { + if (!mi.HasAttribute()) return null; + return mi.GetAttributes().First(); + } + + internal static String GetHeader(MemberInfo mi) + { + var attribute = GetXLColumnAttribute(mi); + if (attribute == null) return null; + return String.IsNullOrWhiteSpace(attribute.Header) ? null : attribute.Header; + } + + internal static Int32 GetOrder(MemberInfo mi) + { + var attribute = GetXLColumnAttribute(mi); + if (attribute == null) return Int32.MaxValue; + return attribute.Order; + } + + internal static Boolean IgnoreMember(MemberInfo mi) + { + var attribute = GetXLColumnAttribute(mi); + if (attribute == null) return false; + return attribute.Ignore; + } + } +} diff --git a/ClosedXML/ClosedXML.csproj b/ClosedXML/ClosedXML.csproj new file mode 100644 index 0000000..6257de1 --- /dev/null +++ b/ClosedXML/ClosedXML.csproj @@ -0,0 +1,91 @@ + + + + netstandard2.0;net40;net46 + 8.0 + ClosedXML + ClosedXML + 0.95.4 + Francois Botha, Aleksei Pankratev, Manuel de Leon, Amir Ghezelbash + Francois Botha, Aleksei Pankratev + + ClosedXML + See https://github.com/ClosedXML/ClosedXML/releases/tag/$(productVersion) + ClosedXML is a .NET library for reading, manipulating and writing Excel 2007+ (.xlsx, .xlsm) files. It aims to provide an intuitive and user-friendly interface to dealing with the underlying OpenXML API. + MIT + https://github.com/ClosedXML/ClosedXML + true + nuget-logo.png + https://raw.githubusercontent.com/ClosedXML/ClosedXML/develop/resources/logo/nuget-logo.png + true + $(NoWarn);NU1605;CS1591 + true + Debug;Release;Release.Signed + MIT + + + + ClosedXML.Signed + bin\Release.Signed\ + true + true + true + ClosedXML.snk + $(DefineConstants);RELEASE;STRONGNAME + + + + true + true + + + + $(DefineConstants);_NETSTANDARD_;_NETSTANDARD2_0_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET40_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET46_ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ClosedXML/ClosedXML.nuspec b/ClosedXML/ClosedXML.nuspec new file mode 100644 index 0000000..f065911 --- /dev/null +++ b/ClosedXML/ClosedXML.nuspec @@ -0,0 +1,27 @@ + + + + $id$ + $version$ + $title$ + $author$ + $author$ + ClosedXML is a .NET library for reading, manipulating and writing Excel 2007+ (.xlsx, .xlsm) files. It aims to provide an intuitive and user-friendly interface to dealing with the underlying OpenXML API. + https://github.com/ClosedXML/ClosedXML/blob/master/LICENSE + https://github.com/ClosedXML/ClosedXML + false + @ClosedXML + ClosedXML + + + + + + + + + + + + + \ No newline at end of file diff --git a/ClosedXML/ClosedXML.snk b/ClosedXML/ClosedXML.snk new file mode 100644 index 0000000..a17cff7 Binary files /dev/null and b/ClosedXML/ClosedXML.snk differ diff --git a/ClosedXML/Excel/AutoFilters/IXLAutoFilter.cs b/ClosedXML/Excel/AutoFilters/IXLAutoFilter.cs new file mode 100644 index 0000000..8afbac9 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/IXLAutoFilter.cs @@ -0,0 +1,35 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLFilterDynamicType { AboveAverage, BelowAverage } + + public enum XLFilterType { Regular, Custom, TopBottom, Dynamic, DateTimeGrouping } + + public enum XLTopBottomPart { Top, Bottom } + + public interface IXLAutoFilter + { + [Obsolete("Use IsEnabled")] + Boolean Enabled { get; set; } + IEnumerable HiddenRows { get; } + Boolean IsEnabled { get; set; } + IXLRange Range { get; set; } + Int32 SortColumn { get; set; } + Boolean Sorted { get; set; } + XLSortOrder SortOrder { get; set; } + IEnumerable VisibleRows { get; } + + IXLAutoFilter Clear(); + + IXLFilterColumn Column(String column); + + IXLFilterColumn Column(Int32 column); + + IXLAutoFilter Reapply(); + + IXLAutoFilter Sort(Int32 columnToSortBy = 1, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + } +} diff --git a/ClosedXML/Excel/AutoFilters/IXLCustomFilteredColumn.cs b/ClosedXML/Excel/AutoFilters/IXLCustomFilteredColumn.cs new file mode 100644 index 0000000..37c6a17 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/IXLCustomFilteredColumn.cs @@ -0,0 +1,19 @@ +using System; +namespace ClosedXML.Excel +{ + public interface IXLCustomFilteredColumn + { + void EqualTo(T value) where T : IComparable; + void NotEqualTo(T value) where T : IComparable; + void GreaterThan(T value) where T : IComparable; + void LessThan(T value) where T : IComparable; + void EqualOrGreaterThan(T value) where T : IComparable; + void EqualOrLessThan(T value) where T : IComparable; + void BeginsWith(String value); + void NotBeginsWith(String value); + void EndsWith(String value); + void NotEndsWith(String value); + void Contains(String value); + void NotContains(String value); + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/AutoFilters/IXLFilterColumn.cs b/ClosedXML/Excel/AutoFilters/IXLFilterColumn.cs new file mode 100644 index 0000000..106f485 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/IXLFilterColumn.cs @@ -0,0 +1,72 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLTopBottomType { Items, Percent } + + public enum XLDateTimeGrouping { Year, Month, Day, Hour, Minute, Second } + + public interface IXLFilterColumn + { + void Clear(); + + IXLFilteredColumn AddFilter(T value) where T : IComparable; + + IXLDateTimeGroupFilteredColumn AddDateGroupFilter(DateTime date, XLDateTimeGrouping dateTimeGrouping); + + void Top(Int32 value, XLTopBottomType type = XLTopBottomType.Items); + + void Bottom(Int32 value, XLTopBottomType type = XLTopBottomType.Items); + + void AboveAverage(); + + void BelowAverage(); + + IXLFilterConnector EqualTo(T value) where T : IComparable; + + IXLFilterConnector NotEqualTo(T value) where T : IComparable; + + IXLFilterConnector GreaterThan(T value) where T : IComparable; + + IXLFilterConnector LessThan(T value) where T : IComparable; + + IXLFilterConnector EqualOrGreaterThan(T value) where T : IComparable; + + IXLFilterConnector EqualOrLessThan(T value) where T : IComparable; + + void Between(T minValue, T maxValue) where T : IComparable; + + void NotBetween(T minValue, T maxValue) where T : IComparable; + + IXLFilterConnector BeginsWith(String value); + + IXLFilterConnector NotBeginsWith(String value); + + IXLFilterConnector EndsWith(String value); + + IXLFilterConnector NotEndsWith(String value); + + IXLFilterConnector Contains(String value); + + IXLFilterConnector NotContains(String value); + + XLFilterType FilterType { get; set; } + Int32 TopBottomValue { get; set; } + XLTopBottomType TopBottomType { get; set; } + XLTopBottomPart TopBottomPart { get; set; } + XLFilterDynamicType DynamicType { get; set; } + Double DynamicValue { get; set; } + + IXLFilterColumn SetFilterType(XLFilterType value); + + IXLFilterColumn SetTopBottomValue(Int32 value); + + IXLFilterColumn SetTopBottomType(XLTopBottomType value); + + IXLFilterColumn SetTopBottomPart(XLTopBottomPart value); + + IXLFilterColumn SetDynamicType(XLFilterDynamicType value); + + IXLFilterColumn SetDynamicValue(Double value); + } +} diff --git a/ClosedXML/Excel/AutoFilters/IXLFilterConnector.cs b/ClosedXML/Excel/AutoFilters/IXLFilterConnector.cs new file mode 100644 index 0000000..d5d52be --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/IXLFilterConnector.cs @@ -0,0 +1,10 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLFilterConnector + { + IXLCustomFilteredColumn And { get; } + IXLCustomFilteredColumn Or { get; } + } +} diff --git a/ClosedXML/Excel/AutoFilters/IXLFilteredColumn.cs b/ClosedXML/Excel/AutoFilters/IXLFilteredColumn.cs new file mode 100644 index 0000000..86db9db --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/IXLFilteredColumn.cs @@ -0,0 +1,8 @@ +using System; +namespace ClosedXML.Excel +{ + public interface IXLFilteredColumn + { + IXLFilteredColumn AddFilter(T value) where T : IComparable; + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/AutoFilters/XLAutoFilter.cs b/ClosedXML/Excel/AutoFilters/XLAutoFilter.cs new file mode 100644 index 0000000..35a51cf --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLAutoFilter.cs @@ -0,0 +1,179 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections.Generic; + + internal class XLAutoFilter : IXLAutoFilter + { + private readonly Dictionary _columns = new Dictionary(); + + public XLAutoFilter() + { + Filters = new Dictionary>(); + } + + public Dictionary> Filters { get; private set; } + + #region IXLAutoFilter Members + + [Obsolete("Use IsEnabled")] + public Boolean Enabled { get => IsEnabled; set => IsEnabled = value; } + public IEnumerable HiddenRows { get => Range.Rows(r => r.WorksheetRow().IsHidden); } + public Boolean IsEnabled { get; set; } + public IXLRange Range { get; set; } + public Int32 SortColumn { get; set; } + public Boolean Sorted { get; set; } + public XLSortOrder SortOrder { get; set; } + public IEnumerable VisibleRows { get => Range.Rows(r => !r.WorksheetRow().IsHidden); } + + IXLAutoFilter IXLAutoFilter.Clear() + { + return Clear(); + } + + public IXLFilterColumn Column(String column) + { + var columnNumber = XLHelper.GetColumnNumberFromLetter(column); + if (columnNumber < 1 || columnNumber > XLHelper.MaxColumnNumber) + throw new ArgumentOutOfRangeException(nameof(column), "Column '" + column + "' is outside the allowed column range."); + + return Column(columnNumber); + } + + public IXLFilterColumn Column(Int32 column) + { + if (column < 1 || column > XLHelper.MaxColumnNumber) + throw new ArgumentOutOfRangeException(nameof(column), "Column " + column + " is outside the allowed column range."); + + if (!_columns.TryGetValue(column, out XLFilterColumn filterColumn)) + { + filterColumn = new XLFilterColumn(this, column); + _columns.Add(column, filterColumn); + } + + return filterColumn; + } + + public IXLAutoFilter Reapply() + { + var ws = Range.Worksheet as XLWorksheet; + ws.SuspendEvents(); + + // Recalculate shown / hidden rows + var rows = Range.Rows(2, Range.RowCount()); + rows.ForEach(row => + row.WorksheetRow().Unhide() + ); + + foreach (IXLRangeRow row in rows) + { + var rowMatch = true; + + foreach (var columnIndex in Filters.Keys) + { + var columnFilters = Filters[columnIndex]; + + var columnFilterMatch = true; + + // If the first filter is an 'Or', we need to fudge the initial condition + if (columnFilters.Count > 0 && columnFilters.First().Connector == XLConnector.Or) + { + columnFilterMatch = false; + } + + foreach (var filter in columnFilters) + { + var condition = filter.Condition; + var isText = filter.Value is String; + var isDateTime = filter.Value is DateTime; + + Boolean filterMatch; + + if (isText) + filterMatch = condition(row.Cell(columnIndex).GetFormattedString()); + else if (isDateTime) + filterMatch = row.Cell(columnIndex).DataType == XLDataType.DateTime && + condition(row.Cell(columnIndex).GetDateTime()); + else + filterMatch = row.Cell(columnIndex).DataType == XLDataType.Number && + condition(row.Cell(columnIndex).GetDouble()); + + if (filter.Connector == XLConnector.And) + { + columnFilterMatch &= filterMatch; + if (!columnFilterMatch) break; + } + else + { + columnFilterMatch |= filterMatch; + if (columnFilterMatch) break; + } + } + + rowMatch &= columnFilterMatch; + + if (!rowMatch) break; + } + + if (!rowMatch) row.WorksheetRow().Hide(); + } + + ws.ResumeEvents(); + return this; + } + + IXLAutoFilter IXLAutoFilter.Sort(Int32 columnToSortBy, XLSortOrder sortOrder, Boolean matchCase, + Boolean ignoreBlanks) + { + return Sort(columnToSortBy, sortOrder, matchCase, ignoreBlanks); + } + + #endregion IXLAutoFilter Members + + public XLAutoFilter Clear() + { + if (!IsEnabled) return this; + + IsEnabled = false; + Filters.Clear(); + foreach (IXLRangeRow row in Range.Rows().Where(r => r.RowNumber() > 1)) + row.WorksheetRow().Unhide(); + return this; + } + + public XLAutoFilter Set(IXLRangeBase range) + { + var firstOverlappingTable = range.Worksheet.Tables.FirstOrDefault(t => t.RangeUsed().Intersects(range)); + if (firstOverlappingTable != null) + throw new InvalidOperationException($"The range {range.RangeAddress.ToStringRelative(includeSheet: true)} is already part of table '{firstOverlappingTable.Name}'"); + + Range = range.AsRange(); + IsEnabled = true; + return this; + } + + public XLAutoFilter Sort(Int32 columnToSortBy, XLSortOrder sortOrder, Boolean matchCase, Boolean ignoreBlanks) + { + if (!IsEnabled) + throw new InvalidOperationException("Filter has not been enabled."); + + var ws = Range.Worksheet as XLWorksheet; + ws.SuspendEvents(); + Range.Range(Range.FirstCell().CellBelow(), Range.LastCell()).Sort(columnToSortBy, sortOrder, matchCase, + ignoreBlanks); + + Sorted = true; + SortOrder = sortOrder; + SortColumn = columnToSortBy; + + ws.ResumeEvents(); + + Reapply(); + + return this; + } + } +} diff --git a/ClosedXML/Excel/AutoFilters/XLCustomFilteredColumn.cs b/ClosedXML/Excel/AutoFilters/XLCustomFilteredColumn.cs new file mode 100644 index 0000000..4c6bd52 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLCustomFilteredColumn.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLCustomFilteredColumn : IXLCustomFilteredColumn + { + private readonly XLAutoFilter _autoFilter; + private readonly Int32 _column; + private readonly XLConnector _connector; + + public XLCustomFilteredColumn(XLAutoFilter autoFilter, Int32 column, XLConnector connector) + { + _autoFilter = autoFilter; + _column = column; + _connector = connector; + } + + #region IXLCustomFilteredColumn Members + + public void EqualTo(T value) where T: IComparable + { + if (typeof(T) == typeof(String)) + { + ApplyCustomFilter(value, XLFilterOperator.Equal, + v => + v.ToString().Equals(value.ToString(), StringComparison.InvariantCultureIgnoreCase)); + } + else + { + ApplyCustomFilter(value, XLFilterOperator.Equal, + v => v.CastTo().CompareTo(value) == 0); + } + } + + public void NotEqualTo(T value) where T: IComparable + { + if (typeof(T) == typeof(String)) + { + ApplyCustomFilter(value, XLFilterOperator.NotEqual, + v => + !v.ToString().Equals(value.ToString(), StringComparison.InvariantCultureIgnoreCase)); + } + else + { + ApplyCustomFilter(value, XLFilterOperator.NotEqual, + v => v.CastTo().CompareTo(value) != 0); + } + } + + public void GreaterThan(T value) where T: IComparable + { + ApplyCustomFilter(value, XLFilterOperator.GreaterThan, + v => v.CastTo().CompareTo(value) > 0); + } + + public void LessThan(T value) where T: IComparable + { + ApplyCustomFilter(value, XLFilterOperator.LessThan, v => v.CastTo().CompareTo(value) < 0); + } + + public void EqualOrGreaterThan(T value) where T: IComparable + { + ApplyCustomFilter(value, XLFilterOperator.EqualOrGreaterThan, + v => v.CastTo().CompareTo(value) >= 0); + } + + public void EqualOrLessThan(T value) where T: IComparable + { + ApplyCustomFilter(value, XLFilterOperator.EqualOrLessThan, + v => v.CastTo().CompareTo(value) <= 0); + } + + public void BeginsWith(String value) + { + ApplyCustomFilter(value + "*", XLFilterOperator.Equal, + s => ((string)s).StartsWith(value, StringComparison.InvariantCultureIgnoreCase)); + } + + public void NotBeginsWith(String value) + { + ApplyCustomFilter(value + "*", XLFilterOperator.NotEqual, + s => + !((string)s).StartsWith(value, StringComparison.InvariantCultureIgnoreCase)); + } + + public void EndsWith(String value) + { + ApplyCustomFilter("*" + value, XLFilterOperator.Equal, + s => ((string)s).EndsWith(value, StringComparison.InvariantCultureIgnoreCase)); + } + + public void NotEndsWith(String value) + { + ApplyCustomFilter("*" + value, XLFilterOperator.NotEqual, + s => !((string)s).EndsWith(value, StringComparison.InvariantCultureIgnoreCase)); + } + + public void Contains(String value) + { + ApplyCustomFilter("*" + value + "*", XLFilterOperator.Equal, + s => ((string)s).ToLower().Contains(value.ToLower())); + } + + public void NotContains(String value) + { + ApplyCustomFilter("*" + value + "*", XLFilterOperator.Equal, + s => !((string)s).ToLower().Contains(value.ToLower())); + } + + #endregion + + private void ApplyCustomFilter(T value, XLFilterOperator op, Func condition) + where T : IComparable + { + _autoFilter.Filters[_column].Add(new XLFilter + { + Value = value, + Operator = op, + Connector = _connector, + Condition = condition + }); + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + foreach (IXLRangeRow row in rows) + { + if (_connector == XLConnector.And) + { + if (!row.WorksheetRow().IsHidden) + { + if (condition(row.Cell(_column).GetValue())) + row.WorksheetRow().Unhide(); + else + row.WorksheetRow().Hide(); + } + } + else if (condition(row.Cell(_column).GetValue())) + row.WorksheetRow().Unhide(); + } + } + } +} diff --git a/ClosedXML/Excel/AutoFilters/XLDateTimeGroupFilteredColumn.cs b/ClosedXML/Excel/AutoFilters/XLDateTimeGroupFilteredColumn.cs new file mode 100644 index 0000000..981532a --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLDateTimeGroupFilteredColumn.cs @@ -0,0 +1,57 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLDateTimeGroupFilteredColumn + { + IXLDateTimeGroupFilteredColumn AddDateGroupFilter(DateTime date, XLDateTimeGrouping dateTimeGrouping); + } + + internal class XLDateTimeGroupFilteredColumn : IXLDateTimeGroupFilteredColumn + { + private readonly XLAutoFilter _autoFilter; + private readonly Int32 _column; + + public XLDateTimeGroupFilteredColumn(XLAutoFilter autoFilter, Int32 column) + { + _autoFilter = autoFilter; + _column = column; + } + + public IXLDateTimeGroupFilteredColumn AddDateGroupFilter(DateTime date, XLDateTimeGrouping dateTimeGrouping) + { + Func condition = date2 => IsMatch(date, (DateTime) date2, dateTimeGrouping); + + _autoFilter.Filters[_column].Add(new XLFilter + { + Value = date, + Condition = condition, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or, + DateTimeGrouping = dateTimeGrouping + }); + + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + foreach (IXLRangeRow row in rows) + { + if (row.Cell(_column).DataType == XLDataType.DateTime && condition(row.Cell(_column).GetDateTime())) + row.WorksheetRow().Unhide(); + } + + return this; + } + + internal static Boolean IsMatch(DateTime date1, DateTime date2, XLDateTimeGrouping dateTimeGrouping) + { + Boolean isMatch = true; + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Year) isMatch &= date1.Year.Equals(date2.Year); + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Month) isMatch &= date1.Month.Equals(date2.Month); + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Day) isMatch &= date1.Day.Equals(date2.Day); + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Hour) isMatch &= date1.Hour.Equals(date2.Hour); + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Minute) isMatch &= date1.Minute.Equals(date2.Minute); + if (isMatch && dateTimeGrouping >= XLDateTimeGrouping.Second) isMatch &= date1.Second.Equals(date2.Second); + + return isMatch; + } + } +} diff --git a/ClosedXML/Excel/AutoFilters/XLFilter.cs b/ClosedXML/Excel/AutoFilters/XLFilter.cs new file mode 100644 index 0000000..42ac112 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLFilter.cs @@ -0,0 +1,23 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal enum XLConnector { And, Or } + + internal enum XLFilterOperator { Equal, NotEqual, GreaterThan, LessThan, EqualOrGreaterThan, EqualOrLessThan } + + internal class XLFilter + { + public XLFilter(XLFilterOperator op = XLFilterOperator.Equal) + { + Operator = op; + } + + public Func Condition { get; set; } + public XLConnector Connector { get; set; } + public XLDateTimeGrouping DateTimeGrouping { get; set; } + public XLFilterOperator Operator { get; set; } + public Object Value { get; set; } + } +} diff --git a/ClosedXML/Excel/AutoFilters/XLFilterColumn.cs b/ClosedXML/Excel/AutoFilters/XLFilterColumn.cs new file mode 100644 index 0000000..3217cf7 --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLFilterColumn.cs @@ -0,0 +1,432 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections.Generic; + + internal class XLFilterColumn : IXLFilterColumn + { + private readonly XLAutoFilter _autoFilter; + private readonly Int32 _column; + + public XLFilterColumn(XLAutoFilter autoFilter, Int32 column) + { + _autoFilter = autoFilter; + _column = column; + } + + #region IXLFilterColumn Members + + public void Clear() + { + if (_autoFilter.Filters.ContainsKey(_column)) + _autoFilter.Filters.Remove(_column); + } + + public IXLFilteredColumn AddFilter(T value) where T : IComparable + { + if (typeof(T) == typeof(String)) + { + ApplyCustomFilter(value, XLFilterOperator.Equal, + v => + v.ToString().Equals(value.ToString(), StringComparison.InvariantCultureIgnoreCase), + XLFilterType.Regular); + } + else + { + ApplyCustomFilter(value, XLFilterOperator.Equal, + v => v.CastTo().CompareTo(value) == 0, XLFilterType.Regular); + } + return new XLFilteredColumn(_autoFilter, _column); + } + + public IXLDateTimeGroupFilteredColumn AddDateGroupFilter(DateTime date, XLDateTimeGrouping dateTimeGrouping) + { + Func condition = date2 => XLDateTimeGroupFilteredColumn.IsMatch(date, (DateTime)date2, dateTimeGrouping); + + _autoFilter.IsEnabled = true; + + if (_autoFilter.Filters.TryGetValue(_column, out List filterList)) + filterList.Add( + new XLFilter + { + Value = date, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or, + Condition = condition, + DateTimeGrouping = dateTimeGrouping + } + ); + else + { + _autoFilter.Filters.Add( + _column, + new List + { + new XLFilter + { + Value = date, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or, + Condition = condition, + DateTimeGrouping = dateTimeGrouping + } + } + ); + } + + _autoFilter.Column(_column).FilterType = XLFilterType.DateTimeGrouping; + + var ws = _autoFilter.Range.Worksheet as XLWorksheet; + ws.SuspendEvents(); + + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + + foreach (IXLRangeRow row in rows) + { + if (row.Cell(_column).DataType == XLDataType.DateTime && condition(row.Cell(_column).GetDateTime())) + row.WorksheetRow().Unhide(); + else + row.WorksheetRow().Hide(); + } + ws.ResumeEvents(); + + return new XLDateTimeGroupFilteredColumn(_autoFilter, _column); + } + + public void Top(Int32 value, XLTopBottomType type = XLTopBottomType.Items) + { + _autoFilter.Column(_column).TopBottomPart = XLTopBottomPart.Top; + SetTopBottom(value, type); + } + + public void Bottom(Int32 value, XLTopBottomType type = XLTopBottomType.Items) + { + _autoFilter.Column(_column).TopBottomPart = XLTopBottomPart.Bottom; + SetTopBottom(value, type, false); + } + + public void AboveAverage() + { + ShowAverage(true); + } + + public void BelowAverage() + { + ShowAverage(false); + } + + public IXLFilterConnector EqualTo(T value) where T : IComparable + { + if (typeof(T) == typeof(String)) + { + return ApplyCustomFilter(value, XLFilterOperator.Equal, + v => + v.ToString().Equals(value.ToString(), + StringComparison.InvariantCultureIgnoreCase)); + } + + return ApplyCustomFilter(value, XLFilterOperator.Equal, + v => v.CastTo().CompareTo(value) == 0); + } + + public IXLFilterConnector NotEqualTo(T value) where T : IComparable + { + if (typeof(T) == typeof(String)) + { + return ApplyCustomFilter(value, XLFilterOperator.NotEqual, + v => + !v.ToString().Equals(value.ToString(), + StringComparison.InvariantCultureIgnoreCase)); + } + + return ApplyCustomFilter(value, XLFilterOperator.NotEqual, + v => v.CastTo().CompareTo(value) != 0); + } + + public IXLFilterConnector GreaterThan(T value) where T : IComparable + { + return ApplyCustomFilter(value, XLFilterOperator.GreaterThan, + v => v.CastTo().CompareTo(value) > 0); + } + + public IXLFilterConnector LessThan(T value) where T : IComparable + { + return ApplyCustomFilter(value, XLFilterOperator.LessThan, + v => v.CastTo().CompareTo(value) < 0); + } + + public IXLFilterConnector EqualOrGreaterThan(T value) where T : IComparable + { + return ApplyCustomFilter(value, XLFilterOperator.EqualOrGreaterThan, + v => v.CastTo().CompareTo(value) >= 0); + } + + public IXLFilterConnector EqualOrLessThan(T value) where T : IComparable + { + return ApplyCustomFilter(value, XLFilterOperator.EqualOrLessThan, + v => v.CastTo().CompareTo(value) <= 0); + } + + public void Between(T minValue, T maxValue) where T : IComparable + { + EqualOrGreaterThan(minValue).And.EqualOrLessThan(maxValue); + } + + public void NotBetween(T minValue, T maxValue) where T : IComparable + { + LessThan(minValue).Or.GreaterThan(maxValue); + } + + public static Func BeginsWithFunction { get; } = (value, input) => ((string)input).StartsWith(value, StringComparison.InvariantCultureIgnoreCase); + + public IXLFilterConnector BeginsWith(String value) + { + return ApplyCustomFilter(value + "*", XLFilterOperator.Equal, s => BeginsWithFunction(value, s)); + } + + public IXLFilterConnector NotBeginsWith(String value) + { + return ApplyCustomFilter(value + "*", XLFilterOperator.NotEqual, s => !BeginsWithFunction(value, s)); + } + + public static Func EndsWithFunction { get; } = (value, input) => ((string)input).EndsWith(value, StringComparison.InvariantCultureIgnoreCase); + + public IXLFilterConnector EndsWith(String value) + { + return ApplyCustomFilter("*" + value, XLFilterOperator.Equal, s => EndsWithFunction(value, s)); + } + + public IXLFilterConnector NotEndsWith(String value) + { + return ApplyCustomFilter("*" + value, XLFilterOperator.NotEqual, s => !EndsWithFunction(value, s)); + } + + public static Func ContainsFunction { get; } = (value, input) => ((string)input).IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0; + + public IXLFilterConnector Contains(String value) + { + return ApplyCustomFilter("*" + value + "*", XLFilterOperator.Equal, s => ContainsFunction(value, s)); + } + + public IXLFilterConnector NotContains(String value) + { + return ApplyCustomFilter("*" + value + "*", XLFilterOperator.Equal, s => !ContainsFunction(value, s)); + } + + public XLFilterType FilterType { get; set; } + + public Int32 TopBottomValue { get; set; } + public XLTopBottomType TopBottomType { get; set; } + public XLTopBottomPart TopBottomPart { get; set; } + + public XLFilterDynamicType DynamicType { get; set; } + public Double DynamicValue { get; set; } + + #endregion IXLFilterColumn Members + + private void SetTopBottom(Int32 value, XLTopBottomType type, Boolean takeTop = true) + { + _autoFilter.IsEnabled = true; + _autoFilter.Column(_column).SetFilterType(XLFilterType.TopBottom) + .SetTopBottomValue(value) + .SetTopBottomType(type); + + var values = GetValues(value, type, takeTop); + + Clear(); + _autoFilter.Filters.Add(_column, new List()); + + Boolean addToList = true; + var ws = _autoFilter.Range.Worksheet as XLWorksheet; + ws.SuspendEvents(); + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + foreach (IXLRangeRow row in rows) + { + Boolean foundOne = false; + foreach (double val in values) + { + Func condition = v => (v as IComparable).CompareTo(val) == 0; + if (addToList) + { + _autoFilter.Filters[_column].Add(new XLFilter + { + Value = val, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or, + Condition = condition + }); + } + + var cell = row.Cell(_column); + if (cell.DataType != XLDataType.Number || !condition(cell.GetDouble())) continue; + row.WorksheetRow().Unhide(); + foundOne = true; + } + if (!foundOne) + row.WorksheetRow().Hide(); + + addToList = false; + } + ws.ResumeEvents(); + } + + private IEnumerable GetValues(int value, XLTopBottomType type, bool takeTop) + { + var column = _autoFilter.Range.Column(_column); + var subColumn = column.Column(2, column.CellCount()); + var cellsUsed = subColumn.CellsUsed(c => c.DataType == XLDataType.Number); + if (takeTop) + { + if (type == XLTopBottomType.Items) + { + return cellsUsed.Select(c => c.GetDouble()).OrderByDescending(d => d).Take(value).Distinct(); + } + + var numerics1 = cellsUsed.Select(c => c.GetDouble()); + Int32 valsToTake1 = numerics1.Count() * value / 100; + return numerics1.OrderByDescending(d => d).Take(valsToTake1).Distinct(); + } + + if (type == XLTopBottomType.Items) + { + return cellsUsed.Select(c => c.GetDouble()).OrderBy(d => d).Take(value).Distinct(); + } + + var numerics = cellsUsed.Select(c => c.GetDouble()); + Int32 valsToTake = numerics.Count() * value / 100; + return numerics.OrderBy(d => d).Take(valsToTake).Distinct(); + } + + private void ShowAverage(Boolean aboveAverage) + { + _autoFilter.IsEnabled = true; + _autoFilter.Column(_column).SetFilterType(XLFilterType.Dynamic) + .SetDynamicType(aboveAverage + ? XLFilterDynamicType.AboveAverage + : XLFilterDynamicType.BelowAverage); + var values = GetAverageValues(aboveAverage); + + Clear(); + _autoFilter.Filters.Add(_column, new List()); + + Boolean addToList = true; + var ws = _autoFilter.Range.Worksheet as XLWorksheet; + ws.SuspendEvents(); + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + + foreach (IXLRangeRow row in rows) + { + Boolean foundOne = false; + foreach (double val in values) + { + Func condition = v => (v as IComparable).CompareTo(val) == 0; + if (addToList) + { + _autoFilter.Filters[_column].Add(new XLFilter + { + Value = val, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or, + Condition = condition + }); + } + + var cell = row.Cell(_column); + if (cell.DataType != XLDataType.Number || !condition(cell.GetDouble())) continue; + row.WorksheetRow().Unhide(); + foundOne = true; + } + + if (!foundOne) + row.WorksheetRow().Hide(); + + addToList = false; + } + + ws.ResumeEvents(); + } + + private IEnumerable GetAverageValues(bool aboveAverage) + { + var column = _autoFilter.Range.Column(_column); + var subColumn = column.Column(2, column.CellCount()); + Double average = subColumn.CellsUsed(c => c.DataType == XLDataType.Number).Select(c => c.GetDouble()) + .Average(); + + if (aboveAverage) + { + return + subColumn.CellsUsed(c => c.DataType == XLDataType.Number).Select(c => c.GetDouble()) + .Where(c => c > average).Distinct(); + } + + return + subColumn.CellsUsed(c => c.DataType == XLDataType.Number).Select(c => c.GetDouble()) + .Where(c => c < average).Distinct(); + } + + private IXLFilterConnector ApplyCustomFilter(T value, XLFilterOperator op, Func condition, + XLFilterType filterType = XLFilterType.Custom) + where T : IComparable + { + _autoFilter.IsEnabled = true; + if (filterType == XLFilterType.Custom) + { + Clear(); + _autoFilter.Filters.Add(_column, + new List + { + new XLFilter + { + Value = value, + Operator = op, + Connector = XLConnector.Or, + Condition = condition + } + }); + } + else + { + if (_autoFilter.Filters.TryGetValue(_column, out List filterList)) + filterList.Add(new XLFilter + { + Value = value, + Operator = op, + Connector = XLConnector.Or, + Condition = condition + }); + else + { + _autoFilter.Filters.Add(_column, + new List + { + new XLFilter + { + Value = value, + Operator = op, + Connector = XLConnector.Or, + Condition = condition + } + }); + } + } + _autoFilter.Column(_column).FilterType = filterType; + _autoFilter.Reapply(); + return new XLFilterConnector(_autoFilter, _column); + } + + public IXLFilterColumn SetFilterType(XLFilterType value) { FilterType = value; return this; } + + public IXLFilterColumn SetTopBottomValue(Int32 value) { TopBottomValue = value; return this; } + + public IXLFilterColumn SetTopBottomType(XLTopBottomType value) { TopBottomType = value; return this; } + + public IXLFilterColumn SetTopBottomPart(XLTopBottomPart value) { TopBottomPart = value; return this; } + + public IXLFilterColumn SetDynamicType(XLFilterDynamicType value) { DynamicType = value; return this; } + + public IXLFilterColumn SetDynamicValue(Double value) { DynamicValue = value; return this; } + } +} diff --git a/ClosedXML/Excel/AutoFilters/XLFilterConnector.cs b/ClosedXML/Excel/AutoFilters/XLFilterConnector.cs new file mode 100644 index 0000000..3aac0ec --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLFilterConnector.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLFilterConnector : IXLFilterConnector + { + private readonly XLAutoFilter _autoFilter; + private readonly Int32 _column; + + public XLFilterConnector(XLAutoFilter autoFilter, Int32 column) + { + _autoFilter = autoFilter; + _column = column; + } + + #region IXLFilterConnector Members + + public IXLCustomFilteredColumn And + { + get { return new XLCustomFilteredColumn(_autoFilter, _column, XLConnector.And); } + } + + public IXLCustomFilteredColumn Or + { + get { return new XLCustomFilteredColumn(_autoFilter, _column, XLConnector.Or); } + } + + #endregion + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/AutoFilters/XLFilteredColumn.cs b/ClosedXML/Excel/AutoFilters/XLFilteredColumn.cs new file mode 100644 index 0000000..fde9dba --- /dev/null +++ b/ClosedXML/Excel/AutoFilters/XLFilteredColumn.cs @@ -0,0 +1,57 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLFilteredColumn : IXLFilteredColumn + { + private readonly XLAutoFilter _autoFilter; + private readonly Int32 _column; + + public XLFilteredColumn(XLAutoFilter autoFilter, Int32 column) + { + _autoFilter = autoFilter; + _column = column; + } + + #region IXLFilteredColumn Members + + public IXLFilteredColumn AddFilter(T value) where T : IComparable + { + Func condition; + Boolean isText; + if (typeof(T) == typeof(String)) + { + condition = v => v.ToString().Equals(value.ToString(), StringComparison.InvariantCultureIgnoreCase); + isText = true; + } + else + { + condition = v => v.CastTo().CompareTo(value) == 0; + isText = false; + } + + _autoFilter.Filters[_column].Add(new XLFilter + { + Value = value, + Condition = condition, + Operator = XLFilterOperator.Equal, + Connector = XLConnector.Or + }); + + var rows = _autoFilter.Range.Rows(2, _autoFilter.Range.RowCount()); + + foreach (IXLRangeRow row in rows) + { + if ((isText && condition(row.Cell(_column).GetString())) || + (!isText && row.Cell(_column).DataType == XLDataType.Number && + condition(row.Cell(_column).GetValue()))) + { + row.WorksheetRow().Unhide(); + } + } + return this; + } + + #endregion IXLFilteredColumn Members + } +} diff --git a/ClosedXML/Excel/Caching/IXLRepository.cs b/ClosedXML/Excel/Caching/IXLRepository.cs new file mode 100644 index 0000000..4b447a1 --- /dev/null +++ b/ClosedXML/Excel/Caching/IXLRepository.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + /// + /// Base interface for an abstract repository. + /// + internal interface IXLRepository + { + /// + /// Clear the repository; + /// + void Clear(); + } + + internal interface IXLRepository : IXLRepository, IEnumerable + where Tkey : struct, IEquatable + where Tvalue : class + { + /// + /// Put the into the repository under the specified + /// if there is no such key present. + /// + /// Key to identify the value. + /// Value to put into the repository if key does not exist. + /// Value stored in the repository under the specified . If key already existed + /// returned value may differ from the input one. + Tvalue Store(ref Tkey key, Tvalue value); + } +} diff --git a/ClosedXML/Excel/Caching/XLAlignmentRepository.cs b/ClosedXML/Excel/Caching/XLAlignmentRepository.cs new file mode 100644 index 0000000..1e091ae --- /dev/null +++ b/ClosedXML/Excel/Caching/XLAlignmentRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLAlignmentRepository : XLRepositoryBase + { + #region Constructors + + public XLAlignmentRepository(Func createNew) + : base(createNew) + { + } + + public XLAlignmentRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLBorderRepository.cs b/ClosedXML/Excel/Caching/XLBorderRepository.cs new file mode 100644 index 0000000..2350e61 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLBorderRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLBorderRepository : XLRepositoryBase + { + #region Constructors + + public XLBorderRepository(Func createNew) + : base(createNew) + { + } + + public XLBorderRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLColorRepository.cs b/ClosedXML/Excel/Caching/XLColorRepository.cs new file mode 100644 index 0000000..d6d671b --- /dev/null +++ b/ClosedXML/Excel/Caching/XLColorRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLColorRepository : XLRepositoryBase + { + #region Constructors + + public XLColorRepository(Func createNew) + : base(createNew) + { + } + + public XLColorRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLFillRepository.cs b/ClosedXML/Excel/Caching/XLFillRepository.cs new file mode 100644 index 0000000..1dbb79a --- /dev/null +++ b/ClosedXML/Excel/Caching/XLFillRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLFillRepository : XLRepositoryBase + { + #region Constructors + + public XLFillRepository(Func createNew) + : base(createNew) + { + } + + public XLFillRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLFontRepository.cs b/ClosedXML/Excel/Caching/XLFontRepository.cs new file mode 100644 index 0000000..ba619ef --- /dev/null +++ b/ClosedXML/Excel/Caching/XLFontRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLFontRepository : XLRepositoryBase + { + #region Constructors + + public XLFontRepository(Func createNew) + : base(createNew) + { + } + + public XLFontRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLNumberFormatRepository.cs b/ClosedXML/Excel/Caching/XLNumberFormatRepository.cs new file mode 100644 index 0000000..80e9d10 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLNumberFormatRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLNumberFormatRepository : XLRepositoryBase + { + #region Constructors + + public XLNumberFormatRepository(Func createNew) + : base(createNew) + { + } + + public XLNumberFormatRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLProtectionRepository.cs b/ClosedXML/Excel/Caching/XLProtectionRepository.cs new file mode 100644 index 0000000..44e7de3 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLProtectionRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLProtectionRepository : XLRepositoryBase + { + #region Constructors + + public XLProtectionRepository(Func createNew) + : base(createNew) + { + } + + public XLProtectionRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLRangeRepository.cs b/ClosedXML/Excel/Caching/XLRangeRepository.cs new file mode 100644 index 0000000..173322d --- /dev/null +++ b/ClosedXML/Excel/Caching/XLRangeRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal class XLRangeRepository : XLWorkbookElementRepositoryBase + { + public XLRangeRepository(XLWorkbook workbook, Func createNew) : base(workbook, createNew) + { + } + + public XLRangeRepository(XLWorkbook workbook, Func createNew, IEqualityComparer сomparer) : base(workbook, createNew, сomparer) + { + } + } +} diff --git a/ClosedXML/Excel/Caching/XLRepositoryBase.cs b/ClosedXML/Excel/Caching/XLRepositoryBase.cs new file mode 100644 index 0000000..347bf36 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLRepositoryBase.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Collections; + +namespace ClosedXML.Excel.Caching +{ + internal abstract class XLRepositoryBase : IXLRepository + { + public abstract void Clear(); + } + + internal abstract class XLRepositoryBase : XLRepositoryBase, IXLRepository + where Tkey : struct, IEquatable + where Tvalue : class + { + const int CONCURRENCY_LEVEL = 4; + const int INITIAL_CAPACITY = 1000; + + private readonly ConcurrentDictionary _storage; + private readonly Func _createNew; + + protected XLRepositoryBase(Func createNew) + : this(createNew, EqualityComparer.Default) + { + } + + protected XLRepositoryBase(Func createNew, IEqualityComparer comparer) + { + _storage = new ConcurrentDictionary(CONCURRENCY_LEVEL, INITIAL_CAPACITY, comparer); + _createNew = createNew; + } + + /// + /// Check if the specified key is presented in the repository. + /// + /// Key to look for. + /// Value from the repository stored under specified key or null if key does + /// not exist or the entry under this key has already bee GCed. + /// True if entry exists and alive, false otherwise. + public bool ContainsKey(ref Tkey key, out Tvalue value) + { + if (_storage.TryGetValue(key, out WeakReference cachedReference)) + { + value = cachedReference.Target as Tvalue; + return value != null; + } + value = null; + return false; + } + + /// + /// Put the entity into the repository under the specified key if no other entity with + /// the same key is presented. + /// + /// Key to identify the entity. + /// Entity to store. + /// Entity that is stored in the repository under the specified key + /// (it can be either the or another entity that has been added to + /// the repository before.) + public Tvalue Store(ref Tkey key, Tvalue value) + { + if (value == null) + return null; + + do + { + if (_storage.TryGetValue(key, out WeakReference cachedReference) && + cachedReference.Target is Tvalue storedValue) + { + return storedValue; + } + } while (!_storage.TryAdd(key, new WeakReference(value))); + + return value; + } + + public Tvalue GetOrCreate(ref Tkey key) + { + if (_storage.TryGetValue(key, out WeakReference cachedReference) && + cachedReference.Target is Tvalue storedValue) + { + return storedValue; + } + + _storage.TryRemove(key, out WeakReference _); + var value = _createNew(key); + return Store(ref key, value); + } + + public Tvalue Replace(ref Tkey oldKey, ref Tkey newKey) + { + if (_storage.TryRemove(oldKey, out WeakReference cachedReference) && cachedReference != null) + { + _storage.TryAdd(newKey, cachedReference); + return GetOrCreate(ref newKey); + } + + return null; + } + + public void Remove(ref Tkey key) + { + _storage.TryRemove(key, out WeakReference _); + } + + public override void Clear() + { + _storage.Clear(); + } + + /// + /// Enumerate items in repository removing "dead" entries. + /// + public IEnumerator GetEnumerator() + { + return _storage + .Select(pair => + { + var val = pair.Value.Target as Tvalue; + if (val == null) + { + _storage.TryRemove(pair.Key, out WeakReference _); + } + return val; + }) + .Where(val => val != null) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/ClosedXML/Excel/Caching/XLStyleRepository.cs b/ClosedXML/Excel/Caching/XLStyleRepository.cs new file mode 100644 index 0000000..d5105d4 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLStyleRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + internal sealed class XLStyleRepository : XLRepositoryBase + { + #region Constructors + + public XLStyleRepository(Func createNew) + : base(createNew) + { + } + + public XLStyleRepository(Func createNew, IEqualityComparer comparer) + : base(createNew, comparer) + { + } + + #endregion Constructors + } +} diff --git a/ClosedXML/Excel/Caching/XLWorkbookElementRepositoryBase.cs b/ClosedXML/Excel/Caching/XLWorkbookElementRepositoryBase.cs new file mode 100644 index 0000000..a001f12 --- /dev/null +++ b/ClosedXML/Excel/Caching/XLWorkbookElementRepositoryBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Caching +{ + /// + /// Base repository for elements. + /// + internal abstract class XLWorkbookElementRepositoryBase : XLRepositoryBase + where Tkey : struct, IEquatable + where Tvalue : class + { + public XLWorkbook Workbook { get; private set; } + + public XLWorkbookElementRepositoryBase(XLWorkbook workbook, Func createNew) + : this(workbook, createNew, EqualityComparer.Default) + { + } + + public XLWorkbookElementRepositoryBase(XLWorkbook workbook, Func createNew, IEqualityComparer сomparer) + : base(createNew, сomparer) + { + Workbook = workbook; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/CalcEngine.cs b/ClosedXML/Excel/CalcEngine/CalcEngine.cs new file mode 100644 index 0000000..75424ff --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/CalcEngine.cs @@ -0,0 +1,844 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using ClosedXML.Excel.CalcEngine.Functions; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// CalcEngine parses strings and returns Expression objects that can + /// be evaluated. + /// + /// + /// This class has three extensibility points: + /// Use the DataContext property to add an object's properties to the engine scope. + /// Use the RegisterFunction method to define custom functions. + /// Override the GetExternalObject method to add arbitrary variables to the engine scope. + /// + internal class CalcEngine + { + private const string defaultFunctionNameSpace = "_xlfn"; + + //--------------------------------------------------------------------------- + + #region ** fields + + // members + private string _expr; // expression being parsed + + private int _len; // length of the expression being parsed + private int _ptr; // current pointer into expression + private char[] _idChars; // valid characters in identifiers (besides alpha and digits) + private Token _token; // current token being parsed + private Dictionary _tkTbl; // table with tokens (+, -, etc) + private Dictionary _fnTbl; // table with constants and functions (pi, sin, etc) + private Dictionary _vars; // table with variables + private object _dataContext; // object with properties + private bool _optimize; // optimize expressions when parsing + protected ExpressionCache _cache; // cache with parsed expressions + private CultureInfo _ci; // culture info used to parse numbers/dates + private char _decimal, _listSep, _percent; // localized decimal separator, list separator, percent sign + + #endregion ** fields + + //--------------------------------------------------------------------------- + + #region ** ctor + + public CalcEngine() + { + CultureInfo = CultureInfo.InvariantCulture; + _tkTbl = GetSymbolTable(); + _fnTbl = GetFunctionTable(); + _vars = new Dictionary(StringComparer.OrdinalIgnoreCase); + _cache = new ExpressionCache(this); + _optimize = true; +#if DEBUG + //this.Test(); +#endif + } + + #endregion ** ctor + + //--------------------------------------------------------------------------- + + #region ** object model + + /// + /// Parses a string into an . + /// + /// String to parse. + /// An object that can be evaluated. + public Expression Parse(string expression) + { + // initialize + _expr = expression; + _len = _expr.Length; + _ptr = 0; + + // skip leading equals sign + if (_len > 0 && _expr[0] == '=') + _ptr++; + + // skip leading +'s + while (_len > _ptr && _expr[_ptr] == '+') + _ptr++; + + // parse the expression + var expr = ParseExpression(); + + // check for errors + if (_token.ID == TKID.OPEN) + Throw("Unknown function: " + expr.LastParseItem); + else if (_token.ID != TKID.END) + Throw("Expected end of expression"); + + // optimize expression + if (_optimize) + { + expr = expr.Optimize(); + } + + // done + return expr; + } + + /// + /// Evaluates a string. + /// + /// Expression to evaluate. + /// The value of the expression. + /// + /// If you are going to evaluate the same expression several times, + /// it is more efficient to parse it only once using the + /// method and then using the Expression.Evaluate method to evaluate + /// the parsed expression. + /// + public object Evaluate(string expression) + { + var x = _cache != null + ? _cache[expression] + : Parse(expression); + return x.Evaluate(); + } + + /// + /// Gets or sets whether the calc engine should keep a cache with parsed + /// expressions. + /// + public bool CacheExpressions + { + get { return _cache != null; } + set + { + if (value != CacheExpressions) + { + _cache = value + ? new ExpressionCache(this) + : null; + } + } + } + + /// + /// Gets or sets whether the calc engine should optimize expressions when + /// they are parsed. + /// + public bool OptimizeExpressions + { + get { return _optimize; } + set { _optimize = value; } + } + + /// + /// Gets or sets a string that specifies special characters that are valid for identifiers. + /// + /// + /// Identifiers must start with a letter or an underscore, which may be followed by + /// additional letters, underscores, or digits. This string allows you to specify + /// additional valid characters such as ':' or '!' (used in Excel range references + /// for example). + /// + public char[] IdentifierChars + { + get { return _idChars; } + set { _idChars = value; } + } + + /// + /// Registers a function that can be evaluated by this . + /// + /// Function name. + /// Minimum parameter count. + /// Maximum parameter count. + /// Delegate that evaluates the function. + public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn) + { + _fnTbl.Add(functionName, new FunctionDefinition(parmMin, parmMax, fn)); + } + + /// + /// Registers a function that can be evaluated by this . + /// + /// Function name. + /// Parameter count. + /// Delegate that evaluates the function. + public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn) + { + RegisterFunction(functionName, parmCount, parmCount, fn); + } + + /// + /// Gets an external object based on an identifier. + /// + /// + /// This method is useful when the engine needs to create objects dynamically. + /// For example, a spreadsheet calc engine would use this method to dynamically create cell + /// range objects based on identifiers that cannot be enumerated at design time + /// (such as "AB12", "A1:AB12", etc.) + /// + public virtual object GetExternalObject(string identifier) + { + return null; + } + + /// + /// Gets or sets the DataContext for this . + /// + /// + /// Once a DataContext is set, all public properties of the object become available + /// to the CalcEngine, including sub-properties such as "Address.Street". These may + /// be used with expressions just like any other constant. + /// + public virtual object DataContext + { + get { return _dataContext; } + set { _dataContext = value; } + } + + /// + /// Gets the dictionary that contains function definitions. + /// + public Dictionary Functions + { + get { return _fnTbl; } + } + + /// + /// Gets the dictionary that contains simple variables (not in the DataContext). + /// + public Dictionary Variables + { + get { return _vars; } + } + + /// + /// Gets or sets the to use when parsing numbers and dates. + /// + public CultureInfo CultureInfo + { + get { return _ci; } + set + { + _ci = value; + var nf = _ci.NumberFormat; + _decimal = nf.NumberDecimalSeparator[0]; + _percent = nf.PercentSymbol[0]; + _listSep = _ci.TextInfo.ListSeparator[0]; + } + } + + #endregion ** object model + + //--------------------------------------------------------------------------- + + #region ** token/keyword tables + + private static readonly IDictionary ErrorMap = new Dictionary() + { + ["#REF!"] = ErrorExpression.ExpressionErrorType.CellReference, + ["#VALUE!"] = ErrorExpression.ExpressionErrorType.CellValue, + ["#DIV/0!"] = ErrorExpression.ExpressionErrorType.DivisionByZero, + ["#NAME?"] = ErrorExpression.ExpressionErrorType.NameNotRecognized, + ["#N/A"] = ErrorExpression.ExpressionErrorType.NoValueAvailable, + ["#NULL!"] = ErrorExpression.ExpressionErrorType.NullValue, + ["#NUM!"] = ErrorExpression.ExpressionErrorType.NumberInvalid + }; + + // build/get static token table + private Dictionary GetSymbolTable() + { + if (_tkTbl == null) + { + _tkTbl = new Dictionary(); + AddToken('&', TKID.CONCAT, TKTYPE.ADDSUB); + AddToken('+', TKID.ADD, TKTYPE.ADDSUB); + AddToken('-', TKID.SUB, TKTYPE.ADDSUB); + AddToken('(', TKID.OPEN, TKTYPE.GROUP); + AddToken(')', TKID.CLOSE, TKTYPE.GROUP); + AddToken('*', TKID.MUL, TKTYPE.MULDIV); + AddToken('.', TKID.PERIOD, TKTYPE.GROUP); + AddToken('/', TKID.DIV, TKTYPE.MULDIV); + AddToken('\\', TKID.DIVINT, TKTYPE.MULDIV); + AddToken('%', TKID.DIV100, TKTYPE.MULDIV_UNARY); + AddToken('=', TKID.EQ, TKTYPE.COMPARE); + AddToken('>', TKID.GT, TKTYPE.COMPARE); + AddToken('<', TKID.LT, TKTYPE.COMPARE); + AddToken('^', TKID.POWER, TKTYPE.POWER); + AddToken("<>", TKID.NE, TKTYPE.COMPARE); + AddToken(">=", TKID.GE, TKTYPE.COMPARE); + AddToken("<=", TKID.LE, TKTYPE.COMPARE); + + // list separator is localized, not necessarily a comma + // so it can't be on the static table + //AddToken(',', TKID.COMMA, TKTYPE.GROUP); + } + return _tkTbl; + } + + private void AddToken(object symbol, TKID id, TKTYPE type) + { + var token = new Token(symbol, id, type); + _tkTbl.Add(symbol, token); + } + + // build/get static keyword table + private Dictionary GetFunctionTable() + { + if (_fnTbl == null) + { + // create table + _fnTbl = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // register built-in functions (and constants) + Engineering.Register(this); + Information.Register(this); + Logical.Register(this); + Lookup.Register(this); + MathTrig.Register(this); + Text.Register(this); + Statistical.Register(this); + DateAndTime.Register(this); + Financial.Register(this); + } + return _fnTbl; + } + + #endregion ** token/keyword tables + + //--------------------------------------------------------------------------- + + #region ** private stuff + + private Expression ParseExpression() + { + GetToken(); + return ParseCompare(); + } + + private Expression ParseCompare() + { + var x = ParseAddSub(); + while (_token.Type == TKTYPE.COMPARE) + { + var t = _token; + GetToken(); + var exprArg = ParseAddSub(); + x = new BinaryExpression(t, x, exprArg); + } + return x; + } + + private Expression ParseAddSub() + { + var x = ParseMulDiv(); + while (_token.Type == TKTYPE.ADDSUB) + { + var t = _token; + GetToken(); + var exprArg = ParseMulDiv(); + x = new BinaryExpression(t, x, exprArg); + } + return x; + } + + private Expression ParseMulDiv() + { + var x = ParsePower(); + while (_token.Type == TKTYPE.MULDIV) + { + var t = _token; + GetToken(); + var a = ParsePower(); + x = new BinaryExpression(t, x, a); + } + return x; + } + + private Expression ParsePower() + { + var x = ParseMulDivUnary(); + while (_token.Type == TKTYPE.POWER) + { + var t = _token; + GetToken(); + var a = ParseMulDivUnary(); + x = new BinaryExpression(t, x, a); + } + return x; + } + + private Expression ParseMulDivUnary() + { + var x = ParseUnary(); + while (_token.Type == TKTYPE.MULDIV_UNARY) + { + var t = _tkTbl['/']; + var a = new Expression(100); + x = new BinaryExpression(t, x, a); + GetToken(); + } + return x; + } + + private Expression ParseUnary() + { + // unary plus and minus + if (_token.Type == TKTYPE.ADDSUB) + { + var sign = 1; + do + { + if (_token.ID == TKID.SUB) + sign = -sign; + GetToken(); + } while (_token.Type == TKTYPE.ADDSUB); + var a = ParseAtom(); + var t = (sign == 1) + ? _tkTbl['+'] + : _tkTbl['-']; + return new UnaryExpression(t, a); + } + + // not unary, return atom + return ParseAtom(); + } + + private Expression ParseAtom() + { + string id; + Expression x = null; + + switch (_token.Type) + { + // literals + case TKTYPE.LITERAL: + x = new Expression(_token); + break; + + // identifiers + case TKTYPE.IDENTIFIER: + + // get identifier + id = (string)_token.Value; + FunctionDefinition functionDefinition; + + var foundFunction = _fnTbl.TryGetValue(id, out functionDefinition); + if (!foundFunction && id.StartsWith($"{defaultFunctionNameSpace}.")) + foundFunction = _fnTbl.TryGetValue(id.Substring(defaultFunctionNameSpace.Length + 1), out functionDefinition); + + // look for functions + if (foundFunction) + { + var p = GetParameters(); + var pCnt = p == null ? 0 : p.Count; + if (functionDefinition.ParmMin != -1 && pCnt < functionDefinition.ParmMin) + { + Throw(string.Format("Too few parameters for function '{0}'. Expected a minimum of {1} and a maximum of {2}.", id, functionDefinition.ParmMin, functionDefinition.ParmMax)); + } + if (functionDefinition.ParmMax != -1 && pCnt > functionDefinition.ParmMax) + { + Throw(string.Format("Too many parameters for function '{0}'.Expected a minimum of {1} and a maximum of {2}.", id, functionDefinition.ParmMin, functionDefinition.ParmMax)); + } + x = new FunctionExpression(functionDefinition, p); + break; + } + + // look for simple variables (much faster than binding!) + if (_vars.ContainsKey(id)) + { + x = new VariableExpression(_vars, id); + break; + } + + // look for external objects + var xObj = GetExternalObject(id); + if (xObj == null) + throw new NameNotRecognizedException($"The identifier `{id}` was not recognised."); + + x = new XObjectExpression(xObj); + break; + + // sub-expressions + case TKTYPE.GROUP: + + // Normally anything other than opening parenthesis is illegal here + // but Excel allows omitted parameters so return empty value expression. + if (_token.ID != TKID.OPEN) + { + return new EmptyValueExpression(); + } + + // get expression + GetToken(); + x = ParseCompare(); + + // check that the parenthesis was closed + if (_token.ID != TKID.CLOSE) + { + Throw("Unbalanced parenthesis."); + } + + break; + + case TKTYPE.ERROR: + x = new ErrorExpression((ErrorExpression.ExpressionErrorType)_token.Value); + break; + } + + // make sure we got something... + if (x == null) + { + Throw(); + } + + // done + GetToken(); + return x; + } + + #endregion ** private stuff + + //--------------------------------------------------------------------------- + + #region ** parser + + private static IDictionary matchingClosingSymbols = new Dictionary() + { + { '\'', '\'' }, + { '[', ']' } + }; + + private void GetToken() + { + // eat white space + while (_ptr < _len && _expr[_ptr] <= ' ') + { + _ptr++; + } + + // are we done? + if (_ptr >= _len) + { + _token = new Token(null, TKID.END, TKTYPE.GROUP); + return; + } + + // prepare to parse + int i; + var c = _expr[_ptr]; + + // operators + // this gets called a lot, so it's pretty optimized. + // note that operators must start with non-letter/digit characters. + var isLetter = char.IsLetter(c); + var isDigit = char.IsDigit(c); + + var isEnclosed = matchingClosingSymbols.TryGetValue(c, out char matchingClosingSymbol); + + if (!isLetter && !isDigit && !isEnclosed) + { + // if this is a number starting with a decimal, don't parse as operator + var nxt = _ptr + 1 < _len ? _expr[_ptr + 1] : '0'; + bool isNumber = c == _decimal && char.IsDigit(nxt); + if (!isNumber) + { + // look up localized list separator + if (c == _listSep) + { + _token = new Token(c, TKID.COMMA, TKTYPE.GROUP); + _ptr++; + return; + } + + // look up single-char tokens on table + if (_tkTbl.TryGetValue(c, out Token tk)) + { + // save token we found + _token = tk; + _ptr++; + + // look for double-char tokens (special case) + if (_ptr < _len + && (c == '>' || c == '<') + && _tkTbl.TryGetValue(_expr.Substring(_ptr - 1, 2), out tk)) + { + _token = tk; + _ptr++; + } + + // found token on the table + return; + } + } + } + + // parse numbers + if (isDigit || c == _decimal) + { + var sci = false; + var div = -1.0; // use double, not int (this may get really big) + var val = 0.0; + for (i = 0; i + _ptr < _len; i++) + { + c = _expr[_ptr + i]; + + // digits always OK + if (char.IsDigit(c)) + { + val = val * 10 + (c - '0'); + if (div > -1) + { + div *= 10; + } + continue; + } + + // one decimal is OK + if (c == _decimal && div < 0) + { + div = 1; + continue; + } + + // scientific notation? + if ((c == 'E' || c == 'e') && !sci) + { + sci = true; + c = _expr[_ptr + i + 1]; + if (c == '+' || c == '-') i++; + continue; + } + + // end of literal + break; + } + + // end of number, get value + if (!sci) + { + // much faster than ParseDouble + if (div > 1) + { + val /= div; + } + } + else + { + var lit = _expr.Substring(_ptr, i); + val = ParseDouble(lit, _ci); + } + + if (c != ':') + { + // build token + _token = new Token(val, TKID.ATOM, TKTYPE.LITERAL); + + // advance pointer and return + _ptr += i; + return; + } + } + + // parse strings + if (c == '\"') + { + // look for end quote, skip double quotes + for (i = 1; i + _ptr < _len; i++) + { + c = _expr[_ptr + i]; + if (c != '\"') continue; + char cNext = i + _ptr < _len - 1 ? _expr[_ptr + i + 1] : ' '; + if (cNext != '\"') break; + i++; + } + + // check that we got the end of the string + if (c != '\"') + { + Throw("Can't find final quote."); + } + + // end of string + var lit = _expr.Substring(_ptr + 1, i - 1); + _ptr += i + 1; + _token = new Token(lit.Replace("\"\"", "\""), TKID.ATOM, TKTYPE.LITERAL); + return; + } + + // parse #REF! (and other errors) in formula + if (c == '#' && ErrorMap.Any(pair => _len > _ptr + pair.Key.Length && _expr.Substring(_ptr, pair.Key.Length).Equals(pair.Key, StringComparison.OrdinalIgnoreCase))) + { + var errorPair = ErrorMap.Single(pair => _len > _ptr + pair.Key.Length && _expr.Substring(_ptr, pair.Key.Length).Equals(pair.Key, StringComparison.OrdinalIgnoreCase)); + _ptr += errorPair.Key.Length; + _token = new Token(errorPair.Value, TKID.ATOM, TKTYPE.ERROR); + return; + } + + // identifiers (functions, objects) must start with alpha or underscore + if (!isEnclosed && !isLetter && c != '_' && (_idChars == null || !_idChars.Contains(c))) + { + Throw("Identifier expected."); + } + + // and must contain only letters/digits/_idChars + for (i = 1; i + _ptr < _len; i++) + { + c = _expr[_ptr + i]; + isLetter = char.IsLetter(c); + isDigit = char.IsDigit(c); + + if (isEnclosed && c == matchingClosingSymbol) + { + isEnclosed = false; + matchingClosingSymbol = '\0'; + + i++; + c = _expr[_ptr + i]; + isLetter = char.IsLetter(c); + isDigit = char.IsDigit(c); + } + + var disallowedSymbols = new List() { '\\', '/', '*', '[', ':', '?' }; + if (isEnclosed && disallowedSymbols.Contains(c)) + break; + + var allowedSymbols = new List() { '_', '.' }; + + if (!isLetter && !isDigit + && !(isEnclosed || allowedSymbols.Contains(c)) + && (_idChars == null || !_idChars.Contains(c))) + break; + } + + // got identifier + var id = _expr.Substring(_ptr, i); + _ptr += i; + _token = new Token(id, TKID.ATOM, TKTYPE.IDENTIFIER); + } + + private static double ParseDouble(string str, CultureInfo ci) + { + if (str.Length > 0 && str[str.Length - 1] == ci.NumberFormat.PercentSymbol[0]) + { + str = str.Substring(0, str.Length - 1); + return double.Parse(str, NumberStyles.Any, ci) / 100.0; + } + return double.Parse(str, NumberStyles.Any, ci); + } + + private List GetParameters() // e.g. myfun(a, b, c+2) + { + // check whether next token is a (, + // restore state and bail if it's not + var pos = _ptr; + var tk = _token; + GetToken(); + if (_token.ID != TKID.OPEN) + { + _ptr = pos; + _token = tk; + return null; + } + + // check for empty Parameter list + pos = _ptr; + GetToken(); + if (_token.ID == TKID.CLOSE) + { + return null; + } + _ptr = pos; + + // get Parameters until we reach the end of the list + var parms = new List(); + var expr = ParseExpression(); + parms.Add(expr); + while (_token.ID == TKID.COMMA) + { + expr = ParseExpression(); + parms.Add(expr); + } + + // make sure the list was closed correctly + if (_token.ID == TKID.OPEN) + Throw("Unknown function: " + expr.LastParseItem); + else if (_token.ID != TKID.CLOSE) + Throw("Syntax error: expected ')'"); + + // done + return parms; + } + + private Token GetMember() + { + // check whether next token is a MEMBER token ('.'), + // restore state and bail if it's not + var pos = _ptr; + var tk = _token; + GetToken(); + if (_token.ID != TKID.PERIOD) + { + _ptr = pos; + _token = tk; + return null; + } + + // skip member token + GetToken(); + if (_token.Type != TKTYPE.IDENTIFIER) + { + Throw("Identifier expected"); + } + return _token; + } + + #endregion ** parser + + //--------------------------------------------------------------------------- + + #region ** static helpers + + private static void Throw() + { + Throw("Syntax error."); + } + + private static void Throw(string msg) + { + throw new ExpressionParseException(msg); + } + + #endregion ** static helpers + } + + /// + /// Delegate that represents CalcEngine functions. + /// + /// List of objects that represent the + /// parameters to be used in the function call. + /// The function result. + internal delegate object CalcEngineFunction(List parms); +} diff --git a/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs b/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs new file mode 100644 index 0000000..4b0f586 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel.CalcEngine +{ + internal static class CalcEngineHelpers + { + private static Lazy>> patternReplacements = + new Lazy>>(() => + { + // key: the literal string to match + // value: a tuple: first item: the search pattern, second item: the replacement + return new Dictionary>() + { + [@"~~"] = new Tuple(@"~~", "~"), + [@"~*"] = new Tuple(@"~\*", @"\*"), + [@"~?"] = new Tuple(@"~\?", @"\?"), + [@"?"] = new Tuple(@"\?", ".?"), + [@"*"] = new Tuple(@"\*", ".*"), + }; + }); + + internal static bool ValueSatisfiesCriteria(object value, object criteria, CalcEngine ce) + { + // safety... + if (value == null) + { + return false; + } + + // Excel treats TRUE and 1 as unequal, but LibreOffice treats them as equal. We follow Excel's convention + if (criteria is Boolean b1) + return (value is Boolean b2) && b1.Equals(b2); + + if (value is Boolean) return false; + + // if criteria is a number, straight comparison + Double cdbl; + if (criteria is Double dbl2) cdbl = dbl2; + else if (criteria is Int32 i) cdbl = i; // results of DATE function can be an integer + else if (criteria is DateTime dt) cdbl = dt.ToOADate(); + else if (criteria is TimeSpan ts) cdbl = ts.TotalDays; + else if (criteria is String cs) + { + if (value is string && (value as string).Trim().Length == 0) + return cs.Length == 0; + + if (cs.Length == 0) + return cs.Equals(value); + + // if criteria is an expression (e.g. ">20"), use calc engine + if ((cs[0] == '=' && cs.IndexOfAny(new[] { '*', '?' }) < 0) + || cs[0] == '<' + || cs[0] == '>') + { + // build expression + var expression = string.Format("{0}{1}", value, cs); + + // add quotes if necessary + var pattern = @"([\w\s]+)(\W+)(\w+)"; + var m = Regex.Match(expression, pattern); + if (m.Groups.Count == 4 + && (!double.TryParse(m.Groups[1].Value, out double d) || + !double.TryParse(m.Groups[3].Value, out d))) + { + expression = string.Format("\"{0}\"{1}\"{2}\"", + m.Groups[1].Value, + m.Groups[2].Value, + m.Groups[3].Value); + } + + // evaluate + return (bool)ce.Evaluate(expression); + } + + // if criteria is a regular expression, use regex + if (cs.IndexOfAny(new[] { '*', '?' }) > -1) + { + if (cs[0] == '=') cs = cs.Substring(1); + + var pattern = Regex.Replace( + cs, + "(" + String.Join( + "|", + patternReplacements.Value.Values.Select(t => t.Item1)) + + ")", + m => patternReplacements.Value[m.Value].Item2); + pattern = $"^{pattern}$"; + + return Regex.IsMatch(value.ToString(), pattern, RegexOptions.IgnoreCase); + } + + // straight string comparison + if (value is string vs) + return vs.Equals(cs, StringComparison.OrdinalIgnoreCase); + else + return string.Equals(value.ToString(), cs, StringComparison.OrdinalIgnoreCase); + } + else + throw new NotImplementedException(); + + Double vdbl; + if (value is Double dbl) vdbl = dbl; + else if (value is Int32 i) vdbl = i; + else if (value is DateTime dt) vdbl = dt.ToOADate(); + else if (value is TimeSpan ts) vdbl = ts.TotalDays; + else if (value is String s) + { + if (!Double.TryParse(s, out vdbl)) return false; + } + else + throw new NotImplementedException(); + + return Math.Abs(vdbl - cdbl) < Double.Epsilon; + } + + internal static bool ValueIsBlank(object value) + { + if (value == null) + return true; + + if (value is string s) + return s.Length == 0; + + return false; + } + + /// + /// Get total count of cells in the specified range without initalizing them all + /// (which might cause serious performance issues on column-wide calculations). + /// + /// Expression referring to the cell range. + /// Total number of cells in the range. + internal static long GetTotalCellsCount(XObjectExpression rangeExpression) + { + var range = (rangeExpression?.Value as CellRangeReference)?.Range; + if (range == null) + return 0; + return (long)(range.LastColumn().ColumnNumber() - range.FirstColumn().ColumnNumber() + 1) * + (long)(range.LastRow().RowNumber() - range.FirstRow().RowNumber() + 1); + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/CellRangeReference.cs b/ClosedXML/Excel/CalcEngine/CellRangeReference.cs new file mode 100644 index 0000000..eadb0d2 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/CellRangeReference.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; + +namespace ClosedXML.Excel.CalcEngine +{ + internal class CellRangeReference : IValueObject, IEnumerable + { + public CellRangeReference(IXLRange range, XLCalcEngine ce) + { + Range = range; + CalcEngine = ce; + } + + internal CalcEngine CalcEngine { get; } + public IXLRange Range { get; } + + // ** IValueObject + public object GetValue() + { + return GetValue(Range.FirstCell()); + } + + // ** IEnumerable + public IEnumerator GetEnumerator() + { + if (Range.Worksheet.IsEmpty(XLCellsUsedOptions.AllContents)) + yield break; + + var lastCellAddress = Range.Worksheet.LastCellUsed().Address; + var maxRow = Math.Min(Range.RangeAddress.LastAddress.RowNumber, lastCellAddress.RowNumber); + var maxColumn = Math.Min(Range.RangeAddress.LastAddress.ColumnNumber, lastCellAddress.ColumnNumber); + + var trimmedRange = (XLRangeBase)Range.Worksheet + .Range( + Range.FirstCell().Address, + new XLAddress(maxRow, maxColumn, fixedRow: false, fixedColumn: false) + ); + + foreach (var c in trimmedRange.CellValues()) + yield return c; + } + + private Boolean _evaluating; + + // ** implementation + private object GetValue(IXLCell cell) + { + if (_evaluating || (cell as XLCell).IsEvaluating) + { + throw new InvalidOperationException($"Circular Reference occured during evaluation. Cell: {cell.Address.ToString(XLReferenceStyle.Default, true)}"); + } + try + { + _evaluating = true; + var f = cell.FormulaA1; + if (String.IsNullOrWhiteSpace(f)) + return cell.Value; + else + { + return (cell as XLCell).Evaluate(); + } + } + finally + { + _evaluating = false; + } + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/CalcEngineException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/CalcEngineException.cs new file mode 100644 index 0000000..1e7f1f7 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/CalcEngineException.cs @@ -0,0 +1,18 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + public abstract class CalcEngineException : ArgumentException + { + protected CalcEngineException() + : base() + { } + protected CalcEngineException(string message) + : base(message) + { } + + protected CalcEngineException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/CellReferenceException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/CellReferenceException.cs new file mode 100644 index 0000000..91e219e --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/CellReferenceException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// This error occurs when you delete a cell referred to in the + /// formula or if you paste cells over the ones referred to in the + /// formula. + /// Corresponds to the #REF! error in Excel + /// + /// + public class CellReferenceException : CalcEngineException + { + internal CellReferenceException() + : base() + { } + + internal CellReferenceException(string message) + : base(message) + { } + + internal CellReferenceException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/CellValueException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/CellValueException.cs new file mode 100644 index 0000000..ad129dc --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/CellValueException.cs @@ -0,0 +1,26 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// This error is most often the result of specifying a + /// mathematical operation with one or more cells that contain + /// text. + /// Corresponds to the #VALUE! error in Excel + /// + /// + public class CellValueException : CalcEngineException + { + internal CellValueException() + : base() + { } + + internal CellValueException(string message) + : base(message) + { } + + internal CellValueException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/DivisionByZeroException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/DivisionByZeroException.cs new file mode 100644 index 0000000..8bc6b7b --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/DivisionByZeroException.cs @@ -0,0 +1,26 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// The division operation in your formula refers to a cell that + /// contains the value 0 or is blank. + /// Corresponds to the #DIV/0! error in Excel + /// + /// + public class DivisionByZeroException : CalcEngineException + { + internal DivisionByZeroException() + : base() + { } + + internal DivisionByZeroException(string message) + : base(message) + { } + + internal DivisionByZeroException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/NameNotRecognizedException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/NameNotRecognizedException.cs new file mode 100644 index 0000000..7a93075 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/NameNotRecognizedException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// This error value appears when you incorrectly type the range + /// name, refer to a deleted range name, or forget to put quotation + /// marks around a text string in a formula. + /// Corresponds to the #NAME? error in Excel + /// + /// + public class NameNotRecognizedException : CalcEngineException + { + internal NameNotRecognizedException() + : base() + { } + + internal NameNotRecognizedException(string message) + : base(message) + { } + + internal NameNotRecognizedException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/NoValueAvailableException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/NoValueAvailableException.cs new file mode 100644 index 0000000..318fb9a --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/NoValueAvailableException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// Technically, this is not an error value but a special value + /// that you can manually enter into a cell to indicate that you + /// don’t yet have a necessary value. + /// Corresponds to the #N/A error in Excel. + /// + /// + public class NoValueAvailableException : CalcEngineException + { + internal NoValueAvailableException() + : base() + { } + + internal NoValueAvailableException(string message) + : base(message) + { } + + internal NoValueAvailableException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/NullValueException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/NullValueException.cs new file mode 100644 index 0000000..5cc74bc --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/NullValueException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// Because a space indicates an intersection, this error will + /// occur if you insert a space instead of a comma(the union operator) + /// between ranges used in function arguments. + /// Corresponds to the #NULL! error in Excel + /// + /// + public class NullValueException : CalcEngineException + { + internal NullValueException() + : base() + { } + + internal NullValueException(string message) + : base(message) + { } + + internal NullValueException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Exceptions/NumberException.cs b/ClosedXML/Excel/CalcEngine/Exceptions/NumberException.cs new file mode 100644 index 0000000..7d248f5 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Exceptions/NumberException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel.CalcEngine.Exceptions +{ + /// + /// This error can be caused by an invalid argument in an Excel + /// function or a formula that produces a number too large or too small + /// to be represented in the worksheet. + /// Corresponds to the #NUM! error in Excel + /// + /// + public class NumberException : CalcEngineException + { + internal NumberException() + : base() + { } + + internal NumberException(string message) + : base(message) + { } + + internal NumberException(string message, Exception innerException) + : base(message, innerException) + { } + + } +} diff --git a/ClosedXML/Excel/CalcEngine/Expression.cs b/ClosedXML/Excel/CalcEngine/Expression.cs new file mode 100644 index 0000000..edb2d14 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Expression.cs @@ -0,0 +1,603 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; + +namespace ClosedXML.Excel.CalcEngine +{ + internal abstract class ExpressionBase + { + public abstract string LastParseItem { get; } + } + + /// + /// Base class that represents parsed expressions. + /// + /// + /// For example: + /// + /// Expression expr = scriptEngine.Parse(strExpression); + /// object val = expr.Evaluate(); + /// + /// + internal class Expression : ExpressionBase, IComparable + { + //--------------------------------------------------------------------------- + + #region ** fields + + internal readonly Token _token; + + #endregion ** fields + + //--------------------------------------------------------------------------- + + #region ** ctors + + internal Expression() + { + _token = new Token(null, TKID.ATOM, TKTYPE.IDENTIFIER); + } + + internal Expression(object value) + { + _token = new Token(value, TKID.ATOM, TKTYPE.LITERAL); + } + + internal Expression(Token tk) + { + _token = tk; + } + + #endregion ** ctors + + //--------------------------------------------------------------------------- + + #region ** object model + + public virtual object Evaluate() + { + if (_token.Type != TKTYPE.LITERAL) + { + throw new ArgumentException("Bad expression."); + } + return _token.Value; + } + + public virtual Expression Optimize() + { + return this; + } + + #endregion ** object model + + //--------------------------------------------------------------------------- + + #region ** implicit converters + + public static implicit operator string(Expression x) + { + if (x is ErrorExpression) + (x as ErrorExpression).ThrowApplicableException(); + + var v = x.Evaluate(); + + if (v == null) + return string.Empty; + + if (v is bool b) + return b.ToString().ToUpper(); + + return v.ToString(); + } + + public static implicit operator double(Expression x) + { + if (x is ErrorExpression) + (x as ErrorExpression).ThrowApplicableException(); + + // evaluate + var v = x.Evaluate(); + + // handle doubles + if (v is double dbl) + { + return dbl; + } + + // handle booleans + if (v is bool b) + { + return b ? 1 : 0; + } + + // handle dates + if (v is DateTime dt) + { + return dt.ToOADate(); + } + + if (v is TimeSpan ts) + { + return ts.TotalDays; + } + + // handle string + if (v is string s && double.TryParse(s, out var doubleValue)) + { + return doubleValue; + } + + // handle nulls + if (v == null || v is string) + { + return 0; + } + + // handle everything else + CultureInfo _ci = Thread.CurrentThread.CurrentCulture; + return (double)Convert.ChangeType(v, typeof(double), _ci); + } + + public static implicit operator bool(Expression x) + { + if (x is ErrorExpression) + (x as ErrorExpression).ThrowApplicableException(); + + // evaluate + var v = x.Evaluate(); + + // handle booleans + if (v is bool b) + { + return b; + } + + // handle nulls + if (v == null) + { + return false; + } + + // handle doubles + if (v is double dbl) + { + return dbl != 0; + } + + // handle everything else + return (double)Convert.ChangeType(v, typeof(double)) != 0; + } + + public static implicit operator DateTime(Expression x) + { + if (x is ErrorExpression) + (x as ErrorExpression).ThrowApplicableException(); + + // evaluate + var v = x.Evaluate(); + + // handle dates + if (v is DateTime dt) + { + return dt; + } + + if (v is TimeSpan ts) + { + return new DateTime().Add(ts); + } + + // handle numbers + if (v.IsNumber()) + { + return DateTime.FromOADate((double)x); + } + + // handle everything else + CultureInfo _ci = Thread.CurrentThread.CurrentCulture; + return (DateTime)Convert.ChangeType(v, typeof(DateTime), _ci); + } + + #endregion ** implicit converters + + //--------------------------------------------------------------------------- + + #region ** IComparable + + public int CompareTo(Expression other) + { + // get both values + var c1 = this.Evaluate() as IComparable; + var c2 = other.Evaluate() as IComparable; + + // handle nulls + if (c1 == null && c2 == null) + { + return 0; + } + if (c2 == null) + { + return -1; + } + if (c1 == null) + { + return +1; + } + + // make sure types are the same + if (c1.GetType() != c2.GetType()) + { + try + { + if (c1 is DateTime) + c2 = ((DateTime)other); + else if (c2 is DateTime) + c1 = ((DateTime)this); + else + c2 = Convert.ChangeType(c2, c1.GetType()) as IComparable; + } + catch (InvalidCastException) { return -1; } + catch (FormatException) { return -1; } + catch (OverflowException) { return -1; } + catch (ArgumentNullException) { return -1; } + } + + // String comparisons should be case insensitive + if (c1 is string s1 && c2 is string s2) + return StringComparer.OrdinalIgnoreCase.Compare(s1, s2); + else + return c1.CompareTo(c2); + } + + #endregion ** IComparable + + //--------------------------------------------------------------------------- + + #region ** ExpressionBase + + public override string LastParseItem + { + get { return _token?.Value?.ToString() ?? "Unknown value"; } + } + + #endregion ** ExpressionBase + } + + /// + /// Unary expression, e.g. +123 + /// + internal class UnaryExpression : Expression + { + // ** ctor + public UnaryExpression(Token tk, Expression expr) : base(tk) + { + Expression = expr; + } + + public Expression Expression { get; private set; } + + // ** object model + override public object Evaluate() + { + switch (_token.ID) + { + case TKID.ADD: + return +(double)Expression; + + case TKID.SUB: + return -(double)Expression; + } + throw new ArgumentException("Bad expression."); + } + + public override Expression Optimize() + { + Expression = Expression.Optimize(); + return Expression._token.Type == TKTYPE.LITERAL + ? new Expression(this.Evaluate()) + : this; + } + + public override string LastParseItem + { + get { return Expression.LastParseItem; } + } + } + + /// + /// Binary expression, e.g. 1+2 + /// + internal class BinaryExpression : Expression + { + // ** ctor + public BinaryExpression(Token tk, Expression exprLeft, Expression exprRight) : base(tk) + { + LeftExpression = exprLeft; + RightExpression = exprRight; + } + + public Expression LeftExpression { get; private set; } + public Expression RightExpression { get; private set; } + + // ** object model + override public object Evaluate() + { + // handle comparisons + if (_token.Type == TKTYPE.COMPARE) + { + var cmp = LeftExpression.CompareTo(RightExpression); + switch (_token.ID) + { + case TKID.GT: return cmp > 0; + case TKID.LT: return cmp < 0; + case TKID.GE: return cmp >= 0; + case TKID.LE: return cmp <= 0; + case TKID.EQ: return cmp == 0; + case TKID.NE: return cmp != 0; + } + } + + // handle everything else + switch (_token.ID) + { + case TKID.CONCAT: + return (string)LeftExpression + (string)RightExpression; + + case TKID.ADD: + return (double)LeftExpression + (double)RightExpression; + + case TKID.SUB: + return (double)LeftExpression - (double)RightExpression; + + case TKID.MUL: + return (double)LeftExpression * (double)RightExpression; + + case TKID.DIV: + if (Math.Abs((double)RightExpression) < double.Epsilon) + throw new DivisionByZeroException(); + + return (double)LeftExpression / (double)RightExpression; + + case TKID.DIVINT: + if (Math.Abs((double)RightExpression) < double.Epsilon) + throw new DivisionByZeroException(); + + return (double)(int)((double)LeftExpression / (double)RightExpression); + + case TKID.MOD: + if (Math.Abs((double)RightExpression) < double.Epsilon) + throw new DivisionByZeroException(); + + return (double)(int)((double)LeftExpression % (double)RightExpression); + + case TKID.POWER: + var a = (double)LeftExpression; + var b = (double)RightExpression; + if (b == 0.0) return 1.0; + if (b == 0.5) return Math.Sqrt(a); + if (b == 1.0) return a; + if (b == 2.0) return a * a; + if (b == 3.0) return a * a * a; + if (b == 4.0) return a * a * a * a; + return Math.Pow((double)LeftExpression, (double)RightExpression); + } + throw new ArgumentException("Bad expression."); + } + + public override Expression Optimize() + { + LeftExpression = LeftExpression.Optimize(); + RightExpression = RightExpression.Optimize(); + return LeftExpression._token.Type == TKTYPE.LITERAL && RightExpression._token.Type == TKTYPE.LITERAL + ? new Expression(this.Evaluate()) + : this; + } + + public override string LastParseItem + { + get { return RightExpression.LastParseItem; } + } + } + + /// + /// Function call expression, e.g. sin(0.5) + /// + internal class FunctionExpression : Expression + { + // ** ctor + internal FunctionExpression() + { } + + public FunctionExpression(FunctionDefinition function, List parms) + { + FunctionDefinition = function; + Parameters = parms; + } + + // ** object model + override public object Evaluate() + { + return FunctionDefinition.Function(Parameters); + } + + public FunctionDefinition FunctionDefinition { get; } + public List Parameters { get; } + + public override Expression Optimize() + { + bool allLits = true; + if (Parameters != null) + { + for (int i = 0; i < Parameters.Count; i++) + { + var p = Parameters[i].Optimize(); + Parameters[i] = p; + if (p._token.Type != TKTYPE.LITERAL) + { + allLits = false; + } + } + } + return allLits + ? new Expression(this.Evaluate()) + : this; + } + + public override string LastParseItem + { + get { return Parameters.Last().LastParseItem; } + } + } + + /// + /// Simple variable reference. + /// + internal class VariableExpression : Expression + { + private readonly Dictionary _dct; + private readonly string _name; + + public VariableExpression(Dictionary dct, string name) + { + _dct = dct; + _name = name; + } + + public override object Evaluate() + { + return _dct[_name]; + } + + public override string LastParseItem + { + get { return _name; } + } + } + + /// + /// Expression that represents an external object. + /// + internal class XObjectExpression : Expression, IEnumerable + { + private readonly object _value; + + // ** ctor + internal XObjectExpression(object value) + { + _value = value; + } + + public object Value { get { return _value; } } + + // ** object model + public override object Evaluate() + { + // use IValueObject if available + var iv = _value as IValueObject; + if (iv != null) + { + return iv.GetValue(); + } + + // return raw object + return _value; + } + + public IEnumerator GetEnumerator() + { + if (_value is string s) + { + yield return s; + } + else if (_value is IEnumerable ie) + { + foreach (var o in ie) + yield return o; + } + else + { + yield return _value; + } + } + + public override string LastParseItem + { + get { return Value.ToString(); } + } + } + + /// + /// Expression that represents an omitted parameter. + /// + internal class EmptyValueExpression : Expression + { + internal EmptyValueExpression() + { + } + + public override string LastParseItem + { + get { return ""; } + } + } + + internal class ErrorExpression : Expression + { + internal enum ExpressionErrorType + { + CellReference, + CellValue, + DivisionByZero, + NameNotRecognized, + NoValueAvailable, + NullValue, + NumberInvalid + } + + internal ErrorExpression(ExpressionErrorType eet) + : base(new Token(eet, TKID.ATOM, TKTYPE.ERROR)) + { } + + public override object Evaluate() + { + return this._token.Value; + } + + public void ThrowApplicableException() + { + var eet = (ExpressionErrorType)_token.Value; + switch (eet) + { + // TODO: include last token in exception message + case ExpressionErrorType.CellReference: + throw new CellReferenceException(); + case ExpressionErrorType.CellValue: + throw new CellValueException(); + case ExpressionErrorType.DivisionByZero: + throw new DivisionByZeroException(); + case ExpressionErrorType.NameNotRecognized: + throw new NameNotRecognizedException(); + case ExpressionErrorType.NoValueAvailable: + throw new NoValueAvailableException(); + case ExpressionErrorType.NullValue: + throw new NullValueException(); + case ExpressionErrorType.NumberInvalid: + throw new NumberException(); + } + } + } + + /// + /// Interface supported by external objects that have to return a value + /// other than themselves (e.g. a cell range object should return the + /// cell content instead of the range itself). + /// + public interface IValueObject + { + object GetValue(); + } +} diff --git a/ClosedXML/Excel/CalcEngine/ExpressionCache.cs b/ClosedXML/Excel/CalcEngine/ExpressionCache.cs new file mode 100644 index 0000000..b688271 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/ExpressionCache.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Caches expressions based on their string representation. + /// This saves parsing time. + /// + /// + /// Uses weak references to avoid accumulating unused expressions. + /// + class ExpressionCache + { + Dictionary _dct; + CalcEngine _ce; + int _hitCount; + + public ExpressionCache(CalcEngine ce) + { + _ce = ce; + _dct = new Dictionary(); + } + + // gets the parsed version of a string expression + public Expression this[string expression] + { + get + { + Expression x; + if (_dct.TryGetValue(expression, out WeakReference wr) && wr.IsAlive) + { + x = wr.Target as Expression; + } + else + { + // remove all dead references from dictionary + if (wr != null && _dct.Count > 100 && _hitCount++ > 100) + { + RemoveDeadReferences(); + _hitCount = 0; + } + + // store this expression + x = _ce.Parse(expression); + _dct[expression] = new WeakReference(x); + } + return x; + } + } + + // remove all dead references from the cache + void RemoveDeadReferences() + { + for (bool done = false; !done; ) + { + done = true; + foreach (var k in _dct.Keys) + { + if (!_dct[k].IsAlive) + { + _dct.Remove(k); + done = false; + break; + } + } + } + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/ExpressionParseException.cs b/ClosedXML/Excel/CalcEngine/ExpressionParseException.cs new file mode 100644 index 0000000..2de2293 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/ExpressionParseException.cs @@ -0,0 +1,21 @@ +using System; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// The exception that is thrown when the strings to be parsed to an expression is invalid. + /// + public class ExpressionParseException : Exception + { + /// + /// Initializes a new instance of the ExpressionParseException class with a + /// specified error message. + /// + /// The message that describes the error. + public ExpressionParseException(string message) + : base(message) + { + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs b/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs new file mode 100644 index 0000000..5f4a55c --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Function definition class (keeps function name, parameter counts, and delegate). + /// + internal class FunctionDefinition + { + // ** fields + public int ParmMin, ParmMax; + public CalcEngineFunction Function; + + // ** ctor + public FunctionDefinition(int parmMin, int parmMax, CalcEngineFunction function) + { + ParmMin = parmMin; + ParmMax = parmMax; + Function = function; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Database.cs b/ClosedXML/Excel/CalcEngine/Functions/Database.cs new file mode 100644 index 0000000..1c25747 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Database.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class Database + { + public static void Register(CalcEngine ce) + { + //ce.RegisterFunction("DAVERAGE", 3, Daverage); // Returns the average of selected database entries + //ce.RegisterFunction("DCOUNT", 1, Dcount); // Counts the cells that contain numbers in a database + //ce.RegisterFunction("DCOUNTA", 1, Dcounta); // Counts nonblank cells in a database + //ce.RegisterFunction("DGET", 1, Dget); // Extracts from a database a single record that matches the specified criteria + //ce.RegisterFunction("DMAX", 1, Dmax); // Returns the maximum value from selected database entries + //ce.RegisterFunction("DMIN", 1, Dmin); // Returns the minimum value from selected database entries + //ce.RegisterFunction("DPRODUCT", 1, Dproduct); // Multiplies the values in a particular field of records that match the criteria in a database + //ce.RegisterFunction("DSTDEV", 1, Dstdev); // Estimates the standard deviation based on a sample of selected database entries + //ce.RegisterFunction("DSTDEVP", 1, Dstdevp); // Calculates the standard deviation based on the entire population of selected database entries + //ce.RegisterFunction("DSUM", 1, Dsum); // Adds the numbers in the field column of records in the database that match the criteria + //ce.RegisterFunction("DVAR", 1, Dvar); // Estimates variance based on a sample from selected database entries + //ce.RegisterFunction("DVARP", 1, Dvarp); // Calculates variance based on the entire population of selected database entries + } + + static object Daverage(List p) + { + var b = true; + foreach (var v in p) + { + b = b && (bool)v; + } + return b; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/DateAndTime.cs b/ClosedXML/Excel/CalcEngine/Functions/DateAndTime.cs new file mode 100644 index 0000000..2e11991 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/DateAndTime.cs @@ -0,0 +1,408 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class DateAndTime + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("DATE", 3, Date); // Returns the serial number of a particular date + ce.RegisterFunction("DATEDIF", 3, Datedif); // Calculates the number of days, months, or years between two dates + ce.RegisterFunction("DATEVALUE", 1, Datevalue); // Converts a date in the form of text to a serial number + ce.RegisterFunction("DAY", 1, Day); // Converts a serial number to a day of the month + ce.RegisterFunction("DAYS", 2, Days); // Returns the number of days between two dates. + ce.RegisterFunction("DAYS360", 2, 3, Days360); // Calculates the number of days between two dates based on a 360-day year + ce.RegisterFunction("EDATE", 2, Edate); // Returns the serial number of the date that is the indicated number of months before or after the start date + ce.RegisterFunction("EOMONTH", 2, Eomonth); // Returns the serial number of the last day of the month before or after a specified number of months + ce.RegisterFunction("HOUR", 1, Hour); // Converts a serial number to an hour + ce.RegisterFunction("ISOWEEKNUM", 1, IsoWeekNum); // Returns number of the ISO week number of the year for a given date. + ce.RegisterFunction("MINUTE", 1, Minute); // Converts a serial number to a minute + ce.RegisterFunction("MONTH", 1, Month); // Converts a serial number to a month + ce.RegisterFunction("NETWORKDAYS", 2, 3, Networkdays); // Returns the number of whole workdays between two dates + ce.RegisterFunction("NOW", 0, Now); // Returns the serial number of the current date and time + ce.RegisterFunction("SECOND", 1, Second); // Converts a serial number to a second + ce.RegisterFunction("TIME", 3, Time); // Returns the serial number of a particular time + ce.RegisterFunction("TIMEVALUE", 1, Timevalue); // Converts a time in the form of text to a serial number + ce.RegisterFunction("TODAY", 0, Today); // Returns the serial number of today's date + ce.RegisterFunction("WEEKDAY", 1, 2, Weekday); // Converts a serial number to a day of the week + ce.RegisterFunction("WEEKNUM", 1, 2, Weeknum); // Converts a serial number to a number representing where the week falls numerically with a year + ce.RegisterFunction("WORKDAY", 2, 3, Workday); // Returns the serial number of the date before or after a specified number of workdays + ce.RegisterFunction("YEAR", 1, Year); // Converts a serial number to a year + ce.RegisterFunction("YEARFRAC", 2, 3, Yearfrac); // Returns the year fraction representing the number of whole days between start_date and end_date + } + + /// + /// Calculates number of business days, taking into account: + /// - weekends (Saturdays and Sundays) + /// - bank holidays in the middle of the week + /// + /// First day in the time interval + /// Last day in the time interval + /// List of bank holidays excluding weekends + /// Number of business days during the 'span' + private static int BusinessDaysUntil(DateTime firstDay, DateTime lastDay, IEnumerable bankHolidays) + { + firstDay = firstDay.Date; + lastDay = lastDay.Date; + if (firstDay > lastDay) + return -BusinessDaysUntil(lastDay, firstDay, bankHolidays); + + TimeSpan span = lastDay - firstDay; + int businessDays = span.Days + 1; + int fullWeekCount = businessDays / 7; + // find out if there are weekends during the time exceedng the full weeks + if (businessDays > fullWeekCount * 7) + { + // we are here to find out if there is a 1-day or 2-days weekend + // in the time interval remaining after subtracting the complete weeks + var firstDayOfWeek = (int)firstDay.DayOfWeek; + var lastDayOfWeek = (int)lastDay.DayOfWeek; + if (lastDayOfWeek < firstDayOfWeek) + lastDayOfWeek += 7; + if (firstDayOfWeek <= 6) + { + if (lastDayOfWeek >= 7)// Both Saturday and Sunday are in the remaining time interval + businessDays -= 2; + else if (lastDayOfWeek >= 6)// Only Saturday is in the remaining time interval + businessDays -= 1; + } + else if (firstDayOfWeek <= 7 && lastDayOfWeek >= 7)// Only Sunday is in the remaining time interval + businessDays -= 1; + } + + // subtract the weekends during the full weeks in the interval + businessDays -= fullWeekCount + fullWeekCount; + + // subtract the number of bank holidays during the time interval + foreach (var bh in bankHolidays) + { + if (firstDay <= bh && bh <= lastDay) + --businessDays; + } + + return businessDays; + } + + private static object Date(List p) + { + var year = (int)p[0]; + var month = (int)p[1]; + var day = (int)p[2]; + + // Excel allows months and days outside the normal range, and adjusts the date accordingly + if (month > 12 || month < 1) + { + year += (int)Math.Floor((double)(month - 1d) / 12.0); + month -= (int)Math.Floor((double)(month - 1d) / 12.0) * 12; + } + + int daysAdjustment = 0; + if (day > DateTime.DaysInMonth(year, month)) + { + daysAdjustment = day - DateTime.DaysInMonth(year, month); + day = DateTime.DaysInMonth(year, month); + } + else if (day < 1) + { + daysAdjustment = day - 1; + day = 1; + } + + return (int)Math.Floor(new DateTime(year, month, day).AddDays(daysAdjustment).ToOADate()); + } + + private static object Datedif(List p) + { + DateTime startDate = p[0]; + DateTime endDate = p[1]; + string unit = p[2]; + + if (startDate > endDate) + throw new NumberException("The start date is greater than the end date"); + + return (unit.ToUpper()) switch + { + "Y" => endDate.Year - startDate.Year - (new DateTime(startDate.Year, endDate.Month, endDate.Day) < startDate ? 1 : 0), + "M" => Math.Truncate((endDate.Year - startDate.Year) * 12d + endDate.Month - startDate.Month - (endDate.Day < startDate.Day ? 1 : 0)), + "D" => Math.Truncate(endDate.Date.Subtract(startDate.Date).TotalDays), + + // Microsoft discouranges the use of the MD parameter + // https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c + "MD" => (endDate.Day - startDate.Day + DateTime.DaysInMonth(startDate.Year, startDate.Month)) % DateTime.DaysInMonth(startDate.Year, startDate.Month), + + "YM" => (endDate.Month - startDate.Month + 12) % 12 - (endDate.Day < startDate.Day ? 1 : 0), + "YD" => Math.Truncate(new DateTime(startDate.Year + (new DateTime(startDate.Year, endDate.Month, endDate.Day) < startDate ? 1 : 0), endDate.Month, endDate.Day).Subtract(startDate).TotalDays), + _ => throw new NumberException(), + }; + } + + private static object Datevalue(List p) + { + var date = (string)p[0]; + + return (int)Math.Floor(DateTime.Parse(date).ToOADate()); + } + + private static object Day(List p) + { + var date = (DateTime)p[0]; + + return date.Day; + } + + private static object Days(List p) + { + Type type; + + int end_date; + + type = p[0]._token.Value.GetType(); + if (type == typeof(string)) + end_date = (int)Datevalue(new List() { p[0] }); + else + end_date = (int)p[0]; + + int start_date; + + type = p[1]._token.Value.GetType(); + if (type == typeof(string)) + start_date = (int)Datevalue(new List() { p[1] }); + else + start_date = (int)p[1]; + + return end_date - start_date; + } + + private static object Days360(List p) + { + var date1 = (DateTime)p[0]; + var date2 = (DateTime)p[1]; + var isEuropean = p.Count == 3 ? p[2] : false; + + return Days360(date1, date2, isEuropean); + } + + private static Int32 Days360(DateTime date1, DateTime date2, Boolean isEuropean) + { + var d1 = date1.Day; + var m1 = date1.Month; + var y1 = date1.Year; + var d2 = date2.Day; + var m2 = date2.Month; + var y2 = date2.Year; + + if (isEuropean) + { + if (d1 == 31) d1 = 30; + if (d2 == 31) d2 = 30; + } + else + { + if (d1 == 31) d1 = 30; + if (d2 == 31 && d1 == 30) d2 = 30; + } + + return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1); + } + + private static object Edate(List p) + { + var date = (DateTime)p[0]; + var mod = (int)p[1]; + + var retDate = date.AddMonths(mod); + return retDate; + } + + private static object Eomonth(List p) + { + var start_date = (DateTime)p[0]; + var months = (int)p[1]; + + var retDate = start_date.AddMonths(months); + return new DateTime(retDate.Year, retDate.Month, DateTime.DaysInMonth(retDate.Year, retDate.Month)); + } + + private static Double GetYearAverage(DateTime date1, DateTime date2) + { + var daysInYears = new List(); + for (int year = date1.Year; year <= date2.Year; year++) + daysInYears.Add(DateTime.IsLeapYear(year) ? 366 : 365); + return daysInYears.Average(); + } + + private static object Hour(List p) + { + var date = (DateTime)p[0]; + + return date.Hour; + } + + // http://stackoverflow.com/questions/11154673/get-the-correct-week-number-of-a-given-date + private static object IsoWeekNum(List p) + { + var date = (DateTime)p[0]; + + // Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll + // be the same week# as whatever Thursday, Friday or Saturday are, + // and we always get those right + DayOfWeek day = CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(date); + if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) + { + date = date.AddDays(3); + } + + // Return the week of our adjusted day + return CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + } + + private static object Minute(List p) + { + var date = (DateTime)p[0]; + + return date.Minute; + } + + private static object Month(List p) + { + var date = (DateTime)p[0]; + + return date.Month; + } + + private static object Networkdays(List p) + { + var date1 = (DateTime)p[0]; + var date2 = (DateTime)p[1]; + var bankHolidays = new List(); + if (p.Count == 3) + { + var t = new Tally { p[2] }; + + bankHolidays.AddRange(t.Select(XLHelper.GetDate)); + } + + return BusinessDaysUntil(date1, date2, bankHolidays); + } + + private static object Now(List p) + { + return DateTime.Now; + } + + private static object Second(List p) + { + var date = (DateTime)p[0]; + + return date.Second; + } + + private static object Time(List p) + { + var hour = (int)p[0]; + var minute = (int)p[1]; + var second = (int)p[2]; + + return new TimeSpan(0, hour, minute, second); + } + + private static object Timevalue(List p) + { + var date = (DateTime)p[0]; + + return (DateTime.MinValue + date.TimeOfDay).ToOADate(); + } + + private static object Today(List p) + { + return DateTime.Today; + } + + private static object Weekday(List p) + { + var dayOfWeek = (int)((DateTime)p[0]).DayOfWeek; + var retType = p.Count == 2 ? (int)p[1] : 1; + + if (retType == 2) return dayOfWeek; + if (retType == 1) return dayOfWeek + 1; + + return dayOfWeek - 1; + } + + private static object Weeknum(List p) + { + var date = (DateTime)p[0]; + var retType = p.Count == 2 ? (int)p[1] : 1; + + DayOfWeek dayOfWeek = retType == 1 ? DayOfWeek.Sunday : DayOfWeek.Monday; + var cal = new GregorianCalendar(GregorianCalendarTypes.Localized); + var val = cal.GetWeekOfYear(date, CalendarWeekRule.FirstDay, dayOfWeek); + + return val; + } + + private static object Workday(List p) + { + var startDate = (DateTime)p[0]; + var daysRequired = (int)p[1]; + + if (daysRequired == 0) return startDate; + + var bankHolidays = new List(); + if (p.Count == 3) + { + var t = new Tally { p[2] }; + + bankHolidays.AddRange(t.Select(XLHelper.GetDate)); + } + var testDate = startDate.AddDays(((daysRequired / 7) + 2) * 7 * Math.Sign(daysRequired)); + var return_date = Workday(startDate, testDate, daysRequired, bankHolidays); + if (Math.Sign(daysRequired) == 1) + return_date = return_date.NextWorkday(bankHolidays); + else + return_date = return_date.PreviousWorkDay(bankHolidays); + + return return_date; + } + + private static DateTime Workday(DateTime startDate, DateTime testDate, int daysRequired, IEnumerable bankHolidays) + { + var businessDays = BusinessDaysUntil(startDate, testDate, bankHolidays); + if (businessDays == daysRequired) + return testDate; + + int days = businessDays > daysRequired ? -1 : 1; + + return Workday(startDate, testDate.AddDays(days), daysRequired, bankHolidays); + } + + private static object Year(List p) + { + var date = (DateTime)p[0]; + + return date.Year; + } + + private static object Yearfrac(List p) + { + var date1 = (DateTime)p[0]; + var date2 = (DateTime)p[1]; + var option = p.Count == 3 ? (int)p[2] : 0; + + if (option == 0) + return Days360(date1, date2, false) / 360.0; + if (option == 1) + return Math.Floor((date2 - date1).TotalDays) / GetYearAverage(date1, date2); + if (option == 2) + return Math.Floor((date2 - date1).TotalDays) / 360.0; + if (option == 3) + return Math.Floor((date2 - date1).TotalDays) / 365.0; + + return Days360(date1, date2, true) / 360.0; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Engineering.cs b/ClosedXML/Excel/CalcEngine/Functions/Engineering.cs new file mode 100644 index 0000000..2ae0a9f --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Engineering.cs @@ -0,0 +1,63 @@ +namespace ClosedXML.Excel.CalcEngine +{ + internal static class Engineering + { + public static void Register(CalcEngine ce) + { + // BESSELI Returns the modified Bessel function In(x) + // BESSELJ Returns the Bessel function Jn(x) + // BESSELK Returns the modified Bessel function Kn(x) + // BESSELY Returns the Bessel function Yn(x) + // BIN2DEC Converts a binary number to decimal + // BIN2HEX Converts a binary number to hexadecimal + // BIN2OCT Converts a binary number to octal + // BITAND Returns a bitwise 'And' of two numbers + // BITLSHIFT Returns a number shifted left by shift_amount bits + // BITOR Returns a bitwise 'Or' of two numbers + // BITRSHIFT Returns a number shifted right by shift_amount bits + // BITXOR Returns a bitwise 'Exclusive Or' of two numbers + // COMPLEX Converts real and imaginary coefficients into a complex number + // CONVERT Converts a number from one measurement system to another + // DEC2BIN Converts a decimal number to binary + // DEC2HEX Converts a decimal number to hexadecimal + // DEC2OCT Converts a decimal number to octal + // DELTA Tests whether two values are equal + // ERF Returns the error function + // ERF.PRECISE Returns the error function + // ERFC Returns the complementary error function + // ERFC.PRECISE Returns the complementary ERF function integrated between x and infinity + // GESTEP Tests whether a number is greater than a threshold value + // HEX2BIN Converts a hexadecimal number to binary + // HEX2DEC Converts a hexadecimal number to decimal + // HEX2OCT Converts a hexadecimal number to octal + // IMABS Returns the absolute value(modulus) of a complex number + // IMAGINARY Returns the imaginary coefficient of a complex number + // IMARGUMENT Returns the argument theta, an angle expressed in radians + // IMCONJUGATE Returns the complex conjugate of a complex number + // IMCOS Returns the cosine of a complex number + // IMCOSH Returns the hyperbolic cosine of a complex number + // IMCOT Returns the cotangent of a complex number + // IMCSC Returns the cosecant of a complex number + // IMCSCH Returns the hyperbolic cosecant of a complex number + // IMDIV Returns the quotient of two complex numbers + // IMEXP Returns the exponential of a complex number + // IMLN Returns the natural logarithm of a complex number + // IMLOG10 Returns the base - 10 logarithm of a complex number + // IMLOG2 Returns the base - 2 logarithm of a complex number + // IMPOWER Returns a complex number raised to an integer power + // IMPRODUCT Returns the product of from 2 to 255 complex numbers + // IMREAL Returns the real coefficient of a complex number + // IMSEC Returns the secant of a complex number + // IMSECH Returns the hyperbolic secant of a complex number + // IMSIN Returns the sine of a complex number + // IMSINH Returns the hyperbolic sine of a complex number + // IMSQRT Returns the square root of a complex number + // IMSUB Returns the difference between two complex numbers + // IMSUM Returns the sum of complex numbers + // IMTAN Returns the tangent of a complex number + // OCT2BIN Converts an octal number to binary + // OCT2DEC Converts an octal number to decimal + // OCT2HEX Converts an octal number to hexadecimal + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Financial.cs b/ClosedXML/Excel/CalcEngine/Functions/Financial.cs new file mode 100644 index 0000000..d8523d9 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Financial.cs @@ -0,0 +1,64 @@ +namespace ClosedXML.Excel.CalcEngine +{ + internal static class Financial + { + public static void Register(CalcEngine ce) + { + // ACCRINT Returns the accrued interest for a security that pays periodic interest + // ACCRINTM Returns the accrued interest for a security that pays interest at maturity + // AMORDEGRC Returns the depreciation for each accounting period by using a depreciation coefficient + // AMORLINC Returns the depreciation for each accounting period + // COUPDAYBS Returns the number of days from the beginning of the coupon period to the settlement date + // COUPDAYS Returns the number of days in the coupon period that contains the settlement date + // COUPDAYSNC Returns the number of days from the settlement date to the next coupon date + // COUPNCD Returns the next coupon date after the settlement date + // COUPNUM Returns the number of coupons payable between the settlement date and maturity date + // COUPPCD Returns the previous coupon date before the settlement date + // CUMIPMT Returns the cumulative interest paid between two periods + // CUMPRINC Returns the cumulative principal paid on a loan between two periods + // DB Returns the depreciation of an asset for a specified period by using the fixed-declining balance method + // DDB Returns the depreciation of an asset for a specified period by using the double-declining balance method or some other method that you specify + // DISC Returns the discount rate for a security + // DOLLARDE Converts a dollar price, expressed as a fraction, into a dollar price, expressed as a decimal number + // DOLLARFR Converts a dollar price, expressed as a decimal number, into a dollar price, expressed as a fraction + // DURATION Returns the annual duration of a security with periodic interest payments + // EFFECT Returns the effective annual interest rate + // FV Returns the future value of an investment + // FVSCHEDULE Returns the future value of an initial principal after applying a series of compound interest rates + // INTRATE Returns the interest rate for a fully invested security + // IPMT Returns the interest payment for an investment for a given period + // IRR Returns the internal rate of return for a series of cash flows + // ISPMT Calculates the interest paid during a specific period of an investment + // MDURATION Returns the Macauley modified duration for a security with an assumed par value of $100 + // MIRR Returns the internal rate of return where positive and negative cash flows are financed at different rates + // NOMINAL Returns the annual nominal interest rate + // NPER Returns the number of periods for an investment + // NPV Returns the net present value of an investment based on a series of periodic cash flows and a discount rate + // ODDFPRICE Returns the price per $100 face value of a security with an odd first period + // ODDFYIELD Returns the yield of a security with an odd first period + // ODDLPRICE Returns the price per $100 face value of a security with an odd last period + // ODDLYIELD Returns the yield of a security with an odd last period + // PDURATION Returns the number of periods required by an investment to reach a specified value + // PMT Returns the periodic payment for an annuity + // PPMT Returns the payment on the principal for an investment for a given period + // PRICE Returns the price per $100 face value of a security that pays periodic interest + // PRICEDISC Returns the price per $100 face value of a discounted security + // PRICEMAT Returns the price per $100 face value of a security that pays interest at maturity + // PV Returns the present value of an investment + // RATE Returns the interest rate per period of an annuity + // RECEIVED Returns the amount received at maturity for a fully invested security + // RRI Returns an equivalent interest rate for the growth of an investment + // SLN Returns the straight-line depreciation of an asset for one period + // SYD Returns the sum-of-years' digits depreciation of an asset for a specified period + // TBILLEQ Returns the bond-equivalent yield for a Treasury bill + // TBILLPRICE Returns the price per $100 face value for a Treasury bill + // TBILLYIELD Returns the yield for a Treasury bill + // VDB Returns the depreciation of an asset for a specified or partial period by using a declining balance method + // XIRR Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic + // XNPV Returns the net present value for a schedule of cash flows that is not necessarily periodic + // YIELD Returns the yield on a security that pays periodic interest + // YIELDDISC Returns the annual yield for a discounted security; for example, a Treasury bill + // YIELDMAT Returns the annual yield of a security that pays interest at maturity + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Information.cs b/ClosedXML/Excel/CalcEngine/Functions/Information.cs new file mode 100644 index 0000000..be8b7bc --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Information.cs @@ -0,0 +1,216 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class Information + { + public static void Register(CalcEngine ce) + { + //TODO: Add documentation + ce.RegisterFunction("ERRORTYPE", 1, ErrorType); + ce.RegisterFunction("ISBLANK", 1, int.MaxValue, IsBlank); + ce.RegisterFunction("ISERR", 1, int.MaxValue, IsErr); + ce.RegisterFunction("ISERROR", 1, int.MaxValue, IsError); + ce.RegisterFunction("ISEVEN", 1, IsEven); + ce.RegisterFunction("ISLOGICAL", 1, int.MaxValue, IsLogical); + ce.RegisterFunction("ISNA", 1, int.MaxValue, IsNa); + ce.RegisterFunction("ISNONTEXT", 1, int.MaxValue, IsNonText); + ce.RegisterFunction("ISNUMBER", 1, int.MaxValue, IsNumber); + ce.RegisterFunction("ISODD", 1, IsOdd); + ce.RegisterFunction("ISREF", 1, int.MaxValue, IsRef); + ce.RegisterFunction("ISTEXT", 1, int.MaxValue, IsText); + ce.RegisterFunction("N", 1, N); + ce.RegisterFunction("NA", 0, NA); + ce.RegisterFunction("TYPE", 1, Type); + } + + static IDictionary errorTypes = new Dictionary() + { + [ErrorExpression.ExpressionErrorType.NullValue] = 1, + [ErrorExpression.ExpressionErrorType.DivisionByZero] = 2, + [ErrorExpression.ExpressionErrorType.CellValue] = 3, + [ErrorExpression.ExpressionErrorType.CellReference] = 4, + [ErrorExpression.ExpressionErrorType.NameNotRecognized] = 5, + [ErrorExpression.ExpressionErrorType.NumberInvalid] = 6, + [ErrorExpression.ExpressionErrorType.NoValueAvailable] = 7 + }; + + static object ErrorType(List p) + { + var v = p[0].Evaluate(); + + if (v is ErrorExpression.ExpressionErrorType) + return errorTypes[(ErrorExpression.ExpressionErrorType)v]; + else + throw new NoValueAvailableException(); + } + + static object IsBlank(List p) + { + var v = (string) p[0]; + var isBlank = string.IsNullOrEmpty(v); + + + if (isBlank && p.Count > 1) { + var sublist = p.GetRange(1, p.Count); + isBlank = (bool)IsBlank(sublist); + } + + return isBlank; + } + + static object IsErr(List p) + { + var v = p[0].Evaluate(); + + return v is ErrorExpression.ExpressionErrorType + && ((ErrorExpression.ExpressionErrorType)v) != ErrorExpression.ExpressionErrorType.NoValueAvailable; + } + + static object IsError(List p) + { + var v = p[0].Evaluate(); + + return v is ErrorExpression.ExpressionErrorType; + } + + static object IsEven(List p) + { + var v = p[0].Evaluate(); + if (v is double) + { + return Math.Abs((double) v%2) < 1; + } + //TODO: Error Exceptions + throw new ArgumentException("Expression doesn't evaluate to double"); + } + + static object IsLogical(List p) + { + var v = p[0].Evaluate(); + var isLogical = v is bool; + + if (isLogical && p.Count > 1) + { + var sublist = p.GetRange(1, p.Count); + isLogical = (bool) IsLogical(sublist); + } + + return isLogical; + } + + static object IsNa(List p) + { + var v = p[0].Evaluate(); + + return v is ErrorExpression.ExpressionErrorType + && ((ErrorExpression.ExpressionErrorType)v) == ErrorExpression.ExpressionErrorType.NoValueAvailable; + } + + static object IsNonText(List p) + { + return !(bool) IsText(p); + } + + static object IsNumber(List p) + { + var v = p[0].Evaluate(); + + var isNumber = v is double; //Normal number formatting + if (!isNumber) + { + isNumber = v is DateTime; //Handle DateTime Format + } + if (!isNumber) + { + //Handle Number Styles + try + { + var stringValue = (string) v; + return double.TryParse(stringValue.TrimEnd('%', ' '), NumberStyles.Any, null, out double dv); + } + catch (Exception) + { + isNumber = false; + } + } + + if (isNumber && p.Count > 1) + { + var sublist = p.GetRange(1, p.Count); + isNumber = (bool)IsNumber(sublist); + } + + return isNumber; + } + + static object IsOdd(List p) + { + return !(bool) IsEven(p); + } + + static object IsRef(List p) + { + var oe = p[0] as XObjectExpression; + if (oe == null) + return false; + + var crr = oe.Value as CellRangeReference; + + return crr != null; + } + + static object IsText(List p) + { + //Evaluate Expressions + var isText = !(bool) IsBlank(p); + if (isText) + { + isText = !(bool) IsNumber(p); + } + if (isText) + { + isText = !(bool) IsLogical(p); + } + return isText; + } + + static object N(List p) + { + return (double) p[0]; + } + + static object NA(List p) + { + return ErrorExpression.ExpressionErrorType.NoValueAvailable; + } + + static object Type(List p) + { + if ((bool) IsNumber(p)) + { + return 1; + } + if ((bool) IsText(p)) + { + return 2; + } + if ((bool) IsLogical(p)) + { + return 4; + } + if ((bool) IsError(p)) + { + return 16; + } + if(p.Count > 1) + { + return 64; + } + return null; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Logical.cs b/ClosedXML/Excel/CalcEngine/Functions/Logical.cs new file mode 100644 index 0000000..d5fa418 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Logical.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.CalcEngine +{ + internal static class Logical + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("AND", 1, int.MaxValue, And); + ce.RegisterFunction("OR", 1, int.MaxValue, Or); + ce.RegisterFunction("NOT", 1, Not); + ce.RegisterFunction("IF", 2, 3, If); + ce.RegisterFunction("TRUE", 0, True); + ce.RegisterFunction("FALSE", 0, False); + ce.RegisterFunction("IFERROR",2,IfError); + } + + static object And(List p) + { + var b = true; + foreach (var v in p) + { + b = b && v; + } + return b; + } + + static object Or(List p) + { + var b = false; + foreach (var v in p) + { + b = b || v; + } + return b; + } + + static object Not(List p) + { + return !p[0]; + } + + static object If(List p) + { + if (p[0]) + { + return p[1].Evaluate(); + } + else if (p.Count > 2) + { + if (p[2] is EmptyValueExpression) + return false; + else + return p[2].Evaluate(); + } + else return false; + } + + static object True(List p) + { + return true; + } + + static object False(List p) + { + return false; + } + + static object IfError(List p) + { + try + { + return p[0].Evaluate(); + } + catch (ArgumentException) + { + return p[1].Evaluate(); + } + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Lookup.cs b/ClosedXML/Excel/CalcEngine/Functions/Lookup.cs new file mode 100644 index 0000000..1b9937a --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Lookup.cs @@ -0,0 +1,266 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel.CalcEngine.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class Lookup + { + public static void Register(CalcEngine ce) + { + //ce.RegisterFunction("ADDRESS", , Address); // Returns a reference as text to a single cell in a worksheet + //ce.RegisterFunction("AREAS", , Areas); // Returns the number of areas in a reference + //ce.RegisterFunction("CHOOSE", , Choose); // Chooses a value from a list of values + //ce.RegisterFunction("COLUMN", , Column); // Returns the column number of a reference + //ce.RegisterFunction("COLUMNS", , Columns); // Returns the number of columns in a reference + //ce.RegisterFunction("FORMULATEXT", , Formulatext); // Returns the formula at the given reference as text + //ce.RegisterFunction("GETPIVOTDATA", , Getpivotdata); // Returns data stored in a PivotTable report + ce.RegisterFunction("HLOOKUP", 3, 4, Hlookup); // Looks in the top row of an array and returns the value of the indicated cell + ce.RegisterFunction("HYPERLINK", 1, 2, Hyperlink); // Creates a shortcut or jump that opens a document stored on a network server, an intranet, or the Internet + ce.RegisterFunction("INDEX", 2, 4, Index); // Uses an index to choose a value from a reference or array + //ce.RegisterFunction("INDIRECT", , Indirect); // Returns a reference indicated by a text value + //ce.RegisterFunction("LOOKUP", , Lookup); // Looks up values in a vector or array + ce.RegisterFunction("MATCH", 2, 3, Match); // Looks up values in a reference or array + //ce.RegisterFunction("OFFSET", , Offset); // Returns a reference offset from a given reference + //ce.RegisterFunction("ROW", , Row); // Returns the row number of a reference + //ce.RegisterFunction("ROWS", , Rows); // Returns the number of rows in a reference + //ce.RegisterFunction("RTD", , Rtd); // Retrieves real-time data from a program that supports COM automation + //ce.RegisterFunction("TRANSPOSE", , Transpose); // Returns the transpose of an array + ce.RegisterFunction("VLOOKUP", 3, 4, Vlookup); // Looks in the first column of an array and moves across the row to return the value of a cell + } + + private static IXLRange ExtractRange(Expression expression) + { + if (!(expression is XObjectExpression objectExpression)) + throw new NoValueAvailableException("Parameter has to be a valid range"); + + if (!(objectExpression.Value is CellRangeReference cellRangeReference)) + throw new NoValueAvailableException("lookup_array has to be a range"); + + var range = cellRangeReference.Range; + return range; + } + + private static object Hlookup(List p) + { + var lookup_value = p[0]; + var range = ExtractRange(p[1]); + var row_index_num = (int)p[2]; + var range_lookup = p.Count < 4 + || p[3] is EmptyValueExpression + || (bool)(p[3]); + + if (row_index_num < 1) + throw new CellReferenceException("Row index has to be positive"); + + if (row_index_num > range.RowCount()) + throw new CellReferenceException("Row index has to be positive"); + + IXLRangeColumn matching_column; + matching_column = range.FindColumn(c => !c.Cell(1).IsEmpty() && new Expression(c.Cell(1).Value).CompareTo(lookup_value) == 0); + if (range_lookup && matching_column == null) + { + var first_column = range.FirstColumn().ColumnNumber(); + var number_of_columns_in_range = range.ColumnsUsed().Count(); + + matching_column = range.FindColumn(c => + { + var column_index_in_range = c.ColumnNumber() - first_column + 1; + if (column_index_in_range < number_of_columns_in_range && !c.Cell(1).IsEmpty() && new Expression(c.Cell(1).Value).CompareTo(lookup_value) <= 0 && !c.ColumnRight().Cell(1).IsEmpty() && new Expression(c.ColumnRight().Cell(1).Value).CompareTo(lookup_value) > 0) + return true; + else if (column_index_in_range == number_of_columns_in_range && !c.Cell(1).IsEmpty() && new Expression(c.Cell(1).Value).CompareTo(lookup_value) <= 0) + return true; + else + return false; + }); + } + + if (matching_column == null) + throw new NoValueAvailableException("No matches found."); + + return matching_column + .Cell(row_index_num) + .Value; + } + + private static object Hyperlink(List p) + { + String address = p[0]; + String toolTip = p.Count == 2 ? p[1] : String.Empty; + return new XLHyperlink(address, toolTip); + } + + private static object Index(List p) + { + // This is one of the few functions that is "overloaded" + var range = ExtractRange(p[0]); + + if (range.ColumnCount() > 1 && range.RowCount() > 1) + { + var row_num = (int)p[1]; + var column_num = (int)p[2]; + + if (row_num > range.RowCount()) + throw new CellReferenceException("Out of bound row number"); + + if (column_num > range.ColumnCount()) + throw new CellReferenceException("Out of bound column number"); + + return range.Row(row_num).Cell(column_num).Value; + } + else if (p.Count == 2) + { + var cellOffset = (int)p[1]; + if (cellOffset > range.RowCount() * range.ColumnCount()) + throw new CellReferenceException(); + + return range.Cells().ElementAt(cellOffset - 1).Value; + } + else + { + int column_num = 1; + int row_num = 1; + + if (!(p[1] is EmptyValueExpression)) + row_num = (int)p[1]; + + if (!(p[2] is EmptyValueExpression)) + column_num = (int)p[2]; + + var rangeIsRow = range.RowCount() == 1; + if (rangeIsRow && row_num > 1) + throw new CellReferenceException(); + + if (!rangeIsRow && column_num > 1) + throw new CellReferenceException(); + + if (row_num > range.RowCount()) + throw new CellReferenceException("Out of bound row number"); + + if (column_num > range.ColumnCount()) + throw new CellReferenceException("Out of bound column number"); + + return range.Row(row_num).Cell(column_num).Value; + } + } + + private static object Match(List p) + { + var lookup_value = p[0]; + var range = ExtractRange(p[1]); + int match_type = 1; + if (p.Count > 2) + match_type = Math.Sign((int)p[2]); + + if (range.ColumnCount() != 1 && range.RowCount() != 1) + throw new CellValueException("Range has to be 1-dimensional"); + + Predicate lookupPredicate = null; + switch (match_type) + { + case 0: + lookupPredicate = i => i == 0; + break; + + case 1: + lookupPredicate = i => i <= 0; + break; + + case -1: + lookupPredicate = i => i >= 0; + break; + + default: + throw new NoValueAvailableException("Invalid match_type"); + } + + IXLCell foundCell = null; + + if (match_type == 0) + foundCell = range + .CellsUsed(XLCellsUsedOptions.Contents, c => lookupPredicate.Invoke(new Expression(c.Value).CompareTo(lookup_value))) + .FirstOrDefault(); + else + { + object previousValue = null; + foundCell = range + .CellsUsed(XLCellsUsedOptions.Contents) + .TakeWhile(c => + { + var currentCellExpression = new Expression(c.Value); + + if (previousValue != null) + { + // When match_type != 0, we have to assume that the order of the items being search is ascending or descending + var previousValueExpression = new Expression(previousValue); + if (!lookupPredicate.Invoke(previousValueExpression.CompareTo(currentCellExpression))) + return false; + } + + previousValue = c.Value; + + return lookupPredicate.Invoke(currentCellExpression.CompareTo(lookup_value)); + }) + .LastOrDefault(); + } + + if (foundCell == null) + throw new NoValueAvailableException(); + + var firstCell = range.FirstCell(); + + return (foundCell.Address.ColumnNumber - firstCell.Address.ColumnNumber + 1) * (foundCell.Address.RowNumber - firstCell.Address.RowNumber + 1); + } + + private static object Vlookup(List p) + { + var lookup_value = p[0]; + var range = ExtractRange(p[1]); + var col_index_num = (int)p[2]; + var range_lookup = p.Count < 4 + || p[3] is EmptyValueExpression + || (bool)(p[3]); + + if (col_index_num < 1) + throw new CellReferenceException("Column index has to be positive"); + + if (col_index_num > range.ColumnCount()) + throw new CellReferenceException("Colum index must be smaller or equal to the number of columns in the table array"); + + IXLRangeRow matching_row; + try + { + matching_row = range.FindRow(r => !r.Cell(1).IsEmpty() && new Expression(r.Cell(1).Value).CompareTo(lookup_value) == 0); + } + catch (Exception ex) + { + throw new NoValueAvailableException("No matches found", ex); + } + if (range_lookup && matching_row == null) + { + var first_row = range.FirstRow().RowNumber(); + var number_of_rows_in_range = range.RowsUsed().Count(); + + matching_row = range.FindRow(r => + { + var row_index_in_range = r.RowNumber() - first_row + 1; + if (row_index_in_range < number_of_rows_in_range && !r.Cell(1).IsEmpty() && new Expression(r.Cell(1).Value).CompareTo(lookup_value) <= 0 && !r.RowBelow().Cell(1).IsEmpty() && new Expression(r.RowBelow().Cell(1).Value).CompareTo(lookup_value) > 0) + return true; + else if (row_index_in_range == number_of_rows_in_range && !r.Cell(1).IsEmpty() && new Expression(r.Cell(1).Value).CompareTo(lookup_value) <= 0) + return true; + else + return false; + }); + } + + if (matching_row == null) + throw new NoValueAvailableException("No matches found."); + + return matching_row + .Cell(col_index_num) + .Value; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs new file mode 100644 index 0000000..7e6dd81 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs @@ -0,0 +1,1095 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using ClosedXML.Excel.CalcEngine.Functions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + internal static class MathTrig + { + private static readonly Random _rnd = new Random(); + + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("ABS", 1, Abs); + ce.RegisterFunction("ACOS", 1, Acos); + ce.RegisterFunction("ACOSH", 1, Acosh); + ce.RegisterFunction("ACOT", 1, Acot); + ce.RegisterFunction("ACOTH", 1, Acoth); + ce.RegisterFunction("ARABIC", 1, Arabic); + ce.RegisterFunction("ASIN", 1, Asin); + ce.RegisterFunction("ASINH", 1, Asinh); + ce.RegisterFunction("ATAN", 1, Atan); + ce.RegisterFunction("ATAN2", 2, Atan2); + ce.RegisterFunction("ATANH", 1, Atanh); + ce.RegisterFunction("BASE", 2, 3, Base); + ce.RegisterFunction("CEILING", 2, Ceiling); + ce.RegisterFunction("CEILING.MATH", 1, 3, CeilingMath); + ce.RegisterFunction("COMBIN", 2, Combin); + ce.RegisterFunction("COMBINA", 2, CombinA); + ce.RegisterFunction("COS", 1, Cos); + ce.RegisterFunction("COSH", 1, Cosh); + ce.RegisterFunction("COT", 1, Cot); + ce.RegisterFunction("COTH", 1, Coth); + ce.RegisterFunction("CSC", 1, Csc); + ce.RegisterFunction("CSCH", 1, Csch); + ce.RegisterFunction("DECIMAL", 2, MathTrig.Decimal); + ce.RegisterFunction("DEGREES", 1, Degrees); + ce.RegisterFunction("EVEN", 1, Even); + ce.RegisterFunction("EXP", 1, Exp); + ce.RegisterFunction("FACT", 1, Fact); + ce.RegisterFunction("FACTDOUBLE", 1, FactDouble); + ce.RegisterFunction("FLOOR", 2, Floor); + ce.RegisterFunction("FLOOR.MATH", 1, 3, FloorMath); + ce.RegisterFunction("GCD", 1, 255, Gcd); + ce.RegisterFunction("INT", 1, Int); + ce.RegisterFunction("LCM", 1, 255, Lcm); + ce.RegisterFunction("LN", 1, Ln); + ce.RegisterFunction("LOG", 1, 2, Log); + ce.RegisterFunction("LOG10", 1, Log10); + ce.RegisterFunction("MDETERM", 1, MDeterm); + ce.RegisterFunction("MINVERSE", 1, MInverse); + ce.RegisterFunction("MMULT", 2, MMult); + ce.RegisterFunction("MOD", 2, Mod); + ce.RegisterFunction("MROUND", 2, MRound); + ce.RegisterFunction("MULTINOMIAL", 1, 255, Multinomial); + ce.RegisterFunction("ODD", 1, Odd); + ce.RegisterFunction("PI", 0, Pi); + ce.RegisterFunction("POWER", 2, Power); + ce.RegisterFunction("PRODUCT", 1, 255, Product); + ce.RegisterFunction("QUOTIENT", 2, Quotient); + ce.RegisterFunction("RADIANS", 1, Radians); + ce.RegisterFunction("RAND", 0, Rand); + ce.RegisterFunction("RANDBETWEEN", 2, RandBetween); + ce.RegisterFunction("ROMAN", 1, 2, Roman); + ce.RegisterFunction("ROUND", 2, Round); + ce.RegisterFunction("ROUNDDOWN", 2, RoundDown); + ce.RegisterFunction("ROUNDUP", 1, 2, RoundUp); + ce.RegisterFunction("SEC", 1, Sec); + ce.RegisterFunction("SECH", 1, Sech); + ce.RegisterFunction("SERIESSUM", 4, SeriesSum); + ce.RegisterFunction("SIGN", 1, Sign); + ce.RegisterFunction("SIN", 1, Sin); + ce.RegisterFunction("SINH", 1, Sinh); + ce.RegisterFunction("SQRT", 1, Sqrt); + ce.RegisterFunction("SQRTPI", 1, SqrtPi); + ce.RegisterFunction("SUBTOTAL", 2, 255, Subtotal); + ce.RegisterFunction("SUM", 1, int.MaxValue, Sum); + ce.RegisterFunction("SUMIF", 2, 3, SumIf); + ce.RegisterFunction("SUMIFS", 3, 255, SumIfs); + ce.RegisterFunction("SUMPRODUCT", 1, 30, SumProduct); + ce.RegisterFunction("SUMSQ", 1, 255, SumSq); + //ce.RegisterFunction("SUMX2MY2", SumX2MY2, 1); + //ce.RegisterFunction("SUMX2PY2", SumX2PY2, 1); + //ce.RegisterFunction("SUMXMY2", SumXMY2, 1); + ce.RegisterFunction("TAN", 1, Tan); + ce.RegisterFunction("TANH", 1, Tanh); + ce.RegisterFunction("TRUNC", 1, 2, Trunc); + } + + private static object Abs(List p) + { + return Math.Abs(p[0]); + } + + private static object Acos(List p) + { + double input = p[0]; + if (Math.Abs(input) > 1) + throw new NumberException(); + + return Math.Acos(p[0]); + } + + private static object Asin(List p) + { + double input = p[0]; + if (Math.Abs(input) > 1) + throw new NumberException(); + + return Math.Asin(input); + } + + private static object Atan(List p) + { + return Math.Atan(p[0]); + } + + private static object Atan2(List p) + { + double x = p[0]; + double y = p[1]; + if (x == 0 && y == 0) + throw new DivisionByZeroException(); + + return Math.Atan2(y, x); + } + + private static object Ceiling(List p) + { + double number = p[0]; + double significance = p[1]; + + if (significance == 0) + return 0d; + else if (significance < 0 && number > 0) + throw new NumberException(); + else if (significance < 0) + return -Math.Ceiling(-number / -significance) * -significance; + else + return Math.Ceiling(number / significance) * significance; + } + + private static object CeilingMath(List p) + { + double number = p[0]; + double significance = 1; + if (p.Count > 1) significance = p[1]; + + double mode = 0; + if (p.Count > 2) mode = p[2]; + + if (significance == 0) + return 0d; + else if (number >= 0) + return Math.Ceiling(number / Math.Abs(significance)) * Math.Abs(significance); + else if (mode == 0) + return Math.Ceiling(number / Math.Abs(significance)) * Math.Abs(significance); + else + return -Math.Ceiling(-number / Math.Abs(significance)) * Math.Abs(significance); + } + + private static object Cos(List p) + { + return Math.Cos(p[0]); + } + + private static object Cosh(List p) + { + return Math.Cosh(p[0]); + } + + private static object Cot(List p) + { + var tan = (double)Math.Tan(p[0]); + + if (tan == 0) + throw new DivisionByZeroException(); + + return 1 / tan; + } + + private static object Coth(List p) + { + double input = p[0]; + if (input == 0) + throw new DivisionByZeroException(); + + return 1 / Math.Tanh(input); + } + + private static object Csc(List p) + { + double input = p[0]; + if (input == 0) + throw new DivisionByZeroException(); + + return 1 / Math.Sin(input); + } + + private static object Csch(List p) + { + if (Math.Abs((double)p[0].Evaluate()) < Double.Epsilon) + throw new DivisionByZeroException(); + + return 1 / Math.Sinh(p[0]); + } + + private static object Decimal(List p) + { + string source = p[0]; + double radix = p[1]; + + if (radix < 2 || radix > 36) + throw new NumberException(); + + var asciiValues = Encoding.ASCII.GetBytes(source.ToUpperInvariant()); + + double result = 0; + int i = 0; + + foreach (byte digit in asciiValues) + { + if (digit > 90) + { + throw new NumberException(); + } + + int digitNumber = digit >= 48 && digit < 58 + ? digit - 48 + : digit - 55; + + if (digitNumber > radix - 1) + throw new NumberException(); + + result = result * radix + digitNumber; + i++; + } + + return result; + } + + private static object Exp(List p) + { + return Math.Exp(p[0]); + } + + private static object Floor(List p) + { + double number = p[0]; + double significance = p[1]; + + if (significance == 0) + throw new DivisionByZeroException(); + else if (significance < 0 && number > 0) + throw new NumberException(); + else if (significance < 0) + return -Math.Floor(-number / -significance) * -significance; + else + return Math.Floor(number / significance) * significance; + } + + private static object FloorMath(List p) + { + double number = p[0]; + double significance = 1; + if (p.Count > 1) significance = p[1]; + + double mode = 0; + if (p.Count > 2) mode = p[2]; + + if (significance == 0) + return 0d; + else if (number >= 0) + return Math.Floor(number / Math.Abs(significance)) * Math.Abs(significance); + else if (mode == 0) + return Math.Floor(number / Math.Abs(significance)) * Math.Abs(significance); + else + return -Math.Floor(-number / Math.Abs(significance)) * Math.Abs(significance); + } + + private static object Int(List p) + { + return Math.Floor(p[0]); + } + + private static object Ln(List p) + { + return Math.Log(p[0]); + } + + private static object Log(List p) + { + var lbase = p.Count > 1 ? (double)p[1] : 10; + return Math.Log(p[0], lbase); + } + + private static object Log10(List p) + { + return Math.Log10(p[0]); + } + + private static object Pi(List p) + { + return Math.PI; + } + + private static object Power(List p) + { + return Math.Pow(p[0], p[1]); + } + + private static object Rand(List p) + { + return _rnd.NextDouble(); + } + + private static object RandBetween(List p) + { + return _rnd.Next((int)(double)p[0], (int)(double)p[1]); + } + + private static object Sign(List p) + { + return Math.Sign(p[0]); + } + + private static object Sin(List p) + { + return Math.Sin(p[0]); + } + + private static object Sinh(List p) + { + return Math.Sinh(p[0]); + } + + private static object Sqrt(List p) + { + return Math.Sqrt(p[0]); + } + + private static object Sum(List p) + { + var tally = new Tally(); + foreach (var e in p) + { + tally.Add(e); + } + return tally.Sum(); + } + + private static object SumIf(List p) + { + // get parameters + var range = p[0] as IEnumerable; // range of values to match the criteria against + var sumRange = p.Count < 3 ? + p[0] as XObjectExpression : + p[2] as XObjectExpression; // range of values to sum up + var criteria = p[1].Evaluate(); // the criteria to evaluate + + var rangeValues = range.Cast().ToList(); + var sumRangeValues = sumRange.Cast().ToList(); + + // compute total + var ce = new CalcEngine(); + var tally = new Tally(); + for (var i = 0; i < Math.Max(rangeValues.Count, sumRangeValues.Count); i++) + { + var targetValue = i < rangeValues.Count ? rangeValues[i] : string.Empty; + if (CalcEngineHelpers.ValueSatisfiesCriteria(targetValue, criteria, ce)) + { + var value = i < sumRangeValues.Count ? sumRangeValues[i] : 0d; + tally.AddValue(value); + } + } + + // done + return tally.Sum(); + } + + private static object SumIfs(List p) + { + // get parameters + var sumRange = p[0] as IEnumerable; + + var sumRangeValues = new List(); + foreach (var value in sumRange) + { + sumRangeValues.Add(value); + } + + var ce = new CalcEngine(); + var tally = new Tally(); + + int numberOfCriteria = p.Count / 2; // int division returns floor() automatically, that's what we want. + + // prepare criteria-parameters: + var criteriaRanges = new Tuple>[numberOfCriteria]; + for (int criteriaPair = 0; criteriaPair < numberOfCriteria; criteriaPair++) + { + var criteriaRange = p[criteriaPair * 2 + 1] as IEnumerable; + + if (criteriaRange == null) + throw new CellReferenceException($"Expected parameter {criteriaPair * 2 + 2} to be a range"); + + var criterion = p[criteriaPair * 2 + 2].Evaluate(); + var criteriaRangeValues = criteriaRange.Cast().ToList(); + + criteriaRanges[criteriaPair] = new Tuple>( + criterion, + criteriaRangeValues); + } + + for (var i = 0; i < sumRangeValues.Count; i++) + { + bool shouldUseValue = true; + + foreach (var criteriaPair in criteriaRanges) + { + if (!CalcEngineHelpers.ValueSatisfiesCriteria( + i < criteriaPair.Item2.Count ? criteriaPair.Item2[i] : string.Empty, + criteriaPair.Item1, + ce)) + { + shouldUseValue = false; + break; // we're done with the inner loop as we can't ever get true again. + } + } + + if (shouldUseValue) + tally.AddValue(sumRangeValues[i]); + } + + // done + return tally.Sum(); + } + + private static object SumProduct(List p) + { + // all parameters should be IEnumerable + if (p.Any(param => !(param is IEnumerable))) + throw new NoValueAvailableException(); + + var counts = p.Cast().Select(param => + { + int i = 0; + foreach (var item in param) + i++; + return i; + }) + .Distinct(); + + // All parameters should have the same length + if (counts.Count() > 1) + throw new NoValueAvailableException(); + + var values = p + .Cast() + .Select(range => + { + var results = new List(); + foreach (var c in range) + { + if (c.IsNumber()) + results.Add(c.CastTo()); + else + results.Add(0.0); + } + return results; + }) + .ToArray(); + + return Enumerable.Range(0, counts.Single()) + .Aggregate(0d, (t, i) => + t + values.Aggregate(1d, + (product, list) => product * list[i] + ) + ); + } + + private static object Tan(List p) + { + return Math.Tan(p[0]); + } + + private static object Tanh(List p) + { + return Math.Tanh(p[0]); + } + + private static object Trunc(List p) + { + var number = (double)p[0]; + + var num_digits = 0d; + if (p.Count > 1) + num_digits = (double)p[1]; + + var scaling = Math.Pow(10, num_digits); + + var truncated = (int)(number * scaling); + return (double)truncated / scaling; + } + + public static double DegreesToRadians(double degrees) + { + return (Math.PI / 180.0) * degrees; + } + + public static double RadiansToDegrees(double radians) + { + return (180.0 / Math.PI) * radians; + } + + public static double GradsToRadians(double grads) + { + return (grads / 200.0) * Math.PI; + } + + public static double RadiansToGrads(double radians) + { + return (radians / Math.PI) * 200.0; + } + + public static double DegreesToGrads(double degrees) + { + return (degrees / 9.0) * 10.0; + } + + public static double GradsToDegrees(double grads) + { + return (grads / 10.0) * 9.0; + } + + public static double ASinh(double x) + { + return (Math.Log(x + Math.Sqrt(x * x + 1.0))); + } + + private static object Acosh(List p) + { + double number = p[0]; + if (number < 1) + throw new NumberException(); + + return XLMath.ACosh(p[0]); + } + + private static object Acot(List p) + { + double x = Math.Atan(1.0 / p[0]); + + // Acot in Excel calculates the modulus of the function above. + // as the % operator is not the modulus, but the remainder, we have to calculate the modulus by hand: + while (x < 0) + x = x + Math.PI; + + return x; + } + + private static object Acoth(List p) + { + double number = p[0]; + if (Math.Abs(number) < 1) + throw new NumberException(); + + return 0.5 * Math.Log((number + 1) / (number - 1)); + } + + private static object Arabic(List p) + { + string input = ((string)p[0]).Trim(); + + try + { + if (input.Length == 0) + return 0; + if (input == "-") + throw new NumberException(); + else if (input[0] == '-') + return -XLMath.RomanToArabic(input.Substring(1)); + else + return XLMath.RomanToArabic(input); + } + catch (ArgumentOutOfRangeException) + { + throw new CellValueException(); + } + catch + { + throw; + } + } + + private static object Asinh(List p) + { + return XLMath.ASinh(p[0]); + } + + private static object Atanh(List p) + { + double input = p[0]; + if (Math.Abs(input) >= 1) + throw new NumberException(); + + return XLMath.ATanh(p[0]); + } + + private static object Base(List p) + { + long number; + int radix; + int minLength = 0; + + var rawNumber = p[0].Evaluate(); + if (rawNumber is long || rawNumber is int || rawNumber is byte || rawNumber is double || rawNumber is float) + number = Convert.ToInt64(rawNumber); + else + throw new CellValueException(); + + var rawRadix = p[1].Evaluate(); + if (rawRadix is long || rawRadix is int || rawRadix is byte || rawRadix is double || rawRadix is float) + radix = Convert.ToInt32(rawRadix); + else + throw new CellValueException(); + + if (p.Count > 2) + { + var rawMinLength = p[2].Evaluate(); + if (rawMinLength is long || rawMinLength is int || rawMinLength is byte || rawMinLength is double || rawMinLength is float) + minLength = Convert.ToInt32(rawMinLength); + else + throw new CellValueException(); + } + + if (number < 0 || radix < 2 || radix > 36) + throw new NumberException(); + + return XLMath.ChangeBase(number, radix).PadLeft(minLength, '0'); + } + + private static object Combin(List p) + { + Int32 n; + Int32 k; + + var rawN = p[0].Evaluate(); + var rawK = p[1].Evaluate(); + if (rawN is long || rawN is int || rawN is byte || rawN is double || rawN is float) + n = (int)Math.Floor((double)rawN); + else + throw new NumberException(); + + if (rawK is long || rawK is int || rawK is byte || rawK is double || rawK is float) + k = (int)Math.Floor((double)rawK); + else + throw new NumberException(); + + n = (int)p[0]; + k = (int)p[1]; + + if (n < 0 || n < k || k < 0) + throw new NumberException(); + + return XLMath.Combin(n, k); + } + + private static object CombinA(List p) + { + Int32 number = (int)p[0]; // casting truncates towards 0 as specified + Int32 chosen = (int)p[1]; + + if (number < 0 || number < chosen) + throw new NumberException(); + if (chosen < 0) + throw new NumberException(); + + int n = number + chosen - 1; + int k = number - 1; + + return n == k || k == 0 + ? 1 + : (long)XLMath.Combin(n, k); + } + + private static object Degrees(List p) + { + return p[0] * (180.0 / Math.PI); + } + + private static object Fact(List p) + { + var input = p[0].Evaluate(); + + if (!(input is long || input is int || input is byte || input is double || input is float)) + throw new CellValueException(); + + var num = Math.Floor((double)input); + double fact = 1.0; + + if (num < 0) + throw new NumberException(); + + if (num > 1) + for (int i = 2; i <= num; i++) + fact *= i; + return fact; + } + + private static object FactDouble(List p) + { + var input = p[0].Evaluate(); + + if (!(input is long || input is int || input is byte || input is double || input is float)) + throw new CellValueException(); + + var num = Math.Floor(p[0]); + double fact = 1.0; + + if (num < -1) + throw new NumberException(); + + if (num > 1) + { + var start = Math.Abs(num % 2) < XLHelper.Epsilon ? 2 : 1; + for (int i = start; i <= num; i = i + 2) + fact *= i; + } + return fact; + } + + private static object Gcd(List p) + { + return p.Select(v => (int)v).Aggregate(Gcd); + } + + private static int Gcd(int a, int b) + { + return b == 0 ? a : Gcd(b, a % b); + } + + private static object Lcm(List p) + { + return p.Select(v => (int)v).Aggregate(Lcm); + } + + private static int Lcm(int a, int b) + { + if (a == 0 || b == 0) return 0; + return a * (b / Gcd(a, b)); + } + + private static object Mod(List p) + { + double number = p[0]; + double divisor = p[1]; + + return number - Math.Floor(number / divisor) * divisor; + } + + private static object MRound(List p) + { + var number = (Double)p[0]; + var multiple = (Double)p[1]; + + if (Math.Sign(number) != Math.Sign(multiple)) + throw new NumberException($"The Number and Multiple arguments must have the same sign."); + + return Math.Round(number / multiple, MidpointRounding.AwayFromZero) * multiple; + } + + private static object Multinomial(List p) + { + return Multinomial(p.Select(v => (double)v).ToList()); + } + + private static double Multinomial(List numbers) + { + double numbersSum = 0; + foreach (var number in numbers) + numbersSum += number; + + double maxNumber = numbers.Max(); + var denomFactorPowers = new double[(uint)numbers.Max() + 1]; + foreach (var number in numbers) + for (int i = 2; i <= number; i++) + denomFactorPowers[i]++; + for (int i = 2; i < denomFactorPowers.Length; i++) + denomFactorPowers[i]--; // reduce with nominator; + + int currentFactor = 2; + double currentPower = 1; + double result = 1; + for (double i = maxNumber + 1; i <= numbersSum; i++) + { + double tempDenom = 1; + while (tempDenom < result && currentFactor < denomFactorPowers.Length) + { + if (currentPower > denomFactorPowers[currentFactor]) + { + currentFactor++; + currentPower = 1; + } + else + { + tempDenom *= currentFactor; + currentPower++; + } + } + result = result / tempDenom * i; + } + + return result; + } + + private static object Odd(List p) + { + var num = (int)Math.Ceiling(p[0]); + var addValue = num >= 0 ? 1 : -1; + return XLMath.IsOdd(num) ? num : num + addValue; + } + + private static object Even(List p) + { + var num = (int)Math.Ceiling(p[0]); + var addValue = num >= 0 ? 1 : -1; + return XLMath.IsEven(num) ? num : num + addValue; + } + + private static object Product(List p) + { + if (p.Count == 0) return 0; + Double total = 1; + p.ForEach(v => total *= v); + return total; + } + + private static object Quotient(List p) + { + Double n = p[0]; + Double k = p[1]; + + return (int)(n / k); + } + + private static object Radians(List p) + { + return p[0] * Math.PI / 180.0; + } + + private static object Roman(List p) + { + if (p.Count == 1 + || (Boolean.TryParse(p[1]._token.Value.ToString(), out bool boolTemp) && boolTemp) + || (Int32.TryParse(p[1]._token.Value.ToString(), out int intTemp) && intTemp == 1)) + return XLMath.ToRoman((int)p[0]); + + throw new ArgumentException("Can only support classic roman types."); + } + + private static object Round(List p) + { + var value = (Double)p[0]; + var digits = (Int32)(Double)p[1]; + if (digits >= 0) + { + return Math.Round(value, digits, MidpointRounding.AwayFromZero); + } + else + { + digits = Math.Abs(digits); + double temp = value / Math.Pow(10, digits); + temp = Math.Round(temp, 0, MidpointRounding.AwayFromZero); + return temp * Math.Pow(10, digits); + } + } + + private static object RoundDown(List p) + { + var value = (Double)p[0]; + var digits = (Int32)(Double)p[1]; + + if (value >= 0) + return Math.Floor(value * Math.Pow(10, digits)) / Math.Pow(10, digits); + + return Math.Ceiling(value * Math.Pow(10, digits)) / Math.Pow(10, digits); + } + + private static object RoundUp(List p) + { + var value = (Double)p[0]; + var digits = (Int32)(Double)p[1]; + + if (value >= 0) + return Math.Ceiling(value * Math.Pow(10, digits)) / Math.Pow(10, digits); + + return Math.Floor(value * Math.Pow(10, digits)) / Math.Pow(10, digits); + } + + private static object Sec(List p) + { + if (double.TryParse(p[0], out double number)) + return 1.0 / Math.Cos(number); + else + throw new CellValueException(); + } + + private static object Sech(List p) + { + return 1.0 / Math.Cosh(p[0]); + } + + private static object SeriesSum(List p) + { + var x = (Double)p[0]; + var n = (Double)p[1]; + var m = (Double)p[2]; + var obj = p[3] as XObjectExpression; + + if (obj == null) + return p[3] * Math.Pow(x, n); + + Double total = 0; + Int32 i = 0; + foreach (var e in obj) + { + total += (double)e * Math.Pow(x, n + i * m); + i++; + } + + return total; + } + + private static object SqrtPi(List p) + { + var num = (Double)p[0]; + return Math.Sqrt(Math.PI * num); + } + + private static object Subtotal(List p) + { + // Skip cells that already evaluate a SUBTOTAL + bool hasSubtotalInFormula(Expression e) + { + if (e is FunctionExpression fe && (fe.FunctionDefinition.Function.Method.Name == nameof(Subtotal) || fe.Parameters.Any(fp => hasSubtotalInFormula(fp)))) + return true; + + if (e is BinaryExpression be) + return hasSubtotalInFormula(be.LeftExpression) || hasSubtotalInFormula(be.RightExpression); + + if (e is UnaryExpression ue) + return hasSubtotalInFormula(ue.Expression); + + return false; + }; + + IEnumerable extractExpressionsWithoutSubtotal(CellRangeReference crr) + { + var ce = crr.CalcEngine as XLCalcEngine; + + return crr.Range + .CellsUsed() + .Where(c => + { + if (c.HasFormula) + { + var expression = ce.ExpressionCache[c.FormulaA1]; + return !hasSubtotalInFormula(expression); + } + else + return true; + }) + .Select(c => new XObjectExpression(new CellRangeReference(c.AsRange(), (XLCalcEngine)crr.CalcEngine)) as Expression); + }; + + var expressions = p.Skip(1) + .SelectMany(e => + e is XObjectExpression xoe && xoe.Value is CellRangeReference crr + ? extractExpressionsWithoutSubtotal(crr) + : new[] { e }) + .ToArray(); + + var fId = (int)(Double)p[0]; + var tally = new Tally(expressions); + + switch (fId) + { + case 1: + return tally.Average(); + + case 2: + return tally.Count(true); + + case 3: + return tally.Count(false); + + case 4: + return tally.Max(); + + case 5: + return tally.Min(); + + case 6: + return tally.Product(); + + case 7: + return tally.Std(); + + case 8: + return tally.StdP(); + + case 9: + return tally.Sum(); + + case 10: + return tally.Var(); + + case 11: + return tally.VarP(); + + default: + throw new ArgumentException("Function not supported."); + } + } + + private static object SumSq(List p) + { + var t = new Tally(p); + return t.NumericValues().Sum(v => Math.Pow(v, 2)); + } + + private static object MMult(List p) + { + Double[,] A = GetArray(p[0]); + Double[,] B = GetArray(p[1]); + + if (A.GetLength(0) != B.GetLength(0) || A.GetLength(1) != B.GetLength(1)) + throw new ArgumentException("Ranges must have the same number of rows and columns."); + + var C = new double[A.GetLength(0), A.GetLength(1)]; + for (int i = 0; i < A.GetLength(0); i++) + { + for (int j = 0; j < B.GetLength(1); j++) + { + for (int k = 0; k < A.GetLength(1); k++) + { + C[i, j] += A[i, k] * B[k, j]; + } + } + } + + return C; + } + + private static double[,] GetArray(Expression expression) + { + var oExp1 = expression as XObjectExpression; + if (oExp1 == null) return new[,] { { (Double)expression } }; + + var range = (oExp1.Value as CellRangeReference).Range; + var rowCount = range.RowCount(); + var columnCount = range.ColumnCount(); + var arr = new double[rowCount, columnCount]; + + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < columnCount; column++) + { + arr[row, column] = range.Cell(row + 1, column + 1).GetDouble(); + } + } + + return arr; + } + + private static object MDeterm(List p) + { + var arr = GetArray(p[0]); + var m = new XLMatrix(arr); + + return m.Determinant(); + } + + private static object MInverse(List p) + { + var arr = GetArray(p[0]); + var m = new XLMatrix(arr); + + return m.Invert().mat; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs b/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs new file mode 100644 index 0000000..28eb388 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs @@ -0,0 +1,276 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.CalcEngine +{ + internal static class Statistical + { + public static void Register(CalcEngine ce) + { + //ce.RegisterFunction("AVEDEV", AveDev, 1, int.MaxValue); + ce.RegisterFunction("AVERAGE", 1, int.MaxValue, Average); // Returns the average (arithmetic mean) of the arguments + ce.RegisterFunction("AVERAGEA", 1, int.MaxValue, AverageA); + //BETADIST Returns the beta cumulative distribution function + //BETAINV Returns the inverse of the cumulative distribution function for a specified beta distribution + //BINOMDIST Returns the individual term binomial distribution probability + //CHIDIST Returns the one-tailed probability of the chi-squared distribution + //CHIINV Returns the inverse of the one-tailed probability of the chi-squared distribution + //CHITEST Returns the test for independence + //CONFIDENCE Returns the confidence interval for a population mean + //CORREL Returns the correlation coefficient between two data sets + ce.RegisterFunction("COUNT", 1, int.MaxValue, Count); + ce.RegisterFunction("COUNTA", 1, int.MaxValue, CountA); + ce.RegisterFunction("COUNTBLANK", 1, CountBlank); + ce.RegisterFunction("COUNTIF", 2, CountIf); + ce.RegisterFunction("COUNTIFS", 2, 144, CountIfs); + //COVAR Returns covariance, the average of the products of paired deviations + //CRITBINOM Returns the smallest value for which the cumulative binomial distribution is less than or equal to a criterion value + //DEVSQ Returns the sum of squares of deviations + //EXPONDIST Returns the exponential distribution + //FDIST Returns the F probability distribution + //FINV Returns the inverse of the F probability distribution + //FISHER Returns the Fisher transformation + //FISHERINV Returns the inverse of the Fisher transformation + //FORECAST Returns a value along a linear trend + //FREQUENCY Returns a frequency distribution as a vertical array + //FTEST Returns the result of an F-test + //GAMMADIST Returns the gamma distribution + //GAMMAINV Returns the inverse of the gamma cumulative distribution + //GAMMALN Returns the natural logarithm of the gamma function, Γ(x) + //GEOMEAN Returns the geometric mean + //GROWTH Returns values along an exponential trend + //HARMEAN Returns the harmonic mean + //HYPGEOMDIST Returns the hypergeometric distribution + //INTERCEPT Returns the intercept of the linear regression line + //KURT Returns the kurtosis of a data set + //LARGE Returns the k-th largest value in a data set + //LINEST Returns the parameters of a linear trend + //LOGEST Returns the parameters of an exponential trend + //LOGINV Returns the inverse of the lognormal distribution + //LOGNORMDIST Returns the cumulative lognormal distribution + ce.RegisterFunction("MAX", 1, int.MaxValue, Max); + ce.RegisterFunction("MAXA", 1, int.MaxValue, MaxA); + //MEDIAN Returns the median of the given numbers + ce.RegisterFunction("MIN", 1, int.MaxValue, Min); + ce.RegisterFunction("MINA", 1, int.MaxValue, MinA); + //MODE Returns the most common value in a data set + //NEGBINOMDIST Returns the negative binomial distribution + //NORMDIST Returns the normal cumulative distribution + //NORMINV Returns the inverse of the normal cumulative distribution + //NORMSDIST Returns the standard normal cumulative distribution + //NORMSINV Returns the inverse of the standard normal cumulative distribution + //PEARSON Returns the Pearson product moment correlation coefficient + //PERCENTILE Returns the k-th percentile of values in a range + //PERCENTRANK Returns the percentage rank of a value in a data set + //PERMUT Returns the number of permutations for a given number of objects + //POISSON Returns the Poisson distribution + //PROB Returns the probability that values in a range are between two limits + //QUARTILE Returns the quartile of a data set + //RANK Returns the rank of a number in a list of numbers + //RSQ Returns the square of the Pearson product moment correlation coefficient + //SKEW Returns the skewness of a distribution + //SLOPE Returns the slope of the linear regression line + //SMALL Returns the k-th smallest value in a data set + //STANDARDIZE Returns a normalized value + ce.RegisterFunction("STDEV", 1, int.MaxValue, StDev); + ce.RegisterFunction("STDEVA", 1, int.MaxValue, StDevA); + ce.RegisterFunction("STDEVP", 1, int.MaxValue, StDevP); + ce.RegisterFunction("STDEVPA", 1, int.MaxValue, StDevPA); + ce.RegisterFunction("STDEV.S", 1, int.MaxValue, StDev); + ce.RegisterFunction("STDEV.P", 1, int.MaxValue, StDevP); + //STEYX Returns the standard error of the predicted y-value for each x in the regression + //TDIST Returns the Student's t-distribution + //TINV Returns the inverse of the Student's t-distribution + //TREND Returns values along a linear trend + //TRIMMEAN Returns the mean of the interior of a data set + //TTEST Returns the probability associated with a Student's t-test + ce.RegisterFunction("VAR", 1, int.MaxValue, Var); + ce.RegisterFunction("VARA", 1, int.MaxValue, VarA); + ce.RegisterFunction("VARP", 1, int.MaxValue, VarP); + ce.RegisterFunction("VARPA", 1, int.MaxValue, VarPA); + ce.RegisterFunction("VAR.S", 1, int.MaxValue, Var); + ce.RegisterFunction("VAR.P", 1, int.MaxValue, VarP); + //WEIBULL Returns the Weibull distribution + //ZTEST Returns the one-tailed probability-value of a z-test + } + + private static object Average(List p) + { + return GetTally(p, true).Average(); + } + + private static object AverageA(List p) + { + return GetTally(p, false).Average(); + } + + private static object Count(List p) + { + return GetTally(p, true).Count(); + } + + private static object CountA(List p) + { + return GetTally(p, false).Count(); + } + + private static object CountBlank(List p) + { + if ((p[0] as XObjectExpression)?.Value as CellRangeReference == null) + throw new NoValueAvailableException("COUNTBLANK should have a single argument which is a range reference"); + + var e = p[0] as XObjectExpression; + long totalCount = CalcEngineHelpers.GetTotalCellsCount(e); + long nonBlankCount = 0; + foreach (var value in e) + { + if (!CalcEngineHelpers.ValueIsBlank(value)) + nonBlankCount++; + } + + return 0d + totalCount - nonBlankCount; + } + + private static object CountIf(List p) + { + CalcEngine ce = new CalcEngine(); + var cnt = 0.0; + long processedCount = 0; + if (p[0] is XObjectExpression ienum) + { + long totalCount = CalcEngineHelpers.GetTotalCellsCount(ienum); + var criteria = p[1].Evaluate(); + foreach (var value in ienum) + { + if (CalcEngineHelpers.ValueSatisfiesCriteria(value, criteria, ce)) + cnt++; + processedCount++; + } + + // Add count of empty cells outside the used range if they match criteria + if (CalcEngineHelpers.ValueSatisfiesCriteria(string.Empty, criteria, ce)) + cnt += (totalCount - processedCount); + } + + return cnt; + } + + private static object CountIfs(List p) + { + // get parameters + var ce = new CalcEngine(); + long count = 0; + + int numberOfCriteria = p.Count / 2; + + long totalCount = 0; + // prepare criteria-parameters: + var criteriaRanges = new Tuple>[numberOfCriteria]; + for (int criteriaPair = 0; criteriaPair < numberOfCriteria; criteriaPair++) + { + var criteriaRange = p[criteriaPair * 2] as XObjectExpression; + var criterion = p[(criteriaPair * 2) + 1].Evaluate(); + var criteriaRangeValues = new List(); + foreach (var value in criteriaRange) + { + criteriaRangeValues.Add(value); + } + + criteriaRanges[criteriaPair] = new Tuple>( + criterion, + criteriaRangeValues); + + if (totalCount == 0) + totalCount = CalcEngineHelpers.GetTotalCellsCount(criteriaRange); + } + + long processedCount = 0; + for (var i = 0; i < criteriaRanges[0].Item2.Count; i++) + { + if (criteriaRanges.All(criteriaPair => CalcEngineHelpers.ValueSatisfiesCriteria( + criteriaPair.Item2[i], criteriaPair.Item1, ce))) + count++; + + processedCount++; + } + + // Add count of empty cells outside the used range if they match criteria + if (criteriaRanges.All(criteriaPair => CalcEngineHelpers.ValueSatisfiesCriteria( + string.Empty, criteriaPair.Item1, ce))) + { + count += (totalCount - processedCount); + } + + // done + return count; + } + + private static object Max(List p) + { + return GetTally(p, true).Max(); + } + + private static object MaxA(List p) + { + return GetTally(p, false).Max(); + } + + private static object Min(List p) + { + return GetTally(p, true).Min(); + } + + private static object MinA(List p) + { + return GetTally(p, false).Min(); + } + + private static object StDev(List p) + { + return GetTally(p, true).Std(); + } + + private static object StDevA(List p) + { + return GetTally(p, false).Std(); + } + + private static object StDevP(List p) + { + return GetTally(p, true).StdP(); + } + + private static object StDevPA(List p) + { + return GetTally(p, false).StdP(); + } + + private static object Var(List p) + { + return GetTally(p, true).Var(); + } + + private static object VarA(List p) + { + return GetTally(p, false).Var(); + } + + private static object VarP(List p) + { + return GetTally(p, true).VarP(); + } + + private static object VarPA(List p) + { + return GetTally(p, false).VarP(); + } + + // utility for tallying statistics + private static Tally GetTally(List p, bool numbersOnly) + { + return new Tally(p, numbersOnly); + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Tally.cs b/ClosedXML/Excel/CalcEngine/Functions/Tally.cs new file mode 100644 index 0000000..8a881c8 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Tally.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace ClosedXML.Excel.CalcEngine +{ + internal class Tally : IEnumerable + { + private readonly List _list = new List(); + private readonly bool NumbersOnly; + + private double[] _numericValues; + + public Tally() + : this(false) + { } + + public Tally(bool numbersOnly) + : this(null, numbersOnly) + { } + + public Tally(IEnumerable p) + : this(p, false) + { } + + public Tally(IEnumerable p, bool numbersOnly) + { + if (p != null) + { + foreach (var e in p) + { + Add(e); + } + } + + NumbersOnly = numbersOnly; + } + + public void Add(Expression e) + { + // handle enumerables + if (e is IEnumerable ienum) + { + foreach (var value in ienum) + { + _list.Add(value); + } + _numericValues = null; + return; + } + + // handle expressions + var val = e.Evaluate(); + if (val is string || !(val is IEnumerable valEnumerable)) + _list.Add(val); + else + foreach (var v in valEnumerable) + _list.Add(v); + + _numericValues = null; + } + + public void AddValue(Object v) + { + _list.Add(v); + _numericValues = null; + } + + public double Count() + { + return Count(NumbersOnly); + } + + public double Count(bool numbersOnly) + { + if (numbersOnly) + return NumericValuesInternal().Length; + else + return _list.Count(o => !CalcEngineHelpers.ValueIsBlank(o)); + } + + private IEnumerable NumericValuesEnumerable() + { + foreach (var value in _list) + { + if (value is string || !(value is IEnumerable vEnumerable)) + { + if (TryParseToDouble(value, aggressiveConversion: false, out double tmp)) + yield return tmp; + } + else + { + foreach (var v in vEnumerable) + { + if (TryParseToDouble(v, aggressiveConversion: false, out double tmp)) + yield return tmp; + } + } + } + } + + // If aggressiveConversion == true, then try to parse non-numeric types to double too + private bool TryParseToDouble(object value, bool aggressiveConversion, out double d) + { + d = 0; + if (value.IsNumber()) + { + d = Convert.ToDouble(value); + return true; + } + else if (value is Boolean b) + { + if (!aggressiveConversion) return false; + + d = (b ? 1 : 0); + return true; + } + else if (value is DateTime dt) + { + d = dt.ToOADate(); + return true; + } + else if (value is TimeSpan ts) + { + d = ts.TotalDays; + return true; + } + else if (value is string s) + { + if (!aggressiveConversion) return false; + return double.TryParse(s, out d); + } + + return false; + } + + private double[] NumericValuesInternal() + => LazyInitializer.EnsureInitialized(ref _numericValues, () => NumericValuesEnumerable().ToArray()); + + public IEnumerable NumericValues() + => NumericValuesInternal().AsEnumerable(); + + public double Product() + { + var nums = NumericValuesInternal(); + return nums.Length == 0 + ? 0 + : nums.Aggregate(1d, (a, b) => a * b); + } + + public double Sum() => NumericValuesInternal().Sum(); + + public double Average() + { + var nums = NumericValuesInternal(); + if (nums.Length == 0) throw new ApplicationException("No values"); + return nums.Average(); + } + + public double Min() + { + var nums = NumericValuesInternal(); + return nums.Length == 0 ? 0 : nums.Min(); + } + + public double Max() + { + var nums = NumericValuesInternal(); + return nums.Length == 0 ? 0 : nums.Max(); + } + + public double Range() => Max() - Min(); + + private static double Sum2(IEnumerable nums) + { + return nums.Sum(d => d * d); + } + + public double VarP() + { + var nums = NumericValuesInternal(); + var avg = nums.Average(); + var sum2 = Sum2(nums); + var count = nums.Length; + return count <= 1 ? 0 : sum2 / count - avg * avg; + } + + public double StdP() + { + var nums = NumericValuesInternal(); + var avg = nums.Average(); + var sum2 = nums.Sum(d => d * d); + var count = nums.Length; + return count <= 1 ? 0 : Math.Sqrt(sum2 / count - avg * avg); + } + + public double Var() + { + var nums = NumericValuesInternal(); + var avg = nums.Average(); + var sum2 = Sum2(nums); + var count = nums.Length; + return count <= 1 ? 0 : (sum2 / count - avg * avg) * count / (count - 1); + } + + public double Std() + { + var values = NumericValuesInternal(); + var count = values.Length; + double ret = 0; + if (count != 0) + { + //Compute the Average + double avg = values.Average(); + //Perform the Sum of (value-avg)_2_2 + double sum = values.Sum(d => Math.Pow(d - avg, 2)); + //Put it all together + ret = Math.Sqrt((sum) / (count - 1)); + } + else + { + throw new ApplicationException("No values"); + } + return ret; + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Text.cs b/ClosedXML/Excel/CalcEngine/Functions/Text.cs new file mode 100644 index 0000000..13d1a67 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Text.cs @@ -0,0 +1,479 @@ +using ClosedXML.Excel.CalcEngine.Exceptions; +using ExcelNumberFormat; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel.CalcEngine +{ + internal static class Text + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("ASC", 1, Asc); // Changes full-width (double-byte) English letters or katakana within a character string to half-width (single-byte) characters + //ce.RegisterFunction("BAHTTEXT Converts a number to text, using the ß (baht) currency format + ce.RegisterFunction("CHAR", 1, _Char); // Returns the character specified by the code number + ce.RegisterFunction("CLEAN", 1, Clean); // Removes all nonprintable characters from text + ce.RegisterFunction("CODE", 1, Code); // Returns a numeric code for the first character in a text string + ce.RegisterFunction("CONCAT", 1, int.MaxValue, Concat); // Joins several text items into one text item + ce.RegisterFunction("CONCATENATE", 1, int.MaxValue, Concatenate); // Joins several text items into one text item + ce.RegisterFunction("DOLLAR", 1, 2, Dollar); // Converts a number to text, using the $ (dollar) currency format + ce.RegisterFunction("EXACT", 2, Exact); // Checks to see if two text values are identical + ce.RegisterFunction("FIND", 2, 3, Find); //Finds one text value within another (case-sensitive) + ce.RegisterFunction("FIXED", 1, 3, Fixed); // Formats a number as text with a fixed number of decimals + //ce.RegisterFunction("JIS Changes half-width (single-byte) English letters or katakana within a character string to full-width (double-byte) characters + ce.RegisterFunction("LEFT", 1, 2, Left); // LEFTB Returns the leftmost characters from a text value + ce.RegisterFunction("LEN", 1, Len); //, Returns the number of characters in a text string + ce.RegisterFunction("LOWER", 1, Lower); // Converts text to lowercase + ce.RegisterFunction("MID", 3, Mid); // Returns a specific number of characters from a text string starting at the position you specify + ce.RegisterFunction("NUMBERVALUE", 1, 3, NumberValue); // Converts a text argument to a number + //ce.RegisterFunction("PHONETIC Extracts the phonetic (furigana) characters from a text string + ce.RegisterFunction("PROPER", 1, Proper); // Capitalizes the first letter in each word of a text value + ce.RegisterFunction("REPLACE", 4, Replace); // Replaces characters within text + ce.RegisterFunction("REPT", 2, Rept); // Repeats text a given number of times + ce.RegisterFunction("RIGHT", 1, 2, Right); // Returns the rightmost characters from a text value + ce.RegisterFunction("SEARCH", 2, 3, Search); // Finds one text value within another (not case-sensitive) + ce.RegisterFunction("SUBSTITUTE", 3, 4, Substitute); // Substitutes new text for old text in a text string + ce.RegisterFunction("T", 1, T); // Converts its arguments to text + ce.RegisterFunction("TEXT", 2, _Text); // Formats a number and converts it to text + ce.RegisterFunction("TEXTJOIN", 3, 254, TextJoin); // Joins text via delimiter + ce.RegisterFunction("TRIM", 1, Trim); // Removes spaces from text + ce.RegisterFunction("UPPER", 1, Upper); // Converts text to uppercase + ce.RegisterFunction("VALUE", 1, Value); // Converts a text argument to a number + } + + private static object _Char(List p) + { + var i = (int)p[0]; + if (i < 1 || i > 255) + throw new CellValueException(string.Format("The number {0} is out of the required range (1 to 255)", i)); + + var c = (char)i; + return c.ToString(); + } + + private static object Code(List p) + { + var s = (string)p[0]; + return (int)s[0]; + } + + private static object Concat(List p) + { + var sb = new StringBuilder(); + foreach (var x in p) + { + if (x is IEnumerable enumerable) + { + foreach (var i in enumerable) + sb.Append((string)(new Expression(i))); + } + else + sb.Append((string)x); + } + return sb.ToString(); + } + + private static object Concatenate(List p) + { + var sb = new StringBuilder(); + foreach (var x in p) + { + if (x is XObjectExpression objectExpression) + { + if (objectExpression.Value is CellRangeReference cellRangeReference) + { + if (!cellRangeReference.Range.RangeAddress.IsValid) + throw new CellReferenceException(); + + // Only single cell range references allows at this stage. See unit test for more details + if (cellRangeReference.Range.RangeAddress.NumberOfCells > 1) + throw new CellValueException("This function does not accept cell ranges as parameters."); + } + else + // I'm unsure about what else objectExpression.Value could be, but let's throw CellReferenceException + throw new CellReferenceException(); + } + + sb.Append((string)x); + } + return sb.ToString(); + } + + private static object Find(List p) + { + var srch = (string)p[0]; + var text = (string)p[1]; + var start = 0; + if (p.Count > 2) + { + start = (int)p[2] - 1; + } + var index = text.IndexOf(srch, start, StringComparison.Ordinal); + if (index == -1) + throw new ArgumentException("String not found."); + else + return index + 1; + } + + private static object Left(List p) + { + var str = (string)p[0]; + var n = 1; + if (p.Count > 1) + { + n = (int)p[1]; + } + if (n >= str.Length) return str; + + return str.Substring(0, n); + } + + private static object Len(List p) + { + return ((string)p[0]).Length; + } + + private static object Lower(List p) + { + return ((string)p[0]).ToLower(); + } + + private static object Mid(List p) + { + var str = (string)p[0]; + var start = (int)p[1] - 1; + var length = (int)p[2]; + if (start > str.Length - 1) + return String.Empty; + if (start + length > str.Length - 1) + return str.Substring(start); + return str.Substring(start, length); + } + + private static string MatchHandler(Match m) + { + return m.Groups[1].Value.ToUpper() + m.Groups[2].Value; + } + + private static object Proper(List p) + { + var s = (string)p[0]; + if (s.Length == 0) return ""; + + MatchEvaluator evaluator = new MatchEvaluator(MatchHandler); + StringBuilder sb = new StringBuilder(); + + string pattern = "\\b(\\w)(\\w+)?\\b"; + Regex regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.IgnoreCase); + return regex.Replace(s.ToLower(), evaluator); + } + + private static object Replace(List p) + { + // old start len new + var s = (string)p[0]; + var start = (int)p[1] - 1; + var len = (int)p[2]; + var rep = (string)p[3]; + + if (s.Length == 0) return rep; + + var sb = new StringBuilder(); + sb.Append(s.Substring(0, start)); + sb.Append(rep); + sb.Append(s.Substring(start + len)); + + return sb.ToString(); + } + + private static object Rept(List p) + { + var sb = new StringBuilder(); + var s = (string)p[0]; + var repeats = (int)p[1]; + if (repeats < 0) throw new IndexOutOfRangeException("repeats"); + for (int i = 0; i < repeats; i++) + { + sb.Append(s); + } + return sb.ToString(); + } + + private static object Right(List p) + { + var str = (string)p[0]; + var n = 1; + if (p.Count > 1) + { + n = (int)p[1]; + } + + if (n >= str.Length) return str; + + return str.Substring(str.Length - n); + } + + private static string WildcardToRegex(string pattern) + { + return Regex.Escape(pattern) + .Replace(".", "\\.") + .Replace("\\*", ".*") + .Replace("\\?", "."); + } + + private static object Search(List p) + { + var search = WildcardToRegex(p[0]); + var text = (string)p[1]; + + if ("" == text) throw new ArgumentException("Invalid input string."); + + var start = 0; + if (p.Count > 2) + { + start = (int)p[2] - 1; + } + + Regex r = new Regex(search, RegexOptions.Compiled | RegexOptions.IgnoreCase); + var match = r.Match(text.Substring(start)); + if (!match.Success) + throw new ArgumentException("Search failed."); + else + return match.Index + start + 1; + //var index = text.IndexOf(search, start, StringComparison.OrdinalIgnoreCase); + //if (index == -1) + // throw new ArgumentException("String not found."); + //else + // return index + 1; + } + + private static object Substitute(List p) + { + // get parameters + var text = (string)p[0]; + var oldText = (string)p[1]; + var newText = (string)p[2]; + + if ("" == text) return ""; + if ("" == oldText) return text; + + // if index not supplied, replace all + if (p.Count == 3) + { + return text.Replace(oldText, newText); + } + + // replace specific instance + int index = (int)p[3]; + if (index < 1) + { + throw new ArgumentException("Invalid index in Substitute."); + } + int pos = text.IndexOf(oldText); + while (pos > -1 && index > 1) + { + pos = text.IndexOf(oldText, pos + 1); + index--; + } + return pos > -1 + ? text.Substring(0, pos) + newText + text.Substring(pos + oldText.Length) + : text; + } + + private static object T(List p) + { + if (p[0]._token.Value.GetType() == typeof(string)) + return (string)p[0]; + else + return ""; + } + + private static object _Text(List p) + { + var value = p[0].Evaluate(); + + // Input values of type string don't get any formatting applied. + if (value is string) return value; + + var number = (double)p[0]; + var format = (string)p[1]; + if (string.IsNullOrEmpty(format.Trim())) return ""; + + var nf = new NumberFormat(format); + + if (nf.IsDateTimeFormat) + return nf.Format(DateTime.FromOADate(number), CultureInfo.InvariantCulture); + else + return nf.Format(number, CultureInfo.InvariantCulture); + } + + /// + /// A function to Join text https://support.office.com/en-us/article/textjoin-function-357b449a-ec91-49d0-80c3-0e8fc845691c + /// + /// Parameters + /// string + /// + /// Delimiter in first param must be a string + /// or + /// Second param must be a boolen (TRUE/FALSE) + /// + private static object TextJoin(List p) + { + var values = new List(); + string delimiter; + bool ignoreEmptyStrings; + try + { + delimiter = (string)p[0]; + ignoreEmptyStrings = (bool)p[1]; + } + catch (Exception e) + { + throw new CellValueException("Failed to parse arguments", e); + } + + foreach (var param in p.Skip(2)) + { + if (param is XObjectExpression tableArray) + { + if (!(tableArray.Value is CellRangeReference rangeReference)) + throw new NoValueAvailableException("tableArray has to be a range"); + + var range = rangeReference.Range; + IEnumerable cellValues; + if (ignoreEmptyStrings) + cellValues = range.CellsUsed() + .Select(c => c.GetString()) + .Where(s => !string.IsNullOrEmpty(s)); + else + cellValues = (range as XLRange).CellValues() + .Cast() + .Select(o => o.ToString()); + + values.AddRange(cellValues); + } + else + { + values.Add((string)param); + } + } + + var retVal = string.Join(delimiter, values); + + if (retVal.Length > 32767) + throw new CellValueException(); + + return retVal; + } + + private static object Trim(List p) + { + //Should not trim non breaking space + //See http://office.microsoft.com/en-us/excel-help/trim-function-HP010062581.aspx + return ((string)p[0]).Trim(' '); + } + + private static object Upper(List p) + { + return ((string)p[0]).ToUpper(); + } + + private static object Value(List p) + { + return double.Parse(p[0], NumberStyles.Any, CultureInfo.InvariantCulture); + } + + private static object NumberValue(List p) + { + var numberFormatInfo = new NumberFormatInfo(); + + numberFormatInfo.NumberDecimalSeparator = p.Count > 1 ? p[1] : CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator; + numberFormatInfo.CurrencyDecimalSeparator = numberFormatInfo.NumberDecimalSeparator; + + numberFormatInfo.NumberGroupSeparator = p.Count > 2 ? p[2] : CultureInfo.InvariantCulture.NumberFormat.NumberGroupSeparator; + numberFormatInfo.CurrencyGroupSeparator = numberFormatInfo.NumberGroupSeparator; + + if (numberFormatInfo.NumberDecimalSeparator == numberFormatInfo.NumberGroupSeparator) + { + throw new CellValueException("CurrencyDecimalSeparator and CurrencyGroupSeparator have to be different."); + } + + //Remove all whitespace characters + var input = Regex.Replace(p[0], @"\s+", "", RegexOptions.Compiled); + if (string.IsNullOrEmpty(input)) + { + return 0d; + } + + if (double.TryParse(input, NumberStyles.Any, numberFormatInfo, out var result)) + { + if (result <= -1e308 || result >= 1e308) + throw new CellValueException("The value is too large"); + + if (result >= -1e-309 && result <= 1e-309 && result != 0) + throw new CellValueException("The value is too tiny"); + + if (result >= -1e-308 && result <= 1e-308) + result = 0d; + + return result; + } + + throw new CellValueException("Could not convert the value to a number"); + } + + private static object Asc(List p) + { + return (string)p[0]; + } + + private static object Clean(List p) + { + var s = (string)p[0]; + + var result = new StringBuilder(); + foreach (var c in from c in s let b = (byte)c where b >= 32 select c) + { + result.Append(c); + } + return result.ToString(); + } + + private static object Dollar(List p) + { + Double value = p[0]; + int dec = p.Count == 2 ? (int)p[1] : 2; + + return value.ToString("C" + dec); + } + + private static object Exact(List p) + { + var t1 = (string)p[0]; + var t2 = (string)p[1]; + + return t1 == t2; + } + + private static object Fixed(List p) + { + if (p[0]._token.Value.GetType() == typeof(string)) + throw new ApplicationException("Input type can't be string"); + + Double value = p[0]; + int decimal_places = p.Count >= 2 ? (int)p[1] : 2; + Boolean no_commas = p.Count == 3 && p[2]; + + var retVal = value.ToString("N" + decimal_places); + if (no_commas) + return retVal.Replace(",", String.Empty); + else + return retVal; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/XLMath.cs b/ClosedXML/Excel/CalcEngine/Functions/XLMath.cs new file mode 100644 index 0000000..3adcaa5 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/XLMath.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + public static class XLMath + { + public static double DegreesToRadians(double degrees) + { + return (Math.PI / 180.0) * degrees; + } + + public static double RadiansToDegrees(double radians) + { + return (180.0 / Math.PI) * radians; + } + + public static double GradsToRadians(double grads) + { + return (grads / 200.0) * Math.PI; + } + + public static double RadiansToGrads(double radians) + { + return (radians / Math.PI) * 200.0; + } + + public static double DegreesToGrads(double degrees) + { + return (degrees / 9.0) * 10.0; + } + + public static double GradsToDegrees(double grads) + { + return (grads / 10.0) * 9.0; + } + + public static double ASinh(double x) + { + return (Math.Log(x + Math.Sqrt(x * x + 1.0))); + } + + public static double ACosh(double x) + { + return (Math.Log(x + Math.Sqrt((x * x) - 1.0))); + } + + public static double ATanh(double x) + { + return (Math.Log((1.0 + x) / (1.0 - x)) / 2.0); + } + + public static double ACoth(double x) + { + //return (Math.Log((x + 1.0) / (x - 1.0)) / 2.0); + return (ATanh(1.0 / x)); + } + + public static double ASech(double x) + { + return (ACosh(1.0 / x)); + } + + public static double ACsch(double x) + { + return (ASinh(1.0 / x)); + } + + public static double Sech(double x) + { + return (1.0 / Math.Cosh(x)); + } + + public static double Csch(double x) + { + return (1.0 / Math.Sinh(x)); + } + + public static double Coth(double x) + { + return (Math.Cosh(x) / Math.Sinh(x)); + } + + public static double Combin(Int32 n, Int32 k) + { + if (k == 0) return 1; + return n * Combin(n - 1, k - 1) / k; + } + + public static Boolean IsEven(Int32 value) + { + return Math.Abs(value % 2) == 0; + } + public static Boolean IsOdd(Int32 value) + { + return Math.Abs(value % 2) != 0; + } + + public static string ToRoman(int number) + { + if ((number < 0) || (number > 3999)) throw new ArgumentOutOfRangeException("insert value betwheen 1 and 3999"); + if (number < 1) return string.Empty; + if (number >= 1000) return "M" + ToRoman(number - 1000); + if (number >= 900) return "CM" + ToRoman(number - 900); + if (number >= 500) return "D" + ToRoman(number - 500); + if (number >= 400) return "CD" + ToRoman(number - 400); + if (number >= 100) return "C" + ToRoman(number - 100); + if (number >= 90) return "XC" + ToRoman(number - 90); + if (number >= 50) return "L" + ToRoman(number - 50); + if (number >= 40) return "XL" + ToRoman(number - 40); + if (number >= 10) return "X" + ToRoman(number - 10); + if (number >= 9) return "IX" + ToRoman(number - 9); + if (number >= 5) return "V" + ToRoman(number - 5); + if (number >= 4) return "IV" + ToRoman(number - 4); + if (number >= 1) return "I" + ToRoman(number - 1); + throw new ArgumentOutOfRangeException("something bad happened"); + } + + public static int RomanToArabic(string text) + { + if (text.Length == 0) + return 0; + if (text.StartsWith("M", StringComparison.InvariantCultureIgnoreCase)) + return 1000 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("CM", StringComparison.InvariantCultureIgnoreCase)) + return 900 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("D", StringComparison.InvariantCultureIgnoreCase)) + return 500 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("CD", StringComparison.InvariantCultureIgnoreCase)) + return 400 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("C", StringComparison.InvariantCultureIgnoreCase)) + return 100 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("XC", StringComparison.InvariantCultureIgnoreCase)) + return 90 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("L", StringComparison.InvariantCultureIgnoreCase)) + return 50 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("XL", StringComparison.InvariantCultureIgnoreCase)) + return 40 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("X", StringComparison.InvariantCultureIgnoreCase)) + return 10 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("IX", StringComparison.InvariantCultureIgnoreCase)) + return 9 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("V", StringComparison.InvariantCultureIgnoreCase)) + return 5 + RomanToArabic(text.Substring(1)); + if (text.StartsWith("IV", StringComparison.InvariantCultureIgnoreCase)) + return 4 + RomanToArabic(text.Substring(2)); + if (text.StartsWith("I", StringComparison.InvariantCultureIgnoreCase)) + return 1 + RomanToArabic(text.Substring(1)); + + throw new ArgumentOutOfRangeException("text is not a valid roman number"); + } + + public static string ChangeBase(long number, int radix) + { + if (number < 0) + throw new ArgumentOutOfRangeException("number must be greater or equal to 0"); + if (radix < 2) + throw new ArgumentOutOfRangeException("radix must be greater or equal to 2"); + if (radix > 36) + throw new ArgumentOutOfRangeException("radix must be smaller than or equal to 36"); + + StringBuilder sb = new StringBuilder(); + long remaining = number; + + if (remaining == 0) + { + sb.Insert(0, '0'); + } + + while (remaining > 0) + { + var nextDigitDecimal = remaining % radix; + remaining = remaining / radix; + + if (nextDigitDecimal < 10) + sb.Insert(0, nextDigitDecimal); + else + sb.Insert(0, (char)(nextDigitDecimal + 55)); + } + + return sb.ToString(); + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/XLMatrix.cs b/ClosedXML/Excel/CalcEngine/Functions/XLMatrix.cs new file mode 100644 index 0000000..79d6a70 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/XLMatrix.cs @@ -0,0 +1,594 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal class XLMatrix + { + public XLMatrix L; + public XLMatrix U; + public int cols; + private double detOfP = 1; + public double[,] mat; + private int[] pi; + public int rows; + + public XLMatrix(int iRows, int iCols) // XLMatrix Class constructor + { + rows = iRows; + cols = iCols; + mat = new double[rows,cols]; + } + public XLMatrix(Double[,] arr) + :this(arr.GetLength(0), arr.GetLength(1)) + { + var roCount = arr.GetLength(0); + var coCount = arr.GetLength(1); + for (int ro = 0; ro < roCount; ro++) + { + for (int co = 0; co < coCount; co++) + { + mat[ro, co] = arr[ro, co]; + } + } + } + + public double this[int iRow, int iCol] // Access this matrix as a 2D array + { + get { return mat[iRow, iCol]; } + set { mat[iRow, iCol] = value; } + } + + public Boolean IsSquare() + { + return (rows == cols); + } + + public XLMatrix GetCol(int k) + { + var m = new XLMatrix(rows, 1); + for (var i = 0; i < rows; i++) m[i, 0] = mat[i, k]; + return m; + } + + public void SetCol(XLMatrix v, int k) + { + for (var i = 0; i < rows; i++) mat[i, k] = v[i, 0]; + } + + public void MakeLU() // Function for LU decomposition + { + if (!IsSquare()) throw new InvalidOperationException("The matrix is not square!"); + L = IdentityMatrix(rows, cols); + U = Duplicate(); + + pi = new int[rows]; + for (var i = 0; i < rows; i++) pi[i] = i; + + var k0 = 0; + + for (var k = 0; k < cols - 1; k++) + { + double p = 0; + for (var i = k; i < rows; i++) // find the row with the biggest pivot + { + if (Math.Abs(U[i, k]) > p) + { + p = Math.Abs(U[i, k]); + k0 = i; + } + } + if (p == 0) + throw new InvalidOperationException("The matrix is singular!"); + + var pom1 = pi[k]; + pi[k] = pi[k0]; + pi[k0] = pom1; // switch two rows in permutation matrix + + double pom2; + for (var i = 0; i < k; i++) + { + pom2 = L[k, i]; + L[k, i] = L[k0, i]; + L[k0, i] = pom2; + } + + if (k != k0) detOfP *= -1; + + for (var i = 0; i < cols; i++) // Switch rows in U + { + pom2 = U[k, i]; + U[k, i] = U[k0, i]; + U[k0, i] = pom2; + } + + for (var i = k + 1; i < rows; i++) + { + L[i, k] = U[i, k]/U[k, k]; + for (var j = k; j < cols; j++) + U[i, j] = U[i, j] - L[i, k]*U[k, j]; + } + } + } + + + public XLMatrix SolveWith(XLMatrix v) // Function solves Ax = v in confirmity with solution vector "v" + { + if (rows != cols) throw new InvalidOperationException("The matrix is not square!"); + if (rows != v.rows) throw new ArgumentException("Wrong number of results in solution vector!"); + if (L == null) MakeLU(); + + var b = new XLMatrix(rows, 1); + for (var i = 0; i < rows; i++) b[i, 0] = v[pi[i], 0]; // switch two items in "v" due to permutation matrix + + var z = SubsForth(L, b); + var x = SubsBack(U, z); + + return x; + } + + public XLMatrix Invert() // Function returns the inverted matrix + { + if (L == null) MakeLU(); + + var inv = new XLMatrix(rows, cols); + + for (var i = 0; i < rows; i++) + { + var Ei = ZeroMatrix(rows, 1); + Ei[i, 0] = 1; + var col = SolveWith(Ei); + inv.SetCol(col, i); + } + return inv; + } + + + public double Determinant() // Function for determinant + { + if (L == null) MakeLU(); + var det = detOfP; + for (var i = 0; i < rows; i++) det *= U[i, i]; + return det; + } + + public XLMatrix GetP() // Function returns permutation matrix "P" due to permutation vector "pi" + { + if (L == null) MakeLU(); + + var matrix = ZeroMatrix(rows, cols); + for (var i = 0; i < rows; i++) matrix[pi[i], i] = 1; + return matrix; + } + + public XLMatrix Duplicate() // Function returns the copy of this matrix + { + var matrix = new XLMatrix(rows, cols); + for (var i = 0; i < rows; i++) + for (var j = 0; j < cols; j++) + matrix[i, j] = mat[i, j]; + return matrix; + } + + public static XLMatrix SubsForth(XLMatrix A, XLMatrix b) // Function solves Ax = b for A as a lower triangular matrix + { + if (A.L == null) A.MakeLU(); + var n = A.rows; + var x = new XLMatrix(n, 1); + + for (var i = 0; i < n; i++) + { + x[i, 0] = b[i, 0]; + for (var j = 0; j < i; j++) x[i, 0] -= A[i, j]*x[j, 0]; + x[i, 0] = x[i, 0]/A[i, i]; + } + return x; + } + + public static XLMatrix SubsBack(XLMatrix A, XLMatrix b) // Function solves Ax = b for A as an upper triangular matrix + { + if (A.L == null) A.MakeLU(); + var n = A.rows; + var x = new XLMatrix(n, 1); + + for (var i = n - 1; i > -1; i--) + { + x[i, 0] = b[i, 0]; + for (var j = n - 1; j > i; j--) x[i, 0] -= A[i, j]*x[j, 0]; + x[i, 0] = x[i, 0]/A[i, i]; + } + return x; + } + + public static XLMatrix ZeroMatrix(int iRows, int iCols) // Function generates the zero matrix + { + var matrix = new XLMatrix(iRows, iCols); + for (var i = 0; i < iRows; i++) + for (var j = 0; j < iCols; j++) + matrix[i, j] = 0; + return matrix; + } + + public static XLMatrix IdentityMatrix(int iRows, int iCols) // Function generates the identity matrix + { + var matrix = ZeroMatrix(iRows, iCols); + for (var i = 0; i < Math.Min(iRows, iCols); i++) + matrix[i, i] = 1; + return matrix; + } + + public static XLMatrix RandomMatrix(int iRows, int iCols, int dispersion) // Function generates the zero matrix + { + var random = new Random(); + var matrix = new XLMatrix(iRows, iCols); + for (var i = 0; i < iRows; i++) + for (var j = 0; j < iCols; j++) + matrix[i, j] = random.Next(-dispersion, dispersion); + return matrix; + } + + public static XLMatrix Parse(string ps) // Function parses the matrix from string + { + var s = NormalizeMatrixString(ps); + var rows = Regex.Split(s, "\r\n"); + var nums = rows[0].Split(' '); + var matrix = new XLMatrix(rows.Length, nums.Length); + try + { + for (var i = 0; i < rows.Length; i++) + { + nums = rows[i].Split(' '); + for (var j = 0; j < nums.Length; j++) matrix[i, j] = double.Parse(nums[j]); + } + } + catch (FormatException fe) + { + throw new FormatException("Wrong input format!", fe); + } + return matrix; + } + + public override string ToString() // Function returns matrix as a string + { + var s = ""; + for (var i = 0; i < rows; i++) + { + for (var j = 0; j < cols; j++) s += String.Format("{0,5:0.00}", mat[i, j]) + " "; + s += "\r\n"; + } + return s; + } + + public static XLMatrix Transpose(XLMatrix m) // XLMatrix transpose, for any rectangular matrix + { + var t = new XLMatrix(m.cols, m.rows); + for (var i = 0; i < m.rows; i++) + for (var j = 0; j < m.cols; j++) + t[j, i] = m[i, j]; + return t; + } + + public static XLMatrix Power(XLMatrix m, int pow) // Power matrix to exponent + { + if (pow == 0) return IdentityMatrix(m.rows, m.cols); + if (pow == 1) return m.Duplicate(); + if (pow == -1) return m.Invert(); + + XLMatrix x; + if (pow < 0) + { + x = m.Invert(); + pow *= -1; + } + else x = m.Duplicate(); + + var ret = IdentityMatrix(m.rows, m.cols); + while (pow != 0) + { + if ((pow & 1) == 1) ret *= x; + x *= x; + pow >>= 1; + } + return ret; + } + + private static void SafeAplusBintoC(XLMatrix A, int xa, int ya, XLMatrix B, int xb, int yb, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) // cols + { + C[i, j] = 0; + if (xa + j < A.cols && ya + i < A.rows) C[i, j] += A[ya + i, xa + j]; + if (xb + j < B.cols && yb + i < B.rows) C[i, j] += B[yb + i, xb + j]; + } + } + + private static void SafeAminusBintoC(XLMatrix A, int xa, int ya, XLMatrix B, int xb, int yb, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) // cols + { + C[i, j] = 0; + if (xa + j < A.cols && ya + i < A.rows) C[i, j] += A[ya + i, xa + j]; + if (xb + j < B.cols && yb + i < B.rows) C[i, j] -= B[yb + i, xb + j]; + } + } + + private static void SafeACopytoC(XLMatrix A, int xa, int ya, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) // cols + { + C[i, j] = 0; + if (xa + j < A.cols && ya + i < A.rows) C[i, j] += A[ya + i, xa + j]; + } + } + + private static void AplusBintoC(XLMatrix A, int xa, int ya, XLMatrix B, int xb, int yb, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) C[i, j] = A[ya + i, xa + j] + B[yb + i, xb + j]; + } + + private static void AminusBintoC(XLMatrix A, int xa, int ya, XLMatrix B, int xb, int yb, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) C[i, j] = A[ya + i, xa + j] - B[yb + i, xb + j]; + } + + private static void ACopytoC(XLMatrix A, int xa, int ya, XLMatrix C, int size) + { + for (var i = 0; i < size; i++) // rows + for (var j = 0; j < size; j++) C[i, j] = A[ya + i, xa + j]; + } + + private static XLMatrix StrassenMultiply(XLMatrix A, XLMatrix B) // Smart matrix multiplication + { + if (A.cols != B.rows) throw new ArgumentException("Wrong dimension of matrix!"); + + XLMatrix R; + + var msize = Math.Max(Math.Max(A.rows, A.cols), Math.Max(B.rows, B.cols)); + + if (msize < 32) + { + R = ZeroMatrix(A.rows, B.cols); + for (var i = 0; i < R.rows; i++) + for (var j = 0; j < R.cols; j++) + for (var k = 0; k < A.cols; k++) + R[i, j] += A[i, k]*B[k, j]; + return R; + } + + var size = 1; + var n = 0; + while (msize > size) + { + size *= 2; + n++; + } + + var h = size/2; + + + var mField = new XLMatrix[n,9]; + + /* + * 8x8, 8x8, 8x8, ... + * 4x4, 4x4, 4x4, ... + * 2x2, 2x2, 2x2, ... + * . . . + */ + + for (var i = 0; i < n - 4; i++) // rows + { + var z = (int) Math.Pow(2, n - i - 1); + for (var j = 0; j < 9; j++) mField[i, j] = new XLMatrix(z, z); + } + + SafeAplusBintoC(A, 0, 0, A, h, h, mField[0, 0], h); + SafeAplusBintoC(B, 0, 0, B, h, h, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 1], 1, mField); // (A11 + A22) * (B11 + B22); + + SafeAplusBintoC(A, 0, h, A, h, h, mField[0, 0], h); + SafeACopytoC(B, 0, 0, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 2], 1, mField); // (A21 + A22) * B11; + + SafeACopytoC(A, 0, 0, mField[0, 0], h); + SafeAminusBintoC(B, h, 0, B, h, h, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 3], 1, mField); //A11 * (B12 - B22); + + SafeACopytoC(A, h, h, mField[0, 0], h); + SafeAminusBintoC(B, 0, h, B, 0, 0, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 4], 1, mField); //A22 * (B21 - B11); + + SafeAplusBintoC(A, 0, 0, A, h, 0, mField[0, 0], h); + SafeACopytoC(B, h, h, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 5], 1, mField); //(A11 + A12) * B22; + + SafeAminusBintoC(A, 0, h, A, 0, 0, mField[0, 0], h); + SafeAplusBintoC(B, 0, 0, B, h, 0, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 6], 1, mField); //(A21 - A11) * (B11 + B12); + + SafeAminusBintoC(A, h, 0, A, h, h, mField[0, 0], h); + SafeAplusBintoC(B, 0, h, B, h, h, mField[0, 1], h); + StrassenMultiplyRun(mField[0, 0], mField[0, 1], mField[0, 1 + 7], 1, mField); // (A12 - A22) * (B21 + B22); + + R = new XLMatrix(A.rows, B.cols); // result + + // C11 + for (var i = 0; i < Math.Min(h, R.rows); i++) // rows + for (var j = 0; j < Math.Min(h, R.cols); j++) // cols + R[i, j] = mField[0, 1 + 1][i, j] + mField[0, 1 + 4][i, j] - mField[0, 1 + 5][i, j] + + mField[0, 1 + 7][i, j]; + + // C12 + for (var i = 0; i < Math.Min(h, R.rows); i++) // rows + for (var j = h; j < Math.Min(2*h, R.cols); j++) // cols + R[i, j] = mField[0, 1 + 3][i, j - h] + mField[0, 1 + 5][i, j - h]; + + // C21 + for (var i = h; i < Math.Min(2*h, R.rows); i++) // rows + for (var j = 0; j < Math.Min(h, R.cols); j++) // cols + R[i, j] = mField[0, 1 + 2][i - h, j] + mField[0, 1 + 4][i - h, j]; + + // C22 + for (var i = h; i < Math.Min(2*h, R.rows); i++) // rows + for (var j = h; j < Math.Min(2*h, R.cols); j++) // cols + R[i, j] = mField[0, 1 + 1][i - h, j - h] - mField[0, 1 + 2][i - h, j - h] + + mField[0, 1 + 3][i - h, j - h] + mField[0, 1 + 6][i - h, j - h]; + + return R; + } + + // function for square matrix 2^N x 2^N + + private static void StrassenMultiplyRun(XLMatrix A, XLMatrix B, XLMatrix C, int l, XLMatrix[,] f) + // A * B into C, level of recursion, matrix field + { + var size = A.rows; + var h = size/2; + + if (size < 32) + { + for (var i = 0; i < C.rows; i++) + for (var j = 0; j < C.cols; j++) + { + C[i, j] = 0; + for (var k = 0; k < A.cols; k++) C[i, j] += A[i, k]*B[k, j]; + } + return; + } + + AplusBintoC(A, 0, 0, A, h, h, f[l, 0], h); + AplusBintoC(B, 0, 0, B, h, h, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 1], l + 1, f); // (A11 + A22) * (B11 + B22); + + AplusBintoC(A, 0, h, A, h, h, f[l, 0], h); + ACopytoC(B, 0, 0, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 2], l + 1, f); // (A21 + A22) * B11; + + ACopytoC(A, 0, 0, f[l, 0], h); + AminusBintoC(B, h, 0, B, h, h, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 3], l + 1, f); //A11 * (B12 - B22); + + ACopytoC(A, h, h, f[l, 0], h); + AminusBintoC(B, 0, h, B, 0, 0, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 4], l + 1, f); //A22 * (B21 - B11); + + AplusBintoC(A, 0, 0, A, h, 0, f[l, 0], h); + ACopytoC(B, h, h, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 5], l + 1, f); //(A11 + A12) * B22; + + AminusBintoC(A, 0, h, A, 0, 0, f[l, 0], h); + AplusBintoC(B, 0, 0, B, h, 0, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 6], l + 1, f); //(A21 - A11) * (B11 + B12); + + AminusBintoC(A, h, 0, A, h, h, f[l, 0], h); + AplusBintoC(B, 0, h, B, h, h, f[l, 1], h); + StrassenMultiplyRun(f[l, 0], f[l, 1], f[l, 1 + 7], l + 1, f); // (A12 - A22) * (B21 + B22); + + // C11 + for (var i = 0; i < h; i++) // rows + for (var j = 0; j < h; j++) // cols + C[i, j] = f[l, 1 + 1][i, j] + f[l, 1 + 4][i, j] - f[l, 1 + 5][i, j] + f[l, 1 + 7][i, j]; + + // C12 + for (var i = 0; i < h; i++) // rows + for (var j = h; j < size; j++) // cols + C[i, j] = f[l, 1 + 3][i, j - h] + f[l, 1 + 5][i, j - h]; + + // C21 + for (var i = h; i < size; i++) // rows + for (var j = 0; j < h; j++) // cols + C[i, j] = f[l, 1 + 2][i - h, j] + f[l, 1 + 4][i - h, j]; + + // C22 + for (var i = h; i < size; i++) // rows + for (var j = h; j < size; j++) // cols + C[i, j] = f[l, 1 + 1][i - h, j - h] - f[l, 1 + 2][i - h, j - h] + f[l, 1 + 3][i - h, j - h] + + f[l, 1 + 6][i - h, j - h]; + } + + public static XLMatrix StupidMultiply(XLMatrix m1, XLMatrix m2) // Stupid matrix multiplication + { + if (m1.cols != m2.rows) throw new ArgumentException("Wrong dimensions of matrix!"); + + var result = ZeroMatrix(m1.rows, m2.cols); + for (var i = 0; i < result.rows; i++) + for (var j = 0; j < result.cols; j++) + for (var k = 0; k < m1.cols; k++) + result[i, j] += m1[i, k]*m2[k, j]; + return result; + } + + private static XLMatrix Multiply(double n, XLMatrix m) // Multiplication by constant n + { + var r = new XLMatrix(m.rows, m.cols); + for (var i = 0; i < m.rows; i++) + for (var j = 0; j < m.cols; j++) + r[i, j] = m[i, j]*n; + return r; + } + + private static XLMatrix Add(XLMatrix m1, XLMatrix m2) + { + if (m1.rows != m2.rows || m1.cols != m2.cols) + throw new ArgumentException("Matrices must have the same dimensions!"); + var r = new XLMatrix(m1.rows, m1.cols); + for (var i = 0; i < r.rows; i++) + for (var j = 0; j < r.cols; j++) + r[i, j] = m1[i, j] + m2[i, j]; + return r; + } + + public static string NormalizeMatrixString(string matStr) // From Andy - thank you! :) + { + // Remove any multiple spaces + while (matStr.IndexOf(" ") != -1) + matStr = matStr.Replace(" ", " "); + + // Remove any spaces before or after newlines + matStr = matStr.Replace(" \r\n", "\r\n"); + matStr = matStr.Replace("\r\n ", "\r\n"); + + // If the data ends in a newline, remove the trailing newline. + // Make it easier by first replacing \r\n’s with |’s then + // restore the |’s with \r\n’s + matStr = matStr.Replace("\r\n", "|"); + while (matStr.LastIndexOf("|") == (matStr.Length - 1)) + matStr = matStr.Substring(0, matStr.Length - 1); + + matStr = matStr.Replace("|", "\r\n"); + return matStr; + } + + // O P E R A T O R S + + public static XLMatrix operator -(XLMatrix m) + { + return Multiply(-1, m); + } + + public static XLMatrix operator +(XLMatrix m1, XLMatrix m2) + { + return Add(m1, m2); + } + + public static XLMatrix operator -(XLMatrix m1, XLMatrix m2) + { + return Add(m1, -m2); + } + + public static XLMatrix operator *(XLMatrix m1, XLMatrix m2) + { + return StrassenMultiply(m1, m2); + } + + public static XLMatrix operator *(double n, XLMatrix m) + { + return Multiply(n, m); + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/CalcEngine/Token.cs b/ClosedXML/Excel/CalcEngine/Token.cs new file mode 100644 index 0000000..5677b68 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Token.cs @@ -0,0 +1,53 @@ +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Represents a node in the expression tree. + /// + internal class Token + { + // ** fields + public TKID ID; + + public TKTYPE Type; + public object Value; + + // ** ctor + public Token(object value, TKID id, TKTYPE type) + { + Value = value; + ID = id; + Type = type; + } + } + + /// + /// Token types (used when building expressions, sequence defines operator priority) + /// + internal enum TKTYPE + { + COMPARE, // < > = <= >= + ADDSUB, // + - + MULDIV, // * / + POWER, // ^ + MULDIV_UNARY,// % + GROUP, // ( ) , . + LITERAL, // 123.32, "Hello", etc. + IDENTIFIER, // functions, external objects, bindings + ERROR // e.g. #REF! + } + + /// + /// Token ID (used when evaluating expressions) + /// + internal enum TKID + { + GT, LT, GE, LE, EQ, NE, // COMPARE + ADD, SUB, // ADDSUB + MUL, DIV, DIVINT, MOD, // MULDIV + POWER, // POWER + DIV100, // MULTIV_UNARY + OPEN, CLOSE, END, COMMA, PERIOD, // GROUP + ATOM, // LITERAL, IDENTIFIER + CONCAT + } +} diff --git a/ClosedXML/Excel/CalcEngine/XLAddressComparer.cs b/ClosedXML/Excel/CalcEngine/XLAddressComparer.cs new file mode 100644 index 0000000..ca2c1c6 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/XLAddressComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.CalcEngine +{ + internal class XLAddressComparer : IEqualityComparer + { + private readonly bool _ignoreFixed; + + public XLAddressComparer(bool ignoreFixed) + { + _ignoreFixed = ignoreFixed; + } + + public bool Equals(IXLAddress x, IXLAddress y) + { + return (x == null && y == null) || + (x != null && y != null && + string.Equals(x.Worksheet.Name, y.Worksheet.Name, StringComparison.InvariantCultureIgnoreCase) && + x.ColumnNumber == y.ColumnNumber && + x.RowNumber == y.RowNumber && + (_ignoreFixed || x.FixedColumn == y.FixedColumn && + x.FixedRow == y.FixedRow)); + } + + public int GetHashCode(IXLAddress obj) + { + return new + { + WorksheetName = obj.Worksheet.Name.ToUpperInvariant(), + obj.ColumnNumber, + obj.RowNumber, + FixedColumn = (_ignoreFixed ? false : obj.FixedColumn), + FixedRow = (_ignoreFixed ? false : obj.FixedRow) + }.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs b/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs new file mode 100644 index 0000000..5c1a7ff --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.CalcEngine +{ + internal class XLCalcEngine : CalcEngine + { + private readonly IXLWorksheet _ws; + private readonly XLWorkbook _wb; + + public XLCalcEngine() + { } + + public XLCalcEngine(XLWorkbook wb) + { + _wb = wb; + IdentifierChars = new char[] { '$', ':', '!' }; + } + + public XLCalcEngine(IXLWorksheet ws) : this(ws.Workbook) + { + _ws = ws; + } + + private IList _cellRanges; + + public ExpressionCache ExpressionCache => this._cache; + + /// + /// Get a collection of cell ranges included into the expression. Order is not preserved. + /// + /// Formula to parse. + /// Collection of ranges included into the expression. + public IEnumerable GetPrecedentRanges(string expression) + { + _cellRanges = new List(); + Parse(expression); + var ranges = _cellRanges; + _cellRanges = null; + var visitedRanges = new HashSet(new XLRangeAddressComparer(true)); + foreach (var range in ranges) + { + if (!visitedRanges.Contains(range.RangeAddress)) + { + visitedRanges.Add(range.RangeAddress); + yield return range; + } + } + } + + public IEnumerable GetPrecedentCells(string expression) + { + if (!String.IsNullOrWhiteSpace(expression)) + { + var ranges = GetPrecedentRanges(expression); + var visitedCells = new HashSet(new XLAddressComparer(true)); + var cells = ranges.SelectMany(range => range.Cells()).Distinct(); + foreach (var cell in cells) + { + if (!visitedCells.Contains(cell.Address)) + { + visitedCells.Add(cell.Address); + yield return cell; + } + } + } + } + + public override object GetExternalObject(string identifier) + { + if (identifier.Contains("!") && _wb != null) + { + var referencedSheetNames = identifier.Split(':') + .Select(part => + { + if (part.Contains("!")) + return part.Substring(0, part.IndexOf('!')).ToLower(); + else + return null; + }) + .Where(sheet => sheet != null) + .Distinct(); + + if (!referencedSheetNames.Any()) + return GetCellRangeReference(_ws.Range(identifier)); + else if (referencedSheetNames.Count() > 1) + throw new ArgumentOutOfRangeException(referencedSheetNames.Last(), "Cross worksheet references may references no more than 1 other worksheet"); + else + { + if (!_wb.TryGetWorksheet(referencedSheetNames.Single(), out IXLWorksheet worksheet)) + throw new ArgumentOutOfRangeException(referencedSheetNames.Single(), "The required worksheet cannot be found"); + + identifier = identifier.ToLower().Replace(string.Format("{0}!", worksheet.Name.ToLower()), ""); + + return GetCellRangeReference(worksheet.Range(identifier)); + } + } + else if (_ws != null) + { + if (TryGetNamedRange(identifier, _ws, out IXLNamedRange namedRange)) + { + var references = (namedRange as XLNamedRange).RangeList.Select(r => + XLHelper.IsValidRangeAddress(r) + ? GetCellRangeReference(_ws.Workbook.Range(r)) + : new XLCalcEngine(_ws).Evaluate(r.ToString()) + ); + if (references.Count() == 1) + return references.Single(); + return references; + } + + return GetCellRangeReference(_ws.Range(identifier)); + } + else if (XLHelper.IsValidRangeAddress(identifier)) + return identifier; + else + return null; + } + + private bool TryGetNamedRange(string identifier, IXLWorksheet worksheet, out IXLNamedRange namedRange) + { + return worksheet.NamedRanges.TryGetValue(identifier, out namedRange) || + worksheet.Workbook.NamedRanges.TryGetValue(identifier, out namedRange); + } + + private CellRangeReference GetCellRangeReference(IXLRange range) + { + if (range == null) + return null; + + var res = new CellRangeReference(range, this); + _cellRanges?.Add(res.Range); + return res; + } + } +} diff --git a/ClosedXML/Excel/CalcEngine/XLRangeAddressComparer.cs b/ClosedXML/Excel/CalcEngine/XLRangeAddressComparer.cs new file mode 100644 index 0000000..a6f7594 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/XLRangeAddressComparer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel.CalcEngine +{ + internal class XLRangeAddressComparer : IEqualityComparer + { + private readonly XLAddressComparer _addressComparer; + + public XLRangeAddressComparer(bool ignoreFixed) + { + _addressComparer = new XLAddressComparer(ignoreFixed); + } + + public bool Equals(IXLRangeAddress x, IXLRangeAddress y) + { + return (x == null && y == null) || + (x != null && y != null && + _addressComparer.Equals(x.FirstAddress, y.FirstAddress) && + _addressComparer.Equals(x.LastAddress, y.LastAddress)); + } + + public int GetHashCode(IXLRangeAddress obj) + { + return new + { + FirstHash = _addressComparer.GetHashCode(obj.FirstAddress), + LastHash = _addressComparer.GetHashCode(obj.LastAddress), + }.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Cells/IXLCell.cs b/ClosedXML/Excel/Cells/IXLCell.cs new file mode 100644 index 0000000..2989c0a --- /dev/null +++ b/ClosedXML/Excel/Cells/IXLCell.cs @@ -0,0 +1,400 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; + +namespace ClosedXML.Excel +{ + public enum XLDataType { Text, Number, Boolean, DateTime, TimeSpan } + + public enum XLTableCellType { None, Header, Data, Total } + + public interface IXLCell + { + Boolean Active { get; set; } + + /// Gets this cell's address, relative to the worksheet. + /// The cell's address. + IXLAddress Address { get; } + + /// + /// Calculated value of cell formula. Is used for decreasing number of computations perfromed. + /// May hold invalid value when flag is True. + /// + Object CachedValue { get; } + + IXLComment Comment { get; } + + /// + /// Returns the current region. The current region is a range bounded by any combination of blank rows and blank columns + /// + /// + /// The current region. + /// + IXLRange CurrentRegion { get; } + + /// + /// Gets or sets the type of this cell's data. + /// Changing the data type will cause ClosedXML to covert the current value to the new data type. + /// An exception will be thrown if the current value cannot be converted to the new data type. + /// + /// + /// The type of the cell's data. + /// + /// + XLDataType DataType { get; set; } + + IXLDataValidation DataValidation { get; } + + /// + /// Gets or sets the cell's formula with A1 references. + /// + /// The formula with A1 references. + String FormulaA1 { get; set; } + + /// + /// Gets or sets the cell's formula with R1C1 references. + /// + /// The formula with R1C1 references. + String FormulaR1C1 { get; set; } + + IXLRangeAddress FormulaReference { get; set; } + + Boolean HasArrayFormula { get; } + + Boolean HasComment { get; } + + Boolean HasDataValidation { get; } + + Boolean HasFormula { get; } + + Boolean HasHyperlink { get; } + + Boolean HasRichText { get; } + + Boolean HasSparkline { get; } + + XLHyperlink Hyperlink { get; set; } + + /// + /// Flag indicating that previously calculated cell value may be not valid anymore and has to be re-evaluated. + /// + Boolean NeedsRecalculation { get; } + + IXLDataValidation NewDataValidation { get; } + + IXLRichText RichText { get; } + + /// + /// Gets or sets a value indicating whether this cell's text should be shared or not. + /// + /// + /// If false the cell's text will not be shared and stored as an inline value. + /// + Boolean ShareString { get; set; } + + IXLSparkline Sparkline { get; } + + /// + /// Gets or sets the cell's style. + /// + IXLStyle Style { get; set; } + + /// + /// Gets or sets the cell's value. To get or set a strongly typed value, use the GetValue<T> and SetValue methods. + /// ClosedXML will try to detect the data type through parsing. If it can't then the value will be left as a string. + /// If the object is an IEnumerable, ClosedXML will copy the collection's data into a table starting from this cell. + /// If the object is a range, ClosedXML will copy the range starting from this cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// If the value starts with a single quote, ClosedXML will assume the value is a text variable and will prefix the value with a single quote in Excel too. + /// + /// + /// The object containing the value(s) to set. + /// + Object Value { get; set; } + + IXLWorksheet Worksheet { get; } + + IXLConditionalFormat AddConditionalFormat(); + + /// + /// Creates a named range out of this cell. + /// If the named range exists, it will add this range to that named range. + /// The default scope for the named range is Workbook. + /// + /// Name of the range. + IXLCell AddToNamed(String rangeName); + + /// + /// Creates a named range out of this cell. + /// If the named range exists, it will add this range to that named range. + /// Name of the range. + /// The scope for the named range. + /// + IXLCell AddToNamed(String rangeName, XLScope scope); + + /// + /// Creates a named range out of this cell. + /// If the named range exists, it will add this range to that named range. + /// Name of the range. + /// The scope for the named range. + /// The comments for the named range. + /// + IXLCell AddToNamed(String rangeName, XLScope scope, String comment); + + /// + /// Returns this cell as an IXLRange. + /// + IXLRange AsRange(); + + IXLCell CellAbove(); + + IXLCell CellAbove(Int32 step); + + IXLCell CellBelow(); + + IXLCell CellBelow(Int32 step); + + IXLCell CellLeft(); + + IXLCell CellLeft(Int32 step); + + IXLCell CellRight(); + + IXLCell CellRight(Int32 step); + + /// + /// Clears the contents of this cell. + /// + /// Specify what you want to clear. + IXLCell Clear(XLClearOptions clearOptions = XLClearOptions.All); + + IXLCell CopyFrom(IXLCell otherCell); + + IXLCell CopyFrom(String otherCell); + + IXLCell CopyTo(IXLCell target); + + IXLCell CopyTo(String target); + + /// + /// Deletes the current cell and shifts the surrounding cells according to the shiftDeleteCells parameter. + /// + /// How to shift the surrounding cells. + void Delete(XLShiftDeletedCells shiftDeleteCells); + + /// + /// Gets the cell's value converted to Boolean. + /// ClosedXML will try to covert the current value to Boolean. + /// An exception will be thrown if the current value cannot be converted to Boolean. + /// + Boolean GetBoolean(); + + /// + /// Gets the cell's value converted to DateTime. + /// ClosedXML will try to covert the current value to DateTime. + /// An exception will be thrown if the current value cannot be converted to DateTime. + /// + DateTime GetDateTime(); + + /// + /// Gets the cell's value converted to Double. + /// ClosedXML will try to covert the current value to Double. + /// An exception will be thrown if the current value cannot be converted to Double. + /// + Double GetDouble(); + + /// + /// Gets the cell's value formatted depending on the cell's data type and style. + /// + String GetFormattedString(); + + XLHyperlink GetHyperlink(); + + /// + /// Gets the cell's value converted to a String. + /// + String GetString(); + + /// + /// Gets the cell's value converted to TimeSpan. + /// ClosedXML will try to covert the current value to TimeSpan. + /// An exception will be thrown if the current value cannot be converted to TimeSpan. + /// + TimeSpan GetTimeSpan(); + + /// + /// Gets the cell's value converted to the T type. + /// ClosedXML will try to covert the current value to the T type. + /// An exception will be thrown if the current value cannot be converted to the T type. + /// + /// The return type. + /// + T GetValue(); + + IXLCells InsertCellsAbove(int numberOfRows); + + IXLCells InsertCellsAfter(int numberOfColumns); + + IXLCells InsertCellsBefore(int numberOfColumns); + + IXLCells InsertCellsBelow(int numberOfRows); + + /// + /// Inserts the IEnumerable data elements and returns the range it occupies. + /// + /// The IEnumerable data. + IXLRange InsertData(IEnumerable data); + + /// + /// Inserts the IEnumerable data elements and returns the range it occupies. + /// + /// The IEnumerable data. + /// if set to true the data will be transposed before inserting. + /// + IXLRange InsertData(IEnumerable data, Boolean transpose); + + /// + /// Inserts the data of a data table. + /// + /// The data table. + /// The range occupied by the inserted data + IXLRange InsertData(DataTable dataTable); + + /// + /// Inserts the IEnumerable data elements as a table and returns it. + /// The new table will receive a generic name: Table# + /// + /// The table data. + IXLTable InsertTable(IEnumerable data); + + /// + /// Inserts the IEnumerable data elements as a table and returns it. + /// The new table will receive a generic name: Table# + /// + /// The table data. + /// + /// if set to true it will create an Excel table. + /// if set to false the table will be created in memory. + /// + IXLTable InsertTable(IEnumerable data, Boolean createTable); + + /// + /// Creates an Excel table from the given IEnumerable data elements. + /// + /// The table data. + /// Name of the table. + IXLTable InsertTable(IEnumerable data, String tableName); + + /// + /// Inserts the IEnumerable data elements as a table and returns it. + /// + /// The table data. + /// Name of the table. + /// + /// if set to true it will create an Excel table. + /// if set to false the table will be created in memory. + /// + IXLTable InsertTable(IEnumerable data, String tableName, Boolean createTable); + + /// + /// Inserts the DataTable data elements as a table and returns it. + /// The new table will receive a generic name: Table# + /// + /// The table data. + IXLTable InsertTable(DataTable data); + + /// + /// Inserts the DataTable data elements as a table and returns it. + /// The new table will receive a generic name: Table# + /// + /// The table data. + /// + /// if set to true it will create an Excel table. + /// if set to false the table will be created in memory. + /// + IXLTable InsertTable(DataTable data, Boolean createTable); + + /// + /// Creates an Excel table from the given DataTable data elements. + /// + /// The table data. + /// Name of the table. + IXLTable InsertTable(DataTable data, String tableName); + + /// + /// Inserts the DataTable data elements as a table and returns it. + /// + /// The table data. + /// Name of the table. + /// + /// if set to true it will create an Excel table. + /// if set to false the table will be created in memory. + /// + IXLTable InsertTable(DataTable data, String tableName, Boolean createTable); + + /// + /// Invalidate so the formula will be re-evaluated next time is accessed. + /// If cell does not contain formula nothing happens. + /// + void InvalidateFormula(); + + Boolean IsEmpty(); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + Boolean IsEmpty(Boolean includeFormats); + + Boolean IsEmpty(XLCellsUsedOptions options); + + Boolean IsMerged(); + + IXLRange MergedRange(); + + void Select(); + + IXLCell SetActive(Boolean value = true); + + /// + /// Sets the type of this cell's data. + /// Changing the data type will cause ClosedXML to covert the current value to the new data type. + /// An exception will be thrown if the current value cannot be converted to the new data type. + /// + /// Type of the data. + /// + IXLCell SetDataType(XLDataType dataType); + + IXLDataValidation SetDataValidation(); + + IXLCell SetFormulaA1(String formula); + + IXLCell SetFormulaR1C1(String formula); + + /// + /// Sets the cell's value. + /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from this cell. + /// If the object is a range ClosedXML will copy the range starting from this cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string. + /// + /// + /// The object containing the value(s) to set. + /// + IXLCell SetValue(T value); + + XLTableCellType TableCellType(); + + /// + /// Returns a string that represents the current state of the cell according to the format. + /// + /// A: address, F: formula, NF: number format, BG: background color, FG: foreground color, V: formatted value + /// + string ToString(string format); + + Boolean TryGetValue(out T value); + + IXLColumn WorksheetColumn(); + + IXLRow WorksheetRow(); + } +} diff --git a/ClosedXML/Excel/Cells/IXLCells.cs b/ClosedXML/Excel/Cells/IXLCells.cs new file mode 100644 index 0000000..7148908 --- /dev/null +++ b/ClosedXML/Excel/Cells/IXLCells.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLCells : IEnumerable + { + /// + /// Sets the cells' value. + /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell. + /// If the object is a range ClosedXML will copy the range starting from each cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string. + /// + /// + /// The object containing the value(s) to set. + /// + Object Value { set; } + + /// + /// Sets the type of the cells' data. + /// Changing the data type will cause ClosedXML to covert the current value to the new data type. + /// An exception will be thrown if the current value cannot be converted to the new data type. + /// + /// + /// The type of the cell's data. + /// + /// + XLDataType DataType { set; } + + IXLCells SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these cells. + /// + /// Specify what you want to clear. + IXLCells Clear(XLClearOptions clearOptions = XLClearOptions.All); + + /// + /// Delete the comments of these cells. + /// + void DeleteComments(); + + /// + /// Delete the sparklines of these cells. + /// + void DeleteSparklines(); + + /// + /// Sets the cells' formula with A1 references. + /// + /// The formula with A1 references. + String FormulaA1 { set; } + + /// + /// Sets the cells' formula with R1C1 references. + /// + /// The formula with R1C1 references. + String FormulaR1C1 { set; } + + IXLStyle Style { get; set; } + + void Select(); + } +} diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs new file mode 100644 index 0000000..af98050 --- /dev/null +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -0,0 +1,3152 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel +{ + using Attributes; + using ClosedXML.Extensions; + + [DebuggerDisplay("{Address}")] + internal class XLCell : XLStylizedBase, IXLCell, IXLStylized + { + public static readonly DateTime BaseDate = new DateTime(1899, 12, 30); + + private static readonly Regex A1Regex = new Regex( + @"(?<=\W)(\$?[a-zA-Z]{1,3}\$?\d{1,7})(?=\W)" // A1 + + @"|(?<=\W)(\$?\d{1,7}:\$?\d{1,7})(?=\W)" // 1:1 + + @"|(?<=\W)(\$?[a-zA-Z]{1,3}:\$?[a-zA-Z]{1,3})(?=\W)", RegexOptions.Compiled); // A:A + + public static readonly Regex A1SimpleRegex = new Regex( + // @"(?<=\W)" // Start with non word + @"(?" // Start Group to pick + + @"(?" // Start Sheet Name, optional + + @"(" + + @"\'([^\[\]\*/\\\?:\']+|\'\')\'" + // Sheet name with special characters, surrounding apostrophes are required + + @"|" + + @"\'?\w+\'?" // Sheet name with letters and numbers, surrounding apostrophes are optional + + @")" + + @"!)?" // End Sheet Name, optional + + @"(?" // Start range + + @"(?:\$?[a-zA-Z]{1,3}\$?\d{1,7})?" // A1 Address 2, optional + + @"(?![\w\d])" // followed by anything but a letter or a number + + @"|" + + @"(?\$?\d{1,7}:\$?\d{1,7})" // 1:1 + + @"|" + + @"(?\$?[a-zA-Z]{1,3}:\$?[a-zA-Z]{1,3})" // A:A + + @")" // End Range + + @")" // End Group to pick + //+ @"(?=\W)" // End with non word + , RegexOptions.Compiled); + + private static readonly Regex A1RowRegex = new Regex( + @"(\$?\d{1,7}:\$?\d{1,7})" // 1:1 + , RegexOptions.Compiled); + + private static readonly Regex A1ColumnRegex = new Regex( + @"(\$?[a-zA-Z]{1,3}:\$?[a-zA-Z]{1,3})" // A:A + , RegexOptions.Compiled); + + private static readonly Regex R1C1Regex = new Regex( + @"(?<=\W)([Rr](?:\[-?\d{0,7}\]|\d{0,7})?[Cc](?:\[-?\d{0,7}\]|\d{0,7})?)(?=\W)" // R1C1 + + @"|(?<=\W)([Rr]\[?-?\d{0,7}\]?:[Rr]\[?-?\d{0,7}\]?)(?=\W)" // R:R + + @"|(?<=\W)([Cc]\[?-?\d{0,5}\]?:[Cc]\[?-?\d{0,5}\]?)(?=\W)", RegexOptions.Compiled); // C:C + + private static readonly Regex utfPattern = new Regex(@"(? + /// Get the data validation rule containing current cell or create a new one if no rule was defined for cell. + /// + public IXLDataValidation DataValidation + { + get + { + return SetDataValidation(); + } + } + + internal XLComment Comment + { + get + { + if (_comment == null) + { + CreateComment(); + } + + return _comment; + } + } + + internal XLComment CreateComment(int? shapeId = null) + { + _comment = new XLComment(this, shapeId: shapeId); + return _comment; + } + + #region IXLCell Members + + IXLDataValidation IXLCell.DataValidation + { + get { return DataValidation; } + } + + IXLWorksheet IXLCell.Worksheet + { + get { return Worksheet; } + } + + IXLAddress IXLCell.Address + { + get { return Address; } + } + + IXLRange IXLCell.AsRange() + { + return AsRange(); + } + + public IXLCell SetValue(T value) + { + if (value is IEnumerable ie && !(value is string)) + { + this.InsertData(ie); + return this; + } + else + return SetValue(value, true); + } + + internal IXLCell SetValue(T value, bool setTableHeader) + { + if (IsInferiorMergedCell()) + return this; + + if (value == null) + return this.Clear(XLClearOptions.Contents); + + FormulaA1 = String.Empty; + _richText = null; + + if (setTableHeader) + { + if (SetTableHeaderValue(value)) return this; + if (SetTableTotalsRowLabel(value)) return this; + } + + var style = GetStyleForRead(); + Boolean parsed; + string parsedValue; + + // For SetValue we set the cell value directly to the parameter + // as opposed to the other SetValue(object value) where we parse the string and try to decude the value + var tuple = SetKnownTypedValue(value, style, acceptString: true); + parsedValue = tuple.Item1; + parsed = tuple.Item2; + + // If parsing was unsuccessful, we throw an ArgumentException + // because we are using SetValue (typed). + // Only in SetValue(object value) to we try to fall back to a value of a different type + if (!parsed) + throw new ArgumentException($"Unable to set cell value to {value.ObjectToInvariantString()}"); + + SetInternalCellValueString(parsedValue, validate: true, parseToCachedValue: false); + + CachedValue = null; + + return this; + } + + // TODO: Replace with (string, bool) ValueTuple later + private Tuple SetKnownTypedValue(T value, XLStyleValue style, Boolean acceptString) + { + string parsedValue; + bool parsed; + if (value is String && acceptString || value is char || value is Guid || value is Enum) + { + parsedValue = value.ObjectToInvariantString(); + _dataType = XLDataType.Text; + if (parsedValue.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + + parsed = true; + } + else if (value.Equals(DBNull.Value)) + { + parsedValue = ""; + _dataType = XLDataType.Text; + parsed = true; + } + else if (value is DateTime d && d >= BaseDate) + { + parsedValue = d.ToOADate().ToInvariantString(); + parsed = true; + SetDateTimeFormat(style, d.Date == d); + } + else if (value is TimeSpan ts) + { + parsedValue = ts.TotalDays.ToInvariantString(); + parsed = true; + SetTimeSpanFormat(style); + } + else if (value is Boolean b) + { + parsedValue = b ? "1" : "0"; + _dataType = XLDataType.Boolean; + parsed = true; + } + else if (value.IsNumber()) + { + if ( + (value is double d1 && (double.IsNaN(d1) || double.IsInfinity(d1))) + || (value is float f && (float.IsNaN(f) || float.IsInfinity(f))) + ) + { + parsedValue = value.ToString(); + _dataType = XLDataType.Text; + parsed = parsedValue.Length != 0; + } + else + { + parsedValue = value.ObjectToInvariantString(); + _dataType = XLDataType.Number; + } + parsed = true; + } + else + { + parsed = false; + parsedValue = null; + } + + return new Tuple(parsedValue, parsed); + } + + private string DeduceCellValueByParsing(string value, XLStyleValue style) + { + if (String.IsNullOrEmpty(value)) + { + _dataType = XLDataType.Text; + } + else if (value[0] == '\'') + { + // If a user sets a cell value to a value starting with a single quote + // ensure the data type is text + // and that it will be prefixed with a quote in Excel too + + value = value.Substring(1, value.Length - 1); + + _dataType = XLDataType.Text; + if (value.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + + this.Style.SetIncludeQuotePrefix(); + } + else if (!string.Equals(value.Trim(), "NaN", StringComparison.OrdinalIgnoreCase) && + Double.TryParse(value, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double _)) + _dataType = XLDataType.Number; + else if (TimeSpan.TryParse(value, out TimeSpan ts)) + { + value = ts.ToInvariantString(); + SetTimeSpanFormat(style); + } + else if (DateTime.TryParse(value, out DateTime dt) && dt >= BaseDate) + { + value = dt.ToOADate().ToInvariantString(); + SetDateTimeFormat(style, dt.Date == dt); + } + else if (Boolean.TryParse(value, out Boolean b)) + { + value = b ? "1" : "0"; + _dataType = XLDataType.Boolean; + } + else + { + _dataType = XLDataType.Text; + if (value.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + } + + return value; + } + + public T GetValue() + { + if (TryGetValue(out T retVal)) + return retVal; + + throw new FormatException($"Cannot convert {this.Address.ToStringRelative(true)}'s value to " + typeof(T)); + } + + public string GetString() + { + return GetValue(); + } + + public double GetDouble() + { + return GetValue(); + } + + public bool GetBoolean() + { + return GetValue(); + } + + public DateTime GetDateTime() + { + return GetValue(); + } + + public TimeSpan GetTimeSpan() + { + return GetValue(); + } + + public string GetFormattedString() + { + var format = GetFormat(); + try + { + return Value.ToExcelFormat(format); + } + catch { } + + try + { + return CachedValue.ToExcelFormat(format); + } + catch { } + + return _cellValue; + } + + /// + /// Flag showing that the cell is in formula evaluation state. + /// + internal bool IsEvaluating { get; private set; } + + /// + /// Calculate a value of the specified formula. + /// + /// Cell formula to evaluate. + /// Null if formula is empty or null, calculated value otherwise. + private object RecalculateFormula(string fA1) + { + if (string.IsNullOrEmpty(fA1)) + return null; + + if (IsEvaluating) + throw new InvalidOperationException("Circular Reference"); + + if (fA1[0] == '{') + fA1 = fA1.Substring(1, fA1.Length - 2); + + string sName; + string cAddress; + if (fA1.Contains('!')) + { + sName = fA1.Substring(0, fA1.IndexOf('!')); + if (sName[0] == '\'') + sName = sName.Substring(1, sName.Length - 2); + + cAddress = fA1.Substring(fA1.IndexOf('!') + 1); + } + else + { + sName = Worksheet.Name; + cAddress = fA1; + } + + if (Worksheet.Workbook.Worksheets.Contains(sName) + && XLHelper.IsValidA1Address(cAddress)) + { + try + { + IsEvaluating = true; + var referenceCell = Worksheet.Workbook.Worksheet(sName).Cell(cAddress); + if (referenceCell.IsEmpty(XLCellsUsedOptions.AllContents)) + return 0; + else + return referenceCell.Value; + } + finally + { + IsEvaluating = false; + } + } + + object retVal; + try + { + IsEvaluating = true; + + if (Worksheet.Workbook.Worksheets.Contains(sName) + && XLHelper.IsValidA1Address(cAddress)) + { + var referenceCell = Worksheet.Workbook.Worksheet(sName).Cell(cAddress); + if (referenceCell.IsEmpty(XLCellsUsedOptions.AllContents)) + return 0; + else + return referenceCell.Value; + } + + retVal = Worksheet.Evaluate(fA1); + } + finally + { + IsEvaluating = false; + } + + if (retVal is IEnumerable retValEnumerable && !(retVal is String)) + return retValEnumerable.Cast().First(); + + return retVal; + } + + public void InvalidateFormula() + { + NeedsRecalculation = true; + Worksheet.Workbook.InvalidateFormulas(); + ModifiedAtVersion = Worksheet.Workbook.RecalculationCounter; + } + + /// + /// Perform an evaluation of cell formula. If cell does not contain formula nothing happens, if cell does not need + /// recalculation ( is False) nothing happens either, unless flag is specified. + /// Otherwise recalculation is perfomed, result value is preserved in and returned. + /// + /// Flag indicating whether a recalculation must be performed even is cell does not need it. + /// Null if cell does not contain a formula. Calculated value otherwise. + public Object Evaluate(Boolean force = false) + { + if (force || NeedsRecalculation) + { + if (HasFormula) + { + CachedValue = RecalculateFormula(FormulaA1); + UpdateCachedValueFromDataType(); + } + else + CachedValue = null; + + EvaluatedAtVersion = Worksheet.Workbook.RecalculationCounter; + NeedsRecalculation = false; + } + return CachedValue; + } + + internal void SetInternalCellValueString(String cellValue) + { + SetInternalCellValueString(cellValue, validate: false, parseToCachedValue: this.HasFormula); + } + + private void SetInternalCellValueString(String cellValue, Boolean validate, Boolean parseToCachedValue) + { + if (validate) + { + if (cellValue.Length > 32767) throw new ArgumentOutOfRangeException(nameof(cellValue), "Cells can hold a maximum of 32,767 characters."); + } + + this._cellValue = cellValue; + + if (parseToCachedValue) + CachedValue = ParseCellValueFromString(); + } + + private void UpdateCachedValueFromDataType() + { + if (CachedValue is double d) + { + if (this.DataType == XLDataType.DateTime && d.IsValidOADateNumber()) + CachedValue = DateTime.FromOADate(d); + else if (this.DataType == XLDataType.TimeSpan) + CachedValue = XLHelper.GetTimeSpan(d); + } + else if (CachedValue is DateTime dt) + { + if (this.DataType == XLDataType.Number) + CachedValue = dt.ToOADate(); + else if (this.DataType == XLDataType.TimeSpan) + CachedValue = XLHelper.GetTimeSpan(dt.ToOADate()); + } + else if (CachedValue is TimeSpan ts) + { + if (this.DataType == XLDataType.DateTime) + CachedValue = DateTime.FromOADate(ts.TotalDays); + else if (this.DataType == XLDataType.Number) + CachedValue = ts.TotalDays; + } + } + + internal void SetDataTypeFast(XLDataType dataType) + { + this._dataType = dataType; + } + + private Object ParseCellValueFromString() + { + return ParseCellValueFromString(_cellValue, _dataType, out String error); + } + + private Object ParseCellValueFromString(String cellValue, XLDataType dataType, out String error) + { + error = ""; + if ("" == cellValue) + return ""; + + if (dataType == XLDataType.Boolean) + { + if (bool.TryParse(cellValue, out Boolean b)) + return b; + else if (cellValue == "0") + return false; + else if (cellValue == "1") + return true; + else + return !string.IsNullOrEmpty(cellValue); + } + + if (dataType == XLDataType.DateTime) + { + if (Double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d)) + { + if (d.IsValidOADateNumber()) + return DateTime.FromOADate(d); + else + return d; + } + else if (DateTime.TryParse(cellValue, out DateTime dt)) + return dt; + else + { + error = string.Format("Cannot set data type to DateTime because '{0}' is not recognized as a date.", cellValue); + return null; + } + } + if (dataType == XLDataType.Number) + { + var v = cellValue; + Double factor = 1.0; + if (v.EndsWith("%")) + { + v = v.Substring(0, v.Length - 1); + factor = 1 / 100.0; + } + + if (Double.TryParse(v, XLHelper.NumberStyle, CultureInfo.InvariantCulture, out Double d)) + return d * factor; + else + { + error = string.Format("Cannot set data type to Number because '{0}' is not recognized as a number.", cellValue); + return null; + } + } + + if (dataType == XLDataType.TimeSpan) + { + if (TimeSpan.TryParse(cellValue, out TimeSpan ts)) + return ts; + else if (Double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d)) + return XLHelper.GetTimeSpan(d); + else + { + error = string.Format("Cannot set data type to TimeSpan because '{0}' is not recognized as a TimeSpan.", cellValue); + return null; + } + } + + return cellValue; + } + + public override string ToString() => ToString("A"); + + public string ToString(string format) + { + return (format.ToUpper()) switch + { + "A" => this.Address.ToString(), + "F" => HasFormula ? this.FormulaA1 : string.Empty, + "NF" => Style.NumberFormat.Format, + "FG" => Style.Font.FontColor.ToString(), + "BG" => Style.Fill.BackgroundColor.ToString(), + "V" => GetFormattedString(), + _ => throw new FormatException($"Format {format} was not recognised."), + }; + } + + public object Value + { + get + { + if (!String.IsNullOrWhiteSpace(_formulaA1) || + !String.IsNullOrEmpty(_formulaR1C1)) + { + return Evaluate(); + } + + var cellValue = HasRichText ? _richText.ToString() : _cellValue; + return ParseCellValueFromString(cellValue, _dataType, out _); + } + set + { + if (IsInferiorMergedCell()) + return; + + FormulaA1 = String.Empty; + + if (value is XLCells) throw new ArgumentException("Cannot assign IXLCells object to the cell value."); + + if (SetTableHeaderValue(value)) return; + + if (SetRangeRows(value)) return; + + if (SetRangeColumns(value)) return; + + if (SetDataTable(value)) return; + + if (SetEnumerable(value)) return; + + if (SetRange(value)) return; + + if (!SetRichText(value)) + SetValue(value); + + CachedValue = null; + + if (_cellValue.Length > 32767) throw new ArgumentOutOfRangeException(nameof(value), "Cells can hold only 32,767 characters."); + } + } + + public IXLTable InsertTable(IEnumerable data) + { + return InsertTable(data, null, true); + } + + public IXLTable InsertTable(IEnumerable data, bool createTable) + { + return InsertTable(data, null, createTable); + } + + public IXLTable InsertTable(IEnumerable data, string tableName) + { + return InsertTable(data, tableName, true); + } + + public IXLTable InsertTable(IEnumerable data, String tableName, Boolean createTable) + { + return InsertTable(data, tableName, createTable, addHeadings: true, transpose: false); + } + + public IXLTable InsertTable(IEnumerable data, String tableName, Boolean createTable, Boolean addHeadings, Boolean transpose) + { + if (createTable && this.Worksheet.Tables.Any(t => t.Contains(this))) + throw new InvalidOperationException(String.Format("This cell '{0}' is already part of a table.", this.Address.ToString())); + + var range = InsertDataInternal(data, addHeadings, transpose); + + if (createTable) + // Create a table and save it in the file + return tableName == null ? range.CreateTable() : range.CreateTable(tableName); + else + // Create a table, but keep it in memory. Saved file will contain only "raw" data and column headers + return tableName == null ? range.AsTable() : range.AsTable(tableName); + } + + public IXLTable InsertTable(DataTable data) + { + return InsertTable(data, null, true); + } + + public IXLTable InsertTable(DataTable data, Boolean createTable) + { + return InsertTable(data, null, createTable); + } + + public IXLTable InsertTable(DataTable data, String tableName) + { + return InsertTable(data, tableName, true); + } + + public IXLTable InsertTable(DataTable data, String tableName, Boolean createTable) + { + if (data == null || data.Columns.Count == 0) + return null; + + if (XLHelper.IsValidA1Address(tableName) || XLHelper.IsValidRCAddress(tableName)) + throw new InvalidOperationException(string.Format("Table name cannot be a valid Cell Address '{0}'.", tableName)); + + if (createTable && this.Worksheet.Tables.Any(t => t.Contains(this))) + throw new InvalidOperationException(String.Format("This cell '{0}' is already part of a table.", this.Address.ToString())); + + if (data.Rows.Cast().Any()) + return InsertTable(data.Rows.Cast(), tableName, createTable); + + var co = _columnNumber; + + foreach (DataColumn col in data.Columns) + { + Worksheet.SetValue(col.ColumnName, _rowNumber, co); + co++; + } + + ClearMerged(); + var range = Worksheet.Range( + _rowNumber, + _columnNumber, + _rowNumber, + co - 1); + + if (createTable) + // Create a table and save it in the file + return tableName == null ? range.CreateTable() : range.CreateTable(tableName); + else + // Create a table, but keep it in memory. Saved file will contain only "raw" data and column headers + return tableName == null ? range.AsTable() : range.AsTable(tableName); + } + + internal XLRange InsertDataInternal(IEnumerable data, Boolean addHeadings, Boolean transpose) + { + if (data == null || data is String) + return null; + + var currentRowNumber = _rowNumber; + if (addHeadings && !transpose) currentRowNumber++; + + var currentColumnNumber = _columnNumber; + if (addHeadings && transpose) currentColumnNumber++; + + var firstRowNumber = _rowNumber; + var hasHeadings = false; + var maximumColumnNumber = currentColumnNumber; + var maximumRowNumber = currentRowNumber; + + var itemType = data.GetItemType(); + var isArray = itemType.IsArray; + var isDataTable = itemType == typeof(DataTable); + var isDataReader = itemType == typeof(IDataReader); + + // Inline functions to handle looping with transposing + ////////////////////////////////////////////////////// + void incrementFieldPosition() + { + if (transpose) + { + maximumRowNumber = Math.Max(maximumRowNumber, currentRowNumber); + currentRowNumber++; + } + else + { + maximumColumnNumber = Math.Max(maximumColumnNumber, currentColumnNumber); + currentColumnNumber++; + } + } + + void incrementRecordPosition() + { + if (transpose) + { + maximumColumnNumber = Math.Max(maximumColumnNumber, currentColumnNumber); + currentColumnNumber++; + } + else + { + maximumRowNumber = Math.Max(maximumRowNumber, currentRowNumber); + currentRowNumber++; + } + } + + void resetRecordPosition() + { + if (transpose) + currentRowNumber = _rowNumber; + else + currentColumnNumber = _columnNumber; + } + ////////////////////////////////////////////////////// + + if (!data.Any()) + { + if (itemType.IsSimpleType()) + maximumColumnNumber = _columnNumber; + else + maximumColumnNumber = _columnNumber + itemType.GetFields().Length + itemType.GetProperties().Length - 1; + } + else if (itemType.IsSimpleType()) + { + foreach (object o in data) + { + resetRecordPosition(); + + if (addHeadings && !hasHeadings) + { + var fieldName = XLColumnAttribute.GetHeader(itemType); + if (String.IsNullOrWhiteSpace(fieldName)) + fieldName = itemType.Name; + + Worksheet.SetValue(fieldName, firstRowNumber, currentColumnNumber); + hasHeadings = true; + resetRecordPosition(); + } + + Worksheet.SetValue(o, currentRowNumber, currentColumnNumber); + incrementFieldPosition(); + incrementRecordPosition(); + } + } + else + { + const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; + var memberCache = new Dictionary(); + MemberInfo[] members = null; + bool isPlainObject = itemType == typeof(object); + + if (!isPlainObject) + { + members = itemType.GetFields(bindingFlags).Cast() + .Concat(itemType.GetProperties(bindingFlags)) + .Where(mi => !XLColumnAttribute.IgnoreMember(mi)) + .OrderBy(mi => XLColumnAttribute.GetOrder(mi)) + .ToArray(); + } + + foreach (T m in data) + { + resetRecordPosition(); + + if (m == null) + { + Worksheet.SetValue(String.Empty, currentRowNumber, currentColumnNumber); + incrementRecordPosition(); + continue; + } + + if (m.GetType().IsSimpleType()) + { + if (addHeadings && !hasHeadings) + { + var fieldName = XLColumnAttribute.GetHeader(itemType); + if (String.IsNullOrWhiteSpace(fieldName)) + fieldName = itemType.Name; + + Worksheet.SetValue(fieldName, firstRowNumber, currentColumnNumber); + hasHeadings = true; + resetRecordPosition(); + } + + Worksheet.SetValue(m as object, currentRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + else + { + if (isPlainObject) + { + // In this case data is just IEnumerable, which means we have to determine the runtime type of each element + // This is very inefficient and we prefer type of T to be a concrete class or struct + var type = m.GetType(); + + isArray |= type.IsArray; + isDataTable |= type == typeof(DataRow); + isDataReader |= type == typeof(IDataRecord); + + if (!memberCache.TryGetValue(type, out members)) + { + members = type.GetFields(bindingFlags).Cast() + .Concat(type.GetProperties(bindingFlags)) + .Where(mi => !XLColumnAttribute.IgnoreMember(mi)) + .OrderBy(mi => XLColumnAttribute.GetOrder(mi)) + .ToArray(); + + memberCache.Add(type, members); + } + } + + if (isArray) + { + foreach (var item in (m as Array)) + { + Worksheet.SetValue(item, currentRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + } + else if (isDataTable || m is DataRow) + { + var row = m as DataRow; + if (!isDataTable) + isDataTable = true; + + if (addHeadings && !hasHeadings) + { + foreach (var fieldName in from DataColumn column in row.Table.Columns + select String.IsNullOrWhiteSpace(column.Caption) + ? column.ColumnName + : column.Caption) + { + Worksheet.SetValue(fieldName, firstRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + + resetRecordPosition(); + hasHeadings = true; + } + + foreach (var item in row.ItemArray) + { + Worksheet.SetValue(item, currentRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + } + else if (isDataReader || m is IDataRecord) + { + if (!isDataReader) + isDataReader = true; + + var record = m as IDataRecord; + + var fieldCount = record.FieldCount; + if (addHeadings && !hasHeadings) + { + for (var i = 0; i < fieldCount; i++) + { + Worksheet.SetValue(record.GetName(i), firstRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + + resetRecordPosition(); + hasHeadings = true; + } + + for (var i = 0; i < fieldCount; i++) + { + Worksheet.SetValue(record[i], currentRowNumber, currentColumnNumber); + incrementFieldPosition(); + } + } + else + { + if (addHeadings && !hasHeadings) + { + foreach (var mi in members) + { + if (!(mi is IEnumerable)) + { + var fieldName = XLColumnAttribute.GetHeader(mi); + if (String.IsNullOrWhiteSpace(fieldName)) + fieldName = mi.Name; + + Worksheet.SetValue(fieldName, firstRowNumber, currentColumnNumber); + } + + incrementFieldPosition(); + } + + resetRecordPosition(); + hasHeadings = true; + } + + foreach (var mi in members) + { + if (mi.MemberType == MemberTypes.Property && (mi as PropertyInfo).GetGetMethod().IsStatic) + Worksheet.SetValue((mi as PropertyInfo).GetValue(null, null), currentRowNumber, currentColumnNumber); + else if (mi.MemberType == MemberTypes.Field && (mi as FieldInfo).IsStatic) + Worksheet.SetValue((mi as FieldInfo).GetValue(null), currentRowNumber, currentColumnNumber); + else if (mi.MemberType == MemberTypes.Property) + Worksheet.SetValue((mi as PropertyInfo).GetValue(m, null), currentRowNumber, currentColumnNumber); + else if (mi.MemberType == MemberTypes.Field) + Worksheet.SetValue((mi as FieldInfo).GetValue(m), currentRowNumber, currentColumnNumber); + + incrementFieldPosition(); + } + } + } + + incrementRecordPosition(); + } + } + + ClearMerged(); + + var range = Worksheet.Range( + _rowNumber, + _columnNumber, + maximumRowNumber, + maximumColumnNumber); + + return range; + } + + public XLTableCellType TableCellType() + { + var table = this.Worksheet.Tables.FirstOrDefault(t => t.AsRange().Contains(this)); + if (table == null) return XLTableCellType.None; + + if (table.ShowHeaderRow && table.HeadersRow().RowNumber().Equals(this._rowNumber)) return XLTableCellType.Header; + if (table.ShowTotalsRow && table.TotalsRow().RowNumber().Equals(this._rowNumber)) return XLTableCellType.Total; + + return XLTableCellType.Data; + } + + public IXLRange InsertData(IEnumerable data) + { + if (data == null || data is String) + return null; + + return InsertDataInternal(data?.Cast(), addHeadings: false, transpose: false); + } + + public IXLRange InsertData(IEnumerable data, Boolean transpose) + { + if (data == null || data is String) + return null; + + return InsertDataInternal(data?.Cast(), addHeadings: false, transpose: transpose); + } + + public IXLRange InsertData(DataTable dataTable) + { + if (dataTable == null) + return null; + + return InsertDataInternal(dataTable?.Rows?.Cast(), addHeadings: false, transpose: false); + } + + public IXLCell SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + public XLDataType DataType + { + get { return _dataType; } + set + { + if (_dataType == value) return; + + if (HasRichText) + { + _cellValue = _richText.ToString(); + _richText = null; + } + + if (!string.IsNullOrEmpty(_cellValue)) + { + // If we're converting the DataType to Text, there are some quirky rules currently + if (value == XLDataType.Text) + { + var v = Value; + switch (v) + { + case DateTime d: + _cellValue = d.ToOADate().ToInvariantString(); + break; + + case TimeSpan ts: + _cellValue = ts.TotalDays.ToInvariantString(); + break; + + case Boolean b: + _cellValue = b ? "True" : "False"; + break; + + default: + _cellValue = v.ObjectToInvariantString(); + break; + } + } + else + { + var v = ParseCellValueFromString(_cellValue, value, out String error); + + if (!String.IsNullOrWhiteSpace(error)) + throw new ArgumentException(error, nameof(value)); + + _cellValue = v?.ObjectToInvariantString() ?? ""; + + var style = GetStyleForRead(); + switch (v) + { + case DateTime d: + _cellValue = d.ToOADate().ToInvariantString(); + + if (style.NumberFormat.Format.Length == 0 && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = _cellValue.Contains('.') ? 22 : 14; + + break; + + case TimeSpan ts: + if (style.NumberFormat.Format.Length == 0 && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = 46; + + break; + + case Boolean b: + _cellValue = b ? "1" : "0"; + break; + } + } + } + + _dataType = value; + + if (HasFormula && !NeedsRecalculation) + UpdateCachedValueFromDataType(); + else + CachedValue = null; + } + } + + public IXLCell Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + return Clear(clearOptions, false); + } + + internal IXLCell Clear(XLClearOptions clearOptions, bool calledFromRange) + { + //Note: We have to check if the cell is part of a merged range. If so we have to clear the whole range + //Checking if called from range to avoid stack overflow + if (!calledFromRange && IsMerged()) + { + var firstOrDefault = Worksheet.Internals.MergedRanges.GetIntersectedRanges(Address).FirstOrDefault(); + if (firstOrDefault != null) + firstOrDefault.Clear(clearOptions); + } + else + { + if (clearOptions.HasFlag(XLClearOptions.Contents)) + { + Hyperlink = null; + _richText = null; + _cellValue = String.Empty; + FormulaA1 = String.Empty; + } + + if (clearOptions.HasFlag(XLClearOptions.DataType)) + _dataType = XLDataType.Text; + + if (clearOptions.HasFlag(XLClearOptions.NormalFormats)) + SetStyle(Worksheet.Style); + + if (clearOptions.HasFlag(XLClearOptions.ConditionalFormats)) + { + AsRange().RemoveConditionalFormatting(); + } + + if (clearOptions.HasFlag(XLClearOptions.Comments)) + _comment = null; + + if (clearOptions.HasFlag(XLClearOptions.Sparklines)) + { + AsRange().RemoveSparklines(); + } + + if (clearOptions.HasFlag(XLClearOptions.DataValidation) && HasDataValidation) + { + var validation = NewDataValidation; + Worksheet.DataValidations.Delete(validation); + } + + if (clearOptions.HasFlag(XLClearOptions.MergedRanges) && IsMerged()) + { + ClearMerged(); + } + } + + return this; + } + + public void Delete(XLShiftDeletedCells shiftDeleteCells) + { + Worksheet.Range(Address, Address).Delete(shiftDeleteCells); + } + + public string FormulaA1 + { + get + { + if (String.IsNullOrWhiteSpace(_formulaA1)) + { + if (!String.IsNullOrWhiteSpace(_formulaR1C1)) + { + _formulaA1 = GetFormulaA1(_formulaR1C1); + return FormulaA1; + } + + return String.Empty; + } + + if (_formulaA1.Trim()[0] == '=') + return _formulaA1.Substring(1); + + if (_formulaA1.Trim().StartsWith("{=")) + return "{" + _formulaA1.Substring(2); + + return _formulaA1; + } + + set + { + if (IsInferiorMergedCell()) + return; + + InvalidateFormula(); + + _formulaA1 = String.IsNullOrWhiteSpace(value) ? null : value; + + _formulaR1C1 = null; + } + } + + public string FormulaR1C1 + { + get + { + if (String.IsNullOrWhiteSpace(_formulaR1C1)) + _formulaR1C1 = GetFormulaR1C1(FormulaA1); + + return _formulaR1C1; + } + + set + { + if (IsInferiorMergedCell()) + return; + + InvalidateFormula(); + + _formulaR1C1 = String.IsNullOrWhiteSpace(value) ? null : value; + + _formulaA1 = null; + } + } + + public bool ShareString { get; set; } + + public XLHyperlink Hyperlink + { + get + { + if (_hyperlink == null) + Hyperlink = new XLHyperlink(); + + return _hyperlink; + } + + set + { + if (Worksheet.Hyperlinks.Any(hl => Address.Equals(hl.Cell.Address))) + Worksheet.Hyperlinks.Delete(Address); + + _hyperlink = value; + + if (_hyperlink == null) return; + + _hyperlink.Worksheet = Worksheet; + _hyperlink.Cell = this; + + Worksheet.Hyperlinks.Add(_hyperlink); + + if (SettingHyperlink) return; + + if (GetStyleForRead().Font.FontColor.Equals(Worksheet.StyleValue.Font.FontColor)) + Style.Font.FontColor = XLColor.FromTheme(XLThemeColor.Hyperlink); + + if (GetStyleForRead().Font.Underline == Worksheet.StyleValue.Font.Underline) + Style.Font.Underline = XLFontUnderlineValues.Single; + } + } + + public IXLCells InsertCellsAbove(int numberOfRows) + { + return AsRange().InsertRowsAbove(numberOfRows).Cells(); + } + + public IXLCells InsertCellsBelow(int numberOfRows) + { + return AsRange().InsertRowsBelow(numberOfRows).Cells(); + } + + public IXLCells InsertCellsAfter(int numberOfColumns) + { + return AsRange().InsertColumnsAfter(numberOfColumns).Cells(); + } + + public IXLCells InsertCellsBefore(int numberOfColumns) + { + return AsRange().InsertColumnsBefore(numberOfColumns).Cells(); + } + + public IXLCell AddToNamed(string rangeName) + { + AsRange().AddToNamed(rangeName); + return this; + } + + public IXLCell AddToNamed(string rangeName, XLScope scope) + { + AsRange().AddToNamed(rangeName, scope); + return this; + } + + public IXLCell AddToNamed(string rangeName, XLScope scope, string comment) + { + AsRange().AddToNamed(rangeName, scope, comment); + return this; + } + + private bool _recalculationNeededLastValue; + + /// + /// Flag indicating that previously calculated cell value may be not valid anymore and has to be re-evaluated. + /// + public bool NeedsRecalculation + { + get + { + if (String.IsNullOrWhiteSpace(_formulaA1) && String.IsNullOrEmpty(_formulaR1C1)) + return false; + + if (NeedsRecalculationEvaluatedAtVersion == Worksheet.Workbook.RecalculationCounter) + return _recalculationNeededLastValue; + + bool res = EvaluatedAtVersion < ModifiedAtVersion || // the cell itself was modified + GetAffectingCells().Any(cell => cell.ModifiedAtVersion > EvaluatedAtVersion || // the affecting cell was modified after this one was evaluated + cell.EvaluatedAtVersion > EvaluatedAtVersion || // the affecting cell was evaluated after this one (normally this should not happen) + cell.NeedsRecalculation); // the affecting cell needs recalculation (recursion to walk through dependencies) + + NeedsRecalculation = res; + return res; + } + internal set + { + _recalculationNeededLastValue = value; + NeedsRecalculationEvaluatedAtVersion = Worksheet.Workbook.RecalculationCounter; + } + } + + private IEnumerable GetAffectingCells() + { + return Worksheet.CalcEngine.GetPrecedentCells(_formulaA1).Cast(); + } + + /// + /// The value of that workbook had at the moment of cell last modification. + /// If this value is greater than then cell needs re-evaluation, as well as all dependent cells do. + /// + private long ModifiedAtVersion { get; set; } + + /// + /// The value of that workbook had at the moment of cell formula evaluation. + /// If this value equals to it indicates that stores + /// correct value and no re-evaluation has to be performed. + /// + private long EvaluatedAtVersion { get; set; } + + /// + /// The value of that workbook had at the moment of determining whether the cell + /// needs re-evaluation (due to it has been edited or some of the affecting cells has). If thie value equals to + /// it indicates that stores correct value and no check has to be performed. + /// + private long NeedsRecalculationEvaluatedAtVersion { get; set; } + + private Object cachedValue; + + public Object CachedValue + { + get + { + if (!HasFormula && cachedValue == null) + cachedValue = Value; + + return cachedValue; + } + private set + { + if (value != null && !HasFormula) + throw new InvalidOperationException("Cached values can be set only for cells with formulas"); + + cachedValue = value; + } + } + + public IXLRichText RichText + { + get + { + if (_richText == null) + { + var style = GetStyleForRead(); + _richText = _cellValue.Length == 0 + ? new XLRichText(new XLFont(Style as XLStyle, style.Font)) + : new XLRichText(GetFormattedString(), new XLFont(Style as XLStyle, style.Font)); + } + + return _richText; + } + } + + public bool HasRichText + { + get { return _richText != null; } + } + + IXLComment IXLCell.Comment + { + get { return Comment; } + } + + public bool HasComment + { + get { return _comment != null; } + } + + public Boolean IsMerged() + { + return Worksheet.Internals.MergedRanges.Contains(this); + } + + public IXLRange MergedRange() + { + return Worksheet + .Internals + .MergedRanges + .GetIntersectedRanges(this) + .FirstOrDefault(); + } + + public Boolean IsEmpty() + { + return IsEmpty(XLCellsUsedOptions.AllContents); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public Boolean IsEmpty(Boolean includeFormats) + { + return IsEmpty(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public Boolean IsEmpty(XLCellsUsedOptions options) + { + if (InnerText.Length > 0) + return false; + + if (options.HasFlag(XLCellsUsedOptions.NormalFormats)) + { + if (StyleValue.IncludeQuotePrefix) + return false; + + if (!StyleValue.Equals(Worksheet.StyleValue)) + return false; + + if (StyleValue.Equals(Worksheet.StyleValue)) + { + if (Worksheet.Internals.RowsCollection.TryGetValue(_rowNumber, out XLRow row) && !row.StyleValue.Equals(Worksheet.StyleValue)) + return false; + + if (Worksheet.Internals.ColumnsCollection.TryGetValue(_columnNumber, out XLColumn column) && !column.StyleValue.Equals(Worksheet.StyleValue)) + return false; + } + } + + if (options.HasFlag(XLCellsUsedOptions.MergedRanges) && IsMerged()) + return false; + + if (options.HasFlag(XLCellsUsedOptions.Comments) && HasComment) + return false; + + if (options.HasFlag(XLCellsUsedOptions.DataValidation) && HasDataValidation) + return false; + + if (options.HasFlag(XLCellsUsedOptions.ConditionalFormats) + && Worksheet.ConditionalFormats.SelectMany(cf => cf.Ranges).Any(range => range.Contains(this))) + return false; + + if (options.HasFlag(XLCellsUsedOptions.Sparklines) && HasSparkline) + return false; + + return true; + } + + public IXLColumn WorksheetColumn() + { + return Worksheet.Column(_columnNumber); + } + + public IXLRow WorksheetRow() + { + return Worksheet.Row(_rowNumber); + } + + public IXLCell CopyTo(IXLCell target) + { + (target as XLCell).CopyFrom(this, XLCellCopyOptions.All); + return target; + } + + public IXLCell CopyTo(String target) + { + return CopyTo(GetTargetCell(target, Worksheet)); + } + + public IXLCell CopyFrom(IXLCell otherCell) + { + return CopyFrom(otherCell as XLCell, XLCellCopyOptions.All); + } + + public IXLCell CopyFrom(String otherCell) + { + return CopyFrom(GetTargetCell(otherCell, Worksheet)); + } + + public IXLCell SetFormulaA1(String formula) + { + FormulaA1 = formula; + return this; + } + + public IXLCell SetFormulaR1C1(String formula) + { + FormulaR1C1 = formula; + return this; + } + + public Boolean HasSparkline => Sparkline != null; + + /// The sparkline assigned to the cell + public IXLSparkline Sparkline => Worksheet.SparklineGroups.GetSparkline(this); + + public Boolean HasDataValidation + { + get { return GetDataValidation() != null; } + } + + /// + /// Get the data validation rule containing current cell. + /// + /// The data validation rule applying to the current cell or null if there is no such rule. + private IXLDataValidation GetDataValidation() + { + Worksheet.DataValidations.TryGet(AsRange().RangeAddress, out var dataValidation); + return dataValidation; + } + + public IXLDataValidation SetDataValidation() + { + var validation = GetDataValidation(); + if (validation == null) + { + validation = new XLDataValidation(AsRange()); + Worksheet.DataValidations.Add(validation); + } + return validation; + } + + public void Select() + { + AsRange().Select(); + } + + public IXLConditionalFormat AddConditionalFormat() + { + return AsRange().AddConditionalFormat(); + } + + public Boolean Active + { + get { return Worksheet.ActiveCell == this; } + set + { + if (value) + Worksheet.ActiveCell = this; + else if (Active) + Worksheet.ActiveCell = null; + } + } + + public IXLCell SetActive(Boolean value = true) + { + Active = value; + return this; + } + + public Boolean HasHyperlink + { + get { return _hyperlink != null; } + } + + public XLHyperlink GetHyperlink() + { + if (HasHyperlink) + return Hyperlink; + + return Value as XLHyperlink; + } + + public Boolean TryGetValue(out T value) + { + var targetType = typeof(T); + var underlyingType = targetType.GetUnderlyingType(); + var isNullable = targetType.IsNullableType(); + + Object currentValue; + try + { + currentValue = Value; + } + catch + { + // May fail for formula evaluation + value = default; + return false; + } + + if (isNullable && (currentValue == null || currentValue is string s && String.IsNullOrEmpty(s))) + { + value = default; + return true; + } + + if (targetType != typeof(String) // Strings are handled later and have some specifics to UTF handling + && currentValue is T t) + { + value = t; + return true; + } + + if (TryGetDateTimeValue(out value, currentValue)) return true; + + if (TryGetTimeSpanValue(out value, currentValue)) return true; + + if (TryGetBooleanValue(out value, currentValue)) return true; + + if (TryGetRichStringValue(out value)) return true; + + if (TryGetStringValue(out value, currentValue)) return true; + + if (TryGetHyperlink(out value)) return true; + + if (currentValue.IsNumber()) + { + try + { + value = (T)Convert.ChangeType(currentValue, underlyingType); + return true; + } + catch (Exception) + { + value = default; + return false; + } + } + + var strValue = currentValue.ToString(); + + if (underlyingType == typeof(sbyte)) return TryGetBasicValue(strValue, sbyte.TryParse, out value); + if (underlyingType == typeof(byte)) return TryGetBasicValue(strValue, byte.TryParse, out value); + if (underlyingType == typeof(short)) return TryGetBasicValue(strValue, short.TryParse, out value); + if (underlyingType == typeof(ushort)) return TryGetBasicValue(strValue, ushort.TryParse, out value); + if (underlyingType == typeof(int)) return TryGetBasicValue(strValue, int.TryParse, out value); + if (underlyingType == typeof(uint)) return TryGetBasicValue(strValue, uint.TryParse, out value); + if (underlyingType == typeof(long)) return TryGetBasicValue(strValue, long.TryParse, out value); + if (underlyingType == typeof(ulong)) return TryGetBasicValue(strValue, ulong.TryParse, out value); + if (underlyingType == typeof(float)) return TryGetBasicValue(strValue, float.TryParse, out value); + if (underlyingType == typeof(double)) return TryGetBasicValue(strValue, double.TryParse, out value); + if (underlyingType == typeof(decimal)) return TryGetBasicValue(strValue, decimal.TryParse, out value); + + if (underlyingType.IsEnum) + { + if (Enum.IsDefined(underlyingType, strValue)) + { + value = (T)Enum.Parse(underlyingType, strValue, ignoreCase: false); + return true; + } + value = default; + return false; + } + + try + { + value = (T)Convert.ChangeType(currentValue, targetType); + return true; + } + catch + { + value = default; + return false; + } + } + + private static bool TryGetDateTimeValue(out T value, object currentValue) + { + if (typeof(T) != typeof(DateTime) && typeof(T) != typeof(DateTime?)) + { + value = default; + return false; + } + + if (currentValue is T v) { value = v; return true; } + + if (currentValue.IsNumber()) + { + var dbl1 = Convert.ToDouble(currentValue); + if (dbl1.IsValidOADateNumber()) + { + value = (T)Convert.ChangeType(DateTime.FromOADate(dbl1), typeof(T)); + return true; + } + } + + if (DateTime.TryParse(currentValue.ToString(), out DateTime ts)) + { + value = (T)Convert.ChangeType(ts, typeof(T)); + return true; + } + + // If the cell value is a string, e.g. "42020", we could theoretically coerce it to a DateTime, but this seems to go against what is expected. + // Leaving this code block here, though. Maybe we revert our decision later. + + //if (Double.TryParse(currentValue.ObjectToInvariantString(), out Double dbl2) && dbl2.IsValidOADateNumber()) + //{ + // value = (T)Convert.ChangeType(DateTime.FromOADate(dbl2), typeof(T)); + // return true; + //} + + value = default; + return false; + } + + private static bool TryGetTimeSpanValue(out T value, object currentValue) + { + if (typeof(T) != typeof(TimeSpan) && typeof(T) != typeof(TimeSpan?)) + { + value = default; + return false; + } + + if (currentValue is T v) { value = v; return true; } + + if (!TimeSpan.TryParse(currentValue.ToString(), out TimeSpan ts)) + { + value = default; + return false; + } + + value = (T)Convert.ChangeType(ts, typeof(T)); + return true; + } + + private bool TryGetRichStringValue(out T value) + { + if (typeof(T) == typeof(IXLRichText)) + { + value = (T)RichText; + return true; + } + value = default; + return false; + } + + private static bool TryGetStringValue(out T value, object currentValue) + { + if (typeof(T) == typeof(String)) + { + var s = currentValue.ToString(); + var matches = utfPattern.Matches(s); + + if (matches.Count == 0) + { + value = (T)Convert.ChangeType(s, typeof(T)); + return true; + } + + var sb = new StringBuilder(); + var lastIndex = 0; + + foreach (var match in matches.Cast()) + { + var matchString = match.Value; + var matchIndex = match.Index; + sb.Append(s.Substring(lastIndex, matchIndex - lastIndex)); + + sb.Append((char)int.Parse(match.Groups[1].Value, NumberStyles.AllowHexSpecifier)); + + lastIndex = matchIndex + matchString.Length; + } + + if (lastIndex < s.Length) + sb.Append(s.Substring(lastIndex)); + + value = (T)Convert.ChangeType(sb.ToString(), typeof(T)); + return true; + } + value = default; + return false; + } + + private static Boolean TryGetBooleanValue(out T value, object currentValue) + { + if (typeof(T) != typeof(Boolean) && typeof(T) != typeof(Boolean?)) + { + value = default; + return false; + } + + if (currentValue is T v) { value = v; return true; } + + if (!Boolean.TryParse(currentValue.ToString(), out Boolean b)) + { + value = default; + return false; + } + + value = (T)Convert.ChangeType(b, typeof(T)); + return true; + } + + private Boolean TryGetHyperlink(out T value) + { + if (typeof(T) == typeof(XLHyperlink)) + { + var hyperlink = GetHyperlink(); + if (hyperlink != null) + { + value = (T)Convert.ChangeType(hyperlink, typeof(T)); + return true; + } + } + + value = default; + return false; + } + + private delegate Boolean ParseFunction(String s, NumberStyles style, IFormatProvider provider, out T result); + + private static Boolean TryGetBasicValue(String currentValue, ParseFunction parseFunction, out T value) + { + if (parseFunction.Invoke(currentValue, NumberStyles.Any, null, out U result)) + { + value = (T)Convert.ChangeType(result, typeof(T).GetUnderlyingType()); + return true; + } + + value = default; + return false; + } + + #endregion IXLCell Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + } + } + + void IXLStylized.ModifyStyle(Func modification) + { + //XLCell cannot have children so the base method may be optimized + var styleKey = modification(StyleValue.Key); + StyleValue = XLStyleValue.FromKey(ref styleKey); + } + + protected override IEnumerable Children + { + get { yield break; } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges { AsRange() }; + return retVal; + } + } + + #endregion IXLStylized Members + + private Boolean SetTableHeaderValue(object value) + { + foreach (var table in Worksheet.Tables.Where(t => t.ShowHeaderRow)) + { + var cell = table.HeadersRow().CellsUsed(c => c.Address.Equals(this.Address)).FirstOrDefault(); + if (cell != null) + { + var oldName = cell.GetString(); + var field = table.Field(oldName); + field.Name = value.ToString(); + return true; + } + } + + return false; + } + + private Boolean SetTableTotalsRowLabel(object value) + { + foreach (var table in Worksheet.Tables.Where(t => t.ShowTotalsRow)) + { + var cell = table.TotalsRow().Cells(c => c.Address.Equals(this.Address)).FirstOrDefault(); + if (cell != null) + { + var field = table.Fields.First(f => f.Column.ColumnNumber() == cell.WorksheetColumn().ColumnNumber()); + field.TotalsRowFunction = XLTotalsRowFunction.None; + + SetInternalCellValueString(value.ObjectToInvariantString(), validate: true, parseToCachedValue: false); + + field.TotalsRowLabel = _cellValue; + this.DataType = XLDataType.Text; + return true; + } + } + + return false; + } + + private bool SetRangeColumns(object value) + { + if (value is XLRangeColumns columns) + { + var cell = this; + foreach (var column in columns) + { + cell.SetRange(column); + cell = cell.CellRight(); + } + return true; + } + else + + return SetColumns(value); + } + + private bool SetColumns(object value) + { + if (value is XLColumns columns) + { + var cell = this; + foreach (var column in columns) + { + cell.SetRange(column); + cell = cell.CellRight(); + } + return true; + } + else + return false; + } + + private bool SetRangeRows(object value) + { + if (value is XLRangeRows rows) + { + var cell = this; + foreach (var row in rows) + { + cell.SetRange(row); + cell = cell.CellBelow(); + } + return true; + } + else + return SetRows(value); + } + + private bool SetRows(object value) + { + if (value is XLRows rows) + { + var cell = this; + foreach (var row in rows) + { + cell.SetRange(row); + cell = cell.CellBelow(); + } + return true; + } + else + return false; + } + + public XLRange AsRange() + { + return Worksheet.Range(Address, Address); + } + + #region Styles + + private XLStyleValue GetStyleForRead() + { + return StyleValue; + } + + private void SetStyle(IXLStyle styleToUse) + { + Style = styleToUse; + } + + public Boolean IsDefaultWorksheetStyle() + { + return StyleValue == Worksheet.StyleValue; + } + + #endregion Styles + + public void DeleteComment() + { + Clear(XLClearOptions.Comments); + } + + public void DeleteSparkline() + { + Clear(XLClearOptions.Sparklines); + } + + private bool IsDateFormat() + { + var style = GetStyleForRead(); + return _dataType == XLDataType.Number + && String.IsNullOrWhiteSpace(style.NumberFormat.Format) + && ((style.NumberFormat.NumberFormatId >= 14 + && style.NumberFormat.NumberFormatId <= 22) + || (style.NumberFormat.NumberFormatId >= 45 + && style.NumberFormat.NumberFormatId <= 47)); + } + + private string GetFormat() + { + var style = GetStyleForRead(); + if (String.IsNullOrWhiteSpace(style.NumberFormat.Format)) + { + var formatCodes = XLPredefinedFormat.FormatCodes; + if (formatCodes.TryGetValue(style.NumberFormat.NumberFormatId, out string format)) + return format; + else + return string.Empty; + } + else + return style.NumberFormat.Format; + } + + private bool SetRichText(object value) + { + if (value is XLRichText asRichString) + { + _richText = asRichString; + _dataType = XLDataType.Text; + return true; + } + else + return false; + } + + private Boolean SetRange(Object rangeObject) + { + var asRange = (rangeObject as XLRangeBase) + ?? (rangeObject as XLCell)?.AsRange(); + + if (asRange != null) + { + var maxRows = asRange.RowCount(); + var maxColumns = asRange.ColumnCount(); + + var lastRow = Math.Min(_rowNumber + maxRows - 1, XLHelper.MaxRowNumber); + var lastColumn = Math.Min(_columnNumber + maxColumns - 1, XLHelper.MaxColumnNumber); + + var targetRange = Worksheet.Range(_rowNumber, _columnNumber, lastRow, lastColumn); + + if (!(asRange is XLRow || asRange is XLColumn)) + { + targetRange.Clear(); + } + + var minRow = asRange.RangeAddress.FirstAddress.RowNumber; + var minColumn = asRange.RangeAddress.FirstAddress.ColumnNumber; + var cellsUsed = asRange.CellsUsed(XLCellsUsedOptions.All + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation + & ~XLCellsUsedOptions.MergedRanges); + foreach (var sourceCell in cellsUsed) + { + Worksheet.Cell( + _rowNumber + sourceCell.Address.RowNumber - minRow, + _columnNumber + sourceCell.Address.ColumnNumber - minColumn + ).CopyFromInternal(sourceCell as XLCell, + XLCellCopyOptions.All & ~XLCellCopyOptions.ConditionalFormats); //Conditional formats are copied separately + } + + var rangesToMerge = asRange.Worksheet.Internals.MergedRanges + .Where(mr => asRange.Contains(mr)) + .Select(mr => + { + var firstRow = _rowNumber + (mr.RangeAddress.FirstAddress.RowNumber - asRange.RangeAddress.FirstAddress.RowNumber); + var firstColumn = _columnNumber + (mr.RangeAddress.FirstAddress.ColumnNumber - asRange.RangeAddress.FirstAddress.ColumnNumber); + return (IXLRange)Worksheet.Range + ( + firstRow, + firstColumn, + firstRow + mr.RowCount() - 1, + firstColumn + mr.ColumnCount() - 1 + ); + }) + .ToList(); + + rangesToMerge.ForEach(r => r.Merge(false)); + + var dataValidations = asRange.Worksheet.DataValidations + .GetAllInRange(asRange.RangeAddress) + .ToList(); + + foreach (var dataValidation in dataValidations) + { + XLDataValidation newDataValidation = null; + foreach (var dvRange in dataValidation.Ranges.Where(r => r.Intersects(asRange))) + { + var dvTargetAddress = dvRange.RangeAddress.Relative(asRange.RangeAddress, targetRange.RangeAddress); + var dvTargetRange = Worksheet.Range(dvTargetAddress); + if (newDataValidation == null) + { + newDataValidation = dvTargetRange.SetDataValidation() as XLDataValidation; + newDataValidation.CopyFrom(dataValidation); + } + else + newDataValidation.AddRange(dvTargetRange); + } + } + + CopyConditionalFormatsFrom(asRange); + + return true; + } + + return false; + } + + private void CopyConditionalFormatsFrom(XLCell otherCell) + { + var conditionalFormats = otherCell + .Worksheet + .ConditionalFormats + .Where(c => c.Ranges.GetIntersectedRanges(otherCell).Any()) + .ToList(); + + foreach (var cf in conditionalFormats) + { + if (otherCell.Worksheet == Worksheet) + { + if (!cf.Ranges.GetIntersectedRanges(this).Any()) + { + cf.Ranges.Add(this); + } + } + else + { + CopyConditionalFormatsFrom(otherCell.AsRange()); + } + } + } + + private void CopyConditionalFormatsFrom(XLRangeBase fromRange) + { + var srcSheet = fromRange.Worksheet; + int minRo = fromRange.RangeAddress.FirstAddress.RowNumber; + int minCo = fromRange.RangeAddress.FirstAddress.ColumnNumber; + if (srcSheet.ConditionalFormats.Any(r => r.Ranges.GetIntersectedRanges(fromRange.RangeAddress).Any())) + { + var fs = srcSheet.ConditionalFormats.SelectMany(cf => cf.Ranges.GetIntersectedRanges(fromRange.RangeAddress)).ToArray(); + if (fs.Any()) + { + minRo = fs.Max(r => r.RangeAddress.LastAddress.RowNumber); + minCo = fs.Max(r => r.RangeAddress.LastAddress.ColumnNumber); + } + } + int rCnt = minRo - fromRange.RangeAddress.FirstAddress.RowNumber + 1; + int cCnt = minCo - fromRange.RangeAddress.FirstAddress.ColumnNumber + 1; + rCnt = Math.Min(rCnt, fromRange.RowCount()); + cCnt = Math.Min(cCnt, fromRange.ColumnCount()); + var toRange = Worksheet.Range(this, Worksheet.Cell(_rowNumber + rCnt - 1, _columnNumber + cCnt - 1)); + var formats = srcSheet.ConditionalFormats.Where(f => f.Ranges.GetIntersectedRanges(fromRange.RangeAddress).Any()); + + foreach (var cf in formats.ToList()) + { + var fmtRanges = cf.Ranges + .GetIntersectedRanges(fromRange.RangeAddress) + .Select(r => r.RangeAddress.Intersection(fromRange.RangeAddress).Relative(fromRange.RangeAddress, toRange.RangeAddress).AsRange() as XLRange) + .ToList(); + + var c = new XLConditionalFormat(fmtRanges, true); + c.CopyFrom(cf); + c.AdjustFormulas((XLCell)cf.Ranges.First().FirstCell(), fmtRanges.First().FirstCell()); + + Worksheet.ConditionalFormats.Add(c); + } + } + + private bool SetDataTable(object o) + { + if (o is DataTable dataTable) + return InsertData(dataTable) != null; + else + return false; + } + + private bool SetEnumerable(object collectionObject) + { + // IXLRichText implements IEnumerable, but we don't want to handle this here. + if (collectionObject is IXLRichText) return false; + + var asEnumerable = collectionObject as IEnumerable; + return InsertData(asEnumerable) != null; + } + + private void ClearMerged() + { + List mergeToDelete = Worksheet.Internals.MergedRanges.GetIntersectedRanges(Address).ToList(); + + mergeToDelete.ForEach(m => Worksheet.Internals.MergedRanges.Remove(m)); + } + + private void SetValue(object value) + { + if (value == null) + { + this.Clear(XLClearOptions.Contents); + return; + } + + FormulaA1 = String.Empty; + _richText = null; + + var style = GetStyleForRead(); + Boolean parsed = false; + string parsedValue = string.Empty; + + //// + // Try easy parsing first. If that doesn't work, we'll have to ToString it and parse it slowly + + // When number format starts with @, we treat any value as text - no parsing required + // This doesn't happen in the SetValue() version + if (style.NumberFormat.Format == "@") + { + parsedValue = value.ObjectToInvariantString(); + + _dataType = XLDataType.Text; + if (parsedValue.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + + parsed = true; + } + else + { + // Don't accept strings, because we're going to try to parse them later + var tuple = SetKnownTypedValue(value, style, acceptString: false); + parsedValue = tuple.Item1; + parsed = tuple.Item2; + } + + //// + if (!parsed) + { + // We'll have to parse it slowly :-( + parsedValue = DeduceCellValueByParsing(value.ToString(), style); + } + + if (SetTableHeaderValue(parsedValue)) return; + if (SetTableTotalsRowLabel(parsedValue)) return; + + SetInternalCellValueString(parsedValue, validate: true, parseToCachedValue: false); + CachedValue = null; + } + + private void SetDateTimeFormat(XLStyleValue style, Boolean onlyDatePart) + { + _dataType = XLDataType.DateTime; + + if (style.NumberFormat.Format.Length == 0 && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = onlyDatePart ? 14 : 22; + } + + private void SetTimeSpanFormat(XLStyleValue style) + { + _dataType = XLDataType.TimeSpan; + + if (style.NumberFormat.Format.Length == 0 && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = 46; + } + + internal string GetFormulaR1C1(string value) + { + return GetFormula(value, FormulaConversionType.A1ToR1C1, 0, 0); + } + + internal string GetFormulaA1(string value) + { + return GetFormula(value, FormulaConversionType.R1C1ToA1, 0, 0); + } + + private string GetFormula(string strValue, FormulaConversionType conversionType, int rowsToShift, + int columnsToShift) + { + if (String.IsNullOrWhiteSpace(strValue)) + return String.Empty; + + var value = ">" + strValue + "<"; + + var regex = conversionType == FormulaConversionType.A1ToR1C1 ? A1Regex : R1C1Regex; + + var sb = new StringBuilder(); + var lastIndex = 0; + + foreach (var match in regex.Matches(value).Cast()) + { + var matchString = match.Value; + var matchIndex = match.Index; + if (value.Substring(0, matchIndex).CharCount('"') % 2 == 0 + && value.Substring(0, matchIndex).CharCount('\'') % 2 == 0) + { + // Check if the match is in between quotes + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex)); + sb.Append(conversionType == FormulaConversionType.A1ToR1C1 + ? GetR1C1Address(matchString, rowsToShift, columnsToShift) + : GetA1Address(matchString, rowsToShift, columnsToShift)); + } + else + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex + matchString.Length)); + lastIndex = matchIndex + matchString.Length; + } + + if (lastIndex < value.Length) + sb.Append(value.Substring(lastIndex)); + + var retVal = sb.ToString(); + return retVal.Substring(1, retVal.Length - 2); + } + + private string GetA1Address(string r1C1Address, int rowsToShift, int columnsToShift) + { + var addressToUse = r1C1Address.ToUpper(); + + if (addressToUse.Contains(':')) + { + var parts = addressToUse.Split(':'); + var p1 = parts[0]; + var p2 = parts[1]; + string leftPart; + string rightPart; + if (p1.StartsWith("R")) + { + leftPart = GetA1Row(p1, rowsToShift); + rightPart = GetA1Row(p2, rowsToShift); + } + else + { + leftPart = GetA1Column(p1, columnsToShift); + rightPart = GetA1Column(p2, columnsToShift); + } + + return leftPart + ":" + rightPart; + } + + try + { + var rowPart = addressToUse.Substring(0, addressToUse.IndexOf("C")); + var rowToReturn = GetA1Row(rowPart, rowsToShift); + + var columnPart = addressToUse.Substring(addressToUse.IndexOf("C")); + var columnToReturn = GetA1Column(columnPart, columnsToShift); + + var retAddress = columnToReturn + rowToReturn; + return retAddress; + } + catch (ArgumentOutOfRangeException) + { + return "#REF!"; + } + } + + private string GetA1Column(string columnPart, int columnsToShift) + { + string columnToReturn; + if (columnPart == "C") + columnToReturn = XLHelper.GetColumnLetterFromNumber(_columnNumber + columnsToShift); + else + { + var bIndex = columnPart.IndexOf("["); + var mIndex = columnPart.IndexOf("-"); + if (bIndex >= 0) + { + columnToReturn = XLHelper.GetColumnLetterFromNumber( + _columnNumber + + Int32.Parse(columnPart.Substring(bIndex + 1, columnPart.Length - bIndex - 2)) + columnsToShift + ); + } + else if (mIndex >= 0) + { + columnToReturn = XLHelper.GetColumnLetterFromNumber( + _columnNumber + Int32.Parse(columnPart.Substring(mIndex)) + columnsToShift + ); + } + else + { + columnToReturn = "$" + + XLHelper.GetColumnLetterFromNumber(Int32.Parse(columnPart.Substring(1)) + + columnsToShift); + } + } + + return columnToReturn; + } + + private string GetA1Row(string rowPart, int rowsToShift) + { + string rowToReturn; + if (rowPart == "R") + rowToReturn = (_rowNumber + rowsToShift).ToString(); + else + { + var bIndex = rowPart.IndexOf("["); + if (bIndex >= 0) + { + rowToReturn = + (_rowNumber + Int32.Parse(rowPart.Substring(bIndex + 1, rowPart.Length - bIndex - 2)) + + rowsToShift).ToString(); + } + else + rowToReturn = "$" + (Int32.Parse(rowPart.Substring(1)) + rowsToShift); + } + + return rowToReturn; + } + + private string GetR1C1Address(string a1Address, int rowsToShift, int columnsToShift) + { + if (a1Address.Contains(':')) + { + var parts = a1Address.Split(':'); + var p1 = parts[0]; + var p2 = parts[1]; + if (Int32.TryParse(p1.Replace("$", string.Empty), out Int32 row1)) + { + var row2 = Int32.Parse(p2.Replace("$", string.Empty)); + var leftPart = GetR1C1Row(row1, p1.Contains('$'), rowsToShift); + var rightPart = GetR1C1Row(row2, p2.Contains('$'), rowsToShift); + return leftPart + ":" + rightPart; + } + else + { + var column1 = XLHelper.GetColumnNumberFromLetter(p1.Replace("$", string.Empty)); + var column2 = XLHelper.GetColumnNumberFromLetter(p2.Replace("$", string.Empty)); + var leftPart = GetR1C1Column(column1, p1.Contains('$'), columnsToShift); + var rightPart = GetR1C1Column(column2, p2.Contains('$'), columnsToShift); + return leftPart + ":" + rightPart; + } + } + + var address = XLAddress.Create(Worksheet, a1Address); + + var rowPart = GetR1C1Row(address.RowNumber, address.FixedRow, rowsToShift); + var columnPart = GetR1C1Column(address.ColumnNumber, address.FixedColumn, columnsToShift); + + return rowPart + columnPart; + } + + private string GetR1C1Row(int rowNumber, bool fixedRow, int rowsToShift) + { + string rowPart; + rowNumber += rowsToShift; + var rowDiff = rowNumber - _rowNumber; + if (rowDiff != 0 || fixedRow) + rowPart = fixedRow ? "R" + rowNumber : "R[" + rowDiff + "]"; + else + rowPart = "R"; + + return rowPart; + } + + private string GetR1C1Column(int columnNumber, bool fixedColumn, int columnsToShift) + { + string columnPart; + columnNumber += columnsToShift; + var columnDiff = columnNumber - _columnNumber; + if (columnDiff != 0 || fixedColumn) + columnPart = fixedColumn ? "C" + columnNumber : "C[" + columnDiff + "]"; + else + columnPart = "C"; + + return columnPart; + } + + internal void CopyValuesFrom(XLCell source) + { + _cellValue = source._cellValue; + _dataType = source._dataType; + FormulaR1C1 = source.FormulaR1C1; + _richText = source._richText == null ? null : new XLRichText(source._richText, source.Style.Font); + _comment = source._comment == null ? null : new XLComment(this, source._comment, source.Style.Font, source._comment.Style); + if (source._hyperlink != null) + { + SettingHyperlink = true; + Hyperlink = new XLHyperlink(source.Hyperlink); + SettingHyperlink = false; + } + } + + private IXLCell GetTargetCell(String target, XLWorksheet defaultWorksheet) + { + var pair = target.Split('!'); + if (pair.Length == 1) + return defaultWorksheet.Cell(target); + + var wsName = pair[0]; + if (wsName.StartsWith("'")) + wsName = wsName.Substring(1, wsName.Length - 2); + return defaultWorksheet.Workbook.Worksheet(wsName).Cell(pair[1]); + } + + internal IXLCell CopyFromInternal(XLCell otherCell, XLCellCopyOptions options) + { + if (options.HasFlag(XLCellCopyOptions.Values)) + CopyValuesFrom(otherCell); + + if (options.HasFlag(XLCellCopyOptions.Styles)) + InnerStyle = otherCell.InnerStyle; + + if (options.HasFlag(XLCellCopyOptions.Sparklines)) + CopySparklineFrom(otherCell); + + if (options.HasFlag(XLCellCopyOptions.ConditionalFormats)) + CopyConditionalFormatsFrom(otherCell); + + if (options.HasFlag(XLCellCopyOptions.DataValidations)) + CopyDataValidationFrom(otherCell); + + return this; + } + + private void CopySparklineFrom(XLCell otherCell) + { + if (!otherCell.HasSparkline) return; + + var sourceDataAddress = otherCell.Sparkline.SourceData.RangeAddress.ToString(); + var shiftedRangeAddress = GetFormulaA1(otherCell.GetFormulaR1C1(sourceDataAddress)); + var sourceDataWorksheet = otherCell.Worksheet == otherCell.Sparkline.SourceData.Worksheet + ? Worksheet + : otherCell.Sparkline.SourceData.Worksheet; + var sourceData = sourceDataWorksheet.Range(shiftedRangeAddress); + + IXLSparklineGroup group; + if (otherCell.Worksheet == Worksheet) + { + group = otherCell.Sparkline.SparklineGroup; + } + else + { + group = Worksheet.SparklineGroups.Add(new XLSparklineGroup(Worksheet, otherCell.Sparkline.SparklineGroup)); + if (otherCell.Sparkline.SparklineGroup.DateRange != null) + { + var dateRangeWorksheet = + otherCell.Worksheet == otherCell.Sparkline.SparklineGroup.DateRange.Worksheet + ? Worksheet + : otherCell.Sparkline.SparklineGroup.DateRange.Worksheet; + var dateRangeAddress = otherCell.Sparkline.SparklineGroup.DateRange.RangeAddress.ToString(); + var shiftedDateRangeAddress = GetFormulaA1(otherCell.GetFormulaR1C1(dateRangeAddress)); + group.SetDateRange(dateRangeWorksheet.Range(shiftedDateRangeAddress)); + } + } + + group.Add(this, sourceData); + } + + public IXLCell CopyFrom(IXLCell otherCell, XLCellCopyOptions options) + { + var source = otherCell as XLCell; // To expose GetFormulaR1C1, etc + + CopyFromInternal(source, options); + return this; + } + + private void CopyDataValidationFrom(XLCell otherCell) + { + var eventTracking = Worksheet.EventTrackingEnabled; + Worksheet.EventTrackingEnabled = false; + if (otherCell.HasDataValidation) + CopyDataValidation(otherCell, otherCell.DataValidation); + else if (HasDataValidation) + { + Worksheet.DataValidations.Delete(AsRange()); + } + Worksheet.EventTrackingEnabled = eventTracking; + } + + internal void CopyDataValidation(XLCell otherCell, IXLDataValidation otherDv) + { + var thisDv = SetDataValidation() as XLDataValidation; + thisDv.CopyFrom(otherDv); + thisDv.Value = GetFormulaA1(otherCell.GetFormulaR1C1(otherDv.Value)); + thisDv.MinValue = GetFormulaA1(otherCell.GetFormulaR1C1(otherDv.MinValue)); + thisDv.MaxValue = GetFormulaA1(otherCell.GetFormulaR1C1(otherDv.MaxValue)); + } + + internal void ShiftFormulaRows(XLRange shiftedRange, int rowsShifted) + { + FormulaA1 = ShiftFormulaRows(FormulaA1, Worksheet, shiftedRange, rowsShifted); + } + + internal static String ShiftFormulaRows(String formulaA1, XLWorksheet worksheetInAction, XLRange shiftedRange, + int rowsShifted) + { + if (String.IsNullOrWhiteSpace(formulaA1)) return String.Empty; + + var value = formulaA1; + + var regex = A1SimpleRegex; + + var sb = new StringBuilder(); + var lastIndex = 0; + + var shiftedWsName = shiftedRange.Worksheet.Name; + foreach (var match in regex.Matches(value).Cast()) + { + var matchString = match.Value; + var matchIndex = match.Index; + if (value.Substring(0, matchIndex).CharCount('"') % 2 == 0) + { + // Check that the match is not between quotes + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex)); + string sheetName; + var useSheetName = false; + if (matchString.Contains('!')) + { + sheetName = matchString.Substring(0, matchString.IndexOf('!')); + if (sheetName[0] == '\'') + sheetName = sheetName.Substring(1, sheetName.Length - 2); + useSheetName = true; + } + else + sheetName = worksheetInAction.Name; + + if (String.Compare(sheetName, shiftedWsName, true) == 0) + { + var rangeAddress = matchString.Substring(matchString.IndexOf('!') + 1); + if (!A1ColumnRegex.IsMatch(rangeAddress)) + { + var matchRange = worksheetInAction.Workbook.Worksheet(sheetName).Range(rangeAddress); + if (shiftedRange.RangeAddress.FirstAddress.RowNumber <= matchRange.RangeAddress.LastAddress.RowNumber + && shiftedRange.RangeAddress.FirstAddress.ColumnNumber <= matchRange.RangeAddress.FirstAddress.ColumnNumber + && shiftedRange.RangeAddress.LastAddress.ColumnNumber >= matchRange.RangeAddress.LastAddress.ColumnNumber) + { + if (useSheetName) + { + sb.Append(sheetName.EscapeSheetName()); + sb.Append('!'); + } + + if (A1RowRegex.IsMatch(rangeAddress)) + { + var rows = rangeAddress.Split(':'); + var row1String = rows[0]; + var row2String = rows[1]; + string row1; + if (row1String[0] == '$') + { + row1 = "$" + + (XLHelper.TrimRowNumber(Int32.Parse(row1String.Substring(1)) + rowsShifted)).ToInvariantString(); + } + else + row1 = (XLHelper.TrimRowNumber(Int32.Parse(row1String) + rowsShifted)).ToInvariantString(); + + string row2; + if (row2String[0] == '$') + { + row2 = "$" + + (XLHelper.TrimRowNumber(Int32.Parse(row2String.Substring(1)) + rowsShifted)).ToInvariantString(); + } + else + row2 = (XLHelper.TrimRowNumber(Int32.Parse(row2String) + rowsShifted)).ToInvariantString(); + + sb.Append(row1); + sb.Append(':'); + sb.Append(row2); + } + else if (shiftedRange.RangeAddress.FirstAddress.RowNumber <= + matchRange.RangeAddress.FirstAddress.RowNumber) + { + if (rangeAddress.Contains(':')) + { + sb.Append( + new XLAddress( + worksheetInAction, + XLHelper.TrimRowNumber(matchRange.RangeAddress.FirstAddress.RowNumber + rowsShifted), + matchRange.RangeAddress.FirstAddress.ColumnLetter, + matchRange.RangeAddress.FirstAddress.FixedRow, + matchRange.RangeAddress.FirstAddress.FixedColumn)); + sb.Append(':'); + sb.Append( + new XLAddress( + worksheetInAction, + XLHelper.TrimRowNumber(matchRange.RangeAddress.LastAddress.RowNumber + rowsShifted), + matchRange.RangeAddress.LastAddress.ColumnLetter, + matchRange.RangeAddress.LastAddress.FixedRow, + matchRange.RangeAddress.LastAddress.FixedColumn)); + } + else + { + sb.Append( + new XLAddress( + worksheetInAction, + XLHelper.TrimRowNumber(matchRange.RangeAddress.FirstAddress.RowNumber + rowsShifted), + matchRange.RangeAddress.FirstAddress.ColumnLetter, + matchRange.RangeAddress.FirstAddress.FixedRow, + matchRange.RangeAddress.FirstAddress.FixedColumn)); + } + } + else + { + sb.Append(matchRange.RangeAddress.FirstAddress); + sb.Append(':'); + sb.Append( + new XLAddress( + worksheetInAction, + XLHelper.TrimRowNumber(matchRange.RangeAddress.LastAddress.RowNumber + rowsShifted), + matchRange.RangeAddress.LastAddress.ColumnLetter, + matchRange.RangeAddress.LastAddress.FixedRow, + matchRange.RangeAddress.LastAddress.FixedColumn)); + } + } + else + sb.Append(matchString); + } + else + sb.Append(matchString); + } + else + sb.Append(matchString); + } + else + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex + matchString.Length)); + + lastIndex = matchIndex + matchString.Length; + } + + if (lastIndex < value.Length) + sb.Append(value.Substring(lastIndex)); + + return sb.ToString(); + } + + internal void ShiftFormulaColumns(XLRange shiftedRange, int columnsShifted) + { + FormulaA1 = ShiftFormulaColumns(FormulaA1, Worksheet, shiftedRange, columnsShifted); + } + + internal static String ShiftFormulaColumns(String formulaA1, XLWorksheet worksheetInAction, XLRange shiftedRange, + int columnsShifted) + { + if (String.IsNullOrWhiteSpace(formulaA1)) return String.Empty; + + var value = formulaA1; + + var regex = A1SimpleRegex; + + var sb = new StringBuilder(); + var lastIndex = 0; + + foreach (var match in regex.Matches(value).Cast()) + { + var matchString = match.Value; + var matchIndex = match.Index; + if (value.Substring(0, matchIndex).CharCount('"') % 2 == 0) + { + // Check that the match is not between quotes + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex)); + string sheetName; + var useSheetName = false; + if (matchString.Contains('!')) + { + sheetName = matchString.Substring(0, matchString.IndexOf('!')); + if (sheetName[0] == '\'') + sheetName = sheetName.Substring(1, sheetName.Length - 2); + useSheetName = true; + } + else + sheetName = worksheetInAction.Name; + + if (String.Compare(sheetName, shiftedRange.Worksheet.Name, true) == 0) + { + var rangeAddress = matchString.Substring(matchString.IndexOf('!') + 1); + if (!A1RowRegex.IsMatch(rangeAddress)) + { + var matchRange = worksheetInAction.Workbook.Worksheet(sheetName).Range(rangeAddress); + + if (shiftedRange.RangeAddress.FirstAddress.ColumnNumber <= + matchRange.RangeAddress.LastAddress.ColumnNumber + && + shiftedRange.RangeAddress.FirstAddress.RowNumber <= + matchRange.RangeAddress.FirstAddress.RowNumber + && + shiftedRange.RangeAddress.LastAddress.RowNumber >= + matchRange.RangeAddress.LastAddress.RowNumber) + { + if (useSheetName) + { + sb.Append(sheetName.EscapeSheetName()); + sb.Append('!'); + } + + if (A1ColumnRegex.IsMatch(rangeAddress)) + { + var columns = rangeAddress.Split(':'); + var column1String = columns[0]; + var column2String = columns[1]; + string column1; + if (column1String[0] == '$') + { + column1 = "$" + + XLHelper.GetColumnLetterFromNumber( + XLHelper.GetColumnNumberFromLetter( + column1String.Substring(1)) + columnsShifted, true); + } + else + { + column1 = + XLHelper.GetColumnLetterFromNumber( + XLHelper.GetColumnNumberFromLetter(column1String) + + columnsShifted, true); + } + + string column2; + if (column2String[0] == '$') + { + column2 = "$" + + XLHelper.GetColumnLetterFromNumber( + XLHelper.GetColumnNumberFromLetter( + column2String.Substring(1)) + columnsShifted, true); + } + else + { + column2 = + XLHelper.GetColumnLetterFromNumber( + XLHelper.GetColumnNumberFromLetter(column2String) + + columnsShifted, true); + } + + sb.Append(column1); + sb.Append(':'); + sb.Append(column2); + } + else if (shiftedRange.RangeAddress.FirstAddress.ColumnNumber <= + matchRange.RangeAddress.FirstAddress.ColumnNumber) + { + if (rangeAddress.Contains(':')) + { + sb.Append( + new XLAddress( + worksheetInAction, + matchRange.RangeAddress.FirstAddress.RowNumber, + XLHelper.TrimColumnNumber(matchRange.RangeAddress.FirstAddress.ColumnNumber + columnsShifted), + matchRange.RangeAddress.FirstAddress.FixedRow, + matchRange.RangeAddress.FirstAddress.FixedColumn)); + sb.Append(':'); + sb.Append( + new XLAddress( + worksheetInAction, + matchRange.RangeAddress.LastAddress.RowNumber, + XLHelper.TrimColumnNumber(matchRange.RangeAddress.LastAddress.ColumnNumber + columnsShifted), + matchRange.RangeAddress.LastAddress.FixedRow, + matchRange.RangeAddress.LastAddress.FixedColumn)); + } + else + { + sb.Append( + new XLAddress( + worksheetInAction, + matchRange.RangeAddress.FirstAddress.RowNumber, + XLHelper.TrimColumnNumber(matchRange.RangeAddress.FirstAddress.ColumnNumber + columnsShifted), + matchRange.RangeAddress.FirstAddress.FixedRow, + matchRange.RangeAddress.FirstAddress.FixedColumn)); + } + } + else + { + sb.Append(matchRange.RangeAddress.FirstAddress); + sb.Append(':'); + sb.Append( + new XLAddress( + worksheetInAction, + matchRange.RangeAddress.LastAddress.RowNumber, + XLHelper.TrimColumnNumber(matchRange.RangeAddress.LastAddress.ColumnNumber + columnsShifted), + matchRange.RangeAddress.LastAddress.FixedRow, + matchRange.RangeAddress.LastAddress.FixedColumn)); + } + } + else + sb.Append(matchString); + } + else + sb.Append(matchString); + } + else + sb.Append(matchString); + } + else + sb.Append(value.Substring(lastIndex, matchIndex - lastIndex + matchString.Length)); + lastIndex = matchIndex + matchString.Length; + } + + if (lastIndex < value.Length) + sb.Append(value.Substring(lastIndex)); + + return sb.ToString(); + } + + private XLCell CellShift(Int32 rowsToShift, Int32 columnsToShift) + { + return Worksheet.Cell(_rowNumber + rowsToShift, _columnNumber + columnsToShift); + } + + #region Nested type: FormulaConversionType + + private enum FormulaConversionType + { + A1ToR1C1, + R1C1ToA1 + }; + + #endregion Nested type: FormulaConversionType + + #region XLCell Above + + IXLCell IXLCell.CellAbove() + { + return CellAbove(); + } + + IXLCell IXLCell.CellAbove(Int32 step) + { + return CellAbove(step); + } + + public XLCell CellAbove() + { + return CellAbove(1); + } + + public XLCell CellAbove(Int32 step) + { + return CellShift(step * -1, 0); + } + + #endregion XLCell Above + + #region XLCell Below + + IXLCell IXLCell.CellBelow() + { + return CellBelow(); + } + + IXLCell IXLCell.CellBelow(Int32 step) + { + return CellBelow(step); + } + + public XLCell CellBelow() + { + return CellBelow(1); + } + + public XLCell CellBelow(Int32 step) + { + return CellShift(step, 0); + } + + #endregion XLCell Below + + #region XLCell Left + + IXLCell IXLCell.CellLeft() + { + return CellLeft(); + } + + IXLCell IXLCell.CellLeft(Int32 step) + { + return CellLeft(step); + } + + public XLCell CellLeft() + { + return CellLeft(1); + } + + public XLCell CellLeft(Int32 step) + { + return CellShift(0, step * -1); + } + + #endregion XLCell Left + + #region XLCell Right + + IXLCell IXLCell.CellRight() + { + return CellRight(); + } + + IXLCell IXLCell.CellRight(Int32 step) + { + return CellRight(step); + } + + public XLCell CellRight() + { + return CellRight(1); + } + + public XLCell CellRight(Int32 step) + { + return CellShift(0, step); + } + + #endregion XLCell Right + + public Boolean HasFormula { get { return !String.IsNullOrWhiteSpace(FormulaA1); } } + + public Boolean HasArrayFormula { get { return FormulaA1.StartsWith("{"); } } + + public IXLRangeAddress FormulaReference { get; set; } + + public IXLRange CurrentRegion + { + get + { + return this.Worksheet.Range(FindCurrentRegion(this.AsRange())); + } + } + + internal IXLRangeAddress FindCurrentRegion(IXLRangeBase range) + { + var rangeAddress = range.RangeAddress; + + var filledCells = range + .SurroundingCells(c => !(c as XLCell).IsEmpty(XLCellsUsedOptions.AllContents)) + .Concat(this.Worksheet.Range(rangeAddress).Cells()); + + var grownRangeAddress = new XLRangeAddress( + new XLAddress(this.Worksheet, filledCells.Min(c => c.Address.RowNumber), filledCells.Min(c => c.Address.ColumnNumber), false, false), + new XLAddress(this.Worksheet, filledCells.Max(c => c.Address.RowNumber), filledCells.Max(c => c.Address.ColumnNumber), false, false) + ); + + if (rangeAddress.Equals(grownRangeAddress)) + return this.Worksheet.Range(grownRangeAddress).RangeAddress; + else + return FindCurrentRegion(this.Worksheet.Range(grownRangeAddress)); + } + + internal bool IsInferiorMergedCell() + { + return this.IsMerged() && !this.Address.Equals(this.MergedRange().RangeAddress.FirstAddress); + } + + internal bool IsSuperiorMergedCell() + { + return this.IsMerged() && this.Address.Equals(this.MergedRange().RangeAddress.FirstAddress); + } + } +} diff --git a/ClosedXML/Excel/Cells/XLCellCopyOptions.cs b/ClosedXML/Excel/Cells/XLCellCopyOptions.cs new file mode 100644 index 0000000..6c8e59e --- /dev/null +++ b/ClosedXML/Excel/Cells/XLCellCopyOptions.cs @@ -0,0 +1,16 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + internal enum XLCellCopyOptions + { + None = 0, + Values = 1 << 1, + Styles = 1 << 2, + ConditionalFormats = 1 << 3, + DataValidations = 1 << 4, + Sparklines = 1 << 5, + All = Values | Styles | ConditionalFormats | DataValidations | Sparklines + } +} diff --git a/ClosedXML/Excel/Cells/XLCells.cs b/ClosedXML/Excel/Cells/XLCells.cs new file mode 100644 index 0000000..e91501e --- /dev/null +++ b/ClosedXML/Excel/Cells/XLCells.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + using System.Linq; + + internal class XLCells : XLStylizedBase, IXLCells, IXLStylized, IEnumerable + { + #region Fields + + private readonly List _rangeAddresses = new List(); + private readonly bool _usedCellsOnly; + private readonly Func _predicate; + private readonly XLCellsUsedOptions _options; + private bool _styleInitialized = false; + + #endregion Fields + + #region Constructor + + public XLCells(bool usedCellsOnly, XLCellsUsedOptions options, Func predicate = null) + : base(XLStyle.Default.Value) + { + _usedCellsOnly = usedCellsOnly; + _options = options; + _predicate = predicate ?? (_ => true); + } + + #endregion Constructor + + #region IEnumerable Members + + private IEnumerable GetAllCells() + { + var grouppedAddresses = _rangeAddresses.GroupBy(addr => addr.Worksheet); + foreach (var worksheetGroup in grouppedAddresses) + { + var ws = worksheetGroup.Key; + var sheetPoints = worksheetGroup.SelectMany(addr => GetAllCellsInRange(addr)) + .Distinct(); + foreach (var sheetPoint in sheetPoints) + { + var c = ws.Cell(sheetPoint.Row, sheetPoint.Column); + if (_predicate(c)) + yield return c; + } + } + } + + private IEnumerable GetAllCellsInRange(IXLRangeAddress rangeAddress) + { + if (!rangeAddress.IsValid) + yield break; + + var normalizedAddress = ((XLRangeAddress)rangeAddress).Normalize(); + var minRow = normalizedAddress.FirstAddress.RowNumber; + var maxRow = normalizedAddress.LastAddress.RowNumber; + var minColumn = normalizedAddress.FirstAddress.ColumnNumber; + var maxColumn = normalizedAddress.LastAddress.ColumnNumber; + + for (var ro = minRow; ro <= maxRow; ro++) + { + for (var co = minColumn; co <= maxColumn; co++) + { + yield return new XLSheetPoint(ro, co); + } + } + } + + private IEnumerable GetUsedCells() + { + var grouppedAddresses = _rangeAddresses.GroupBy(addr => addr.Worksheet); + foreach (var worksheetGroup in grouppedAddresses) + { + var ws = worksheetGroup.Key; + + var usedCellsCandidates = GetUsedCellsCandidates(ws); + + var cells = worksheetGroup.SelectMany(addr => GetUsedCellsInRange(addr, ws, usedCellsCandidates)) + .OrderBy(cell => cell.Address.RowNumber) + .ThenBy(cell => cell.Address.ColumnNumber); + + var visitedCells = new HashSet(); + foreach (var cell in cells) + { + if (visitedCells.Contains(cell.Address)) continue; + + visitedCells.Add(cell.Address); + + yield return cell; + } + } + } + + private IEnumerable GetUsedCellsInRange(XLRangeAddress rangeAddress, XLWorksheet worksheet, IEnumerable usedCellsCandidates) + { + if (!rangeAddress.IsValid) + yield break; + var normalizedAddress = rangeAddress.Normalize(); + var minRow = normalizedAddress.FirstAddress.RowNumber; + var maxRow = normalizedAddress.LastAddress.RowNumber; + var minColumn = normalizedAddress.FirstAddress.ColumnNumber; + var maxColumn = normalizedAddress.LastAddress.ColumnNumber; + + var cellRange = worksheet.Internals.CellsCollection + .GetCells(minRow, minColumn, maxRow, maxColumn, _predicate) + .Where(c => !c.IsEmpty(_options)); + + foreach (var cell in cellRange) + { + if (_predicate(cell)) + yield return cell; + } + + foreach (var sheetPoint in usedCellsCandidates) + { + if (sheetPoint.Row.Between(minRow, maxRow) && + sheetPoint.Column.Between(minColumn, maxColumn)) + { + var cell = worksheet.Cell(sheetPoint.Row, sheetPoint.Column); + + if (_predicate(cell)) + yield return cell; + } + } + } + + private IEnumerable GetUsedCellsCandidates(XLWorksheet worksheet) + { + var candidates = Enumerable.Empty(); + + if (_options.HasFlag(XLCellsUsedOptions.MergedRanges)) + candidates = candidates.Union( + worksheet.Internals.MergedRanges.SelectMany(r => GetAllCellsInRange(r.RangeAddress))); + + if (_options.HasFlag(XLCellsUsedOptions.ConditionalFormats)) + candidates = candidates.Union( + worksheet.ConditionalFormats.SelectMany(cf => cf.Ranges.SelectMany(r => GetAllCellsInRange(r.RangeAddress)))); + + if (_options.HasFlag(XLCellsUsedOptions.DataValidation)) + candidates = candidates.Union( + worksheet.DataValidations.SelectMany(dv => dv.Ranges.SelectMany(r => GetAllCellsInRange(r.RangeAddress)))); + + return candidates.Distinct(); + } + + public IEnumerator GetEnumerator() + { + var cells = (_usedCellsOnly) ? GetUsedCells() : GetAllCells(); + foreach (var cell in cells) + { + yield return cell; + } + } + + #endregion IEnumerable Members + + #region IXLCells Members + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (XLCell cell in this) + yield return cell; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Object Value + { + set { this.ForEach(c => c.Value = value); } + } + + public IXLCells SetDataType(XLDataType dataType) + { + this.ForEach(c => c.DataType = dataType); + return this; + } + + public XLDataType DataType + { + set { this.ForEach(c => c.DataType = value); } + } + + public IXLCells Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + this.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void DeleteComments() + { + this.ForEach(c => c.DeleteComment()); + } + + public void DeleteSparklines() + { + this.ForEach(c => c.DeleteSparkline()); + } + + public String FormulaA1 + { + set { this.ForEach(c => c.FormulaA1 = value); } + } + + public String FormulaR1C1 + { + set { this.ForEach(c => c.FormulaR1C1 = value); } + } + + #endregion IXLCells Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + foreach (XLCell c in this) + yield return c.Style; + } + } + + protected override IEnumerable Children + { + get + { + foreach (XLCell c in this) + yield return c; + } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + public void Add(XLRangeAddress rangeAddress) + { + _rangeAddresses.Add(rangeAddress); + + if (_styleInitialized) + return; + + var worksheetStyle = rangeAddress.Worksheet?.Style; + if (worksheetStyle == null) + return; + + InnerStyle = worksheetStyle; + _styleInitialized = true; + } + + public void Add(XLCell cell) + { + Add(new XLRangeAddress(cell.Address, cell.Address)); + } + + public void Select() + { + foreach (var cell in this) + cell.Select(); + } + } +} diff --git a/ClosedXML/Excel/Cells/XLCellsCollection.cs b/ClosedXML/Excel/Cells/XLCellsCollection.cs new file mode 100644 index 0000000..187d4b1 --- /dev/null +++ b/ClosedXML/Excel/Cells/XLCellsCollection.cs @@ -0,0 +1,421 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLCellsCollection + { + internal Dictionary ColumnsUsed { get; } = new Dictionary(); + internal Dictionary> Deleted { get; } = new Dictionary>(); + internal Dictionary> RowsCollection { get; } = new Dictionary>(); + + public Int32 MaxColumnUsed; + public Int32 MaxRowUsed; + public Dictionary RowsUsed = new Dictionary(); + + public XLCellsCollection() + { + Clear(); + } + + public Int32 Count { get; private set; } + + public void Add(XLSheetPoint sheetPoint, XLCell cell) + { + Add(sheetPoint.Row, sheetPoint.Column, cell); + } + + public void Add(Int32 row, Int32 column, XLCell cell) + { + Count++; + + IncrementUsage(RowsUsed, row); + IncrementUsage(ColumnsUsed, column); + + if (!RowsCollection.TryGetValue(row, out Dictionary columnsCollection)) + { + columnsCollection = new Dictionary(); + RowsCollection.Add(row, columnsCollection); + } + columnsCollection.Add(column, cell); + if (row > MaxRowUsed) MaxRowUsed = row; + if (column > MaxColumnUsed) MaxColumnUsed = column; + + if (Deleted.TryGetValue(row, out HashSet delHash)) + delHash.Remove(column); + } + + private static void IncrementUsage(Dictionary dictionary, Int32 key) + { + if (dictionary.TryGetValue(key, out Int32 value)) + dictionary[key] = value + 1; + else + dictionary.Add(key, 1); + } + + /// + /// True if the number was lowered to zero so MaxColumnUsed or MaxRowUsed may require + /// recomputation. + private static bool DecrementUsage(Dictionary dictionary, Int32 key) + { + if (!dictionary.TryGetValue(key, out Int32 count)) return false; + + if (count > 1) + { + dictionary[key]--; + return false; + } + else + { + dictionary.Remove(key); + return true; + } + } + + public void Clear() + { + Count = 0; + RowsUsed.Clear(); + ColumnsUsed.Clear(); + + RowsCollection.Clear(); + MaxRowUsed = 0; + MaxColumnUsed = 0; + } + + public void Remove(XLSheetPoint sheetPoint) + { + Remove(sheetPoint.Row, sheetPoint.Column); + } + + public void Remove(Int32 row, Int32 column) + { + Count--; + var rowRemoved = DecrementUsage(RowsUsed, row); + var columnRemoved = DecrementUsage(ColumnsUsed, column); + + if (rowRemoved && row == MaxRowUsed) + { + MaxRowUsed = RowsUsed.Keys.Any() + ? RowsUsed.Keys.Max() + : 0; + } + + if (columnRemoved && column == MaxColumnUsed) + { + MaxColumnUsed = ColumnsUsed.Keys.Any() + ? ColumnsUsed.Keys.Max() + : 0; + } + + if (Deleted.TryGetValue(row, out HashSet delHash)) + { + if (!delHash.Contains(column)) + delHash.Add(column); + } + else + { + delHash = new HashSet(); + delHash.Add(column); + Deleted.Add(row, delHash); + } + + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection)) + { + columnsCollection.Remove(column); + if (columnsCollection.Count == 0) + { + RowsCollection.Remove(row); + } + } + } + + internal IEnumerable GetCells(Int32 rowStart, Int32 columnStart, + Int32 rowEnd, Int32 columnEnd, + Func predicate = null) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = rowStart; ro <= finalRow; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = columnStart; co <= finalColumn; co++) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && (predicate == null || predicate(cell))) + yield return cell; + } + } + } + } + + public int FirstRowUsed(int rowStart, int columnStart, int rowEnd, int columnEnd, XLCellsUsedOptions options, + Func predicate = null) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = rowStart; ro <= finalRow; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = columnStart; co <= finalColumn; co++) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && !cell.IsEmpty(options) + && (predicate == null || predicate(cell))) + + return ro; + } + } + } + + return 0; + } + + public int FirstColumnUsed(int rowStart, int columnStart, int rowEnd, int columnEnd, XLCellsUsedOptions options, + Func predicate = null) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + int firstColumnUsed = finalColumn; + var found = false; + for (int ro = rowStart; ro <= finalRow; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = columnStart; co <= firstColumnUsed; co++) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && !cell.IsEmpty(options) + && (predicate == null || predicate(cell)) + && co <= firstColumnUsed) + { + firstColumnUsed = co; + found = true; + break; + } + } + } + } + + return found ? firstColumnUsed : 0; + } + + public int LastRowUsed(int rowStart, int columnStart, int rowEnd, int columnEnd, XLCellsUsedOptions options, + Func predicate = null) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = finalRow; ro >= rowStart; ro--) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = finalColumn; co >= columnStart; co--) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && !cell.IsEmpty(options) + && (predicate == null || predicate(cell))) + + return ro; + } + } + } + return 0; + } + + public int LastColumnUsed(int rowStart, int columnStart, int rowEnd, int columnEnd, XLCellsUsedOptions options, + Func predicate = null) + { + int maxCo = 0; + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = finalRow; ro >= rowStart; ro--) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = finalColumn; co >= columnStart && co > maxCo; co--) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && !cell.IsEmpty(options) + && (predicate == null || predicate(cell))) + + maxCo = co; + } + } + } + return maxCo; + } + + public void RemoveAll(Int32 rowStart, Int32 columnStart, + Int32 rowEnd, Int32 columnEnd) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = rowStart; ro <= finalRow; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = columnStart; co <= finalColumn; co++) + { + if (columnsCollection.ContainsKey(co)) + Remove(ro, co); + } + } + } + } + + public IEnumerable GetSheetPoints(Int32 rowStart, Int32 columnStart, + Int32 rowEnd, Int32 columnEnd) + { + int finalRow = rowEnd > MaxRowUsed ? MaxRowUsed : rowEnd; + int finalColumn = columnEnd > MaxColumnUsed ? MaxColumnUsed : columnEnd; + for (int ro = rowStart; ro <= finalRow; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = columnStart; co <= finalColumn; co++) + { + if (columnsCollection.ContainsKey(co)) + yield return new XLSheetPoint(ro, co); + } + } + } + } + + public XLCell GetCell(Int32 row, Int32 column) + { + if (row > MaxRowUsed || column > MaxColumnUsed) + return null; + + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection)) + { + return columnsCollection.TryGetValue(column, out XLCell cell) ? cell : null; + } + return null; + } + + public XLCell GetCell(XLSheetPoint sp) + { + return GetCell(sp.Row, sp.Column); + } + + internal void SwapRanges(XLSheetRange sheetRange1, XLSheetRange sheetRange2, XLWorksheet worksheet) + { + Int32 rowCount = sheetRange1.LastPoint.Row - sheetRange1.FirstPoint.Row + 1; + Int32 columnCount = sheetRange1.LastPoint.Column - sheetRange1.FirstPoint.Column + 1; + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < columnCount; column++) + { + var sp1 = new XLSheetPoint(sheetRange1.FirstPoint.Row + row, sheetRange1.FirstPoint.Column + column); + var sp2 = new XLSheetPoint(sheetRange2.FirstPoint.Row + row, sheetRange2.FirstPoint.Column + column); + var cell1 = GetCell(sp1); + var cell2 = GetCell(sp2); + + if (cell1 == null) cell1 = worksheet.Cell(sp1.Row, sp1.Column); + if (cell2 == null) cell2 = worksheet.Cell(sp2.Row, sp2.Column); + + //if (cell1 != null) + //{ + cell1.Address = new XLAddress(cell1.Worksheet, sp2.Row, sp2.Column, false, false); + Remove(sp1); + //if (cell2 != null) + Add(sp1, cell2); + //} + + //if (cell2 == null) continue; + + cell2.Address = new XLAddress(cell2.Worksheet, sp1.Row, sp1.Column, false, false); + Remove(sp2); + //if (cell1 != null) + Add(sp2, cell1); + } + } + } + + internal IEnumerable GetCells() + { + return GetCells(1, 1, MaxRowUsed, MaxColumnUsed); + } + + internal IEnumerable GetCells(Func predicate) + { + for (int ro = 1; ro <= MaxRowUsed; ro++) + { + if (RowsCollection.TryGetValue(ro, out Dictionary columnsCollection)) + { + for (int co = 1; co <= MaxColumnUsed; co++) + { + if (columnsCollection.TryGetValue(co, out XLCell cell) + && (predicate == null || predicate(cell))) + yield return cell; + } + } + } + } + + public Boolean Contains(Int32 row, Int32 column) + { + return RowsCollection.TryGetValue(row, out Dictionary columnsCollection) + && columnsCollection.ContainsKey(column); + } + + public Int32 MinRowInColumn(Int32 column) + { + for (int row = 1; row <= MaxRowUsed; row++) + { + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection) + && columnsCollection.ContainsKey(column)) + + return row; + } + + return 0; + } + + public Int32 MaxRowInColumn(Int32 column) + { + for (int row = MaxRowUsed; row >= 1; row--) + { + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection) + && columnsCollection.ContainsKey(column)) + + return row; + } + + return 0; + } + + public Int32 MinColumnInRow(Int32 row) + { + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection) + && columnsCollection.Any()) + + return columnsCollection.Keys.Min(); + + return 0; + } + + public Int32 MaxColumnInRow(Int32 row) + { + if (RowsCollection.TryGetValue(row, out Dictionary columnsCollection) + && columnsCollection.Any()) + + return columnsCollection.Keys.Max(); + + return 0; + } + + public IEnumerable GetCellsInColumn(Int32 column) + { + return GetCells(1, column, MaxRowUsed, column); + } + + public IEnumerable GetCellsInRow(Int32 row) + { + return GetCells(row, 1, row, MaxColumnUsed); + } + } +} diff --git a/ClosedXML/Excel/Charts/IXLChart.cs b/ClosedXML/Excel/Charts/IXLChart.cs new file mode 100644 index 0000000..7cfc813 --- /dev/null +++ b/ClosedXML/Excel/Charts/IXLChart.cs @@ -0,0 +1,90 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLChartType { + Area, + Area3D, + AreaStacked, + AreaStacked100Percent, + AreaStacked100Percent3D, + AreaStacked3D, + BarClustered, + BarClustered3D, + BarStacked, + BarStacked100Percent, + BarStacked100Percent3D, + BarStacked3D, + Bubble, + Bubble3D, + Column3D, + ColumnClustered, + ColumnClustered3D, + ColumnStacked, + ColumnStacked100Percent, + ColumnStacked100Percent3D, + ColumnStacked3D, + Cone, + ConeClustered, + ConeHorizontalClustered, + ConeHorizontalStacked, + ConeHorizontalStacked100Percent, + ConeStacked, + ConeStacked100Percent, + Cylinder, + CylinderClustered, + CylinderHorizontalClustered, + CylinderHorizontalStacked, + CylinderHorizontalStacked100Percent, + CylinderStacked, + CylinderStacked100Percent, + Doughnut, + DoughnutExploded, + Line, + Line3D, + LineStacked, + LineStacked100Percent, + LineWithMarkers, + LineWithMarkersStacked, + LineWithMarkersStacked100Percent, + Pie, + Pie3D, + PieExploded, + PieExploded3D, + PieToBar, + PieToPie, + Pyramid, + PyramidClustered, + PyramidHorizontalClustered, + PyramidHorizontalStacked, + PyramidHorizontalStacked100Percent, + PyramidStacked, + PyramidStacked100Percent, + Radar, + RadarFilled, + RadarWithMarkers, + StockHighLowClose, + StockOpenHighLowClose, + StockVolumeHighLowClose, + StockVolumeOpenHighLowClose, + Surface, + SurfaceContour, + SurfaceContourWireframe, + SurfaceWireframe, + XYScatterMarkers, + XYScatterSmoothLinesNoMarkers, + XYScatterSmoothLinesWithMarkers, + XYScatterStraightLinesNoMarkers, + XYScatterStraightLinesWithMarkers + } + public interface IXLChart: IXLDrawing + { + Boolean RightAngleAxes { get; set; } + IXLChart SetRightAngleAxes(); + IXLChart SetRightAngleAxes(Boolean rightAngleAxes); + + XLChartType ChartType { get; set; } + IXLChart SetChartType(XLChartType chartType); + + } +} diff --git a/ClosedXML/Excel/Charts/IXLCharts.cs b/ClosedXML/Excel/Charts/IXLCharts.cs new file mode 100644 index 0000000..70c1478 --- /dev/null +++ b/ClosedXML/Excel/Charts/IXLCharts.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLCharts: IEnumerable + { + void Add(IXLChart chart); + } +} diff --git a/ClosedXML/Excel/Charts/XLChart.cs b/ClosedXML/Excel/Charts/XLChart.cs new file mode 100644 index 0000000..d928e6c --- /dev/null +++ b/ClosedXML/Excel/Charts/XLChart.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal enum XLChartTypeCategory { Bar3D } + internal enum XLBarOrientation { Vertical, Horizontal } + internal enum XLBarGrouping { Clustered, Percent, Stacked, Standard } + internal class XLChart: XLDrawing, IXLChart + { + internal IXLWorksheet worksheet; + public XLChart(XLWorksheet worksheet) + { + Container = this; + this.worksheet = worksheet; + Int32 zOrder; + if (worksheet.Charts.Any()) + zOrder = worksheet.Charts.Max(c => c.ZOrder) + 1; + else + zOrder = 1; + ZOrder = zOrder; + ShapeId = worksheet.Workbook.ShapeIdManager.GetNext(); + RightAngleAxes = true; + } + + public Boolean RightAngleAxes { get; set; } + public IXLChart SetRightAngleAxes() + { + RightAngleAxes = true; + return this; + } + public IXLChart SetRightAngleAxes(Boolean rightAngleAxes) + { + RightAngleAxes = rightAngleAxes; + return this; + } + + public XLChartType ChartType { get; set; } + public IXLChart SetChartType(XLChartType chartType) + { + ChartType = chartType; + return this; + } + + public XLChartTypeCategory ChartTypeCategory + { + get + { + if (Bar3DCharts.Contains(ChartType)) + return XLChartTypeCategory.Bar3D; + else + throw new NotImplementedException(); + + } + } + + private HashSet Bar3DCharts = new HashSet { + XLChartType.BarClustered3D, + XLChartType.BarStacked100Percent3D, + XLChartType.BarStacked3D, + XLChartType.Column3D, + XLChartType.ColumnClustered3D, + XLChartType.ColumnStacked100Percent3D, + XLChartType.ColumnStacked3D + }; + + public XLBarOrientation BarOrientation + { + get + { + if (HorizontalCharts.Contains(ChartType)) + return XLBarOrientation.Horizontal; + else + return XLBarOrientation.Vertical; + } + } + + private HashSet HorizontalCharts = new HashSet{ + XLChartType.BarClustered, + XLChartType.BarClustered3D, + XLChartType.BarStacked, + XLChartType.BarStacked100Percent, + XLChartType.BarStacked100Percent3D, + XLChartType.BarStacked3D, + XLChartType.ConeHorizontalClustered, + XLChartType.ConeHorizontalStacked, + XLChartType.ConeHorizontalStacked100Percent, + XLChartType.CylinderHorizontalClustered, + XLChartType.CylinderHorizontalStacked, + XLChartType.CylinderHorizontalStacked100Percent, + XLChartType.PyramidHorizontalClustered, + XLChartType.PyramidHorizontalStacked, + XLChartType.PyramidHorizontalStacked100Percent + }; + + public XLBarGrouping BarGrouping + { + get + { + if (ClusteredCharts.Contains(ChartType)) + return XLBarGrouping.Clustered; + else if (PercentCharts.Contains(ChartType)) + return XLBarGrouping.Percent; + else if (StackedCharts.Contains(ChartType)) + return XLBarGrouping.Stacked; + else + return XLBarGrouping.Standard; + } + } + + public HashSet ClusteredCharts = new HashSet() + { + XLChartType.BarClustered, + XLChartType.BarClustered3D, + XLChartType.ColumnClustered, + XLChartType.ColumnClustered3D, + XLChartType.ConeClustered, + XLChartType.ConeHorizontalClustered, + XLChartType.CylinderClustered, + XLChartType.CylinderHorizontalClustered, + XLChartType.PyramidClustered, + XLChartType.PyramidHorizontalClustered + }; + + public HashSet PercentCharts = new HashSet() { + XLChartType.AreaStacked100Percent, + XLChartType.AreaStacked100Percent3D, + XLChartType.BarStacked100Percent, + XLChartType.BarStacked100Percent3D, + XLChartType.ColumnStacked100Percent, + XLChartType.ColumnStacked100Percent3D, + XLChartType.ConeHorizontalStacked100Percent, + XLChartType.ConeStacked100Percent, + XLChartType.CylinderHorizontalStacked100Percent, + XLChartType.CylinderStacked100Percent, + XLChartType.LineStacked100Percent, + XLChartType.LineWithMarkersStacked100Percent, + XLChartType.PyramidHorizontalStacked100Percent, + XLChartType.PyramidStacked100Percent + }; + + public HashSet StackedCharts = new HashSet() + { + XLChartType.AreaStacked, + XLChartType.AreaStacked3D, + XLChartType.BarStacked, + XLChartType.BarStacked3D, + XLChartType.ColumnStacked, + XLChartType.ColumnStacked3D, + XLChartType.ConeHorizontalStacked, + XLChartType.ConeStacked, + XLChartType.CylinderHorizontalStacked, + XLChartType.CylinderStacked, + XLChartType.LineStacked, + XLChartType.LineWithMarkersStacked, + XLChartType.PyramidHorizontalStacked, + XLChartType.PyramidStacked + }; + } +} diff --git a/ClosedXML/Excel/Charts/XLCharts.cs b/ClosedXML/Excel/Charts/XLCharts.cs new file mode 100644 index 0000000..112a6fc --- /dev/null +++ b/ClosedXML/Excel/Charts/XLCharts.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLCharts: IXLCharts + { + private List charts = new List(); + public IEnumerator GetEnumerator() + { + return charts.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(IXLChart chart) + { + charts.Add(chart); + } + } +} diff --git a/ClosedXML/Excel/Columns/IXLColumn.cs b/ClosedXML/Excel/Columns/IXLColumn.cs new file mode 100644 index 0000000..db2aecb --- /dev/null +++ b/ClosedXML/Excel/Columns/IXLColumn.cs @@ -0,0 +1,197 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLColumn : IXLRangeBase + { + /// + /// Gets or sets the width of this column. + /// + /// + /// The width of this column. + /// + Double Width { get; set; } + + /// + /// Deletes this column and shifts the columns at the right of this one accordingly. + /// + void Delete(); + + /// + /// Gets this column's number + /// + Int32 ColumnNumber(); + + /// + /// Gets this column's letter + /// + String ColumnLetter(); + + /// + /// Inserts X number of columns at the right of this one. + /// All columns at the right will be shifted accordingly. + /// + /// The number of columns to insert. + IXLColumns InsertColumnsAfter(Int32 numberOfColumns); + + /// + /// Inserts X number of columns at the left of this one. + /// This column and all at the right will be shifted accordingly. + /// + /// The number of columns to insert. + IXLColumns InsertColumnsBefore(Int32 numberOfColumns); + + /// + /// Gets the cell in the specified row. + /// + /// The cell's row. + IXLCell Cell(Int32 rowNumber); + + /// + /// Returns the specified group of cells, separated by commas. + /// e.g. Cells("1"), Cells("1:5"), Cells("1,3:5") + /// + /// The column cells to return. + new IXLCells Cells(String cellsInColumn); + + /// + /// Returns the specified group of cells. + /// + /// The first row in the group of cells to return. + /// The last row in the group of cells to return. + IXLCells Cells(Int32 firstRow, Int32 lastRow); + + /// + /// Adjusts the width of the column based on its contents. + /// + IXLColumn AdjustToContents(); + + /// + /// Adjusts the width of the column based on its contents, starting from the startRow. + /// + /// The row to start calculating the column width. + IXLColumn AdjustToContents(Int32 startRow); + + /// + /// Adjusts the width of the column based on its contents, starting from the startRow and ending at endRow. + /// + /// The row to start calculating the column width. + /// The row to end calculating the column width. + IXLColumn AdjustToContents(Int32 startRow, Int32 endRow); + + IXLColumn AdjustToContents(Double minWidth, Double maxWidth); + + IXLColumn AdjustToContents(Int32 startRow, Double minWidth, Double maxWidth); + + IXLColumn AdjustToContents(Int32 startRow, Int32 endRow, Double minWidth, Double maxWidth); + + /// + /// Hides this column. + /// + IXLColumn Hide(); + + /// Unhides this column. + IXLColumn Unhide(); + + /// + /// Gets a value indicating whether this column is hidden or not. + /// + /// + /// true if this column is hidden; otherwise, false. + /// + Boolean IsHidden { get; } + + /// + /// Gets or sets the outline level of this column. + /// + /// + /// The outline level of this column. + /// + Int32 OutlineLevel { get; set; } + + /// + /// Adds this column to the next outline level (Increments the outline level for this column by 1). + /// + IXLColumn Group(); + + /// + /// Adds this column to the next outline level (Increments the outline level for this column by 1). + /// + /// If set to true the column will be shown collapsed. + IXLColumn Group(Boolean collapse); + + /// + /// Sets outline level for this column. + /// + /// The outline level. + IXLColumn Group(Int32 outlineLevel); + + /// + /// Sets outline level for this column. + /// + /// The outline level. + /// If set to true the column will be shown collapsed. + IXLColumn Group(Int32 outlineLevel, Boolean collapse); + + /// + /// Adds this column to the previous outline level (decrements the outline level for this column by 1). + /// + IXLColumn Ungroup(); + + /// + /// Adds this column to the previous outline level (decrements the outline level for this column by 1). + /// + /// If set to true it will remove this column from all outline levels. + IXLColumn Ungroup(Boolean fromAll); + + /// + /// Show this column as collapsed. + /// + IXLColumn Collapse(); + + /// Expands this column (if it's collapsed). + IXLColumn Expand(); + + Int32 CellCount(); + + IXLRangeColumn CopyTo(IXLCell cell); + + IXLRangeColumn CopyTo(IXLRangeBase range); + + IXLColumn CopyTo(IXLColumn column); + + IXLColumn Sort(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRangeColumn Column(Int32 start, Int32 end); + + IXLRangeColumn Column(IXLCell start, IXLCell end); + + IXLRangeColumns Columns(String columns); + + /// + /// Adds a vertical page break after this column. + /// + IXLColumn AddVerticalPageBreak(); + + IXLColumn SetDataType(XLDataType dataType); + + IXLColumn ColumnLeft(); + + IXLColumn ColumnLeft(Int32 step); + + IXLColumn ColumnRight(); + + IXLColumn ColumnRight(Int32 step); + + /// + /// Clears the contents of this column. + /// + /// Specify what you want to clear. + new IXLColumn Clear(XLClearOptions clearOptions = XLClearOptions.All); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn ColumnUsed(Boolean includeFormats); + + IXLRangeColumn ColumnUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents); + } +} diff --git a/ClosedXML/Excel/Columns/IXLColumns.cs b/ClosedXML/Excel/Columns/IXLColumns.cs new file mode 100644 index 0000000..56a8edf --- /dev/null +++ b/ClosedXML/Excel/Columns/IXLColumns.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLColumns : IEnumerable + { + /// + /// Sets the width of all columns. + /// + /// + /// The width of all columns. + /// + Double Width { set; } + + /// + /// Deletes all columns and shifts the columns at the right of them accordingly. + /// + void Delete(); + + /// + /// Adjusts the width of all columns based on its contents. + /// + IXLColumns AdjustToContents(); + + /// + /// Adjusts the width of all columns based on its contents, starting from the startRow. + /// + /// The row to start calculating the column width. + IXLColumns AdjustToContents(Int32 startRow); + + /// + /// Adjusts the width of all columns based on its contents, starting from the startRow and ending at endRow. + /// + /// The row to start calculating the column width. + /// The row to end calculating the column width. + IXLColumns AdjustToContents(Int32 startRow, Int32 endRow); + + IXLColumns AdjustToContents(Double minWidth, Double maxWidth); + + IXLColumns AdjustToContents(Int32 startRow, Double minWidth, Double maxWidth); + + IXLColumns AdjustToContents(Int32 startRow, Int32 endRow, Double minWidth, Double maxWidth); + + /// + /// Hides all columns. + /// + void Hide(); + + /// Unhides all columns. + void Unhide(); + + /// + /// Increments the outline level of all columns by 1. + /// + void Group(); + + /// + /// Increments the outline level of all columns by 1. + /// + /// If set to true the columns will be shown collapsed. + void Group(Boolean collapse); + + /// + /// Sets outline level for all columns. + /// + /// The outline level. + void Group(Int32 outlineLevel); + + /// + /// Sets outline level for all columns. + /// + /// The outline level. + /// If set to true the columns will be shown collapsed. + void Group(Int32 outlineLevel, Boolean collapse); + + /// + /// Decrements the outline level of all columns by 1. + /// + void Ungroup(); + + /// + /// Decrements the outline level of all columns by 1. + /// + /// If set to true it will remove the columns from all outline levels. + void Ungroup(Boolean fromAll); + + /// + /// Show all columns as collapsed. + /// + void Collapse(); + + /// Expands all columns (if they're collapsed). + void Expand(); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + IXLCells CellsUsed(Boolean includeFormats); + + IXLStyle Style { get; set; } + + /// + /// Adds a vertical page break after these columns. + /// + IXLColumns AddVerticalPageBreaks(); + + IXLColumns SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these columns. + /// + /// Specify what you want to clear. + IXLColumns Clear(XLClearOptions clearOptions = XLClearOptions.All); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Columns/XLColumn.cs b/ClosedXML/Excel/Columns/XLColumn.cs new file mode 100644 index 0000000..e651d44 --- /dev/null +++ b/ClosedXML/Excel/Columns/XLColumn.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLColumn : XLRangeBase, IXLColumn + { + #region Private fields + + private int _outlineLevel; + + #endregion Private fields + + #region Constructor + + /// + /// The direct contructor should only be used in . + /// + public XLColumn(XLWorksheet worksheet, Int32 column) + : base(XLRangeAddress.EntireColumn(worksheet, column), worksheet.StyleValue) + { + SetColumnNumber(column); + + Width = worksheet.ColumnWidth; + } + + #endregion Constructor + + public override XLRangeType RangeType + { + get { return XLRangeType.Column; } + } + + public override IEnumerable Styles + { + get + { + yield return Style; + + int column = ColumnNumber(); + + foreach (XLCell cell in Worksheet.Internals.CellsCollection.GetCellsInColumn(column)) + yield return cell.Style; + } + } + + protected override IEnumerable Children + { + get + { + int column = ColumnNumber(); + foreach (XLCell cell in Worksheet.Internals.CellsCollection.GetCellsInColumn(column)) + yield return cell; + } + } + + public Boolean Collapsed { get; set; } + + #region IXLColumn Members + + public Double Width { get; set; } + + public void Delete() + { + int columnNumber = ColumnNumber(); + Delete(XLShiftDeletedCells.ShiftCellsLeft); + Worksheet.DeleteColumn(columnNumber); + } + + public new IXLColumn Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + public IXLCell Cell(Int32 rowNumber) + { + return Cell(rowNumber, 1); + } + + public override IXLCells Cells(String cellsInColumn) + { + var retVal = new XLCells(false, XLCellsUsedOptions.All); + var rangePairs = cellsInColumn.Split(','); + foreach (string pair in rangePairs) + retVal.Add(Range(pair.Trim()).RangeAddress); + return retVal; + } + + public override IXLCells Cells() + { + return Cells(true, XLCellsUsedOptions.All); + } + + public override IXLCells Cells(Boolean usedCellsOnly) + { + if (usedCellsOnly) + return Cells(true, XLCellsUsedOptions.AllContents); + else + return Cells(FirstCellUsed().Address.RowNumber, LastCellUsed().Address.RowNumber); + } + + public IXLCells Cells(Int32 firstRow, Int32 lastRow) + { + return Cells(firstRow + ":" + lastRow); + } + + public new IXLColumns InsertColumnsAfter(Int32 numberOfColumns) + { + int columnNum = ColumnNumber(); + Worksheet.Internals.ColumnsCollection.ShiftColumnsRight(columnNum + 1, numberOfColumns); + Worksheet.Column(columnNum).InsertColumnsAfterVoid(true, numberOfColumns); + var newColumns = Worksheet.Columns(columnNum + 1, columnNum + numberOfColumns); + CopyColumns(newColumns); + return newColumns; + } + + public new IXLColumns InsertColumnsBefore(Int32 numberOfColumns) + { + int columnNum = ColumnNumber(); + if (columnNum > 1) + { + return Worksheet.Column(columnNum - 1).InsertColumnsAfter(numberOfColumns); + } + + Worksheet.Internals.ColumnsCollection.ShiftColumnsRight(columnNum, numberOfColumns); + Worksheet.Column(columnNum).InsertColumnsBeforeVoid(true, numberOfColumns); + + return Worksheet.Columns(columnNum, columnNum + numberOfColumns - 1); + } + + private void CopyColumns(IXLColumns newColumns) + { + foreach (var newColumn in newColumns) + { + var internalColumn = Worksheet.Internals.ColumnsCollection[newColumn.ColumnNumber()]; + internalColumn.Width = Width; + internalColumn.InnerStyle = InnerStyle; + internalColumn.Collapsed = Collapsed; + internalColumn.IsHidden = IsHidden; + internalColumn._outlineLevel = OutlineLevel; + } + } + + public IXLColumn AdjustToContents() + { + return AdjustToContents(1); + } + + public IXLColumn AdjustToContents(Int32 startRow) + { + return AdjustToContents(startRow, XLHelper.MaxRowNumber); + } + + public IXLColumn AdjustToContents(Int32 startRow, Int32 endRow) + { + return AdjustToContents(startRow, endRow, 0, Double.MaxValue); + } + + public IXLColumn AdjustToContents(Double minWidth, Double maxWidth) + { + return AdjustToContents(1, XLHelper.MaxRowNumber, minWidth, maxWidth); + } + + public IXLColumn AdjustToContents(Int32 startRow, Double minWidth, Double maxWidth) + { + return AdjustToContents(startRow, XLHelper.MaxRowNumber, minWidth, maxWidth); + } + + public IXLColumn AdjustToContents(Int32 startRow, Int32 endRow, Double minWidth, Double maxWidth) + { + var fontCache = new Dictionary(); + + Double colMaxWidth = minWidth; + + List autoFilterRows = new List(); + if (this.Worksheet.AutoFilter != null && this.Worksheet.AutoFilter.Range != null) + autoFilterRows.Add(this.Worksheet.AutoFilter.Range.FirstRow().RowNumber()); + + autoFilterRows.AddRange(Worksheet.Tables.Where(t => + t.AutoFilter != null + && t.AutoFilter.Range != null + && !autoFilterRows.Contains(t.AutoFilter.Range.FirstRow().RowNumber())) + .Select(t => t.AutoFilter.Range.FirstRow().RowNumber())); + + XLStyle cellStyle = null; + foreach (var c in Column(startRow, endRow).CellsUsed().Cast()) + { + if (c.IsMerged()) continue; + if (cellStyle == null || cellStyle.Value != c.StyleValue) + cellStyle = c.Style as XLStyle; + + Double thisWidthMax = 0; + Int32 textRotation = cellStyle.Alignment.TextRotation; + if (c.HasRichText || textRotation != 0 || c.InnerText.Contains(Environment.NewLine)) + { + var kpList = new List>(); + + #region if (c.HasRichText) + + if (c.HasRichText) + { + foreach (IXLRichString rt in c.RichText) + { + String formattedString = rt.Text; + var arr = formattedString.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + Int32 arrCount = arr.Count(); + for (Int32 i = 0; i < arrCount; i++) + { + String s = arr[i]; + if (i < arrCount - 1) + s += Environment.NewLine; + kpList.Add(new KeyValuePair(rt, s)); + } + } + } + else + { + String formattedString = c.GetFormattedString(); + var arr = formattedString.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + Int32 arrCount = arr.Count(); + for (Int32 i = 0; i < arrCount; i++) + { + String s = arr[i]; + if (i < arrCount - 1) + s += Environment.NewLine; + kpList.Add(new KeyValuePair(cellStyle.Font, s)); + } + } + + #endregion if (c.HasRichText) + + #region foreach (var kp in kpList) + + Double runningWidth = 0; + Boolean rotated = false; + Double maxLineWidth = 0; + Int32 lineCount = 1; + foreach (KeyValuePair kp in kpList) + { + var f = kp.Key; + String formattedString = kp.Value; + + Int32 newLinePosition = formattedString.IndexOf(Environment.NewLine); + if (textRotation == 0) + { + #region if (newLinePosition >= 0) + + if (newLinePosition >= 0) + { + if (newLinePosition > 0) + runningWidth += f.GetWidth(formattedString.Substring(0, newLinePosition), fontCache); + + if (runningWidth > thisWidthMax) + thisWidthMax = runningWidth; + + runningWidth = newLinePosition < formattedString.Length - 2 + ? f.GetWidth(formattedString.Substring(newLinePosition + 2), fontCache) + : 0; + } + else + runningWidth += f.GetWidth(formattedString, fontCache); + + #endregion if (newLinePosition >= 0) + } + else + { + #region if (textRotation == 255) + + if (textRotation == 255) + { + if (runningWidth <= 0) + runningWidth = f.GetWidth("X", fontCache); + + if (newLinePosition >= 0) + runningWidth += f.GetWidth("X", fontCache); + } + else + { + rotated = true; + Double vWidth = f.GetWidth("X", fontCache); + if (vWidth > maxLineWidth) + maxLineWidth = vWidth; + + if (newLinePosition >= 0) + { + lineCount++; + + if (newLinePosition > 0) + runningWidth += f.GetWidth(formattedString.Substring(0, newLinePosition), fontCache); + + if (runningWidth > thisWidthMax) + thisWidthMax = runningWidth; + + runningWidth = newLinePosition < formattedString.Length - 2 + ? f.GetWidth(formattedString.Substring(newLinePosition + 2), fontCache) + : 0; + } + else + runningWidth += f.GetWidth(formattedString, fontCache); + } + + #endregion if (textRotation == 255) + } + } + + #endregion foreach (var kp in kpList) + + if (runningWidth > thisWidthMax) + thisWidthMax = runningWidth; + + #region if (rotated) + + if (rotated) + { + Int32 rotation; + if (textRotation == 90 || textRotation == 180 || textRotation == 255) + rotation = 90; + else + rotation = textRotation % 90; + + Double r = DegreeToRadian(rotation); + + thisWidthMax = (thisWidthMax * Math.Cos(r)) + (maxLineWidth * lineCount); + } + + #endregion if (rotated) + } + else + thisWidthMax = cellStyle.Font.GetWidth(c.GetFormattedString(), fontCache); + + if (autoFilterRows.Contains(c.Address.RowNumber)) + thisWidthMax += 2.7148; // Allow room for arrow icon in autofilter + + if (thisWidthMax >= maxWidth) + { + colMaxWidth = maxWidth; + break; + } + + if (thisWidthMax > colMaxWidth) + colMaxWidth = thisWidthMax + 1; + } + + if (colMaxWidth <= 0) + colMaxWidth = Worksheet.ColumnWidth; + + Width = colMaxWidth; + + foreach (IDisposable font in fontCache.Values) + { + font.Dispose(); + } + return this; + } + + public IXLColumn Hide() + { + IsHidden = true; + return this; + } + + public IXLColumn Unhide() + { + IsHidden = false; + return this; + } + + public Boolean IsHidden { get; set; } + + public Int32 OutlineLevel + { + get { return _outlineLevel; } + set + { + if (value < 0 || value > 8) + throw new ArgumentOutOfRangeException("value", "Outline level must be between 0 and 8."); + + Worksheet.IncrementColumnOutline(value); + Worksheet.DecrementColumnOutline(_outlineLevel); + _outlineLevel = value; + } + } + + public IXLColumn Group() + { + return Group(false); + } + + public IXLColumn Group(Boolean collapse) + { + if (OutlineLevel < 8) + OutlineLevel += 1; + + Collapsed = collapse; + return this; + } + + public IXLColumn Group(Int32 outlineLevel) + { + return Group(outlineLevel, false); + } + + public IXLColumn Group(Int32 outlineLevel, Boolean collapse) + { + OutlineLevel = outlineLevel; + Collapsed = collapse; + return this; + } + + public IXLColumn Ungroup() + { + return Ungroup(false); + } + + public IXLColumn Ungroup(Boolean ungroupFromAll) + { + if (ungroupFromAll) + OutlineLevel = 0; + else + { + if (OutlineLevel > 0) + OutlineLevel -= 1; + } + return this; + } + + public IXLColumn Collapse() + { + Collapsed = true; + return Hide(); + } + + public IXLColumn Expand() + { + Collapsed = false; + return Unhide(); + } + + public Int32 CellCount() + { + return RangeAddress.LastAddress.ColumnNumber - RangeAddress.FirstAddress.ColumnNumber + 1; + } + + public IXLColumn Sort(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, + Boolean ignoreBlanks = true) + { + Sort(1, sortOrder, matchCase, ignoreBlanks); + return this; + } + + IXLRangeColumn IXLColumn.CopyTo(IXLCell target) + { + var copy = AsRange().CopyTo(target); + return copy.Column(1); + } + + IXLRangeColumn IXLColumn.CopyTo(IXLRangeBase target) + { + var copy = AsRange().CopyTo(target); + return copy.Column(1); + } + + public IXLColumn CopyTo(IXLColumn column) + { + column.Clear(); + var newColumn = (XLColumn)column; + newColumn.Width = Width; + newColumn.InnerStyle = InnerStyle; + newColumn.IsHidden = IsHidden; + + (this as XLRangeBase).CopyTo(column); + + return newColumn; + } + + public IXLRangeColumn Column(Int32 start, Int32 end) + { + return Range(start, 1, end, 1).Column(1); + } + + public IXLRangeColumn Column(IXLCell start, IXLCell end) + { + return Column(start.Address.RowNumber, end.Address.RowNumber); + } + + public IXLRangeColumns Columns(String columns) + { + var retVal = new XLRangeColumns(); + var columnPairs = columns.Split(','); + foreach (string pair in columnPairs) + AsRange().Columns(pair.Trim()).ForEach(retVal.Add); + return retVal; + } + + /// + /// Adds a vertical page break after this column. + /// + public IXLColumn AddVerticalPageBreak() + { + Worksheet.PageSetup.AddVerticalPageBreak(ColumnNumber()); + return this; + } + + public IXLColumn SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRangeColumn ColumnUsed(Boolean includeFormats) + { + return ColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLRangeColumn ColumnUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents) + { + return Column((this as IXLRangeBase).FirstCellUsed(options), + (this as IXLRangeBase).LastCellUsed(options)); + } + + #endregion IXLColumn Members + + public override XLRange AsRange() + { + return Range(1, 1, XLHelper.MaxRowNumber, 1); + } + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + return; // Columns are shifted by XLColumnCollection + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + //do nothing + } + + internal void SetColumnNumber(int column) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + 1, + column, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + XLHelper.MaxRowNumber, + column, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + + public override XLRange Range(String rangeAddressStr) + { + String rangeAddressToUse; + if (rangeAddressStr.Contains(':') || rangeAddressStr.Contains('-')) + { + if (rangeAddressStr.Contains('-')) + rangeAddressStr = rangeAddressStr.Replace('-', ':'); + + var arrRange = rangeAddressStr.Split(':'); + string firstPart = arrRange[0]; + string secondPart = arrRange[1]; + rangeAddressToUse = FixColumnAddress(firstPart) + ":" + FixColumnAddress(secondPart); + } + else + rangeAddressToUse = FixColumnAddress(rangeAddressStr); + + var rangeAddress = new XLRangeAddress(Worksheet, rangeAddressToUse); + return Range(rangeAddress); + } + + public IXLRangeColumn Range(int firstRow, int lastRow) + { + return Range(firstRow, 1, lastRow, 1).Column(1); + } + + private static double DegreeToRadian(double angle) + { + return Math.PI * angle / 180.0; + } + + private XLColumn ColumnShift(Int32 columnsToShift) + { + return Worksheet.Column(ColumnNumber() + columnsToShift); + } + + #region XLColumn Left + + IXLColumn IXLColumn.ColumnLeft() + { + return ColumnLeft(); + } + + IXLColumn IXLColumn.ColumnLeft(Int32 step) + { + return ColumnLeft(step); + } + + public XLColumn ColumnLeft() + { + return ColumnLeft(1); + } + + public XLColumn ColumnLeft(Int32 step) + { + return ColumnShift(step * -1); + } + + #endregion XLColumn Left + + #region XLColumn Right + + IXLColumn IXLColumn.ColumnRight() + { + return ColumnRight(); + } + + IXLColumn IXLColumn.ColumnRight(Int32 step) + { + return ColumnRight(step); + } + + public XLColumn ColumnRight() + { + return ColumnRight(1); + } + + public XLColumn ColumnRight(Int32 step) + { + return ColumnShift(step); + } + + #endregion XLColumn Right + + public override Boolean IsEmpty() + { + return IsEmpty(XLCellsUsedOptions.AllContents); + } + + public override Boolean IsEmpty(XLCellsUsedOptions options) + { + if (options.HasFlag(XLCellsUsedOptions.NormalFormats) && + !StyleValue.Equals(Worksheet.StyleValue)) + return false; + + return base.IsEmpty(options); + } + + public override Boolean IsEntireRow() + { + return false; + } + + public override Boolean IsEntireColumn() + { + return true; + } + } +} diff --git a/ClosedXML/Excel/Columns/XLColumnCollection.cs b/ClosedXML/Excel/Columns/XLColumnCollection.cs new file mode 100644 index 0000000..4c33ee9 --- /dev/null +++ b/ClosedXML/Excel/Columns/XLColumnCollection.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLColumnsCollection : IDictionary + { + public void ShiftColumnsRight(Int32 startingColumn, Int32 columnsToShift) + { + foreach (var co in _dictionary.Keys.Where(k => k >= startingColumn).OrderByDescending(k => k)) + { + var columnToMove = _dictionary[co]; + _dictionary.Remove(co); + Int32 newColumnNum = co + columnsToShift; + if (newColumnNum <= XLHelper.MaxColumnNumber) + { + columnToMove.SetColumnNumber(newColumnNum); + _dictionary.Add(newColumnNum, columnToMove); + } + } + } + + private readonly Dictionary _dictionary = new Dictionary(); + + public void Add(int key, XLColumn value) + { + _dictionary.Add(key, value); + } + + public bool ContainsKey(int key) + { + return _dictionary.ContainsKey(key); + } + + public ICollection Keys + { + get { return _dictionary.Keys; } + } + + public bool Remove(int key) + { + return _dictionary.Remove(key); + } + + public bool TryGetValue(int key, out XLColumn value) + { + return _dictionary.TryGetValue(key, out value); + } + + public ICollection Values + { + get { return _dictionary.Values; } + } + + public XLColumn this[int key] + { + get + { + return _dictionary[key]; + } + set + { + _dictionary[key] = value; + } + } + + public void Add(KeyValuePair item) + { + _dictionary.Add(item.Key, item.Value); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _dictionary.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public int Count + { + get { return _dictionary.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool Remove(KeyValuePair item) + { + return _dictionary.Remove(item.Key); + } + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + public void RemoveAll(Func predicate) + { + _dictionary.RemoveAll(predicate); + } + } +} diff --git a/ClosedXML/Excel/Columns/XLColumns.cs b/ClosedXML/Excel/Columns/XLColumns.cs new file mode 100644 index 0000000..108f70b --- /dev/null +++ b/ClosedXML/Excel/Columns/XLColumns.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLColumns : XLStylizedBase, IXLColumns, IXLStylized + { + private readonly List _columns = new List(); + private readonly XLWorksheet _worksheet; + + /// + /// Create a new instance of . + /// + /// If worksheet is specified it means that the created instance represents + /// all columns on a worksheet so changing its width will affect all columns. + /// Default style to use when initializing child entries. + public XLColumns(XLWorksheet worksheet, XLStyleValue defaultStyle = null) + : base(defaultStyle) + { + _worksheet = worksheet; + } + + #region IXLColumns Members + + public IEnumerator GetEnumerator() + { + return _columns.Cast().OrderBy(r => r.ColumnNumber()).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Double Width + { + set + { + _columns.ForEach(c => c.Width = value); + + if (_worksheet == null) return; + + _worksheet.ColumnWidth = value; + _worksheet.Internals.ColumnsCollection.ForEach(c => c.Value.Width = value); + } + } + + public void Delete() + { + if (_worksheet != null) + { + _worksheet.Internals.ColumnsCollection.Clear(); + _worksheet.Internals.CellsCollection.Clear(); + } + else + { + var toDelete = new Dictionary>(); + foreach (XLColumn c in _columns) + { + if (!toDelete.TryGetValue(c.Worksheet, out List list)) + { + list = new List(); + toDelete.Add(c.Worksheet, list); + } + + list.Add(c.ColumnNumber()); + } + + foreach (KeyValuePair> kp in toDelete) + { + foreach (int c in kp.Value.OrderByDescending(c => c)) + kp.Key.Column(c).Delete(); + } + } + } + + public IXLColumns AdjustToContents() + { + _columns.ForEach(c => c.AdjustToContents()); + return this; + } + + public IXLColumns AdjustToContents(Int32 startRow) + { + _columns.ForEach(c => c.AdjustToContents(startRow)); + return this; + } + + public IXLColumns AdjustToContents(Int32 startRow, Int32 endRow) + { + _columns.ForEach(c => c.AdjustToContents(startRow, endRow)); + return this; + } + + public IXLColumns AdjustToContents(Double minWidth, Double maxWidth) + { + _columns.ForEach(c => c.AdjustToContents(minWidth, maxWidth)); + return this; + } + + public IXLColumns AdjustToContents(Int32 startRow, Double minWidth, Double maxWidth) + { + _columns.ForEach(c => c.AdjustToContents(startRow, minWidth, maxWidth)); + return this; + } + + public IXLColumns AdjustToContents(Int32 startRow, Int32 endRow, Double minWidth, Double maxWidth) + { + _columns.ForEach(c => c.AdjustToContents(startRow, endRow, minWidth, maxWidth)); + return this; + } + + public void Hide() + { + _columns.ForEach(c => c.Hide()); + } + + public void Unhide() + { + _columns.ForEach(c => c.Unhide()); + } + + public void Group() + { + Group(false); + } + + public void Group(Int32 outlineLevel) + { + Group(outlineLevel, false); + } + + public void Ungroup() + { + Ungroup(false); + } + + public void Group(Boolean collapse) + { + _columns.ForEach(c => c.Group(collapse)); + } + + public void Group(Int32 outlineLevel, Boolean collapse) + { + _columns.ForEach(c => c.Group(outlineLevel, collapse)); + } + + public void Ungroup(Boolean ungroupFromAll) + { + _columns.ForEach(c => c.Ungroup(ungroupFromAll)); + } + + public void Collapse() + { + _columns.ForEach(c => c.Collapse()); + } + + public void Expand() + { + _columns.ForEach(c => c.Expand()); + } + + public IXLCells Cells() + { + var cells = new XLCells(false, XLCellsUsedOptions.All); + foreach (XLColumn container in _columns) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(true, XLCellsUsedOptions.All); + foreach (XLColumn container in _columns) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(true, options); + foreach (XLColumn container in _columns) + cells.Add(container.RangeAddress); + return cells; + } + + /// + /// Adds a vertical page break after this column. + /// + public IXLColumns AddVerticalPageBreaks() + { + foreach (XLColumn col in _columns) + col.Worksheet.PageSetup.AddVerticalPageBreak(col.ColumnNumber()); + return this; + } + + public IXLColumns SetDataType(XLDataType dataType) + { + _columns.ForEach(c => c.DataType = dataType); + return this; + } + + #endregion IXLColumns Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + if (_worksheet != null) + yield return _worksheet.Style; + else + { + foreach (IXLStyle s in _columns.SelectMany(col => col.Styles)) + { + yield return s; + } + } + } + } + + protected override IEnumerable Children + { + get + { + if (_worksheet != null) + yield return _worksheet; + else + { + foreach (XLColumn column in _columns) + yield return column; + } + } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + public void Add(XLColumn column) + { + _columns.Add(column); + } + + public void CollapseOnly() + { + _columns.ForEach(c => c.Collapsed = true); + } + + public IXLColumns Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _columns.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void Select() + { + foreach (var range in this) + range.Select(); + } + } +} diff --git a/ClosedXML/Excel/Comments/IXLComment.cs b/ClosedXML/Excel/Comments/IXLComment.cs new file mode 100644 index 0000000..9b3b30e --- /dev/null +++ b/ClosedXML/Excel/Comments/IXLComment.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLComment : IXLFormattedText, IXLDrawing + { + /// + /// Gets or sets this comment's author's name + /// + String Author { get; set; } + /// + /// Sets the name of the comment's author + /// + /// Author's name + IXLComment SetAuthor(String value); + + /// + /// Adds a bolded line with the author's name + /// + IXLRichString AddSignature(); + + void Delete(); + } + +} diff --git a/ClosedXML/Excel/Comments/XLComment.cs b/ClosedXML/Excel/Comments/XLComment.cs new file mode 100644 index 0000000..9144c40 --- /dev/null +++ b/ClosedXML/Excel/Comments/XLComment.cs @@ -0,0 +1,202 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLComment : XLFormattedText, IXLComment + { + private XLCell _cell; + + public XLComment(XLCell cell, IXLFontBase defaultFont = null, int? shapeId = null) + : base(defaultFont ?? XLFont.DefaultCommentFont) + { + Initialize(cell, shapeId: shapeId); + } + + public XLComment(XLCell cell, XLFormattedText defaultComment, IXLFontBase defaultFont, IXLDrawingStyle style) + : base(defaultComment, defaultFont) + { + Initialize(cell, style); + } + + public XLComment(XLCell cell, String text, IXLFontBase defaultFont) + : base(text, defaultFont) + { + Initialize(cell); + } + + #region IXLComment Members + + public String Author { get; set; } + + public IXLComment SetAuthor(String value) + { + Author = value; + return this; + } + + public IXLRichString AddSignature() + { + AddText(Author + ":").SetBold(); + return AddText(Environment.NewLine); + } + + public void Delete() + { + _cell.DeleteComment(); + } + + #endregion IXLComment Members + + #region IXLDrawing + + public String Name { get; set; } + public String Description { get; set; } + public XLDrawingAnchor Anchor { get; set; } + public Boolean HorizontalFlip { get; set; } + public Boolean VerticalFlip { get; set; } + public Int32 Rotation { get; set; } + public Int32 ExtentLength { get; set; } + public Int32 ExtentWidth { get; set; } + public Int32 ShapeId { get; internal set; } + public Boolean Visible { get; set; } + + public IXLComment SetVisible() + { + Visible = true; + return Container; + } + + public IXLComment SetVisible(Boolean hidden) + { + Visible = hidden; + return Container; + } + + public IXLDrawingPosition Position { get; private set; } + + public Int32 ZOrder { get; set; } + + public IXLComment SetZOrder(Int32 zOrder) + { + ZOrder = zOrder; + return Container; + } + + public IXLDrawingStyle Style { get; private set; } + + public IXLComment SetName(String name) + { + Name = name; + return Container; + } + + public IXLComment SetDescription(String description) + { + Description = description; + return Container; + } + + public IXLComment SetHorizontalFlip() + { + HorizontalFlip = true; + return Container; + } + + public IXLComment SetHorizontalFlip(Boolean horizontalFlip) + { + HorizontalFlip = horizontalFlip; + return Container; + } + + public IXLComment SetVerticalFlip() + { + VerticalFlip = true; + return Container; + } + + public IXLComment SetVerticalFlip(Boolean verticalFlip) + { + VerticalFlip = verticalFlip; + return Container; + } + + public IXLComment SetRotation(Int32 rotation) + { + Rotation = rotation; + return Container; + } + + public IXLComment SetExtentLength(Int32 extentLength) + { + ExtentLength = extentLength; + return Container; + } + + public IXLComment SetExtentWidth(Int32 extentWidth) + { + ExtentWidth = extentWidth; + return Container; + } + + #endregion IXLDrawing + + private void Initialize(XLCell cell, IXLDrawingStyle style = null, int? shapeId = null) + { + style = style ?? XLDrawingStyle.DefaultCommentStyle; + shapeId = shapeId ?? cell.Worksheet.Workbook.ShapeIdManager.GetNext(); + + Author = cell.Worksheet.Author; + Container = this; + Anchor = XLDrawingAnchor.MoveAndSizeWithCells; + Style = new XLDrawingStyle(); + Int32 previousRowNumber = cell.Address.RowNumber; + Double previousRowOffset = 0; + + if (previousRowNumber > 1) + { + previousRowNumber--; + + if (cell.Worksheet.Internals.RowsCollection.TryGetValue(previousRowNumber, out XLRow previousRow)) + previousRowOffset = Math.Max(0, previousRow.Height - 7); + else + previousRowOffset = Math.Max(0, cell.Worksheet.RowHeight - 7); + } + + Position = new XLDrawingPosition + { + Column = cell.Address.ColumnNumber + 1, + ColumnOffset = 2, + Row = previousRowNumber, + RowOffset = previousRowOffset + }; + + ZOrder = cell.Worksheet.ZOrder++; + Style + .Margins.SetLeft(style.Margins.Left) + .Margins.SetRight(style.Margins.Right) + .Margins.SetTop(style.Margins.Top) + .Margins.SetBottom(style.Margins.Bottom) + .Margins.SetAutomatic(style.Margins.Automatic) + .Size.SetHeight(style.Size.Height) + .Size.SetWidth(style.Size.Width) + .ColorsAndLines.SetLineColor(style.ColorsAndLines.LineColor) + .ColorsAndLines.SetFillColor(style.ColorsAndLines.FillColor) + .ColorsAndLines.SetLineDash(style.ColorsAndLines.LineDash) + .ColorsAndLines.SetLineStyle(style.ColorsAndLines.LineStyle) + .ColorsAndLines.SetLineWeight(style.ColorsAndLines.LineWeight) + .ColorsAndLines.SetFillTransparency(style.ColorsAndLines.FillTransparency) + .ColorsAndLines.SetLineTransparency(style.ColorsAndLines.LineTransparency) + .Alignment.SetHorizontal(style.Alignment.Horizontal) + .Alignment.SetVertical(style.Alignment.Vertical) + .Alignment.SetDirection(style.Alignment.Direction) + .Alignment.SetOrientation(style.Alignment.Orientation) + .Alignment.SetAutomaticSize(style.Alignment.AutomaticSize) + .Properties.SetPositioning(style.Properties.Positioning) + .Protection.SetLocked(style.Protection.Locked) + .Protection.SetLockText(style.Protection.LockText); + + _cell = cell; + ShapeId = shapeId.Value; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMax.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMax.cs new file mode 100644 index 0000000..40217cd --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMax.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLCFColorScaleMax + { + void Maximum(XLCFContentType type, String value, XLColor color); + void Maximum(XLCFContentType type, Double value, XLColor color); + void HighestValue(XLColor color); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMid.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMid.cs new file mode 100644 index 0000000..95e5145 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMid.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLCFColorScaleMid + { + IXLCFColorScaleMax Midpoint(XLCFContentType type, String value, XLColor color); + IXLCFColorScaleMax Midpoint(XLCFContentType type, Double value, XLColor color); + void Maximum(XLCFContentType type, String value, XLColor color); + void Maximum(XLCFContentType type, Double value, XLColor color); + void HighestValue(XLColor color); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMin.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMin.cs new file mode 100644 index 0000000..7abc9ea --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFColorScaleMin.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public enum XLCFContentType { Number, Percent, Formula, Percentile, Minimum, Maximum } + public interface IXLCFColorScaleMin + { + IXLCFColorScaleMid Minimum(XLCFContentType type, String value, XLColor color); + IXLCFColorScaleMid Minimum(XLCFContentType type, Double value, XLColor color); + IXLCFColorScaleMid LowestValue(XLColor color); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMax.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMax.cs new file mode 100644 index 0000000..05df66a --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMax.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLCFDataBarMax + { + void Maximum(XLCFContentType type, String value); + void Maximum(XLCFContentType type, Double value); + void HighestValue(); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMin.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMin.cs new file mode 100644 index 0000000..cd1236f --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFDataBarMin.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLCFDataBarMin + { + IXLCFDataBarMax Minimum(XLCFContentType type, String value); + IXLCFDataBarMax Minimum(XLCFContentType type, Double value); + IXLCFDataBarMax LowestValue(); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLCFIconSet.cs b/ClosedXML/Excel/ConditionalFormats/IXLCFIconSet.cs new file mode 100644 index 0000000..e9f8073 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLCFIconSet.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public enum XLCFIconSetOperator {GreaterThan, EqualOrGreaterThan} + public interface IXLCFIconSet + { + IXLCFIconSet AddValue(XLCFIconSetOperator setOperator, String value, XLCFContentType type); + IXLCFIconSet AddValue(XLCFIconSetOperator setOperator, Double value, XLCFContentType type); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormat.cs b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormat.cs new file mode 100644 index 0000000..ba18bf4 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormat.cs @@ -0,0 +1,177 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLTimePeriod + { + Yesterday, + Today, + Tomorrow, + InTheLast7Days, + LastWeek, + ThisWeek, + NextWeek, + LastMonth, + ThisMonth, + NextMonth + } + + public enum XLIconSetStyle + { + ThreeArrows, + ThreeArrowsGray, + ThreeFlags, + ThreeTrafficLights1, + ThreeTrafficLights2, + ThreeSigns, + ThreeSymbols, + ThreeSymbols2, + FourArrows, + FourArrowsGray, + FourRedToBlack, + FourRating, + FourTrafficLights, + FiveArrows, + FiveArrowsGray, + FiveRating, + FiveQuarters + } + + public enum XLConditionalFormatType + { + Expression, + CellIs, + ColorScale, + DataBar, + IconSet, + Top10, + IsUnique, + IsDuplicate, + ContainsText, + NotContainsText, + StartsWith, + EndsWith, + IsBlank, + NotBlank, + IsError, + NotError, + TimePeriod, + AboveAverage + } + + public enum XLCFOperator { Equal, NotEqual, GreaterThan, LessThan, EqualOrGreaterThan, EqualOrLessThan, Between, NotBetween, Contains, NotContains, StartsWith, EndsWith } + + public interface IXLConditionalFormat + { + IXLStyle Style { get; set; } + + IXLStyle WhenIsBlank(); + + IXLStyle WhenNotBlank(); + + IXLStyle WhenIsError(); + + IXLStyle WhenNotError(); + + IXLStyle WhenDateIs(XLTimePeriod timePeriod); + + IXLStyle WhenContains(String value); + + IXLStyle WhenNotContains(String value); + + IXLStyle WhenStartsWith(String value); + + IXLStyle WhenEndsWith(String value); + + IXLStyle WhenEquals(String value); + + IXLStyle WhenNotEquals(String value); + + IXLStyle WhenGreaterThan(String value); + + IXLStyle WhenLessThan(String value); + + IXLStyle WhenEqualOrGreaterThan(String value); + + IXLStyle WhenEqualOrLessThan(String value); + + IXLStyle WhenBetween(String minValue, String maxValue); + + IXLStyle WhenNotBetween(String minValue, String maxValue); + + IXLStyle WhenEquals(Double value); + + IXLStyle WhenNotEquals(Double value); + + IXLStyle WhenGreaterThan(Double value); + + IXLStyle WhenLessThan(Double value); + + IXLStyle WhenEqualOrGreaterThan(Double value); + + IXLStyle WhenEqualOrLessThan(Double value); + + IXLStyle WhenBetween(Double minValue, Double maxValue); + + IXLStyle WhenNotBetween(Double minValue, Double maxValue); + + IXLStyle WhenIsDuplicate(); + + IXLStyle WhenIsUnique(); + + IXLStyle WhenIsTrue(String formula); + + IXLStyle WhenIsTop(Int32 value, XLTopBottomType topBottomType = XLTopBottomType.Items); + + IXLStyle WhenIsBottom(Int32 value, XLTopBottomType topBottomType); + + IXLCFColorScaleMin ColorScale(); + + IXLCFDataBarMin DataBar(XLColor color, Boolean showBarOnly = false); + + IXLCFDataBarMin DataBar(XLColor positiveColor, XLColor negativeColor, Boolean showBarOnly = false); + + IXLCFIconSet IconSet(XLIconSetStyle iconSetStyle, Boolean reverseIconOrder = false, Boolean showIconOnly = false); + + XLConditionalFormatType ConditionalFormatType { get; } + + XLIconSetStyle IconSetStyle { get; } + + XLTimePeriod TimePeriod { get; } + + Boolean ReverseIconOrder { get; } + + Boolean ShowIconOnly { get; } + + Boolean ShowBarOnly { get; } + + Boolean StopIfTrue { get; } + + /// + /// The first of the . + /// + IXLRange Range { get; set; } + + IXLRanges Ranges { get; } + + XLDictionary Values { get; } + + XLDictionary Colors { get; } + + XLDictionary ContentTypes { get; } + + XLDictionary IconSetOperators { get; } + + XLCFOperator Operator { get; } + + Boolean Bottom { get; } + + Boolean Percent { get; } + + IXLConditionalFormat SetStopIfTrue(); + + IXLConditionalFormat SetStopIfTrue(Boolean value); + + IXLConditionalFormat CopyTo(IXLWorksheet targetSheet); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs new file mode 100644 index 0000000..cae5f8c --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLConditionalFormats : IEnumerable + { + void Add(IXLConditionalFormat conditionalFormat); + + void RemoveAll(); + + void Remove(Predicate predicate); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverter.cs new file mode 100644 index 0000000..bfa1778 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DocumentFormat.OpenXml.Spreadsheet; + +namespace ClosedXML.Excel +{ + internal interface IXLCFConverter + { + ConditionalFormattingRule Convert(IXLConditionalFormat cf, Int32 priority, XLWorkbook.SaveContext context); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverterExtension.cs b/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverterExtension.cs new file mode 100644 index 0000000..82b11a0 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/IXLCFConverterExtension.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DocumentFormat.OpenXml.Office2010.Excel; + +namespace ClosedXML.Excel +{ + internal interface IXLCFConverterExtension + { + ConditionalFormattingRule Convert(IXLConditionalFormat cf, XLWorkbook.SaveContext context); + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFBaseConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFBaseConverter.cs new file mode 100644 index 0000000..5461406 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFBaseConverter.cs @@ -0,0 +1,18 @@ +using ClosedXML.Utils; +using DocumentFormat.OpenXml.Spreadsheet; + +namespace ClosedXML.Excel +{ + internal static class XLCFBaseConverter + { + public static ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority) + { + return new ConditionalFormattingRule + { + Type = cf.ConditionalFormatType.ToOpenXml(), + Priority = priority, + StopIfTrue = OpenXmlHelper.GetBooleanValue(cf.StopIfTrue, false) + }; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFCellIsConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFCellIsConverter.cs new file mode 100644 index 0000000..256eaef --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFCellIsConverter.cs @@ -0,0 +1,46 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Globalization; + +namespace ClosedXML.Excel +{ + internal class XLCFCellIsConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + String val = GetQuoted(cf.Values[1]); + + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Operator = cf.Operator.ToOpenXml(); + + var formula = new Formula(val); + conditionalFormattingRule.Append(formula); + + if (cf.Operator == XLCFOperator.Between || cf.Operator == XLCFOperator.NotBetween) + { + var formula2 = new Formula { Text = GetQuoted(cf.Values[2]) }; + conditionalFormattingRule.Append(formula2); + } + + return conditionalFormattingRule; + } + + private String GetQuoted(XLFormula formula) + { + String value = formula.Value; + + if (formula.IsFormula || + value.StartsWith("\"") && value.EndsWith("\"") || + Double.TryParse(value, XLHelper.NumberStyle, XLHelper.ParseCulture, out double num)) + { + return value; + } + + return String.Format("\"{0}\"", value.Replace("\"", "\"\"")); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs new file mode 100644 index 0000000..b47906e --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs @@ -0,0 +1,51 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFColorScaleConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, Int32 priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + + var colorScale = new ColorScale(); + for (Int32 i = 1; i <= cf.ContentTypes.Count; i++) + { + var type = cf.ContentTypes[i].ToOpenXml(); + var val = cf.Values.TryGetValue(i, out XLFormula formula) ? formula?.Value : null; + + var conditionalFormatValueObject = new ConditionalFormatValueObject { Type = type }; + if (val != null) + conditionalFormatValueObject.Val = val; + + colorScale.Append(conditionalFormatValueObject); + } + + for (Int32 i = 1; i <= cf.Colors.Count; i++) + { + var xlColor = cf.Colors[i]; + var color = new Color(); + switch (xlColor.ColorType) + { + case XLColorType.Color: + color.Rgb = xlColor.Color.ToHex(); + break; + case XLColorType.Theme: + color.Theme = System.Convert.ToUInt32(xlColor.ThemeColor); + break; + + case XLColorType.Indexed: + color.Indexed = System.Convert.ToUInt32(xlColor.Indexed); + break; + } + + colorScale.Append(color); + } + + conditionalFormattingRule.Append(colorScale); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFContainsConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFContainsConverter.cs new file mode 100644 index 0000000..d4a43af --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFContainsConverter.cs @@ -0,0 +1,26 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFContainsConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + String val = cf.Values[1].Value; + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Operator = ConditionalFormattingOperatorValues.ContainsText; + conditionalFormattingRule.Text = val; + + var formula = new Formula { Text = "NOT(ISERROR(SEARCH(\"" + val + "\"," + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + ")))" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFConverters.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFConverters.cs new file mode 100644 index 0000000..c451c7c --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFConverters.cs @@ -0,0 +1,42 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLCFConverters + { + private static readonly Dictionary Converters; + static XLCFConverters() + { + Converters = new Dictionary + { + {XLConditionalFormatType.ColorScale, new XLCFColorScaleConverter()}, + {XLConditionalFormatType.StartsWith, new XLCFStartsWithConverter()}, + {XLConditionalFormatType.EndsWith, new XLCFEndsWithConverter()}, + {XLConditionalFormatType.IsBlank, new XLCFIsBlankConverter()}, + {XLConditionalFormatType.NotBlank, new XLCFNotBlankConverter()}, + {XLConditionalFormatType.IsError, new XLCFIsErrorConverter()}, + {XLConditionalFormatType.NotError, new XLCFNotErrorConverter()}, + {XLConditionalFormatType.ContainsText, new XLCFContainsConverter()}, + {XLConditionalFormatType.NotContainsText, new XLCFNotContainsConverter()}, + {XLConditionalFormatType.CellIs, new XLCFCellIsConverter()}, + {XLConditionalFormatType.IsUnique, new XLCFUniqueConverter()}, + {XLConditionalFormatType.IsDuplicate, new XLCFUniqueConverter()}, + {XLConditionalFormatType.Expression, new XLCFCellIsConverter()}, + {XLConditionalFormatType.Top10, new XLCFTopConverter()}, + {XLConditionalFormatType.DataBar, new XLCFDataBarConverter()}, + {XLConditionalFormatType.IconSet, new XLCFIconSetConverter()}, + {XLConditionalFormatType.TimePeriod, new XLCFDatesOccurringConverter()} + }; + } + + public static ConditionalFormattingRule Convert(IXLConditionalFormat conditionalFormat, Int32 priority, XLWorkbook.SaveContext context) + { + if (!Converters.TryGetValue(conditionalFormat.ConditionalFormatType, out var converter)) + throw new NotImplementedException(string.Format("Conditional formatting rule '{0}' hasn't been implemented", conditionalFormat.ConditionalFormatType)); + + return converter.Convert(conditionalFormat, priority, context); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFConvertersExtension.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFConvertersExtension.cs new file mode 100644 index 0000000..7b85951 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFConvertersExtension.cs @@ -0,0 +1,28 @@ +using DocumentFormat.OpenXml.Office2010.Excel; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLCFConvertersExtension + { + private readonly static Dictionary Converters; + + static XLCFConvertersExtension() + { + XLCFConvertersExtension.Converters = new Dictionary() + { + { XLConditionalFormatType.DataBar, new XLCFDataBarConverterExtension() } + }; + } + + public XLCFConvertersExtension() + { + } + + public static ConditionalFormattingRule Convert(IXLConditionalFormat conditionalFormat, XLWorkbook.SaveContext context) + { + return XLCFConvertersExtension.Converters[conditionalFormat.ConditionalFormatType].Convert(conditionalFormat, context); + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverter.cs new file mode 100644 index 0000000..91919a9 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverter.cs @@ -0,0 +1,82 @@ +using ClosedXML.Extensions; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFDataBarConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, Int32 priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + + var dataBar = new DataBar { ShowValue = !cf.ShowBarOnly }; + + var conditionalFormatValueObject1 = GetConditionalFormatValueObjectByIndex(cf, 1, ConditionalFormatValueObjectValues.Min); + var conditionalFormatValueObject2 = GetConditionalFormatValueObjectByIndex(cf, 2, ConditionalFormatValueObjectValues.Max); + + var color = new Color(); + switch (cf.Colors[1].ColorType) + { + case XLColorType.Color: + color.Rgb = cf.Colors[1].Color.ToHex(); + break; + + case XLColorType.Theme: + color.Theme = System.Convert.ToUInt32(cf.Colors[1].ThemeColor); + break; + + case XLColorType.Indexed: + color.Indexed = System.Convert.ToUInt32(cf.Colors[1].Indexed); + break; + } + + dataBar.Append(conditionalFormatValueObject1); + dataBar.Append(conditionalFormatValueObject2); + dataBar.Append(color); + + conditionalFormattingRule.Append(dataBar); + + var conditionalFormattingRuleExtensionList = new ConditionalFormattingRuleExtensionList(); + conditionalFormattingRuleExtensionList.Append(BuildRuleExtension(cf)); + conditionalFormattingRule.Append(conditionalFormattingRuleExtensionList); + + return conditionalFormattingRule; + } + + private ConditionalFormattingRuleExtension BuildRuleExtension(IXLConditionalFormat cf) + { + var conditionalFormattingRuleExtension = new ConditionalFormattingRuleExtension { Uri = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" }; + conditionalFormattingRuleExtension.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"); + var id = new DocumentFormat.OpenXml.Office2010.Excel.Id + { + Text = (cf as XLConditionalFormat).Id.WrapInBraces() + }; + conditionalFormattingRuleExtension.Append(id); + + return conditionalFormattingRuleExtension; + } + + private ConditionalFormatValueObject GetConditionalFormatValueObjectByIndex(IXLConditionalFormat cf, int index, ConditionalFormatValueObjectValues defaultType) + { + var conditionalFormatValueObject = new ConditionalFormatValueObject(); + + if (cf.ContentTypes.TryGetValue(index, out var contentType)) + { + conditionalFormatValueObject.Type = contentType.ToOpenXml(); + } + else + { + conditionalFormatValueObject.Type = defaultType; + } + + if (cf.Values.TryGetValue(index, out var value1) && value1?.Value != null) + { + conditionalFormatValueObject.Val = value1.Value; + } + + return conditionalFormatValueObject; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs new file mode 100644 index 0000000..8c24428 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs @@ -0,0 +1,91 @@ +using ClosedXML.Extensions; +using DocumentFormat.OpenXml.Office.Excel; +using DocumentFormat.OpenXml.Office2010.Excel; +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLCFDataBarConverterExtension : IXLCFConverterExtension + { + public XLCFDataBarConverterExtension() + { + } + + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, XLWorkbook.SaveContext context) + { + ConditionalFormattingRule conditionalFormattingRule = new ConditionalFormattingRule() + { + Type = DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValues.DataBar, + Id = (cf as XLConditionalFormat).Id.WrapInBraces() + }; + + DataBar dataBar = new DataBar() + { + MinLength = 0, + MaxLength = 100, + Gradient = true, + ShowValue = !cf.ShowBarOnly + }; + + var cfMinType = cf.ContentTypes.TryGetValue(1, out var contentType1) + ? Convert(contentType1.ToOpenXml()) + : ConditionalFormattingValueObjectTypeValues.AutoMin; + var cfMin = new ConditionalFormattingValueObject { Type = cfMinType }; + if (cf.Values.Any() && cf.Values[1]?.Value != null) + { + cfMin.Type = ConditionalFormattingValueObjectTypeValues.Numeric; + cfMin.Append(new Formula() { Text = cf.Values[1].Value }); + } + + var cfMaxType = cf.ContentTypes.TryGetValue(2, out var contentType2) + ? Convert(contentType2.ToOpenXml()) + : ConditionalFormattingValueObjectTypeValues.AutoMax; + var cfMax = new ConditionalFormattingValueObject { Type = cfMaxType }; + if (cf.Values.Count >= 2 && cf.Values[2]?.Value != null) + { + cfMax.Type = ConditionalFormattingValueObjectTypeValues.Numeric; + cfMax.Append(new Formula() { Text = cf.Values[2].Value }); + } + + var barAxisColor = new BarAxisColor { Rgb = XLColor.Black.Color.ToHex() }; + + var negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[1].Color.ToHex() }; + if (cf.Colors.Count == 2) + { + negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[2].Color.ToHex() }; + } + + dataBar.Append(cfMin); + dataBar.Append(cfMax); + + dataBar.Append(negativeFillColor); + dataBar.Append(barAxisColor); + + conditionalFormattingRule.Append(dataBar); + + return conditionalFormattingRule; + } + + private ConditionalFormattingValueObjectTypeValues Convert(DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues obj) + { + switch (obj) + { + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Max: + return ConditionalFormattingValueObjectTypeValues.AutoMax; + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Min: + return ConditionalFormattingValueObjectTypeValues.AutoMin; + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Number: + return ConditionalFormattingValueObjectTypeValues.Numeric; + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percent: + return ConditionalFormattingValueObjectTypeValues.Percent; + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percentile: + return ConditionalFormattingValueObjectTypeValues.Percentile; + case DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Formula: + return ConditionalFormattingValueObjectTypeValues.Formula; + default: + throw new NotImplementedException(); + } + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDatesOccuringConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDatesOccuringConverter.cs new file mode 100644 index 0000000..e71d3c4 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDatesOccuringConverter.cs @@ -0,0 +1,40 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLCFDatesOccurringConverter : IXLCFConverter + { + private static readonly IDictionary formulaTemplates = new Dictionary() + { + [XLTimePeriod.Today] = "FLOOR({0},1)=TODAY()", + [XLTimePeriod.Yesterday] = "FLOOR({0},1)=TODAY()-1", + [XLTimePeriod.Tomorrow] = "FLOOR({0},1)=TODAY()+1", + [XLTimePeriod.InTheLast7Days] = "AND(TODAY()-FLOOR({0},1)<=6,FLOOR({0},1)<=TODAY())", + [XLTimePeriod.ThisMonth] = "AND(MONTH({0})=MONTH(TODAY()),YEAR({0})=YEAR(TODAY()))", + [XLTimePeriod.LastMonth] = "AND(MONTH({0})=MONTH(EDATE(TODAY(),0-1)),YEAR({0})=YEAR(EDATE(TODAY(),0-1)))", + [XLTimePeriod.NextMonth] = "AND(MONTH({0})=MONTH(EDATE(TODAY(),0+1)),YEAR({0})=YEAR(EDATE(TODAY(),0+1)))", + [XLTimePeriod.ThisWeek] = "AND(TODAY()-ROUNDDOWN({0},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN({0},0)-TODAY()<=7-WEEKDAY(TODAY()))", + [XLTimePeriod.LastWeek] = "AND(TODAY()-ROUNDDOWN({0},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN({0},0)-TODAY()<=7-WEEKDAY(TODAY()))", + [XLTimePeriod.NextWeek] = "AND(ROUNDDOWN({0},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN({0},0)-TODAY()<(15-WEEKDAY(TODAY())))" + }; + + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.TimePeriod = cf.TimePeriod.ToOpenXml(); + + var address = cf.Range.RangeAddress.FirstAddress.ToStringRelative(false); + var formula = new Formula { Text = String.Format(formulaTemplates[cf.TimePeriod], address) }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFEndsWithConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFEndsWithConverter.cs new file mode 100644 index 0000000..df10a31 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFEndsWithConverter.cs @@ -0,0 +1,28 @@ +using System; +using DocumentFormat.OpenXml.Spreadsheet; + +namespace ClosedXML.Excel +{ + internal class XLCFEndsWithConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + String val = cf.Values[1].Value; + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Operator = ConditionalFormattingOperatorValues.EndsWith; + conditionalFormattingRule.Text = val; + + var formula = new Formula { Text = "RIGHT(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "," + val.Length.ToString() + ")=\"" + val + "\"" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + + + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFIconSetConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIconSetConverter.cs new file mode 100644 index 0000000..722465d --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIconSetConverter.cs @@ -0,0 +1,24 @@ +using System; +using DocumentFormat.OpenXml.Spreadsheet; + +namespace ClosedXML.Excel +{ + internal class XLCFIconSetConverter:IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, Int32 priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + + var iconSet = new IconSet {ShowValue = !cf.ShowIconOnly, Reverse = cf.ReverseIconOrder, IconSetValue = cf.IconSetStyle.ToOpenXml()}; + Int32 count = cf.Values.Count; + for(Int32 i=1;i<= count; i++ ) + { + var conditionalFormatValueObject = new ConditionalFormatValueObject { Type = cf.ContentTypes[i].ToOpenXml(), Val = cf.Values[i].Value, GreaterThanOrEqual = cf.IconSetOperators[i] == XLCFIconSetOperator.EqualOrGreaterThan}; + iconSet.Append(conditionalFormatValueObject); + + } + conditionalFormattingRule.Append(iconSet); + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsBlankConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsBlankConverter.cs new file mode 100644 index 0000000..8571b7f --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsBlankConverter.cs @@ -0,0 +1,22 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFIsBlankConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + var formula = new Formula { Text = "LEN(TRIM(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "))=0" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsErrorConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsErrorConverter.cs new file mode 100644 index 0000000..b98be05 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFIsErrorConverter.cs @@ -0,0 +1,22 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFIsErrorConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + var formula = new Formula { Text = "ISERROR(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + ")" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotBlankConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotBlankConverter.cs new file mode 100644 index 0000000..d6b6041 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotBlankConverter.cs @@ -0,0 +1,22 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFNotBlankConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + var formula = new Formula { Text = "LEN(TRIM(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "))>0" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotContainsConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotContainsConverter.cs new file mode 100644 index 0000000..94d310a --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotContainsConverter.cs @@ -0,0 +1,26 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFNotContainsConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + String val = cf.Values[1].Value; + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Operator = ConditionalFormattingOperatorValues.NotContains; + conditionalFormattingRule.Text = val; + + var formula = new Formula { Text = "ISERROR(SEARCH(\"" + val + "\"," + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "))" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotErrorConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotErrorConverter.cs new file mode 100644 index 0000000..e09299d --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFNotErrorConverter.cs @@ -0,0 +1,22 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFNotErrorConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + var formula = new Formula { Text = "NOT(ISERROR(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "))" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFStartsWithConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFStartsWithConverter.cs new file mode 100644 index 0000000..1b5d124 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFStartsWithConverter.cs @@ -0,0 +1,26 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFStartsWithConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + String val = cf.Values[1].Value; + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Operator = ConditionalFormattingOperatorValues.BeginsWith; + conditionalFormattingRule.Text = val; + + var formula = new Formula { Text = "LEFT(" + cf.Range.RangeAddress.FirstAddress.ToStringRelative(false) + "," + val.Length.ToString() + ")=\"" + val + "\"" }; + + conditionalFormattingRule.Append(formula); + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFTopConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFTopConverter.cs new file mode 100644 index 0000000..4870315 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFTopConverter.cs @@ -0,0 +1,22 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFTopConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + UInt32 val = UInt32.Parse(cf.Values[1].Value); + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + conditionalFormattingRule.Percent = cf.Percent; + conditionalFormattingRule.Rank = val; + conditionalFormattingRule.Bottom = cf.Bottom; + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFUniqueConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFUniqueConverter.cs new file mode 100644 index 0000000..fddc065 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFUniqueConverter.cs @@ -0,0 +1,18 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFUniqueConverter : IXLCFConverter + { + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, int priority, XLWorkbook.SaveContext context) + { + var conditionalFormattingRule = XLCFBaseConverter.Convert(cf, priority); + var cfStyle = (cf.Style as XLStyle).Value; + if (!cfStyle.Equals(XLWorkbook.DefaultStyleValue)) + conditionalFormattingRule.FormatId = (UInt32)context.DifferentialFormats[cfStyle]; + + return conditionalFormattingRule; + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMax.cs b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMax.cs new file mode 100644 index 0000000..8d4cd00 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMax.cs @@ -0,0 +1,30 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFColorScaleMax : IXLCFColorScaleMax + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFColorScaleMax(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + + public void Maximum(XLCFContentType type, String value, XLColor color) + { + _conditionalFormat.Values.Add(new XLFormula { Value = value }); + _conditionalFormat.Colors.Add(color); + _conditionalFormat.ContentTypes.Add(type); + } + public void Maximum(XLCFContentType type, Double value, XLColor color) + { + Maximum(type, value.ToInvariantString(), color); + } + public void HighestValue(XLColor color) + { + _conditionalFormat.Values.Add(null); + _conditionalFormat.Colors.Add(color); + _conditionalFormat.ContentTypes.Add(XLCFContentType.Maximum); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMid.cs b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMid.cs new file mode 100644 index 0000000..222649e --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMid.cs @@ -0,0 +1,38 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFColorScaleMid : IXLCFColorScaleMid + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFColorScaleMid(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + public IXLCFColorScaleMax Midpoint(XLCFContentType type, String value, XLColor color) + { + _conditionalFormat.Values.Add(new XLFormula { Value = value }); + _conditionalFormat.Colors.Add(color); + _conditionalFormat.ContentTypes.Add(type); + return new XLCFColorScaleMax(_conditionalFormat); + } + public IXLCFColorScaleMax Midpoint(XLCFContentType type, Double value, XLColor color) + { + return Midpoint(type, value.ToInvariantString(), color); + } + public void Maximum(XLCFContentType type, String value, XLColor color) + { + Midpoint(type, value, color); + } + public void Maximum(XLCFContentType type, Double value, XLColor color) + { + Maximum(type, value.ToInvariantString(), color); + } + public void HighestValue(XLColor color) + { + _conditionalFormat.Values.Initialize(null); + _conditionalFormat.Colors.Add(color); + _conditionalFormat.ContentTypes.Add(XLCFContentType.Maximum); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMin.cs b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMin.cs new file mode 100644 index 0000000..35c9a78 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFColorScaleMin.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLCFColorScaleMin : IXLCFColorScaleMin + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFColorScaleMin(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + + public IXLCFColorScaleMid Minimum(XLCFContentType type, String value, XLColor color) + { + _conditionalFormat.Values.Initialize(new XLFormula { Value = value }); + _conditionalFormat.Colors.Initialize(color); + _conditionalFormat.ContentTypes.Initialize(type); + return new XLCFColorScaleMid(_conditionalFormat); + } + public IXLCFColorScaleMid Minimum(XLCFContentType type, Double value, XLColor color) + { + return Minimum(type, value.ToInvariantString(), color); + } + + public IXLCFColorScaleMid LowestValue(XLColor color) + { + _conditionalFormat.Values.Initialize(null); + _conditionalFormat.Colors.Initialize(color); + _conditionalFormat.ContentTypes.Initialize(XLCFContentType.Minimum); + return new XLCFColorScaleMid(_conditionalFormat); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMax.cs b/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMax.cs new file mode 100644 index 0000000..50658ac --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMax.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLCFDataBarMax : IXLCFDataBarMax + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFDataBarMax(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + + public void Maximum(XLCFContentType type, String value) + { + _conditionalFormat.ContentTypes.Add(type); + _conditionalFormat.Values.Add(new XLFormula { Value = value }); + } + public void Maximum(XLCFContentType type, Double value) + { + Maximum(type, value.ToInvariantString()); + } + + public void HighestValue() + { + Maximum(XLCFContentType.Maximum, "0"); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMin.cs b/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMin.cs new file mode 100644 index 0000000..b6803dc --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFDataBarMin.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLCFDataBarMin : IXLCFDataBarMin + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFDataBarMin(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + + public IXLCFDataBarMax Minimum(XLCFContentType type, String value) + { + _conditionalFormat.ContentTypes.Initialize(type); + _conditionalFormat.Values.Initialize(new XLFormula { Value = value }); + return new XLCFDataBarMax(_conditionalFormat); + } + public IXLCFDataBarMax Minimum(XLCFContentType type, Double value) + { + return Minimum(type, value.ToInvariantString()); + } + + public IXLCFDataBarMax LowestValue() + { + return Minimum(XLCFContentType.Minimum, "0"); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLCFIconSet.cs b/ClosedXML/Excel/ConditionalFormats/XLCFIconSet.cs new file mode 100644 index 0000000..2c88ea5 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLCFIconSet.cs @@ -0,0 +1,24 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLCFIconSet : IXLCFIconSet + { + private readonly XLConditionalFormat _conditionalFormat; + public XLCFIconSet(XLConditionalFormat conditionalFormat) + { + _conditionalFormat = conditionalFormat; + } + public IXLCFIconSet AddValue(XLCFIconSetOperator setOperator, String value, XLCFContentType type) + { + _conditionalFormat.IconSetOperators.Add(setOperator); + _conditionalFormat.Values.Add(new XLFormula { Value = value }); + _conditionalFormat.ContentTypes.Add(type); + return new XLCFIconSet(_conditionalFormat); + } + public IXLCFIconSet AddValue(XLCFIconSetOperator setOperator, Double value, XLCFContentType type) + { + return AddValue(setOperator, value.ToInvariantString(), type); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs new file mode 100644 index 0000000..7012825 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLConditionalFormat : XLStylizedBase, IXLConditionalFormat, IXLStylized + { + private sealed class FullEqualityComparer : IEqualityComparer + { + private readonly bool _compareRange; + private readonly DictionaryComparer _colorsComparer = new DictionaryComparer(); + private readonly EnumerableComparer _listComparer = new EnumerableComparer(); + private readonly DictionaryComparer _contentsTypeComparer = new DictionaryComparer(); + private readonly DictionaryComparer _iconSetTypeComparer = new DictionaryComparer(); + + public FullEqualityComparer(bool compareRange) + { + _compareRange = compareRange; + } + + public bool Equals(IXLConditionalFormat x, IXLConditionalFormat y) + { + var xx = (XLConditionalFormat)x; + var yy = (XLConditionalFormat)y; + if (ReferenceEquals(xx, yy)) return true; + if (ReferenceEquals(xx, null)) return false; + if (ReferenceEquals(yy, null)) return false; + if (xx.GetType() != yy.GetType()) return false; + + var xxValues = xx.Values.Values.Where(v => v == null || !v.IsFormula).Select(v => v?.Value); + var yyValues = yy.Values.Values.Where(v => v == null || !v.IsFormula).Select(v => v?.Value); + var xxFormulas = x.Ranges.Count > 0 ? xx.Values.Values.Where(v => v != null && v.IsFormula).Select(f => ((XLCell)x.Ranges.First().FirstCell()).GetFormulaR1C1(f.Value)) : null; + var yyFormulas = y.Ranges.Count > 0 ? yy.Values.Values.Where(v => v != null && v.IsFormula).Select(f => ((XLCell)y.Ranges.First().FirstCell()).GetFormulaR1C1(f.Value)) : null; + + var xStyle = xx.StyleValue; + var yStyle = yy.StyleValue; + + return Equals(xStyle, yStyle) + && xx.CopyDefaultModify == yy.CopyDefaultModify + && xx.ConditionalFormatType == yy.ConditionalFormatType + && xx.TimePeriod == yy.TimePeriod + && xx.IconSetStyle == yy.IconSetStyle + && xx.Operator == yy.Operator + && xx.Bottom == yy.Bottom + && xx.Percent == yy.Percent + && xx.ReverseIconOrder == yy.ReverseIconOrder + && xx.StopIfTrue == yy.StopIfTrue + && xx.ShowIconOnly == yy.ShowIconOnly + && xx.ShowBarOnly == yy.ShowBarOnly + && _listComparer.Equals(xxValues, yyValues) + && _listComparer.Equals(xxFormulas, yyFormulas) + && _colorsComparer.Equals(xx.Colors, yy.Colors) + && _contentsTypeComparer.Equals(xx.ContentTypes, yy.ContentTypes) + && _iconSetTypeComparer.Equals(xx.IconSetOperators, yy.IconSetOperators) + && (!_compareRange || XLRanges.Equals(xx.Ranges, yy.Ranges)); + } + + public int GetHashCode(IXLConditionalFormat obj) + { + var xx = (XLConditionalFormat)obj; + var xStyle = (obj.Style as XLStyle).Value; + var xValues = xx.Values.Values.Where(v => !v.IsFormula).Select(v => v.Value); + if (obj.Ranges.Count > 0) + xValues = xValues + .Union(xx.Values.Values.Where(v => v.IsFormula).Select(f => ((XLCell)obj.Ranges.First().FirstCell()).GetFormulaR1C1(f.Value))); + + unchecked + { + var hashCode = xStyle.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.StyleValue.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.CopyDefaultModify.GetHashCode(); + hashCode = (hashCode * 397) ^ xValues.GetHashCode(); + hashCode = (hashCode * 397) ^ (xx.Colors != null ? xx.Colors.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (xx.ContentTypes != null ? xx.ContentTypes.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (xx.IconSetOperators != null ? xx.IconSetOperators.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_compareRange && xx.Ranges != null ? xx.Ranges.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)xx.ConditionalFormatType; + hashCode = (hashCode * 397) ^ (int)xx.TimePeriod; + hashCode = (hashCode * 397) ^ (int)xx.IconSetStyle; + hashCode = (hashCode * 397) ^ (int)xx.Operator; + hashCode = (hashCode * 397) ^ xx.Bottom.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.Percent.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.ReverseIconOrder.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.ShowIconOnly.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.ShowBarOnly.GetHashCode(); + hashCode = (hashCode * 397) ^ xx.StopIfTrue.GetHashCode(); + return hashCode; + } + } + } + + internal void AdjustFormulas(XLCell baseCell, XLCell targetCell) + { + var keys = Values.Keys.ToList(); + foreach (var key in keys) + { + if (Values[key] == null || !Values[key].IsFormula) + continue; + + var r1c1 = baseCell.GetFormulaR1C1(Values[key].Value); + Values[key] = new XLFormula { _value = targetCell.GetFormulaA1(r1c1), IsFormula = true }; + } + } + + private static readonly IEqualityComparer FullComparerInstance = new FullEqualityComparer(true); + + public static IEqualityComparer FullComparer + { + get { return FullComparerInstance; } + } + + private static readonly IEqualityComparer NoRangeComparerInstance = new FullEqualityComparer(false); + + public static IEqualityComparer NoRangeComparer + { + get { return NoRangeComparerInstance; } + } + + #region Constructors + + private XLConditionalFormat(XLStyleValue style) + : base(XLStyle.Default.Value) + { + Id = Guid.NewGuid(); + Ranges = new XLRanges(); + Values = new XLDictionary(); + Colors = new XLDictionary(); + ContentTypes = new XLDictionary(); + IconSetOperators = new XLDictionary(); + } + + public XLConditionalFormat(XLRange range, Boolean copyDefaultModify = false) + : this(XLStyle.Default.Value) + { + if (range != null) + Ranges.Add(range); + CopyDefaultModify = copyDefaultModify; + } + + public XLConditionalFormat(IEnumerable ranges, Boolean copyDefaultModify = false) + : this(XLStyle.Default.Value) + { + ranges?.ForEach(range => Ranges.Add(range)); + CopyDefaultModify = copyDefaultModify; + } + + public XLConditionalFormat(XLConditionalFormat conditionalFormat, IXLRange targetRange) + : this(conditionalFormat, new[] { targetRange }) + { + } + + public XLConditionalFormat(XLConditionalFormat conditionalFormat, IEnumerable targetRanges) + : this(conditionalFormat.StyleValue) + { + targetRanges?.ForEach(range => Ranges.Add(range)); + CopyFrom(conditionalFormat); + } + + #endregion Constructors + + public Guid Id { get; internal set; } + + internal Int32 OriginalPriority { get; set; } + + public Boolean CopyDefaultModify { get; set; } + + public override IEnumerable Styles + { + get + { + yield return Style; + } + } + + protected override IEnumerable Children + { + get { yield break; } + } + + public override IXLRanges RangesUsed + { + get { return new XLRanges(); } + } + + public XLDictionary Values { get; private set; } + + public XLDictionary Colors { get; private set; } + + public XLDictionary ContentTypes { get; private set; } + + public XLDictionary IconSetOperators { get; private set; } + + public IXLRange Range + { + get { return Ranges.FirstOrDefault(); } + set + { + Ranges.RemoveAll(); + Ranges.Add(value); + } + } + + public IXLRanges Ranges { get; private set; } + + public XLConditionalFormatType ConditionalFormatType { get; set; } + + public XLTimePeriod TimePeriod { get; set; } + + public XLIconSetStyle IconSetStyle { get; set; } + + public XLCFOperator Operator { get; set; } + + public Boolean Bottom { get; set; } + + public Boolean Percent { get; set; } + + public Boolean ReverseIconOrder { get; set; } + + public Boolean ShowIconOnly { get; set; } + + public Boolean ShowBarOnly { get; set; } + + public Boolean StopIfTrue { get; set; } + + public IXLConditionalFormat SetStopIfTrue() + { + return SetStopIfTrue(true); + } + + public IXLConditionalFormat SetStopIfTrue(bool value) + { + this.StopIfTrue = value; + return this; + } + + public IXLConditionalFormat CopyTo(IXLWorksheet targetSheet) + { + if (targetSheet == Range?.Worksheet) + throw new InvalidOperationException("Cannot copy conditional format to the worksheet it already belongs to."); + var targetRanges = Ranges.Select(r => targetSheet.Range(((XLRangeAddress)r.RangeAddress).WithoutWorksheet())); + var newCf = new XLConditionalFormat(this, targetRanges); + targetSheet.ConditionalFormats.Add(newCf); + return newCf; + } + + public void CopyFrom(IXLConditionalFormat other) + { + InnerStyle = other.Style; + ConditionalFormatType = other.ConditionalFormatType; + TimePeriod = other.TimePeriod; + IconSetStyle = other.IconSetStyle; + Operator = other.Operator; + Bottom = other.Bottom; + Percent = other.Percent; + ReverseIconOrder = other.ReverseIconOrder; + ShowIconOnly = other.ShowIconOnly; + ShowBarOnly = other.ShowBarOnly; + StopIfTrue = other.StopIfTrue; + + Values.Clear(); + other.Values.ForEach(kp => Values.Add(kp.Key, new XLFormula(kp.Value))); + //CopyDictionary(Values, other.Values); + CopyDictionary(Colors, other.Colors); + CopyDictionary(ContentTypes, other.ContentTypes); + CopyDictionary(IconSetOperators, other.IconSetOperators); + } + + private void CopyDictionary(XLDictionary target, XLDictionary source) + { + target.Clear(); + source.ForEach(kp => target.Add(kp.Key, kp.Value)); + } + + public IXLStyle WhenIsBlank() + { + ConditionalFormatType = XLConditionalFormatType.IsBlank; + return Style; + } + + public IXLStyle WhenNotBlank() + { + ConditionalFormatType = XLConditionalFormatType.NotBlank; + return Style; + } + + public IXLStyle WhenIsError() + { + ConditionalFormatType = XLConditionalFormatType.IsError; + return Style; + } + + public IXLStyle WhenNotError() + { + ConditionalFormatType = XLConditionalFormatType.NotError; + return Style; + } + + public IXLStyle WhenDateIs(XLTimePeriod timePeriod) + { + TimePeriod = timePeriod; + ConditionalFormatType = XLConditionalFormatType.TimePeriod; + return Style; + } + + public IXLStyle WhenContains(String value) + { + Values.Initialize(new XLFormula { Value = value }); + ConditionalFormatType = XLConditionalFormatType.ContainsText; + Operator = XLCFOperator.Contains; + return Style; + } + + public IXLStyle WhenNotContains(String value) + { + Values.Initialize(new XLFormula { Value = value }); + ConditionalFormatType = XLConditionalFormatType.NotContainsText; + Operator = XLCFOperator.NotContains; + return Style; + } + + public IXLStyle WhenStartsWith(String value) + { + Values.Initialize(new XLFormula { Value = value }); + ConditionalFormatType = XLConditionalFormatType.StartsWith; + Operator = XLCFOperator.StartsWith; + return Style; + } + + public IXLStyle WhenEndsWith(String value) + { + Values.Initialize(new XLFormula { Value = value }); + ConditionalFormatType = XLConditionalFormatType.EndsWith; + Operator = XLCFOperator.EndsWith; + return Style; + } + + public IXLStyle WhenEquals(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.Equal; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenNotEquals(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.NotEqual; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenGreaterThan(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.GreaterThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenLessThan(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.LessThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenEqualOrGreaterThan(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.EqualOrGreaterThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenEqualOrLessThan(String value) + { + Values.Initialize(new XLFormula { Value = value }); + Operator = XLCFOperator.EqualOrLessThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenBetween(String minValue, String maxValue) + { + Values.Initialize(new XLFormula { Value = minValue }); + Values.Add(new XLFormula { Value = maxValue }); + Operator = XLCFOperator.Between; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenNotBetween(String minValue, String maxValue) + { + Values.Initialize(new XLFormula { Value = minValue }); + Values.Add(new XLFormula { Value = maxValue }); + Operator = XLCFOperator.NotBetween; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenEquals(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.Equal; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenNotEquals(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.NotEqual; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenGreaterThan(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.GreaterThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenLessThan(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.LessThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenEqualOrGreaterThan(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.EqualOrGreaterThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenEqualOrLessThan(Double value) + { + Values.Initialize(new XLFormula(value)); + Operator = XLCFOperator.EqualOrLessThan; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenBetween(Double minValue, Double maxValue) + { + Values.Initialize(new XLFormula(minValue)); + Values.Add(new XLFormula(maxValue)); + Operator = XLCFOperator.Between; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenNotBetween(Double minValue, Double maxValue) + { + Values.Initialize(new XLFormula(minValue)); + Values.Add(new XLFormula(maxValue)); + Operator = XLCFOperator.NotBetween; + ConditionalFormatType = XLConditionalFormatType.CellIs; + return Style; + } + + public IXLStyle WhenIsDuplicate() + { + ConditionalFormatType = XLConditionalFormatType.IsDuplicate; + return Style; + } + + public IXLStyle WhenIsUnique() + { + ConditionalFormatType = XLConditionalFormatType.IsUnique; + return Style; + } + + public IXLStyle WhenIsTrue(String formula) + { + String f = formula.TrimStart()[0] == '=' ? formula : "=" + formula; + Values.Initialize(new XLFormula { Value = f }); + ConditionalFormatType = XLConditionalFormatType.Expression; + return Style; + } + + public IXLStyle WhenIsTop(Int32 value, XLTopBottomType topBottomType = XLTopBottomType.Items) + { + Values.Initialize(new XLFormula(value)); + Percent = topBottomType == XLTopBottomType.Percent; + ConditionalFormatType = XLConditionalFormatType.Top10; + Bottom = false; + return Style; + } + + public IXLStyle WhenIsBottom(Int32 value, XLTopBottomType topBottomType = XLTopBottomType.Items) + { + Values.Initialize(new XLFormula(value)); + Percent = topBottomType == XLTopBottomType.Percent; + ConditionalFormatType = XLConditionalFormatType.Top10; + Bottom = true; + return Style; + } + + public IXLCFColorScaleMin ColorScale() + { + ConditionalFormatType = XLConditionalFormatType.ColorScale; + return new XLCFColorScaleMin(this); + } + + public IXLCFDataBarMin DataBar(XLColor color, Boolean showBarOnly = false) + { + Colors.Initialize(color); + ShowBarOnly = showBarOnly; + ConditionalFormatType = XLConditionalFormatType.DataBar; + return new XLCFDataBarMin(this); + } + + public IXLCFDataBarMin DataBar(XLColor positiveColor, XLColor negativeColor, Boolean showBarOnly = false) + { + Colors.Initialize(positiveColor); + Colors.Add(negativeColor); + ShowBarOnly = showBarOnly; + ConditionalFormatType = XLConditionalFormatType.DataBar; + return new XLCFDataBarMin(this); + } + + public IXLCFIconSet IconSet(XLIconSetStyle iconSetStyle, Boolean reverseIconOrder = false, Boolean showIconOnly = false) + { + IconSetOperators.Clear(); + Values.Clear(); + ContentTypes.Clear(); + ConditionalFormatType = XLConditionalFormatType.IconSet; + IconSetStyle = iconSetStyle; + ReverseIconOrder = reverseIconOrder; + ShowIconOnly = showIconOnly; + return new XLCFIconSet(this); + } + } + + internal class DictionaryComparer : + IEqualityComparer> + { + private readonly IEqualityComparer _valueComparer; + + public DictionaryComparer(IEqualityComparer valueComparer = null) + { + this._valueComparer = valueComparer ?? EqualityComparer.Default; + } + + public bool Equals(Dictionary x, Dictionary y) + { + if (x.Count != y.Count) + return false; + if (x.Keys.Except(y.Keys).Any()) + return false; + if (y.Keys.Except(x.Keys).Any()) + return false; + foreach (var pair in x) + if (!_valueComparer.Equals(pair.Value, y[pair.Key])) + return false; + return true; + } + + public int GetHashCode(Dictionary obj) + { + throw new NotImplementedException(); + } + } + + internal class EnumerableComparer : IEqualityComparer> + { + private readonly IEqualityComparer _valueComparer; + + public EnumerableComparer(IEqualityComparer valueComparer = null) + { + this._valueComparer = valueComparer ?? EqualityComparer.Default; + } + + public bool Equals(IEnumerable x, IEnumerable y) + { + return SetEquals(x, y, _valueComparer); + } + + public int GetHashCode(IEnumerable obj) + { + throw new NotImplementedException(); + } + + public static bool SetEquals(IEnumerable first, IEnumerable second, + IEqualityComparer comparer) + { + return new HashSet(second, comparer ?? EqualityComparer.Default) + .SetEquals(first); + } + } +} diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs new file mode 100644 index 0000000..5a000f6 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLConditionalFormats : IXLConditionalFormats + { + private readonly List _conditionalFormats = new List(); + + private static readonly List _conditionalFormatTypesExcludedFromConsolidation = new List() + { + XLConditionalFormatType.DataBar, + XLConditionalFormatType.ColorScale, + XLConditionalFormatType.IconSet, + XLConditionalFormatType.Top10, + XLConditionalFormatType.AboveAverage, + XLConditionalFormatType.IsDuplicate, + XLConditionalFormatType.IsUnique + }; + + public void Add(IXLConditionalFormat conditionalFormat) + { + _conditionalFormats.Add(conditionalFormat); + } + + public IEnumerator GetEnumerator() + { + return _conditionalFormats.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Remove(Predicate predicate) + { + _conditionalFormats.RemoveAll(predicate); + } + + /// + /// The method consolidate the same conditional formats, which are located in adjacent ranges. + /// + internal void Consolidate() + { + var formats = _conditionalFormats + .Where(cf => cf.Ranges.Any()) + .ToList(); + _conditionalFormats.Clear(); + + while (formats.Count > 0) + { + var item = formats.First(); + + if (!_conditionalFormatTypesExcludedFromConsolidation.Contains(item.ConditionalFormatType)) + { + var rangesToJoin = new XLRanges(); + item.Ranges.ForEach(r => rangesToJoin.Add(r)); + var firstRange = item.Ranges.First(); + var skippedRanges = new XLRanges(); + Func IsSameFormat = f => + f != item && f.Ranges.First().Worksheet.Position == firstRange.Worksheet.Position && + XLConditionalFormat.NoRangeComparer.Equals(f, item); + + //Get the top left corner of the rectangle covering all the ranges + var baseAddress = new XLAddress( + item.Ranges.Select(r => r.RangeAddress.FirstAddress.RowNumber).Min(), + item.Ranges.Select(r => r.RangeAddress.FirstAddress.ColumnNumber).Min(), + false, false); + var baseCell = firstRange.Worksheet.Cell(baseAddress) as XLCell; + + int i = 1; + bool stop = false; + List similarFormats = new List(); + do + { + stop = (i >= formats.Count); + + if (!stop) + { + var nextFormat = formats[i]; + + var intersectsSkipped = + skippedRanges.Any(left => nextFormat.Ranges.GetIntersectedRanges(left.RangeAddress).Any()); + + var isSameFormat = IsSameFormat(nextFormat); + + if (isSameFormat && !intersectsSkipped) + { + similarFormats.Add(nextFormat); + nextFormat.Ranges.ForEach(r => rangesToJoin.Add(r)); + } + else if (rangesToJoin.Any(left => nextFormat.Ranges.GetIntersectedRanges(left.RangeAddress).Any()) || + intersectsSkipped) + { + // if we reached the rule intersecting any of captured ranges stop for not breaking the priorities + stop = true; + } + + if (!isSameFormat) + nextFormat.Ranges.ForEach(r => skippedRanges.Add(r)); + } + + i++; + } while (!stop); + + var consRanges = rangesToJoin.Consolidate(); + item.Ranges.RemoveAll(); + consRanges.ForEach(r => item.Ranges.Add(r)); + + var targetCell = item.Ranges.First().FirstCell() as XLCell; + (item as XLConditionalFormat).AdjustFormulas(baseCell, targetCell); + + similarFormats.ForEach(cf => formats.Remove(cf)); + } + + _conditionalFormats.Add(item); + formats.Remove(item); + } + } + + public void RemoveAll() + { + _conditionalFormats.Clear(); + } + + /// + /// Reorders the according to original priority. Done during load process + /// + public void ReorderAccordingToOriginalPriority() + { + var reorderedFormats = _conditionalFormats.OrderBy(cf => (cf as XLConditionalFormat).OriginalPriority).ToList(); + _conditionalFormats.Clear(); + _conditionalFormats.AddRange(reorderedFormats); + } + } +} diff --git a/ClosedXML/Excel/ContentManagers/XLBaseContentManager.cs b/ClosedXML/Excel/ContentManagers/XLBaseContentManager.cs new file mode 100644 index 0000000..6d031b5 --- /dev/null +++ b/ClosedXML/Excel/ContentManagers/XLBaseContentManager.cs @@ -0,0 +1,38 @@ +using DocumentFormat.OpenXml; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.ContentManagers +{ + internal abstract class XLBaseContentManager + { + + } + + internal abstract class XLBaseContentManager : XLBaseContentManager + where T : struct, IConvertible + + { + protected readonly IDictionary contents = new Dictionary(); + + public OpenXmlElement GetPreviousElementFor(T content) + { + var i = content.CastTo(); + + var previousElements = contents.Keys + .Where(key => key.CastTo() < i && contents[key] != null) + .OrderBy(key => key.CastTo()); + + if (previousElements.Any()) + return contents[previousElements.Last()]; + else + return null; + } + + public void SetElement(T content, OpenXmlElement element) + { + contents[content] = element; + } + } +} diff --git a/ClosedXML/Excel/ContentManagers/XLSheetViewContentManager.cs b/ClosedXML/Excel/ContentManagers/XLSheetViewContentManager.cs new file mode 100644 index 0000000..12c74ad --- /dev/null +++ b/ClosedXML/Excel/ContentManagers/XLSheetViewContentManager.cs @@ -0,0 +1,24 @@ +using DocumentFormat.OpenXml.Spreadsheet; +using System.Linq; + +namespace ClosedXML.Excel.ContentManagers +{ + internal enum XLSheetViewContents + { + Pane, + Selection, + PivotSelection, + ExtensionList + } + + internal class XLSheetViewContentManager : XLBaseContentManager + { + public XLSheetViewContentManager(SheetView sheetView) + { + contents.Add(XLSheetViewContents.Pane, sheetView.Elements().LastOrDefault()); + contents.Add(XLSheetViewContents.Selection, sheetView.Elements().LastOrDefault()); + contents.Add(XLSheetViewContents.PivotSelection, sheetView.Elements().LastOrDefault()); + contents.Add(XLSheetViewContents.ExtensionList, sheetView.Elements().LastOrDefault()); + } + } +} diff --git a/ClosedXML/Excel/ContentManagers/XLWorksheetContentManager.cs b/ClosedXML/Excel/ContentManagers/XLWorksheetContentManager.cs new file mode 100644 index 0000000..d3f90c0 --- /dev/null +++ b/ClosedXML/Excel/ContentManagers/XLWorksheetContentManager.cs @@ -0,0 +1,97 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Spreadsheet; +using System.Linq; + +namespace ClosedXML.Excel.ContentManagers +{ + internal enum XLWorksheetContents + { + SheetProperties = 1, + SheetDimension = 2, + SheetViews = 3, + SheetFormatProperties = 4, + Columns = 5, + SheetData = 6, + SheetCalculationProperties = 7, + SheetProtection = 8, + ProtectedRanges = 9, + Scenarios = 10, + AutoFilter = 11, + SortState = 12, + DataConsolidate = 13, + CustomSheetViews = 14, + MergeCells = 15, + PhoneticProperties = 16, + ConditionalFormatting = 17, + DataValidations = 18, + Hyperlinks = 19, + PrintOptions = 20, + PageMargins = 21, + PageSetup = 22, + HeaderFooter = 23, + RowBreaks = 24, + ColumnBreaks = 25, + CustomProperties = 26, + CellWatches = 27, + IgnoredErrors = 28, + SmartTags = 29, + Drawing = 30, + LegacyDrawing = 31, + LegacyDrawingHeaderFooter = 32, + DrawingHeaderFooter = 33, + Picture = 34, + OleObjects = 35, + Controls = 36, + AlternateContent = 37, + WebPublishItems = 38, + TableParts = 39, + WorksheetExtensionList = 40 + } + + internal class XLWorksheetContentManager : XLBaseContentManager + { + public XLWorksheetContentManager(Worksheet opWorksheet) + { + contents.Add(XLWorksheetContents.SheetProperties, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetDimension, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetViews, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetFormatProperties, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Columns, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetData, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetCalculationProperties, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SheetProtection, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.ProtectedRanges, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Scenarios, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.AutoFilter, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.SortState, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.DataConsolidate, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.CustomSheetViews, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.MergeCells, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.PhoneticProperties, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.ConditionalFormatting, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.DataValidations, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Hyperlinks, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.PrintOptions, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.PageMargins, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.PageSetup, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.HeaderFooter, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.RowBreaks, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.ColumnBreaks, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.CustomProperties, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.CellWatches, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.IgnoredErrors, opWorksheet.Elements().LastOrDefault()); + //contents.Add(XLWSContents.SmartTags, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Drawing, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.LegacyDrawing, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.LegacyDrawingHeaderFooter, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.DrawingHeaderFooter, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Picture, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.OleObjects, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.Controls, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.AlternateContent, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.WebPublishItems, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.TableParts, opWorksheet.Elements().LastOrDefault()); + contents.Add(XLWorksheetContents.WorksheetExtensionList, opWorksheet.Elements().LastOrDefault()); + } + } +} diff --git a/ClosedXML/Excel/Coordinates/IXLAddress.cs b/ClosedXML/Excel/Coordinates/IXLAddress.cs new file mode 100644 index 0000000..6158251 --- /dev/null +++ b/ClosedXML/Excel/Coordinates/IXLAddress.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLAddress : IEqualityComparer, IEquatable + { + String ColumnLetter { get; } + Int32 ColumnNumber { get; } + Boolean FixedColumn { get; } + Boolean FixedRow { get; } + Int32 RowNumber { get; } + String UniqueId { get; } + IXLWorksheet Worksheet { get; } + + String ToString(XLReferenceStyle referenceStyle); + + String ToString(XLReferenceStyle referenceStyle, Boolean includeSheet); + + String ToStringFixed(); + + String ToStringFixed(XLReferenceStyle referenceStyle); + + String ToStringFixed(XLReferenceStyle referenceStyle, Boolean includeSheet); + + String ToStringRelative(); + + + String ToStringRelative(Boolean includeSheet); + } +} diff --git a/ClosedXML/Excel/Coordinates/XLAddress.cs b/ClosedXML/Excel/Coordinates/XLAddress.cs new file mode 100644 index 0000000..11d34d1 --- /dev/null +++ b/ClosedXML/Excel/Coordinates/XLAddress.cs @@ -0,0 +1,465 @@ +using ClosedXML.Extensions; +using System; +using System.Diagnostics; + +namespace ClosedXML.Excel +{ + internal struct XLAddress : IXLAddress, IEquatable + { + #region Static + /// + /// Create address without worksheet. For calculation only! + /// + /// + /// + public static XLAddress Create(string cellAddressString) + { + return Create(null, cellAddressString); + } + + public static XLAddress Create(XLWorksheet worksheet, string cellAddressString) + { + var fixedColumn = cellAddressString[0] == '$'; + Int32 startPos; + if (fixedColumn) + { + startPos = 1; + } + else + { + startPos = 0; + } + + int rowPos = startPos; + while (cellAddressString[rowPos] > '9') + { + rowPos++; + } + + var fixedRow = cellAddressString[rowPos] == '$'; + string columnLetter; + int rowNumber; + if (fixedRow) + { + if (fixedColumn) + { + columnLetter = cellAddressString.Substring(startPos, rowPos - 1); + } + else + { + columnLetter = cellAddressString.Substring(startPos, rowPos); + } + + rowNumber = int.Parse(cellAddressString.Substring(rowPos + 1), XLHelper.NumberStyle, XLHelper.ParseCulture); + } + else + { + if (fixedColumn) + { + columnLetter = cellAddressString.Substring(startPos, rowPos - 1); + } + else + { + columnLetter = cellAddressString.Substring(startPos, rowPos); + } + + rowNumber = Int32.Parse(cellAddressString.Substring(rowPos), XLHelper.NumberStyle, XLHelper.ParseCulture); + } + return new XLAddress(worksheet, rowNumber, columnLetter, fixedRow, fixedColumn); + } + + #endregion Static + + #region Private fields + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool _fixedRow; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool _fixedColumn; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly int _rowNumber; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly int _columnNumber; + + private string _trimmedAddress; + + #endregion Private fields + + #region Constructors + + /// + /// Initializes a new struct using a mixed notation. Attention: without worksheet for calculation only! + /// + /// The row number of the cell address. + /// The column letter of the cell address. + /// + /// + public XLAddress(int rowNumber, string columnLetter, bool fixedRow, bool fixedColumn) + : this(null, rowNumber, columnLetter, fixedRow, fixedColumn) + { + } + + /// + /// Initializes a new struct using a mixed notation. + /// + /// + /// The row number of the cell address. + /// The column letter of the cell address. + /// + /// + public XLAddress(XLWorksheet worksheet, int rowNumber, string columnLetter, bool fixedRow, bool fixedColumn) + : this(worksheet, rowNumber, XLHelper.GetColumnNumberFromLetter(columnLetter), fixedRow, fixedColumn) + { + } + + /// + /// Initializes a new struct using R1C1 notation. Attention: without worksheet for calculation only! + /// + /// The row number of the cell address. + /// The column number of the cell address. + /// + /// + public XLAddress(int rowNumber, int columnNumber, bool fixedRow, bool fixedColumn) + : this(null, rowNumber, columnNumber, fixedRow, fixedColumn) + { + } + + /// + /// Initializes a new struct using R1C1 notation. + /// + /// + /// The row number of the cell address. + /// The column number of the cell address. + /// + /// + public XLAddress(XLWorksheet worksheet, int rowNumber, int columnNumber, bool fixedRow, bool fixedColumn) : this() + + { + Worksheet = worksheet; + + _rowNumber = rowNumber; + _columnNumber = columnNumber; + _fixedColumn = fixedColumn; + _fixedRow = fixedRow; + } + + #endregion Constructors + + #region Properties + + public XLWorksheet Worksheet { get; internal set; } + + IXLWorksheet IXLAddress.Worksheet + { + [DebuggerStepThrough] + get { return Worksheet; } + } + + public bool HasWorksheet + { + [DebuggerStepThrough] + get { return Worksheet != null; } + } + + public bool FixedRow + { + get { return _fixedRow; } + } + + public bool FixedColumn + { + get { return _fixedColumn; } + } + + /// + /// Gets the row number of this address. + /// + public Int32 RowNumber + { + get { return _rowNumber; } + } + + /// + /// Gets the column number of this address. + /// + public Int32 ColumnNumber + { + get { return _columnNumber; } + } + + /// + /// Gets the column letter(s) of this address. + /// + public String ColumnLetter + { + get { return XLHelper.GetColumnLetterFromNumber(_columnNumber); } + } + + #endregion Properties + + #region Overrides + + public override string ToString() + { + if (!IsValid) + return "#REF!"; + + String retVal = ColumnLetter; + if (_fixedColumn) + { + retVal = "$" + retVal; + } + if (_fixedRow) + { + retVal += "$"; + } + retVal += _rowNumber.ToInvariantString(); + return retVal; + } + + public string ToString(XLReferenceStyle referenceStyle) + { + return ToString(referenceStyle, false); + } + + public string ToString(XLReferenceStyle referenceStyle, bool includeSheet) + { + string address; + if (!IsValid) + address = "#REF!"; + else if (referenceStyle == XLReferenceStyle.A1) + address = GetTrimmedAddress(); + else if (referenceStyle == XLReferenceStyle.R1C1 + || HasWorksheet && Worksheet.Workbook.ReferenceStyle == XLReferenceStyle.R1C1) + address = "R" + _rowNumber.ToInvariantString() + "C" + ColumnNumber.ToInvariantString(); + else + address = GetTrimmedAddress(); + + if (includeSheet) + return String.Concat( + WorksheetIsDeleted ? "#REF" : Worksheet.Name.EscapeSheetName(), + '!', + address); + + return address; + } + + #endregion Overrides + + #region Methods + + public string GetTrimmedAddress() + { + return _trimmedAddress ?? (_trimmedAddress = ColumnLetter + _rowNumber.ToInvariantString()); + } + + #endregion Methods + + #region Operator Overloads + + public static XLAddress operator +(XLAddress left, XLAddress right) + { + return new XLAddress(left.Worksheet, + left.RowNumber + right.RowNumber, + left.ColumnNumber + right.ColumnNumber, + left._fixedRow, + left._fixedColumn); + } + + public static XLAddress operator -(XLAddress left, XLAddress right) + { + return new XLAddress(left.Worksheet, + left.RowNumber - right.RowNumber, + left.ColumnNumber - right.ColumnNumber, + left._fixedRow, + left._fixedColumn); + } + + public static XLAddress operator +(XLAddress left, Int32 right) + { + return new XLAddress(left.Worksheet, + left.RowNumber + right, + left.ColumnNumber + right, + left._fixedRow, + left._fixedColumn); + } + + public static XLAddress operator -(XLAddress left, Int32 right) + { + return new XLAddress(left.Worksheet, + left.RowNumber - right, + left.ColumnNumber - right, + left._fixedRow, + left._fixedColumn); + } + + public static Boolean operator ==(XLAddress left, XLAddress right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + return !ReferenceEquals(left, null) && left.Equals(right); + } + + public static Boolean operator !=(XLAddress left, XLAddress right) + { + return !(left == right); + } + + #endregion Operator Overloads + + #region Interface Requirements + + #region IEqualityComparer Members + + public Boolean Equals(IXLAddress x, IXLAddress y) + { + return x == y; + } + + public new Boolean Equals(object x, object y) + { + return x == y; + } + + #endregion IEqualityComparer Members + + #region IEquatable Members + + public bool Equals(IXLAddress other) + { + if (other == null) + return false; + + return _rowNumber == other.RowNumber && + _columnNumber == other.ColumnNumber && + _fixedRow == other.FixedRow && + _fixedColumn == other.FixedColumn; + } + + public bool Equals(XLAddress other) + { + return _rowNumber == other._rowNumber && + _columnNumber == other._columnNumber && + _fixedRow == other._fixedRow && + _fixedColumn == other._fixedColumn; + } + + public override Boolean Equals(Object other) + { + return Equals(other as IXLAddress); + } + + public override int GetHashCode() + { + var hashCode = 2122234362; + hashCode = hashCode * -1521134295 + _fixedRow.GetHashCode(); + hashCode = hashCode * -1521134295 + _fixedColumn.GetHashCode(); + hashCode = hashCode * -1521134295 + _rowNumber.GetHashCode(); + hashCode = hashCode * -1521134295 + _columnNumber.GetHashCode(); + return hashCode; + } + + public int GetHashCode(IXLAddress obj) + { + return ((XLAddress)obj).GetHashCode(); + } + + #endregion IEquatable Members + + #endregion Interface Requirements + + public String ToStringRelative() + { + return ToStringRelative(false); + } + + public String ToStringFixed() + { + return ToStringFixed(XLReferenceStyle.Default); + } + + public String ToStringRelative(Boolean includeSheet) + { + var address = IsValid ? GetTrimmedAddress() : "#REF!"; + + if (includeSheet) + return String.Concat( + WorksheetIsDeleted ? "#REF" : Worksheet.Name.EscapeSheetName(), + '!', + address + ); + + return address; + } + + internal XLAddress WithoutWorksheet() + { + return new XLAddress(RowNumber, ColumnNumber, FixedRow, FixedColumn); + } + + public String ToStringFixed(XLReferenceStyle referenceStyle) + { + return ToStringFixed(referenceStyle, false); + } + + public String ToStringFixed(XLReferenceStyle referenceStyle, Boolean includeSheet) + { + String address; + + if (referenceStyle == XLReferenceStyle.Default && HasWorksheet) + referenceStyle = Worksheet.Workbook.ReferenceStyle; + + if (referenceStyle == XLReferenceStyle.Default) + referenceStyle = XLReferenceStyle.A1; + + Debug.Assert(referenceStyle != XLReferenceStyle.Default); + + if (!IsValid) + { + address = "#REF!"; + } + else + { + switch (referenceStyle) + { + case XLReferenceStyle.A1: + address = String.Concat('$', ColumnLetter, '$', _rowNumber.ToInvariantString()); + break; + + case XLReferenceStyle.R1C1: + address = String.Concat('R', _rowNumber.ToInvariantString(), 'C', ColumnNumber); + break; + + default: + throw new NotImplementedException(); + } + } + + if (includeSheet) + return String.Concat( + WorksheetIsDeleted ? "#REF" : Worksheet.Name.EscapeSheetName(), + '!', + address); + + return address; + } + + public String UniqueId { get { return RowNumber.ToString("0000000") + ColumnNumber.ToString("00000"); } } + + public bool IsValid + { + get + { + return 0 < RowNumber && RowNumber <= XLHelper.MaxRowNumber && + 0 < ColumnNumber && ColumnNumber <= XLHelper.MaxColumnNumber; + } + } + + private bool WorksheetIsDeleted => Worksheet?.IsDeleted == true; + } +} diff --git a/ClosedXML/Excel/Coordinates/XLSheetPoint.cs b/ClosedXML/Excel/Coordinates/XLSheetPoint.cs new file mode 100644 index 0000000..f1c3cea --- /dev/null +++ b/ClosedXML/Excel/Coordinates/XLSheetPoint.cs @@ -0,0 +1,41 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLSheetPoint:IEquatable + { + public XLSheetPoint(Int32 row, Int32 column) + { + Row = row; + Column = column; + } + + public readonly Int32 Row; + public readonly Int32 Column; + + public override bool Equals(object obj) + { + return Equals((XLSheetPoint)obj); + } + + public bool Equals(XLSheetPoint other) + { + return Row == other.Row && Column == other.Column; + } + + public override int GetHashCode() + { + return (Row * -1) ^ Column; + } + + public static bool operator==(XLSheetPoint a, XLSheetPoint b) + { + return a.Row == b.Row && a.Column == b.Column; + } + + public static bool operator !=(XLSheetPoint a, XLSheetPoint b) + { + return a.Row != b.Row || a.Column != b.Column; + } + } +} diff --git a/ClosedXML/Excel/Coordinates/XLSheetRange.cs b/ClosedXML/Excel/Coordinates/XLSheetRange.cs new file mode 100644 index 0000000..9fb23a2 --- /dev/null +++ b/ClosedXML/Excel/Coordinates/XLSheetRange.cs @@ -0,0 +1,26 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLSheetRange:IEquatable + { + public XLSheetRange(XLSheetPoint firstPoint, XLSheetPoint lastPoint) + { + FirstPoint = firstPoint; + LastPoint = lastPoint; + } + + public readonly XLSheetPoint FirstPoint; + public readonly XLSheetPoint LastPoint; + + public bool Equals(XLSheetRange other) + { + return FirstPoint.Equals(other.FirstPoint) && LastPoint.Equals(other.LastPoint); + } + + public override int GetHashCode() + { + return FirstPoint.GetHashCode() ^ LastPoint.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/CustomProperties/IXLCustomProperties.cs b/ClosedXML/Excel/CustomProperties/IXLCustomProperties.cs new file mode 100644 index 0000000..83e906a --- /dev/null +++ b/ClosedXML/Excel/CustomProperties/IXLCustomProperties.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLCustomProperties: IEnumerable + { + void Add(IXLCustomProperty customProperty); + void Add(String name, T value); + void Delete(String name); + IXLCustomProperty CustomProperty(String name); + + } +} diff --git a/ClosedXML/Excel/CustomProperties/IXLCustomProperty.cs b/ClosedXML/Excel/CustomProperties/IXLCustomProperty.cs new file mode 100644 index 0000000..cfed06e --- /dev/null +++ b/ClosedXML/Excel/CustomProperties/IXLCustomProperty.cs @@ -0,0 +1,13 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLCustomPropertyType { Text, Number, Date, Boolean} + public interface IXLCustomProperty + { + String Name { get; set; } + XLCustomPropertyType Type { get; } + Object Value { get; set; } + T GetValue(); + } +} diff --git a/ClosedXML/Excel/CustomProperties/XLCustomProperties.cs b/ClosedXML/Excel/CustomProperties/XLCustomProperties.cs new file mode 100644 index 0000000..0322069 --- /dev/null +++ b/ClosedXML/Excel/CustomProperties/XLCustomProperties.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLCustomProperties: IXLCustomProperties, IEnumerable + { + XLWorkbook workbook; + public XLCustomProperties(XLWorkbook workbook) + { + this.workbook = workbook; + } + + private Dictionary customProperties = new Dictionary(); + public void Add(IXLCustomProperty customProperty) + { + customProperties.Add(customProperty.Name, customProperty); + } + public void Add(String name, T value) + { + var cp = new XLCustomProperty(workbook) { Name = name, Value = value }; + Add(cp); + } + + public void Delete(String name) + { + customProperties.Remove(name); + } + public IXLCustomProperty CustomProperty(String name) + { + return customProperties[name]; + } + + public IEnumerator GetEnumerator() + { + return customProperties.Values.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + + } +} diff --git a/ClosedXML/Excel/CustomProperties/XLCustomProperty.cs b/ClosedXML/Excel/CustomProperties/XLCustomProperty.cs new file mode 100644 index 0000000..a1d4d19 --- /dev/null +++ b/ClosedXML/Excel/CustomProperties/XLCustomProperty.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLCustomProperty : IXLCustomProperty + { + private readonly XLWorkbook _workbook; + + private String name; + + public XLCustomProperty(XLWorkbook workbook) + { + _workbook = workbook; + } + + #region IXLCustomProperty Members + + public String Name + { + get { return name; } + set + { + if (name == value) return; + + if (_workbook.CustomProperties.Any(t => t.Name == value)) + throw new ArgumentException( + String.Format("This workbook already contains a custom property named '{0}'", value)); + + name = value; + } + } + + public XLCustomPropertyType Type + { + get + { + if (Value is DateTime) + return XLCustomPropertyType.Date; + + if (Value is Boolean) + return XLCustomPropertyType.Boolean; + + if (Double.TryParse(Value.ToString(), out Double dTest)) + return XLCustomPropertyType.Number; + + return XLCustomPropertyType.Text; + } + } + + public Object Value { get; set; } + + public T GetValue() + { + return (T)Convert.ChangeType(Value, typeof(T)); + } + + #endregion + } +} diff --git a/ClosedXML/Excel/DataValidation/IXLDataValidation.cs b/ClosedXML/Excel/DataValidation/IXLDataValidation.cs new file mode 100644 index 0000000..895bfa1 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/IXLDataValidation.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLErrorStyle { Stop, Warning, Information } + public enum XLAllowedValues { AnyValue, WholeNumber, Decimal, Date, Time, TextLength, List, Custom } + public enum XLOperator { EqualTo, NotEqualTo, GreaterThan, LessThan, EqualOrGreaterThan, EqualOrLessThan, Between, NotBetween } + public interface IXLDataValidation + { + /// + /// A collection of ranges the data validation rule applies too. + /// + IEnumerable Ranges { get; } + + /// + /// Add a range to the collection of ranges this rule applies to. + /// If the specified range does not belong to the worksheet of the data validation + /// rule it is transferred to the target worksheet. + /// + /// A range to add. + void AddRange(IXLRange range); + + /// + /// Add a collection of ranges to the collection of ranges this rule applies to. + /// Ranges that do not belong to the worksheet of the data validation + /// rule are transferred to the target worksheet. + /// + /// Ranges to add. + void AddRanges(IEnumerable ranges); + + /// + /// Detach data validation rule of all ranges it applies to. + /// + void ClearRanges(); + + /// + /// Remove the specified range from the collection of range this rule applies to. + /// + /// A range to remove. + bool RemoveRange(IXLRange range); + + //void Delete(); + //void CopyFrom(IXLDataValidation dataValidation); + Boolean ShowInputMessage { get; set; } + Boolean ShowErrorMessage { get; set; } + Boolean IgnoreBlanks { get; set; } + Boolean InCellDropdown { get; set; } + String InputTitle { get; set; } + String InputMessage { get; set; } + String ErrorTitle { get; set; } + String ErrorMessage { get; set; } + XLErrorStyle ErrorStyle { get; set; } + XLAllowedValues AllowedValues { get; set; } + XLOperator Operator { get; set; } + + String Value { get; set; } + String MinValue { get; set; } + String MaxValue { get; set; } + + XLWholeNumberCriteria WholeNumber { get; } + XLDecimalCriteria Decimal { get; } + XLDateCriteria Date { get; } + XLTimeCriteria Time { get; } + XLTextLengthCriteria TextLength { get; } + + void List(String list); + void List(String list, Boolean inCellDropdown); + void List(IXLRange range); + void List(IXLRange range, Boolean inCellDropdown); + + void Custom(String customValidation); + void Clear(); + Boolean IsDirty(); + } +} diff --git a/ClosedXML/Excel/DataValidation/IXLDataValidations.cs b/ClosedXML/Excel/DataValidation/IXLDataValidations.cs new file mode 100644 index 0000000..76b2e48 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/IXLDataValidations.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLDataValidations : IEnumerable + { + IXLWorksheet Worksheet { get; } + + /// + /// Add data validation rule to the collection. If the specified rule refers to another + /// worksheet than the collection, the copy will be created and its ranges will refer + /// to the worksheet of the collection. Otherwise the original instance will be placed + /// in the collection. + /// + /// A data validation rule to add. + /// The instance that has actually been added in the collection + /// (may be a copy of the specified one). + IXLDataValidation Add(IXLDataValidation dataValidation); + + Boolean ContainsSingle(IXLRange range); + + void Delete(Predicate predicate); + + /// + /// Get the data validation rule for the range with the specified address if it exists. + /// + /// A range address. + /// Data validation rule which ranges collection includes the specified + /// address. The specified range should be fully covered with the data validation rule. + /// For example, if the rule is applied to ranges A1:A3,C1:C3 then this method will + /// return True for ranges A1:A3, C1:C2, A2:A3, and False for ranges A1:C3, A1:C1, etc. + /// True is the data validation rule was found, false otherwise. + bool TryGet(IXLRangeAddress rangeAddress, out IXLDataValidation dataValidation); + + /// + /// Get all data validation rules applied to ranges that intersect the specified range. + /// + IEnumerable GetAllInRange(IXLRangeAddress rangeAddress); + } +} diff --git a/ClosedXML/Excel/DataValidation/IXLValidationCriteria.cs b/ClosedXML/Excel/DataValidation/IXLValidationCriteria.cs new file mode 100644 index 0000000..9463193 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/IXLValidationCriteria.cs @@ -0,0 +1,34 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLValidationCriteria + { + void EqualTo(String value); + void NotEqualTo(String value); + void GreaterThan(String value); + void LessThan(String value); + void EqualOrGreaterThan(String value); + void EqualOrLessThan(String value); + void Between(String minValue, String maxValue); + void NotBetween(String minValue, String maxValue); + + void EqualTo(IXLRange range); + void NotEqualTo(IXLRange range); + void GreaterThan(IXLRange range); + void LessThan(IXLRange range); + void EqualOrGreaterThan(IXLRange range); + void EqualOrLessThan(IXLRange range); + void Between(IXLRange minValue, IXLRange maxValue); + void NotBetween(IXLRange minValue, IXLRange maxValue); + + void EqualTo(IXLCell cell); + void NotEqualTo(IXLCell cell); + void GreaterThan(IXLCell cell); + void LessThan(IXLCell cell); + void EqualOrGreaterThan(IXLCell cell); + void EqualOrLessThan(IXLCell cell); + void Between(IXLCell minValue, IXLCell maxValue); + void NotBetween(IXLCell minValue, IXLCell maxValue); + } +} diff --git a/ClosedXML/Excel/DataValidation/XLDataValidation.cs b/ClosedXML/Excel/DataValidation/XLDataValidation.cs new file mode 100644 index 0000000..82b22ae --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLDataValidation.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLDataValidation : IXLDataValidation + { + private readonly XLWorksheet _worksheet; + private readonly XLRanges _ranges; + + internal XLWorksheet Worksheet => _worksheet; + + + private XLDataValidation(XLWorksheet worksheet) + { + _worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + _ranges = new XLRanges(); + Initialize(); + } + + public XLDataValidation(IXLRange range) + : this(range?.Worksheet as XLWorksheet) + { + if (range == null) throw new ArgumentNullException(nameof(range)); + + AddRange(range); + } + + public XLDataValidation(IXLDataValidation dataValidation, XLWorksheet worksheet) + : this(worksheet) + { + _worksheet = worksheet; + CopyFrom(dataValidation); + } + + private void Initialize() + { + AllowedValues = XLAllowedValues.AnyValue; + IgnoreBlanks = true; + ShowErrorMessage = true; + ShowInputMessage = true; + InCellDropdown = true; + InputTitle = String.Empty; + InputMessage = String.Empty; + ErrorTitle = String.Empty; + ErrorMessage = String.Empty; + ErrorStyle = XLErrorStyle.Stop; + Operator = XLOperator.Between; + Value = String.Empty; + MinValue = String.Empty; + MaxValue = String.Empty; + } + + public Boolean IsDirty() + { + return + AllowedValues != XLAllowedValues.AnyValue + || (ShowInputMessage && + (!String.IsNullOrWhiteSpace(InputTitle) || !String.IsNullOrWhiteSpace(InputMessage))) + || (ShowErrorMessage && + (!String.IsNullOrWhiteSpace(ErrorTitle) || !String.IsNullOrWhiteSpace(ErrorMessage))); + } + + #region IXLDataValidation Members + + public IEnumerable Ranges => _ranges.AsEnumerable(); + + /// + /// Add a range to the collection of ranges this rule applies to. + /// If the specified range does not belong to the worksheet of the data validation + /// rule it is transferred to the target worksheet. + /// + /// A range to add. + public void AddRange(IXLRange range) + { + if (range == null) throw new ArgumentNullException(nameof(range)); + + if (range.Worksheet != Worksheet) + range = Worksheet.Range(((XLRangeAddress) range.RangeAddress).WithoutWorksheet()); + + _ranges.Add(range); + + RangeAdded?.Invoke(this, new RangeEventArgs(range)); + } + + /// + /// Add a collection of ranges to the collection of ranges this rule applies to. + /// Ranges that do not belong to the worksheet of the data validation + /// rule are transferred to the target worksheet. + /// + /// Ranges to add. + public void AddRanges(IEnumerable ranges) + { + ranges = ranges ?? Enumerable.Empty(); + + foreach (var range in ranges) + { + AddRange(range); + } + } + + /// + /// Detach data validation rule of all ranges it applies to. + /// + public void ClearRanges() + { + var allRanges = _ranges.ToList(); + _ranges.RemoveAll(); + + foreach (var range in allRanges) + { + RangeRemoved?.Invoke(this, new RangeEventArgs(range)); + } + } + + /// + /// Remove the specified range from the collection of range this rule applies to. + /// + /// A range to remove. + public bool RemoveRange(IXLRange range) + { + if (range == null) + return false; + + var res = _ranges.Remove(range); + + if (res) + { + RangeRemoved?.Invoke(this, new RangeEventArgs(range)); + } + + return res; + } + + public Boolean IgnoreBlanks { get; set; } + public Boolean InCellDropdown { get; set; } + public Boolean ShowInputMessage { get; set; } + public String InputTitle { get; set; } + public String InputMessage { get; set; } + public Boolean ShowErrorMessage { get; set; } + public String ErrorTitle { get; set; } + public String ErrorMessage { get; set; } + public XLErrorStyle ErrorStyle { get; set; } + public XLAllowedValues AllowedValues { get; set; } + public XLOperator Operator { get; set; } + + public String Value + { + get { return MinValue; } + set { MinValue = value; } + } + + private String minValue; + public String MinValue { get => minValue; set { Validate(value); minValue = value; } } + + private String maxValue; + public String MaxValue { get => maxValue; set { Validate(value); maxValue = value; } } + + public XLWholeNumberCriteria WholeNumber + { + get + { + AllowedValues = XLAllowedValues.WholeNumber; + return new XLWholeNumberCriteria(this); + } + } + + public XLDecimalCriteria Decimal + { + get + { + AllowedValues = XLAllowedValues.Decimal; + return new XLDecimalCriteria(this); + } + } + + public XLDateCriteria Date + { + get + { + AllowedValues = XLAllowedValues.Date; + return new XLDateCriteria(this); + } + } + + public XLTimeCriteria Time + { + get + { + AllowedValues = XLAllowedValues.Time; + return new XLTimeCriteria(this); + } + } + + public XLTextLengthCriteria TextLength + { + get + { + AllowedValues = XLAllowedValues.TextLength; + return new XLTextLengthCriteria(this); + } + } + + public void List(String list) + { + List(list, true); + } + + public void List(String list, Boolean inCellDropdown) + { + AllowedValues = XLAllowedValues.List; + InCellDropdown = inCellDropdown; + Value = list; + } + + public void List(IXLRange range) + { + List(range, true); + } + + public void List(IXLRange range, Boolean inCellDropdown) + { + List(range.RangeAddress.ToStringFixed(XLReferenceStyle.A1, true)); + } + + public void Custom(String customValidation) + { + AllowedValues = XLAllowedValues.Custom; + Value = customValidation; + } + + #endregion IXLDataValidation Members + + public void CopyFrom(IXLDataValidation dataValidation) + { + if (dataValidation == this) return; + + if (!_ranges.Any()) + AddRanges(dataValidation.Ranges); + + IgnoreBlanks = dataValidation.IgnoreBlanks; + InCellDropdown = dataValidation.InCellDropdown; + ShowErrorMessage = dataValidation.ShowErrorMessage; + ShowInputMessage = dataValidation.ShowInputMessage; + InputTitle = dataValidation.InputTitle; + InputMessage = dataValidation.InputMessage; + ErrorTitle = dataValidation.ErrorTitle; + ErrorMessage = dataValidation.ErrorMessage; + ErrorStyle = dataValidation.ErrorStyle; + AllowedValues = dataValidation.AllowedValues; + Operator = dataValidation.Operator; + MinValue = dataValidation.MinValue; + MaxValue = dataValidation.MaxValue; + } + + public void Clear() + { + Initialize(); + } + + private void Validate(String value) + { + if (value.Length > 255) + throw new ArgumentOutOfRangeException(nameof(value), "The maximum allowed length of the value is 255 characters."); + } + + internal event EventHandler RangeAdded; + + internal event EventHandler RangeRemoved; + + internal void SplitBy(IXLRangeAddress rangeAddress) + { + var rangesToSplit = _ranges.GetIntersectedRanges(rangeAddress).ToList(); + + foreach (var rangeToSplit in rangesToSplit) + { + var newRanges = (rangeToSplit as XLRange).Split(rangeAddress, includeIntersection: false); + RemoveRange(rangeToSplit); + newRanges.ForEach(AddRange); + } + } + } + + internal class RangeEventArgs : EventArgs + { + public RangeEventArgs(IXLRange range) + { + Range = range ?? throw new ArgumentNullException(nameof(range)); + } + + public IXLRange Range { get; } + } +} diff --git a/ClosedXML/Excel/DataValidation/XLDataValidations.cs b/ClosedXML/Excel/DataValidation/XLDataValidations.cs new file mode 100644 index 0000000..22367c0 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLDataValidations.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using ClosedXML.Excel.Ranges.Index; + +namespace ClosedXML.Excel +{ + using System.Collections; + using System.Linq; + + internal class XLDataValidations : IXLDataValidations + { + private readonly XLRangeIndex _dataValidationIndex; + + internal XLWorksheet Worksheet => _worksheet; + + private readonly XLWorksheet _worksheet; + private readonly List _dataValidations = new List(); + + public XLDataValidations(XLWorksheet worksheet) + { + _worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + _dataValidationIndex = new XLRangeIndex(_worksheet); + } + + #region IXLDataValidations Members + + IXLWorksheet IXLDataValidations.Worksheet => _worksheet; + + public IXLDataValidation Add(IXLDataValidation dataValidation) + { + return Add(dataValidation, skipIntersectionsCheck: false); + } + + internal IXLDataValidation Add(IXLDataValidation dataValidation, bool skipIntersectionsCheck) + { + if (dataValidation == null) throw new ArgumentNullException(nameof(dataValidation)); + + XLDataValidation xlDataValidation; + if (!(dataValidation is XLDataValidation) || + dataValidation.Ranges.Any(r => r.Worksheet != Worksheet)) + { + xlDataValidation = new XLDataValidation(dataValidation, Worksheet); + } + else + { + xlDataValidation = (XLDataValidation)dataValidation; + } + + xlDataValidation.RangeAdded += OnRangeAdded; + xlDataValidation.RangeRemoved += OnRangeRemoved; + + foreach (var range in xlDataValidation.Ranges) + { + ProcessRangeAdded(range, xlDataValidation, skipIntersectionsCheck); + } + + _dataValidations.Add(xlDataValidation); + + return xlDataValidation; + } + + public void Delete(Predicate predicate) + { + var dataValidationsToRemove = _dataValidations.Where(dv => predicate(dv)) + .ToList(); + + dataValidationsToRemove.ForEach(Delete); + } + + /// + /// Get the data validation rule for the range with the specified address if it exists. + /// + /// A range address. + /// Data validation rule which ranges collection includes the specified + /// address. The specified range should be fully covered with the data validation rule. + /// For example, if the rule is applied to ranges A1:A3,C1:C3 then this method will + /// return True for ranges A1:A3, C1:C2, A2:A3, and False for ranges A1:C3, A1:C1, etc. + /// True is the data validation rule was found, false otherwise. + public bool TryGet(IXLRangeAddress rangeAddress, out IXLDataValidation dataValidation) + { + dataValidation = null; + if (rangeAddress == null || !rangeAddress.IsValid) + return false; + + var candidates = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .Where(c => c.RangeAddress.Contains(rangeAddress.FirstAddress) && + c.RangeAddress.Contains(rangeAddress.LastAddress)); + + if (!candidates.Any()) + return false; + + dataValidation = candidates.First().DataValidation; + + return true; + } + + /// + /// Get all data validation rules applied to ranges that intersect the specified range. + /// + public IEnumerable GetAllInRange(IXLRangeAddress rangeAddress) + { + if (rangeAddress == null || !rangeAddress.IsValid) + return Enumerable.Empty(); + + return _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .Select(indexEntry => indexEntry.DataValidation) + .Distinct(); + } + + + public IEnumerator GetEnumerator() + { + return _dataValidations.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Boolean ContainsSingle(IXLRange range) + { + Int32 count = 0; + foreach (var xlDataValidation in _dataValidations.Where(dv => dv.Ranges.Contains(range))) + { + count++; + if (count > 1) return false; + } + + return count == 1; + } + + public void Delete(IXLDataValidation dataValidation) + { + if (!_dataValidations.Remove(dataValidation)) + return; + var xlDataValidation = dataValidation as XLDataValidation; + xlDataValidation.RangeAdded -= OnRangeAdded; + xlDataValidation.RangeRemoved -= OnRangeRemoved; + + foreach (var range in dataValidation.Ranges) + { + ProcessRangeRemoved(range); + } + } + + public void Delete(IXLRange range) + { + if (range == null) throw new ArgumentNullException(nameof(range)); + + var dataValidationsToRemove = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) range.RangeAddress) + .Select(e => e.DataValidation) + .Distinct() + .ToList(); + + dataValidationsToRemove.ForEach(Delete); + } + + #endregion IXLDataValidations Members + + public void Consolidate() + { + Func areEqual = (dv1, dv2) => + { + return + dv1.IgnoreBlanks == dv2.IgnoreBlanks && + dv1.InCellDropdown == dv2.InCellDropdown && + dv1.ShowErrorMessage == dv2.ShowErrorMessage && + dv1.ShowInputMessage == dv2.ShowInputMessage && + dv1.InputTitle == dv2.InputTitle && + dv1.InputMessage == dv2.InputMessage && + dv1.ErrorTitle == dv2.ErrorTitle && + dv1.ErrorMessage == dv2.ErrorMessage && + dv1.ErrorStyle == dv2.ErrorStyle && + dv1.AllowedValues == dv2.AllowedValues && + dv1.Operator == dv2.Operator && + dv1.MinValue == dv2.MinValue && + dv1.MaxValue == dv2.MaxValue && + dv1.Value == dv2.Value; + }; + + var rules = _dataValidations.ToList(); + rules.ForEach(Delete); + + while (rules.Any()) + { + var similarRules = rules.Where(r => areEqual(rules.First(), r)).ToList(); + similarRules.ForEach(r => rules.Remove(r)); + + var consRule = similarRules.First(); + var ranges = similarRules.SelectMany(dv => dv.Ranges).ToList(); + + IXLRanges consolidatedRanges = new XLRanges(); + ranges.ForEach(r => consolidatedRanges.Add(r)); + consolidatedRanges = consolidatedRanges.Consolidate(); + + consRule.ClearRanges(); + consRule.AddRanges(consolidatedRanges); + Add(consRule); + } + } + + + private void OnRangeAdded(object sender, RangeEventArgs e) + { + ProcessRangeAdded(e.Range, sender as XLDataValidation, skipIntersectionCheck: false); + } + private void OnRangeRemoved(object sender, RangeEventArgs e) + { + ProcessRangeRemoved(e.Range); + } + + private void ProcessRangeAdded(IXLRange range, XLDataValidation dataValidation, bool skipIntersectionCheck) + { + if (!skipIntersectionCheck) + { + SplitExistingRanges(range.RangeAddress); + } + + var indexEntry = new XLDataValidationIndexEntry(range.RangeAddress, dataValidation); + _dataValidationIndex.Add(indexEntry); + } + + /// + /// The flag used to avoid unnecessary check for splitting intersected ranges when we already + /// are performing the splitting. + /// + private bool _skipSplittingExistingRanges = false; + private void SplitExistingRanges(IXLRangeAddress rangeAddress) + { + if (_skipSplittingExistingRanges) return; + + try + { + _skipSplittingExistingRanges = true; + var entries = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .ToList(); + + foreach (var entry in entries) + { + entry.DataValidation.SplitBy(rangeAddress); + } + } + finally + { + _skipSplittingExistingRanges = false; + } + + //TODO Remove empty data validations + } + + private void ProcessRangeRemoved(IXLRange range) + { + var entry = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) range.RangeAddress) + .SingleOrDefault(e => Equals(e.RangeAddress, range.RangeAddress)); + + if (entry != null) + { + _dataValidationIndex.Remove(entry.RangeAddress); + } + } + + /// + /// Class used for indexing data validation rules. + /// + private class XLDataValidationIndexEntry : IXLAddressable + { + /// + /// Gets an object with the boundaries of this range. + /// + public IXLRangeAddress RangeAddress { get; } + + public XLDataValidation DataValidation { get; } + + public XLDataValidationIndexEntry(IXLRangeAddress rangeAddress, XLDataValidation dataValidation) + { + RangeAddress = rangeAddress; + DataValidation = dataValidation; + } + } + } +} diff --git a/ClosedXML/Excel/DataValidation/XLDateCriteria.cs b/ClosedXML/Excel/DataValidation/XLDateCriteria.cs new file mode 100644 index 0000000..9f9bac0 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLDateCriteria.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; + +namespace ClosedXML.Excel +{ + public class XLDateCriteria : XLValidationCriteria + { + public XLDateCriteria(IXLDataValidation dataValidation) + : base(dataValidation) + { + } + + public void EqualTo(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(DateTime value) + { + dataValidation.Value = value.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(DateTime minValue, DateTime maxValue) + { + dataValidation.MinValue = minValue.ToOADate().ToInvariantString(); + dataValidation.MaxValue = maxValue.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(DateTime minValue, DateTime maxValue) + { + dataValidation.MinValue = minValue.ToOADate().ToInvariantString(); + dataValidation.MaxValue = maxValue.ToOADate().ToInvariantString(); + dataValidation.Operator = XLOperator.NotBetween; + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/DataValidation/XLDecimalCriteria.cs b/ClosedXML/Excel/DataValidation/XLDecimalCriteria.cs new file mode 100644 index 0000000..88088a1 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLDecimalCriteria.cs @@ -0,0 +1,62 @@ +using System; + +namespace ClosedXML.Excel +{ + public class XLDecimalCriteria : XLValidationCriteria + { + public XLDecimalCriteria(IXLDataValidation dataValidation) + : base(dataValidation) + { + } + + public void EqualTo(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(Double value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(Double minValue, Double maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(Double minValue, Double maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.NotBetween; + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/DataValidation/XLTextLengthCriteria.cs b/ClosedXML/Excel/DataValidation/XLTextLengthCriteria.cs new file mode 100644 index 0000000..6a1ce41 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLTextLengthCriteria.cs @@ -0,0 +1,62 @@ +using System; + +namespace ClosedXML.Excel +{ + public class XLTextLengthCriteria : XLValidationCriteria + { + public XLTextLengthCriteria(IXLDataValidation dataValidation) + : base(dataValidation) + { + } + + public void EqualTo(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(Int32 minValue, Int32 maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(Int32 minValue, Int32 maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.NotBetween; + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/DataValidation/XLTimeCriteria.cs b/ClosedXML/Excel/DataValidation/XLTimeCriteria.cs new file mode 100644 index 0000000..51a4f80 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLTimeCriteria.cs @@ -0,0 +1,67 @@ +using System; + +namespace ClosedXML.Excel +{ + public class XLTimeCriteria : XLValidationCriteria + { + public XLTimeCriteria(IXLDataValidation dataValidation) + : base(dataValidation) + { + } + + private static String GetXLTime(TimeSpan value) + { + return (value.TotalHours / 24.0).ToString(); + } + + public void EqualTo(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(TimeSpan value) + { + dataValidation.Value = GetXLTime(value); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(TimeSpan minValue, TimeSpan maxValue) + { + dataValidation.MinValue = GetXLTime(minValue); + dataValidation.MaxValue = GetXLTime(maxValue); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(TimeSpan minValue, TimeSpan maxValue) + { + dataValidation.MinValue = GetXLTime(minValue); + dataValidation.MaxValue = GetXLTime(maxValue); + dataValidation.Operator = XLOperator.NotBetween; + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/DataValidation/XLValidationCriteria.cs b/ClosedXML/Excel/DataValidation/XLValidationCriteria.cs new file mode 100644 index 0000000..8932e7f --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLValidationCriteria.cs @@ -0,0 +1,169 @@ +using System; + +namespace ClosedXML.Excel +{ + public abstract class XLValidationCriteria : IXLValidationCriteria + { + protected IXLDataValidation dataValidation; + + internal XLValidationCriteria(IXLDataValidation dataValidation) + { + this.dataValidation = dataValidation; + } + + #region IXLValidationCriteria Members + + public void EqualTo(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(String value) + { + dataValidation.Value = value; + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(String minValue, String maxValue) + { + dataValidation.MinValue = minValue; + dataValidation.MaxValue = maxValue; + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(String minValue, String maxValue) + { + dataValidation.MinValue = minValue; + dataValidation.MaxValue = maxValue; + dataValidation.Operator = XLOperator.NotBetween; + } + + + public void EqualTo(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(IXLRange range) + { + dataValidation.Value = range.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(IXLRange minValue, IXLRange maxValue) + { + dataValidation.MinValue = minValue.RangeAddress.ToStringFixed(); + dataValidation.MaxValue = maxValue.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(IXLRange minValue, IXLRange maxValue) + { + dataValidation.MinValue = minValue.RangeAddress.ToStringFixed(); + dataValidation.MaxValue = maxValue.RangeAddress.ToStringFixed(); + dataValidation.Operator = XLOperator.NotBetween; + } + + public void EqualTo(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualTo; + } + + public void NotEqualTo(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + + public void GreaterThan(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.GreaterThan; + } + + public void LessThan(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.LessThan; + } + + public void EqualOrGreaterThan(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + + public void EqualOrLessThan(IXLCell cell) + { + dataValidation.Value = cell.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + + public void Between(IXLCell minValue, IXLCell maxValue) + { + dataValidation.MinValue = minValue.Address.ToStringFixed(); + dataValidation.MaxValue = maxValue.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.Between; + } + + public void NotBetween(IXLCell minValue, IXLCell maxValue) + { + dataValidation.MinValue = minValue.Address.ToStringFixed(); + dataValidation.MaxValue = maxValue.Address.ToStringFixed(); + dataValidation.Operator = XLOperator.NotBetween; + } + + #endregion + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/DataValidation/XLWholeNumberCriteria.cs b/ClosedXML/Excel/DataValidation/XLWholeNumberCriteria.cs new file mode 100644 index 0000000..2fdcef1 --- /dev/null +++ b/ClosedXML/Excel/DataValidation/XLWholeNumberCriteria.cs @@ -0,0 +1,57 @@ +using System; + +namespace ClosedXML.Excel +{ + public class XLWholeNumberCriteria : XLValidationCriteria + { + public XLWholeNumberCriteria(IXLDataValidation dataValidation): base(dataValidation) + { + + } + + public void EqualTo(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualTo; + } + public void NotEqualTo(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.NotEqualTo; + } + public void GreaterThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.GreaterThan; + } + public void LessThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.LessThan; + } + public void EqualOrGreaterThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrGreaterThan; + } + public void EqualOrLessThan(Int32 value) + { + dataValidation.Value = value.ToString(); + dataValidation.Operator = XLOperator.EqualOrLessThan; + } + public void Between(Int32 minValue, Int32 maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.Between; + } + public void NotBetween(Int32 minValue, Int32 maxValue) + { + dataValidation.MinValue = minValue.ToString(); + dataValidation.MaxValue = maxValue.ToString(); + dataValidation.Operator = XLOperator.NotBetween; + } + + + } +} diff --git a/ClosedXML/Excel/Drawings/IXLDrawing.cs b/ClosedXML/Excel/Drawings/IXLDrawing.cs new file mode 100644 index 0000000..c006315 --- /dev/null +++ b/ClosedXML/Excel/Drawings/IXLDrawing.cs @@ -0,0 +1,45 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLDrawingAnchor { MoveAndSizeWithCells, MoveWithCells, Absolute} + public interface IXLDrawing + { + Int32 ShapeId { get; } + + Boolean Visible { get; set; } + T SetVisible(); + T SetVisible(Boolean hidden); + + ////String Name { get; set; } + ////T SetName(String name); + + ////String Description { get; set; } + ////T SetDescription(String description); + + IXLDrawingPosition Position { get; } + + Int32 ZOrder { get; set; } + T SetZOrder(Int32 zOrder); + + //Boolean HorizontalFlip { get; set; } + //T SetHorizontalFlip(); + //T SetHorizontalFlip(Boolean horizontalFlip); + + //Boolean VerticalFlip { get; set; } + //T SetVerticalFlip(); + //T SetVerticalFlip(Boolean verticalFlip); + + //Int32 Rotation { get; set; } + //T SetRotation(Int32 rotation); + + //Int32 ExtentLength { get; set; } + //T SetExtentLength(Int32 ExtentLength); + + //Int32 ExtentWidth { get; set; } + //T SetExtentWidth(Int32 extentWidth); + + IXLDrawingStyle Style { get; } + + } +} diff --git a/ClosedXML/Excel/Drawings/IXLDrawingPosition.cs b/ClosedXML/Excel/Drawings/IXLDrawingPosition.cs new file mode 100644 index 0000000..2b33229 --- /dev/null +++ b/ClosedXML/Excel/Drawings/IXLDrawingPosition.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingPosition + { + Int32 Column { get; set; } + IXLDrawingPosition SetColumn(Int32 column); + Double ColumnOffset { get; set; } + IXLDrawingPosition SetColumnOffset(Double columnOffset); + + Int32 Row { get; set; } + IXLDrawingPosition SetRow(Int32 row); + Double RowOffset { get; set; } + IXLDrawingPosition SetRowOffset(Double rowOffset); + } +} diff --git a/ClosedXML/Excel/Drawings/IXLPicture.cs b/ClosedXML/Excel/Drawings/IXLPicture.cs new file mode 100644 index 0000000..2036470 --- /dev/null +++ b/ClosedXML/Excel/Drawings/IXLPicture.cs @@ -0,0 +1,86 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Drawing; +using System.IO; + +namespace ClosedXML.Excel.Drawings +{ + public interface IXLPicture : IDisposable + { + IXLCell BottomRightCell { get; } + + /// + /// Type of image. The supported formats are defined by OpenXML's ImagePartType. + /// Default value is "jpeg" + /// + XLPictureFormat Format { get; } + + Int32 Height { get; set; } + + Int32 Id { get; } + + MemoryStream ImageStream { get; } + + Int32 Left { get; set; } + + String Name { get; set; } + + Int32 OriginalHeight { get; } + + Int32 OriginalWidth { get; } + + XLPicturePlacement Placement { get; set; } + + Int32 Top { get; set; } + + IXLCell TopLeftCell { get; } + + Int32 Width { get; set; } + + IXLWorksheet Worksheet { get; } + + /// + /// Create a copy of the picture on a different worksheet. + /// + /// The worksheet to which the picture will be copied. + /// A created copy of the picture. + IXLPicture CopyTo(IXLWorksheet targetSheet); + + /// + /// Deletes this picture. + /// + void Delete(); + + /// + /// Create a copy of the picture on the same worksheet. + /// + /// A created copy of the picture. + IXLPicture Duplicate(); + + Point GetOffset(XLMarkerPosition position); + + IXLPicture MoveTo(Int32 left, Int32 top); + + IXLPicture MoveTo(IXLCell cell); + + IXLPicture MoveTo(IXLCell cell, Int32 xOffset, Int32 yOffset); + + IXLPicture MoveTo(IXLCell cell, Point offset); + + IXLPicture MoveTo(IXLCell fromCell, IXLCell toCell); + + IXLPicture MoveTo(IXLCell fromCell, Int32 fromCellXOffset, Int32 fromCellYOffset, IXLCell toCell, Int32 toCellXOffset, Int32 toCellYOffset); + + IXLPicture MoveTo(IXLCell fromCell, Point fromOffset, IXLCell toCell, Point toOffset); + + IXLPicture Scale(Double factor, Boolean relativeToOriginal = false); + + IXLPicture ScaleHeight(Double factor, Boolean relativeToOriginal = false); + + IXLPicture ScaleWidth(Double factor, Boolean relativeToOriginal = false); + + IXLPicture WithPlacement(XLPicturePlacement value); + + IXLPicture WithSize(Int32 width, Int32 height); + } +} diff --git a/ClosedXML/Excel/Drawings/IXLPictures.cs b/ClosedXML/Excel/Drawings/IXLPictures.cs new file mode 100644 index 0000000..34ed782 --- /dev/null +++ b/ClosedXML/Excel/Drawings/IXLPictures.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +namespace ClosedXML.Excel.Drawings +{ + public interface IXLPictures : IEnumerable + { + int Count { get; } + + IXLPicture Add(Stream stream); + + IXLPicture Add(Stream stream, String name); + + IXLPicture Add(Stream stream, XLPictureFormat format); + + IXLPicture Add(Stream stream, XLPictureFormat format, String name); + + IXLPicture Add(Bitmap bitmap); + + IXLPicture Add(Bitmap bitmap, String name); + + IXLPicture Add(String imageFile); + + IXLPicture Add(String imageFile, String name); + + bool Contains(String pictureName); + + void Delete(String pictureName); + + void Delete(IXLPicture picture); + + IXLPicture Picture(String pictureName); + + bool TryGetPicture(String pictureName, out IXLPicture picture); + } +} diff --git a/ClosedXML/Excel/Drawings/PictureEnums.cs b/ClosedXML/Excel/Drawings/PictureEnums.cs new file mode 100644 index 0000000..f00f85b --- /dev/null +++ b/ClosedXML/Excel/Drawings/PictureEnums.cs @@ -0,0 +1,28 @@ +namespace ClosedXML.Excel.Drawings +{ + public enum XLMarkerPosition + { + TopLeft, + BottomRight + } + + public enum XLPictureFormat + { + Bmp = 0, + Gif = 1, + Png = 2, + Tiff = 3, + Icon = 4, + Pcx = 5, + Jpeg = 6 + //Emf = 7, + //Wmf = 8 + } + + public enum XLPicturePlacement + { + MoveAndSize = 0, + Move = 1, + FreeFloating = 2 + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingAlignment.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingAlignment.cs new file mode 100644 index 0000000..eb68bbb --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingAlignment.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public enum XLDrawingTextDirection + { + Context, + LeftToRight, + RightToLeft + } + public enum XLDrawingTextOrientation + { + LeftToRight, + Vertical, + BottomToTop, + TopToBottom + } + public enum XLDrawingHorizontalAlignment { Left, Justify, Center, Right, Distributed } + public enum XLDrawingVerticalAlignment { Top, Justify, Center, Bottom, Distributed } + public interface IXLDrawingAlignment + { + XLDrawingHorizontalAlignment Horizontal { get; set; } + XLDrawingVerticalAlignment Vertical { get; set; } + Boolean AutomaticSize { get; set; } + XLDrawingTextDirection Direction { get; set; } + XLDrawingTextOrientation Orientation { get; set; } + + IXLDrawingStyle SetHorizontal(XLDrawingHorizontalAlignment value); + IXLDrawingStyle SetVertical(XLDrawingVerticalAlignment value); + IXLDrawingStyle SetAutomaticSize(); IXLDrawingStyle SetAutomaticSize(Boolean value); + IXLDrawingStyle SetDirection(XLDrawingTextDirection value); + IXLDrawingStyle SetOrientation(XLDrawingTextOrientation value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingColorsAndLines.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingColorsAndLines.cs new file mode 100644 index 0000000..72a6fec --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingColorsAndLines.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public enum XLDashStyle + { + Solid, + RoundDot, + SquareDot, + Dash, + DashDot, + LongDash, + LongDashDot, + LongDashDotDot + } + public enum XLLineStyle + { + Single, ThinThin, ThinThick, ThickThin, ThickBetweenThin + } + public interface IXLDrawingColorsAndLines + { + XLColor FillColor { get; set; } + Double FillTransparency { get; set; } + XLColor LineColor { get; set; } + Double LineTransparency { get; set; } + Double LineWeight { get; set; } + XLDashStyle LineDash { get; set; } + XLLineStyle LineStyle { get; set; } + + IXLDrawingStyle SetFillColor(XLColor value); + IXLDrawingStyle SetFillTransparency(Double value); + IXLDrawingStyle SetLineColor(XLColor value); + IXLDrawingStyle SetLineTransparency(Double value); + IXLDrawingStyle SetLineWeight(Double value); + IXLDrawingStyle SetLineDash(XLDashStyle value); + IXLDrawingStyle SetLineStyle(XLLineStyle value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingFont.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingFont.cs new file mode 100644 index 0000000..878f23b --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingFont.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingFont : IXLFontBase + { + IXLDrawingStyle SetBold(); IXLDrawingStyle SetBold(Boolean value); + IXLDrawingStyle SetItalic(); IXLDrawingStyle SetItalic(Boolean value); + IXLDrawingStyle SetUnderline(); IXLDrawingStyle SetUnderline(XLFontUnderlineValues value); + IXLDrawingStyle SetStrikethrough(); IXLDrawingStyle SetStrikethrough(Boolean value); + IXLDrawingStyle SetVerticalAlignment(XLFontVerticalTextAlignmentValues value); + IXLDrawingStyle SetShadow(); IXLDrawingStyle SetShadow(Boolean value); + IXLDrawingStyle SetFontSize(Double value); + IXLDrawingStyle SetFontColor(XLColor value); + IXLDrawingStyle SetFontName(String value); + IXLDrawingStyle SetFontFamilyNumbering(XLFontFamilyNumberingValues value); + IXLDrawingStyle SetFontCharSet(XLFontCharSet value); + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingMargins.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingMargins.cs new file mode 100644 index 0000000..759cd44 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingMargins.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingMargins + { + Boolean Automatic { get; set; } + Double Left { get; set; } + Double Right { get; set; } + Double Top { get; set; } + Double Bottom { get; set; } + Double All { set; } + + IXLDrawingStyle SetAutomatic(); IXLDrawingStyle SetAutomatic(Boolean value); + IXLDrawingStyle SetLeft(Double value); + IXLDrawingStyle SetRight(Double value); + IXLDrawingStyle SetTop(Double value); + IXLDrawingStyle SetBottom(Double value); + IXLDrawingStyle SetAll(Double value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingProperties.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingProperties.cs new file mode 100644 index 0000000..6157946 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingProperties.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingProperties + { + XLDrawingAnchor Positioning { get; set; } + IXLDrawingStyle SetPositioning(XLDrawingAnchor value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingProtection.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingProtection.cs new file mode 100644 index 0000000..36f896b --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingProtection.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingProtection + { + Boolean Locked { get; set; } + Boolean LockText { get; set; } + + IXLDrawingStyle SetLocked(); IXLDrawingStyle SetLocked(Boolean value); + IXLDrawingStyle SetLockText(); IXLDrawingStyle SetLockText(Boolean value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingSize.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingSize.cs new file mode 100644 index 0000000..5b83d32 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingSize.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingSize + { + Boolean AutomaticSize { get; set; } + Double Height { get; set; } + Double Width { get; set; } + + IXLDrawingStyle SetAutomaticSize(); IXLDrawingStyle SetAutomaticSize(Boolean value); + IXLDrawingStyle SetHeight(Double value); + IXLDrawingStyle SetWidth(Double value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingStyle.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingStyle.cs new file mode 100644 index 0000000..9eed6a8 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingStyle.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingStyle + { + //IXLDrawingFont Font { get; } + IXLDrawingAlignment Alignment { get; } + IXLDrawingColorsAndLines ColorsAndLines { get; } + IXLDrawingSize Size { get; } + IXLDrawingProtection Protection { get; } + IXLDrawingProperties Properties { get; } + IXLDrawingMargins Margins { get; } + IXLDrawingWeb Web { get; } + } +} diff --git a/ClosedXML/Excel/Drawings/Style/IXLDrawingWeb.cs b/ClosedXML/Excel/Drawings/Style/IXLDrawingWeb.cs new file mode 100644 index 0000000..0032488 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/IXLDrawingWeb.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLDrawingWeb + { + String AlternateText { get; set; } + IXLDrawingStyle SetAlternateText(String value); + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingAlignment.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingAlignment.cs new file mode 100644 index 0000000..3da9f89 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingAlignment.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingAlignment: IXLDrawingAlignment + { + private readonly IXLDrawingStyle _style; + + public XLDrawingAlignment(IXLDrawingStyle style) + { + _style = style; + } + public XLDrawingHorizontalAlignment Horizontal { get; set; } public IXLDrawingStyle SetHorizontal(XLDrawingHorizontalAlignment value) { Horizontal = value; return _style; } + public XLDrawingVerticalAlignment Vertical { get; set; } public IXLDrawingStyle SetVertical(XLDrawingVerticalAlignment value) { Vertical = value; return _style; } + public Boolean AutomaticSize { get; set; } public IXLDrawingStyle SetAutomaticSize() { AutomaticSize = true; return _style; } public IXLDrawingStyle SetAutomaticSize(Boolean value) { AutomaticSize = value; return _style; } + public XLDrawingTextDirection Direction { get; set; } public IXLDrawingStyle SetDirection(XLDrawingTextDirection value) { Direction = value; return _style; } + public XLDrawingTextOrientation Orientation { get; set; } public IXLDrawingStyle SetOrientation(XLDrawingTextOrientation value) { Orientation = value; return _style; } + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingColorsAndLines.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingColorsAndLines.cs new file mode 100644 index 0000000..ec8e4db --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingColorsAndLines.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingColorsAndLines: IXLDrawingColorsAndLines + { + private readonly IXLDrawingStyle _style; + + public XLDrawingColorsAndLines(IXLDrawingStyle style) + { + _style = style; + } + public XLColor FillColor { get; set; } public IXLDrawingStyle SetFillColor(XLColor value) { FillColor = value; return _style; } + public Double FillTransparency { get; set; } public IXLDrawingStyle SetFillTransparency(Double value) { FillTransparency = value; return _style; } + public XLColor LineColor { get; set; } public IXLDrawingStyle SetLineColor(XLColor value) { LineColor = value; return _style; } + public Double LineTransparency { get; set; } public IXLDrawingStyle SetLineTransparency(Double value) { LineTransparency = value; return _style; } + public Double LineWeight { get; set; } public IXLDrawingStyle SetLineWeight(Double value) { LineWeight = value; return _style; } + public XLDashStyle LineDash { get; set; } public IXLDrawingStyle SetLineDash(XLDashStyle value) { LineDash = value; return _style; } + public XLLineStyle LineStyle { get; set; } public IXLDrawingStyle SetLineStyle(XLLineStyle value) { LineStyle = value; return _style; } + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingFont.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingFont.cs new file mode 100644 index 0000000..744d781 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingFont.cs @@ -0,0 +1,127 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLDrawingFont : IXLDrawingFont + { + private readonly IXLDrawingStyle _style; + + public XLDrawingFont(IXLDrawingStyle style) + { + _style = style; + FontName = "Tahoma"; + FontSize = 9; + Underline = XLFontUnderlineValues.None; + FontColor = XLColor.FromIndex(64); + } + + public Boolean Bold { get; set; } + public Boolean Italic { get; set; } + public XLFontUnderlineValues Underline { get; set; } + public Boolean Strikethrough { get; set; } + public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; } + public Boolean Shadow { get; set; } + public Double FontSize { get; set; } + public XLColor FontColor { get; set; } + public String FontName { get; set; } + public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; } + + public XLFontCharSet FontCharSet { get; set; } + + public IXLDrawingStyle SetBold() + { + Bold = true; + return _style; + } + + public IXLDrawingStyle SetBold(Boolean value) + { + Bold = value; + return _style; + } + + public IXLDrawingStyle SetItalic() + { + Italic = true; + return _style; + } + + public IXLDrawingStyle SetItalic(Boolean value) + { + Italic = value; + return _style; + } + + public IXLDrawingStyle SetUnderline() + { + Underline = XLFontUnderlineValues.Single; + return _style; + } + + public IXLDrawingStyle SetUnderline(XLFontUnderlineValues value) + { + Underline = value; + return _style; + } + + public IXLDrawingStyle SetStrikethrough() + { + Strikethrough = true; + return _style; + } + + public IXLDrawingStyle SetStrikethrough(Boolean value) + { + Strikethrough = value; + return _style; + } + + public IXLDrawingStyle SetVerticalAlignment(XLFontVerticalTextAlignmentValues value) + { + VerticalAlignment = value; + return _style; + } + + public IXLDrawingStyle SetShadow() + { + Shadow = true; + return _style; + } + + public IXLDrawingStyle SetShadow(Boolean value) + { + Shadow = value; + return _style; + } + + public IXLDrawingStyle SetFontSize(Double value) + { + FontSize = value; + return _style; + } + + public IXLDrawingStyle SetFontColor(XLColor value) + { + FontColor = value; + return _style; + } + + public IXLDrawingStyle SetFontName(String value) + { + FontName = value; + return _style; + } + + public IXLDrawingStyle SetFontFamilyNumbering(XLFontFamilyNumberingValues value) + { + FontFamilyNumbering = value; + return _style; + } + + public IXLDrawingStyle SetFontCharSet(XLFontCharSet value) + { + FontCharSet = value; + return _style; + } + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingMargins.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingMargins.cs new file mode 100644 index 0000000..650f7a1 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingMargins.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingMargins: IXLDrawingMargins + { + private readonly IXLDrawingStyle _style; + public XLDrawingMargins(IXLDrawingStyle style) + { + _style = style; + } + public Boolean Automatic { get; set; } public IXLDrawingStyle SetAutomatic() { Automatic = true; return _style; } public IXLDrawingStyle SetAutomatic(Boolean value) { Automatic = value; return _style; } + Double _left; + public Double Left { get { return _left; } set { _left = value; Automatic = false; } } + public IXLDrawingStyle SetLeft(Double value) { Left = value; return _style; } + Double _right; + public Double Right { get { return _right; } set { _right = value; Automatic = false; } } public IXLDrawingStyle SetRight(Double value) { Right = value; return _style; } + Double _top; + public Double Top { get { return _top; } set { _top = value; Automatic = false; } } public IXLDrawingStyle SetTop(Double value) { Top = value; return _style; } + Double _bottom; + public Double Bottom { get { return _bottom; } set { _bottom = value; Automatic = false; } } public IXLDrawingStyle SetBottom(Double value) { Bottom = value; return _style; } + public Double All + { + set + { + _left = value; + _right = value; + _top = value; + _bottom = value; + Automatic = false; + } + } + public IXLDrawingStyle SetAll(Double value) { All = value; return _style; } + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingProperties.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingProperties.cs new file mode 100644 index 0000000..a702762 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingProperties.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingProperties : IXLDrawingProperties + { + private readonly IXLDrawingStyle _style; + + public XLDrawingProperties(IXLDrawingStyle style) + { + _style = style; + } + public XLDrawingAnchor Positioning { get; set; } public IXLDrawingStyle SetPositioning(XLDrawingAnchor value) { Positioning = value; return _style; } + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingProtection.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingProtection.cs new file mode 100644 index 0000000..8d0c570 --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingProtection.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingProtection : IXLDrawingProtection + { + private readonly IXLDrawingStyle _style; + + public XLDrawingProtection(IXLDrawingStyle style) + { + _style = style; + } + public Boolean Locked { get; set; } public IXLDrawingStyle SetLocked() { Locked = true; return _style; } public IXLDrawingStyle SetLocked(Boolean value) { Locked = value; return _style; } + public Boolean LockText { get; set; } public IXLDrawingStyle SetLockText() { LockText = true; return _style; } public IXLDrawingStyle SetLockText(Boolean value) { LockText = value; return _style; } + + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingSize.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingSize.cs new file mode 100644 index 0000000..84f4f1b --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingSize.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingSize : IXLDrawingSize + { + private readonly IXLDrawingStyle _style; + + public XLDrawingSize(IXLDrawingStyle style) + { + _style = style; + } + + public Boolean AutomaticSize { get { return _style.Alignment.AutomaticSize; } set { _style.Alignment.AutomaticSize = value; } } + public IXLDrawingStyle SetAutomaticSize() { AutomaticSize = true; return _style; } public IXLDrawingStyle SetAutomaticSize(Boolean value) { AutomaticSize = value; return _style; } + public Double Height { get; set; } public IXLDrawingStyle SetHeight(Double value) { Height = value; return _style; } + public Double Width { get; set; } public IXLDrawingStyle SetWidth(Double value) { Width = value; return _style; } + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingStyle.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingStyle.cs new file mode 100644 index 0000000..6fd952a --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingStyle.cs @@ -0,0 +1,60 @@ +namespace ClosedXML.Excel +{ + internal class XLDrawingStyle : IXLDrawingStyle + { + public XLDrawingStyle() + { + //Font = new XLDrawingFont(this); + Alignment = new XLDrawingAlignment(this); + ColorsAndLines = new XLDrawingColorsAndLines(this); + Size = new XLDrawingSize(this); + Protection = new XLDrawingProtection(this); + Properties = new XLDrawingProperties(this); + Margins = new XLDrawingMargins(this); + Web = new XLDrawingWeb(this); + } + + //public IXLDrawingFont Font { get; private set; } + public IXLDrawingAlignment Alignment { get; private set; } + + public IXLDrawingColorsAndLines ColorsAndLines { get; private set; } + public IXLDrawingSize Size { get; private set; } + public IXLDrawingProtection Protection { get; private set; } + public IXLDrawingProperties Properties { get; private set; } + public IXLDrawingMargins Margins { get; private set; } + public IXLDrawingWeb Web { get; private set; } + + public static IXLDrawingStyle DefaultCommentStyle + { + get + { + var defaultCommentStyle = new XLDrawingStyle(); + + defaultCommentStyle + .Margins.SetLeft(0.1) + .Margins.SetRight(0.1) + .Margins.SetTop(0.05) + .Margins.SetBottom(0.05) + .Margins.SetAutomatic() + .Size.SetHeight(59.25) + .Size.SetWidth(19.2) + .ColorsAndLines.SetLineColor(XLColor.Black) + .ColorsAndLines.SetFillColor(XLColor.FromArgb(255, 255, 225)) + .ColorsAndLines.SetLineDash(XLDashStyle.Solid) + .ColorsAndLines.SetLineStyle(XLLineStyle.Single) + .ColorsAndLines.SetLineWeight(0.75) + .ColorsAndLines.SetFillTransparency(1) + .ColorsAndLines.SetLineTransparency(1) + .Alignment.SetHorizontal(XLDrawingHorizontalAlignment.Left) + .Alignment.SetVertical(XLDrawingVerticalAlignment.Top) + .Alignment.SetDirection(XLDrawingTextDirection.LeftToRight) + .Alignment.SetOrientation(XLDrawingTextOrientation.LeftToRight) + .Properties.SetPositioning(XLDrawingAnchor.Absolute) + .Protection.SetLocked() + .Protection.SetLockText(); + + return defaultCommentStyle; + } + } + } +} diff --git a/ClosedXML/Excel/Drawings/Style/XLDrawingWeb.cs b/ClosedXML/Excel/Drawings/Style/XLDrawingWeb.cs new file mode 100644 index 0000000..f47cb3b --- /dev/null +++ b/ClosedXML/Excel/Drawings/Style/XLDrawingWeb.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingWeb : IXLDrawingWeb + { + private readonly IXLDrawingStyle _style; + + public XLDrawingWeb(IXLDrawingStyle style) + { + _style = style; + } + public String AlternateText { get; set; } public IXLDrawingStyle SetAlternateText(String value) { AlternateText = value; return _style; } + + } +} diff --git a/ClosedXML/Excel/Drawings/XLDrawing.cs b/ClosedXML/Excel/Drawings/XLDrawing.cs new file mode 100644 index 0000000..c3e3a83 --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLDrawing.cs @@ -0,0 +1,114 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLDrawing: IXLDrawing + { + internal T Container; + public XLDrawing() + { + Style = new XLDrawingStyle(); + Position = new XLDrawingPosition(); + } + + public Int32 ShapeId { get; internal set; } + + public Boolean Visible { get; set; } + public T SetVisible() + { + Visible = true; + return Container; + } + public T SetVisible(Boolean hidden) + { + Visible = hidden; + return Container; + } + + public String Name { get; set; } + public T SetName(String name) + { + Name = name; + return Container; + } + + public String Description { get; set; } + public T SetDescription(String description) + { + Description = description; + return Container; + } + + public IXLDrawingPosition Position { get; private set; } + + public Int32 ZOrder { get; set; } + public T SetZOrder(Int32 zOrder) + { + ZOrder = zOrder; + return Container; + } + + public Boolean HorizontalFlip { get; set; } + public T SetHorizontalFlip() + { + HorizontalFlip = true; + return Container; + } + public T SetHorizontalFlip(Boolean horizontalFlip) + { + HorizontalFlip = horizontalFlip; + return Container; + } + + public Boolean VerticalFlip { get; set; } + public T SetVerticalFlip() + { + VerticalFlip = true; + return Container; + } + public T SetVerticalFlip(Boolean verticalFlip) + { + VerticalFlip = verticalFlip; + return Container; + } + + public Int32 Rotation { get; set; } + public T SetRotation(Int32 rotation) + { + Rotation = rotation; + return Container; + } + + public Int32 OffsetX { get; set; } + public T SetOffsetX(Int32 offsetX) + { + OffsetX = offsetX; + return Container; + } + + public Int32 OffsetY { get; set; } + public T SetOffsetY(Int32 offsetY) + { + OffsetY = offsetY; + return Container; + } + + public Int32 ExtentLength { get; set; } + public T SetExtentLength(Int32 extentLength) + { + ExtentLength = extentLength; + return Container; + } + + public Int32 ExtentWidth { get; set; } + public T SetExtentWidth(Int32 extentWidth) + { + ExtentWidth = extentWidth; + return Container; + } + + public IXLDrawingStyle Style { get; private set; } + + + } +} diff --git a/ClosedXML/Excel/Drawings/XLDrawingPosition.cs b/ClosedXML/Excel/Drawings/XLDrawingPosition.cs new file mode 100644 index 0000000..b7309f2 --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLDrawingPosition.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLDrawingPosition: IXLDrawingPosition + { + public Int32 Column { get; set; } + public IXLDrawingPosition SetColumn(Int32 column) { Column = column; return this; } + public Double ColumnOffset { get; set; } + public IXLDrawingPosition SetColumnOffset(Double columnOffset) { ColumnOffset = columnOffset; return this; } + + public Int32 Row { get; set; } + public IXLDrawingPosition SetRow(Int32 row) { Row = row; return this; } + public Double RowOffset { get; set; } + public IXLDrawingPosition SetRowOffset(Double rowOffset) { RowOffset = rowOffset; return this; } + } +} diff --git a/ClosedXML/Excel/Drawings/XLMarker.cs b/ClosedXML/Excel/Drawings/XLMarker.cs new file mode 100644 index 0000000..57d496a --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLMarker.cs @@ -0,0 +1,37 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Diagnostics; +using System.Drawing; + +namespace ClosedXML.Excel.Drawings +{ + [DebuggerDisplay("{Address} {Offset}")] + internal class XLMarker + { + // Using a range to store the location so that it gets added to the range repository + // and hence will be adjusted when there are insertions / deletions + private readonly IXLRange rangeCell; + + internal XLMarker(IXLCell cell) + : this(cell.AsRange(), new Point(0, 0)) + { } + + internal XLMarker(IXLCell cell, Point offset) + : this(cell.AsRange(), offset) + { } + + private XLMarker(IXLRange rangeCell, Point offset) + { + if (rangeCell.RowCount() != 1 || rangeCell.ColumnCount() != 1) + throw new ArgumentException("Range should contain only one cell.", nameof(rangeCell)); + + this.rangeCell = rangeCell; + this.Offset = offset; + } + + public IXLCell Cell { get => rangeCell.FirstCell(); } + public Int32 ColumnNumber { get => rangeCell.RangeAddress.FirstAddress.ColumnNumber; } + public Point Offset { get; set; } + public Int32 RowNumber { get => rangeCell.RangeAddress.FirstAddress.RowNumber; } + } +} diff --git a/ClosedXML/Excel/Drawings/XLPicture.cs b/ClosedXML/Excel/Drawings/XLPicture.cs new file mode 100644 index 0000000..6dd3776 --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLPicture.cs @@ -0,0 +1,448 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace ClosedXML.Excel.Drawings +{ + [DebuggerDisplay("{Name}")] + internal class XLPicture : IXLPicture + { + private const String InvalidNameChars = @":\/?*[]"; + private static IDictionary FormatMap; + private Int32 height; + private Int32 id; + private String name = string.Empty; + private Int32 width; + + static XLPicture() + { + var properties = typeof(ImageFormat).GetProperties(BindingFlags.Static | BindingFlags.Public); + FormatMap = Enum.GetValues(typeof(XLPictureFormat)) + .Cast() + .Where(pf => properties.Any(pi => pi.Name.Equals(pf.ToString(), StringComparison.OrdinalIgnoreCase))) + .ToDictionary( + pf => pf, + pf => properties.Single(pi => pi.Name.Equals(pf.ToString(), StringComparison.OrdinalIgnoreCase)).GetValue(null, null) as ImageFormat + ); + } + + internal XLPicture(IXLWorksheet worksheet, Stream stream) + : this(worksheet) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + this.ImageStream = new MemoryStream(); + { + stream.Position = 0; + stream.CopyTo(ImageStream); + ImageStream.Seek(0, SeekOrigin.Begin); + + using (var bitmap = new Bitmap(ImageStream)) + { + if (FormatMap.Values.Select(f => f.Guid).Contains(bitmap.RawFormat.Guid)) + this.Format = FormatMap.Single(f => f.Value.Guid.Equals(bitmap.RawFormat.Guid)).Key; + + DeduceDimensionsFromBitmap(bitmap); + } + ImageStream.Seek(0, SeekOrigin.Begin); + } + } + + internal XLPicture(IXLWorksheet worksheet, Stream stream, XLPictureFormat format) + : this(worksheet) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + this.Format = format; + + this.ImageStream = new MemoryStream(); + { + stream.Position = 0; + stream.CopyTo(ImageStream); + ImageStream.Seek(0, SeekOrigin.Begin); + + using (var bitmap = new Bitmap(ImageStream)) + { + if (FormatMap.TryGetValue(this.Format, out ImageFormat imageFormat) && imageFormat.Guid != bitmap.RawFormat.Guid) + throw new ArgumentException("The picture format in the stream and the parameter don't match"); + + DeduceDimensionsFromBitmap(bitmap); + } + ImageStream.Seek(0, SeekOrigin.Begin); + } + } + + internal XLPicture(IXLWorksheet worksheet, Bitmap bitmap) + : this(worksheet) + { + if (bitmap == null) throw new ArgumentNullException(nameof(bitmap)); + this.ImageStream = new MemoryStream(); + bitmap.Save(ImageStream, bitmap.RawFormat); + ImageStream.Seek(0, SeekOrigin.Begin); + DeduceDimensionsFromBitmap(bitmap); + + var formats = FormatMap.Where(f => f.Value.Guid.Equals(bitmap.RawFormat.Guid)); + if (!formats.Any() || formats.Count() > 1) + throw new ArgumentException("Unsupported or unknown image format in bitmap"); + + this.Format = formats.Single().Key; + } + + private XLPicture(IXLWorksheet worksheet) + { + this.Worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + this.Placement = XLPicturePlacement.MoveAndSize; + this.Markers = new Dictionary() + { + [XLMarkerPosition.TopLeft] = null, + [XLMarkerPosition.BottomRight] = null + }; + + // Calculate default picture ID + var allPictures = worksheet.Workbook.Worksheets.SelectMany(ws => ws.Pictures); + if (allPictures.Any()) + this.id = allPictures.Max(p => p.Id) + 1; + else + this.id = 1; + } + + public IXLCell BottomRightCell + { + get + { + return Markers[XLMarkerPosition.BottomRight].Cell; + } + + private set + { + if (!value.Worksheet.Equals(this.Worksheet)) + throw new InvalidOperationException("A picture and its anchor cells must be on the same worksheet"); + + this.Markers[XLMarkerPosition.BottomRight] = new XLMarker(value); + } + } + + public XLPictureFormat Format { get; private set; } + + public Int32 Height + { + get { return height; } + set + { + if (this.Placement == XLPicturePlacement.MoveAndSize) + throw new ArgumentException("To set the height, the placement should be FreeFloating or Move"); + height = value; + } + } + + public Int32 Id + { + get { return id; } + internal set + { + if ((Worksheet.Pictures.FirstOrDefault(p => p.Id.Equals(value)) ?? this) != this) + throw new ArgumentException($"The picture ID '{value}' already exists."); + + id = value; + } + } + + public MemoryStream ImageStream { get; private set; } + + public Int32 Left + { + get { return Markers[XLMarkerPosition.TopLeft]?.Offset.X ?? 0; } + set + { + if (this.Placement != XLPicturePlacement.FreeFloating) + throw new ArgumentException("To set the left-hand offset, the placement should be FreeFloating"); + + Markers[XLMarkerPosition.TopLeft] = new XLMarker(Worksheet.Cell(1, 1), new Point(value, this.Top)); + } + } + + public String Name + { + get { return name; } + set + { + if (name == value) return; + + if ((Worksheet.Pictures.FirstOrDefault(p => p.Name.Equals(value, StringComparison.OrdinalIgnoreCase)) ?? this) != this) + throw new ArgumentException($"The picture name '{value}' already exists."); + + SetName(value); + } + } + + public Int32 OriginalHeight { get; private set; } + + public Int32 OriginalWidth { get; private set; } + + public XLPicturePlacement Placement { get; set; } + + public Int32 Top + { + get { return Markers[XLMarkerPosition.TopLeft]?.Offset.Y ?? 0; } + set + { + if (this.Placement != XLPicturePlacement.FreeFloating) + throw new ArgumentException("To set the top offset, the placement should be FreeFloating"); + + Markers[XLMarkerPosition.TopLeft] = new XLMarker(Worksheet.Cell(1, 1), new Point(this.Left, value)); + } + } + + public IXLCell TopLeftCell + { + get + { + return Markers[XLMarkerPosition.TopLeft].Cell; + } + + private set + { + if (!value.Worksheet.Equals(this.Worksheet)) + throw new InvalidOperationException("A picture and its anchor cells must be on the same worksheet"); + + this.Markers[XLMarkerPosition.TopLeft] = new XLMarker(value); + } + } + + public Int32 Width + { + get { return width; } + set + { + if (this.Placement == XLPicturePlacement.MoveAndSize) + throw new ArgumentException("To set the width, the placement should be FreeFloating or Move"); + width = value; + } + } + + public IXLWorksheet Worksheet { get; } + + internal IDictionary Markers { get; private set; } + + internal String RelId { get; set; } + + /// + /// Create a copy of the picture on a different worksheet. + /// + /// The worksheet to which the picture will be copied. + /// A created copy of the picture. + public IXLPicture CopyTo(IXLWorksheet targetSheet) + { + return CopyTo((XLWorksheet)targetSheet); + } + + public void Delete() + { + Worksheet.Pictures.Delete(this.Name); + } + + // Used by Janitor.Fody + private void DisposeManaged() + { + this.ImageStream.Dispose(); + } + +#if _NET40_ + + public void Dispose() + { + // net40 doesn't support Janitor.Fody, so let's dispose manually + DisposeManaged(); + } + +#else + + public void Dispose() + { + // Leave this empty (for non net40 targets) so that Janitor.Fody can do its work + } + +#endif + + /// + /// Create a copy of the picture on the same worksheet. + /// + /// A created copy of the picture. + public IXLPicture Duplicate() + { + return CopyTo(Worksheet); + } + + public Point GetOffset(XLMarkerPosition position) + { + return Markers[position].Offset; + } + + public IXLPicture MoveTo(Int32 left, Int32 top) + { + this.Placement = XLPicturePlacement.FreeFloating; + this.Left = left; + this.Top = top; + return this; + } + + public IXLPicture MoveTo(IXLCell cell) + { + return MoveTo(cell, 0, 0); + } + + public IXLPicture MoveTo(IXLCell cell, Int32 xOffset, Int32 yOffset) + { + return MoveTo(cell, new Point(xOffset, yOffset)); + } + + public IXLPicture MoveTo(IXLCell cell, Point offset) + { + if (cell == null) throw new ArgumentNullException(nameof(cell)); + this.Placement = XLPicturePlacement.Move; + this.TopLeftCell = cell; + this.Markers[XLMarkerPosition.TopLeft].Offset = offset; + return this; + } + + public IXLPicture MoveTo(IXLCell fromCell, IXLCell toCell) + { + return MoveTo(fromCell, 0, 0, toCell, 0, 0); + } + + public IXLPicture MoveTo(IXLCell fromCell, Int32 fromCellXOffset, Int32 fromCellYOffset, IXLCell toCell, Int32 toCellXOffset, Int32 toCellYOffset) + { + return MoveTo(fromCell, new Point(fromCellXOffset, fromCellYOffset), toCell, new Point(toCellXOffset, toCellYOffset)); + } + + public IXLPicture MoveTo(IXLCell fromCell, Point fromOffset, IXLCell toCell, Point toOffset) + { + if (fromCell == null) throw new ArgumentNullException(nameof(fromCell)); + if (toCell == null) throw new ArgumentNullException(nameof(toCell)); + this.Placement = XLPicturePlacement.MoveAndSize; + + this.TopLeftCell = fromCell; + this.Markers[XLMarkerPosition.TopLeft].Offset = fromOffset; + + this.BottomRightCell = toCell; + this.Markers[XLMarkerPosition.BottomRight].Offset = toOffset; + + return this; + } + + public IXLPicture Scale(Double factor, Boolean relativeToOriginal = false) + { + return this.ScaleHeight(factor, relativeToOriginal).ScaleWidth(factor, relativeToOriginal); + } + + public IXLPicture ScaleHeight(Double factor, Boolean relativeToOriginal = false) + { + this.Height = Convert.ToInt32((relativeToOriginal ? this.OriginalHeight : this.Height) * factor); + return this; + } + + public IXLPicture ScaleWidth(Double factor, Boolean relativeToOriginal = false) + { + this.Width = Convert.ToInt32((relativeToOriginal ? this.OriginalWidth : this.Width) * factor); + return this; + } + + public IXLPicture WithPlacement(XLPicturePlacement value) + { + this.Placement = value; + return this; + } + + public IXLPicture WithSize(Int32 width, Int32 height) + { + this.Width = width; + this.Height = height; + return this; + } + + internal IXLPicture CopyTo(XLWorksheet targetSheet) + { + if (targetSheet == null) + targetSheet = Worksheet as XLWorksheet; + + IXLPicture newPicture; + if (targetSheet == Worksheet) + newPicture = targetSheet.AddPicture(ImageStream, Format); + else + newPicture = targetSheet.AddPicture(ImageStream, Format, Name); + + newPicture = newPicture + .WithPlacement(XLPicturePlacement.FreeFloating) + .WithSize(Width, Height) + .WithPlacement(Placement); + + switch (Placement) + { + case XLPicturePlacement.FreeFloating: + newPicture.MoveTo(Left, Top); + break; + + case XLPicturePlacement.Move: + newPicture.MoveTo(targetSheet.Cell(TopLeftCell.Address), GetOffset(XLMarkerPosition.TopLeft)); + break; + + case XLPicturePlacement.MoveAndSize: + newPicture.MoveTo(targetSheet.Cell(TopLeftCell.Address), GetOffset(XLMarkerPosition.TopLeft), targetSheet.Cell(BottomRightCell.Address), + GetOffset(XLMarkerPosition.BottomRight)); + break; + } + + return newPicture; + } + + internal void SetName(string value) + { + if (String.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Picture names cannot be empty"); + + if (value.IndexOfAny(InvalidNameChars.ToCharArray()) != -1) + throw new ArgumentException($"Picture names cannot contain any of the following characters: {InvalidNameChars}"); + + if (value.Length > 31) + throw new ArgumentException("Picture names cannot be more than 31 characters"); + + name = value; + } + + private static ImageFormat FromMimeType(string mimeType) + { + var guid = ImageCodecInfo.GetImageDecoders().FirstOrDefault(c => c.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))?.FormatID; + if (!guid.HasValue) return null; + var property = typeof(System.Drawing.Imaging.ImageFormat).GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(pi => (pi.GetValue(null, null) as ImageFormat).Guid.Equals(guid.Value)); + + if (property == null) return null; + return (property.GetValue(null, null) as ImageFormat); + } + + private static string GetMimeType(Image i) + { + var imgguid = i.RawFormat.Guid; + foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageDecoders()) + { + if (codec.FormatID == imgguid) + return codec.MimeType; + } + return "image/unknown"; + } + + private void DeduceDimensionsFromBitmap(Bitmap bitmap) + { + this.OriginalWidth = bitmap.Width; + this.OriginalHeight = bitmap.Height; + + this.width = bitmap.Width; + this.height = bitmap.Height; + } + } +} diff --git a/ClosedXML/Excel/Drawings/XLPictures.cs b/ClosedXML/Excel/Drawings/XLPictures.cs new file mode 100644 index 0000000..6e17a7a --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLPictures.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace ClosedXML.Excel.Drawings +{ + internal class XLPictures : IXLPictures, IEnumerable + { + private readonly List _pictures = new List(); + private readonly XLWorksheet _worksheet; + + public XLPictures(XLWorksheet worksheet) + { + _worksheet = worksheet; + Deleted = new HashSet(); + } + + public int Count + { + [DebuggerStepThrough] + get { return _pictures.Count; } + } + + internal ICollection Deleted { get; private set; } + + public IXLPicture Add(Stream stream) + { + var picture = new XLPicture(_worksheet, stream); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Stream stream, string name) + { + var picture = Add(stream); + picture.Name = name; + return picture; + } + + public IXLPicture Add(Stream stream, XLPictureFormat format) + { + var picture = new XLPicture(_worksheet, stream, format); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Stream stream, XLPictureFormat format, string name) + { + var picture = Add(stream, format); + picture.Name = name; + return picture; + } + + public IXLPicture Add(Bitmap bitmap) + { + var picture = new XLPicture(_worksheet, bitmap); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Bitmap bitmap, string name) + { + var picture = Add(bitmap); + picture.Name = name; + return picture; + } + + public IXLPicture Add(string imageFile) + { + using (var fs = File.OpenRead(imageFile)) + { + var picture = new XLPicture(_worksheet, fs); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + } + + public IXLPicture Add(string imageFile, string name) + { + var picture = Add(imageFile); + picture.Name = name; + return picture; + } + + public bool Contains(string pictureName) + { + return _pictures.Any(p => string.Equals(p.Name, pictureName, StringComparison.OrdinalIgnoreCase)); + } + + public void Delete(IXLPicture picture) + { + Delete(picture.Name); + } + + public void Delete(string pictureName) + { + var picturesToDelete = _pictures + .Where(picture => picture.Name.Equals(pictureName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!picturesToDelete.Any()) + throw new ArgumentOutOfRangeException(nameof(pictureName), $"Picture {pictureName} was not found."); + + foreach (var picture in picturesToDelete) + { + if (!string.IsNullOrEmpty(picture.RelId)) + Deleted.Add(picture.RelId); + + _pictures.Remove(picture); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _pictures.Cast().GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_pictures).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLPicture Picture(string pictureName) + { + if (TryGetPicture(pictureName, out IXLPicture p)) + return p; + + throw new ArgumentOutOfRangeException(nameof(pictureName), $"Picture {pictureName} was not found."); + } + + public bool TryGetPicture(string pictureName, out IXLPicture picture) + { + var matches = _pictures.Where(p => p.Name.Equals(pictureName, StringComparison.OrdinalIgnoreCase)); + if (matches.Any()) + { + picture = matches.First(); + return true; + } + picture = null; + return false; + } + + internal IXLPicture Add(Stream stream, string name, int Id) + { + var picture = Add(stream) as XLPicture; + picture.SetName(name); + picture.Id = Id; + return picture; + } + + private String GetNextPictureName() + { + var pictureNumber = this.Count; + while (_pictures.Any(p => p.Name == $"Picture {pictureNumber}")) + { + pictureNumber++; + } + return $"Picture {pictureNumber}"; + } + } +} diff --git a/ClosedXML/Excel/EnumConverter.cs b/ClosedXML/Excel/EnumConverter.cs new file mode 100644 index 0000000..31b1414 --- /dev/null +++ b/ClosedXML/Excel/EnumConverter.cs @@ -0,0 +1,1703 @@ +using ClosedXML.Excel.Drawings; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using Vml = DocumentFormat.OpenXml.Vml; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; +using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet; + +namespace ClosedXML.Excel +{ + internal static class EnumConverter + { + #region To OpenXml + + public static UnderlineValues ToOpenXml(this XLFontUnderlineValues value) + { + switch (value) + { + case XLFontUnderlineValues.Double: + return UnderlineValues.Double; + + case XLFontUnderlineValues.DoubleAccounting: + return UnderlineValues.DoubleAccounting; + + case XLFontUnderlineValues.None: + return UnderlineValues.None; + + case XLFontUnderlineValues.Single: + return UnderlineValues.Single; + + case XLFontUnderlineValues.SingleAccounting: + return UnderlineValues.SingleAccounting; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static OrientationValues ToOpenXml(this XLPageOrientation value) + { + switch (value) + { + case XLPageOrientation.Default: + return OrientationValues.Default; + + case XLPageOrientation.Landscape: + return OrientationValues.Landscape; + + case XLPageOrientation.Portrait: + return OrientationValues.Portrait; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static VerticalAlignmentRunValues ToOpenXml(this XLFontVerticalTextAlignmentValues value) + { + switch (value) + { + case XLFontVerticalTextAlignmentValues.Baseline: + return VerticalAlignmentRunValues.Baseline; + + case XLFontVerticalTextAlignmentValues.Subscript: + return VerticalAlignmentRunValues.Subscript; + + case XLFontVerticalTextAlignmentValues.Superscript: + return VerticalAlignmentRunValues.Superscript; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static PatternValues ToOpenXml(this XLFillPatternValues value) + { + switch (value) + { + case XLFillPatternValues.DarkDown: + return PatternValues.DarkDown; + + case XLFillPatternValues.DarkGray: + return PatternValues.DarkGray; + + case XLFillPatternValues.DarkGrid: + return PatternValues.DarkGrid; + + case XLFillPatternValues.DarkHorizontal: + return PatternValues.DarkHorizontal; + + case XLFillPatternValues.DarkTrellis: + return PatternValues.DarkTrellis; + + case XLFillPatternValues.DarkUp: + return PatternValues.DarkUp; + + case XLFillPatternValues.DarkVertical: + return PatternValues.DarkVertical; + + case XLFillPatternValues.Gray0625: + return PatternValues.Gray0625; + + case XLFillPatternValues.Gray125: + return PatternValues.Gray125; + + case XLFillPatternValues.LightDown: + return PatternValues.LightDown; + + case XLFillPatternValues.LightGray: + return PatternValues.LightGray; + + case XLFillPatternValues.LightGrid: + return PatternValues.LightGrid; + + case XLFillPatternValues.LightHorizontal: + return PatternValues.LightHorizontal; + + case XLFillPatternValues.LightTrellis: + return PatternValues.LightTrellis; + + case XLFillPatternValues.LightUp: + return PatternValues.LightUp; + + case XLFillPatternValues.LightVertical: + return PatternValues.LightVertical; + + case XLFillPatternValues.MediumGray: + return PatternValues.MediumGray; + + case XLFillPatternValues.None: + return PatternValues.None; + + case XLFillPatternValues.Solid: + return PatternValues.Solid; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static BorderStyleValues ToOpenXml(this XLBorderStyleValues value) + { + switch (value) + { + case XLBorderStyleValues.DashDot: + return BorderStyleValues.DashDot; + + case XLBorderStyleValues.DashDotDot: + return BorderStyleValues.DashDotDot; + + case XLBorderStyleValues.Dashed: + return BorderStyleValues.Dashed; + + case XLBorderStyleValues.Dotted: + return BorderStyleValues.Dotted; + + case XLBorderStyleValues.Double: + return BorderStyleValues.Double; + + case XLBorderStyleValues.Hair: + return BorderStyleValues.Hair; + + case XLBorderStyleValues.Medium: + return BorderStyleValues.Medium; + + case XLBorderStyleValues.MediumDashDot: + return BorderStyleValues.MediumDashDot; + + case XLBorderStyleValues.MediumDashDotDot: + return BorderStyleValues.MediumDashDotDot; + + case XLBorderStyleValues.MediumDashed: + return BorderStyleValues.MediumDashed; + + case XLBorderStyleValues.None: + return BorderStyleValues.None; + + case XLBorderStyleValues.SlantDashDot: + return BorderStyleValues.SlantDashDot; + + case XLBorderStyleValues.Thick: + return BorderStyleValues.Thick; + + case XLBorderStyleValues.Thin: + return BorderStyleValues.Thin; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static HorizontalAlignmentValues ToOpenXml(this XLAlignmentHorizontalValues value) + { + switch (value) + { + case XLAlignmentHorizontalValues.Center: + return HorizontalAlignmentValues.Center; + + case XLAlignmentHorizontalValues.CenterContinuous: + return HorizontalAlignmentValues.CenterContinuous; + + case XLAlignmentHorizontalValues.Distributed: + return HorizontalAlignmentValues.Distributed; + + case XLAlignmentHorizontalValues.Fill: + return HorizontalAlignmentValues.Fill; + + case XLAlignmentHorizontalValues.General: + return HorizontalAlignmentValues.General; + + case XLAlignmentHorizontalValues.Justify: + return HorizontalAlignmentValues.Justify; + + case XLAlignmentHorizontalValues.Left: + return HorizontalAlignmentValues.Left; + + case XLAlignmentHorizontalValues.Right: + return HorizontalAlignmentValues.Right; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static VerticalAlignmentValues ToOpenXml(this XLAlignmentVerticalValues value) + { + switch (value) + { + case XLAlignmentVerticalValues.Bottom: + return VerticalAlignmentValues.Bottom; + + case XLAlignmentVerticalValues.Center: + return VerticalAlignmentValues.Center; + + case XLAlignmentVerticalValues.Distributed: + return VerticalAlignmentValues.Distributed; + + case XLAlignmentVerticalValues.Justify: + return VerticalAlignmentValues.Justify; + + case XLAlignmentVerticalValues.Top: + return VerticalAlignmentValues.Top; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static PageOrderValues ToOpenXml(this XLPageOrderValues value) + { + switch (value) + { + case XLPageOrderValues.DownThenOver: + return PageOrderValues.DownThenOver; + + case XLPageOrderValues.OverThenDown: + return PageOrderValues.OverThenDown; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static CellCommentsValues ToOpenXml(this XLShowCommentsValues value) + { + switch (value) + { + case XLShowCommentsValues.AsDisplayed: + return CellCommentsValues.AsDisplayed; + + case XLShowCommentsValues.AtEnd: + return CellCommentsValues.AtEnd; + + case XLShowCommentsValues.None: + return CellCommentsValues.None; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static PrintErrorValues ToOpenXml(this XLPrintErrorValues value) + { + switch (value) + { + case XLPrintErrorValues.Blank: + return PrintErrorValues.Blank; + + case XLPrintErrorValues.Dash: + return PrintErrorValues.Dash; + + case XLPrintErrorValues.Displayed: + return PrintErrorValues.Displayed; + + case XLPrintErrorValues.NA: + return PrintErrorValues.NA; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static CalculateModeValues ToOpenXml(this XLCalculateMode value) + { + switch (value) + { + case XLCalculateMode.Auto: + return CalculateModeValues.Auto; + + case XLCalculateMode.AutoNoTable: + return CalculateModeValues.AutoNoTable; + + case XLCalculateMode.Manual: + return CalculateModeValues.Manual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ReferenceModeValues ToOpenXml(this XLReferenceStyle value) + { + switch (value) + { + case XLReferenceStyle.R1C1: + return ReferenceModeValues.R1C1; + + case XLReferenceStyle.A1: + return ReferenceModeValues.A1; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static uint ToOpenXml(this XLAlignmentReadingOrderValues value) + { + switch (value) + { + case XLAlignmentReadingOrderValues.ContextDependent: + return 0; + + case XLAlignmentReadingOrderValues.LeftToRight: + return 1; + + case XLAlignmentReadingOrderValues.RightToLeft: + return 2; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static TotalsRowFunctionValues ToOpenXml(this XLTotalsRowFunction value) + { + switch (value) + { + case XLTotalsRowFunction.None: + return TotalsRowFunctionValues.None; + + case XLTotalsRowFunction.Sum: + return TotalsRowFunctionValues.Sum; + + case XLTotalsRowFunction.Minimum: + return TotalsRowFunctionValues.Minimum; + + case XLTotalsRowFunction.Maximum: + return TotalsRowFunctionValues.Maximum; + + case XLTotalsRowFunction.Average: + return TotalsRowFunctionValues.Average; + + case XLTotalsRowFunction.Count: + return TotalsRowFunctionValues.Count; + + case XLTotalsRowFunction.CountNumbers: + return TotalsRowFunctionValues.CountNumbers; + + case XLTotalsRowFunction.StandardDeviation: + return TotalsRowFunctionValues.StandardDeviation; + + case XLTotalsRowFunction.Variance: + return TotalsRowFunctionValues.Variance; + + case XLTotalsRowFunction.Custom: + return TotalsRowFunctionValues.Custom; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DataValidationValues ToOpenXml(this XLAllowedValues value) + { + switch (value) + { + case XLAllowedValues.AnyValue: + return DataValidationValues.None; + + case XLAllowedValues.Custom: + return DataValidationValues.Custom; + + case XLAllowedValues.Date: + return DataValidationValues.Date; + + case XLAllowedValues.Decimal: + return DataValidationValues.Decimal; + + case XLAllowedValues.List: + return DataValidationValues.List; + + case XLAllowedValues.TextLength: + return DataValidationValues.TextLength; + + case XLAllowedValues.Time: + return DataValidationValues.Time; + + case XLAllowedValues.WholeNumber: + return DataValidationValues.Whole; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DataValidationErrorStyleValues ToOpenXml(this XLErrorStyle value) + { + switch (value) + { + case XLErrorStyle.Information: + return DataValidationErrorStyleValues.Information; + + case XLErrorStyle.Warning: + return DataValidationErrorStyleValues.Warning; + + case XLErrorStyle.Stop: + return DataValidationErrorStyleValues.Stop; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DataValidationOperatorValues ToOpenXml(this XLOperator value) + { + switch (value) + { + case XLOperator.Between: + return DataValidationOperatorValues.Between; + + case XLOperator.EqualOrGreaterThan: + return DataValidationOperatorValues.GreaterThanOrEqual; + + case XLOperator.EqualOrLessThan: + return DataValidationOperatorValues.LessThanOrEqual; + + case XLOperator.EqualTo: + return DataValidationOperatorValues.Equal; + + case XLOperator.GreaterThan: + return DataValidationOperatorValues.GreaterThan; + + case XLOperator.LessThan: + return DataValidationOperatorValues.LessThan; + + case XLOperator.NotBetween: + return DataValidationOperatorValues.NotBetween; + + case XLOperator.NotEqualTo: + return DataValidationOperatorValues.NotEqual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static SheetStateValues ToOpenXml(this XLWorksheetVisibility value) + { + switch (value) + { + case XLWorksheetVisibility.Visible: + return SheetStateValues.Visible; + + case XLWorksheetVisibility.Hidden: + return SheetStateValues.Hidden; + + case XLWorksheetVisibility.VeryHidden: + return SheetStateValues.VeryHidden; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static PhoneticAlignmentValues ToOpenXml(this XLPhoneticAlignment value) + { + switch (value) + { + case XLPhoneticAlignment.Center: + return PhoneticAlignmentValues.Center; + + case XLPhoneticAlignment.Distributed: + return PhoneticAlignmentValues.Distributed; + + case XLPhoneticAlignment.Left: + return PhoneticAlignmentValues.Left; + + case XLPhoneticAlignment.NoControl: + return PhoneticAlignmentValues.NoControl; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static PhoneticValues ToOpenXml(this XLPhoneticType value) + { + switch (value) + { + case XLPhoneticType.FullWidthKatakana: + return PhoneticValues.FullWidthKatakana; + + case XLPhoneticType.HalfWidthKatakana: + return PhoneticValues.HalfWidthKatakana; + + case XLPhoneticType.Hiragana: + return PhoneticValues.Hiragana; + + case XLPhoneticType.NoConversion: + return PhoneticValues.NoConversion; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DataConsolidateFunctionValues ToOpenXml(this XLPivotSummary value) + { + switch (value) + { + case XLPivotSummary.Sum: return DataConsolidateFunctionValues.Sum; + case XLPivotSummary.Count: return DataConsolidateFunctionValues.Count; + case XLPivotSummary.Average: return DataConsolidateFunctionValues.Average; + case XLPivotSummary.Minimum: return DataConsolidateFunctionValues.Minimum; + case XLPivotSummary.Maximum: return DataConsolidateFunctionValues.Maximum; + case XLPivotSummary.Product: return DataConsolidateFunctionValues.Product; + case XLPivotSummary.CountNumbers: return DataConsolidateFunctionValues.CountNumbers; + case XLPivotSummary.StandardDeviation: return DataConsolidateFunctionValues.StandardDeviation; + case XLPivotSummary.PopulationStandardDeviation: return DataConsolidateFunctionValues.StandardDeviationP; + case XLPivotSummary.Variance: return DataConsolidateFunctionValues.Variance; + case XLPivotSummary.PopulationVariance: return DataConsolidateFunctionValues.VarianceP; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ShowDataAsValues ToOpenXml(this XLPivotCalculation value) + { + switch (value) + { + case XLPivotCalculation.Normal: return ShowDataAsValues.Normal; + case XLPivotCalculation.DifferenceFrom: return ShowDataAsValues.Difference; + case XLPivotCalculation.PercentageOf: return ShowDataAsValues.Percent; + case XLPivotCalculation.PercentageDifferenceFrom: return ShowDataAsValues.PercentageDifference; + case XLPivotCalculation.RunningTotal: return ShowDataAsValues.RunTotal; + case XLPivotCalculation.PercentageOfRow: return ShowDataAsValues.PercentOfRaw; // There's a typo in the OpenXML SDK =) + case XLPivotCalculation.PercentageOfColumn: return ShowDataAsValues.PercentOfColumn; + case XLPivotCalculation.PercentageOfTotal: return ShowDataAsValues.PercentOfTotal; + case XLPivotCalculation.Index: return ShowDataAsValues.Index; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static FilterOperatorValues ToOpenXml(this XLFilterOperator value) + { + switch (value) + { + case XLFilterOperator.Equal: return FilterOperatorValues.Equal; + case XLFilterOperator.NotEqual: return FilterOperatorValues.NotEqual; + case XLFilterOperator.GreaterThan: return FilterOperatorValues.GreaterThan; + case XLFilterOperator.EqualOrGreaterThan: return FilterOperatorValues.GreaterThanOrEqual; + case XLFilterOperator.LessThan: return FilterOperatorValues.LessThan; + case XLFilterOperator.EqualOrLessThan: return FilterOperatorValues.LessThanOrEqual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DynamicFilterValues ToOpenXml(this XLFilterDynamicType value) + { + switch (value) + { + case XLFilterDynamicType.AboveAverage: return DynamicFilterValues.AboveAverage; + case XLFilterDynamicType.BelowAverage: return DynamicFilterValues.BelowAverage; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static DateTimeGroupingValues ToOpenXml(this XLDateTimeGrouping value) + { + switch (value) + { + case XLDateTimeGrouping.Year: return DateTimeGroupingValues.Year; + case XLDateTimeGrouping.Month: return DateTimeGroupingValues.Month; + case XLDateTimeGrouping.Day: return DateTimeGroupingValues.Day; + case XLDateTimeGrouping.Hour: return DateTimeGroupingValues.Hour; + case XLDateTimeGrouping.Minute: return DateTimeGroupingValues.Minute; + case XLDateTimeGrouping.Second: return DateTimeGroupingValues.Second; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static SheetViewValues ToOpenXml(this XLSheetViewOptions value) + { + switch (value) + { + case XLSheetViewOptions.Normal: return SheetViewValues.Normal; + case XLSheetViewOptions.PageBreakPreview: return SheetViewValues.PageBreakPreview; + case XLSheetViewOptions.PageLayout: return SheetViewValues.PageLayout; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static Vml.StrokeLineStyleValues ToOpenXml(this XLLineStyle value) + { + switch (value) + { + case XLLineStyle.Single: return Vml.StrokeLineStyleValues.Single; + case XLLineStyle.ThickBetweenThin: return Vml.StrokeLineStyleValues.ThickBetweenThin; + case XLLineStyle.ThickThin: return Vml.StrokeLineStyleValues.ThickThin; + case XLLineStyle.ThinThick: return Vml.StrokeLineStyleValues.ThinThick; + case XLLineStyle.ThinThin: return Vml.StrokeLineStyleValues.ThinThin; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ConditionalFormatValues ToOpenXml(this XLConditionalFormatType value) + { + switch (value) + { + case XLConditionalFormatType.Expression: return ConditionalFormatValues.Expression; + case XLConditionalFormatType.CellIs: return ConditionalFormatValues.CellIs; + case XLConditionalFormatType.ColorScale: return ConditionalFormatValues.ColorScale; + case XLConditionalFormatType.DataBar: return ConditionalFormatValues.DataBar; + case XLConditionalFormatType.IconSet: return ConditionalFormatValues.IconSet; + case XLConditionalFormatType.Top10: return ConditionalFormatValues.Top10; + case XLConditionalFormatType.IsUnique: return ConditionalFormatValues.UniqueValues; + case XLConditionalFormatType.IsDuplicate: return ConditionalFormatValues.DuplicateValues; + case XLConditionalFormatType.ContainsText: return ConditionalFormatValues.ContainsText; + case XLConditionalFormatType.NotContainsText: return ConditionalFormatValues.NotContainsText; + case XLConditionalFormatType.StartsWith: return ConditionalFormatValues.BeginsWith; + case XLConditionalFormatType.EndsWith: return ConditionalFormatValues.EndsWith; + case XLConditionalFormatType.IsBlank: return ConditionalFormatValues.ContainsBlanks; + case XLConditionalFormatType.NotBlank: return ConditionalFormatValues.NotContainsBlanks; + case XLConditionalFormatType.IsError: return ConditionalFormatValues.ContainsErrors; + case XLConditionalFormatType.NotError: return ConditionalFormatValues.NotContainsErrors; + case XLConditionalFormatType.TimePeriod: return ConditionalFormatValues.TimePeriod; + case XLConditionalFormatType.AboveAverage: return ConditionalFormatValues.AboveAverage; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ConditionalFormatValueObjectValues ToOpenXml(this XLCFContentType value) + { + switch (value) + { + case XLCFContentType.Number: return ConditionalFormatValueObjectValues.Number; + case XLCFContentType.Percent: return ConditionalFormatValueObjectValues.Percent; + case XLCFContentType.Maximum: return ConditionalFormatValueObjectValues.Max; + case XLCFContentType.Minimum: return ConditionalFormatValueObjectValues.Min; + case XLCFContentType.Formula: return ConditionalFormatValueObjectValues.Formula; + case XLCFContentType.Percentile: return ConditionalFormatValueObjectValues.Percentile; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ConditionalFormattingOperatorValues ToOpenXml(this XLCFOperator value) + { + switch (value) + { + case XLCFOperator.LessThan: return ConditionalFormattingOperatorValues.LessThan; + case XLCFOperator.EqualOrLessThan: return ConditionalFormattingOperatorValues.LessThanOrEqual; + case XLCFOperator.Equal: return ConditionalFormattingOperatorValues.Equal; + case XLCFOperator.NotEqual: return ConditionalFormattingOperatorValues.NotEqual; + case XLCFOperator.EqualOrGreaterThan: return ConditionalFormattingOperatorValues.GreaterThanOrEqual; + case XLCFOperator.GreaterThan: return ConditionalFormattingOperatorValues.GreaterThan; + case XLCFOperator.Between: return ConditionalFormattingOperatorValues.Between; + case XLCFOperator.NotBetween: return ConditionalFormattingOperatorValues.NotBetween; + case XLCFOperator.Contains: return ConditionalFormattingOperatorValues.ContainsText; + case XLCFOperator.NotContains: return ConditionalFormattingOperatorValues.NotContains; + case XLCFOperator.StartsWith: return ConditionalFormattingOperatorValues.BeginsWith; + case XLCFOperator.EndsWith: return ConditionalFormattingOperatorValues.EndsWith; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static IconSetValues ToOpenXml(this XLIconSetStyle value) + { + switch (value) + { + case XLIconSetStyle.ThreeArrows: return IconSetValues.ThreeArrows; + case XLIconSetStyle.ThreeArrowsGray: return IconSetValues.ThreeArrowsGray; + case XLIconSetStyle.ThreeFlags: return IconSetValues.ThreeFlags; + case XLIconSetStyle.ThreeTrafficLights1: return IconSetValues.ThreeTrafficLights1; + case XLIconSetStyle.ThreeTrafficLights2: return IconSetValues.ThreeTrafficLights2; + case XLIconSetStyle.ThreeSigns: return IconSetValues.ThreeSigns; + case XLIconSetStyle.ThreeSymbols: return IconSetValues.ThreeSymbols; + case XLIconSetStyle.ThreeSymbols2: return IconSetValues.ThreeSymbols2; + case XLIconSetStyle.FourArrows: return IconSetValues.FourArrows; + case XLIconSetStyle.FourArrowsGray: return IconSetValues.FourArrowsGray; + case XLIconSetStyle.FourRedToBlack: return IconSetValues.FourRedToBlack; + case XLIconSetStyle.FourRating: return IconSetValues.FourRating; + case XLIconSetStyle.FourTrafficLights: return IconSetValues.FourTrafficLights; + case XLIconSetStyle.FiveArrows: return IconSetValues.FiveArrows; + case XLIconSetStyle.FiveArrowsGray: return IconSetValues.FiveArrowsGray; + case XLIconSetStyle.FiveRating: return IconSetValues.FiveRating; + case XLIconSetStyle.FiveQuarters: return IconSetValues.FiveQuarters; + + default: + throw new ArgumentOutOfRangeException("Not implemented value!"); + } + } + + public static TimePeriodValues ToOpenXml(this XLTimePeriod value) + { + switch (value) + { + case XLTimePeriod.Yesterday: return TimePeriodValues.Yesterday; + case XLTimePeriod.Today: return TimePeriodValues.Today; + case XLTimePeriod.Tomorrow: return TimePeriodValues.Tomorrow; + case XLTimePeriod.InTheLast7Days: return TimePeriodValues.Last7Days; + case XLTimePeriod.LastWeek: return TimePeriodValues.LastWeek; + case XLTimePeriod.ThisWeek: return TimePeriodValues.ThisWeek; + case XLTimePeriod.NextWeek: return TimePeriodValues.NextWeek; + case XLTimePeriod.LastMonth: return TimePeriodValues.LastMonth; + case XLTimePeriod.ThisMonth: return TimePeriodValues.ThisMonth; + case XLTimePeriod.NextMonth: return TimePeriodValues.NextMonth; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static ImagePartType ToOpenXml(this XLPictureFormat value) + { + return Enum.Parse(typeof(ImagePartType), value.ToString()).CastTo(); + } + + public static Xdr.EditAsValues ToOpenXml(this XLPicturePlacement value) + { + switch (value) + { + case XLPicturePlacement.FreeFloating: + return Xdr.EditAsValues.Absolute; + + case XLPicturePlacement.Move: + return Xdr.EditAsValues.OneCell; + + case XLPicturePlacement.MoveAndSize: + return Xdr.EditAsValues.TwoCell; + + default: + throw new ArgumentOutOfRangeException("Not implemented value!"); + } + } + + public static PivotAreaValues ToOpenXml(this XLPivotAreaValues value) + { + switch (value) + { + case XLPivotAreaValues.None: + return PivotAreaValues.None; + + case XLPivotAreaValues.Normal: + return PivotAreaValues.Normal; + + case XLPivotAreaValues.Data: + return PivotAreaValues.Data; + + case XLPivotAreaValues.All: + return PivotAreaValues.All; + + case XLPivotAreaValues.Origin: + return PivotAreaValues.Origin; + + case XLPivotAreaValues.Button: + return PivotAreaValues.Button; + + case XLPivotAreaValues.TopRight: + return PivotAreaValues.TopRight; + + case XLPivotAreaValues.TopEnd: + return PivotAreaValues.TopEnd; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "XLPivotAreaValues value not implemented"); + } + } + + public static X14.SparklineTypeValues ToOpenXml(this XLSparklineType value) + { + switch (value) + { + case XLSparklineType.Line: return X14.SparklineTypeValues.Line; + case XLSparklineType.Column: return X14.SparklineTypeValues.Column; + case XLSparklineType.Stacked: return X14.SparklineTypeValues.Stacked; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static X14.SparklineAxisMinMaxValues ToOpenXml(this XLSparklineAxisMinMax value) + { + switch (value) + { + case XLSparklineAxisMinMax.Automatic: return X14.SparklineAxisMinMaxValues.Individual; + case XLSparklineAxisMinMax.SameForAll: return X14.SparklineAxisMinMaxValues.Group; + case XLSparklineAxisMinMax.Custom: return X14.SparklineAxisMinMaxValues.Custom; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static X14.DisplayBlanksAsValues ToOpenXml(this XLDisplayBlanksAsValues value) + { + switch (value) + { + case XLDisplayBlanksAsValues.Interpolate: return X14.DisplayBlanksAsValues.Span; + case XLDisplayBlanksAsValues.NotPlotted: return X14.DisplayBlanksAsValues.Gap; + case XLDisplayBlanksAsValues.Zero: return X14.DisplayBlanksAsValues.Zero; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + #endregion To OpenXml + + #region To ClosedXml + + public static XLFontUnderlineValues ToClosedXml(this UnderlineValues value) + { + switch (value) + { + case UnderlineValues.Double: + return XLFontUnderlineValues.Double; + + case UnderlineValues.DoubleAccounting: + return XLFontUnderlineValues.DoubleAccounting; + + case UnderlineValues.None: + return XLFontUnderlineValues.None; + + case UnderlineValues.Single: + return XLFontUnderlineValues.Single; + + case UnderlineValues.SingleAccounting: + return XLFontUnderlineValues.SingleAccounting; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPageOrientation ToClosedXml(this OrientationValues value) + { + switch (value) + { + case OrientationValues.Default: + return XLPageOrientation.Default; + + case OrientationValues.Landscape: + return XLPageOrientation.Landscape; + + case OrientationValues.Portrait: + return XLPageOrientation.Portrait; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLFontVerticalTextAlignmentValues ToClosedXml(this VerticalAlignmentRunValues value) + { + switch (value) + { + case VerticalAlignmentRunValues.Baseline: + return XLFontVerticalTextAlignmentValues.Baseline; + + case VerticalAlignmentRunValues.Subscript: + return XLFontVerticalTextAlignmentValues.Subscript; + + case VerticalAlignmentRunValues.Superscript: + return XLFontVerticalTextAlignmentValues.Superscript; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLFillPatternValues ToClosedXml(this PatternValues value) + { + switch (value) + { + case PatternValues.DarkDown: + return XLFillPatternValues.DarkDown; + + case PatternValues.DarkGray: + return XLFillPatternValues.DarkGray; + + case PatternValues.DarkGrid: + return XLFillPatternValues.DarkGrid; + + case PatternValues.DarkHorizontal: + return XLFillPatternValues.DarkHorizontal; + + case PatternValues.DarkTrellis: + return XLFillPatternValues.DarkTrellis; + + case PatternValues.DarkUp: + return XLFillPatternValues.DarkUp; + + case PatternValues.DarkVertical: + return XLFillPatternValues.DarkVertical; + + case PatternValues.Gray0625: + return XLFillPatternValues.Gray0625; + + case PatternValues.Gray125: + return XLFillPatternValues.Gray125; + + case PatternValues.LightDown: + return XLFillPatternValues.LightDown; + + case PatternValues.LightGray: + return XLFillPatternValues.LightGray; + + case PatternValues.LightGrid: + return XLFillPatternValues.LightGrid; + + case PatternValues.LightHorizontal: + return XLFillPatternValues.LightHorizontal; + + case PatternValues.LightTrellis: + return XLFillPatternValues.LightTrellis; + + case PatternValues.LightUp: + return XLFillPatternValues.LightUp; + + case PatternValues.LightVertical: + return XLFillPatternValues.LightVertical; + + case PatternValues.MediumGray: + return XLFillPatternValues.MediumGray; + + case PatternValues.None: + return XLFillPatternValues.None; + + case PatternValues.Solid: + return XLFillPatternValues.Solid; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLBorderStyleValues ToClosedXml(this BorderStyleValues value) + { + switch (value) + { + case BorderStyleValues.DashDot: + return XLBorderStyleValues.DashDot; + + case BorderStyleValues.DashDotDot: + return XLBorderStyleValues.DashDotDot; + + case BorderStyleValues.Dashed: + return XLBorderStyleValues.Dashed; + + case BorderStyleValues.Dotted: + return XLBorderStyleValues.Dotted; + + case BorderStyleValues.Double: + return XLBorderStyleValues.Double; + + case BorderStyleValues.Hair: + return XLBorderStyleValues.Hair; + + case BorderStyleValues.Medium: + return XLBorderStyleValues.Medium; + + case BorderStyleValues.MediumDashDot: + return XLBorderStyleValues.MediumDashDot; + + case BorderStyleValues.MediumDashDotDot: + return XLBorderStyleValues.MediumDashDotDot; + + case BorderStyleValues.MediumDashed: + return XLBorderStyleValues.MediumDashed; + + case BorderStyleValues.None: + return XLBorderStyleValues.None; + + case BorderStyleValues.SlantDashDot: + return XLBorderStyleValues.SlantDashDot; + + case BorderStyleValues.Thick: + return XLBorderStyleValues.Thick; + + case BorderStyleValues.Thin: + return XLBorderStyleValues.Thin; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLAlignmentHorizontalValues ToClosedXml(this HorizontalAlignmentValues value) + { + switch (value) + { + case HorizontalAlignmentValues.Center: + return XLAlignmentHorizontalValues.Center; + + case HorizontalAlignmentValues.CenterContinuous: + return XLAlignmentHorizontalValues.CenterContinuous; + + case HorizontalAlignmentValues.Distributed: + return XLAlignmentHorizontalValues.Distributed; + + case HorizontalAlignmentValues.Fill: + return XLAlignmentHorizontalValues.Fill; + + case HorizontalAlignmentValues.General: + return XLAlignmentHorizontalValues.General; + + case HorizontalAlignmentValues.Justify: + return XLAlignmentHorizontalValues.Justify; + + case HorizontalAlignmentValues.Left: + return XLAlignmentHorizontalValues.Left; + + case HorizontalAlignmentValues.Right: + return XLAlignmentHorizontalValues.Right; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLAlignmentVerticalValues ToClosedXml(this VerticalAlignmentValues value) + { + switch (value) + { + case VerticalAlignmentValues.Bottom: + return XLAlignmentVerticalValues.Bottom; + + case VerticalAlignmentValues.Center: + return XLAlignmentVerticalValues.Center; + + case VerticalAlignmentValues.Distributed: + return XLAlignmentVerticalValues.Distributed; + + case VerticalAlignmentValues.Justify: + return XLAlignmentVerticalValues.Justify; + + case VerticalAlignmentValues.Top: + return XLAlignmentVerticalValues.Top; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPageOrderValues ToClosedXml(this PageOrderValues value) + { + switch (value) + { + case PageOrderValues.DownThenOver: + return XLPageOrderValues.DownThenOver; + + case PageOrderValues.OverThenDown: + return XLPageOrderValues.OverThenDown; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLShowCommentsValues ToClosedXml(this CellCommentsValues value) + { + switch (value) + { + case CellCommentsValues.AsDisplayed: + return XLShowCommentsValues.AsDisplayed; + + case CellCommentsValues.AtEnd: + return XLShowCommentsValues.AtEnd; + + case CellCommentsValues.None: + return XLShowCommentsValues.None; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPrintErrorValues ToClosedXml(this PrintErrorValues value) + { + switch (value) + { + case PrintErrorValues.Blank: + return XLPrintErrorValues.Blank; + + case PrintErrorValues.Dash: + return XLPrintErrorValues.Dash; + + case PrintErrorValues.Displayed: + return XLPrintErrorValues.Displayed; + + case PrintErrorValues.NA: + return XLPrintErrorValues.NA; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLCalculateMode ToClosedXml(this CalculateModeValues value) + { + switch (value) + { + case CalculateModeValues.Auto: + return XLCalculateMode.Auto; + + case CalculateModeValues.AutoNoTable: + return XLCalculateMode.AutoNoTable; + + case CalculateModeValues.Manual: + return XLCalculateMode.Manual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLReferenceStyle ToClosedXml(this ReferenceModeValues value) + { + switch (value) + { + case ReferenceModeValues.R1C1: + return XLReferenceStyle.R1C1; + + case ReferenceModeValues.A1: + return XLReferenceStyle.A1; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLAlignmentReadingOrderValues ToClosedXml(this uint value) + { + switch (value) + { + case 0: + return XLAlignmentReadingOrderValues.ContextDependent; + + case 1: + return XLAlignmentReadingOrderValues.LeftToRight; + + case 2: + return XLAlignmentReadingOrderValues.RightToLeft; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLTotalsRowFunction ToClosedXml(this TotalsRowFunctionValues value) + { + switch (value) + { + case TotalsRowFunctionValues.None: + return XLTotalsRowFunction.None; + + case TotalsRowFunctionValues.Sum: + return XLTotalsRowFunction.Sum; + + case TotalsRowFunctionValues.Minimum: + return XLTotalsRowFunction.Minimum; + + case TotalsRowFunctionValues.Maximum: + return XLTotalsRowFunction.Maximum; + + case TotalsRowFunctionValues.Average: + return XLTotalsRowFunction.Average; + + case TotalsRowFunctionValues.Count: + return XLTotalsRowFunction.Count; + + case TotalsRowFunctionValues.CountNumbers: + return XLTotalsRowFunction.CountNumbers; + + case TotalsRowFunctionValues.StandardDeviation: + return XLTotalsRowFunction.StandardDeviation; + + case TotalsRowFunctionValues.Variance: + return XLTotalsRowFunction.Variance; + + case TotalsRowFunctionValues.Custom: + return XLTotalsRowFunction.Custom; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLAllowedValues ToClosedXml(this DataValidationValues value) + { + switch (value) + { + case DataValidationValues.None: + return XLAllowedValues.AnyValue; + + case DataValidationValues.Custom: + return XLAllowedValues.Custom; + + case DataValidationValues.Date: + return XLAllowedValues.Date; + + case DataValidationValues.Decimal: + return XLAllowedValues.Decimal; + + case DataValidationValues.List: + return XLAllowedValues.List; + + case DataValidationValues.TextLength: + return XLAllowedValues.TextLength; + + case DataValidationValues.Time: + return XLAllowedValues.Time; + + case DataValidationValues.Whole: + return XLAllowedValues.WholeNumber; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLErrorStyle ToClosedXml(this DataValidationErrorStyleValues value) + { + switch (value) + { + case DataValidationErrorStyleValues.Information: + return XLErrorStyle.Information; + + case DataValidationErrorStyleValues.Warning: + return XLErrorStyle.Warning; + + case DataValidationErrorStyleValues.Stop: + return XLErrorStyle.Stop; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLOperator ToClosedXml(this DataValidationOperatorValues value) + { + switch (value) + { + case DataValidationOperatorValues.Between: + return XLOperator.Between; + + case DataValidationOperatorValues.GreaterThanOrEqual: + return XLOperator.EqualOrGreaterThan; + + case DataValidationOperatorValues.LessThanOrEqual: + return XLOperator.EqualOrLessThan; + + case DataValidationOperatorValues.Equal: + return XLOperator.EqualTo; + + case DataValidationOperatorValues.GreaterThan: + return XLOperator.GreaterThan; + + case DataValidationOperatorValues.LessThan: + return XLOperator.LessThan; + + case DataValidationOperatorValues.NotBetween: + return XLOperator.NotBetween; + + case DataValidationOperatorValues.NotEqual: + return XLOperator.NotEqualTo; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLWorksheetVisibility ToClosedXml(this SheetStateValues value) + { + switch (value) + { + case SheetStateValues.Visible: + return XLWorksheetVisibility.Visible; + + case SheetStateValues.Hidden: + return XLWorksheetVisibility.Hidden; + + case SheetStateValues.VeryHidden: + return XLWorksheetVisibility.VeryHidden; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPhoneticAlignment ToClosedXml(this PhoneticAlignmentValues value) + { + switch (value) + { + case PhoneticAlignmentValues.Center: + return XLPhoneticAlignment.Center; + + case PhoneticAlignmentValues.Distributed: + return XLPhoneticAlignment.Distributed; + + case PhoneticAlignmentValues.Left: + return XLPhoneticAlignment.Left; + + case PhoneticAlignmentValues.NoControl: + return XLPhoneticAlignment.NoControl; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPhoneticType ToClosedXml(this PhoneticValues value) + { + switch (value) + { + case PhoneticValues.FullWidthKatakana: return XLPhoneticType.FullWidthKatakana; + case PhoneticValues.HalfWidthKatakana: + return XLPhoneticType.HalfWidthKatakana; + + case PhoneticValues.Hiragana: + return XLPhoneticType.Hiragana; + + case PhoneticValues.NoConversion: + return XLPhoneticType.NoConversion; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPivotSummary ToClosedXml(this DataConsolidateFunctionValues value) + { + switch (value) + { + case DataConsolidateFunctionValues.Sum: return XLPivotSummary.Sum; + case DataConsolidateFunctionValues.Count: return XLPivotSummary.Count; + case DataConsolidateFunctionValues.Average: return XLPivotSummary.Average; + case DataConsolidateFunctionValues.Minimum: return XLPivotSummary.Minimum; + case DataConsolidateFunctionValues.Maximum: return XLPivotSummary.Maximum; + case DataConsolidateFunctionValues.Product: return XLPivotSummary.Product; + case DataConsolidateFunctionValues.CountNumbers: return XLPivotSummary.CountNumbers; + case DataConsolidateFunctionValues.StandardDeviation: return XLPivotSummary.StandardDeviation; + case DataConsolidateFunctionValues.StandardDeviationP: return XLPivotSummary.PopulationStandardDeviation; + case DataConsolidateFunctionValues.Variance: return XLPivotSummary.Variance; + case DataConsolidateFunctionValues.VarianceP: return XLPivotSummary.PopulationVariance; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPivotCalculation ToClosedXml(this ShowDataAsValues value) + { + switch (value) + { + case ShowDataAsValues.Normal: return XLPivotCalculation.Normal; + case ShowDataAsValues.Difference: return XLPivotCalculation.DifferenceFrom; + case ShowDataAsValues.Percent: return XLPivotCalculation.PercentageOf; + case ShowDataAsValues.PercentageDifference: return XLPivotCalculation.PercentageDifferenceFrom; + case ShowDataAsValues.RunTotal: return XLPivotCalculation.RunningTotal; + case ShowDataAsValues.PercentOfRaw: return XLPivotCalculation.PercentageOfRow; // There's a typo in the OpenXML SDK =) + case ShowDataAsValues.PercentOfColumn: return XLPivotCalculation.PercentageOfColumn; + case ShowDataAsValues.PercentOfTotal: return XLPivotCalculation.PercentageOfTotal; + case ShowDataAsValues.Index: return XLPivotCalculation.Index; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLFilterOperator ToClosedXml(this FilterOperatorValues value) + { + switch (value) + { + case FilterOperatorValues.Equal: return XLFilterOperator.Equal; + case FilterOperatorValues.NotEqual: return XLFilterOperator.NotEqual; + case FilterOperatorValues.GreaterThan: return XLFilterOperator.GreaterThan; + case FilterOperatorValues.LessThan: return XLFilterOperator.LessThan; + case FilterOperatorValues.GreaterThanOrEqual: return XLFilterOperator.EqualOrGreaterThan; + case FilterOperatorValues.LessThanOrEqual: return XLFilterOperator.EqualOrLessThan; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLFilterDynamicType ToClosedXml(this DynamicFilterValues value) + { + switch (value) + { + case DynamicFilterValues.AboveAverage: return XLFilterDynamicType.AboveAverage; + case DynamicFilterValues.BelowAverage: return XLFilterDynamicType.BelowAverage; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLDateTimeGrouping ToClosedXml(this DateTimeGroupingValues value) + { + switch (value) + { + case DateTimeGroupingValues.Year: return XLDateTimeGrouping.Year; + case DateTimeGroupingValues.Month: return XLDateTimeGrouping.Month; + case DateTimeGroupingValues.Day: return XLDateTimeGrouping.Day; + case DateTimeGroupingValues.Hour: return XLDateTimeGrouping.Hour; + case DateTimeGroupingValues.Minute: return XLDateTimeGrouping.Minute; + case DateTimeGroupingValues.Second: return XLDateTimeGrouping.Second; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLSheetViewOptions ToClosedXml(this SheetViewValues value) + { + switch (value) + { + case SheetViewValues.Normal: return XLSheetViewOptions.Normal; + case SheetViewValues.PageBreakPreview: return XLSheetViewOptions.PageBreakPreview; + case SheetViewValues.PageLayout: return XLSheetViewOptions.PageLayout; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLLineStyle ToClosedXml(this Vml.StrokeLineStyleValues value) + { + switch (value) + { + case Vml.StrokeLineStyleValues.Single: return XLLineStyle.Single; + case Vml.StrokeLineStyleValues.ThickBetweenThin: return XLLineStyle.ThickBetweenThin; + case Vml.StrokeLineStyleValues.ThickThin: return XLLineStyle.ThickThin; + case Vml.StrokeLineStyleValues.ThinThick: return XLLineStyle.ThinThick; + case Vml.StrokeLineStyleValues.ThinThin: return XLLineStyle.ThinThin; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLConditionalFormatType ToClosedXml(this ConditionalFormatValues value) + { + switch (value) + { + case ConditionalFormatValues.Expression: return XLConditionalFormatType.Expression; + case ConditionalFormatValues.CellIs: return XLConditionalFormatType.CellIs; + case ConditionalFormatValues.ColorScale: return XLConditionalFormatType.ColorScale; + case ConditionalFormatValues.DataBar: return XLConditionalFormatType.DataBar; + case ConditionalFormatValues.IconSet: return XLConditionalFormatType.IconSet; + case ConditionalFormatValues.Top10: return XLConditionalFormatType.Top10; + case ConditionalFormatValues.UniqueValues: return XLConditionalFormatType.IsUnique; + case ConditionalFormatValues.DuplicateValues: return XLConditionalFormatType.IsDuplicate; + case ConditionalFormatValues.ContainsText: return XLConditionalFormatType.ContainsText; + case ConditionalFormatValues.NotContainsText: return XLConditionalFormatType.NotContainsText; + case ConditionalFormatValues.BeginsWith: return XLConditionalFormatType.StartsWith; + case ConditionalFormatValues.EndsWith: return XLConditionalFormatType.EndsWith; + case ConditionalFormatValues.ContainsBlanks: return XLConditionalFormatType.IsBlank; + case ConditionalFormatValues.NotContainsBlanks: return XLConditionalFormatType.NotBlank; + case ConditionalFormatValues.ContainsErrors: return XLConditionalFormatType.IsError; + case ConditionalFormatValues.NotContainsErrors: return XLConditionalFormatType.NotError; + case ConditionalFormatValues.TimePeriod: return XLConditionalFormatType.TimePeriod; + case ConditionalFormatValues.AboveAverage: return XLConditionalFormatType.AboveAverage; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLCFContentType ToClosedXml(this ConditionalFormatValueObjectValues value) + { + switch (value) + { + case ConditionalFormatValueObjectValues.Number: return XLCFContentType.Number; + case ConditionalFormatValueObjectValues.Percent: return XLCFContentType.Percent; + case ConditionalFormatValueObjectValues.Max: return XLCFContentType.Maximum; + case ConditionalFormatValueObjectValues.Min: return XLCFContentType.Minimum; + case ConditionalFormatValueObjectValues.Formula: return XLCFContentType.Formula; + case ConditionalFormatValueObjectValues.Percentile: return XLCFContentType.Percentile; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLCFOperator ToClosedXml(this ConditionalFormattingOperatorValues value) + { + switch (value) + { + case ConditionalFormattingOperatorValues.LessThan: return XLCFOperator.LessThan; + case ConditionalFormattingOperatorValues.LessThanOrEqual: return XLCFOperator.EqualOrLessThan; + case ConditionalFormattingOperatorValues.Equal: return XLCFOperator.Equal; + case ConditionalFormattingOperatorValues.NotEqual: return XLCFOperator.NotEqual; + case ConditionalFormattingOperatorValues.GreaterThanOrEqual: return XLCFOperator.EqualOrGreaterThan; + case ConditionalFormattingOperatorValues.GreaterThan: return XLCFOperator.GreaterThan; + case ConditionalFormattingOperatorValues.Between: return XLCFOperator.Between; + case ConditionalFormattingOperatorValues.NotBetween: return XLCFOperator.NotBetween; + case ConditionalFormattingOperatorValues.ContainsText: return XLCFOperator.Contains; + case ConditionalFormattingOperatorValues.NotContains: return XLCFOperator.NotContains; + case ConditionalFormattingOperatorValues.BeginsWith: return XLCFOperator.StartsWith; + case ConditionalFormattingOperatorValues.EndsWith: return XLCFOperator.EndsWith; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLIconSetStyle ToClosedXml(this IconSetValues value) + { + switch (value) + { + case IconSetValues.ThreeArrows: return XLIconSetStyle.ThreeArrows; + case IconSetValues.ThreeArrowsGray: return XLIconSetStyle.ThreeArrowsGray; + case IconSetValues.ThreeFlags: return XLIconSetStyle.ThreeFlags; + case IconSetValues.ThreeTrafficLights1: return XLIconSetStyle.ThreeTrafficLights1; + case IconSetValues.ThreeTrafficLights2: return XLIconSetStyle.ThreeTrafficLights2; + case IconSetValues.ThreeSigns: return XLIconSetStyle.ThreeSigns; + case IconSetValues.ThreeSymbols: return XLIconSetStyle.ThreeSymbols; + case IconSetValues.ThreeSymbols2: return XLIconSetStyle.ThreeSymbols2; + case IconSetValues.FourArrows: return XLIconSetStyle.FourArrows; + case IconSetValues.FourArrowsGray: return XLIconSetStyle.FourArrowsGray; + case IconSetValues.FourRedToBlack: return XLIconSetStyle.FourRedToBlack; + case IconSetValues.FourRating: return XLIconSetStyle.FourRating; + case IconSetValues.FourTrafficLights: return XLIconSetStyle.FourTrafficLights; + case IconSetValues.FiveArrows: return XLIconSetStyle.FiveArrows; + case IconSetValues.FiveArrowsGray: return XLIconSetStyle.FiveArrowsGray; + case IconSetValues.FiveRating: return XLIconSetStyle.FiveRating; + case IconSetValues.FiveQuarters: return XLIconSetStyle.FiveQuarters; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLTimePeriod ToClosedXml(this TimePeriodValues value) + { + switch (value) + { + case TimePeriodValues.Yesterday: return XLTimePeriod.Yesterday; + case TimePeriodValues.Today: return XLTimePeriod.Today; + case TimePeriodValues.Tomorrow: return XLTimePeriod.Tomorrow; + case TimePeriodValues.Last7Days: return XLTimePeriod.InTheLast7Days; + case TimePeriodValues.LastWeek: return XLTimePeriod.LastWeek; + case TimePeriodValues.ThisWeek: return XLTimePeriod.ThisWeek; + case TimePeriodValues.NextWeek: return XLTimePeriod.NextWeek; + case TimePeriodValues.LastMonth: return XLTimePeriod.LastMonth; + case TimePeriodValues.ThisMonth: return XLTimePeriod.ThisMonth; + case TimePeriodValues.NextMonth: return XLTimePeriod.NextMonth; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPictureFormat ToClosedXml(this ImagePartType value) + { + return Enum.Parse(typeof(XLPictureFormat), value.ToString()).CastTo(); + } + + public static XLPicturePlacement ToClosedXml(this Xdr.EditAsValues value) + { + switch (value) + { + case Xdr.EditAsValues.Absolute: + return XLPicturePlacement.FreeFloating; + + case Xdr.EditAsValues.OneCell: + return XLPicturePlacement.Move; + + case Xdr.EditAsValues.TwoCell: + return XLPicturePlacement.MoveAndSize; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLPivotAreaValues ToClosedXml(this PivotAreaValues value) + { + switch (value) + { + case PivotAreaValues.None: + return XLPivotAreaValues.None; + + case PivotAreaValues.Normal: + return XLPivotAreaValues.Normal; + + case PivotAreaValues.Data: + return XLPivotAreaValues.Data; + + case PivotAreaValues.All: + return XLPivotAreaValues.All; + + case PivotAreaValues.Origin: + return XLPivotAreaValues.Origin; + + case PivotAreaValues.Button: + return XLPivotAreaValues.Button; + + case PivotAreaValues.TopRight: + return XLPivotAreaValues.TopRight; + + case PivotAreaValues.TopEnd: + return XLPivotAreaValues.TopEnd; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "PivotAreaValues value not implemented"); + } + } + + public static XLSparklineType ToClosedXml(this X14.SparklineTypeValues value) + { + switch (value) + { + case X14.SparklineTypeValues.Line: return XLSparklineType.Line; + case X14.SparklineTypeValues.Column: return XLSparklineType.Column; + case X14.SparklineTypeValues.Stacked: return XLSparklineType.Stacked; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLSparklineAxisMinMax ToClosedXml(this X14.SparklineAxisMinMaxValues value) + { + switch (value) + { + case X14.SparklineAxisMinMaxValues.Individual: return XLSparklineAxisMinMax.Automatic; + case X14.SparklineAxisMinMaxValues.Group: return XLSparklineAxisMinMax.SameForAll; + case X14.SparklineAxisMinMaxValues.Custom: return XLSparklineAxisMinMax.Custom; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + public static XLDisplayBlanksAsValues ToClosedXml(this X14.DisplayBlanksAsValues value) + { + switch (value) + { + case X14.DisplayBlanksAsValues.Span: return XLDisplayBlanksAsValues.Interpolate; + case X14.DisplayBlanksAsValues.Gap: return XLDisplayBlanksAsValues.NotPlotted; + case X14.DisplayBlanksAsValues.Zero: return XLDisplayBlanksAsValues.Zero; + + default: + throw new ArgumentOutOfRangeException(nameof(value), "Not implemented value!"); + } + } + + #endregion To ClosedXml + } +} diff --git a/ClosedXML/Excel/Exceptions/ClosedXMLException.cs b/ClosedXML/Excel/Exceptions/ClosedXMLException.cs new file mode 100644 index 0000000..3a00816 --- /dev/null +++ b/ClosedXML/Excel/Exceptions/ClosedXMLException.cs @@ -0,0 +1,19 @@ +using System; + +namespace ClosedXML.Excel.Exceptions +{ + public abstract class ClosedXMLException : Exception + { + protected ClosedXMLException() + : base() + { } + + protected ClosedXMLException(String message) + : base(message) + { } + + protected ClosedXMLException(String message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/ClosedXML/Excel/Exceptions/EmptyTableException.cs b/ClosedXML/Excel/Exceptions/EmptyTableException.cs new file mode 100644 index 0000000..1f657e2 --- /dev/null +++ b/ClosedXML/Excel/Exceptions/EmptyTableException.cs @@ -0,0 +1,19 @@ +using System; + +namespace ClosedXML.Excel.Exceptions +{ + public class EmptyTableException : ClosedXMLException + { + public EmptyTableException() + : base() + { } + + public EmptyTableException(String message) + : base(message) + { } + + public EmptyTableException(String message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/ClosedXML/Excel/Hyperlinks/IXLHyperlinks.cs b/ClosedXML/Excel/Hyperlinks/IXLHyperlinks.cs new file mode 100644 index 0000000..a569e49 --- /dev/null +++ b/ClosedXML/Excel/Hyperlinks/IXLHyperlinks.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLHyperlinks: IEnumerable + { + void Add(XLHyperlink hyperlink); + void Delete(XLHyperlink hyperlink); + void Delete(IXLAddress address); + + } +} diff --git a/ClosedXML/Excel/Hyperlinks/XLHyperlink_Internal.cs b/ClosedXML/Excel/Hyperlinks/XLHyperlink_Internal.cs new file mode 100644 index 0000000..3705568 --- /dev/null +++ b/ClosedXML/Excel/Hyperlinks/XLHyperlink_Internal.cs @@ -0,0 +1,66 @@ +using System; + +namespace ClosedXML.Excel +{ + public partial class XLHyperlink + { + internal XLHyperlink() + { + + } + + internal XLHyperlink(XLHyperlink hyperlink) + { + _externalAddress = hyperlink._externalAddress; + _internalAddress = hyperlink._internalAddress; + Tooltip = hyperlink.Tooltip; + IsExternal = hyperlink.IsExternal; + } + + internal void SetValues(String address, String tooltip) + { + Tooltip = tooltip; + if (address[0] == '.') + { + _externalAddress = new Uri(address, UriKind.Relative); + IsExternal = true; + } + else + { + if (Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) + { + _externalAddress = uri; + IsExternal = true; + } + else + { + _internalAddress = address; + IsExternal = false; + } + } + } + + internal void SetValues(Uri uri, String tooltip) + { + Tooltip = tooltip; + _externalAddress = uri; + IsExternal = true; + } + + internal void SetValues(IXLCell cell, String tooltip) + { + Tooltip = tooltip; + _internalAddress = cell.Address.ToString(XLReferenceStyle.A1, true); + IsExternal = false; + } + + internal void SetValues(IXLRangeBase range, String tooltip) + { + Tooltip = tooltip; + _internalAddress = range.RangeAddress.ToString(XLReferenceStyle.A1, true); + IsExternal = false; + } + + internal XLWorksheet Worksheet { get; set; } + } +} diff --git a/ClosedXML/Excel/Hyperlinks/XLHyperlink_public.cs b/ClosedXML/Excel/Hyperlinks/XLHyperlink_public.cs new file mode 100644 index 0000000..cbc96f5 --- /dev/null +++ b/ClosedXML/Excel/Hyperlinks/XLHyperlink_public.cs @@ -0,0 +1,110 @@ +using ClosedXML.Extensions; +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + public partial class XLHyperlink + { + private Uri _externalAddress; + private String _internalAddress; + + public XLHyperlink(String address) + { + SetValues(address, String.Empty); + } + + public XLHyperlink(String address, String tooltip) + { + SetValues(address, tooltip); + } + + public XLHyperlink(IXLCell cell) + { + SetValues(cell, String.Empty); + } + + public XLHyperlink(IXLCell cell, String tooltip) + { + SetValues(cell, tooltip); + } + + public XLHyperlink(IXLRangeBase range) + { + SetValues(range, String.Empty); + } + + public XLHyperlink(IXLRangeBase range, String tooltip) + { + SetValues(range, tooltip); + } + + public XLHyperlink(Uri uri) + { + SetValues(uri, String.Empty); + } + + public XLHyperlink(Uri uri, String tooltip) + { + SetValues(uri, tooltip); + } + + public Boolean IsExternal { get; set; } + + public Uri ExternalAddress + { + get + { + return IsExternal ? _externalAddress : null; + } + set + { + _externalAddress = value; + IsExternal = true; + } + } + + public IXLCell Cell { get; internal set; } + + public String InternalAddress + { + get + { + if (IsExternal) + return null; + if (_internalAddress.Contains('!')) + { + return _internalAddress[0] != '\'' + ? String.Concat( + _internalAddress + .Substring(0, _internalAddress.IndexOf('!')) + .EscapeSheetName(), + '!', + _internalAddress.Substring(_internalAddress.IndexOf('!') + 1)) + : _internalAddress; + } + return String.Concat( + Worksheet.Name.EscapeSheetName(), + '!', + _internalAddress); + } + set + { + _internalAddress = value; + IsExternal = false; + } + } + + public String Tooltip { get; set; } + + public void Delete() + { + if (Cell == null) return; + Worksheet.Hyperlinks.Delete(Cell.Address); + if (Cell.Style.Font.FontColor.Equals(XLColor.FromTheme(XLThemeColor.Hyperlink))) + Cell.Style.Font.FontColor = Worksheet.StyleValue.Font.FontColor; + + Cell.Style.Font.Underline = Worksheet.StyleValue.Font.Underline; + } + } +} diff --git a/ClosedXML/Excel/Hyperlinks/XLHyperlinks.cs b/ClosedXML/Excel/Hyperlinks/XLHyperlinks.cs new file mode 100644 index 0000000..6ba4cfc --- /dev/null +++ b/ClosedXML/Excel/Hyperlinks/XLHyperlinks.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLHyperlinks: IXLHyperlinks + { + private Dictionary hyperlinks = new Dictionary(); + public IEnumerator GetEnumerator() + { + return hyperlinks.Values.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(XLHyperlink hyperlink) + { + hyperlinks.Add(hyperlink.Cell.Address, hyperlink); + } + + public void Delete(XLHyperlink hyperlink) + { + hyperlinks.Remove(hyperlink.Cell.Address); + } + + public void Delete(IXLAddress address) + { + hyperlinks.Remove(address); + } + + } +} diff --git a/ClosedXML/Excel/IXLFileSharing.cs b/ClosedXML/Excel/IXLFileSharing.cs new file mode 100644 index 0000000..d75c18e --- /dev/null +++ b/ClosedXML/Excel/IXLFileSharing.cs @@ -0,0 +1,17 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLFileSharing + { + //String AlgorithmName { get; set; } + //Byte[] HashValue { get; set; } + Boolean ReadOnlyRecommended { get; set; } + + //Byte[] ReservationPassword { get; set; } + //Byte[] SaltValue { get; set; } + //Int32 SpinCount { get; set; } + String UserName { get; set; } + } +} diff --git a/ClosedXML/Excel/IXLOutline.cs b/ClosedXML/Excel/IXLOutline.cs new file mode 100644 index 0000000..33b4711 --- /dev/null +++ b/ClosedXML/Excel/IXLOutline.cs @@ -0,0 +1,11 @@ + +namespace ClosedXML.Excel +{ + public enum XLOutlineSummaryVLocation { Top, Bottom }; + public enum XLOutlineSummaryHLocation { Left, Right }; + public interface IXLOutline + { + XLOutlineSummaryVLocation SummaryVLocation { get; set; } + XLOutlineSummaryHLocation SummaryHLocation { get; set; } + } +} diff --git a/ClosedXML/Excel/IXLSheetView.cs b/ClosedXML/Excel/IXLSheetView.cs new file mode 100644 index 0000000..7a7f14e --- /dev/null +++ b/ClosedXML/Excel/IXLSheetView.cs @@ -0,0 +1,78 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public enum XLSheetViewOptions { Normal, PageBreakPreview, PageLayout } + + public interface IXLSheetView + { + /// + /// Gets or sets the column after which the horizontal split should take place. + /// + Int32 SplitColumn { get; set; } + + /// + /// Gets or sets the row after which the vertical split should take place. + /// + Int32 SplitRow { get; set; } + + /// + /// Gets or sets the location of the top left visible cell + /// + /// + /// The scroll position's top left cell. + /// + IXLAddress TopLeftCellAddress { get; set; } + + XLSheetViewOptions View { get; set; } + + IXLWorksheet Worksheet { get; } + + /// + /// Window zoom magnification for current view representing percent values. Horizontal and vertical scale together. + /// + /// Representing percent values ranging from 10 to 400. + Int32 ZoomScale { get; set; } + + /// + /// Zoom magnification to use when in normal view. Horizontal and vertical scale together + /// + /// Representing percent values ranging from 10 to 400. + Int32 ZoomScaleNormal { get; set; } + + /// + /// Zoom magnification to use when in page layout view. Horizontal and vertical scale together. + /// + /// Representing percent values ranging from 10 to 400. + Int32 ZoomScalePageLayoutView { get; set; } + + /// + /// Zoom magnification to use when in page break preview. Horizontal and vertical scale together. + /// + /// Representing percent values ranging from 10 to 400. + Int32 ZoomScaleSheetLayoutView { get; set; } + + /// + /// Freezes the specified rows and columns. + /// + /// The rows to freeze. + /// The columns to freeze. + void Freeze(Int32 rows, Int32 columns); + + /// + /// Freezes the left X columns. + /// + /// The columns to freeze. + void FreezeColumns(Int32 columns); + + //Boolean FreezePanes { get; set; } + /// + /// Freezes the top X rows. + /// + /// The rows to freeze. + void FreezeRows(Int32 rows); + + IXLSheetView SetView(XLSheetViewOptions value); + } +} diff --git a/ClosedXML/Excel/IXLTheme.cs b/ClosedXML/Excel/IXLTheme.cs new file mode 100644 index 0000000..b506e22 --- /dev/null +++ b/ClosedXML/Excel/IXLTheme.cs @@ -0,0 +1,20 @@ +namespace ClosedXML.Excel +{ + public interface IXLTheme + { + XLColor Background1 { get; set; } + XLColor Text1 { get; set; } + XLColor Background2 { get; set; } + XLColor Text2 { get; set; } + XLColor Accent1 { get; set; } + XLColor Accent2 { get; set; } + XLColor Accent3 { get; set; } + XLColor Accent4 { get; set; } + XLColor Accent5 { get; set; } + XLColor Accent6 { get; set; } + XLColor Hyperlink { get; set; } + XLColor FollowedHyperlink { get; set; } + + XLColor ResolveThemeColor(XLThemeColor themeColor); + } +} diff --git a/ClosedXML/Excel/IXLWorkbook.cs b/ClosedXML/Excel/IXLWorkbook.cs new file mode 100644 index 0000000..49f84af --- /dev/null +++ b/ClosedXML/Excel/IXLWorkbook.cs @@ -0,0 +1,248 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; + +namespace ClosedXML.Excel +{ + public interface IXLWorkbook : IXLProtectable, IDisposable + { + String Author { get; set; } + + /// + /// Gets or sets the workbook's calculation mode. + /// + XLCalculateMode CalculateMode { get; set; } + + Boolean CalculationOnSave { get; set; } + + /// + /// Gets or sets the default column width for the workbook. + /// All new worksheets will use this column width. + /// + Double ColumnWidth { get; set; } + + IXLCustomProperties CustomProperties { get; } + + Boolean DefaultRightToLeft { get; } + + Boolean DefaultShowFormulas { get; } + + Boolean DefaultShowGridLines { get; } + + Boolean DefaultShowOutlineSymbols { get; } + + Boolean DefaultShowRowColHeaders { get; } + + Boolean DefaultShowRuler { get; } + + Boolean DefaultShowWhiteSpace { get; } + + Boolean DefaultShowZeros { get; } + + IXLFileSharing FileSharing { get; } + + Boolean ForceFullCalculation { get; set; } + + Boolean FullCalculationOnLoad { get; set; } + + Boolean FullPrecision { get; set; } + + //Boolean IsPasswordProtected { get; } + + //Boolean IsProtected { get; } + + Boolean LockStructure { get; set; } + + Boolean LockWindows { get; set; } + + /// + /// Gets an object to manipulate this workbook's named ranges. + /// + IXLNamedRanges NamedRanges { get; } + + /// + /// Gets or sets the default outline options for the workbook. + /// All new worksheets will use these outline options. + /// + IXLOutline Outline { get; set; } + + /// + /// Gets or sets the default page options for the workbook. + /// All new worksheets will use these page options. + /// + IXLPageSetup PageOptions { get; set; } + + /// + /// Gets or sets the workbook's properties. + /// + XLWorkbookProperties Properties { get; set; } + + /// + /// Gets or sets the workbook's reference style. + /// + XLReferenceStyle ReferenceStyle { get; set; } + + Boolean RightToLeft { get; set; } + + /// + /// Gets or sets the default row height for the workbook. + /// All new worksheets will use this row height. + /// + Double RowHeight { get; set; } + + Boolean ShowFormulas { get; set; } + + Boolean ShowGridLines { get; set; } + + Boolean ShowOutlineSymbols { get; set; } + + Boolean ShowRowColHeaders { get; set; } + + Boolean ShowRuler { get; set; } + + Boolean ShowWhiteSpace { get; set; } + + Boolean ShowZeros { get; set; } + + /// + /// Gets or sets the default style for the workbook. + /// All new worksheets will use this style. + /// + IXLStyle Style { get; set; } + + /// + /// Gets an object to manipulate this workbook's theme. + /// + IXLTheme Theme { get; } + + Boolean Use1904DateSystem { get; set; } + + /// + /// Gets an object to manipulate the worksheets. + /// + IXLWorksheets Worksheets { get; } + + IXLWorksheet AddWorksheet(); + + IXLWorksheet AddWorksheet(Int32 position); + + IXLWorksheet AddWorksheet(String sheetName); + + IXLWorksheet AddWorksheet(String sheetName, Int32 position); + + IXLWorksheet AddWorksheet(DataTable dataTable); + + void AddWorksheet(DataSet dataSet); + + void AddWorksheet(IXLWorksheet worksheet); + + IXLWorksheet AddWorksheet(DataTable dataTable, String sheetName); + + IXLCell Cell(String namedCell); + + IXLCells Cells(String namedCells); + + IXLCustomProperty CustomProperty(String name); + + Object Evaluate(String expression); + + IXLCells FindCells(Func predicate); + + IXLColumns FindColumns(Func predicate); + + IXLRows FindRows(Func predicate); + + IXLNamedRange NamedRange(String rangeName); + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + IXLWorkbookProtection Protect(Boolean lockStructure, Boolean lockWindows, String password); + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + IXLWorkbookProtection Protect(Boolean lockStructure); + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + IXLWorkbookProtection Protect(Boolean lockStructure, Boolean lockWindows); + + IXLRange Range(String range); + + IXLRange RangeFromFullAddress(String rangeAddress, out IXLWorksheet ws); + + IXLRanges Ranges(String ranges); + + /// + /// Force recalculation of all cell formulas. + /// + void RecalculateAllFormulas(); + + /// + /// Saves the current workbook. + /// + void Save(); + + /// + /// Saves the current workbook and optionally performs validation + /// + void Save(Boolean validate, Boolean evaluateFormulae = false); + + void Save(SaveOptions options); + + /// + /// Saves the current workbook to a file. + /// + void SaveAs(String file); + + /// + /// Saves the current workbook to a file and optionally validates it. + /// + void SaveAs(String file, Boolean validate, Boolean evaluateFormulae = false); + + void SaveAs(String file, SaveOptions options); + + /// + /// Saves the current workbook to a stream. + /// + void SaveAs(Stream stream); + + /// + /// Saves the current workbook to a stream and optionally validates it. + /// + void SaveAs(Stream stream, Boolean validate, Boolean evaluateFormulae = false); + + void SaveAs(Stream stream, SaveOptions options); + + /// + /// Searches the cells' contents for a given piece of text + /// + /// The search text. + /// The compare options. + /// if set to true search formulae instead of cell values. + /// + IEnumerable Search(String searchText, CompareOptions compareOptions = CompareOptions.Ordinal, Boolean searchFormulae = false); + + XLWorkbook SetLockStructure(Boolean value); + + XLWorkbook SetLockWindows(Boolean value); + + XLWorkbook SetUse1904DateSystem(); + + XLWorkbook SetUse1904DateSystem(Boolean value); + + /// + /// Gets the Excel table of the given name + /// + /// Name of the table to return. + /// One of the enumeration values that specifies how the strings will be compared. + /// The table with given name + /// If no tables with this name could be found in the workbook. + IXLTable Table(String tableName, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase); + + Boolean TryGetWorksheet(String name, out IXLWorksheet worksheet); + + IXLWorksheet Worksheet(String name); + + IXLWorksheet Worksheet(Int32 position); + } +} diff --git a/ClosedXML/Excel/IXLWorksheet.cs b/ClosedXML/Excel/IXLWorksheet.cs new file mode 100644 index 0000000..0d57d7c --- /dev/null +++ b/ClosedXML/Excel/IXLWorksheet.cs @@ -0,0 +1,483 @@ +using ClosedXML.Excel.Drawings; +using System; +using System.Drawing; +using System.IO; + +namespace ClosedXML.Excel +{ + public enum XLWorksheetVisibility { Visible, Hidden, VeryHidden } + + public interface IXLWorksheet : IXLRangeBase, IXLProtectable + { + /// + /// Gets the workbook that contains this worksheet + /// + XLWorkbook Workbook { get; } + + /// + /// Gets or sets the default column width for this worksheet. + /// + Double ColumnWidth { get; set; } + + /// + /// Gets or sets the default row height for this worksheet. + /// + Double RowHeight { get; set; } + + /// + /// Gets or sets the name (caption) of this worksheet. + /// + String Name { get; set; } + + /// + /// Gets or sets the position of the sheet. + /// When setting the Position all other sheets' positions are shifted accordingly. + /// + Int32 Position { get; set; } + + /// + /// Gets an object to manipulate the sheet's print options. + /// + IXLPageSetup PageSetup { get; } + + /// + /// Gets an object to manipulate the Outline levels. + /// + IXLOutline Outline { get; } + + /// + /// Gets the first row of the worksheet. + /// + IXLRow FirstRow(); + + /// + /// Gets the first row of the worksheet that contains a cell with a value. + /// Formatted empty cells do not count. + /// + IXLRow FirstRowUsed(); + + /// + /// Gets the first row of the worksheet that contains a cell with a value. + /// + /// If set to true formatted empty cells will count as used. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRow FirstRowUsed(Boolean includeFormats); + + IXLRow FirstRowUsed(XLCellsUsedOptions options); + + /// + /// Gets the last row of the worksheet. + /// + IXLRow LastRow(); + + /// + /// Gets the last row of the worksheet that contains a cell with a value. + /// + IXLRow LastRowUsed(); + + /// + /// Gets the last row of the worksheet that contains a cell with a value. + /// + /// If set to true formatted empty cells will count as used. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRow LastRowUsed(Boolean includeFormats); + + IXLRow LastRowUsed(XLCellsUsedOptions options); + + /// + /// Gets the first column of the worksheet. + /// + IXLColumn FirstColumn(); + + /// + /// Gets the first column of the worksheet that contains a cell with a value. + /// + IXLColumn FirstColumnUsed(); + + /// + /// Gets the first column of the worksheet that contains a cell with a value. + /// + /// If set to true formatted empty cells will count as used. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLColumn FirstColumnUsed(Boolean includeFormats); + + IXLColumn FirstColumnUsed(XLCellsUsedOptions options); + + /// + /// Gets the last column of the worksheet. + /// + IXLColumn LastColumn(); + + /// + /// Gets the last column of the worksheet that contains a cell with a value. + /// + IXLColumn LastColumnUsed(); + + /// + /// Gets the last column of the worksheet that contains a cell with a value. + /// + /// If set to true formatted empty cells will count as used. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLColumn LastColumnUsed(Boolean includeFormats); + + IXLColumn LastColumnUsed(XLCellsUsedOptions options); + + /// + /// Gets a collection of all columns in this worksheet. + /// + IXLColumns Columns(); + + /// + /// Gets a collection of the specified columns in this worksheet, separated by commas. + /// e.g. Columns("G:H"), Columns("10:11,13:14"), Columns("P:Q,S:T"), Columns("V") + /// + /// The columns to return. + IXLColumns Columns(String columns); + + /// + /// Gets a collection of the specified columns in this worksheet. + /// + /// The first column to return. + /// The last column to return. + IXLColumns Columns(String firstColumn, String lastColumn); + + /// + /// Gets a collection of the specified columns in this worksheet. + /// + /// The first column to return. + /// The last column to return. + IXLColumns Columns(Int32 firstColumn, Int32 lastColumn); + + /// + /// Gets a collection of all rows in this worksheet. + /// + IXLRows Rows(); + + /// + /// Gets a collection of the specified rows in this worksheet, separated by commas. + /// e.g. Rows("4:5"), Rows("7:8,10:11"), Rows("13") + /// + /// The rows to return. + IXLRows Rows(String rows); + + /// + /// Gets a collection of the specified rows in this worksheet. + /// + /// The first row to return. + /// The last row to return. + /// + IXLRows Rows(Int32 firstRow, Int32 lastRow); + + /// + /// Gets the specified row of the worksheet. + /// + /// The worksheet's row. + IXLRow Row(Int32 row); + + /// + /// Gets the specified column of the worksheet. + /// + /// The worksheet's column. + IXLColumn Column(Int32 column); + + /// + /// Gets the specified column of the worksheet. + /// + /// The worksheet's column. + IXLColumn Column(String column); + + /// + /// Gets the cell at the specified row and column. + /// + /// The cell's row. + /// The cell's column. + IXLCell Cell(int row, int column); + + /// Gets the cell at the specified address. + /// The cell address in the worksheet. + IXLCell Cell(string cellAddressInRange); + + /// + /// Gets the cell at the specified row and column. + /// + /// The cell's row. + /// The cell's column. + IXLCell Cell(int row, string column); + + /// Gets the cell at the specified address. + /// The cell address in the worksheet. + IXLCell Cell(IXLAddress cellAddressInRange); + + /// + /// Returns the specified range. + /// + /// The range boundaries. + IXLRange Range(IXLRangeAddress rangeAddress); + + /// Returns the specified range. + /// e.g. Range("A1"), Range("A1:C2") + /// The range boundaries. + IXLRange Range(string rangeAddress); + + /// Returns the specified range. + /// The first cell in the range. + /// The last cell in the range. + IXLRange Range(IXLCell firstCell, IXLCell lastCell); + + /// Returns the specified range. + /// The first cell address in the worksheet. + /// The last cell address in the worksheet. + IXLRange Range(string firstCellAddress, string lastCellAddress); + + /// Returns the specified range. + /// The first cell address in the worksheet. + /// The last cell address in the worksheet. + IXLRange Range(IXLAddress firstCellAddress, IXLAddress lastCellAddress); + + /// Returns a collection of ranges, separated by commas. + /// e.g. Ranges("A1"), Ranges("A1:C2"), Ranges("A1:B2,D1:D4") + /// The ranges to return. + IXLRanges Ranges(string ranges); + + /// Returns the specified range. + /// The first cell's row of the range to return. + /// The first cell's column of the range to return. + /// The last cell's row of the range to return. + /// The last cell's column of the range to return. + /// . + IXLRange Range(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn); + + /// Gets the number of rows in this worksheet. + int RowCount(); + + /// Gets the number of columns in this worksheet. + int ColumnCount(); + + /// + /// Collapses all outlined rows. + /// + IXLWorksheet CollapseRows(); + + /// + /// Collapses all outlined columns. + /// + IXLWorksheet CollapseColumns(); + + /// + /// Expands all outlined rows. + /// + IXLWorksheet ExpandRows(); + + /// + /// Expands all outlined columns. + /// + IXLWorksheet ExpandColumns(); + + /// + /// Collapses the outlined rows of the specified level. + /// + /// The outline level. + IXLWorksheet CollapseRows(Int32 outlineLevel); + + /// + /// Collapses the outlined columns of the specified level. + /// + /// The outline level. + IXLWorksheet CollapseColumns(Int32 outlineLevel); + + /// + /// Expands the outlined rows of the specified level. + /// + /// The outline level. + IXLWorksheet ExpandRows(Int32 outlineLevel); + + /// + /// Expands the outlined columns of the specified level. + /// + /// The outline level. + IXLWorksheet ExpandColumns(Int32 outlineLevel); + + /// + /// Deletes this worksheet. + /// + void Delete(); + + /// + /// Gets an object to manage this worksheet's named ranges. + /// + IXLNamedRanges NamedRanges { get; } + + /// + /// Gets the specified named range. + /// + /// Name of the range. + IXLNamedRange NamedRange(String rangeName); + + /// + /// Gets an object to manage how the worksheet is going to displayed by Excel. + /// + IXLSheetView SheetView { get; } + + /// + /// Gets the Excel table of the given index + /// + /// Index of the table to return + IXLTable Table(Int32 index); + + /// + /// Gets the Excel table of the given name + /// + /// Name of the table to return + IXLTable Table(String name); + + /// + /// Gets an object to manage this worksheet's Excel tables + /// + IXLTables Tables { get; } + + /// + /// Copies the + /// + /// + /// + IXLWorksheet CopyTo(String newSheetName); + + IXLWorksheet CopyTo(String newSheetName, Int32 position); + + IXLWorksheet CopyTo(XLWorkbook workbook, String newSheetName); + + IXLWorksheet CopyTo(XLWorkbook workbook, String newSheetName, Int32 position); + + IXLRange RangeUsed(); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRange RangeUsed(bool includeFormats); + + IXLRange RangeUsed(XLCellsUsedOptions options); + + IXLDataValidations DataValidations { get; } + + XLWorksheetVisibility Visibility { get; set; } + + IXLWorksheet Hide(); + + IXLWorksheet Unhide(); + + IXLSortElements SortRows { get; } + + IXLSortElements SortColumns { get; } + + IXLRange Sort(); + + IXLRange Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRange Sort(Int32 columnToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRange SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + //IXLCharts Charts { get; } + + Boolean ShowFormulas { get; set; } + + Boolean ShowGridLines { get; set; } + + Boolean ShowOutlineSymbols { get; set; } + + Boolean ShowRowColHeaders { get; set; } + + Boolean ShowRuler { get; set; } + + Boolean ShowWhiteSpace { get; set; } + + Boolean ShowZeros { get; set; } + + IXLWorksheet SetShowFormulas(); IXLWorksheet SetShowFormulas(Boolean value); + + IXLWorksheet SetShowGridLines(); IXLWorksheet SetShowGridLines(Boolean value); + + IXLWorksheet SetShowOutlineSymbols(); IXLWorksheet SetShowOutlineSymbols(Boolean value); + + IXLWorksheet SetShowRowColHeaders(); IXLWorksheet SetShowRowColHeaders(Boolean value); + + IXLWorksheet SetShowRuler(); IXLWorksheet SetShowRuler(Boolean value); + + IXLWorksheet SetShowWhiteSpace(); IXLWorksheet SetShowWhiteSpace(Boolean value); + + IXLWorksheet SetShowZeros(); IXLWorksheet SetShowZeros(Boolean value); + + XLColor TabColor { get; set; } + + IXLWorksheet SetTabColor(XLColor color); + + Boolean TabSelected { get; set; } + + Boolean TabActive { get; set; } + + IXLWorksheet SetTabSelected(); IXLWorksheet SetTabSelected(Boolean value); + + IXLWorksheet SetTabActive(); IXLWorksheet SetTabActive(Boolean value); + + IXLPivotTable PivotTable(String name); + + IXLPivotTables PivotTables { get; } + + Boolean RightToLeft { get; set; } + + IXLWorksheet SetRightToLeft(); IXLWorksheet SetRightToLeft(Boolean value); + + IXLAutoFilter AutoFilter { get; } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRows RowsUsed(Boolean includeFormats, Func predicate = null); + + IXLRows RowsUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents, Func predicate = null); + + IXLRows RowsUsed(Func predicate); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLColumns ColumnsUsed(Boolean includeFormats, Func predicate = null); + + IXLColumns ColumnsUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents, Func predicate = null); + + IXLColumns ColumnsUsed(Func predicate); + + IXLRanges MergedRanges { get; } + + IXLConditionalFormats ConditionalFormats { get; } + + IXLSparklineGroups SparklineGroups { get; } + + IXLRanges SelectedRanges { get; } + + IXLCell ActiveCell { get; set; } + + Object Evaluate(String expression); + + /// + /// Force recalculation of all cell formulas. + /// + void RecalculateAllFormulas(); + + String Author { get; set; } + + IXLPictures Pictures { get; } + + IXLPicture Picture(String pictureName); + + IXLPicture AddPicture(Stream stream); + + IXLPicture AddPicture(Stream stream, String name); + + IXLPicture AddPicture(Stream stream, XLPictureFormat format); + + IXLPicture AddPicture(Stream stream, XLPictureFormat format, String name); + + IXLPicture AddPicture(Bitmap bitmap); + + IXLPicture AddPicture(Bitmap bitmap, String name); + + IXLPicture AddPicture(String imageFile); + + IXLPicture AddPicture(String imageFile, String name); + } +} diff --git a/ClosedXML/Excel/IXLWorksheets.cs b/ClosedXML/Excel/IXLWorksheets.cs new file mode 100644 index 0000000..ea79b0b --- /dev/null +++ b/ClosedXML/Excel/IXLWorksheets.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Data; + +namespace ClosedXML.Excel +{ + public interface IXLWorksheets : IEnumerable + { + int Count { get; } + + IXLWorksheet Add(); + + IXLWorksheet Add(Int32 position); + + IXLWorksheet Add(String sheetName); + + IXLWorksheet Add(String sheetName, Int32 position); + + IXLWorksheet Add(DataTable dataTable); + + IXLWorksheet Add(DataTable dataTable, String sheetName); + + void Add(DataSet dataSet); + + Boolean Contains(String sheetName); + + void Delete(String sheetName); + + void Delete(Int32 position); + + bool TryGetWorksheet(string sheetName, out IXLWorksheet worksheet); + + IXLWorksheet Worksheet(String sheetName); + + IXLWorksheet Worksheet(Int32 position); + } +} diff --git a/ClosedXML/Excel/LoadOptions.cs b/ClosedXML/Excel/LoadOptions.cs new file mode 100644 index 0000000..3f4afc5 --- /dev/null +++ b/ClosedXML/Excel/LoadOptions.cs @@ -0,0 +1,11 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public class LoadOptions + { + public XLEventTracking EventTracking { get; set; } = XLEventTracking.Enabled; + public Boolean RecalculateAllFormulas { get; set; } = false; + } +} diff --git a/ClosedXML/Excel/Misc/XLDictionary.cs b/ClosedXML/Excel/Misc/XLDictionary.cs new file mode 100644 index 0000000..b386e89 --- /dev/null +++ b/ClosedXML/Excel/Misc/XLDictionary.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public class XLDictionary: Dictionary + { + public XLDictionary() + { + + } + public XLDictionary(XLDictionary other) + { + other.Values.ForEach(Add); + } + + public void Initialize(T value) + { + if (Count > 0) + Clear(); + + Add(value); + } + + public void Add(T value) + { + Add(Count + 1, value); + } + } +} diff --git a/ClosedXML/Excel/Misc/XLFormula.cs b/ClosedXML/Excel/Misc/XLFormula.cs new file mode 100644 index 0000000..8027667 --- /dev/null +++ b/ClosedXML/Excel/Misc/XLFormula.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public class XLFormula + { + public XLFormula() + {} + + public XLFormula(XLFormula defaultFormula) + { + _value = defaultFormula._value; + IsFormula = defaultFormula.IsFormula; + } + + public XLFormula(String value) + { + Value = value; + } + + public XLFormula(double value) + { + Value = value.ToInvariantString(); + } + + public XLFormula(int value) + { + Value = value.ToInvariantString(); + } + + internal String _value; + public String Value + { + get { return _value; } + set + { + if (value == null) + { + _value = String.Empty; + } + else + { + _value = value.Trim(); + IsFormula = !String.IsNullOrWhiteSpace(_value) && _value.TrimStart()[0] == '=' ; + if (IsFormula) + _value = _value.Substring(1); + } + + + } + } + + public Boolean IsFormula { get; internal set; } + } +} diff --git a/ClosedXML/Excel/Misc/XLIdManager.cs b/ClosedXML/Excel/Misc/XLIdManager.cs new file mode 100644 index 0000000..7dde3e8 --- /dev/null +++ b/ClosedXML/Excel/Misc/XLIdManager.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLIdManager + { + private HashSet _hash = new HashSet(); + + + public Int32 GetNext() + { + if (_hash.Count == 0) + { + _hash.Add(1); + return 1; + } + + Int32 id = 1; + while (true) + { + if (!_hash.Contains(id)) + { + _hash.Add(id); + return id; + } + id++; + } + } + public void Add(Int32 value) + { + _hash.Add(value); + } + public void Add(IEnumerable values) + { + values.ForEach(v => _hash.Add(v)); + } + } +} diff --git a/ClosedXML/Excel/NamedRanges/IXLNamedRange.cs b/ClosedXML/Excel/NamedRanges/IXLNamedRange.cs new file mode 100644 index 0000000..56de925 --- /dev/null +++ b/ClosedXML/Excel/NamedRanges/IXLNamedRange.cs @@ -0,0 +1,123 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLNamedRangeScope + { + Worksheet, + Workbook + } + + public interface IXLNamedRange + { + #region Public Properties + + /// + /// Gets or sets the comment for this named range. + /// + /// + /// The comment for this named range. + /// + String Comment { get; set; } + + /// + /// Checks if the named range contains invalid references (#REF!). + /// + bool IsValid { get; } + + /// + /// Gets or sets the name of the range. + /// + /// + /// The name of the range. + /// + String Name { get; set; } + + /// + /// Gets the ranges associated with this named range. + /// Note: A named range can point to multiple ranges. + /// + IXLRanges Ranges { get; } + String RefersTo { get; set; } + + /// + /// Gets the scope of this named range. + /// + XLNamedRangeScope Scope { get; } + + /// + /// Gets or sets the visibility of this named range. + /// + /// + /// true if visible; otherwise, false. + /// + Boolean Visible { get; set; } + + #endregion Public Properties + + #region Public Methods + + /// + /// Adds the specified range to this named range. + /// Note: A named range can point to multiple ranges. + /// + /// Workbook containing the range + /// The range address to add. + IXLRanges Add(XLWorkbook workbook, String rangeAddress); + + /// + /// Adds the specified range to this named range. + /// Note: A named range can point to multiple ranges. + /// + /// The range to add. + IXLRanges Add(IXLRange range); + + /// + /// Adds the specified ranges to this named range. + /// Note: A named range can point to multiple ranges. + /// + /// The ranges to add. + IXLRanges Add(IXLRanges ranges); + + /// + /// Clears the list of ranges associated with this named range. + /// (it does not clear the cells) + /// + void Clear(); + + IXLNamedRange CopyTo(IXLWorksheet targetSheet); + + /// + /// Deletes this named range (not the cells). + /// + void Delete(); + /// + /// Removes the specified range from this named range. + /// Note: A named range can point to multiple ranges. + /// + /// The range address to remove. + void Remove(String rangeAddress); + + /// + /// Removes the specified range from this named range. + /// Note: A named range can point to multiple ranges. + /// + /// The range to remove. + void Remove(IXLRange range); + + /// + /// Removes the specified ranges from this named range. + /// Note: A named range can point to multiple ranges. + /// + /// The ranges to remove. + void Remove(IXLRanges ranges); + + IXLNamedRange SetRefersTo(String range); + + IXLNamedRange SetRefersTo(IXLRangeBase range); + + IXLNamedRange SetRefersTo(IXLRanges ranges); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/NamedRanges/IXLNamedRanges.cs b/ClosedXML/Excel/NamedRanges/IXLNamedRanges.cs new file mode 100644 index 0000000..3d0eb51 --- /dev/null +++ b/ClosedXML/Excel/NamedRanges/IXLNamedRanges.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLNamedRanges: IEnumerable + { + /// + /// Gets the specified named range. + /// + /// Name of the range. + IXLNamedRange NamedRange(String rangeName); + + /// + /// Adds a new named range. + /// + /// Name of the range to add. + /// The range address to add. + /// + IXLNamedRange Add(String rangeName, String rangeAddress); + + /// + /// Adds a new named range. + /// + /// Name of the range to add. + /// The range to add. + /// + IXLNamedRange Add(String rangeName, IXLRange range); + + /// + /// Adds a new named range. + /// + /// Name of the range to add. + /// The ranges to add. + /// + IXLNamedRange Add(String rangeName, IXLRanges ranges); + + /// + /// Adds a new named range. + /// + /// Name of the ranges to add. + /// The range address to add. + /// The comment for the new named range. + IXLNamedRange Add(String rangeName, String rangeAddress, String comment); + + /// + /// Adds a new named range. + /// + /// Name of the ranges to add. + /// The range to add. + /// The comment for the new named range. + IXLNamedRange Add(String rangeName, IXLRange range, String comment); + + /// + /// Adds a new named range. + /// + /// Name of the ranges to add. + /// The ranges to add. + /// The comment for the new named range. + IXLNamedRange Add(String rangeName, IXLRanges ranges, String comment); + + /// + /// Deletes the specified named range (not the cells). + /// + /// Name of the range to delete. + void Delete(String rangeName); + + /// + /// Deletes the specified named range's index (not the cells). + /// + /// Index of the named range to delete. + void Delete(Int32 rangeIndex); + + + /// + /// Deletes all named ranges (not the cells). + /// + void DeleteAll(); + + Boolean TryGetValue(String name, out IXLNamedRange range); + + Boolean Contains(String name); + + /// + /// Returns a subset of named ranges that do not have invalid references. + /// + IEnumerable ValidNamedRanges(); + + /// + /// Returns a subset of named ranges that do have invalid references. + /// + IEnumerable InvalidNamedRanges(); + } +} diff --git a/ClosedXML/Excel/NamedRanges/XLNamedRange.cs b/ClosedXML/Excel/NamedRanges/XLNamedRange.cs new file mode 100644 index 0000000..538fedf --- /dev/null +++ b/ClosedXML/Excel/NamedRanges/XLNamedRange.cs @@ -0,0 +1,241 @@ +using ClosedXML.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLNamedRange : IXLNamedRange + { + private String _name; + private readonly XLNamedRanges _namedRanges; + + internal XLWorkbook Workbook => _namedRanges.Workbook; + + public XLNamedRange(XLNamedRanges namedRanges, String rangeName, String range, String comment = null) + : this(namedRanges, rangeName, validateName: true, range, comment) + { + } + + public XLNamedRange(XLNamedRanges namedRanges, String rangeName, IXLRanges ranges, String comment = null) + : this(namedRanges, rangeName, validateName: true, comment) + { + ranges.ForEach(r => RangeList.Add(r.RangeAddress.ToStringFixed(XLReferenceStyle.A1, true))); + } + + internal XLNamedRange(XLNamedRanges namedRanges, String rangeName, Boolean validateName, String range, String comment) + : this(namedRanges, rangeName, validateName, comment) + { + //TODO range.Split(',') may produce incorrect result if a worksheet name contains comma. Refactoring needed. + range.Split(',').ForEach(r => RangeList.Add(r)); + } + + internal XLNamedRange(XLNamedRanges namedRanges, String rangeName, Boolean validateName, String comment) + { + _namedRanges = namedRanges ?? throw new ArgumentNullException(nameof(namedRanges)); + Visible = true; + + if (validateName) + Name = rangeName; + else + SetNameWithoutValidation(rangeName); + + Comment = comment; + } + + /// + /// Checks if the named range contains invalid references (#REF!). + /// + public bool IsValid + { + get + { + return RangeList.SelectMany(c => c.Split(',')).All(r => + !r.StartsWith("#REF!", StringComparison.OrdinalIgnoreCase) && + !r.EndsWith("#REF!", StringComparison.OrdinalIgnoreCase)); + } + } + + public String Name + { + get { return _name; } + set + { + if (_name == value) return; + + var oldname = _name ?? string.Empty; + + var existingNames = _namedRanges.Select(nr => nr.Name).ToList(); + if (_namedRanges.Scope == XLNamedRangeScope.Workbook) + existingNames.AddRange(_namedRanges.Workbook.NamedRanges.Select(nr => nr.Name)); + + if (_namedRanges.Scope == XLNamedRangeScope.Worksheet) + existingNames.AddRange(_namedRanges.Worksheet.NamedRanges.Select(nr => nr.Name)); + + existingNames = existingNames.Distinct().ToList(); + + if (!XLHelper.ValidateName("named range", value, oldname, existingNames, out String message)) + throw new ArgumentException(message, nameof(value)); + + _name = value; + + if (!String.IsNullOrWhiteSpace(oldname) && !String.Equals(oldname, _name, StringComparison.OrdinalIgnoreCase)) + { + _namedRanges.Delete(oldname); + _namedRanges.Add(_name, this); + } + } + } + + private void SetNameWithoutValidation(string value) + { + _name = value; + } + + public IXLRanges Ranges + { + get + { + var ranges = new XLRanges(); + foreach (var rangeToAdd in + from rangeAddress in RangeList.SelectMany(c => c.Split(',')).Where(s => s[0] != '"') + let match = XLHelper.NamedRangeReferenceRegex.Match(rangeAddress) + select + match.Groups["Sheet"].Success && Workbook.Worksheets.Contains(match.Groups["Sheet"].Value) + ? Workbook.WorksheetsInternal.Worksheet(match.Groups["Sheet"].Value).Range(match.Groups["Range"].Value) as IXLRangeBase + : Workbook.Worksheets.SelectMany(sheet => sheet.Tables).SingleOrDefault(table => table.Name == match.Groups["Table"].Value)? + .DataRange?.Column(match.Groups["Column"].Value)) + { + if (rangeToAdd != null) + ranges.Add(rangeToAdd); + } + return ranges; + } + } + + public String Comment { get; set; } + + public Boolean Visible { get; set; } + + public XLNamedRangeScope Scope { get { return _namedRanges.Scope; } } + + public IXLRanges Add(XLWorkbook workbook, String rangeAddress) + { + var ranges = new XLRanges(); + var byExclamation = rangeAddress.Split('!'); + var wsName = byExclamation[0].Replace("'", ""); + var rng = byExclamation[1]; + var rangeToAdd = workbook.WorksheetsInternal.Worksheet(wsName).Range(rng); + + ranges.Add(rangeToAdd); + return Add(ranges); + } + + public IXLRanges Add(IXLRange range) + { + var ranges = new XLRanges { range }; + return Add(ranges); + } + + public IXLRanges Add(IXLRanges ranges) + { + ranges.ForEach(r => RangeList.Add(r.ToString())); + return ranges; + } + + public void Delete() + { + _namedRanges.Delete(Name); + } + + public void Clear() + { + RangeList.Clear(); + } + + public void Remove(String rangeAddress) + { + RangeList.Remove(rangeAddress); + } + + public void Remove(IXLRange range) + { + RangeList.Remove(range.ToString()); + } + + public void Remove(IXLRanges ranges) + { + ranges.ForEach(r => RangeList.Remove(r.ToString())); + } + + public override string ToString() + { + String retVal = RangeList.Aggregate(String.Empty, (agg, r) => agg + (r + ",")); + if (retVal.Length > 0) retVal = retVal.Substring(0, retVal.Length - 1); + return retVal; + } + + public String RefersTo + { + get { return ToString(); } + set + { + RangeList.Clear(); + RangeList.Add(value); + } + } + + public IXLNamedRange CopyTo(IXLWorksheet targetSheet) + { + if (targetSheet == _namedRanges.Worksheet) + throw new InvalidOperationException("Cannot copy named range to the worksheet it already belongs to."); + + var ranges = new XLRanges(); + foreach (var r in Ranges) + { + if (_namedRanges.Worksheet == r.Worksheet) + // Named ranges on the source worksheet have to point to the new destination sheet + ranges.Add(targetSheet.Range(((XLRangeAddress)r.RangeAddress).WithoutWorksheet())); + else + ranges.Add(r); + } + + return targetSheet.NamedRanges.Add(Name, ranges); + } + + internal IList RangeList { get; set; } = new List(); + + public IXLNamedRange SetRefersTo(String range) + { + RefersTo = range; + return this; + } + + public IXLNamedRange SetRefersTo(IXLRangeBase range) + { + RangeList.Clear(); + RangeList.Add(range.RangeAddress.ToStringFixed(XLReferenceStyle.A1, true)); + return this; + } + + public IXLNamedRange SetRefersTo(IXLRanges ranges) + { + RangeList.Clear(); + ranges.ForEach(r => RangeList.Add(r.RangeAddress.ToStringFixed(XLReferenceStyle.A1, true))); + return this; + } + + internal void OnWorksheetDeleted(string worksheetName) + { + var escapedSheetName = worksheetName.EscapeSheetName(); + RangeList = RangeList + .Select( + rl => string.Join(",", rl + .Split(',') + .Select(r => r.StartsWith(escapedSheetName + "!", StringComparison.OrdinalIgnoreCase) + ? "#REF!" + r.Substring(escapedSheetName.Length + 1) + : r)) + ).ToList(); + } + } +} diff --git a/ClosedXML/Excel/NamedRanges/XLNamedRanges.cs b/ClosedXML/Excel/NamedRanges/XLNamedRanges.cs new file mode 100644 index 0000000..99b88f8 --- /dev/null +++ b/ClosedXML/Excel/NamedRanges/XLNamedRanges.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLNamedRanges : IXLNamedRanges + { + private readonly Dictionary _namedRanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal XLWorkbook Workbook { get; set; } + + internal XLWorksheet Worksheet { get; set; } + + internal XLNamedRangeScope Scope { get; } + + public XLNamedRanges(XLWorksheet worksheet) + : this(worksheet.Workbook) + { + Worksheet = worksheet; + Scope = XLNamedRangeScope.Worksheet; + } + + public XLNamedRanges(XLWorkbook workbook) + { + Workbook = workbook; + Scope = XLNamedRangeScope.Workbook; + } + + #region IXLNamedRanges Members + + public IXLNamedRange NamedRange(String rangeName) + { + if (_namedRanges.TryGetValue(rangeName, out IXLNamedRange range)) + return range; + + return null; + } + + public IXLNamedRange Add(String rangeName, String rangeAddress) + { + return Add(rangeName, rangeAddress, null); + } + + public IXLNamedRange Add(String rangeName, IXLRange range) + { + return Add(rangeName, range, null); + } + + public IXLNamedRange Add(String rangeName, IXLRanges ranges) + { + return Add(rangeName, ranges, null); + } + + public IXLNamedRange Add(String rangeName, String rangeAddress, String comment) + { + return Add(rangeName, rangeAddress, comment, validateName: true, validateRangeAddress: true); + } + + /// + /// Adds the specified range name. + /// + /// Name of the range. + /// The range address. + /// The comment. + /// if set to true validates the name. + /// if set to true range address will be checked for validity. + /// + /// + /// + /// For named ranges in the workbook scope, specify the sheet name in the reference. + /// + internal IXLNamedRange Add(String rangeName, String rangeAddress, String comment, Boolean validateName, Boolean validateRangeAddress) + { + // When loading named ranges from an existing file, we do not validate the range address or name. + if (validateRangeAddress) + { + var match = XLHelper.NamedRangeReferenceRegex.Match(rangeAddress); + + if (!match.Success) + { + if (XLHelper.IsValidRangeAddress(rangeAddress)) + { + IXLRange range = null; + if (Scope == XLNamedRangeScope.Worksheet) + range = Worksheet.Range(rangeAddress); + else if (Scope == XLNamedRangeScope.Workbook) + range = Workbook.Range(rangeAddress); + else + throw new NotSupportedException($"Scope {Scope} is not supported"); + + if (range == null) + throw new ArgumentException(string.Format( + "The range address '{0}' for the named range '{1}' is not a valid range.", rangeAddress, + rangeName)); + + if (Scope == XLNamedRangeScope.Workbook || !XLHelper.NamedRangeReferenceRegex.Match(range.ToString()).Success) + throw new ArgumentException( + "For named ranges in the workbook scope, specify the sheet name in the reference."); + + rangeAddress = Worksheet.Range(rangeAddress).ToString(); + } + } + } + + var namedRange = new XLNamedRange(this, rangeName, validateName, rangeAddress, comment); + _namedRanges.Add(rangeName, namedRange); + return namedRange; + } + + public IXLNamedRange Add(String rangeName, IXLRange range, String comment) + { + var ranges = new XLRanges { range }; + return Add(rangeName, ranges, comment); + } + + public IXLNamedRange Add(String rangeName, IXLRanges ranges, String comment) + { + var namedRange = new XLNamedRange(this, rangeName, ranges, comment); + _namedRanges.Add(rangeName, namedRange); + return namedRange; + } + + public IXLNamedRange Add(String rangeName, IXLNamedRange namedRange) + { + _namedRanges.Add(rangeName, namedRange); + return namedRange; + } + + public void Delete(String rangeName) + { + _namedRanges.Remove(rangeName); + } + + public void Delete(Int32 rangeIndex) + { + _namedRanges.Remove(_namedRanges.ElementAt(rangeIndex).Key); + } + + public void DeleteAll() + { + _namedRanges.Clear(); + } + + /// + /// Returns a subset of named ranges that do not have invalid references. + /// + public IEnumerable ValidNamedRanges() + { + return this.Where(nr => nr.IsValid); + } + + /// + /// Returns a subset of named ranges that do have invalid references. + /// + public IEnumerable InvalidNamedRanges() + { + return this.Where(nr => !nr.IsValid); + } + + #endregion IXLNamedRanges Members + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return _namedRanges.Values.GetEnumerator(); + } + + #endregion IEnumerable Members + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion IEnumerable Members + + public Boolean TryGetValue(String name, out IXLNamedRange range) + { + if (_namedRanges.TryGetValue(name, out range)) return true; + + if (Scope == XLNamedRangeScope.Workbook) + range = Workbook.NamedRange(name); + + return range != null; + } + + public Boolean Contains(String name) + { + if (_namedRanges.ContainsKey(name)) return true; + + if (Scope == XLNamedRangeScope.Workbook) + return Workbook.NamedRange(name) != null; + else + return false; + } + + internal void OnWorksheetDeleted(string worksheetName) + { + _namedRanges.Values + .Cast() + .ForEach(nr => nr.OnWorksheetDeleted(worksheetName)); + } + } +} diff --git a/ClosedXML/Excel/PageSetup/IXLHFItem.cs b/ClosedXML/Excel/PageSetup/IXLHFItem.cs new file mode 100644 index 0000000..5f7fc0e --- /dev/null +++ b/ClosedXML/Excel/PageSetup/IXLHFItem.cs @@ -0,0 +1,48 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLHFPredefinedText + { + PageNumber, NumberOfPages, Date, Time, FullPath, Path, File, SheetName + } + public enum XLHFOccurrence + { + AllPages, OddPages, EvenPages, FirstPage + } + + public interface IXLHFItem: IXLWithRichString + { + /// + /// Gets the text of the specified header/footer occurrence. + /// + /// The occurrence. + String GetText(XLHFOccurrence occurrence); + + /// + /// Adds the given predefined text to this header/footer item. + /// + /// The predefined text to add to this header/footer item. + IXLRichString AddText(XLHFPredefinedText predefinedText); + + /// + /// Adds the given text to this header/footer item. + /// + /// The text to add to this header/footer item. + /// The occurrence for the text. + IXLRichString AddText(String text, XLHFOccurrence occurrence); + + /// + /// Adds the given predefined text to this header/footer item. + /// + /// The predefined text to add to this header/footer item. + /// The occurrence for the predefined text. + IXLRichString AddText(XLHFPredefinedText predefinedText, XLHFOccurrence occurrence); + + /// Clears the text/formats of this header/footer item. + /// The occurrence to clear. + void Clear(XLHFOccurrence occurrence = XLHFOccurrence.AllPages); + + IXLRichString AddImage(String imagePath, XLHFOccurrence occurrence = XLHFOccurrence.AllPages); + } +} diff --git a/ClosedXML/Excel/PageSetup/IXLHeaderFooter.cs b/ClosedXML/Excel/PageSetup/IXLHeaderFooter.cs new file mode 100644 index 0000000..2358754 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/IXLHeaderFooter.cs @@ -0,0 +1,31 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLHFMode { OddPagesOnly, OddAndEvenPages, Odd } + public interface IXLHeaderFooter + { + /// + /// Gets the left header/footer item. + /// + IXLHFItem Left { get; } + + /// + /// Gets the middle header/footer item. + /// + IXLHFItem Center { get; } + + /// + /// Gets the right header/footer item. + /// + IXLHFItem Right { get; } + + /// + /// Gets the text of the specified header/footer occurrence. + /// + /// The occurrence. + String GetText(XLHFOccurrence occurrence); + + IXLHeaderFooter Clear(XLHFOccurrence occurrence = XLHFOccurrence.AllPages); + } +} diff --git a/ClosedXML/Excel/PageSetup/IXLMargins.cs b/ClosedXML/Excel/PageSetup/IXLMargins.cs new file mode 100644 index 0000000..148fcfb --- /dev/null +++ b/ClosedXML/Excel/PageSetup/IXLMargins.cs @@ -0,0 +1,39 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLMargins + { + /// Gets or sets the Left margin. + /// The Left margin. + Double Left { get; set; } + + /// Gets or sets the Right margin. + /// The Right margin. + Double Right { get; set; } + + /// Gets or sets the Top margin. + /// The Top margin. + Double Top { get; set; } + + /// Gets or sets the Bottom margin. + /// The Bottom margin. + Double Bottom { get; set; } + + /// Gets or sets the Header margin. + /// The Header margin. + Double Header { get; set; } + + /// Gets or sets the Footer margin. + /// The Footer margin. + Double Footer { get; set; } + + IXLMargins SetLeft(Double value); + IXLMargins SetRight(Double value); + IXLMargins SetTop(Double value); + IXLMargins SetBottom(Double value); + IXLMargins SetHeader(Double value); + IXLMargins SetFooter(Double value); + + } +} diff --git a/ClosedXML/Excel/PageSetup/IXLPageSetup.cs b/ClosedXML/Excel/PageSetup/IXLPageSetup.cs new file mode 100644 index 0000000..d474a70 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/IXLPageSetup.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLPageOrientation { Default, Portrait, Landscape } + public enum XLPaperSize + { + LetterPaper = 1, + LetterSmallPaper = 2, + TabloidPaper = 3, + LedgerPaper = 4, + LegalPaper = 5, + StatementPaper = 6, + ExecutivePaper = 7, + A3Paper = 8, + A4Paper = 9, + A4SmallPaper = 10, + A5Paper = 11, + B4Paper = 12, + B5Paper = 13, + FolioPaper = 14, + QuartoPaper = 15, + StandardPaper = 16, + StandardPaper1 = 17, + NotePaper = 18, + No9Envelope = 19, + No10Envelope = 20, + No11Envelope = 21, + No12Envelope = 22, + No14Envelope = 23, + CPaper = 24, + DPaper = 25, + EPaper = 26, + DlEnvelope = 27, + C5Envelope = 28, + C3Envelope = 29, + C4Envelope = 30, + C6Envelope = 31, + C65Envelope = 32, + B4Envelope = 33, + B5Envelope = 34, + B6Envelope = 35, + ItalyEnvelope = 36, + MonarchEnvelope = 37, + No634Envelope = 38, + UsStandardFanfold = 39, + GermanStandardFanfold = 40, + GermanLegalFanfold = 41, + IsoB4 = 42, + JapaneseDoublePostcard = 43, + StandardPaper2 = 44, + StandardPaper3 = 45, + StandardPaper4 = 46, + InviteEnvelope = 47, + LetterExtraPaper = 50, + LegalExtraPaper = 51, + TabloidExtraPaper = 52, + A4ExtraPaper = 53, + LetterTransversePaper = 54, + A4TransversePaper = 55, + LetterExtraTransversePaper = 56, + SuperaSuperaA4Paper = 57, + SuperbSuperbA3Paper = 58, + LetterPlusPaper = 59, + A4PlusPaper = 60, + A5TransversePaper = 61, + JisB5TransversePaper = 62, + A3ExtraPaper = 63, + A5ExtraPaper = 64, + IsoB5ExtraPaper = 65, + A2Paper = 66, + A3TransversePaper = 67, + A3ExtraTransversePaper = 68 + } + public enum XLPageOrderValues { DownThenOver, OverThenDown } + public enum XLShowCommentsValues { None, AtEnd, AsDisplayed } + public enum XLPrintErrorValues { Blank, Dash, Displayed, NA } + + public interface IXLPageSetup + { + /// + /// Gets an object to manage the print areas of the worksheet. + /// + IXLPrintAreas PrintAreas { get; } + /// + /// Gets the first row that will repeat on the top of the printed pages. + /// Use SetRowsToRepeatAtTop() to set the rows that will be repeated on the top of the printed pages. + /// + Int32 FirstRowToRepeatAtTop { get; } + /// + /// Gets the last row that will repeat on the top of the printed pages. + /// Use SetRowsToRepeatAtTop() to set the rows that will be repeated on the top of the printed pages. + /// + Int32 LastRowToRepeatAtTop { get; } + /// + /// Sets the rows to repeat on the top of the printed pages. + /// + /// The range of rows to repeat on the top of the printed pages. + void SetRowsToRepeatAtTop(String range); + /// + /// Sets the rows to repeat on the top of the printed pages. + /// + /// The first row to repeat at top. + /// The last row to repeat at top. + void SetRowsToRepeatAtTop(Int32 firstRowToRepeatAtTop, Int32 lastRowToRepeatAtTop); + + /// Gets the first column to repeat on the left of the printed pages. + /// The first column to repeat on the left of the printed pages. + Int32 FirstColumnToRepeatAtLeft { get; } + /// Gets the last column to repeat on the left of the printed pages. + /// The last column to repeat on the left of the printed pages. + Int32 LastColumnToRepeatAtLeft { get; } + /// + /// Sets the rows to repeat on the left of the printed pages. + /// + /// The first column to repeat at left. + /// The last column to repeat at left. + void SetColumnsToRepeatAtLeft(Int32 firstColumnToRepeatAtLeft, Int32 lastColumnToRepeatAtLeft); + /// + /// Sets the rows to repeat on the left of the printed pages. + /// + /// The range of rows to repeat on the left of the printed pages. + void SetColumnsToRepeatAtLeft(String range); + + /// Gets or sets the page orientation for printing. + /// The page orientation. + XLPageOrientation PageOrientation { get; set; } + /// + /// Gets or sets the number of pages wide (horizontal) the worksheet will be printed on. + /// If you don't specify the PagesTall, Excel will adjust that value + /// based on the contents of the worksheet and the PagesWide number. + /// Setting this value will override the Scale value. + /// + Int32 PagesWide { get; set; } + /// + /// Gets or sets the number of pages tall (vertical) the worksheet will be printed on. + /// If you don't specify the PagesWide, Excel will adjust that value + /// based on the contents of the worksheet and the PagesTall number. + /// Setting this value will override the Scale value. + /// + Int32 PagesTall { get; set; } + /// + /// Gets or sets the scale at which the worksheet will be printed. + /// The worksheet will be printed on as many pages as necessary to print at the given scale. + /// Setting this value will override the PagesWide and PagesTall values. + /// + Int32 Scale { get; set; } + /// + /// Gets or sets the horizontal dpi for printing the worksheet. + /// + Int32 HorizontalDpi { get; set; } + /// + /// Gets or sets the vertical dpi for printing the worksheet. + /// + Int32 VerticalDpi { get; set; } + /// + /// Gets or sets the page number that will begin the printout. + /// For example, the first page of your printout could be numbered page 5. + /// + UInt32? FirstPageNumber { get; set; } + /// + /// Gets or sets a value indicating whether the worksheet will be centered on the page horizontally. + /// + /// + /// true if the worksheet will be centered on the page horizontally; otherwise, false. + /// + Boolean CenterHorizontally { get; set; } + /// + /// Gets or sets a value indicating whether the worksheet will be centered on the page vertically. + /// + /// + /// true if the worksheet will be centered on the page vartically; otherwise, false. + /// + Boolean CenterVertically { get; set; } + /// + /// Sets the scale at which the worksheet will be printed. This is equivalent to setting the Scale property. + /// The worksheet will be printed on as many pages as necessary to print at the given scale. + /// Setting this value will override the PagesWide and PagesTall values. + /// + /// The scale at which the worksheet will be printed. + void AdjustTo(Int32 percentageOfNormalSize); + /// + /// Gets or sets the number of pages the worksheet will be printed on. + /// This is equivalent to setting both PagesWide and PagesTall properties. + /// Setting this value will override the Scale value. + /// + /// The pages wide. + /// The pages tall. + void FitToPages(Int32 pagesWide, Int32 pagesTall); + /// + /// Gets or sets the size of the paper to print the worksheet. + /// + XLPaperSize PaperSize { get; set; } + /// + /// Gets an object to work with the page margins. + /// + IXLMargins Margins { get; } + + /// + /// Gets an object to work with the page headers. + /// + IXLHeaderFooter Header { get; } + /// + /// Gets an object to work with the page footers. + /// + IXLHeaderFooter Footer { get; } + /// + /// Gets or sets a value indicating whether Excel will automatically adjust the font size to the scale of the worksheet. + /// + /// + /// true if Excel will automatically adjust the font size to the scale of the worksheet; otherwise, false. + /// + Boolean ScaleHFWithDocument { get; set; } + /// + /// Gets or sets a value indicating whether the header and footer margins are aligned with the left and right margins of the worksheet. + /// + /// + /// true if the header and footer margins are aligned with the left and right margins of the worksheet; otherwise, false. + /// + Boolean AlignHFWithMargins { get; set; } + + /// + /// Gets or sets a value indicating whether the gridlines will be printed. + /// + /// + /// true if the gridlines will be printed; otherwise, false. + /// + Boolean ShowGridlines { get; set; } + /// + /// Gets or sets a value indicating whether to show row numbers and column letters/numbers. + /// + /// + /// true to show row numbers and column letters/numbers; otherwise, false. + /// + Boolean ShowRowAndColumnHeadings { get; set; } + /// + /// Gets or sets a value indicating whether the worksheet will be printed in black and white. + /// + /// + /// true if the worksheet will be printed in black and white; otherwise, false. + /// + Boolean BlackAndWhite { get; set; } + /// + /// Gets or sets a value indicating whether the worksheet will be printed in draft quality. + /// + /// + /// true if the worksheet will be printed in draft quality; otherwise, false. + /// + Boolean DraftQuality { get; set; } + /// + /// Gets or sets the page order for printing. + /// + XLPageOrderValues PageOrder { get; set; } + /// + /// Gets or sets how the comments will be printed. + /// + XLShowCommentsValues ShowComments { get; set; } + + /// + /// Gets a list with the row breaks (for printing). + /// + List RowBreaks { get; } + /// + /// Gets a list with the column breaks (for printing). + /// + List ColumnBreaks { get; } + /// + /// Adds a horizontal page break after the given row. + /// + /// The row to insert the break. + void AddHorizontalPageBreak(Int32 row); + + /// + /// Adds a vertical page break after the given column. + /// + /// The column to insert the break. + void AddVerticalPageBreak(Int32 column); + + /// + /// Gets or sets how error values will be printed. + /// + XLPrintErrorValues PrintErrorValue { get; set; } + + IXLPageSetup SetPageOrientation(XLPageOrientation value); + IXLPageSetup SetPagesWide(Int32 value); + IXLPageSetup SetPagesTall(Int32 value); + IXLPageSetup SetScale(Int32 value); + IXLPageSetup SetHorizontalDpi(Int32 value); + IXLPageSetup SetVerticalDpi(Int32 value); + IXLPageSetup SetFirstPageNumber(UInt32? value); + IXLPageSetup SetCenterHorizontally(); IXLPageSetup SetCenterHorizontally(Boolean value); + IXLPageSetup SetCenterVertically(); IXLPageSetup SetCenterVertically(Boolean value); + IXLPageSetup SetPaperSize(XLPaperSize value); + IXLPageSetup SetScaleHFWithDocument(); IXLPageSetup SetScaleHFWithDocument(Boolean value); + IXLPageSetup SetAlignHFWithMargins(); IXLPageSetup SetAlignHFWithMargins(Boolean value); + IXLPageSetup SetShowGridlines(); IXLPageSetup SetShowGridlines(Boolean value); + IXLPageSetup SetShowRowAndColumnHeadings(); IXLPageSetup SetShowRowAndColumnHeadings(Boolean value); + IXLPageSetup SetBlackAndWhite(); IXLPageSetup SetBlackAndWhite(Boolean value); + IXLPageSetup SetDraftQuality(); IXLPageSetup SetDraftQuality(Boolean value); + IXLPageSetup SetPageOrder(XLPageOrderValues value); + IXLPageSetup SetShowComments(XLShowCommentsValues value); + IXLPageSetup SetPrintErrorValue(XLPrintErrorValues value); + + + Boolean DifferentFirstPageOnHF { get; set; } + IXLPageSetup SetDifferentFirstPageOnHF(); IXLPageSetup SetDifferentFirstPageOnHF(Boolean value); + Boolean DifferentOddEvenPagesOnHF { get; set; } + IXLPageSetup SetDifferentOddEvenPagesOnHF(); IXLPageSetup SetDifferentOddEvenPagesOnHF(Boolean value); + } +} diff --git a/ClosedXML/Excel/PageSetup/IXLPrintAreas.cs b/ClosedXML/Excel/PageSetup/IXLPrintAreas.cs new file mode 100644 index 0000000..8f04642 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/IXLPrintAreas.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLPrintAreas: IEnumerable + { + /// Removes the print areas from the worksheet. + void Clear(); + + /// Adds a range to the print areas. + /// The first cell row. + /// The first cell column. + /// The last cell row. + /// The last cell column. + void Add(Int32 firstCellRow, Int32 firstCellColumn, Int32 lastCellRow, Int32 lastCellColumn); + + /// Adds a range to the print areas. + /// The range address to add. + void Add(String rangeAddress); + + /// Adds a range to the print areas. + /// The first cell address. + /// The last cell address. + void Add(String firstCellAddress, String lastCellAddress); + + /// Adds a range to the print areas. + /// The first cell address. + /// The last cell address. + void Add(IXLAddress firstCellAddress, IXLAddress lastCellAddress); + } +} diff --git a/ClosedXML/Excel/PageSetup/XLHFItem.cs b/ClosedXML/Excel/PageSetup/XLHFItem.cs new file mode 100644 index 0000000..f46500c --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLHFItem.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLHFItem : IXLHFItem + { + internal readonly XLHeaderFooter HeaderFooter; + public XLHFItem(XLHeaderFooter headerFooter) + { + HeaderFooter = headerFooter; + } + public XLHFItem(XLHFItem defaultHFItem, XLHeaderFooter headerFooter) + :this(headerFooter) + { + defaultHFItem.texts.ForEach(kp => texts.Add(kp.Key, kp.Value)); + } + private readonly Dictionary> texts = new Dictionary>(); + public String GetText(XLHFOccurrence occurrence) + { + var sb = new StringBuilder(); + if(texts.TryGetValue(occurrence, out List hfTexts)) + { + foreach (var hfText in hfTexts) + sb.Append(hfText.GetHFText(sb.ToString())); + } + + return sb.ToString(); + } + + public IXLRichString AddText(String text) + { + return AddText(text, XLHFOccurrence.AllPages); + } + public IXLRichString AddText(XLHFPredefinedText predefinedText) + { + return AddText(predefinedText, XLHFOccurrence.AllPages); + } + + public IXLRichString AddText(String text, XLHFOccurrence occurrence) + { + XLRichString richText = new XLRichString(text, this.HeaderFooter.Worksheet.Style.Font, this); + + var hfText = new XLHFText(richText, this); + if (occurrence == XLHFOccurrence.AllPages) + { + AddTextToOccurrence(hfText, XLHFOccurrence.EvenPages); + AddTextToOccurrence(hfText, XLHFOccurrence.FirstPage); + AddTextToOccurrence(hfText, XLHFOccurrence.OddPages); + } + else + { + AddTextToOccurrence(hfText, occurrence); + } + + return richText; + } + + public IXLRichString AddNewLine() + { + return AddText(Environment.NewLine); + } + + public IXLRichString AddImage(String imagePath, XLHFOccurrence occurrence = XLHFOccurrence.AllPages) + { + throw new NotImplementedException(); + } + + private void AddTextToOccurrence(XLHFText hfText, XLHFOccurrence occurrence) + { + if (texts.TryGetValue(occurrence, out List hfTexts)) + hfTexts.Add(hfText); + else + texts.Add(occurrence, new List { hfText }); + + this.HeaderFooter.Changed = true; + } + + public IXLRichString AddText(XLHFPredefinedText predefinedText, XLHFOccurrence occurrence) + { + String hfText; + switch (predefinedText) + { + case XLHFPredefinedText.PageNumber: hfText = "&P"; break; + case XLHFPredefinedText.NumberOfPages : hfText = "&N"; break; + case XLHFPredefinedText.Date : hfText = "&D"; break; + case XLHFPredefinedText.Time : hfText = "&T"; break; + case XLHFPredefinedText.Path : hfText = "&Z"; break; + case XLHFPredefinedText.File : hfText = "&F"; break; + case XLHFPredefinedText.SheetName : hfText = "&A"; break; + case XLHFPredefinedText.FullPath: hfText = "&Z&F"; break; + default: throw new NotImplementedException(); + } + return AddText(hfText, occurrence); + } + + public void Clear(XLHFOccurrence occurrence = XLHFOccurrence.AllPages) + { + if (occurrence == XLHFOccurrence.AllPages) + { + ClearOccurrence(XLHFOccurrence.EvenPages); + ClearOccurrence(XLHFOccurrence.FirstPage); + ClearOccurrence(XLHFOccurrence.OddPages); + } + else + { + ClearOccurrence(occurrence); + } + } + + private void ClearOccurrence(XLHFOccurrence occurrence) + { + if (texts.ContainsKey(occurrence)) + texts.Remove(occurrence); + } + } +} diff --git a/ClosedXML/Excel/PageSetup/XLHFText.cs b/ClosedXML/Excel/PageSetup/XLHFText.cs new file mode 100644 index 0000000..6659cd6 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLHFText.cs @@ -0,0 +1,104 @@ +using System; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLHFText + { + private readonly XLHFItem _hfItem; + + public XLHFText(XLRichString richText, XLHFItem hfItem) + { + RichText = richText; + _hfItem = hfItem; + } + + public XLRichString RichText { get; private set; } + + public String GetHFText(String prevText) + { + var wsFont = _hfItem.HeaderFooter.Worksheet.Style.Font; + + var isRichText = RichText.FontName != null && RichText.FontName != wsFont.FontName + || RichText.Bold != wsFont.Bold + || RichText.Italic != wsFont.Italic + || RichText.Strikethrough != wsFont.Strikethrough + || RichText.FontSize > 0 && Math.Abs(RichText.FontSize - wsFont.FontSize) > XLHelper.Epsilon + || RichText.VerticalAlignment != wsFont.VerticalAlignment + || RichText.Underline != wsFont.Underline + || !RichText.FontColor.Equals(wsFont.FontColor); + + if (!isRichText) + return RichText.Text; + + StringBuilder sb = new StringBuilder(); + + if (RichText.FontName != null && RichText.FontName != wsFont.FontName) + sb.Append("&\"" + RichText.FontName); + else + sb.Append("&\"-"); + + if (RichText.Bold && RichText.Italic) + sb.Append(",Bold Italic\""); + else if (RichText.Bold) + sb.Append(",Bold\""); + else if (RichText.Italic) + sb.Append(",Italic\""); + else + sb.Append(",Regular\""); + + if (RichText.FontSize > 0 && Math.Abs(RichText.FontSize - wsFont.FontSize) > XLHelper.Epsilon) + sb.Append("&" + RichText.FontSize); + + if (RichText.Strikethrough && !wsFont.Strikethrough) + sb.Append("&S"); + + if (RichText.VerticalAlignment != wsFont.VerticalAlignment) + { + if (RichText.VerticalAlignment == XLFontVerticalTextAlignmentValues.Subscript) + sb.Append("&Y"); + else if (RichText.VerticalAlignment == XLFontVerticalTextAlignmentValues.Superscript) + sb.Append("&X"); + } + + if (RichText.Underline != wsFont.Underline) + { + if (RichText.Underline == XLFontUnderlineValues.Single) + sb.Append("&U"); + else if (RichText.Underline == XLFontUnderlineValues.Double) + sb.Append("&E"); + } + + var lastColorPosition = prevText.LastIndexOf("&K"); + + if ( + (lastColorPosition >= 0 && !RichText.FontColor.Equals(XLColor.FromHtml("#" + prevText.Substring(lastColorPosition + 2, 6)))) + || (lastColorPosition == -1 && !RichText.FontColor.Equals(wsFont.FontColor)) + ) + sb.Append("&K" + RichText.FontColor.Color.ToHex().Substring(2)); + + sb.Append(RichText.Text); + + if (RichText.Underline != wsFont.Underline) + { + if (RichText.Underline == XLFontUnderlineValues.Single) + sb.Append("&U"); + else if (RichText.Underline == XLFontUnderlineValues.Double) + sb.Append("&E"); + } + + if (RichText.VerticalAlignment != wsFont.VerticalAlignment) + { + if (RichText.VerticalAlignment == XLFontVerticalTextAlignmentValues.Subscript) + sb.Append("&Y"); + else if (RichText.VerticalAlignment == XLFontVerticalTextAlignmentValues.Superscript) + sb.Append("&X"); + } + + if (RichText.Strikethrough && !wsFont.Strikethrough) + sb.Append("&S"); + + return sb.ToString(); + } + } +} diff --git a/ClosedXML/Excel/PageSetup/XLHeaderFooter.cs b/ClosedXML/Excel/PageSetup/XLHeaderFooter.cs new file mode 100644 index 0000000..2290db0 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLHeaderFooter.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + using System.Linq; + + internal class XLHeaderFooter: IXLHeaderFooter + { + + public XLHeaderFooter(XLWorksheet worksheet) + { + this.Worksheet = worksheet; + Left = new XLHFItem(this); + Right = new XLHFItem(this); + Center = new XLHFItem(this); + SetAsInitial(); + } + + public XLHeaderFooter(XLHeaderFooter defaultHF, XLWorksheet worksheet) + { + this.Worksheet = worksheet; + defaultHF.innerTexts.ForEach(kp => innerTexts.Add(kp.Key, kp.Value)); + Left = new XLHFItem(defaultHF.Left as XLHFItem, this); + Center = new XLHFItem(defaultHF.Center as XLHFItem, this); + Right = new XLHFItem(defaultHF.Right as XLHFItem, this); + SetAsInitial(); + } + + internal readonly IXLWorksheet Worksheet; + + public IXLHFItem Left { get; private set; } + public IXLHFItem Center { get; private set; } + public IXLHFItem Right { get; private set; } + + public String GetText(XLHFOccurrence occurrence) + { + //if (innerTexts.ContainsKey(occurrence)) return innerTexts[occurrence]; + + var retVal = String.Empty; + var leftText = Left.GetText(occurrence); + var centerText = Center.GetText(occurrence); + var rightText = Right.GetText(occurrence); + retVal += leftText.Length > 0 ? "&L" + leftText : String.Empty; + retVal += centerText.Length > 0 ? "&C" + centerText : String.Empty; + retVal += rightText.Length > 0 ? "&R" + rightText : String.Empty; + if (retVal.Length > 255) + throw new ArgumentOutOfRangeException("Headers and Footers cannot be longer than 255 characters (including style markups)"); + return retVal; + } + + private Dictionary innerTexts = new Dictionary(); + internal void SetInnerText(XLHFOccurrence occurrence, String text) + { + var parsedElements = ParseFormattedHeaderFooterText(text); + + if (parsedElements.Any(e => e.Position == 'L')) + this.Left.AddText(string.Join("\r\n", parsedElements.Where(e => e.Position == 'L').Select(e => e.Text).ToArray()), occurrence); + + if (parsedElements.Any(e => e.Position == 'C')) + this.Center.AddText(string.Join("\r\n", parsedElements.Where(e => e.Position == 'C').Select(e => e.Text).ToArray()), occurrence); + + if (parsedElements.Any(e => e.Position == 'R')) + this.Right.AddText(string.Join("\r\n", parsedElements.Where(e => e.Position == 'R').Select(e => e.Text).ToArray()), occurrence); + + innerTexts[occurrence] = text; + } + + private struct ParsedHeaderFooterElement + { + public char Position; + public string Text; + } + + private static IEnumerable ParseFormattedHeaderFooterText(string text) + { + Func IsAtPositionIndicator = i => i < text.Length - 1 && text[i] == '&' && (new char[] { 'L', 'C', 'R' }.Contains(text[i + 1])); + + var parsedElements = new List(); + var currentPosition = 'L'; // default is LEFT + var hfElement = ""; + + for (int i = 0; i < text.Length; i++) + { + if (IsAtPositionIndicator(i)) + { + if ("" != hfElement) parsedElements.Add(new ParsedHeaderFooterElement() + { + Position = currentPosition, + Text = hfElement + }); + + currentPosition = text[i + 1]; + i += 2; + hfElement = ""; + } + + if (i < text.Length) + { + if (IsAtPositionIndicator(i)) + i--; + else + hfElement += text[i]; + } + } + + if ("" != hfElement) + parsedElements.Add(new ParsedHeaderFooterElement() + { + Position = currentPosition, + Text = hfElement + }); + return parsedElements; + } + + private Dictionary _initialTexts; + + private Boolean _changed; + internal Boolean Changed + { + get + { + return _changed || _initialTexts.Any(it => GetText(it.Key) != it.Value); + } + set { _changed = value; } + } + + internal void SetAsInitial() + { + _initialTexts = new Dictionary(); + foreach (var o in Enum.GetValues(typeof(XLHFOccurrence)).Cast()) + { + _initialTexts.Add(o, GetText(o)); + } + } + + + public IXLHeaderFooter Clear(XLHFOccurrence occurrence = XLHFOccurrence.AllPages) + { + Left.Clear(occurrence); + Right.Clear(occurrence); + Center.Clear(occurrence); + return this; + } + } +} diff --git a/ClosedXML/Excel/PageSetup/XLMargins.cs b/ClosedXML/Excel/PageSetup/XLMargins.cs new file mode 100644 index 0000000..9643542 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLMargins.cs @@ -0,0 +1,22 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLMargins: IXLMargins + { + public Double Left { get; set; } + public Double Right { get; set; } + public Double Top { get; set; } + public Double Bottom { get; set; } + public Double Header { get; set; } + public Double Footer { get; set; } + + public IXLMargins SetLeft(Double value) { Left = value; return this; } + public IXLMargins SetRight(Double value) { Right = value; return this; } + public IXLMargins SetTop(Double value) { Top = value; return this; } + public IXLMargins SetBottom(Double value) { Bottom = value; return this; } + public IXLMargins SetHeader(Double value) { Header = value; return this; } + public IXLMargins SetFooter(Double value) { Footer = value; return this; } + + } +} diff --git a/ClosedXML/Excel/PageSetup/XLPageSetup.cs b/ClosedXML/Excel/PageSetup/XLPageSetup.cs new file mode 100644 index 0000000..ecb5430 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLPageSetup.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPageSetup : IXLPageSetup + { + public XLPageSetup(XLPageSetup defaultPageOptions, XLWorksheet worksheet) + { + + if (defaultPageOptions != null) + { + PrintAreas = new XLPrintAreas(defaultPageOptions.PrintAreas as XLPrintAreas, worksheet); + CenterHorizontally = defaultPageOptions.CenterHorizontally; + CenterVertically = defaultPageOptions.CenterVertically; + FirstPageNumber = defaultPageOptions.FirstPageNumber; + HorizontalDpi = defaultPageOptions.HorizontalDpi; + PageOrientation = defaultPageOptions.PageOrientation; + VerticalDpi = defaultPageOptions.VerticalDpi; + FirstRowToRepeatAtTop = defaultPageOptions.FirstRowToRepeatAtTop; + LastRowToRepeatAtTop = defaultPageOptions.LastRowToRepeatAtTop; + FirstColumnToRepeatAtLeft = defaultPageOptions.FirstColumnToRepeatAtLeft; + LastColumnToRepeatAtLeft = defaultPageOptions.LastColumnToRepeatAtLeft; + ShowComments = defaultPageOptions.ShowComments; + + + PaperSize = defaultPageOptions.PaperSize; + _pagesTall = defaultPageOptions.PagesTall; + _pagesWide = defaultPageOptions.PagesWide; + _scale = defaultPageOptions.Scale; + + + if (defaultPageOptions.Margins != null) + { + Margins = new XLMargins + { + Top = defaultPageOptions.Margins.Top, + Bottom = defaultPageOptions.Margins.Bottom, + Left = defaultPageOptions.Margins.Left, + Right = defaultPageOptions.Margins.Right, + Header = defaultPageOptions.Margins.Header, + Footer = defaultPageOptions.Margins.Footer + }; + } + AlignHFWithMargins = defaultPageOptions.AlignHFWithMargins; + ScaleHFWithDocument = defaultPageOptions.ScaleHFWithDocument; + ShowGridlines = defaultPageOptions.ShowGridlines; + ShowRowAndColumnHeadings = defaultPageOptions.ShowRowAndColumnHeadings; + BlackAndWhite = defaultPageOptions.BlackAndWhite; + DraftQuality = defaultPageOptions.DraftQuality; + PageOrder = defaultPageOptions.PageOrder; + + ColumnBreaks = defaultPageOptions.ColumnBreaks.ToList(); + RowBreaks = defaultPageOptions.RowBreaks.ToList(); + Header = new XLHeaderFooter(defaultPageOptions.Header as XLHeaderFooter, worksheet); + Footer = new XLHeaderFooter(defaultPageOptions.Footer as XLHeaderFooter, worksheet); + PrintErrorValue = defaultPageOptions.PrintErrorValue; + } + else + { + PrintAreas = new XLPrintAreas(worksheet); + Header = new XLHeaderFooter(worksheet); + Footer = new XLHeaderFooter(worksheet); + ColumnBreaks = new List(); + RowBreaks = new List(); + } + } + public IXLPrintAreas PrintAreas { get; private set; } + + + public Int32 FirstRowToRepeatAtTop { get; private set; } + public Int32 LastRowToRepeatAtTop { get; private set; } + public void SetRowsToRepeatAtTop(String range) + { + var arrRange = range.Replace("$", "").Split(':'); + SetRowsToRepeatAtTop(Int32.Parse(arrRange[0]), Int32.Parse(arrRange[1])); + } + public void SetRowsToRepeatAtTop(Int32 firstRowToRepeatAtTop, Int32 lastRowToRepeatAtTop) + { + if (firstRowToRepeatAtTop <= 0) throw new ArgumentOutOfRangeException("The first row has to be greater than zero."); + if (firstRowToRepeatAtTop > lastRowToRepeatAtTop) throw new ArgumentOutOfRangeException("The first row has to be less than the second row."); + + FirstRowToRepeatAtTop = firstRowToRepeatAtTop; + LastRowToRepeatAtTop = lastRowToRepeatAtTop; + } + public Int32 FirstColumnToRepeatAtLeft { get; private set; } + public Int32 LastColumnToRepeatAtLeft { get; private set; } + public void SetColumnsToRepeatAtLeft(String range) + { + var arrRange = range.Replace("$", "").Split(':'); + if (Int32.TryParse(arrRange[0], out int iTest)) + SetColumnsToRepeatAtLeft(Int32.Parse(arrRange[0]), Int32.Parse(arrRange[1])); + else + SetColumnsToRepeatAtLeft(arrRange[0], arrRange[1]); + } + public void SetColumnsToRepeatAtLeft(String firstColumnToRepeatAtLeft, String lastColumnToRepeatAtLeft) + { + SetColumnsToRepeatAtLeft(XLHelper.GetColumnNumberFromLetter(firstColumnToRepeatAtLeft), XLHelper.GetColumnNumberFromLetter(lastColumnToRepeatAtLeft)); + } + public void SetColumnsToRepeatAtLeft(Int32 firstColumnToRepeatAtLeft, Int32 lastColumnToRepeatAtLeft) + { + if (firstColumnToRepeatAtLeft <= 0) throw new ArgumentOutOfRangeException("The first column has to be greater than zero."); + if (firstColumnToRepeatAtLeft > lastColumnToRepeatAtLeft) throw new ArgumentOutOfRangeException("The first column has to be less than the second column."); + + FirstColumnToRepeatAtLeft = firstColumnToRepeatAtLeft; + LastColumnToRepeatAtLeft = lastColumnToRepeatAtLeft; + } + + public XLPageOrientation PageOrientation { get; set; } + public XLPaperSize PaperSize { get; set; } + public Int32 HorizontalDpi { get; set; } + public Int32 VerticalDpi { get; set; } + public UInt32? FirstPageNumber { get; set; } + public Boolean CenterHorizontally { get; set; } + public Boolean CenterVertically { get; set; } + public XLPrintErrorValues PrintErrorValue { get; set; } + public IXLMargins Margins { get; set; } + + private Int32 _pagesWide; + public Int32 PagesWide + { + get + { + return _pagesWide; + } + set + { + _pagesWide = value; + if (_pagesWide >0) + _scale = 0; + } + } + + private Int32 _pagesTall; + public Int32 PagesTall + { + get + { + return _pagesTall; + } + set + { + _pagesTall = value; + if (_pagesTall >0) + _scale = 0; + } + } + + private Int32 _scale; + public Int32 Scale + { + get + { + return _scale; + } + set + { + _scale = value; + if (_scale <= 0) return; + _pagesTall = 0; + _pagesWide = 0; + } + } + + public void AdjustTo(Int32 percentageOfNormalSize) + { + Scale = percentageOfNormalSize; + _pagesWide = 0; + _pagesTall = 0; + } + public void FitToPages(Int32 pagesWide, Int32 pagesTall) + { + _pagesWide = pagesWide; + this._pagesTall = pagesTall; + _scale = 0; + } + + + public IXLHeaderFooter Header { get; private set; } + public IXLHeaderFooter Footer { get; private set; } + + public Boolean ScaleHFWithDocument { get; set; } + public Boolean AlignHFWithMargins { get; set; } + + public Boolean ShowGridlines { get; set; } + public Boolean ShowRowAndColumnHeadings { get; set; } + public Boolean BlackAndWhite { get; set; } + public Boolean DraftQuality { get; set; } + + public XLPageOrderValues PageOrder { get; set; } + public XLShowCommentsValues ShowComments { get; set; } + + public List RowBreaks { get; private set; } + public List ColumnBreaks { get; private set; } + public void AddHorizontalPageBreak(Int32 row) + { + if (!RowBreaks.Contains(row)) + RowBreaks.Add(row); + RowBreaks.Sort(); + } + public void AddVerticalPageBreak(Int32 column) + { + if (!ColumnBreaks.Contains(column)) + ColumnBreaks.Add(column); + ColumnBreaks.Sort(); + } + + //public void SetPageBreak(IXLRange range, XLPageBreakLocations breakLocation) + //{ + // switch (breakLocation) + // { + // case XLPageBreakLocations.AboveRange: RowBreaks.Add(range.Internals.Worksheet.Row(range.RowNumber)); break; + // case XLPageBreakLocations.BelowRange: RowBreaks.Add(range.Internals.Worksheet.Row(range.RowCount())); break; + // case XLPageBreakLocations.LeftOfRange: ColumnBreaks.Add(range.Internals.Worksheet.Column(range.ColumnNumber)); break; + // case XLPageBreakLocations.RightOfRange: ColumnBreaks.Add(range.Internals.Worksheet.Column(range.ColumnCount())); break; + // default: throw new NotImplementedException(); + // } + //} + + public IXLPageSetup SetPageOrientation(XLPageOrientation value) { PageOrientation = value; return this; } + public IXLPageSetup SetPagesWide(Int32 value) { PagesWide = value; return this; } + public IXLPageSetup SetPagesTall(Int32 value) { PagesTall = value; return this; } + public IXLPageSetup SetScale(Int32 value) { Scale = value; return this; } + public IXLPageSetup SetHorizontalDpi(Int32 value) { HorizontalDpi = value; return this; } + public IXLPageSetup SetVerticalDpi(Int32 value) { VerticalDpi = value; return this; } + public IXLPageSetup SetFirstPageNumber(UInt32? value) { FirstPageNumber = value; return this; } + public IXLPageSetup SetCenterHorizontally() { CenterHorizontally = true; return this; } public IXLPageSetup SetCenterHorizontally(Boolean value) { CenterHorizontally = value; return this; } + public IXLPageSetup SetCenterVertically() { CenterVertically = true; return this; } public IXLPageSetup SetCenterVertically(Boolean value) { CenterVertically = value; return this; } + public IXLPageSetup SetPaperSize(XLPaperSize value) { PaperSize = value; return this; } + public IXLPageSetup SetScaleHFWithDocument() { ScaleHFWithDocument = true; return this; } public IXLPageSetup SetScaleHFWithDocument(Boolean value) { ScaleHFWithDocument = value; return this; } + public IXLPageSetup SetAlignHFWithMargins() { AlignHFWithMargins = true; return this; } public IXLPageSetup SetAlignHFWithMargins(Boolean value) { AlignHFWithMargins = value; return this; } + public IXLPageSetup SetShowGridlines() { ShowGridlines = true; return this; } public IXLPageSetup SetShowGridlines(Boolean value) { ShowGridlines = value; return this; } + public IXLPageSetup SetShowRowAndColumnHeadings() { ShowRowAndColumnHeadings = true; return this; } public IXLPageSetup SetShowRowAndColumnHeadings(Boolean value) { ShowRowAndColumnHeadings = value; return this; } + public IXLPageSetup SetBlackAndWhite() { BlackAndWhite = true; return this; } public IXLPageSetup SetBlackAndWhite(Boolean value) { BlackAndWhite = value; return this; } + public IXLPageSetup SetDraftQuality() { DraftQuality = true; return this; } public IXLPageSetup SetDraftQuality(Boolean value) { DraftQuality = value; return this; } + public IXLPageSetup SetPageOrder(XLPageOrderValues value) { PageOrder = value; return this; } + public IXLPageSetup SetShowComments(XLShowCommentsValues value) { ShowComments = value; return this; } + public IXLPageSetup SetPrintErrorValue(XLPrintErrorValues value) { PrintErrorValue = value; return this; } + + public Boolean DifferentFirstPageOnHF { get; set; } + public IXLPageSetup SetDifferentFirstPageOnHF() + { + return SetDifferentFirstPageOnHF(true); + } + public IXLPageSetup SetDifferentFirstPageOnHF(Boolean value) + { + DifferentFirstPageOnHF = value; + return this; + } + public Boolean DifferentOddEvenPagesOnHF { get; set; } + public IXLPageSetup SetDifferentOddEvenPagesOnHF() + { + return SetDifferentOddEvenPagesOnHF(true); + } + public IXLPageSetup SetDifferentOddEvenPagesOnHF(Boolean value) + { + DifferentOddEvenPagesOnHF = value; + return this; + } + } +} diff --git a/ClosedXML/Excel/PageSetup/XLPrintAreas.cs b/ClosedXML/Excel/PageSetup/XLPrintAreas.cs new file mode 100644 index 0000000..6aea4b9 --- /dev/null +++ b/ClosedXML/Excel/PageSetup/XLPrintAreas.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPrintAreas : IXLPrintAreas + { + List ranges = new List(); + private XLWorksheet worksheet; + public XLPrintAreas(XLWorksheet worksheet) + { + this.worksheet = worksheet; + } + + public XLPrintAreas(XLPrintAreas defaultPrintAreas, XLWorksheet worksheet) + { + ranges = defaultPrintAreas.ranges.ToList(); + this.worksheet = worksheet; + } + + public void Clear() + { + ranges.Clear(); + } + + public void Add(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn) + { + ranges.Add(worksheet.Range(firstCellRow, firstCellColumn, lastCellRow, lastCellColumn)); + } + + public void Add(string rangeAddress) + { + ranges.Add(worksheet.Range(rangeAddress)); + } + + public void Add(string firstCellAddress, string lastCellAddress) + { + ranges.Add(worksheet.Range(firstCellAddress, lastCellAddress)); + } + + public void Add(IXLAddress firstCellAddress, IXLAddress lastCellAddress) + { + ranges.Add(worksheet.Range(firstCellAddress, lastCellAddress)); + } + + public IEnumerator GetEnumerator() + { + return ranges.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/ClosedXML/Excel/Patterns/Quadrant.cs b/ClosedXML/Excel/Patterns/Quadrant.cs new file mode 100644 index 0000000..5591ff5 --- /dev/null +++ b/ClosedXML/Excel/Patterns/Quadrant.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.Patterns +{ + /// + /// Implementation of QuadTree adapted to Excel worksheet specifics. Differences with the classic implementation + /// are that the topmost level is split to 128 square parts (2 columns of 64 blocks, each 8192*8192 cells) and that splitting + /// the quadrant onto 4 smaller quadrants does not depend on the number of items in this quadrant. When the range is added to the + /// QuadTree it is placed on the bottommost level where it fits to a single quadrant. That means, row-wide and column-wide ranges + /// are always placed at the level 0, and the smaller the range is the deeper it goes down the tree. This approach eliminates + /// the need of transferring ranges between levels. + /// + internal class Quadrant + { + #region Public Properties + + /// + /// Smaller quadrants which the current one is splitted to. Is NULL until ranges are added to child quadrants. + /// + public IEnumerable Children { get; private set; } + + /// + /// The level of current quadrant. Top most has level 0, child quadrants has levels (Level + 1). + /// + public byte Level { get; } + + /// + /// Minimum column included in this quadrant. + /// + public int MinimumColumn { get; } + + /// + /// Minimum row included in this quadrant. + /// + public int MinimumRow { get; } + + /// + /// Maximum column included in this quadrant. + /// + public int MaximumColumn { get; } + + /// + /// Maximum row included in this quadrant. + /// + public int MaximumRow { get; } + + /// + /// Collection of ranges belonging to this quadrant (does not include ranges from child quadrants). + /// + public IEnumerable Ranges + { + get => _ranges?.Values.AsEnumerable(); + } + + /// + /// The number of current quadrant by horizontal axis. + /// + public short X { get; private set; } + + /// + /// The number of current quadrant by vertical axis. + /// + public short Y { get; private set; } + + #endregion Public Properties + + #region Constructors + + public Quadrant() : this(0, 0, 0) + { } + + private Quadrant(byte level, short x, short y) + { + Level = level; + X = x; + Y = y; + + MinimumColumn = (Level == 0) ? 1 : 1 + XLHelper.MaxColumnNumber / (int)Math.Pow(2, Level) * X; + MinimumRow = (Level == 0) ? 1 : 1 + XLHelper.MaxColumnNumber / (int)Math.Pow(2, Level) * Y; //MaxColumnNumber here is not a mistake + MaximumColumn = (Level == 0) + ? XLHelper.MaxColumnNumber + : XLHelper.MaxColumnNumber / (int)Math.Pow(2, Level) * (X + 1); + MaximumRow = (Level == 0) + ? XLHelper.MaxRowNumber + : XLHelper.MaxColumnNumber / (int)Math.Pow(2, Level) * (Y + 1); //MaxColumnNumber here is not a mistake + } + + #endregion Constructors + + #region Public Methods + + /// + /// Add a range to the quadrant or to one of the child quadrants (recursively). + /// + /// True, if range was successfully added, false if it has been added before. + public bool Add(IXLAddressable range) + { + bool res = false; + var children = Children ?? CreateChildren().ToList(); + bool addToChild = false; + foreach (var childQuadrant in children) + { + var rangeAddress = range.RangeAddress; + if (childQuadrant.Covers(in rangeAddress)) + { + res |= childQuadrant.Add(range); + addToChild = true; + break; + } + } + + if (!addToChild) + res = AddInternal(range); + + if (Children == null && addToChild) + Children = children; + + return res; + } + + /// + /// Get all ranges from the quadrant and all child quadrants (recursively). + /// + public IEnumerable GetAll() + { + if (Ranges != null) + { + foreach (var range in Ranges) + yield return range; + } + + if (Children != null) + { + foreach (var childQuadrant in Children) + { + var childRanges = childQuadrant.GetAll(); + foreach (var range in childRanges) + yield return range; + } + } + } + + /// + /// Get all ranges from the quadrant and all child quadrants (recursively) that intersect the specified address. + /// + public IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) + { + if (Ranges != null) + { + foreach (var range in Ranges) + { + if (range.RangeAddress.Intersects(rangeAddress)) + yield return range; + } + } + + if (Children != null) + { + foreach (var childQuadrant in Children) + { + if (childQuadrant.Intersects(in rangeAddress)) + { + var childRanges = childQuadrant.GetIntersectedRanges(rangeAddress); + foreach (var range in childRanges) + yield return range; + } + } + } + } + + /// + /// Get all ranges from the quadrant and all child quadrants (recursively) that cover the specified address. + /// + public IEnumerable GetIntersectedRanges(IXLAddress address) + { + if (Ranges != null) + { + foreach (var range in Ranges) + { + if (range.RangeAddress.Contains(address)) + yield return range; + } + } + + if (Children != null) + { + foreach (var childQuadrant in Children) + { + if (childQuadrant.Covers(in address)) + { + var childRanges = childQuadrant.GetIntersectedRanges(address); + foreach (var range in childRanges) + yield return range; + } + } + } + } + + /// + /// Remove the range from the quadrant or from child quadrants (recursively). + /// + /// True if the range was removed, false if it does not exist in the QuadTree. + public bool Remove(IXLRangeAddress rangeAddress) + { + bool res = false; + + bool coveredByChild = false; + if (Children != null) + { + foreach (var childQuadrant in Children) + { + if (childQuadrant.Covers(rangeAddress)) + { + res |= childQuadrant.Remove(rangeAddress); + coveredByChild = true; + } + } + } + + if (!coveredByChild) + { + if (_ranges?.Remove(rangeAddress) == true) + res = true; + } + + return res; + } + + /// + /// Remove all the ranges matching specified criteria from the quadrant and its child quadrants (recursively). + /// Don't use it for searching intersections as it would be much less efficient than . + /// + public IEnumerable RemoveAll(Predicate predicate) + { + if (_ranges != null) + { + var ranges = _ranges.Values.Where(r => predicate(r)); + var keysToRemove = new List(); + foreach (var range in ranges) + { + keysToRemove.Add(range.RangeAddress); + yield return range; + } + + foreach (var keyToRemove in keysToRemove) + { + _ranges.Remove(keyToRemove); + } + } + + if (Children != null) + { + foreach (var childQuadrant in Children) + foreach (var childRange in childQuadrant.RemoveAll(predicate)) + { + yield return childRange; + } + } + } + + #endregion Public Methods + + #region Private Fields + + /// + /// Maximum depth of the QuadTree. Value 10 corresponds to the smallest quadrants having size 16*16 cells. + /// + private const byte MAX_LEVEL = 10; + + /// + /// Collection of ranges belonging to the current quadrant (that cannot fit into child quadrants). + /// + private Dictionary _ranges; + + #endregion Private Fields + + #region Private Methods + + /// + /// Add a range to the collection of quadrant's own ranges. + /// + /// True if the range was successfully added, false if it had been added before. + private bool AddInternal(IXLAddressable range) + { + if (_ranges == null) + _ranges = new Dictionary(); + + if (_ranges.ContainsKey(range.RangeAddress)) + return false; + + _ranges.Add(range.RangeAddress, range); + return true; + } + + /// + /// Check if the current quadrant fully covers the specified address. + /// + private bool Covers(in IXLRangeAddress rangeAddress) + { + return MinimumColumn <= rangeAddress.FirstAddress.ColumnNumber && + MaximumColumn >= rangeAddress.LastAddress.ColumnNumber && + MinimumRow <= rangeAddress.FirstAddress.RowNumber && + MaximumRow >= rangeAddress.LastAddress.RowNumber; + } + + /// + /// Check if the current quadrant covers the specified address. + /// + private bool Covers(in IXLAddress address) + { + return MinimumColumn <= address.ColumnNumber && + MaximumColumn >= address.ColumnNumber && + MinimumRow <= address.RowNumber && + MaximumRow >= address.RowNumber; + } + + /// + /// Check if the current quadrant intersects the specified address. + /// + private bool Intersects(in IXLRangeAddress rangeAddress) + { + return ((MinimumRow <= rangeAddress.FirstAddress.RowNumber && rangeAddress.FirstAddress.RowNumber <= MaximumRow) || + (rangeAddress.FirstAddress.RowNumber <= MinimumRow && MinimumRow <= rangeAddress.LastAddress.RowNumber)) + && + ((MinimumColumn <= rangeAddress.FirstAddress.ColumnNumber && rangeAddress.FirstAddress.ColumnNumber <= MaximumColumn) || + (rangeAddress.FirstAddress.ColumnNumber <= MinimumColumn && MinimumColumn <= rangeAddress.LastAddress.ColumnNumber)); + } + + /// + /// Create a collection of child quadrants dividing the current one. + /// + private IEnumerable CreateChildren() + { + byte childLevel = (byte)(Level + 1); + if (childLevel > MAX_LEVEL) + yield break; + byte xCount = 2; // Always divide on halves + byte yCount = (byte)((Level == 0) ? (XLHelper.MaxRowNumber / XLHelper.MaxColumnNumber) : 2); // Level 0 divide onto 64 parts, the rest - on halves + + for (byte dy = 0; dy < yCount; dy++) + { + for (byte dx = 0; dx < xCount; dx++) + { + yield return new Quadrant(childLevel, (short)(X * 2 + dx), (short)(Y * 2 + dy)); + } + } + } + + #endregion Private Methods + } + + /// + /// A generic version of + /// + internal class Quadrant : Quadrant + where T:IXLAddressable + { + public new IEnumerable Ranges => base.Ranges.Cast(); + + public bool Add(T range) + { + return base.Add(range); + } + + public new IEnumerable GetAll() + { + return base.GetAll().Cast(); + } + + public new IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) + { + return base.GetIntersectedRanges(rangeAddress).Cast(); + } + + public new IEnumerable GetIntersectedRanges(IXLAddress address) + { + return base.GetIntersectedRanges(address).Cast(); + } + + public bool Remove(T range) + { + return Remove(range.RangeAddress); + } + public IEnumerable RemoveAll(Predicate predicate) + { + return base.RemoveAll(r => predicate((T) r)).Cast(); + } + } +} diff --git a/ClosedXML/Excel/PivotTables/IXLPivotField.cs b/ClosedXML/Excel/PivotTables/IXLPivotField.cs new file mode 100644 index 0000000..ba719e4 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/IXLPivotField.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLSubtotalFunction + { + Automatic, + None, + Sum, + Count, + Average, + Minimum, + Maximum, + Product, + CountNumbers, + StandardDeviation, + PopulationStandardDeviation, + Variance, + PopulationVariance, + } + + public enum XLPivotLayout { Outline, Tabular, Compact } + + public interface IXLPivotField + { + String SourceName { get; } + String CustomName { get; set; } + String SubtotalCaption { get; set; } + + List Subtotals { get; } + Boolean IncludeNewItemsInFilter { get; set; } + + Boolean Outline { get; set; } + Boolean Compact { get; set; } + Boolean? SubtotalsAtTop { get; set; } + Boolean RepeatItemLabels { get; set; } + Boolean InsertBlankLines { get; set; } + Boolean ShowBlankItems { get; set; } + Boolean InsertPageBreaks { get; set; } + Boolean Collapsed { get; set; } + XLPivotSortType SortType { get; set; } + + IXLPivotField SetCustomName(String value); + + IXLPivotField SetSubtotalCaption(String value); + + IXLPivotField AddSubtotal(XLSubtotalFunction value); + + IXLPivotField SetIncludeNewItemsInFilter(); IXLPivotField SetIncludeNewItemsInFilter(Boolean value); + + IXLPivotField SetLayout(XLPivotLayout value); + + IXLPivotField SetSubtotalsAtTop(); IXLPivotField SetSubtotalsAtTop(Boolean value); + + IXLPivotField SetRepeatItemLabels(); IXLPivotField SetRepeatItemLabels(Boolean value); + + IXLPivotField SetInsertBlankLines(); IXLPivotField SetInsertBlankLines(Boolean value); + + IXLPivotField SetShowBlankItems(); IXLPivotField SetShowBlankItems(Boolean value); + + IXLPivotField SetInsertPageBreaks(); IXLPivotField SetInsertPageBreaks(Boolean value); + + IXLPivotField SetCollapsed(); IXLPivotField SetCollapsed(Boolean value); + + IXLPivotField SetSort(XLPivotSortType value); + + IList SelectedValues { get; } + + IXLPivotField AddSelectedValue(Object value); + + IXLPivotFieldStyleFormats StyleFormats { get; } + + Boolean IsOnRowAxis { get; } + Boolean IsOnColumnAxis { get; } + Boolean IsInFilterList { get; } + + Int32 Offset { get; } + } +} diff --git a/ClosedXML/Excel/PivotTables/IXLPivotFields.cs b/ClosedXML/Excel/PivotTables/IXLPivotFields.cs new file mode 100644 index 0000000..148120d --- /dev/null +++ b/ClosedXML/Excel/PivotTables/IXLPivotFields.cs @@ -0,0 +1,28 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLPivotFields : IEnumerable + { + IXLPivotField Add(String sourceName); + + IXLPivotField Add(String sourceName, String customName); + + void Clear(); + + Boolean Contains(String sourceName); + + Boolean Contains(IXLPivotField pivotField); + + IXLPivotField Get(String sourceName); + + IXLPivotField Get(Int32 index); + + Int32 IndexOf(String sourceName); + Int32 IndexOf(IXLPivotField pf); + + void Remove(String sourceName); + } +} diff --git a/ClosedXML/Excel/PivotTables/IXLPivotTable.cs b/ClosedXML/Excel/PivotTables/IXLPivotTable.cs new file mode 100644 index 0000000..d818d4e --- /dev/null +++ b/ClosedXML/Excel/PivotTables/IXLPivotTable.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLPivotTableTheme + { + None, + PivotStyleDark1, + PivotStyleDark10, + PivotStyleDark11, + PivotStyleDark12, + PivotStyleDark13, + PivotStyleDark14, + PivotStyleDark15, + PivotStyleDark16, + PivotStyleDark17, + PivotStyleDark18, + PivotStyleDark19, + PivotStyleDark2, + PivotStyleDark20, + PivotStyleDark21, + PivotStyleDark22, + PivotStyleDark23, + PivotStyleDark24, + PivotStyleDark25, + PivotStyleDark26, + PivotStyleDark27, + PivotStyleDark28, + PivotStyleDark3, + PivotStyleDark4, + PivotStyleDark5, + PivotStyleDark6, + PivotStyleDark7, + PivotStyleDark8, + PivotStyleDark9, + PivotStyleLight1, + PivotStyleLight10, + PivotStyleLight11, + PivotStyleLight12, + PivotStyleLight13, + PivotStyleLight14, + PivotStyleLight15, + PivotStyleLight16, + PivotStyleLight17, + PivotStyleLight18, + PivotStyleLight19, + PivotStyleLight2, + PivotStyleLight20, + PivotStyleLight21, + PivotStyleLight22, + PivotStyleLight23, + PivotStyleLight24, + PivotStyleLight25, + PivotStyleLight26, + PivotStyleLight27, + PivotStyleLight28, + PivotStyleLight3, + PivotStyleLight4, + PivotStyleLight5, + PivotStyleLight6, + PivotStyleLight7, + PivotStyleLight8, + PivotStyleLight9, + PivotStyleMedium1, + PivotStyleMedium10, + PivotStyleMedium11, + PivotStyleMedium12, + PivotStyleMedium13, + PivotStyleMedium14, + PivotStyleMedium15, + PivotStyleMedium16, + PivotStyleMedium17, + PivotStyleMedium18, + PivotStyleMedium19, + PivotStyleMedium2, + PivotStyleMedium20, + PivotStyleMedium21, + PivotStyleMedium22, + PivotStyleMedium23, + PivotStyleMedium24, + PivotStyleMedium25, + PivotStyleMedium26, + PivotStyleMedium27, + PivotStyleMedium28, + PivotStyleMedium3, + PivotStyleMedium4, + PivotStyleMedium5, + PivotStyleMedium6, + PivotStyleMedium7, + PivotStyleMedium8, + PivotStyleMedium9 + } + + public enum XLPivotSortType + { + Default = 0, + Ascending = 1, + Descending = 2 + } + + public enum XLPivotSubtotals + { + DoNotShow, + AtTop, + AtBottom + } + + public enum XLFilterAreaOrder { DownThenOver, OverThenDown } + + public enum XLItemsToRetain { Automatic, None, Max } + + public enum XLPivotTableSourceType { Range, Table } + + public interface IXLPivotTable + { + XLPivotTableTheme Theme { get; set; } + + IXLPivotFields Fields { get; } + IXLPivotFields ReportFilters { get; } + IXLPivotFields ColumnLabels { get; } + IXLPivotFields RowLabels { get; } + IXLPivotValues Values { get; } + + String Name { get; set; } + String Title { get; set; } + String Description { get; set; } + + String ColumnHeaderCaption { get; set; } + String RowHeaderCaption { get; set; } + + IXLCell TargetCell { get; set; } + + IXLRange SourceRange { get; set; } + IXLTable SourceTable { get; set; } + XLPivotTableSourceType SourceType { get; } + + IEnumerable SourceRangeFieldsAvailable { get; } + + Boolean MergeAndCenterWithLabels { get; set; } // MergeItem + Int32 RowLabelIndent { get; set; } // Indent + XLFilterAreaOrder FilterAreaOrder { get; set; } // PageOverThenDown + Int32 FilterFieldsPageWrap { get; set; } // PageWrap + String ErrorValueReplacement { get; set; } // ErrorCaption + String EmptyCellReplacement { get; set; } // MissingCaption + Boolean AutofitColumns { get; set; } //UseAutoFormatting + Boolean PreserveCellFormatting { get; set; } // PreserveFormatting + + Boolean ShowGrandTotalsRows { get; set; } // RowGrandTotals + Boolean ShowGrandTotalsColumns { get; set; } // ColumnGrandTotals + Boolean FilteredItemsInSubtotals { get; set; } // Subtotal filtered page items + Boolean AllowMultipleFilters { get; set; } // MultipleFieldFilters + Boolean UseCustomListsForSorting { get; set; } // CustomListSort + + Boolean ShowExpandCollapseButtons { get; set; } + Boolean ShowContextualTooltips { get; set; } + Boolean ShowPropertiesInTooltips { get; set; } + Boolean DisplayCaptionsAndDropdowns { get; set; } + Boolean ClassicPivotTableLayout { get; set; } + Boolean ShowValuesRow { get; set; } + Boolean ShowEmptyItemsOnRows { get; set; } + Boolean ShowEmptyItemsOnColumns { get; set; } + Boolean DisplayItemLabels { get; set; } + Boolean SortFieldsAtoZ { get; set; } + + Boolean PrintExpandCollapsedButtons { get; set; } + Boolean RepeatRowLabels { get; set; } + Boolean PrintTitles { get; set; } + + Boolean SaveSourceData { get; set; } + Boolean EnableShowDetails { get; set; } + Boolean RefreshDataOnOpen { get; set; } + XLItemsToRetain ItemsToRetainPerField { get; set; } + Boolean EnableCellEditing { get; set; } + + IXLPivotTable SetName(String value); + + IXLPivotTable SetTitle(String value); + + IXLPivotTable SetDescription(String value); + + IXLPivotTable SetMergeAndCenterWithLabels(); IXLPivotTable SetMergeAndCenterWithLabels(Boolean value); + + IXLPivotTable SetRowLabelIndent(Int32 value); + + IXLPivotTable SetFilterAreaOrder(XLFilterAreaOrder value); + + IXLPivotTable SetFilterFieldsPageWrap(Int32 value); + + IXLPivotTable SetErrorValueReplacement(String value); + + IXLPivotTable SetEmptyCellReplacement(String value); + + IXLPivotTable SetAutofitColumns(); IXLPivotTable SetAutofitColumns(Boolean value); + + IXLPivotTable SetPreserveCellFormatting(); IXLPivotTable SetPreserveCellFormatting(Boolean value); + + IXLPivotTable SetShowGrandTotalsRows(); IXLPivotTable SetShowGrandTotalsRows(Boolean value); + + IXLPivotTable SetShowGrandTotalsColumns(); IXLPivotTable SetShowGrandTotalsColumns(Boolean value); + + IXLPivotTable SetFilteredItemsInSubtotals(); IXLPivotTable SetFilteredItemsInSubtotals(Boolean value); + + IXLPivotTable SetAllowMultipleFilters(); IXLPivotTable SetAllowMultipleFilters(Boolean value); + + IXLPivotTable SetUseCustomListsForSorting(); IXLPivotTable SetUseCustomListsForSorting(Boolean value); + + IXLPivotTable SetShowExpandCollapseButtons(); IXLPivotTable SetShowExpandCollapseButtons(Boolean value); + + IXLPivotTable SetShowContextualTooltips(); IXLPivotTable SetShowContextualTooltips(Boolean value); + + IXLPivotTable SetShowPropertiesInTooltips(); IXLPivotTable SetShowPropertiesInTooltips(Boolean value); + + IXLPivotTable SetDisplayCaptionsAndDropdowns(); IXLPivotTable SetDisplayCaptionsAndDropdowns(Boolean value); + + IXLPivotTable SetClassicPivotTableLayout(); IXLPivotTable SetClassicPivotTableLayout(Boolean value); + + IXLPivotTable SetShowValuesRow(); IXLPivotTable SetShowValuesRow(Boolean value); + + IXLPivotTable SetShowEmptyItemsOnRows(); IXLPivotTable SetShowEmptyItemsOnRows(Boolean value); + + IXLPivotTable SetShowEmptyItemsOnColumns(); IXLPivotTable SetShowEmptyItemsOnColumns(Boolean value); + + IXLPivotTable SetDisplayItemLabels(); IXLPivotTable SetDisplayItemLabels(Boolean value); + + IXLPivotTable SetSortFieldsAtoZ(); IXLPivotTable SetSortFieldsAtoZ(Boolean value); + + IXLPivotTable SetPrintExpandCollapsedButtons(); IXLPivotTable SetPrintExpandCollapsedButtons(Boolean value); + + IXLPivotTable SetRepeatRowLabels(); IXLPivotTable SetRepeatRowLabels(Boolean value); + + IXLPivotTable SetPrintTitles(); IXLPivotTable SetPrintTitles(Boolean value); + + IXLPivotTable SetSaveSourceData(); IXLPivotTable SetSaveSourceData(Boolean value); + + IXLPivotTable SetEnableShowDetails(); IXLPivotTable SetEnableShowDetails(Boolean value); + + IXLPivotTable SetRefreshDataOnOpen(); IXLPivotTable SetRefreshDataOnOpen(Boolean value); + + IXLPivotTable SetItemsToRetainPerField(XLItemsToRetain value); + + IXLPivotTable SetEnableCellEditing(); IXLPivotTable SetEnableCellEditing(Boolean value); + + IXLPivotTable SetColumnHeaderCaption(String value); + + IXLPivotTable SetRowHeaderCaption(String value); + + Boolean ShowRowHeaders { get; set; } + Boolean ShowColumnHeaders { get; set; } + Boolean ShowRowStripes { get; set; } + Boolean ShowColumnStripes { get; set; } + XLPivotSubtotals Subtotals { get; set; } + XLPivotLayout Layout { set; } + Boolean InsertBlankLines { set; } + + IXLPivotTable SetShowRowHeaders(); IXLPivotTable SetShowRowHeaders(Boolean value); + + IXLPivotTable SetShowColumnHeaders(); IXLPivotTable SetShowColumnHeaders(Boolean value); + + IXLPivotTable SetShowRowStripes(); IXLPivotTable SetShowRowStripes(Boolean value); + + IXLPivotTable SetShowColumnStripes(); IXLPivotTable SetShowColumnStripes(Boolean value); + + IXLPivotTable SetSubtotals(XLPivotSubtotals value); + + IXLPivotTable SetLayout(XLPivotLayout value); + + IXLPivotTable SetInsertBlankLines(); IXLPivotTable SetInsertBlankLines(Boolean value); + + IXLWorksheet Worksheet { get; } + + IXLPivotTableStyleFormats StyleFormats { get; } + } +} diff --git a/ClosedXML/Excel/PivotTables/IXLPivotTables.cs b/ClosedXML/Excel/PivotTables/IXLPivotTables.cs new file mode 100644 index 0000000..45ca935 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/IXLPivotTables.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLPivotTables : IEnumerable + { + IXLPivotTable Add(String name, IXLCell targetCell, IXLRange range); + + IXLPivotTable Add(String name, IXLCell targetCell, IXLTable table); + + [Obsolete("Use Add instead")] + IXLPivotTable AddNew(String name, IXLCell targetCell, IXLRange range); + + [Obsolete("Use Add instead")] + IXLPivotTable AddNew(String name, IXLCell targetCell, IXLTable table); + + Boolean Contains(String name); + + void Delete(String name); + + void DeleteAll(); + + IXLPivotTable PivotTable(String name); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/AbstractPivotFieldReference.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/AbstractPivotFieldReference.cs new file mode 100644 index 0000000..e22635c --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/AbstractPivotFieldReference.cs @@ -0,0 +1,19 @@ +using DocumentFormat.OpenXml; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal abstract class AbstractPivotFieldReference + { + public Boolean DefaultSubtotal { get; set; } + + internal abstract UInt32Value GetFieldOffset(); + + /// + ///

Helper function used during saving to calculate the indices of the filtered values

+ ///
+ /// Indices of the filtered values + internal abstract IEnumerable Match(XLWorkbook.PivotTableInfo pti, IXLPivotTable pt); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotFieldStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotFieldStyleFormats.cs new file mode 100644 index 0000000..31d2454 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotFieldStyleFormats.cs @@ -0,0 +1,11 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + public interface IXLPivotFieldStyleFormats + { + IXLPivotValueStyleFormat DataValuesFormat { get; } + IXLPivotStyleFormat Header { get; } + IXLPivotStyleFormat Label { get; } + IXLPivotStyleFormat Subtotal { get; } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormat.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormat.cs new file mode 100644 index 0000000..848ea04 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormat.cs @@ -0,0 +1,10 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + public interface IXLPivotStyleFormat + { + XLPivotStyleFormatElement AppliesTo { get; } + IXLPivotField PivotField { get; } + IXLStyle Style { get; set; } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormats.cs new file mode 100644 index 0000000..4f2b843 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotStyleFormats.cs @@ -0,0 +1,10 @@ +// Keep this file CodeMaid organised and cleaned +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLPivotStyleFormats : IEnumerable + { + IXLPivotStyleFormat ForElement(XLPivotStyleFormatElement element); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotTableStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotTableStyleFormats.cs new file mode 100644 index 0000000..a1f0456 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotTableStyleFormats.cs @@ -0,0 +1,9 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + public interface IXLPivotTableStyleFormats + { + IXLPivotStyleFormats ColumnGrandTotalFormats { get; } + IXLPivotStyleFormats RowGrandTotalFormats { get; } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotValueStyleFormat.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotValueStyleFormat.cs new file mode 100644 index 0000000..c12c719 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/IXLPivotValueStyleFormat.cs @@ -0,0 +1,14 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLPivotValueStyleFormat : IXLPivotStyleFormat + { + IXLPivotValueStyleFormat AndWith(IXLPivotField field); + + IXLPivotValueStyleFormat AndWith(IXLPivotField field, Predicate predicate); + + IXLPivotValueStyleFormat ForValueField(IXLPivotValue valueField); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotLabelFieldReference.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotLabelFieldReference.cs new file mode 100644 index 0000000..09570fd --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotLabelFieldReference.cs @@ -0,0 +1,42 @@ +using DocumentFormat.OpenXml; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class PivotLabelFieldReference : AbstractPivotFieldReference + { + private readonly Predicate predicate; + + public PivotLabelFieldReference(IXLPivotField pivotField) + : this(pivotField, null) + { } + + public PivotLabelFieldReference(IXLPivotField pivotField, Predicate predicate) + { + this.PivotField = pivotField ?? throw new ArgumentNullException(nameof(pivotField)); + this.predicate = predicate; + } + + public IXLPivotField PivotField { get; set; } + + internal override UInt32Value GetFieldOffset() + { + return UInt32Value.FromUInt32((uint)PivotField.Offset); + } + + internal override IEnumerable Match(XLWorkbook.PivotTableInfo pti, IXLPivotTable pt) + { + var values = pti.Fields[PivotField.SourceName].DistinctValues.ToList(); + + if (predicate == null) + return new Int32[] { }; + + return values.Select((Value, Index) => new { Value, Index }) + .Where(v => predicate.Invoke(v.Value)) + .Select(v => v.Index) + .ToList(); + } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotValueFieldReference.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotValueFieldReference.cs new file mode 100644 index 0000000..3629446 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/PivotValueFieldReference.cs @@ -0,0 +1,29 @@ +using DocumentFormat.OpenXml; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class PivotValueFieldReference : AbstractPivotFieldReference + { + public PivotValueFieldReference(String value) + { + this.Value = value; + } + + public String Value { get; } + + internal override UInt32Value GetFieldOffset() + { + return UInt32Value.FromUInt32(unchecked((uint)-2)); + } + + internal override IEnumerable Match(XLWorkbook.PivotTableInfo pti, IXLPivotTable pt) + { + return new Int32[] + { + pt.Values.IndexOf(Value.ToString()) + }; + } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotFieldStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotFieldStyleFormats.cs new file mode 100644 index 0000000..4e2840d --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotFieldStyleFormats.cs @@ -0,0 +1,82 @@ +// Keep this file CodeMaid organised and cleaned + +namespace ClosedXML.Excel +{ + internal class XLPivotFieldStyleFormats : IXLPivotFieldStyleFormats + { + private IXLPivotValueStyleFormat dataValuesFormat; + private IXLPivotStyleFormat headerFormat; + private IXLPivotStyleFormat labelFormat; + private IXLPivotStyleFormat subtotalFormat; + + public XLPivotFieldStyleFormats(IXLPivotField field) + { + this.PivotField = field; + } + + public IXLPivotField PivotField { get; } + + #region IXLPivotFieldStyleFormats + + public IXLPivotValueStyleFormat DataValuesFormat + { + get + { + if (dataValuesFormat == null) + { + dataValuesFormat = new XLPivotValueStyleFormat(PivotField) + { + AppliesTo = XLPivotStyleFormatElement.Data + }; + } + return dataValuesFormat; + } + set { dataValuesFormat = value; } + } + + public IXLPivotStyleFormat Header + { + get + { + if (headerFormat == null) + { + headerFormat = new XLPivotStyleFormat(PivotField); + } + return headerFormat; + } + set { headerFormat = value; } + } + + public IXLPivotStyleFormat Label + { + get + { + if (labelFormat == null) + { + labelFormat = new XLPivotStyleFormat(PivotField) + { + AppliesTo = XLPivotStyleFormatElement.Label + }; + } + return labelFormat; + } + set { labelFormat = value; } + } + + public IXLPivotStyleFormat Subtotal + { + get + { + if (subtotalFormat == null) + { + subtotalFormat = new XLPivotStyleFormat(PivotField); + } + + return subtotalFormat; + } + set { subtotalFormat = value; } + } + + #endregion IXLPivotFieldStyleFormats + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormat.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormat.cs new file mode 100644 index 0000000..7d05cfb --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormat.cs @@ -0,0 +1,27 @@ +// Keep this file CodeMaid organised and cleaned +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLPivotStyleFormat : IXLPivotStyleFormat + { + public XLPivotStyleFormat(IXLPivotField field = null, IXLStyle style = null) + { + PivotField = field; + Style = style ?? XLStyle.Default; + } + + #region IXLPivotStyleFormat members + + public XLPivotStyleFormatElement AppliesTo { get; set; } = XLPivotStyleFormatElement.Data; + public IXLPivotField PivotField { get; set; } + public IXLStyle Style { get; set; } + + #endregion IXLPivotStyleFormat members + + internal XLPivotAreaValues AreaType { get; set; } = XLPivotAreaValues.Normal; + internal bool CollapsedLevelsAreSubtotals { get; set; } = false; + internal IList FieldReferences { get; } = new List(); + internal bool Outline { get; set; } = true; + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormatEnums.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormatEnums.cs new file mode 100644 index 0000000..20a9a40 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormatEnums.cs @@ -0,0 +1,24 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + public enum XLPivotStyleFormatElement + { + None = 0, + Label = 1 << 1, + Data = 1 << 2, + + All = Label | Data + } + + internal enum XLPivotStyleFormatTarget + { + PivotTable, + GrandTotal, + Subtotal, + Header, + Label, + Data + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormats.cs new file mode 100644 index 0000000..3f559fa --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotStyleFormats.cs @@ -0,0 +1,61 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLPivotStyleFormats : IXLPivotStyleFormats + { + private readonly IXLPivotField _pivotField; + private readonly Dictionary _styleFormats = new Dictionary(); + + public XLPivotStyleFormats() + : this(null) + { } + + public XLPivotStyleFormats(IXLPivotField pivotField) + { + this._pivotField = pivotField; + } + + #region IXLPivotStyleFormats members + + public IXLPivotStyleFormat ForElement(XLPivotStyleFormatElement element) + { + if (element == XLPivotStyleFormatElement.None) + throw new ArgumentException("Choose an enum value that represents an element", nameof(element)); + + if (!_styleFormats.TryGetValue(element, out IXLPivotStyleFormat pivotStyleFormat)) + { + pivotStyleFormat = new XLPivotStyleFormat(_pivotField) { AppliesTo = element }; + _styleFormats.Add(element, pivotStyleFormat); + } + + return pivotStyleFormat; + } + + public IEnumerator GetEnumerator() + { + return _styleFormats.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion IXLPivotStyleFormats members + + public void Add(IXLPivotStyleFormat styleFormat) + { + _styleFormats.Add(styleFormat.AppliesTo, styleFormat); + } + + public void AddRange(IEnumerable styleFormats) + { + foreach (var styleFormat in styleFormats) + Add(styleFormat); + } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotTableStyleFormats.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotTableStyleFormats.cs new file mode 100644 index 0000000..718b650 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotTableStyleFormats.cs @@ -0,0 +1,25 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + internal class XLPivotTableStyleFormats : IXLPivotTableStyleFormats + { + private IXLPivotStyleFormats columnGrandTotalFormats; + private IXLPivotStyleFormats rowGrandTotalFormats; + + #region IXLPivotTableStyleFormats members + + public IXLPivotStyleFormats ColumnGrandTotalFormats + { + get { return columnGrandTotalFormats ?? (columnGrandTotalFormats = new XLPivotStyleFormats()); } + set { columnGrandTotalFormats = value; } + } + + public IXLPivotStyleFormats RowGrandTotalFormats + { + get { return rowGrandTotalFormats ?? (rowGrandTotalFormats = new XLPivotStyleFormats()); } + set { rowGrandTotalFormats = value; } + } + + #endregion IXLPivotTableStyleFormats members + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotValueStyleFormat.cs b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotValueStyleFormat.cs new file mode 100644 index 0000000..05721c3 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotStyleFormats/XLPivotValueStyleFormat.cs @@ -0,0 +1,33 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLPivotValueStyleFormat : XLPivotStyleFormat, IXLPivotValueStyleFormat + { + public XLPivotValueStyleFormat(IXLPivotField field = null, IXLStyle style = null) + : base(field, style) + { } + + #region IXLPivotValueStyleFormat members + + public IXLPivotValueStyleFormat AndWith(IXLPivotField field) + { + return AndWith(field, null); + } + + public IXLPivotValueStyleFormat AndWith(IXLPivotField field, Predicate predicate) + { + FieldReferences.Add(new PivotLabelFieldReference(field, predicate)); + return this; + } + + public IXLPivotValueStyleFormat ForValueField(IXLPivotValue valueField) + { + FieldReferences.Add(new PivotValueFieldReference(valueField.SourceName)); + return this; + } + + #endregion IXLPivotValueStyleFormat members + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValue.cs b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValue.cs new file mode 100644 index 0000000..3723e82 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValue.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public enum XLPivotSummary + { + Sum, + Count, + Average, + Minimum, + Maximum, + Product, + CountNumbers, + StandardDeviation, + PopulationStandardDeviation, + Variance, + PopulationVariance, + } + + public enum XLPivotCalculation + { + Normal, + DifferenceFrom, + PercentageOf, + PercentageDifferenceFrom, + RunningTotal, + PercentageOfRow, + PercentageOfColumn, + PercentageOfTotal, + Index + } + public enum XLPivotCalculationItem + { + Value, Previous, Next + } + + public interface IXLPivotValue + { + String SourceName { get; } + String CustomName { get; set; } + + IXLPivotValueFormat NumberFormat { get; } + + XLPivotSummary SummaryFormula { get; set; } + XLPivotCalculation Calculation { get; set; } + String BaseField { get; set; } + String BaseItem { get; set; } + XLPivotCalculationItem CalculationItem { get; set; } + + IXLPivotValue SetSummaryFormula(XLPivotSummary value); + IXLPivotValue SetCalculation(XLPivotCalculation value); + IXLPivotValue SetBaseField(String value); + IXLPivotValue SetBaseItem(String value); + IXLPivotValue SetCalculationItem(XLPivotCalculationItem value); + + + IXLPivotValue ShowAsNormal(); + IXLPivotValueCombination ShowAsDifferenceFrom(String fieldSourceName); + IXLPivotValueCombination ShowAsPercentageFrom(String fieldSourceName); + IXLPivotValueCombination ShowAsPercentageDifferenceFrom(String fieldSourceName); + IXLPivotValue ShowAsRunningTotalIn(String fieldSourceName); + IXLPivotValue ShowAsPercentageOfRow(); + IXLPivotValue ShowAsPercentageOfColumn(); + IXLPivotValue ShowAsPercentageOfTotal(); + IXLPivotValue ShowAsIndex(); + + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueCombination.cs b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueCombination.cs new file mode 100644 index 0000000..a6e3a72 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueCombination.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + public interface IXLPivotValueCombination + { + IXLPivotValue And(String item); + IXLPivotValue AndPrevious(); + IXLPivotValue AndNext(); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueFormat.cs b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueFormat.cs new file mode 100644 index 0000000..2923cb5 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValueFormat.cs @@ -0,0 +1,11 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLPivotValueFormat : IXLNumberFormatBase, IEquatable + { + IXLPivotValue SetNumberFormatId(Int32 value); + + IXLPivotValue SetFormat(String value); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValues.cs b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValues.cs new file mode 100644 index 0000000..68b9c9f --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/IXLPivotValues.cs @@ -0,0 +1,29 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLPivotValues : IEnumerable + { + IXLPivotValue Add(String sourceName); + + IXLPivotValue Add(String sourceName, String customName); + + void Clear(); + + Boolean Contains(String sourceName); + + Boolean Contains(IXLPivotValue pivotValue); + + IXLPivotValue Get(String sourceName); + + IXLPivotValue Get(Int32 index); + + Int32 IndexOf(String sourceName); + + Int32 IndexOf(IXLPivotValue pivotValue); + + void Remove(String sourceName); + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValue.cs b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValue.cs new file mode 100644 index 0000000..baed68c --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValue.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLPivotValue: IXLPivotValue + { + public XLPivotValue(string sourceName) + { + SourceName = sourceName; + NumberFormat = new XLPivotValueFormat(this); + } + + public IXLPivotValueFormat NumberFormat { get; private set; } + public String SourceName { get; private set; } + public String CustomName { get; set; } public IXLPivotValue SetCustomName(String value) { CustomName = value; return this; } + + public XLPivotSummary SummaryFormula { get; set; } public IXLPivotValue SetSummaryFormula(XLPivotSummary value) { SummaryFormula = value; return this; } + public XLPivotCalculation Calculation { get; set; } public IXLPivotValue SetCalculation(XLPivotCalculation value) { Calculation = value; return this; } + public String BaseField { get; set; } public IXLPivotValue SetBaseField(String value) { BaseField = value; return this; } + public String BaseItem { get; set; } public IXLPivotValue SetBaseItem(String value) { BaseItem = value; return this; } + public XLPivotCalculationItem CalculationItem { get; set; } public IXLPivotValue SetCalculationItem(XLPivotCalculationItem value) { CalculationItem = value; return this; } + + + public IXLPivotValue ShowAsNormal() + { + return SetCalculation(XLPivotCalculation.Normal); + } + public IXLPivotValueCombination ShowAsDifferenceFrom(String fieldSourceName) + { + BaseField = fieldSourceName; + SetCalculation(XLPivotCalculation.DifferenceFrom); + return new XLPivotValueCombination(this); + } + public IXLPivotValueCombination ShowAsPercentageFrom(String fieldSourceName) + { + BaseField = fieldSourceName; + SetCalculation(XLPivotCalculation.PercentageOf); + return new XLPivotValueCombination(this); + } + public IXLPivotValueCombination ShowAsPercentageDifferenceFrom(String fieldSourceName) + { + BaseField = fieldSourceName; + SetCalculation(XLPivotCalculation.PercentageDifferenceFrom); + return new XLPivotValueCombination(this); + } + public IXLPivotValue ShowAsRunningTotalIn(String fieldSourceName) + { + BaseField = fieldSourceName; + return SetCalculation(XLPivotCalculation.RunningTotal); + } + public IXLPivotValue ShowAsPercentageOfRow() + { + return SetCalculation(XLPivotCalculation.PercentageOfRow); + } + + public IXLPivotValue ShowAsPercentageOfColumn() + { + return SetCalculation(XLPivotCalculation.PercentageOfColumn); + } + + public IXLPivotValue ShowAsPercentageOfTotal() + { + return SetCalculation(XLPivotCalculation.PercentageOfTotal); + } + + public IXLPivotValue ShowAsIndex() + { + return SetCalculation(XLPivotCalculation.Index); + } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueCombination.cs b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueCombination.cs new file mode 100644 index 0000000..5d8c124 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueCombination.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLPivotValueCombination: IXLPivotValueCombination + { + private readonly IXLPivotValue _pivotValue; + public XLPivotValueCombination(IXLPivotValue pivotValue) + { + _pivotValue = pivotValue; + } + public IXLPivotValue And(String item) + { + _pivotValue.BaseItem = item; + _pivotValue.CalculationItem = XLPivotCalculationItem.Value; + return _pivotValue; + } + public IXLPivotValue AndPrevious() + { + _pivotValue.CalculationItem = XLPivotCalculationItem.Previous; + return _pivotValue; + } + public IXLPivotValue AndNext() + { + _pivotValue.CalculationItem = XLPivotCalculationItem.Next; + return _pivotValue; + } + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueFormat.cs b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueFormat.cs new file mode 100644 index 0000000..8ab869b --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValueFormat.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLPivotValueFormat: IXLPivotValueFormat + { + private readonly XLPivotValue _pivotValue; + public XLPivotValueFormat(XLPivotValue pivotValue) + { + _pivotValue = pivotValue; + _format = "General"; + _numberFormatId = 0; + } + + private Int32 _numberFormatId = -1; + public Int32 NumberFormatId + { + get { return _numberFormatId; } + set + { + _numberFormatId = value; + _format = string.Empty; + } + } + + private String _format = String.Empty; + public String Format + { + get { return _format; } + set + { + _format = value; + _numberFormatId = -1; + } + } + + public IXLPivotValue SetNumberFormatId(Int32 value) + { + NumberFormatId = value; + return _pivotValue; + } + public IXLPivotValue SetFormat(String value) + { + Format = value; + + switch (value) + { + case "General": + _numberFormatId = 0; + break; + case "0": + _numberFormatId = 1; + break; + case "0.00": + _numberFormatId = 2; + break; + case "#,##0": + _numberFormatId = 3; + break; + case "#,##0.00": + _numberFormatId = 4; + break; + case "0%": + _numberFormatId = 9; + break; + case "0.00%": + _numberFormatId = 10; + break; + case "0.00E+00": + _numberFormatId = 11; + break; + } + + + return _pivotValue; + } + + #region Overrides + public bool Equals(IXLNumberFormatBase other) + { + return + _numberFormatId == other.NumberFormatId + && _format == other.Format + ; + } + + public override bool Equals(object obj) + { + return Equals((IXLNumberFormatBase)obj); + } + + public override int GetHashCode() + { + return NumberFormatId + ^ Format.GetHashCode(); + } + + #endregion + + } +} diff --git a/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValues.cs b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValues.cs new file mode 100644 index 0000000..186eb5e --- /dev/null +++ b/ClosedXML/Excel/PivotTables/PivotValues/XLPivotValues.cs @@ -0,0 +1,92 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPivotValues : IXLPivotValues + { + private readonly IXLPivotTable _pivotTable; + private readonly Dictionary _pivotValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal XLPivotValues(IXLPivotTable pivotTable) + { + this._pivotTable = pivotTable; + } + + public IXLPivotValue Add(String sourceName) + { + return Add(sourceName, sourceName); + } + + public IXLPivotValue Add(String sourceName, String customName) + { + if (sourceName != XLConstants.PivotTable.ValuesSentinalLabel && !this._pivotTable.SourceRangeFieldsAvailable.Contains(sourceName)) + throw new ArgumentOutOfRangeException(nameof(sourceName), String.Format("The column '{0}' does not appear in the source range.", sourceName)); + + var pivotValue = new XLPivotValue(sourceName) { CustomName = customName }; + _pivotValues.Add(customName, pivotValue); + + if (_pivotValues.Count > 1 && this._pivotTable.ColumnLabels.All(cl => cl.SourceName != XLConstants.PivotTable.ValuesSentinalLabel) && this._pivotTable.RowLabels.All(rl => rl.SourceName != XLConstants.PivotTable.ValuesSentinalLabel)) + _pivotTable.ColumnLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + + return pivotValue; + } + + public void Clear() + { + _pivotValues.Clear(); + } + + public Boolean Contains(String sourceName) + { + return _pivotValues.ContainsKey(sourceName); + } + + public Boolean Contains(IXLPivotValue pivotValue) + { + return _pivotValues.ContainsKey(pivotValue.SourceName); + } + + public IXLPivotValue Get(String sourceName) + { + return _pivotValues[sourceName]; + } + + public IXLPivotValue Get(Int32 index) + { + return _pivotValues.Values.ElementAt(index); + } + + public IEnumerator GetEnumerator() + { + return _pivotValues.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Int32 IndexOf(String sourceName) + { + var selectedItem = _pivotValues.Select((item, index) => new { Item = item, Position = index }).FirstOrDefault(i => i.Item.Key == sourceName); + if (selectedItem == null) + throw new ArgumentNullException(nameof(sourceName), "Invalid field name."); + + return selectedItem.Position; + } + + public Int32 IndexOf(IXLPivotValue pivotValue) + { + return IndexOf(pivotValue.SourceName); + } + + public void Remove(String sourceName) + { + _pivotValues.Remove(sourceName); + } + } +} diff --git a/ClosedXML/Excel/PivotTables/XLPivotAreaValues.cs b/ClosedXML/Excel/PivotTables/XLPivotAreaValues.cs new file mode 100644 index 0000000..6609b4a --- /dev/null +++ b/ClosedXML/Excel/PivotTables/XLPivotAreaValues.cs @@ -0,0 +1,14 @@ +namespace ClosedXML.Excel +{ + internal enum XLPivotAreaValues + { + None = 0, + Normal = 1, + Data = 2, + All = 3, + Origin = 4, + Button = 5, + TopRight = 6, + TopEnd = 7 + } +} diff --git a/ClosedXML/Excel/PivotTables/XLPivotField.cs b/ClosedXML/Excel/PivotTables/XLPivotField.cs new file mode 100644 index 0000000..71ce833 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/XLPivotField.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ClosedXML.Excel +{ + [DebuggerDisplay("{SourceName}")] + internal class XLPivotField : IXLPivotField + { + private readonly IXLPivotTable _pivotTable; + public XLPivotField(IXLPivotTable pivotTable, string sourceName) + { + this._pivotTable = pivotTable; + SourceName = sourceName; + Subtotals = new List(); + SelectedValues = new List(); + SortType = XLPivotSortType.Default; + SetExcelDefaults(); + + StyleFormats = new XLPivotFieldStyleFormats(this); + } + + public String SourceName { get; private set; } + public String CustomName { get; set; } + + public IXLPivotField SetCustomName(String value) { CustomName = value; return this; } + + public String SubtotalCaption { get; set; } + + public IXLPivotField SetSubtotalCaption(String value) { SubtotalCaption = value; return this; } + + public List Subtotals { get; private set; } + + public IXLPivotField AddSubtotal(XLSubtotalFunction value) { Subtotals.Add(value); return this; } + + public Boolean IncludeNewItemsInFilter { get; set; } + + public IXLPivotField SetIncludeNewItemsInFilter() { IncludeNewItemsInFilter = true; return this; } + + public IXLPivotField SetIncludeNewItemsInFilter(Boolean value) { IncludeNewItemsInFilter = value; return this; } + + public bool Outline { get; set; } + public bool Compact { get; set; } + + public IXLPivotField SetLayout(XLPivotLayout value) + { + Compact = false; + Outline = false; + switch (value) + { + case XLPivotLayout.Compact: Compact = true; break; + case XLPivotLayout.Outline: Outline = true; break; + } + return this; + } + + public Boolean? SubtotalsAtTop { get; set; } + + public IXLPivotField SetSubtotalsAtTop() { SubtotalsAtTop = true; return this; } + + public IXLPivotField SetSubtotalsAtTop(Boolean value) { SubtotalsAtTop = value; return this; } + + public Boolean RepeatItemLabels { get; set; } + + public IXLPivotField SetRepeatItemLabels() { RepeatItemLabels = true; return this; } + + public IXLPivotField SetRepeatItemLabels(Boolean value) { RepeatItemLabels = value; return this; } + + public Boolean InsertBlankLines { get; set; } + + public IXLPivotField SetInsertBlankLines() { InsertBlankLines = true; return this; } + + public IXLPivotField SetInsertBlankLines(Boolean value) { InsertBlankLines = value; return this; } + + public Boolean ShowBlankItems { get; set; } + + public IXLPivotField SetShowBlankItems() { ShowBlankItems = true; return this; } + + public IXLPivotField SetShowBlankItems(Boolean value) { ShowBlankItems = value; return this; } + + public Boolean InsertPageBreaks { get; set; } + + public IXLPivotField SetInsertPageBreaks() { InsertPageBreaks = true; return this; } + + public IXLPivotField SetInsertPageBreaks(Boolean value) { InsertPageBreaks = value; return this; } + + public Boolean Collapsed { get; set; } + + public IXLPivotField SetCollapsed() { Collapsed = true; return this; } + + public IXLPivotField SetCollapsed(Boolean value) { Collapsed = value; return this; } + + public XLPivotSortType SortType { get; set; } + + public IXLPivotField SetSort(XLPivotSortType value) { SortType = value; return this; } + + public IList SelectedValues { get; private set; } + + public IXLPivotField AddSelectedValue(Object value) + { + SelectedValues.Add(value); + return this; + } + + private void SetExcelDefaults() + { + IncludeNewItemsInFilter = false; + Outline = true; + Compact = true; + InsertBlankLines = false; + ShowBlankItems = false; + InsertPageBreaks = false; + RepeatItemLabels = false; + SubtotalsAtTop = true; + Collapsed = false; + } + + public IXLPivotFieldStyleFormats StyleFormats { get; set; } + + public Boolean IsOnRowAxis => _pivotTable.RowLabels.Contains(this.SourceName); + + public Boolean IsOnColumnAxis => _pivotTable.ColumnLabels.Contains(this.SourceName); + + public Boolean IsInFilterList => _pivotTable.ReportFilters.Contains(this.SourceName); + + public Int32 Offset => _pivotTable.SourceRangeFieldsAvailable.ToList().IndexOf(this.SourceName); + } +} diff --git a/ClosedXML/Excel/PivotTables/XLPivotFields.cs b/ClosedXML/Excel/PivotTables/XLPivotFields.cs new file mode 100644 index 0000000..bc3b07b --- /dev/null +++ b/ClosedXML/Excel/PivotTables/XLPivotFields.cs @@ -0,0 +1,91 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPivotFields : IXLPivotFields + { + private readonly Dictionary _pivotFields = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly IXLPivotTable _pivotTable; + + internal XLPivotFields(IXLPivotTable pivotTable) + { + this._pivotTable = pivotTable; + } + + #region IXLPivotFields members + + public IXLPivotField Add(String sourceName) + { + return Add(sourceName, sourceName); + } + + public IXLPivotField Add(String sourceName, String customName) + { + if (sourceName != XLConstants.PivotTable.ValuesSentinalLabel && !this._pivotTable.SourceRangeFieldsAvailable.Contains(sourceName)) + throw new ArgumentOutOfRangeException(nameof(sourceName), String.Format("The column '{0}' does not appear in the source range.", sourceName)); + + var pivotField = new XLPivotField(_pivotTable, sourceName) { CustomName = customName }; + _pivotFields.Add(sourceName, pivotField); + return pivotField; + } + + public void Clear() + { + _pivotFields.Clear(); + } + + public Boolean Contains(String sourceName) + { + return _pivotFields.ContainsKey(sourceName); + } + + public bool Contains(IXLPivotField pivotField) + { + return _pivotFields.ContainsKey(pivotField.SourceName); + } + + public IXLPivotField Get(String sourceName) + { + return _pivotFields[sourceName]; + } + + public IXLPivotField Get(Int32 index) + { + return _pivotFields.Values.ElementAt(index); + } + + public IEnumerator GetEnumerator() + { + return _pivotFields.Values.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Int32 IndexOf(String sourceName) + { + var selectedItem = _pivotFields.Select((item, index) => new { Item = item, Position = index }).FirstOrDefault(i => i.Item.Key == sourceName); + if (selectedItem == null) + throw new ArgumentNullException(nameof(sourceName), "Invalid field name."); + + return selectedItem.Position; + } + + public Int32 IndexOf(IXLPivotField pf) + { + return IndexOf(pf.SourceName); + } + + public void Remove(String sourceName) + { + _pivotFields.Remove(sourceName); + } + + #endregion IXLPivotFields members + } +} diff --git a/ClosedXML/Excel/PivotTables/XLPivotTable.cs b/ClosedXML/Excel/PivotTables/XLPivotTable.cs new file mode 100644 index 0000000..7b572ec --- /dev/null +++ b/ClosedXML/Excel/PivotTables/XLPivotTable.cs @@ -0,0 +1,412 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ClosedXML.Excel +{ + [DebuggerDisplay("{Name}")] + internal class XLPivotTable : IXLPivotTable + { + private String _name; + public Guid Guid { get; private set; } + + public XLPivotTable(IXLWorksheet worksheet) + { + this.Worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + this.Guid = Guid.NewGuid(); + + Fields = new XLPivotFields(this); + ReportFilters = new XLPivotFields(this); + ColumnLabels = new XLPivotFields(this); + RowLabels = new XLPivotFields(this); + Values = new XLPivotValues(this); + Theme = XLPivotTableTheme.PivotStyleLight16; + + SetExcelDefaults(); + } + + public IXLCell TargetCell { get; set; } + + private IXLRange sourceRange; + + public IXLRange SourceRange + { + get { return sourceRange; } + set + { + if (value is IXLTable) + SourceType = XLPivotTableSourceType.Table; + else + SourceType = XLPivotTableSourceType.Range; + + sourceRange = value; + } + } + + public IXLTable SourceTable + { + get { return SourceRange as IXLTable; } + set { SourceRange = value; } + } + + public XLPivotTableSourceType SourceType { get; private set; } + + public IEnumerable SourceRangeFieldsAvailable + { + get { return this.SourceRange.FirstRow().Cells().Select(c => c.GetString()); } + } + + public IXLPivotFields Fields { get; private set; } + public IXLPivotFields ReportFilters { get; private set; } + public IXLPivotFields ColumnLabels { get; private set; } + public IXLPivotFields RowLabels { get; private set; } + public IXLPivotValues Values { get; private set; } + + public IEnumerable ImplementedFields + { + get + { + foreach (var pf in ReportFilters) + yield return pf; + + foreach (var pf in RowLabels) + yield return pf; + + foreach (var pf in ColumnLabels) + yield return pf; + } + } + + public XLPivotTableTheme Theme { get; set; } + + public IXLPivotTable SetTheme(XLPivotTableTheme value) { Theme = value; return this; } + + public String Name + { + get { return _name; } + set + { + if (_name == value) return; + + var oldname = _name ?? string.Empty; + + if (!XLHelper.ValidateName("pivot table", value, oldname, Worksheet.PivotTables.Select(pvt => pvt.Name), out String message)) + throw new ArgumentException(message, nameof(value)); + + _name = value; + + if (!String.IsNullOrWhiteSpace(oldname) && !String.Equals(oldname, _name, StringComparison.OrdinalIgnoreCase)) + { + Worksheet.PivotTables.Delete(oldname); + (Worksheet.PivotTables as XLPivotTables).Add(_name, this); + } + } + } + + public IXLPivotTable SetName(String value) { Name = value; return this; } + + public String Title { get; set; } + + public IXLPivotTable SetTitle(String value) { Title = value; return this; } + + public String Description { get; set; } + + public IXLPivotTable SetDescription(String value) { Description = value; return this; } + + public String ColumnHeaderCaption { get; set; } + + public IXLPivotTable SetColumnHeaderCaption(String value) + { + ColumnHeaderCaption = value; + return this; + } + + public String RowHeaderCaption { get; set; } + + public IXLPivotTable SetRowHeaderCaption(String value) + { + RowHeaderCaption = value; + return this; + } + + public Boolean MergeAndCenterWithLabels { get; set; } + + public IXLPivotTable SetMergeAndCenterWithLabels() { MergeAndCenterWithLabels = true; return this; } + + public IXLPivotTable SetMergeAndCenterWithLabels(Boolean value) { MergeAndCenterWithLabels = value; return this; } + + public Int32 RowLabelIndent { get; set; } + + public IXLPivotTable SetRowLabelIndent(Int32 value) { RowLabelIndent = value; return this; } + + public XLFilterAreaOrder FilterAreaOrder { get; set; } + + public IXLPivotTable SetFilterAreaOrder(XLFilterAreaOrder value) { FilterAreaOrder = value; return this; } + + public Int32 FilterFieldsPageWrap { get; set; } + + public IXLPivotTable SetFilterFieldsPageWrap(Int32 value) { FilterFieldsPageWrap = value; return this; } + + public String ErrorValueReplacement { get; set; } + + public IXLPivotTable SetErrorValueReplacement(String value) { ErrorValueReplacement = value; return this; } + + public String EmptyCellReplacement { get; set; } + + public IXLPivotTable SetEmptyCellReplacement(String value) { EmptyCellReplacement = value; return this; } + + public Boolean AutofitColumns { get; set; } + + public IXLPivotTable SetAutofitColumns() { AutofitColumns = true; return this; } + + public IXLPivotTable SetAutofitColumns(Boolean value) { AutofitColumns = value; return this; } + + public Boolean PreserveCellFormatting { get; set; } + + public IXLPivotTable SetPreserveCellFormatting() { PreserveCellFormatting = true; return this; } + + public IXLPivotTable SetPreserveCellFormatting(Boolean value) { PreserveCellFormatting = value; return this; } + + public Boolean ShowGrandTotalsRows { get; set; } + + public IXLPivotTable SetShowGrandTotalsRows() { ShowGrandTotalsRows = true; return this; } + + public IXLPivotTable SetShowGrandTotalsRows(Boolean value) { ShowGrandTotalsRows = value; return this; } + + public Boolean ShowGrandTotalsColumns { get; set; } + + public IXLPivotTable SetShowGrandTotalsColumns() { ShowGrandTotalsColumns = true; return this; } + + public IXLPivotTable SetShowGrandTotalsColumns(Boolean value) { ShowGrandTotalsColumns = value; return this; } + + public Boolean FilteredItemsInSubtotals { get; set; } + + public IXLPivotTable SetFilteredItemsInSubtotals() { FilteredItemsInSubtotals = true; return this; } + + public IXLPivotTable SetFilteredItemsInSubtotals(Boolean value) { FilteredItemsInSubtotals = value; return this; } + + public Boolean AllowMultipleFilters { get; set; } + + public IXLPivotTable SetAllowMultipleFilters() { AllowMultipleFilters = true; return this; } + + public IXLPivotTable SetAllowMultipleFilters(Boolean value) { AllowMultipleFilters = value; return this; } + + public Boolean UseCustomListsForSorting { get; set; } + + public IXLPivotTable SetUseCustomListsForSorting() { UseCustomListsForSorting = true; return this; } + + public IXLPivotTable SetUseCustomListsForSorting(Boolean value) { UseCustomListsForSorting = value; return this; } + + public Boolean ShowExpandCollapseButtons { get; set; } + + public IXLPivotTable SetShowExpandCollapseButtons() { ShowExpandCollapseButtons = true; return this; } + + public IXLPivotTable SetShowExpandCollapseButtons(Boolean value) { ShowExpandCollapseButtons = value; return this; } + + public Boolean ShowContextualTooltips { get; set; } + + public IXLPivotTable SetShowContextualTooltips() { ShowContextualTooltips = true; return this; } + + public IXLPivotTable SetShowContextualTooltips(Boolean value) { ShowContextualTooltips = value; return this; } + + public Boolean ShowPropertiesInTooltips { get; set; } + + public IXLPivotTable SetShowPropertiesInTooltips() { ShowPropertiesInTooltips = true; return this; } + + public IXLPivotTable SetShowPropertiesInTooltips(Boolean value) { ShowPropertiesInTooltips = value; return this; } + + public Boolean DisplayCaptionsAndDropdowns { get; set; } + + public IXLPivotTable SetDisplayCaptionsAndDropdowns() { DisplayCaptionsAndDropdowns = true; return this; } + + public IXLPivotTable SetDisplayCaptionsAndDropdowns(Boolean value) { DisplayCaptionsAndDropdowns = value; return this; } + + public Boolean ClassicPivotTableLayout { get; set; } + + public IXLPivotTable SetClassicPivotTableLayout() { ClassicPivotTableLayout = true; return this; } + + public IXLPivotTable SetClassicPivotTableLayout(Boolean value) { ClassicPivotTableLayout = value; return this; } + + public Boolean ShowValuesRow { get; set; } + + public IXLPivotTable SetShowValuesRow() { ShowValuesRow = true; return this; } + + public IXLPivotTable SetShowValuesRow(Boolean value) { ShowValuesRow = value; return this; } + + public Boolean ShowEmptyItemsOnRows { get; set; } + + public IXLPivotTable SetShowEmptyItemsOnRows() { ShowEmptyItemsOnRows = true; return this; } + + public IXLPivotTable SetShowEmptyItemsOnRows(Boolean value) { ShowEmptyItemsOnRows = value; return this; } + + public Boolean ShowEmptyItemsOnColumns { get; set; } + + public IXLPivotTable SetShowEmptyItemsOnColumns() { ShowEmptyItemsOnColumns = true; return this; } + + public IXLPivotTable SetShowEmptyItemsOnColumns(Boolean value) { ShowEmptyItemsOnColumns = value; return this; } + + public Boolean DisplayItemLabels { get; set; } + + public IXLPivotTable SetDisplayItemLabels() { DisplayItemLabels = true; return this; } + + public IXLPivotTable SetDisplayItemLabels(Boolean value) { DisplayItemLabels = value; return this; } + + public Boolean SortFieldsAtoZ { get; set; } + + public IXLPivotTable SetSortFieldsAtoZ() { SortFieldsAtoZ = true; return this; } + + public IXLPivotTable SetSortFieldsAtoZ(Boolean value) { SortFieldsAtoZ = value; return this; } + + public Boolean PrintExpandCollapsedButtons { get; set; } + + public IXLPivotTable SetPrintExpandCollapsedButtons() { PrintExpandCollapsedButtons = true; return this; } + + public IXLPivotTable SetPrintExpandCollapsedButtons(Boolean value) { PrintExpandCollapsedButtons = value; return this; } + + public Boolean RepeatRowLabels { get; set; } + + public IXLPivotTable SetRepeatRowLabels() { RepeatRowLabels = true; return this; } + + public IXLPivotTable SetRepeatRowLabels(Boolean value) { RepeatRowLabels = value; return this; } + + public Boolean PrintTitles { get; set; } + + public IXLPivotTable SetPrintTitles() { PrintTitles = true; return this; } + + public IXLPivotTable SetPrintTitles(Boolean value) { PrintTitles = value; return this; } + + public Boolean SaveSourceData { get; set; } + + public IXLPivotTable SetSaveSourceData() { SaveSourceData = true; return this; } + + public IXLPivotTable SetSaveSourceData(Boolean value) { SaveSourceData = value; return this; } + + public Boolean EnableShowDetails { get; set; } + + public IXLPivotTable SetEnableShowDetails() { EnableShowDetails = true; return this; } + + public IXLPivotTable SetEnableShowDetails(Boolean value) { EnableShowDetails = value; return this; } + + public Boolean RefreshDataOnOpen { get; set; } + + public IXLPivotTable SetRefreshDataOnOpen() { RefreshDataOnOpen = true; return this; } + + public IXLPivotTable SetRefreshDataOnOpen(Boolean value) { RefreshDataOnOpen = value; return this; } + + public XLItemsToRetain ItemsToRetainPerField { get; set; } + + public IXLPivotTable SetItemsToRetainPerField(XLItemsToRetain value) { ItemsToRetainPerField = value; return this; } + + public Boolean EnableCellEditing { get; set; } + + public IXLPivotTable SetEnableCellEditing() { EnableCellEditing = true; return this; } + + public IXLPivotTable SetEnableCellEditing(Boolean value) { EnableCellEditing = value; return this; } + + public Boolean ShowRowHeaders { get; set; } + + public IXLPivotTable SetShowRowHeaders() { ShowRowHeaders = true; return this; } + + public IXLPivotTable SetShowRowHeaders(Boolean value) { ShowRowHeaders = value; return this; } + + public Boolean ShowColumnHeaders { get; set; } + + public IXLPivotTable SetShowColumnHeaders() { ShowColumnHeaders = true; return this; } + + public IXLPivotTable SetShowColumnHeaders(Boolean value) { ShowColumnHeaders = value; return this; } + + public Boolean ShowRowStripes { get; set; } + + public IXLPivotTable SetShowRowStripes() { ShowRowStripes = true; return this; } + + public IXLPivotTable SetShowRowStripes(Boolean value) { ShowRowStripes = value; return this; } + + public Boolean ShowColumnStripes { get; set; } + + public IXLPivotTable SetShowColumnStripes() { ShowColumnStripes = true; return this; } + + public IXLPivotTable SetShowColumnStripes(Boolean value) { ShowColumnStripes = value; return this; } + + public XLPivotSubtotals Subtotals { get; set; } + + public IXLPivotTable SetSubtotals(XLPivotSubtotals value) { Subtotals = value; return this; } + + public XLPivotLayout Layout + { + set { Fields.ForEach(f => f.SetLayout(value)); } + } + + public IXLPivotTable SetLayout(XLPivotLayout value) { Layout = value; return this; } + + public Boolean InsertBlankLines + { + set { Fields.ForEach(f => f.SetInsertBlankLines(value)); } + } + + public IXLPivotTable SetInsertBlankLines() { InsertBlankLines = true; return this; } + + public IXLPivotTable SetInsertBlankLines(Boolean value) { InsertBlankLines = value; return this; } + + internal String RelId { get; set; } + internal String CacheDefinitionRelId { get; set; } + internal String WorkbookCacheRelId { get; set; } + + private void SetExcelDefaults() + { + EmptyCellReplacement = String.Empty; + SaveSourceData = true; + ShowColumnHeaders = true; + ShowRowHeaders = true; + + // source http://www.datypic.com/sc/ooxml/e-ssml_pivotTableDefinition.html + DisplayItemLabels = true; // Show Item Names + ShowExpandCollapseButtons = true; // Show Expand Collapse + PrintExpandCollapsedButtons = false; // Print Drill Indicators + ShowPropertiesInTooltips = true; // Show Member Property ToolTips + ShowContextualTooltips = true; // Show ToolTips on Data + EnableShowDetails = true; // Enable Drill Down + PreserveCellFormatting = true; // Preserve Formatting + AutofitColumns = false; // Auto Formatting + FilterAreaOrder = XLFilterAreaOrder.DownThenOver; // Page Over Then Down + FilteredItemsInSubtotals = false; // Subtotal Hidden Items + ShowGrandTotalsRows = true; // Row Grand Totals + ShowGrandTotalsColumns = true; // Grand Totals On Columns + PrintTitles = false; // Field Print Titles + RepeatRowLabels = false; // Item Print Titles + MergeAndCenterWithLabels = false; // Merge Titles + RowLabelIndent = 1; // Indentation for Compact Axis + ShowEmptyItemsOnRows = false; // Show Empty Row + ShowEmptyItemsOnColumns = false; // Show Empty Column + DisplayCaptionsAndDropdowns = true; // Show Field Headers + ClassicPivotTableLayout = false; // Enable Drop Zones + AllowMultipleFilters = true; // Multiple Field Filters + SortFieldsAtoZ = false; // Default Sort Order + UseCustomListsForSorting = true; // Custom List AutoSort + } + + public IXLWorksheet Worksheet { get; } + + public IXLPivotTableStyleFormats StyleFormats { get; } = new XLPivotTableStyleFormats(); + + public IEnumerable AllStyleFormats + { + get + { + foreach (var styleFormat in this.StyleFormats.RowGrandTotalFormats) + yield return styleFormat; + + foreach (var styleFormat in this.StyleFormats.ColumnGrandTotalFormats) + yield return styleFormat; + + foreach (var pivotField in ImplementedFields) + { + yield return pivotField.StyleFormats.Subtotal; + yield return pivotField.StyleFormats.Header; + yield return pivotField.StyleFormats.Label; + yield return pivotField.StyleFormats.DataValuesFormat; + } + } + } + } +} diff --git a/ClosedXML/Excel/PivotTables/XLPivotTables.cs b/ClosedXML/Excel/PivotTables/XLPivotTables.cs new file mode 100644 index 0000000..a601d58 --- /dev/null +++ b/ClosedXML/Excel/PivotTables/XLPivotTables.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPivotTables : IXLPivotTables + { + private readonly Dictionary _pivotTables = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public XLPivotTables(IXLWorksheet worksheet) + { + this.Worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + } + + internal void Add(String name, IXLPivotTable pivotTable) + { + _pivotTables.Add(name, (XLPivotTable)pivotTable); + } + + public IXLPivotTable Add(string name, IXLCell targetCell, IXLRange range) + { + var pivotTable = new XLPivotTable(this.Worksheet) + { + Name = name, + TargetCell = targetCell, + SourceRange = range + }; + _pivotTables.Add(name, pivotTable); + return pivotTable; + } + + public IXLPivotTable Add(string name, IXLCell targetCell, IXLTable table) + { + var pivotTable = new XLPivotTable(this.Worksheet) + { + Name = name, + TargetCell = targetCell, + SourceTable = table + }; + _pivotTables.Add(name, pivotTable); + return pivotTable; + } + + public IXLPivotTable AddNew(string name, IXLCell targetCell, IXLRange range) + { + return Add(name, targetCell, range); + } + + public IXLPivotTable AddNew(string name, IXLCell targetCell, IXLTable table) + { + return Add(name, targetCell, table); + } + + public Boolean Contains(String name) + { + return _pivotTables.ContainsKey(name); + } + + public void Delete(String name) + { + _pivotTables.Remove(name); + } + + public void DeleteAll() + { + _pivotTables.Clear(); + } + + public IEnumerator GetEnumerator() + { + return _pivotTables.Values.Cast().GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public XLPivotTable PivotTable(String name) + { + return _pivotTables[name]; + } + + IXLPivotTable IXLPivotTables.PivotTable(String name) + { + return PivotTable(name); + } + + public IXLWorksheet Worksheet { get; private set; } + } +} diff --git a/ClosedXML/Excel/Protection/IXLElementProtection.cs b/ClosedXML/Excel/Protection/IXLElementProtection.cs new file mode 100644 index 0000000..754ffa5 --- /dev/null +++ b/ClosedXML/Excel/Protection/IXLElementProtection.cs @@ -0,0 +1,82 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + public interface IXLElementProtection : IXLElementProtection + where T : struct + { + /// Gets or sets the elements that are allowed to be edited by the user, i.e. those that are not protected. + /// The allowed elements. + T AllowedElements { get; set; } + + /// + /// Adds the specified element to the list of allowed elements. + /// Beware that if you pass through "None", this will have no effect. + /// + /// The element to add + /// Set to true to allow the element or false to disallow the element + /// The current protection instance + IXLElementProtection AllowElement(T element, Boolean allowed = true); + + /// Allows all elements to be edited. + /// + IXLElementProtection AllowEverything(); + + /// Allows no elements to be edited. Protects all elements. + /// + IXLElementProtection AllowNone(); + + /// Copies all the protection settings from a different instance. + /// The protectable. + /// + IXLElementProtection CopyFrom(IXLElementProtection protectable); + + /// + /// Removes the element to the list of allowed elements. + /// Beware that if you pass through "None", this will have no effect. + /// + /// The element to remove + /// The current protection instance + IXLElementProtection DisallowElement(T element); + + /// Protects this instance without a password. + /// + IXLElementProtection Protect(); + + /// Protects this instance using the specified password and password hash algorithm. + /// The password. + /// The algorithm. + /// + IXLElementProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm); + + /// Unprotects this instance without a password. + /// + IXLElementProtection Unprotect(); + + /// Unprotects this instance using the specified password. + /// The password. + /// + IXLElementProtection Unprotect(String password); + } + + public interface IXLElementProtection : ICloneable + { + /// Gets the algorithm used to hash the password. + /// The algorithm. + Algorithm Algorithm { get; } + + /// Gets a value indicating whether this instance is protected with a password. + /// + /// true if this instance is password protected; otherwise, false. + /// + Boolean IsPasswordProtected { get; } + + /// Gets a value indicating whether this instance is protected, either with or without a password. + /// + /// true if this instance is protected; otherwise, false. + /// + Boolean IsProtected { get; } + } +} diff --git a/ClosedXML/Excel/Protection/IXLProtectable.cs b/ClosedXML/Excel/Protection/IXLProtectable.cs new file mode 100644 index 0000000..f253c36 --- /dev/null +++ b/ClosedXML/Excel/Protection/IXLProtectable.cs @@ -0,0 +1,73 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + public interface IXLProtectable : IXLProtectable + where TProtection : IXLElementProtection + where TElement : struct + { + TProtection Protection { get; set; } + + /// Protects this instance without a password. + /// + new TProtection Protect(); + + /// Protects this instance using the specified password and password hash algorithm. + /// The password. + /// The algorithm. + /// + new TProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm); + + /// Protects this instance with the specified password, password hash algorithm and set elements that the user is allowed to change. + /// The password. + /// The algorithm. + /// The allowed elements. + /// + TProtection Protect(String password, Algorithm algorithm, TElement allowedElements); + + /// Unprotects this instance without a password. + /// + new TProtection Unprotect(); + + /// Unprotects this instance using the specified password. + /// The password. + /// + new TProtection Unprotect(String password); + } + + public interface IXLProtectable + { + /// Gets a value indicating whether this instance is protected with a password. + /// + /// true if this instance is password protected; otherwise, false. + /// + Boolean IsPasswordProtected { get; } + + /// Gets a value indicating whether this instance is protected, either with or without a password. + /// + /// true if this instance is protected; otherwise, false. + /// + Boolean IsProtected { get; } + + /// Protects this instance without a password. + /// + IXLElementProtection Protect(); + + /// Protects this instance using the specified password and password hash algorithm. + /// The password. + /// The algorithm. + /// + IXLElementProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm); + + /// Unprotects this instance without a password. + /// + IXLElementProtection Unprotect(); + + /// Unprotects this instance using the specified password. + /// The password. + /// + IXLElementProtection Unprotect(String password); + } +} diff --git a/ClosedXML/Excel/Protection/IXLSheetProtection.cs b/ClosedXML/Excel/Protection/IXLSheetProtection.cs new file mode 100644 index 0000000..d15ea96 --- /dev/null +++ b/ClosedXML/Excel/Protection/IXLSheetProtection.cs @@ -0,0 +1,6 @@ +namespace ClosedXML.Excel +{ + public interface IXLSheetProtection : IXLElementProtection + { + } +} diff --git a/ClosedXML/Excel/Protection/IXLWorkbookProtection.cs b/ClosedXML/Excel/Protection/IXLWorkbookProtection.cs new file mode 100644 index 0000000..6ef5dad --- /dev/null +++ b/ClosedXML/Excel/Protection/IXLWorkbookProtection.cs @@ -0,0 +1,6 @@ +namespace ClosedXML.Excel +{ + public interface IXLWorkbookProtection : IXLElementProtection + { + } +} diff --git a/ClosedXML/Excel/Protection/XLProtectionAlgorithm.cs b/ClosedXML/Excel/Protection/XLProtectionAlgorithm.cs new file mode 100644 index 0000000..f1aeee8 --- /dev/null +++ b/ClosedXML/Excel/Protection/XLProtectionAlgorithm.cs @@ -0,0 +1,47 @@ +// Keep this file CodeMaid organised and cleaned +using System.ComponentModel; + +namespace ClosedXML.Excel +{ + public static class XLProtectionAlgorithm + { + public const Algorithm DefaultProtectionAlgorithm = Algorithm.SimpleHash; + + public enum Algorithm + { + // The default hashing algorithm as described in http://kohei.us/2008/01/18/excel-sheet-protection-password-hash/ + [Description("SimpleHash")] + SimpleHash, + + //[Description("MD2")] + //MD2, + + //[Description("MD4")] + //MD4, + + //[Description("MD5")] + //MD5, + + //[Description("RIPEMD-128")] + //RIPEMD128, + + //[Description("RIPEMD-160")] + //RIPEMD160, + + //[Description("SHA-1")] + //SHA1, + + //[Description("SHA-256")] + //SHA256, + + //[Description("SHA-384")] + //SHA384, + + [Description("SHA-512")] + SHA512, + + //[Description("WHIRLPOOL")] + //WHIRLPOOL + } + } +} diff --git a/ClosedXML/Excel/Protection/XLSheetProtection.cs b/ClosedXML/Excel/Protection/XLSheetProtection.cs new file mode 100644 index 0000000..e58d6c6 --- /dev/null +++ b/ClosedXML/Excel/Protection/XLSheetProtection.cs @@ -0,0 +1,154 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + internal class XLSheetProtection : IXLSheetProtection + { + public XLSheetProtection(Algorithm algorithm) + { + this.Algorithm = algorithm; + AllowedElements = XLSheetProtectionElements.SelectEverything; + } + + public Algorithm Algorithm { get; internal set; } + public XLSheetProtectionElements AllowedElements { get; set; } + + public Boolean IsPasswordProtected => this.IsProtected && !String.IsNullOrEmpty(PasswordHash); + public Boolean IsProtected { get; internal set; } + + internal String Base64EncodedSalt { get; set; } + internal String PasswordHash { get; set; } + internal UInt32 SpinCount { get; set; } = 100000; + + public IXLSheetProtection AllowElement(XLSheetProtectionElements element, Boolean allowed = true) + { + if (!allowed) + return DisallowElement(element); + + AllowedElements |= element; + return this; + } + + public IXLSheetProtection AllowEverything() + { + return AllowElement(XLSheetProtectionElements.Everything); + } + + public IXLSheetProtection AllowNone() + { + AllowedElements = XLSheetProtectionElements.None; + return this; + } + + public object Clone() + { + return new XLSheetProtection(this.Algorithm) + { + IsProtected = this.IsProtected, + PasswordHash = this.PasswordHash, + SpinCount = this.SpinCount, + Base64EncodedSalt = this.Base64EncodedSalt, + AllowedElements = this.AllowedElements + }; + } + + public XLSheetProtection CopyFrom(IXLElementProtection sheetProtection) + { + if (sheetProtection is XLSheetProtection xlSheetProtection) + { + this.IsProtected = xlSheetProtection.IsProtected; + this.Algorithm = xlSheetProtection.Algorithm; + this.PasswordHash = xlSheetProtection.PasswordHash; + this.SpinCount = xlSheetProtection.SpinCount; + this.Base64EncodedSalt = xlSheetProtection.Base64EncodedSalt; + this.AllowedElements = xlSheetProtection.AllowedElements; + } + return this; + } + + public IXLSheetProtection DisallowElement(XLSheetProtectionElements element) + { + AllowedElements &= ~element; + return this; + } + + public IXLSheetProtection Protect() + { + return Protect(String.Empty); + } + + public IXLSheetProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm, XLSheetProtectionElements allowedElements = XLSheetProtectionElements.SelectEverything) + { + if (IsProtected) + { + throw new InvalidOperationException("The worksheet is already protected"); + } + else + { + IsProtected = true; + + password = password ?? ""; + + this.Algorithm = algorithm; + this.Base64EncodedSalt = Utils.CryptographicAlgorithms.GenerateNewSalt(this.Algorithm); + this.PasswordHash = Utils.CryptographicAlgorithms.GetPasswordHash(this.Algorithm, password, this.Base64EncodedSalt, this.SpinCount); + } + + this.AllowedElements = allowedElements; + + return this; + } + + public IXLSheetProtection Unprotect() + { + return Unprotect(String.Empty); + } + + public IXLSheetProtection Unprotect(String password) + { + if (IsProtected) + { + password = password ?? ""; + + if ("" != PasswordHash && "" == password) + throw new InvalidOperationException("The worksheet is password protected"); + + var hash = Utils.CryptographicAlgorithms.GetPasswordHash(this.Algorithm, password, this.Base64EncodedSalt, this.SpinCount); + if (hash != PasswordHash) + throw new ArgumentException("Invalid password"); + else + { + IsProtected = false; + PasswordHash = String.Empty; + this.Base64EncodedSalt = String.Empty; + } + } + + return this; + } + + #region IXLProtectable interface + + IXLElementProtection IXLElementProtection.AllowElement(XLSheetProtectionElements element, Boolean allowed) => AllowElement(element, allowed); + + IXLElementProtection IXLElementProtection.AllowEverything() => AllowEverything(); + + IXLElementProtection IXLElementProtection.AllowNone() => AllowNone(); + + IXLElementProtection IXLElementProtection.CopyFrom(IXLElementProtection protectable) => CopyFrom(protectable); + + IXLElementProtection IXLElementProtection.DisallowElement(XLSheetProtectionElements element) => DisallowElement(element); + + IXLElementProtection IXLElementProtection.Protect() => Protect(); + + IXLElementProtection IXLElementProtection.Protect(String password, Algorithm algorithm) => Protect(password, algorithm); + + IXLElementProtection IXLElementProtection.Unprotect() => Unprotect(); + + IXLElementProtection IXLElementProtection.Unprotect(String password) => Unprotect(password); + + #endregion IXLProtectable interface + } +} diff --git a/ClosedXML/Excel/Protection/XLSheetProtectionElements.cs b/ClosedXML/Excel/Protection/XLSheetProtectionElements.cs new file mode 100644 index 0000000..afa56db --- /dev/null +++ b/ClosedXML/Excel/Protection/XLSheetProtectionElements.cs @@ -0,0 +1,39 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + public enum XLSheetProtectionElements + { + None = 0, + AutoFilter = 1 << 1, + DeleteColumns = 1 << 2, + DeleteRows = 1 << 3, + EditObjects = 1 << 4, + EditScenarios = 1 << 5, + FormatCells = 1 << 6, + FormatColumns = 1 << 7, + FormatRows = 1 << 8, + InsertColumns = 1 << 9, + InsertHyperlinks = 1 << 10, + InsertRows = 1 << 11, + PivotTables = 1 << 12, + SelectLockedCells = 1 << 13, + SelectUnlockedCells = 1 << 14, + Sort = 1 << 15, + + DeleteEverything = DeleteColumns | DeleteRows, + FormatEverything = FormatCells | FormatColumns | FormatRows, + InsertEverything = InsertColumns | InsertHyperlinks | InsertRows, + SelectEverything = SelectLockedCells | SelectUnlockedCells, + + Everything = AutoFilter + | DeleteColumns | DeleteRows + | EditObjects | EditScenarios + | FormatCells | FormatColumns | FormatRows + | InsertColumns | InsertHyperlinks | InsertRows + | PivotTables + | SelectLockedCells | SelectUnlockedCells + | Sort + } +} diff --git a/ClosedXML/Excel/Protection/XLWorkbookProtection.cs b/ClosedXML/Excel/Protection/XLWorkbookProtection.cs new file mode 100644 index 0000000..886ee1b --- /dev/null +++ b/ClosedXML/Excel/Protection/XLWorkbookProtection.cs @@ -0,0 +1,158 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + internal class XLWorkbookProtection : IXLWorkbookProtection + { + public XLWorkbookProtection(Algorithm algorithm) + : this(algorithm, XLWorkbookProtectionElements.Windows) + { + } + + public XLWorkbookProtection(Algorithm algorithm, XLWorkbookProtectionElements allowedElements) + { + this.Algorithm = algorithm; + this.AllowedElements = allowedElements; + } + + public Algorithm Algorithm { get; internal set; } + public XLWorkbookProtectionElements AllowedElements { get; set; } + public bool IsPasswordProtected => this.IsProtected && !String.IsNullOrEmpty(PasswordHash); + public bool IsProtected { get; internal set; } + + internal String Base64EncodedSalt { get; set; } + internal String PasswordHash { get; set; } + internal UInt32 SpinCount { get; set; } = 100000; + + public IXLWorkbookProtection AllowElement(XLWorkbookProtectionElements element, Boolean allowed = true) + { + if (allowed) + AllowedElements |= element; + else + AllowedElements &= ~element; + + return this; + } + + public IXLWorkbookProtection AllowEverything() + { + AllowedElements = XLWorkbookProtectionElements.Everything; + return this; + } + + public IXLWorkbookProtection AllowNone() + { + AllowedElements = XLWorkbookProtectionElements.None; + return this; + } + + public object Clone() + { + return new XLWorkbookProtection(this.Algorithm, this.AllowedElements) + { + IsProtected = this.IsProtected, + PasswordHash = this.PasswordHash, + SpinCount = this.SpinCount, + Base64EncodedSalt = this.Base64EncodedSalt + }; + } + + public IXLWorkbookProtection CopyFrom(IXLElementProtection workbookProtection) + { + if (workbookProtection is XLWorkbookProtection xlWorkbookProtection) + { + this.IsProtected = xlWorkbookProtection.IsProtected; + this.Algorithm = xlWorkbookProtection.Algorithm; + this.PasswordHash = xlWorkbookProtection.PasswordHash; + this.SpinCount = xlWorkbookProtection.SpinCount; + this.Base64EncodedSalt = xlWorkbookProtection.Base64EncodedSalt; + this.AllowedElements = xlWorkbookProtection.AllowedElements; + } + return this; + } + + public IXLWorkbookProtection DisallowElement(XLWorkbookProtectionElements element) + { + return AllowElement(element, allowed: false); + } + + public IXLWorkbookProtection Protect() + { + return Protect(String.Empty); + } + + public IXLWorkbookProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm, XLWorkbookProtectionElements allowedElements = XLWorkbookProtectionElements.Windows) + { + if (IsProtected) + { + throw new InvalidOperationException("The workbook structure is already protected"); + } + else + { + IsProtected = true; + + password = password ?? ""; + + this.Algorithm = algorithm; + this.Base64EncodedSalt = Utils.CryptographicAlgorithms.GenerateNewSalt(this.Algorithm); + this.PasswordHash = Utils.CryptographicAlgorithms.GetPasswordHash(this.Algorithm, password, this.Base64EncodedSalt, this.SpinCount); + } + + this.AllowedElements = allowedElements; + + return this; + } + + public IXLWorkbookProtection Unprotect() + { + return Unprotect(String.Empty); + } + + public IXLWorkbookProtection Unprotect(String password) + { + if (IsProtected) + { + password = password ?? ""; + + if ("" != PasswordHash && "" == password) + throw new InvalidOperationException("The workbook structure is password protected"); + + var hash = Utils.CryptographicAlgorithms.GetPasswordHash(this.Algorithm, password, this.Base64EncodedSalt, this.SpinCount); + if (hash != PasswordHash) + throw new ArgumentException("Invalid password"); + else + { + IsProtected = false; + PasswordHash = String.Empty; + this.Base64EncodedSalt = String.Empty; + } + } + + return this; + } + + #region IXLProtectable interface + + IXLElementProtection IXLElementProtection.AllowElement(XLWorkbookProtectionElements element, Boolean allowed) => AllowElement(element, allowed); + + IXLElementProtection IXLElementProtection.AllowEverything() => AllowEverything(); + + IXLElementProtection IXLElementProtection.AllowNone() => AllowNone(); + + IXLElementProtection IXLElementProtection.DisallowElement(XLWorkbookProtectionElements element) => DisallowElement(element); + + IXLElementProtection IXLElementProtection.Protect() => Protect(); + + IXLElementProtection IXLElementProtection.Protect(String password, Algorithm algorithm) => Protect(password, algorithm); + + IXLElementProtection IXLElementProtection.Unprotect() => Unprotect(); + + IXLElementProtection IXLElementProtection.Unprotect(String password) => Unprotect(password); + + IXLElementProtection IXLElementProtection.CopyFrom(IXLElementProtection protectable) => CopyFrom(protectable); + + #endregion IXLProtectable interface + } +} diff --git a/ClosedXML/Excel/Protection/XLWorkbookProtectionElements.cs b/ClosedXML/Excel/Protection/XLWorkbookProtectionElements.cs new file mode 100644 index 0000000..7995720 --- /dev/null +++ b/ClosedXML/Excel/Protection/XLWorkbookProtectionElements.cs @@ -0,0 +1,18 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + public enum XLWorkbookProtectionElements + { + None = 0, + Structure = 1 << 0, + + /// + /// The Windows option is available only in Excel 2007, Excel 2010, Excel for Mac 2011, and Excel 2016 for Mac. Select the Windows option if you want to prevent users from moving, resizing, or closing the workbook window, or hide/unhide windows. + /// + Windows = 1 << 1, + + Everything = Structure | Windows + } +} diff --git a/ClosedXML/Excel/Ranges/IXLAddressable.cs b/ClosedXML/Excel/Ranges/IXLAddressable.cs new file mode 100644 index 0000000..b2921b4 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLAddressable.cs @@ -0,0 +1,15 @@ +namespace ClosedXML.Excel +{ + /// + /// A very lightweight interface for entities that have an address as + /// a rectangular range. + /// + public interface IXLAddressable + { + /// + /// Gets an object with the boundaries of this range. + /// + + IXLRangeAddress RangeAddress { get; } + } +} diff --git a/ClosedXML/Excel/Ranges/IXLBaseCollection.cs b/ClosedXML/Excel/Ranges/IXLBaseCollection.cs new file mode 100644 index 0000000..d4d6c83 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLBaseCollection.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLBaseCollection : IEnumerable + { + Int32 Count { get; } + + IXLStyle Style { get; set; } + + IXLDataValidation SetDataValidation(); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// The default scope for the named range is Workbook. + /// + /// Name of the range. + TMultiple AddToNamed(String rangeName); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// Name of the range. + /// The scope for the named range. + /// + TMultiple AddToNamed(String rangeName, XLScope scope); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// Name of the range. + /// The scope for the named range. + /// The comments for the named range. + /// + TMultiple AddToNamed(String rangeName, XLScope scope, String comment); + + /// + /// Sets the cells' value. + /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell. + /// If the object is a range ClosedXML will copy the range starting from each cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string. + /// + /// + /// The object containing the value(s) to set. + /// + Object Value { set; } + + TMultiple SetValue(T value); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + IXLCells CellsUsed(Boolean includeFormats); + + TMultiple SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these ranges. + /// + /// Specify what you want to clear. + TMultiple Clear(XLClearOptions clearOptions = XLClearOptions.All); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRange.cs b/ClosedXML/Excel/Ranges/IXLRange.cs new file mode 100644 index 0000000..527a8ce --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRange.cs @@ -0,0 +1,313 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLShiftDeletedCells { ShiftCellsUp, ShiftCellsLeft } + + public enum XLTransposeOptions { MoveCells, ReplaceCells } + + public enum XLSearchContents { Values, Formulas, ValuesAndFormulas } + + public interface IXLRange : IXLRangeBase + { + /// + /// Gets the cell at the specified row and column. + /// The cell address is relative to the parent range. + /// + /// The cell's row. + /// The cell's column. + IXLCell Cell(int row, int column); + + /// Gets the cell at the specified address. + /// The cell address is relative to the parent range. + /// The cell address in the parent range. + IXLCell Cell(string cellAddressInRange); + + /// + /// Gets the cell at the specified row and column. + /// The cell address is relative to the parent range. + /// + /// The cell's row. + /// The cell's column. + IXLCell Cell(int row, string column); + + /// Gets the cell at the specified address. + /// The cell address is relative to the parent range. + /// The cell address in the parent range. + IXLCell Cell(IXLAddress cellAddressInRange); + + /// + /// Gets the specified column of the range. + /// + /// 1-based column number relative to the first column of this range. + /// The relevant column + IXLRangeColumn Column(int columnNumber); + + /// + /// Gets the specified column of the range. + /// + /// Column letter. + IXLRangeColumn Column(string columnLetter); + + /// + /// Gets the first column of the range. + /// + IXLRangeColumn FirstColumn(Func predicate = null); + + /// + /// Gets the first column of the range that contains a cell with a value. + /// + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn FirstColumnUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeColumn FirstColumnUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeColumn FirstColumnUsed(Func predicate = null); + + /// + /// Gets the last column of the range. + /// + IXLRangeColumn LastColumn(Func predicate = null); + + /// + /// Gets the last column of the range that contains a cell with a value. + /// + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn LastColumnUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeColumn LastColumnUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeColumn LastColumnUsed(Func predicate = null); + + /// + /// Gets a collection of all columns in this range. + /// + IXLRangeColumns Columns(Func predicate = null); + + /// + /// Gets a collection of the specified columns in this range. + /// + /// The first column to return. 1-based column number relative to the first column of this range. + /// The last column to return. 1-based column number relative to the first column of this range. + IXLRangeColumns Columns(int firstColumn, int lastColumn); + + /// + /// Gets a collection of the specified columns in this range. + /// + /// The first column to return. + /// The last column to return. + /// The relevant columns + IXLRangeColumns Columns(string firstColumn, string lastColumn); + + /// + /// Gets a collection of the specified columns in this range, separated by commas. + /// e.g. Columns("G:H"), Columns("10:11,13:14"), Columns("P:Q,S:T"), Columns("V") + /// + /// The columns to return. + IXLRangeColumns Columns(string columns); + + /// + /// Returns the first row that matches the given predicate + /// + IXLRangeColumn FindColumn(Func predicate); + + /// + /// Returns the first row that matches the given predicate + /// + IXLRangeRow FindRow(Func predicate); + + /// + /// Gets the first row of the range. + /// + IXLRangeRow FirstRow(Func predicate = null); + + /// + /// Gets the first row of the range that contains a cell with a value. + /// + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow FirstRowUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeRow FirstRowUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeRow FirstRowUsed(Func predicate = null); + + /// + /// Gets the last row of the range. + /// + IXLRangeRow LastRow(Func predicate = null); + + /// + /// Gets the last row of the range that contains a cell with a value. + /// + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow LastRowUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeRow LastRowUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeRow LastRowUsed(Func predicate = null); + + /// + /// Gets the specified row of the range. + /// + /// 1-based row number relative to the first row of this range. + /// The relevant row + IXLRangeRow Row(int row); + + IXLRangeRows Rows(Func predicate = null); + + /// + /// Gets a collection of the specified rows in this range. + /// + /// The first row to return. 1-based row number relative to the first row of this range. + /// The last row to return. 1-based row number relative to the first row of this range. + /// + IXLRangeRows Rows(int firstRow, int lastRow); + + /// + /// Gets a collection of the specified rows in this range, separated by commas. + /// e.g. Rows("4:5"), Rows("7:8,10:11"), Rows("13") + /// + /// The rows to return. + IXLRangeRows Rows(string rows); + + /// + /// Returns the specified range. + /// + /// The range boundaries. + IXLRange Range(IXLRangeAddress rangeAddress); + + /// Returns the specified range. + /// e.g. Range("A1"), Range("A1:C2") + /// The range boundaries. + IXLRange Range(string rangeAddress); + + /// Returns the specified range. + /// The first cell in the range. + /// The last cell in the range. + IXLRange Range(IXLCell firstCell, IXLCell lastCell); + + /// Returns the specified range. + /// The first cell address in the range. + /// The last cell address in the range. + IXLRange Range(string firstCellAddress, string lastCellAddress); + + /// Returns the specified range. + /// The first cell address in the range. + /// The last cell address in the range. + IXLRange Range(IXLAddress firstCellAddress, IXLAddress lastCellAddress); + + /// Returns a collection of ranges, separated by commas. + /// e.g. Ranges("A1"), Ranges("A1:C2"), Ranges("A1:B2,D1:D4") + /// The ranges to return. + IXLRanges Ranges(string ranges); + + /// Returns the specified range. + /// The first cell's row of the range to return. + /// The first cell's column of the range to return. + /// The last cell's row of the range to return. + /// The last cell's column of the range to return. + /// . + IXLRange Range(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn); + + /// Gets the number of rows in this range. + int RowCount(); + + /// Gets the number of columns in this range. + int ColumnCount(); + + /// + /// Inserts X number of columns to the right of this range. + /// All cells to the right of this range will be shifted X number of columns. + /// + /// Number of columns to insert. + IXLRangeColumns InsertColumnsAfter(int numberOfColumns); + + IXLRangeColumns InsertColumnsAfter(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of columns to the left of this range. + /// This range and all cells to the right of this range will be shifted X number of columns. + /// + /// Number of columns to insert. + IXLRangeColumns InsertColumnsBefore(int numberOfColumns); + + IXLRangeColumns InsertColumnsBefore(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of rows on top of this range. + /// This range and all cells below this range will be shifted X number of rows. + /// + /// Number of rows to insert. + IXLRangeRows InsertRowsAbove(int numberOfRows); + + IXLRangeRows InsertRowsAbove(int numberOfRows, Boolean expandRange); + + /// + /// Inserts X number of rows below this range. + /// All cells below this range will be shifted X number of rows. + /// + /// Number of rows to insert. + IXLRangeRows InsertRowsBelow(int numberOfRows); + + IXLRangeRows InsertRowsBelow(int numberOfRows, Boolean expandRange); + + /// + /// Deletes this range and shifts the surrounding cells accordingly. + /// + /// How to shift the surrounding cells. + void Delete(XLShiftDeletedCells shiftDeleteCells); + + /// + /// Transposes the contents and styles of all cells in this range. + /// + /// How to handle the surrounding cells when transposing the range. + void Transpose(XLTransposeOptions transposeOption); + + IXLTable AsTable(); + + IXLTable AsTable(String name); + + IXLTable CreateTable(); + + IXLTable CreateTable(String name); + + IXLRange RangeUsed(); + + IXLRange CopyTo(IXLCell target); + + IXLRange CopyTo(IXLRangeBase target); + + IXLSortElements SortRows { get; } + IXLSortElements SortColumns { get; } + + IXLRange Sort(); + + IXLRange Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRange Sort(Int32 columnToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRange SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRange SetDataType(XLDataType dataType); + + /// + /// Clears the contents of this range. + /// + /// Specify what you want to clear. + new IXLRange Clear(XLClearOptions clearOptions = XLClearOptions.All); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRows RowsUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeRows RowsUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeRows RowsUsed(Func predicate = null); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumns ColumnsUsed(Boolean includeFormats, Func predicate = null); + + IXLRangeColumns ColumnsUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLRangeColumns ColumnsUsed(Func predicate = null); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeAddress.cs b/ClosedXML/Excel/Ranges/IXLRangeAddress.cs new file mode 100644 index 0000000..1dc6d50 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeAddress.cs @@ -0,0 +1,110 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLRangeAddress + { + /// + /// Gets the number of columns in the area covered by the range address. + /// + int ColumnSpan { get; } + + /// + /// Gets or sets the first address in the range. + /// + /// + /// The first address. + /// + IXLAddress FirstAddress { get; } + + /// + /// Gets or sets a value indicating whether this range is valid. + /// + /// + /// true if this instance is valid; otherwise, false. + /// + Boolean IsValid { get; } + + /// + /// Gets or sets the last address in the range. + /// + /// + /// The last address. + /// + IXLAddress LastAddress { get; } + + /// + /// Gets the number of cells in the area covered by the range address. + /// + int NumberOfCells { get; } + + /// + /// Gets the number of rows in the area covered by the range address. + /// + int RowSpan { get; } + + IXLWorksheet Worksheet { get; } + + /// Allocates the current range address in the internal range repository and returns it + IXLRange AsRange(); + + Boolean Contains(IXLAddress address); + + /// + /// Returns the intersection of this range address with another range address on the same worksheet. + /// + /// The other range address. + /// The intersection's range address + IXLRangeAddress Intersection(IXLRangeAddress otherRangeAddress); + + Boolean Intersects(IXLRangeAddress otherAddress); + + /// + /// Determines whether range address spans the entire column. + /// + /// + /// true if is entire column; otherwise, false. + /// + Boolean IsEntireColumn(); + + /// + /// Determines whether range address spans the entire row. + /// + /// + /// true if is entire row; otherwise, false. + /// + Boolean IsEntireRow(); + + /// + /// Determines whether the range address spans the entire worksheet. + /// + /// + /// true if is entire sheet; otherwise, false. + /// + Boolean IsEntireSheet(); + + /// + /// Returns a range address so that its offset from the target base address is equal to the offset of the current range address to the source base address. + /// For example, if the current range address is D4:E4, the source base address is A1:C3, then the relative address to the target base address B10:D13 is E14:F14 + /// + /// The source base range address. + /// The target base range address. + /// The relative range + IXLRangeAddress Relative(IXLRangeAddress sourceRangeAddress, IXLRangeAddress targetRangeAddress); + + String ToString(XLReferenceStyle referenceStyle); + + String ToString(XLReferenceStyle referenceStyle, Boolean includeSheet); + + String ToStringFixed(); + + String ToStringFixed(XLReferenceStyle referenceStyle); + + String ToStringFixed(XLReferenceStyle referenceStyle, Boolean includeSheet); + + String ToStringRelative(); + + String ToStringRelative(Boolean includeSheet); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeBase.cs b/ClosedXML/Excel/Ranges/IXLRangeBase.cs new file mode 100644 index 0000000..3af95e5 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeBase.cs @@ -0,0 +1,379 @@ +using System; +using System.Globalization; + +namespace ClosedXML.Excel +{ + public enum XLScope + { + Workbook, + Worksheet + } + + public interface IXLRangeBase : IXLAddressable + { + IXLWorksheet Worksheet { get; } + + /// + /// Sets a value to every cell in this range. + /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell. + /// If the object is a range ClosedXML will copy the range starting from each cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string. + /// + /// + /// The object containing the value(s) to set. + /// + Object Value { set; } + + /// + /// Sets the type of the cells' data. + /// Changing the data type will cause ClosedXML to covert the current value to the new data type. + /// An exception will be thrown if the current value cannot be converted to the new data type. + /// + /// + /// The type of the cell's data. + /// + /// + XLDataType DataType { set; } + + /// + /// Sets the cells' formula with A1 references. + /// + /// The formula with A1 references. + String FormulaA1 { set; } + + /// + /// Sets the cells' formula with R1C1 references. + /// + /// The formula with R1C1 references. + String FormulaR1C1 { set; } + + IXLStyle Style { get; set; } + + /// + /// Gets or sets a value indicating whether this cell's text should be shared or not. + /// + /// + /// If false the cell's text will not be shared and stored as an inline value. + /// + Boolean ShareString { set; } + + IXLHyperlinks Hyperlinks { get; } + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + IXLCells Cells(Boolean usedCellsOnly); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells Cells(Boolean usedCellsOnly, Boolean includeFormats); + + IXLCells Cells(Boolean usedCellsOnly, XLCellsUsedOptions options); + + IXLCells Cells(String cells); + + IXLCells Cells(Func predicate); + + /// + /// Returns the collection of cells that have a value. Formats are ignored. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + IXLCells CellsUsed(Func predicate); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats, Func predicate); + + IXLCells CellsUsed(XLCellsUsedOptions options, Func predicate); + + /// + /// Searches the cells' contents for a given piece of text + /// + /// The search text. + /// The compare options. + /// if set to true search formulae instead of cell values. + /// + IXLCells Search(String searchText, CompareOptions compareOptions = CompareOptions.Ordinal, Boolean searchFormulae = false); + + /// + /// Returns the first cell of this range. + /// + IXLCell FirstCell(); + + /// + /// Returns the first cell with a value of this range. Formats are ignored. + /// The cell's address is going to be ([First Row with a value], [First Column with a value]) + /// + IXLCell FirstCellUsed(); + + /// + /// Returns the first cell with a value of this range. + /// + /// The cell's address is going to be ([First Row with a value], [First Column with a value]) + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell FirstCellUsed(Boolean includeFormats); + + IXLCell FirstCellUsed(XLCellsUsedOptions options); + + IXLCell FirstCellUsed(Func predicate); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell FirstCellUsed(Boolean includeFormats, Func predicate); + + IXLCell FirstCellUsed(XLCellsUsedOptions options, Func predicate); + + /// + /// Returns the last cell of this range. + /// + IXLCell LastCell(); + + /// + /// Returns the last cell with a value of this range. Formats are ignored. + /// The cell's address is going to be ([Last Row with a value], [Last Column with a value]) + /// + IXLCell LastCellUsed(); + + /// + /// Returns the last cell with a value of this range. + /// + /// The cell's address is going to be ([Last Row with a value], [Last Column with a value]) + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell LastCellUsed(Boolean includeFormats); + + IXLCell LastCellUsed(XLCellsUsedOptions options); + + IXLCell LastCellUsed(Func predicate); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell LastCellUsed(Boolean includeFormats, Func predicate); + + IXLCell LastCellUsed(XLCellsUsedOptions options, Func predicate); + + /// + /// Determines whether this range contains the specified range (completely). + /// For partial matches use the range.Intersects method. + /// + /// The range address. + /// + /// true if this range contains the specified range; otherwise, false. + /// + Boolean Contains(String rangeAddress); + + /// + /// Determines whether this range contains the specified range (completely). + /// For partial matches use the range.Intersects method. + /// + /// The range to match. + /// + /// true if this range contains the specified range; otherwise, false. + /// + Boolean Contains(IXLRangeBase range); + + Boolean Contains(IXLCell cell); + + /// + /// Determines whether this range intersects the specified range. + /// For whole matches use the range.Contains method. + /// + /// The range address. + /// + /// true if this range intersects the specified range; otherwise, false. + /// + Boolean Intersects(String rangeAddress); + + /// + /// Determines whether this range contains the specified range. + /// For whole matches use the range.Contains method. + /// + /// The range to match. + /// + /// true if this range intersects the specified range; otherwise, false. + /// + Boolean Intersects(IXLRangeBase range); + + /// + /// Unmerges this range. + /// + IXLRange Unmerge(); + + /// + /// Merges this range. + /// The contents and style of the merged cells will be equal to the first cell. + /// + IXLRange Merge(); + + IXLRange Merge(Boolean checkIntersect); + + /// + /// Creates a named range out of this range. + /// If the named range exists, it will add this range to that named range. + /// The default scope for the named range is Workbook. + /// + /// Name of the range. + IXLRange AddToNamed(String rangeName); + + /// + /// Creates a named range out of this range. + /// If the named range exists, it will add this range to that named range. + /// Name of the range. + /// The scope for the named range. + /// + IXLRange AddToNamed(String rangeName, XLScope scope); + + /// + /// Creates a named range out of this range. + /// If the named range exists, it will add this range to that named range. + /// Name of the range. + /// The scope for the named range. + /// The comments for the named range. + /// + IXLRange AddToNamed(String rangeName, XLScope scope, String comment); + + /// + /// Clears the contents of this range. + /// + /// Specify what you want to clear. + IXLRangeBase Clear(XLClearOptions clearOptions = XLClearOptions.All); + + /// + /// Deletes the cell comments from this range. + /// + void DeleteComments(); + + IXLRangeBase SetValue(T value); + + /// + /// Converts this object to a range. + /// + IXLRange AsRange(); + + Boolean IsMerged(); + + Boolean IsEmpty(); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + Boolean IsEmpty(Boolean includeFormats); + + Boolean IsEmpty(XLCellsUsedOptions options); + + /// + /// Determines whether range address spans the entire column. + /// + /// + /// true if is entire column; otherwise, false. + /// + Boolean IsEntireColumn(); + + /// + /// Determines whether range address spans the entire row. + /// + /// + /// true if is entire row; otherwise, false. + /// + + Boolean IsEntireRow(); + + /// + /// Determines whether the range address spans the entire worksheet. + /// + /// + /// true if is entire sheet; otherwise, false. + /// + Boolean IsEntireSheet(); + + IXLPivotTable CreatePivotTable(IXLCell targetCell, String name); + + //IXLChart CreateChart(Int32 firstRow, Int32 firstColumn, Int32 lastRow, Int32 lastColumn); + + IXLAutoFilter SetAutoFilter(); + + IXLAutoFilter SetAutoFilter(Boolean value); + + IXLDataValidation SetDataValidation(); + + IXLConditionalFormat AddConditionalFormat(); + + void Select(); + + /// + /// Grows this the current range by one cell to each side + /// + IXLRangeBase Grow(); + + /// + /// Grows this the current range by the specified number of cells to each side. + /// + /// The grow count. + /// + IXLRangeBase Grow(Int32 growCount); + + /// + /// Shrinks this current range by one cell. + /// + IXLRangeBase Shrink(); + + /// + /// Shrinks the current range by the specified number of cells from each side. + /// + /// The shrink count. + /// + IXLRangeBase Shrink(Int32 shrinkCount); + + /// + /// Returns the intersection of this range with another range on the same worksheet. + /// + /// The other range. + /// Predicate applied to this range's cells. + /// Predicate applied to the other range's cells. + /// The range address of the intersection + IXLRangeAddress Intersection(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null); + + /// + /// Returns the set of cells surrounding the current range. + /// + /// The predicate to apply on the resulting set of cells. + IXLCells SurroundingCells(Func predicate = null); + + /// + /// Calculates the union of two ranges on the same worksheet. + /// + /// The other range. + /// Predicate applied to this range's cells. + /// Predicate applied to the other range's cells. + /// + /// The union + /// + IXLCells Union(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null); + + /// + /// Returns all cells in the current range that are not in the other range. + /// + /// The other range. + /// Predicate applied to this range's cells. + /// Predicate applied to the other range's cells. + /// + IXLCells Difference(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null); + + /// + /// Returns a range so that its offset from the target base range is equal to the offset of the current range to the source base range. + /// For example, if the current range is D4:E4, the source base range is A1:C3, then the relative range to the target base range B10:D13 is E14:F14 + /// + /// The source base range. + /// The target base range. + /// The relative range + IXLRangeBase Relative(IXLRangeBase sourceBaseRange, IXLRangeBase targetBaseRange); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeColumn.cs b/ClosedXML/Excel/Ranges/IXLRangeColumn.cs new file mode 100644 index 0000000..48094b3 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeColumn.cs @@ -0,0 +1,129 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLRangeColumn : IXLRangeBase + { + /// + /// Gets the cell in the specified row. + /// + /// The cell's row. + IXLCell Cell(Int32 rowNumber); + + /// + /// Returns the specified group of cells, separated by commas. + /// e.g. Cells("1"), Cells("1:5"), Cells("1:2,4:5") + /// + /// The column cells to return. + new IXLCells Cells(String cellsInColumn); + + /// + /// Returns the specified group of cells. + /// + /// The first row in the group of cells to return. + /// The last row in the group of cells to return. + IXLCells Cells(Int32 firstRow, Int32 lastRow); + + /// + /// Inserts X number of columns to the right of this range. + /// All cells to the right of this range will be shifted X number of columns. + /// + /// Number of columns to insert. + IXLRangeColumns InsertColumnsAfter(int numberOfColumns); + + IXLRangeColumns InsertColumnsAfter(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of columns to the left of this range. + /// This range and all cells to the right of this range will be shifted X number of columns. + /// + /// Number of columns to insert. + IXLRangeColumns InsertColumnsBefore(int numberOfColumns); + + IXLRangeColumns InsertColumnsBefore(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of cells on top of this column. + /// This column and all cells below it will be shifted X number of rows. + /// + /// Number of cells to insert. + IXLCells InsertCellsAbove(int numberOfRows); + + IXLCells InsertCellsAbove(int numberOfRows, Boolean expandRange); + + /// + /// Inserts X number of cells below this range. + /// All cells below this column will be shifted X number of rows. + /// + /// Number of cells to insert. + IXLCells InsertCellsBelow(int numberOfRows); + + IXLCells InsertCellsBelow(int numberOfRows, Boolean expandRange); + + /// + /// Deletes this range and shifts the cells at the right. + /// + void Delete(); + + /// + /// Deletes this range and shifts the surrounding cells accordingly. + /// + /// How to shift the surrounding cells. + void Delete(XLShiftDeletedCells shiftDeleteCells); + + /// + /// Gets this column's number in the range + /// + Int32 ColumnNumber(); + + /// + /// Gets this column's letter in the range + /// + String ColumnLetter(); + + Int32 CellCount(); + + IXLRangeColumn CopyTo(IXLCell target); + + IXLRangeColumn CopyTo(IXLRangeBase target); + + IXLRangeColumn Sort(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRangeColumn Column(Int32 start, Int32 end); + + IXLRangeColumn Column(IXLCell start, IXLCell end); + + IXLRangeColumns Columns(String columns); + + IXLRangeColumn SetDataType(XLDataType dataType); + + IXLRangeColumn ColumnLeft(); + + IXLRangeColumn ColumnLeft(Int32 step); + + IXLRangeColumn ColumnRight(); + + IXLRangeColumn ColumnRight(Int32 step); + + IXLColumn WorksheetColumn(); + + IXLTable AsTable(); + + IXLTable AsTable(String name); + + IXLTable CreateTable(); + + IXLTable CreateTable(String name); + + /// + /// Clears the contents of this column. + /// + /// Specify what you want to clear. + new IXLRangeColumn Clear(XLClearOptions clearOptions = XLClearOptions.All); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn ColumnUsed(Boolean includeFormats); + + IXLRangeColumn ColumnUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeColumns.cs b/ClosedXML/Excel/Ranges/IXLRangeColumns.cs new file mode 100644 index 0000000..ad97118 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeColumns.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLRangeColumns : IEnumerable + { + /// + /// Adds a column range to this group. + /// + /// The column range to add. + void Add(IXLRangeColumn columRange); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + /// + /// Deletes all columns and shifts the columns at the right of them accordingly. + /// + void Delete(); + + IXLStyle Style { get; set; } + + IXLRangeColumns SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these columns. + /// + /// Specify what you want to clear. + IXLRangeColumns Clear(XLClearOptions clearOptions = XLClearOptions.All); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeRow.cs b/ClosedXML/Excel/Ranges/IXLRangeRow.cs new file mode 100644 index 0000000..d709a88 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeRow.cs @@ -0,0 +1,131 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLRangeRow : IXLRangeBase + { + /// + /// Gets the cell in the specified column. + /// + /// The cell's column. + IXLCell Cell(Int32 columnNumber); + + /// + /// Gets the cell in the specified column. + /// + /// The cell's column. + IXLCell Cell(String columnLetter); + + /// + /// Returns the specified group of cells, separated by commas. + /// e.g. Cells("1"), Cells("1:5"), Cells("1:2,4:5") + /// + /// The row's cells to return. + new IXLCells Cells(String cellsInRow); + + /// + /// Returns the specified group of cells. + /// + /// The first column in the group of cells to return. + /// The last column in the group of cells to return. + IXLCells Cells(Int32 firstColumn, Int32 lastColumn); + + /// + /// Returns the specified group of cells. + /// + /// The first column in the group of cells to return. + /// The last column in the group of cells to return. + IXLCells Cells(String firstColumn, String lastColumn); + + /// + /// Inserts X number of cells to the right of this row. + /// All cells to the right of this row will be shifted X number of columns. + /// + /// Number of cells to insert. + IXLCells InsertCellsAfter(int numberOfColumns); + + IXLCells InsertCellsAfter(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of cells to the left of this row. + /// This row and all cells to the right of it will be shifted X number of columns. + /// + /// Number of cells to insert. + IXLCells InsertCellsBefore(int numberOfColumns); + + IXLCells InsertCellsBefore(int numberOfColumns, Boolean expandRange); + + /// + /// Inserts X number of rows on top of this row. + /// This row and all cells below it will be shifted X number of rows. + /// + /// Number of rows to insert. + IXLRangeRows InsertRowsAbove(int numberOfRows); + + IXLRangeRows InsertRowsAbove(int numberOfRows, Boolean expandRange); + + /// + /// Inserts X number of rows below this row. + /// All cells below this row will be shifted X number of rows. + /// + /// Number of rows to insert. + IXLRangeRows InsertRowsBelow(int numberOfRows); + + IXLRangeRows InsertRowsBelow(int numberOfRows, Boolean expandRange); + + /// + /// Deletes this range and shifts the cells below. + /// + void Delete(); + + /// + /// Deletes this range and shifts the surrounding cells accordingly. + /// + /// How to shift the surrounding cells. + void Delete(XLShiftDeletedCells shiftDeleteCells); + + /// + /// Gets this row's number in the range + /// + Int32 RowNumber(); + + Int32 CellCount(); + + IXLRangeRow CopyTo(IXLCell target); + + IXLRangeRow CopyTo(IXLRangeBase target); + + IXLRangeRow Sort(); + + IXLRangeRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRangeRow Row(Int32 start, Int32 end); + + IXLRangeRow Row(IXLCell start, IXLCell end); + + IXLRangeRows Rows(String rows); + + IXLRangeRow SetDataType(XLDataType dataType); + + IXLRangeRow RowAbove(); + + IXLRangeRow RowAbove(Int32 step); + + IXLRangeRow RowBelow(); + + IXLRangeRow RowBelow(Int32 step); + + IXLRow WorksheetRow(); + + /// + /// Clears the contents of this row. + /// + /// Specify what you want to clear. + new IXLRangeRow Clear(XLClearOptions clearOptions = XLClearOptions.All); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow RowUsed(Boolean includeFormats); + + IXLRangeRow RowUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeRows.cs b/ClosedXML/Excel/Ranges/IXLRangeRows.cs new file mode 100644 index 0000000..280fc8d --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRangeRows.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLRangeRows : IEnumerable + { + /// + /// Adds a row range to this group. + /// + /// The row range to add. + void Add(IXLRangeRow rowRange); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + /// + /// Deletes all rows and shifts the rows below them accordingly. + /// + void Delete(); + + IXLStyle Style { get; set; } + + IXLRangeRows SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these rows. + /// + /// Specify what you want to clear. + IXLRangeRows Clear(XLClearOptions clearOptions = XLClearOptions.All); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRanges.cs b/ClosedXML/Excel/Ranges/IXLRanges.cs new file mode 100644 index 0000000..16dfa72 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLRanges.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLRanges : IEnumerable + { + /// + /// Adds the specified range to this group. + /// + /// The range to add to this group. + void Add(IXLRangeBase range); + + void Add(IXLCell range); + + /// + /// Removes the specified range from this group. + /// + /// The range to remove from this group. + bool Remove(IXLRange range); + + /// + /// Removes ranges matching the criteria from the collection, optionally releasing their event handlers. + /// + /// Criteria to filter ranges. Only those ranges that satisfy the criteria will be removed. + /// Null means the entire collection should be cleared. + /// Specify whether or not should removed ranges be unsubscribed from + /// row/column shifting events. Until ranges are unsubscribed they cannot be collected by GC. + void RemoveAll(Predicate match = null, bool releaseEventHandlers = true); + + Int32 Count { get; } + + Boolean Contains(IXLRange range); + + /// + /// Filter ranges from a collection that intersect the specified address. Is much more efficient + /// that using Linq expression .Where(). + /// + IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress); + + /// + /// Filter ranges from a collection that intersect the specified address. Is much more efficient + /// that using Linq expression .Where(). + /// + IEnumerable GetIntersectedRanges(IXLAddress address); + + /// + /// Filter ranges from a collection that intersect the specified cell. Is much more efficient + /// that using Linq expression .Where(). + /// + IEnumerable GetIntersectedRanges(IXLCell cell); + + + IXLStyle Style { get; set; } + + IXLDataValidation SetDataValidation(); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// The default scope for the named range is Workbook. + /// + /// Name of the range. + IXLRanges AddToNamed(String rangeName); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// Name of the range. + /// The scope for the named range. + /// + IXLRanges AddToNamed(String rangeName, XLScope scope); + + /// + /// Creates a named range out of these ranges. + /// If the named range exists, it will add these ranges to that named range. + /// Name of the range. + /// The scope for the named range. + /// The comments for the named range. + /// + IXLRanges AddToNamed(String rangeName, XLScope scope, String comment); + + /// + /// Sets the cells' value. + /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell. + /// If the object is a range ClosedXML will copy the range starting from each cell. + /// Setting the value to an object (not IEnumerable/range) will call the object's ToString() method. + /// ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string. + /// + /// + /// The object containing the value(s) to set. + /// + Object Value { set; } + + IXLRanges SetValue(T value); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + IXLRanges SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these ranges. + /// + /// Specify what you want to clear. + IXLRanges Clear(XLClearOptions clearOptions = XLClearOptions.All); + + /// + /// Create a new collection of ranges which are consolidated version of source ranges. + /// + IXLRanges Consolidate(); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs b/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs new file mode 100644 index 0000000..ecc0e88 --- /dev/null +++ b/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel.Ranges.Index +{ + /// + /// Interface for the engine aimed to speed-up the search for the range intersections. + /// + internal interface IXLRangeIndex + { + bool Add(IXLAddressable range); + + bool Remove(IXLRangeAddress rangeAddress); + + int RemoveAll(Predicate predicate = null); + + IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress); + + IEnumerable GetIntersectedRanges(XLAddress address); + + IEnumerable GetAll(); + + bool Intersects(in XLRangeAddress rangeAddress); + + bool Contains(in XLAddress address); + + bool MatchesType(XLRangeType rangeType); + } + + internal interface IXLRangeIndex : IXLRangeIndex + where T : IXLAddressable + { + bool Add(T range); + + int RemoveAll(Predicate predicate = null); + + new IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress); + + new IEnumerable GetIntersectedRanges(XLAddress address); + + new IEnumerable GetAll(); + } +} diff --git a/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs b/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs new file mode 100644 index 0000000..fd92068 --- /dev/null +++ b/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs @@ -0,0 +1,271 @@ +using ClosedXML.Excel.Patterns; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel.Ranges.Index +{ + /// + /// Implementation of internally using QuadTree. + /// + internal abstract class XLRangeIndex : IXLRangeIndex + { + #region Public Constructors + + public XLRangeIndex(IXLWorksheet worksheet) + { + _worksheet = worksheet; + _rangeList = new List(); + (_worksheet as XLWorksheet).RegisterRangeIndex(this); + } + + #endregion Public Constructors + + #region Public Methods + + public abstract bool MatchesType(XLRangeType rangeType); + + public bool Add(IXLAddressable range) + { + if (range == null) + throw new ArgumentNullException(nameof(range)); + + if (!range.RangeAddress.IsValid) + throw new ArgumentException("Range is invalid"); + + CheckWorksheet(range.RangeAddress.Worksheet); + + _count++; + if (_count < MinimumCountForIndexing) + { + if (_rangeList.Any(r => r == range)) + return false; + + _rangeList.Add(range); + return true; + } + + if (_quadTree == null) + InitializeTree(); + + return _quadTree.Add(range); + } + + public bool Contains(in XLAddress address) + { + CheckWorksheet(address.Worksheet); + + if (_quadTree == null) + { + var addr = address; + return _rangeList.Any(r => r.RangeAddress.Contains(addr)); + } + + return _quadTree.GetIntersectedRanges(address).Any(); + } + + public IEnumerable GetAll() + { + if (_quadTree == null) + { + return _rangeList.AsEnumerable(); + } + + return _quadTree.GetAll(); + } + + public IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress) + { + CheckWorksheet(rangeAddress.Worksheet); + + if (_quadTree == null) + { + return _rangeList.Where(r => r.RangeAddress.Intersects(rangeAddress)); + } + + return _quadTree.GetIntersectedRanges(rangeAddress); + } + + public IEnumerable GetIntersectedRanges(XLAddress address) + { + CheckWorksheet(address.Worksheet); + + if (_quadTree == null) + { + return _rangeList.Where(r => r.RangeAddress.Contains(address)); + } + + return _quadTree.GetIntersectedRanges(address); + } + + public bool Intersects(in XLRangeAddress rangeAddress) + { + CheckWorksheet(rangeAddress.Worksheet); + + if (_quadTree == null) + { + var addr = rangeAddress; + return _rangeList.Any(r => r.RangeAddress.Intersects(addr)); + } + + return _quadTree.GetIntersectedRanges(rangeAddress).Any(); + } + + public bool Remove(IXLRangeAddress rangeAddress) + { + if (rangeAddress == null) + throw new ArgumentNullException(nameof(rangeAddress)); + + CheckWorksheet(rangeAddress.Worksheet); + + if (_quadTree == null) + { + return _rangeList.RemoveAll(r => Equals(r.RangeAddress, rangeAddress)) > 0; + } + + return _quadTree.Remove(rangeAddress); + } + + public int RemoveAll(Predicate predicate = null) + { + predicate = predicate ?? (_ => true); + + if (_quadTree == null) + { + return _rangeList.RemoveAll(predicate); + } + + return _quadTree.RemoveAll(predicate).Count(); + } + + #endregion Public Methods + + #region Private Fields + + /// + /// The minimum number of ranges to be included into a QuadTree. Until it is reached the ranges + /// are added into a simple list to minimize the overhead of searching intersections on small collections. + /// + private const int MinimumCountForIndexing = 20; + + /// + /// A collection of ranges used before the QuadTree is initialized (until + /// is reached. + /// + protected readonly List _rangeList; + + private readonly IXLWorksheet _worksheet; + private int _count = 0; + protected Quadrant _quadTree; + + #endregion Private Fields + + #region Private Methods + + private void CheckWorksheet(IXLWorksheet worksheet) + { + if (worksheet != _worksheet) + throw new ArgumentException("Range belongs to a different worksheet"); + } + + private void InitializeTree() + { + _quadTree = CreateQuadTree(); + _rangeList.ForEach(r => _quadTree.Add(r)); + _rangeList.Clear(); + } + + protected virtual Quadrant CreateQuadTree() + { + return new Quadrant(); + } + + #endregion Private Methods + } + + /// + /// Generic version of . + /// + internal class XLRangeIndex : XLRangeIndex, IXLRangeIndex + where T : IXLAddressable + { + public XLRangeIndex(IXLWorksheet worksheet) : base(worksheet) + { + } + + public bool Add(T range) + { + return base.Add(range); + } + + public int RemoveAll(Predicate predicate) + { + predicate = predicate ?? (_ => true); + + return base.RemoveAll(r => predicate((T)r)); + } + + public new IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress) + { + return base.GetIntersectedRanges(rangeAddress).Cast(); + } + + public new IEnumerable GetIntersectedRanges(XLAddress address) + { + return base.GetIntersectedRanges(address).Cast(); + } + + public override bool MatchesType(XLRangeType rangeType) + { + var innerType = typeof(T); + + if (innerType == typeof(IXLRangeBase) || + innerType == typeof(XLRangeBase)) + return true; + + switch (rangeType) + { + case XLRangeType.Range: + return innerType == typeof(IXLRange) || + innerType == typeof(XLRange); + + case XLRangeType.Column: + return innerType == typeof(IXLColumn) || + innerType == typeof(XLColumn); + + case XLRangeType.Row: + return innerType == typeof(IXLRow) || + innerType == typeof(XLRow); + + case XLRangeType.RangeColumn: + return innerType == typeof(IXLRangeColumn) || + innerType == typeof(XLRangeColumn); + + case XLRangeType.RangeRow: + return innerType == typeof(IXLRangeRow) || + innerType == typeof(XLRangeRow); + + case XLRangeType.Table: + return innerType == typeof(IXLTable) || + innerType == typeof(XLTable); + + case XLRangeType.Worksheet: + return innerType == typeof(IXLWorksheet) || + innerType == typeof(XLWorksheet); + + default: + throw new NotImplementedException(nameof(rangeType)); + } + } + + public new IEnumerable GetAll() + { + return base.GetAll().Cast(); + } + + protected override Quadrant CreateQuadTree() + { + return new Quadrant(); + } + } +} diff --git a/ClosedXML/Excel/Ranges/Sort/IXLSortElement.cs b/ClosedXML/Excel/Ranges/Sort/IXLSortElement.cs new file mode 100644 index 0000000..e7ec5d9 --- /dev/null +++ b/ClosedXML/Excel/Ranges/Sort/IXLSortElement.cs @@ -0,0 +1,14 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLSortOrder { Ascending, Descending } + public enum XLSortOrientation { TopToBottom, LeftToRight } + public interface IXLSortElement + { + Int32 ElementNumber { get; set; } + XLSortOrder SortOrder { get; set; } + Boolean IgnoreBlanks { get; set; } + Boolean MatchCase { get; set; } + } +} diff --git a/ClosedXML/Excel/Ranges/Sort/IXLSortElements.cs b/ClosedXML/Excel/Ranges/Sort/IXLSortElements.cs new file mode 100644 index 0000000..24f2fc9 --- /dev/null +++ b/ClosedXML/Excel/Ranges/Sort/IXLSortElements.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLSortElements: IEnumerable + { + void Add(Int32 elementNumber); + void Add(Int32 elementNumber, XLSortOrder sortOrder); + void Add(Int32 elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks); + void Add(Int32 elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks, Boolean matchCase); + + void Add(String elementNumber); + void Add(String elementNumber, XLSortOrder sortOrder); + void Add(String elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks); + void Add(String elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks, Boolean matchCase); + + void Clear(); + + void Remove(Int32 elementNumber); + } +} diff --git a/ClosedXML/Excel/Ranges/Sort/XLSortElement.cs b/ClosedXML/Excel/Ranges/Sort/XLSortElement.cs new file mode 100644 index 0000000..bb801d7 --- /dev/null +++ b/ClosedXML/Excel/Ranges/Sort/XLSortElement.cs @@ -0,0 +1,12 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLSortElement: IXLSortElement + { + public Int32 ElementNumber { get; set; } + public XLSortOrder SortOrder { get; set; } + public Boolean IgnoreBlanks { get; set; } + public Boolean MatchCase { get; set; } + } +} diff --git a/ClosedXML/Excel/Ranges/Sort/XLSortElements.cs b/ClosedXML/Excel/Ranges/Sort/XLSortElements.cs new file mode 100644 index 0000000..7aa7c8b --- /dev/null +++ b/ClosedXML/Excel/Ranges/Sort/XLSortElements.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLSortElements: IXLSortElements + { + List elements = new List(); + public void Add(Int32 elementNumber) + { + Add(elementNumber, XLSortOrder.Ascending); + } + public void Add(Int32 elementNumber, XLSortOrder sortOrder) + { + Add(elementNumber, sortOrder, true); + } + public void Add(Int32 elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks) + { + Add(elementNumber, sortOrder, ignoreBlanks, false); + } + public void Add(Int32 elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks, Boolean matchCase) + { + elements.Add(new XLSortElement() + { + ElementNumber = elementNumber, + SortOrder = sortOrder, + IgnoreBlanks = ignoreBlanks, + MatchCase = matchCase + }); + } + + public void Add(String elementNumber) + { + Add(elementNumber, XLSortOrder.Ascending); + } + public void Add(String elementNumber, XLSortOrder sortOrder) + { + Add(elementNumber, sortOrder, true); + } + public void Add(String elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks) + { + Add(elementNumber, sortOrder, ignoreBlanks, false); + } + public void Add(String elementNumber, XLSortOrder sortOrder, Boolean ignoreBlanks, Boolean matchCase) + { + elements.Add(new XLSortElement() + { + ElementNumber = XLHelper.GetColumnNumberFromLetter(elementNumber), + SortOrder = sortOrder, + IgnoreBlanks = ignoreBlanks, + MatchCase = matchCase + }); + } + + public IEnumerator GetEnumerator() + { + return elements.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Clear() + { + elements.Clear(); + } + + public void Remove(Int32 elementNumber) + { + elements.RemoveAt(elementNumber - 1); + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRange.cs b/ClosedXML/Excel/Ranges/XLRange.cs new file mode 100644 index 0000000..57236ba --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRange.cs @@ -0,0 +1,958 @@ +using ClosedXML.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLRange : XLRangeBase, IXLRange + { + #region Constructor + + public XLRange(XLRangeParameters xlRangeParameters) + : base(xlRangeParameters.RangeAddress, (xlRangeParameters.DefaultStyle as XLStyle).Value) + { + } + + #endregion Constructor + + public override XLRangeType RangeType + { + get { return XLRangeType.Range; } + } + + #region IXLRange Members + + IXLRangeRow IXLRange.Row(Int32 row) + { + return Row(row); + } + + IXLRangeColumn IXLRange.Column(Int32 columnNumber) + { + return Column(columnNumber); + } + + IXLRangeColumn IXLRange.Column(String columnLetter) + { + return Column(columnLetter); + } + + public virtual IXLRangeColumns Columns(Func predicate = null) + { + var retVal = new XLRangeColumns(); + Int32 columnCount = ColumnCount(); + for (Int32 c = 1; c <= columnCount; c++) + { + var column = Column(c); + if (predicate == null || predicate(column)) + retVal.Add(column); + } + return retVal; + } + + public virtual IXLRangeColumns Columns(Int32 firstColumn, Int32 lastColumn) + { + var retVal = new XLRangeColumns(); + + for (int co = firstColumn; co <= lastColumn; co++) + retVal.Add(Column(co)); + return retVal; + } + + public virtual IXLRangeColumns Columns(String firstColumn, String lastColumn) + { + return Columns(XLHelper.GetColumnNumberFromLetter(firstColumn), + XLHelper.GetColumnNumberFromLetter(lastColumn)); + } + + public virtual IXLRangeColumns Columns(String columns) + { + var retVal = new XLRangeColumns(); + var columnPairs = columns.Split(','); + foreach (string tPair in columnPairs.Select(pair => pair.Trim())) + { + String firstColumn; + String lastColumn; + if (tPair.Contains(':') || tPair.Contains('-')) + { + string[] columnRange = XLHelper.SplitRange(tPair); + + firstColumn = columnRange[0]; + lastColumn = columnRange[1]; + } + else + { + firstColumn = tPair; + lastColumn = tPair; + } + + if (Int32.TryParse(firstColumn, out Int32 tmp)) + { + foreach (IXLRangeColumn col in Columns(Int32.Parse(firstColumn), Int32.Parse(lastColumn))) + retVal.Add(col); + } + else + { + foreach (IXLRangeColumn col in Columns(firstColumn, lastColumn)) + retVal.Add(col); + } + } + return retVal; + } + + IXLCell IXLRange.Cell(int row, int column) + { + return Cell(row, column); + } + + IXLCell IXLRange.Cell(string cellAddressInRange) + { + return Cell(cellAddressInRange); + } + + IXLCell IXLRange.Cell(int row, string column) + { + return Cell(row, column); + } + + IXLCell IXLRange.Cell(IXLAddress cellAddressInRange) + { + return Cell(cellAddressInRange); + } + + IXLRange IXLRange.Range(IXLRangeAddress rangeAddress) + { + return Range(rangeAddress); + } + + IXLRange IXLRange.Range(string rangeAddress) + { + return Range(rangeAddress); + } + + IXLRange IXLRange.Range(IXLCell firstCell, IXLCell lastCell) + { + return Range(firstCell, lastCell); + } + + IXLRange IXLRange.Range(string firstCellAddress, string lastCellAddress) + { + return Range(firstCellAddress, lastCellAddress); + } + + IXLRange IXLRange.Range(IXLAddress firstCellAddress, IXLAddress lastCellAddress) + { + return Range(firstCellAddress, lastCellAddress); + } + + IXLRange IXLRange.Range(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn) + { + return Range(firstCellRow, firstCellColumn, lastCellRow, lastCellColumn); + } + + public IXLRangeRows Rows(Func predicate = null) + { + var retVal = new XLRangeRows(); + Int32 rowCount = RowCount(); + for (Int32 r = 1; r <= rowCount; r++) + { + var row = Row(r); + if (predicate == null || predicate(row)) + retVal.Add(Row(r)); + } + return retVal; + } + + public IXLRangeRows Rows(Int32 firstRow, Int32 lastRow) + { + var retVal = new XLRangeRows(); + + for (int ro = firstRow; ro <= lastRow; ro++) + retVal.Add(Row(ro)); + return retVal; + } + + public IXLRangeRows Rows(String rows) + { + var retVal = new XLRangeRows(); + var rowPairs = rows.Split(','); + foreach (string tPair in rowPairs.Select(pair => pair.Trim())) + { + String firstRow; + String lastRow; + if (tPair.Contains(':') || tPair.Contains('-')) + { + string[] rowRange = XLHelper.SplitRange(tPair); + + firstRow = rowRange[0]; + lastRow = rowRange[1]; + } + else + { + firstRow = tPair; + lastRow = tPair; + } + foreach (IXLRangeRow row in Rows(Int32.Parse(firstRow), Int32.Parse(lastRow))) + retVal.Add(row); + } + return retVal; + } + + public void Transpose(XLTransposeOptions transposeOption) + { + int rowCount = RowCount(); + int columnCount = ColumnCount(); + int squareSide = rowCount > columnCount ? rowCount : columnCount; + + var firstCell = FirstCell(); + + MoveOrClearForTranspose(transposeOption, rowCount, columnCount); + TransposeMerged(squareSide); + TransposeRange(squareSide); + RangeAddress = new XLRangeAddress( + RangeAddress.FirstAddress, + new XLAddress(Worksheet, + firstCell.Address.RowNumber + columnCount - 1, + firstCell.Address.ColumnNumber + rowCount - 1, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + + if (rowCount > columnCount) + { + var rng = Worksheet.Range( + RangeAddress.LastAddress.RowNumber + 1, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber + (rowCount - columnCount), + RangeAddress.LastAddress.ColumnNumber); + rng.Delete(XLShiftDeletedCells.ShiftCellsUp); + } + else if (columnCount > rowCount) + { + var rng = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + 1, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + (columnCount - rowCount)); + rng.Delete(XLShiftDeletedCells.ShiftCellsLeft); + } + + foreach (IXLCell c in Range(1, 1, columnCount, rowCount).Cells()) + { + var border = (c.Style as XLStyle).Value.Border; + c.Style.Border.TopBorder = border.LeftBorder; + c.Style.Border.TopBorderColor = border.LeftBorderColor; + c.Style.Border.LeftBorder = border.TopBorder; + c.Style.Border.LeftBorderColor = border.TopBorderColor; + c.Style.Border.RightBorder = border.BottomBorder; + c.Style.Border.RightBorderColor = border.BottomBorderColor; + c.Style.Border.BottomBorder = border.RightBorder; + c.Style.Border.BottomBorderColor = border.RightBorderColor; + } + } + + public IXLTable AsTable() + { + return Worksheet.Table(this, false); + } + + public IXLTable AsTable(String name) + { + return Worksheet.Table(this, name, false); + } + + IXLTable IXLRange.CreateTable() + { + return CreateTable(); + } + + public XLTable CreateTable() + { + return (XLTable)Worksheet.Table(this, true, true); + } + + IXLTable IXLRange.CreateTable(String name) + { + return CreateTable(name); + } + + public XLTable CreateTable(String name) + { + return (XLTable)Worksheet.Table(this, name, true, true); + } + + public IXLTable CreateTable(String name, Boolean setAutofilter) + { + return Worksheet.Table(this, name, true, setAutofilter); + } + + public new IXLRange CopyTo(IXLCell target) + { + base.CopyTo(target); + + Int32 lastRowNumber = target.Address.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + Int32 lastColumnNumber = target.Address.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range(target.Address.RowNumber, + target.Address.ColumnNumber, + lastRowNumber, + lastColumnNumber); + } + + public new IXLRange CopyTo(IXLRangeBase target) + { + base.CopyTo(target); + + Int32 lastRowNumber = target.RangeAddress.FirstAddress.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + Int32 lastColumnNumber = target.RangeAddress.FirstAddress.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range(target.RangeAddress.FirstAddress.RowNumber, + target.RangeAddress.FirstAddress.ColumnNumber, + lastRowNumber, + lastColumnNumber); + } + + public IXLRange SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + public new IXLRange Sort() + { + return base.Sort().AsRange(); + } + + public new IXLRange Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return base.Sort(columnsToSortBy, sortOrder, matchCase, ignoreBlanks).AsRange(); + } + + public new IXLRange Sort(Int32 columnToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return base.Sort(columnToSortBy, sortOrder, matchCase, ignoreBlanks).AsRange(); + } + + public new IXLRange SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return base.SortLeftToRight(sortOrder, matchCase, ignoreBlanks).AsRange(); + } + + #endregion IXLRange Members + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + RangeAddress = (XLRangeAddress)ShiftColumns(RangeAddress, range, columnsShifted); + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + RangeAddress = (XLRangeAddress)ShiftRows(RangeAddress, range, rowsShifted); + } + + IXLRangeColumn IXLRange.FirstColumn(Func predicate) + { + return FirstColumn(predicate); + } + + internal XLRangeColumn FirstColumn(Func predicate = null) + { + if (predicate == null) + return Column(1); + + Int32 columnCount = ColumnCount(); + for (Int32 c = 1; c <= columnCount; c++) + { + var column = Column(c); + if (predicate(column)) return column; + } + + return null; + } + + IXLRangeColumn IXLRange.LastColumn(Func predicate) + { + return LastColumn(predicate); + } + + internal XLRangeColumn LastColumn(Func predicate = null) + { + Int32 columnCount = ColumnCount(); + if (predicate == null) + return Column(columnCount); + + for (Int32 c = columnCount; c >= 1; c--) + { + var column = Column(c); + if (predicate(column)) return column; + } + + return null; + } + + IXLRangeColumn IXLRange.FirstColumnUsed(Func predicate) + { + return FirstColumnUsed(XLCellsUsedOptions.AllContents, predicate); + } + + internal XLRangeColumn FirstColumnUsed(Func predicate = null) + { + return FirstColumnUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn IXLRange.FirstColumnUsed(Boolean includeFormats, Func predicate) + { + return FirstColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + IXLRangeColumn IXLRange.FirstColumnUsed(XLCellsUsedOptions options, Func predicate) + { + return FirstColumnUsed(options, predicate); + } + + internal XLRangeColumn FirstColumnUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + { + Int32 firstColumnUsed = Worksheet.Internals.CellsCollection.FirstColumnUsed( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + options); + + return firstColumnUsed == 0 ? null : Column(firstColumnUsed - RangeAddress.FirstAddress.ColumnNumber + 1); + } + + Int32 columnCount = ColumnCount(); + for (Int32 co = 1; co <= columnCount; co++) + { + var column = Column(co); + + if (!column.IsEmpty(options) && predicate(column)) + return column; + } + return null; + } + + IXLRangeColumn IXLRange.LastColumnUsed(Func predicate) + { + return LastColumnUsed(XLCellsUsedOptions.AllContents, predicate); + } + + internal XLRangeColumn LastColumnUsed(Func predicate = null) + { + return LastColumnUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeColumn IXLRange.LastColumnUsed(Boolean includeFormats, Func predicate) + { + return LastColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + IXLRangeColumn IXLRange.LastColumnUsed(XLCellsUsedOptions options, Func predicate) + { + return LastColumnUsed(options, predicate); + } + + internal XLRangeColumn LastColumnUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + { + Int32 lastColumnUsed = Worksheet.Internals.CellsCollection.LastColumnUsed( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + options); + + return lastColumnUsed == 0 ? null : Column(lastColumnUsed - RangeAddress.FirstAddress.ColumnNumber + 1); + } + + Int32 columnCount = ColumnCount(); + for (Int32 co = columnCount; co >= 1; co--) + { + var column = Column(co); + + if (!column.IsEmpty(options) && predicate(column)) + return column; + } + return null; + } + + IXLRangeRow IXLRange.FirstRow(Func predicate) + { + return FirstRow(predicate); + } + + public XLRangeRow FirstRow(Func predicate = null) + { + if (predicate == null) + return Row(1); + + Int32 rowCount = RowCount(); + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = Row(ro); + if (predicate(row)) return row; + } + + return null; + } + + IXLRangeRow IXLRange.LastRow(Func predicate) + { + return LastRow(predicate); + } + + public XLRangeRow LastRow(Func predicate = null) + { + Int32 rowCount = RowCount(); + if (predicate == null) + return Row(rowCount); + + for (Int32 ro = rowCount; ro >= 1; ro--) + { + var row = Row(ro); + if (predicate(row)) return row; + } + + return null; + } + + IXLRangeRow IXLRange.FirstRowUsed(Func predicate) + { + return FirstRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + internal XLRangeRow FirstRowUsed(Func predicate = null) + { + return FirstRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow IXLRange.FirstRowUsed(Boolean includeFormats, Func predicate) + { + return FirstRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, predicate); + } + + IXLRangeRow IXLRange.FirstRowUsed(XLCellsUsedOptions options, Func predicate) + { + return FirstRowUsed(options, predicate); + } + + internal XLRangeRow FirstRowUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + { + Int32 rowFromCells = Worksheet.Internals.CellsCollection.FirstRowUsed( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + options); + + //Int32 rowFromRows = Worksheet.Internals.RowsCollection.FirstRowUsed(includeFormats); + + return rowFromCells == 0 ? null : Row(rowFromCells - RangeAddress.FirstAddress.RowNumber + 1); + } + + Int32 rowCount = RowCount(); + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = Row(ro); + + if (!row.IsEmpty(options) && predicate(row)) + return row; + } + return null; + } + + IXLRangeRow IXLRange.LastRowUsed(Func predicate) + { + return LastRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + internal XLRangeRow LastRowUsed(Func predicate = null) + { + return LastRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow IXLRange.LastRowUsed(Boolean includeFormats, Func predicate) + { + return LastRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, predicate); + } + + IXLRangeRow IXLRange.LastRowUsed(XLCellsUsedOptions options, Func predicate) + { + return LastRowUsed(options, predicate); + } + + internal XLRangeRow LastRowUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + { + Int32 lastRowUsed = Worksheet.Internals.CellsCollection.LastRowUsed( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + options); + + return lastRowUsed == 0 ? null : Row(lastRowUsed - RangeAddress.FirstAddress.RowNumber + 1); + } + + Int32 rowCount = RowCount(); + for (Int32 ro = rowCount; ro >= 1; ro--) + { + var row = Row(ro); + + if (!row.IsEmpty(options) && predicate(row)) + return row; + } + return null; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRows IXLRange.RowsUsed(Boolean includeFormats, Func predicate) + { + return RowsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, predicate); + } + + IXLRangeRows IXLRange.RowsUsed(XLCellsUsedOptions options, Func predicate) + { + return RowsUsed(options, predicate); + } + + internal XLRangeRows RowsUsed(XLCellsUsedOptions options, Func predicate = null) + { + XLRangeRows rows = new XLRangeRows(); + Int32 rowCount = RowCount(); + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = Row(ro); + + if (!row.IsEmpty(options) && (predicate == null || predicate(row))) + rows.Add(row); + } + return rows; + } + + IXLRangeRows IXLRange.RowsUsed(Func predicate) + { + return RowsUsed(predicate); + } + + internal XLRangeRows RowsUsed(Func predicate = null) + { + return RowsUsed(XLCellsUsedOptions.AllContents, predicate); + } + + IXLRangeColumns IXLRange.ColumnsUsed(Boolean includeFormats, Func predicate) + { + return ColumnsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, predicate); + } + + IXLRangeColumns IXLRange.ColumnsUsed(XLCellsUsedOptions options, Func predicate) + { + return ColumnsUsed(options, predicate); + } + + internal virtual XLRangeColumns ColumnsUsed(XLCellsUsedOptions options, Func predicate = null) + { + XLRangeColumns columns = new XLRangeColumns(); + Int32 columnCount = ColumnCount(); + for (Int32 co = 1; co <= columnCount; co++) + { + var column = Column(co); + + if (!column.IsEmpty(options) && (predicate == null || predicate(column))) + columns.Add(column); + } + return columns; + } + + IXLRangeColumns IXLRange.ColumnsUsed(Func predicate) + { + return ColumnsUsed(predicate); + } + + internal virtual XLRangeColumns ColumnsUsed(Func predicate = null) + { + return ColumnsUsed(XLCellsUsedOptions.AllContents, predicate); + } + + public XLRangeRow Row(Int32 row) + { + if (row <= 0 || row > XLHelper.MaxRowNumber + RangeAddress.FirstAddress.RowNumber - 1) + throw new ArgumentOutOfRangeException(nameof(row), String.Format("Row number must be between 1 and {0}", XLHelper.MaxRowNumber + RangeAddress.FirstAddress.RowNumber - 1)); + + var firstCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber + row - 1, + RangeAddress.FirstAddress.ColumnNumber, + false, + false); + + var lastCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber + row - 1, + RangeAddress.LastAddress.ColumnNumber, + false, + false); + return Worksheet.RangeRow(new XLRangeAddress(firstCellAddress, lastCellAddress)); + } + + public virtual XLRangeColumn Column(Int32 columnNumber) + { + if (columnNumber <= 0 || columnNumber > XLHelper.MaxColumnNumber + RangeAddress.FirstAddress.ColumnNumber - 1) + throw new ArgumentOutOfRangeException(nameof(columnNumber), String.Format("Column number must be between 1 and {0}", XLHelper.MaxColumnNumber + RangeAddress.FirstAddress.ColumnNumber - 1)); + + var firstCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber + columnNumber - 1, + false, + false); + var lastCellAddress = new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber + columnNumber - 1, + false, + false); + return Worksheet.RangeColumn(new XLRangeAddress(firstCellAddress, lastCellAddress)); + } + + public virtual XLRangeColumn Column(String columnLetter) + { + return Column(XLHelper.GetColumnNumberFromLetter(columnLetter)); + } + + internal IEnumerable Split(IXLRangeAddress anotherRange, bool includeIntersection) + { + if (!RangeAddress.Intersects(anotherRange)) + { + yield return this; + yield break; + } + + var thisRow1 = RangeAddress.FirstAddress.RowNumber; + var thisRow2 = RangeAddress.LastAddress.RowNumber; + var thisColumn1 = RangeAddress.FirstAddress.ColumnNumber; + var thisColumn2 = RangeAddress.LastAddress.ColumnNumber; + + var otherRow1 = Math.Min(Math.Max(thisRow1, anotherRange.FirstAddress.RowNumber), thisRow2 + 1); + var otherRow2 = Math.Max(Math.Min(thisRow2, anotherRange.LastAddress.RowNumber), thisRow1 - 1); + var otherColumn1 = Math.Min(Math.Max(thisColumn1, anotherRange.FirstAddress.ColumnNumber), thisColumn2 + 1); + var otherColumn2 = Math.Max(Math.Min(thisColumn2, anotherRange.LastAddress.ColumnNumber), thisColumn1 - 1); + + var candidates = new[] + { + // to the top of the intersection + new XLRangeAddress( + new XLAddress(thisRow1,thisColumn1, false, false), + new XLAddress(otherRow1 - 1, thisColumn2, false, false)), + + // to the left of the intersection + new XLRangeAddress( + new XLAddress(otherRow1,thisColumn1, false, false), + new XLAddress(otherRow2, otherColumn1 - 1, false, false)), + + includeIntersection + ? new XLRangeAddress( + new XLAddress(otherRow1, otherColumn1, false, false), + new XLAddress(otherRow2, otherColumn2, false, false)) + : XLRangeAddress.Invalid, + + // to the right of the intersection + new XLRangeAddress( + new XLAddress(otherRow1,otherColumn2 + 1, false, false), + new XLAddress(otherRow2, thisColumn2, false, false)), + + // to the bottom of the intersection + new XLRangeAddress( + new XLAddress(otherRow2 + 1,thisColumn1, false, false), + new XLAddress(thisRow2, thisColumn2, false, false)), + }; + + foreach (var rangeAddress in candidates.Where(c => c.IsValid && c.IsNormalized)) + { + yield return Worksheet.Range(rangeAddress); + } + } + + private void TransposeRange(int squareSide) + { + var cellsToInsert = new Dictionary(); + var cellsToDelete = new List(); + var rngToTranspose = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.RowNumber + squareSide - 1, + RangeAddress.FirstAddress.ColumnNumber + squareSide - 1); + + Int32 roCount = rngToTranspose.RowCount(); + Int32 coCount = rngToTranspose.ColumnCount(); + for (Int32 ro = 1; ro <= roCount; ro++) + { + for (Int32 co = 1; co <= coCount; co++) + { + var oldCell = rngToTranspose.Cell(ro, co); + var newKey = rngToTranspose.Cell(co, ro).Address; + // new XLAddress(Worksheet, c.Address.ColumnNumber, c.Address.RowNumber); + var newCell = new XLCell(Worksheet, newKey, oldCell.StyleValue); + newCell.CopyFrom(oldCell, XLCellCopyOptions.All); + cellsToInsert.Add(new XLSheetPoint(newKey.RowNumber, newKey.ColumnNumber), newCell); + cellsToDelete.Add(new XLSheetPoint(oldCell.Address.RowNumber, oldCell.Address.ColumnNumber)); + } + } + + cellsToDelete.ForEach(c => Worksheet.Internals.CellsCollection.Remove(c)); + cellsToInsert.ForEach(c => Worksheet.Internals.CellsCollection.Add(c.Key, c.Value)); + } + + private void TransposeMerged(Int32 squareSide) + { + var rngToTranspose = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.RowNumber + squareSide - 1, + RangeAddress.FirstAddress.ColumnNumber + squareSide - 1); + + foreach (var merge in Worksheet.Internals.MergedRanges.Where(Contains).Cast()) + { + merge.RangeAddress = new XLRangeAddress( + merge.RangeAddress.FirstAddress, + rngToTranspose.Cell(merge.ColumnCount(), merge.RowCount()).Address); + } + } + + private void MoveOrClearForTranspose(XLTransposeOptions transposeOption, int rowCount, int columnCount) + { + if (transposeOption == XLTransposeOptions.MoveCells) + { + if (rowCount > columnCount) + InsertColumnsAfter(false, rowCount - columnCount, false); + else if (columnCount > rowCount) + InsertRowsBelow(false, columnCount - rowCount, false); + } + else + { + if (rowCount > columnCount) + { + int toMove = rowCount - columnCount; + var rngToClear = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + 1, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + toMove); + rngToClear.Clear(); + } + else if (columnCount > rowCount) + { + int toMove = columnCount - rowCount; + var rngToClear = Worksheet.Range( + RangeAddress.LastAddress.RowNumber + 1, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber + toMove, + RangeAddress.LastAddress.ColumnNumber); + rngToClear.Clear(); + } + } + } + + public override bool Equals(object obj) + { + var other = obj as XLRange; + if (other == null) + return false; + return RangeAddress.Equals(other.RangeAddress) + && Worksheet.Equals(other.Worksheet); + } + + public override int GetHashCode() + { + return RangeAddress.GetHashCode() + ^ Worksheet.GetHashCode(); + } + + public new IXLRange Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + public IXLRangeColumn FindColumn(Func predicate) + { + Int32 columnCount = ColumnCount(); + for (Int32 c = 1; c <= columnCount; c++) + { + var column = Column(c); + if (predicate == null || predicate(column)) + return column; + } + return null; + } + + public IXLRangeRow FindRow(Func predicate) + { + Int32 rowCount = RowCount(); + for (Int32 r = 1; r <= rowCount; r++) + { + var row = Row(r); + if (predicate(row)) + return row; + } + return null; + } + + public override string ToString() + { + if (IsEntireSheet()) + { + return Worksheet.Name; + } + else if (IsEntireRow()) + { + return String.Concat( + Worksheet.Name.EscapeSheetName(), + '!', + RangeAddress.FirstAddress.RowNumber, + ':', + RangeAddress.LastAddress.RowNumber); + } + else if (IsEntireColumn()) + { + return String.Concat( + Worksheet.Name.EscapeSheetName(), + '!', + RangeAddress.FirstAddress.ColumnLetter, + ':', + RangeAddress.LastAddress.ColumnLetter); + } + else + return base.ToString(); + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeAddress.cs b/ClosedXML/Excel/Ranges/XLRangeAddress.cs new file mode 100644 index 0000000..a6a6acd --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeAddress.cs @@ -0,0 +1,491 @@ +using ClosedXML.Extensions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal struct XLRangeAddress : IXLRangeAddress, IEquatable + { + #region Static members + + public static XLRangeAddress EntireColumn(XLWorksheet worksheet, int column) + { + return new XLRangeAddress( + new XLAddress(worksheet, 1, column, false, false), + new XLAddress(worksheet, XLHelper.MaxRowNumber, column, false, false)); + } + + public static XLRangeAddress EntireRow(XLWorksheet worksheet, int row) + { + return new XLRangeAddress( + new XLAddress(worksheet, row, 1, false, false), + new XLAddress(worksheet, row, XLHelper.MaxColumnNumber, false, false)); + } + + public static readonly XLRangeAddress Invalid = new XLRangeAddress( + new XLAddress(-1, -1, fixedRow: true, fixedColumn: true), + new XLAddress(-1, -1, fixedRow: true, fixedColumn: true) + ); + + #endregion Static members + + #region Constructor + + public XLRangeAddress(XLAddress firstAddress, XLAddress lastAddress) : this() + { + Worksheet = firstAddress.Worksheet; + FirstAddress = firstAddress; + LastAddress = lastAddress; + } + + public XLRangeAddress(XLWorksheet worksheet, String rangeAddress) : this() + { + string addressToUse = rangeAddress.Contains("!") + ? rangeAddress.Substring(rangeAddress.IndexOf("!") + 1) + : rangeAddress; + + string firstPart; + string secondPart; + if (addressToUse.Contains(':')) + { + var arrRange = addressToUse.Split(':'); + firstPart = arrRange[0]; + secondPart = arrRange[1]; + } + else + { + firstPart = addressToUse; + secondPart = addressToUse; + } + + if (XLHelper.IsValidA1Address(firstPart)) + { + FirstAddress = XLAddress.Create(worksheet, firstPart); + LastAddress = XLAddress.Create(worksheet, secondPart); + } + else + { + firstPart = firstPart.Replace("$", String.Empty); + secondPart = secondPart.Replace("$", String.Empty); + if (char.IsDigit(firstPart[0])) + { + FirstAddress = XLAddress.Create(worksheet, "A" + firstPart); + LastAddress = XLAddress.Create(worksheet, XLHelper.MaxColumnLetter + secondPart); + } + else + { + FirstAddress = XLAddress.Create(worksheet, firstPart + "1"); + LastAddress = XLAddress.Create(worksheet, secondPart + XLHelper.MaxRowNumber.ToInvariantString()); + } + } + + Worksheet = worksheet; + } + + #endregion Constructor + + #region Public properties + + public XLWorksheet Worksheet { get; } + + public XLAddress FirstAddress { get; } + + public XLAddress LastAddress { get; } + + IXLWorksheet IXLRangeAddress.Worksheet + { + get { return Worksheet; } + } + + IXLAddress IXLRangeAddress.FirstAddress + { + [DebuggerStepThrough] + get { return FirstAddress; } + } + + IXLAddress IXLRangeAddress.LastAddress + { + [DebuggerStepThrough] + get { return LastAddress; } + } + + public bool IsValid => FirstAddress.IsValid && LastAddress.IsValid; + + public int ColumnSpan + { + get + { + if (!IsValid) + throw new InvalidOperationException("Range address is invalid."); + + return Math.Abs(LastAddress.ColumnNumber - FirstAddress.ColumnNumber) + 1; + } + } + + public int NumberOfCells => ColumnSpan * RowSpan; + + public int RowSpan + { + get + { + if (!IsValid) + throw new InvalidOperationException("Range address is invalid."); + + return Math.Abs(LastAddress.RowNumber - FirstAddress.RowNumber) + 1; + } + } + + private bool WorksheetIsDeleted => Worksheet?.IsDeleted == true; + + #endregion Public properties + + #region Public methods + + public Boolean IsNormalized => LastAddress.RowNumber >= FirstAddress.RowNumber + && LastAddress.ColumnNumber >= FirstAddress.ColumnNumber; + + /// + /// Lead a range address to a normal form - when points to the top-left address and + /// points to the bottom-right address. + /// + /// + public XLRangeAddress Normalize() + { + if (FirstAddress.RowNumber <= LastAddress.RowNumber && + FirstAddress.ColumnNumber <= LastAddress.ColumnNumber) + return this; + + int firstRow, firstColumn, lastRow, lastColumn; + bool firstRowFixed, firstColumnFixed, lastRowFixed, lastColumnFixed; + + if (FirstAddress.RowNumber <= LastAddress.RowNumber) + { + firstRow = FirstAddress.RowNumber; + firstRowFixed = FirstAddress.FixedRow; + lastRow = LastAddress.RowNumber; + lastRowFixed = LastAddress.FixedRow; + } + else + { + firstRow = LastAddress.RowNumber; + firstRowFixed = LastAddress.FixedRow; + lastRow = FirstAddress.RowNumber; + lastRowFixed = FirstAddress.FixedRow; + } + + if (FirstAddress.ColumnNumber <= LastAddress.ColumnNumber) + { + firstColumn = FirstAddress.ColumnNumber; + firstColumnFixed = FirstAddress.FixedColumn; + lastColumn = LastAddress.ColumnNumber; + lastColumnFixed = LastAddress.FixedColumn; + } + else + { + firstColumn = LastAddress.ColumnNumber; + firstColumnFixed = LastAddress.FixedColumn; + lastColumn = FirstAddress.ColumnNumber; + lastColumnFixed = FirstAddress.FixedColumn; + } + + return new XLRangeAddress( + new XLAddress(FirstAddress.Worksheet, firstRow, firstColumn, firstRowFixed, firstColumnFixed), + new XLAddress(LastAddress.Worksheet, lastRow, lastColumn, lastRowFixed, lastColumnFixed)); + } + + public bool Intersects(IXLRangeAddress otherAddress) + { + var xlOtherAddress = (XLRangeAddress)otherAddress; + return Intersects(in xlOtherAddress); + } + + internal bool Intersects(in XLRangeAddress otherAddress) + { + return !( // See if the two ranges intersect... + otherAddress.FirstAddress.ColumnNumber > LastAddress.ColumnNumber + || otherAddress.LastAddress.ColumnNumber < FirstAddress.ColumnNumber + || otherAddress.FirstAddress.RowNumber > LastAddress.RowNumber + || otherAddress.LastAddress.RowNumber < FirstAddress.RowNumber + ); + } + + public bool Contains(IXLAddress address) + { + var xlAddress = (XLAddress)address; + return Contains(in xlAddress); + } + + internal IXLRangeAddress WithoutWorksheet() + { + return new XLRangeAddress( + FirstAddress.WithoutWorksheet(), + LastAddress.WithoutWorksheet()); + } + + internal bool Contains(in XLAddress address) + { + return FirstAddress.RowNumber <= address.RowNumber && + address.RowNumber <= LastAddress.RowNumber && + FirstAddress.ColumnNumber <= address.ColumnNumber && + address.ColumnNumber <= LastAddress.ColumnNumber; + } + + public String ToStringRelative() + { + return ToStringRelative(false); + } + + public String ToStringFixed() + { + return ToStringFixed(XLReferenceStyle.A1); + } + + public String ToStringRelative(Boolean includeSheet) + { + string address; + if (!IsValid) + address = "#REF!"; + else + { + if (IsEntireSheet()) + address = $"1:{XLHelper.MaxRowNumber}"; + else if (IsEntireRow()) + address = String.Concat(FirstAddress.RowNumber.ToString(), ":", LastAddress.RowNumber.ToString()); + else if (IsEntireColumn()) + address = String.Concat(FirstAddress.ColumnLetter, ":", LastAddress.ColumnLetter); + else + address = String.Concat(FirstAddress.ToStringRelative(), ":", LastAddress.ToStringRelative()); + } + + if (includeSheet || WorksheetIsDeleted) + return String.Concat( + WorksheetIsDeleted ? "#REF" : Worksheet.Name.EscapeSheetName(), + "!", address); + + return address; + } + + public String ToStringFixed(XLReferenceStyle referenceStyle) + { + return ToStringFixed(referenceStyle, false); + } + + public String ToStringFixed(XLReferenceStyle referenceStyle, Boolean includeSheet) + { + string address; + if (!IsValid) + address = "#REF!"; + else + { + if (IsEntireSheet()) + address = $"$1:${XLHelper.MaxRowNumber}"; + else if (IsEntireRow()) + address = String.Concat("$", FirstAddress.RowNumber.ToString(), ":$", LastAddress.RowNumber.ToString()); + else if (IsEntireColumn()) + address = String.Concat("$", FirstAddress.ColumnLetter, ":$", LastAddress.ColumnLetter); + else + address = String.Concat(FirstAddress.ToStringFixed(referenceStyle), ":", LastAddress.ToStringFixed(referenceStyle)); + } + + if (includeSheet || WorksheetIsDeleted) + return String.Concat( + WorksheetIsDeleted ? "#REF" : Worksheet.Name.EscapeSheetName(), + "!", address); + + return address; + } + + public override string ToString() + { + if (!IsValid || WorksheetIsDeleted) + { + var worksheet = WorksheetIsDeleted ? "#REF!" : ""; + + var address = (!FirstAddress.IsValid || !LastAddress.IsValid) ? "#REF!" : String.Concat(FirstAddress.ToString(), ":", LastAddress.ToString()); + return String.Concat(worksheet, address); + } + + if (IsEntireSheet()) + { + var worksheet = WorksheetIsDeleted ? "#REF!" : ""; + var address = $"1:{XLHelper.MaxRowNumber}"; + return String.Concat(worksheet, address); + } + else if (IsEntireRow()) + { + var worksheet = WorksheetIsDeleted ? "#REF!" : ""; + var firstAddress = FirstAddress.IsValid ? FirstAddress.RowNumber.ToString() : "#REF!"; + var lastAddress = LastAddress.IsValid ? LastAddress.RowNumber.ToString() : "#REF!"; + + return String.Concat(worksheet, firstAddress, ':', lastAddress); + } + else if (IsEntireColumn()) + { + var worksheet = WorksheetIsDeleted ? "#REF!" : ""; + var firstAddress = FirstAddress.IsValid ? FirstAddress.ColumnLetter : "#REF!"; + var lastAddress = LastAddress.IsValid ? LastAddress.ColumnLetter : "#REF!"; + + return String.Concat(worksheet, firstAddress, ':', lastAddress); + } + else + { + return String.Concat(FirstAddress.ToString(), ":", LastAddress.ToString()); + } + } + + public string ToString(XLReferenceStyle referenceStyle) + { + return ToString(referenceStyle, false); + } + + public string ToString(XLReferenceStyle referenceStyle, bool includeSheet) + { + if (referenceStyle == XLReferenceStyle.R1C1) + return ToStringFixed(referenceStyle, true); + else + return ToStringRelative(includeSheet); + } + + public override bool Equals(object obj) + { + if (!(obj is XLRangeAddress)) + { + return false; + } + + var address = (XLRangeAddress)obj; + return FirstAddress.Equals(address.FirstAddress) && + LastAddress.Equals(address.LastAddress) && + EqualityComparer.Default.Equals(Worksheet, address.Worksheet); + } + + public override int GetHashCode() + { + var hashCode = -778064135; + hashCode = hashCode * -1521134295 + FirstAddress.GetHashCode(); + hashCode = hashCode * -1521134295 + LastAddress.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Worksheet); + return hashCode; + } + + public bool Equals(XLRangeAddress other) + { + return ReferenceEquals(Worksheet, other.Worksheet) && + FirstAddress == other.FirstAddress && + LastAddress == other.LastAddress; + } + + public bool IsEntireColumn() + { + return IsValid + && FirstAddress.RowNumber == 1 + && LastAddress.RowNumber == XLHelper.MaxRowNumber; + } + + public bool IsEntireRow() + { + return IsValid + && FirstAddress.ColumnNumber == 1 + && LastAddress.ColumnNumber == XLHelper.MaxColumnNumber; + } + + public bool IsEntireSheet() + { + return IsValid && IsEntireColumn() && IsEntireRow(); + } + + public IXLRangeAddress Relative(IXLRangeAddress sourceRangeAddress, IXLRangeAddress targetRangeAddress) + { + var xlSourceRangeAddress = (XLRangeAddress)sourceRangeAddress; + var xlTargetRangeAddress = (XLRangeAddress)targetRangeAddress; + + return Relative(in xlSourceRangeAddress, in xlTargetRangeAddress); + } + + internal XLRangeAddress Relative(in XLRangeAddress sourceRangeAddress, in XLRangeAddress targetRangeAddress) + { + var sheet = targetRangeAddress.Worksheet; + + return new XLRangeAddress + ( + new XLAddress + ( + sheet, + this.FirstAddress.RowNumber - sourceRangeAddress.FirstAddress.RowNumber + targetRangeAddress.FirstAddress.RowNumber, + this.FirstAddress.ColumnNumber - sourceRangeAddress.FirstAddress.ColumnNumber + targetRangeAddress.FirstAddress.ColumnNumber, + fixedRow: false, + fixedColumn: false + ), + new XLAddress + ( + sheet, + this.LastAddress.RowNumber - sourceRangeAddress.FirstAddress.RowNumber + targetRangeAddress.FirstAddress.RowNumber, + this.LastAddress.ColumnNumber - sourceRangeAddress.FirstAddress.ColumnNumber + targetRangeAddress.FirstAddress.ColumnNumber, + fixedRow: false, + fixedColumn: false + ) + ); + } + + public IXLRangeAddress Intersection(IXLRangeAddress otherRangeAddress) + { + if (otherRangeAddress == null) + throw new ArgumentNullException(nameof(otherRangeAddress)); + + var xlOtherRangeAddress = (XLRangeAddress)otherRangeAddress; + return Intersection(in xlOtherRangeAddress); + } + + internal XLRangeAddress Intersection(in XLRangeAddress otherRangeAddress) + { + if (!this.Worksheet.Equals(otherRangeAddress.Worksheet)) + throw new ArgumentOutOfRangeException(nameof(otherRangeAddress), "The other range address is on a different worksheet"); + + var thisRangeAddressNormalized = this.Normalize(); + var otherRangeAddressNormalized = otherRangeAddress.Normalize(); + + var firstRow = Math.Max(thisRangeAddressNormalized.FirstAddress.RowNumber, otherRangeAddressNormalized.FirstAddress.RowNumber); + var firstColumn = Math.Max(thisRangeAddressNormalized.FirstAddress.ColumnNumber, otherRangeAddressNormalized.FirstAddress.ColumnNumber); + var lastRow = Math.Min(thisRangeAddressNormalized.LastAddress.RowNumber, otherRangeAddressNormalized.LastAddress.RowNumber); + var lastColumn = Math.Min(thisRangeAddressNormalized.LastAddress.ColumnNumber, otherRangeAddressNormalized.LastAddress.ColumnNumber); + + if (lastRow < firstRow || lastColumn < firstColumn) + return XLRangeAddress.Invalid; + + return new XLRangeAddress + ( + new XLAddress(this.Worksheet, firstRow, firstColumn, fixedRow: false, fixedColumn: false), + new XLAddress(this.Worksheet, lastRow, lastColumn, fixedRow: false, fixedColumn: false) + ); + } + + public IXLRange AsRange() + { + if (this.Worksheet == null) + throw new InvalidOperationException("The worksheet of the current range address has not been set."); + + if (!this.IsValid) + return null; + + return this.Worksheet.Range(this); + } + + #endregion Public methods + + #region Operators + + public static bool operator ==(XLRangeAddress left, XLRangeAddress right) + { + return left.Equals(right); + } + + public static bool operator !=(XLRangeAddress left, XLRangeAddress right) + { + return !(left == right); + } + + #endregion Operators + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeBase.cs b/ClosedXML/Excel/Ranges/XLRangeBase.cs new file mode 100644 index 0000000..47a03c4 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeBase.cs @@ -0,0 +1,2300 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal abstract class XLRangeBase : XLStylizedBase, IXLRangeBase, IXLStylized + { + #region Fields + + private XLSortElements _sortRows; + private XLSortElements _sortColumns; + + #endregion Fields + + protected IXLStyle GetStyle() + { + return Style; + } + + #region Constructor + + private static Int32 IdCounter = 0; + private readonly Int32 Id; + + protected XLRangeBase(XLRangeAddress rangeAddress, XLStyleValue styleValue) + : base(styleValue) + { + Id = ++IdCounter; + + _rangeAddress = rangeAddress; + } + + #endregion Constructor + + protected virtual void OnRangeAddressChanged(XLRangeAddress oldAddress, XLRangeAddress newAddress) + { + Worksheet.RellocateRange(RangeType, oldAddress, newAddress); + } + + #region Public properties + + private XLRangeAddress _rangeAddress; + + public XLRangeAddress RangeAddress + { + get { return _rangeAddress; } + protected set + { + if (_rangeAddress != value) + { + var oldAddress = _rangeAddress; + _rangeAddress = value; + OnRangeAddressChanged(oldAddress, _rangeAddress); + } + } + } + + public XLWorksheet Worksheet + { + get { return RangeAddress.Worksheet; } + } + + public IXLDataValidation NewDataValidation + { + get + { + var newRange = AsRange(); + var dataValidation = new XLDataValidation(newRange); + Worksheet.DataValidations.Add(dataValidation); + return dataValidation; + } + } + + /// + /// Get the data validation rule containing current range or create a new one if no rule was defined for range. + /// + public IXLDataValidation DataValidation + { + get + { + return SetDataValidation(); + } + } + + private IXLDataValidation GetDataValidation() + { + Worksheet.DataValidations.TryGet(RangeAddress, out var existingDataValidation); + return existingDataValidation; + } + + #region IXLRangeBase Members + + IXLRangeAddress IXLAddressable.RangeAddress + { + get { return RangeAddress; } + } + + IXLWorksheet IXLRangeBase.Worksheet + { + get { return RangeAddress.Worksheet; } + } + + public String FormulaA1 + { + set + { + Cells().ForEach(c => + { + c.FormulaA1 = value; + c.FormulaReference = RangeAddress; + }); + } + } + + public String FormulaR1C1 + { + set + { + Cells().ForEach(c => + { + c.FormulaR1C1 = value; + c.FormulaReference = RangeAddress; + }); + } + } + + public Boolean ShareString + { + set { Cells().ForEach(c => c.ShareString = value); } + } + + public IXLHyperlinks Hyperlinks + { + get + { + var hyperlinks = new XLHyperlinks(); + var hls = from hl in Worksheet.Hyperlinks + where Contains(hl.Cell.AsRange()) + select hl; + hls.ForEach(hyperlinks.Add); + return hyperlinks; + } + } + + public Object Value + { + set { Cells().ForEach(c => c.Value = value); } + } + + public XLDataType DataType + { + set { Cells().ForEach(c => c.DataType = value); } + } + + #endregion IXLRangeBase Members + + #region IXLStylized Members + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges { AsRange() }; + return retVal; + } + } + + protected override IEnumerable Children + { + get + { + foreach (var cell in Cells().OfType()) + yield return cell; + } + } + + public override IEnumerable Styles + { + get + { + foreach (IXLCell cell in Cells()) + yield return cell.Style; + } + } + + #endregion IXLStylized Members + + #endregion Public properties + + #region IXLRangeBase Members + + IXLCell IXLRangeBase.FirstCell() + { + return FirstCell(); + } + + IXLCell IXLRangeBase.LastCell() + { + return LastCell(); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.FirstCellUsed() + { + return FirstCellUsed(false); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.FirstCellUsed(bool includeFormats) + { + return FirstCellUsed(includeFormats); + } + + IXLCell IXLRangeBase.FirstCellUsed(XLCellsUsedOptions options) + { + return FirstCellUsed(options, null); + } + + IXLCell IXLRangeBase.FirstCellUsed(Func predicate) + { + return FirstCellUsed(predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.FirstCellUsed(Boolean includeFormats, Func predicate) + { + return FirstCellUsed(includeFormats, predicate); + } + + IXLCell IXLRangeBase.FirstCellUsed(XLCellsUsedOptions options, Func predicate) + { + return FirstCellUsed(options, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.LastCellUsed() + { + return LastCellUsed(false); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.LastCellUsed(bool includeFormats) + { + return LastCellUsed(includeFormats); + } + + IXLCell IXLRangeBase.LastCellUsed(XLCellsUsedOptions options) + { + return LastCellUsed(options, null); + } + + IXLCell IXLRangeBase.LastCellUsed(Func predicate) + { + return LastCellUsed(predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCell IXLRangeBase.LastCellUsed(Boolean includeFormats, Func predicate) + { + return LastCellUsed(includeFormats, predicate); + } + + IXLCell IXLRangeBase.LastCellUsed(XLCellsUsedOptions options, Func predicate) + { + return LastCellUsed(options, predicate); + } + + public virtual IXLCells Cells() + { + return Cells(false); + } + + public virtual IXLCells Cells(Boolean usedCellsOnly) + { + return Cells(usedCellsOnly, XLCellsUsedOptions.AllContents); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells Cells(Boolean usedCellsOnly, Boolean includeFormats) + { + return Cells(usedCellsOnly, includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents + ); + } + + public IXLCells Cells(Boolean usedCellsOnly, XLCellsUsedOptions options) + { + var cells = new XLCells(usedCellsOnly, options) { RangeAddress }; + return cells; + } + + public virtual IXLCells Cells(String cells) + { + return Ranges(cells).Cells(); + } + + public IXLCells Cells(Func predicate) + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents, predicate) { RangeAddress }; + return cells; + } + + public IXLCells CellsUsed() + { + return Cells(true); + } + + /// + /// Return the collection of cell values not initializing empty cells. + /// + public IEnumerable CellValues() + { + for (int ro = RangeAddress.FirstAddress.RowNumber; ro <= RangeAddress.LastAddress.RowNumber; ro++) + { + for (int co = RangeAddress.FirstAddress.ColumnNumber; co <= RangeAddress.LastAddress.ColumnNumber; co++) + { + yield return Worksheet.GetCellValue(ro, co); + } + } + } + + public IXLRange Merge() + { + return Merge(true); + } + + public IXLRange Merge(Boolean checkIntersect) + { + if (RangeAddress.FirstAddress.Equals(RangeAddress.LastAddress)) + return Worksheet.Range(RangeAddress); + + var asRange = AsRange(); + + if (checkIntersect) + { + var intersectedMergedRanges = + Worksheet.Internals.MergedRanges.GetIntersectedRanges(RangeAddress).ToList(); + foreach (var intersectedMergedRange in intersectedMergedRanges) + { + Worksheet.Internals.MergedRanges.Remove(intersectedMergedRange); + } + + var firstCell = FirstCell(); + var firstCellStyleKey = (firstCell.Style as XLStyle).Key; + var firstCellStyle = firstCell.Style; + var defaultStyleKey = XLStyle.Default.Key; + var cellsUsed = + CellsUsed(XLCellsUsedOptions.All & ~XLCellsUsedOptions.MergedRanges, c => c != firstCell).ToList(); + cellsUsed.ForEach(c => c.Clear(XLClearOptions.All + & ~XLClearOptions.MergedRanges + & ~XLClearOptions.NormalFormats)); + + if (firstCellStyleKey.Alignment != defaultStyleKey.Alignment) + asRange.Style.Alignment = firstCellStyle.Alignment; + else + cellsUsed.ForEach(c => c.Style.Alignment = firstCellStyle.Alignment); + + if (firstCellStyleKey.Fill != defaultStyleKey.Fill) + asRange.Style.Fill = firstCellStyle.Fill; + else + cellsUsed.ForEach(c => c.Style.Fill = firstCellStyle.Fill); + + if (firstCellStyleKey.Font != defaultStyleKey.Font) + asRange.Style.Font = firstCellStyle.Font; + else + cellsUsed.ForEach(c => c.Style.Font = firstCellStyle.Font); + + if (firstCellStyleKey.IncludeQuotePrefix != defaultStyleKey.IncludeQuotePrefix) + asRange.Style.IncludeQuotePrefix = firstCellStyle.IncludeQuotePrefix; + else + cellsUsed.ForEach(c => c.Style.IncludeQuotePrefix = firstCellStyle.IncludeQuotePrefix); + + if (firstCellStyleKey.NumberFormat != defaultStyleKey.NumberFormat) + asRange.Style.NumberFormat = firstCellStyle.NumberFormat; + else + cellsUsed.ForEach(c => c.Style.NumberFormat = firstCellStyle.NumberFormat); + + if (firstCellStyleKey.Protection != defaultStyleKey.Protection) + asRange.Style.Protection = firstCellStyle.Protection; + else + cellsUsed.ForEach(c => c.Style.Protection = firstCellStyle.Protection); + + if (cellsUsed.Any(c => (c.Style as XLStyle).Key.Border != defaultStyleKey.Border)) + asRange.Style.Border.SetInsideBorder(XLBorderStyleValues.None); + } + + Worksheet.Internals.MergedRanges.Add(asRange); + return asRange; + } + + public IXLRange Unmerge() + { + string tAddress = RangeAddress.ToString(); + var asRange = AsRange(); + if (Worksheet.Internals.MergedRanges.Select(m => m.RangeAddress.ToString()).Any(mAddress => mAddress == tAddress)) + Worksheet.Internals.MergedRanges.Remove(asRange); + + return asRange; + } + + public IXLRangeBase Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + var cellClearOptions = clearOptions + & ~XLClearOptions.ConditionalFormats + & ~XLClearOptions.DataValidation + & ~XLClearOptions.MergedRanges; + var cellUsedOptions = cellClearOptions.ToCellsUsedOptions(); + foreach (var cell in CellsUsed(cellUsedOptions)) + { + // We'll clear the conditional formatting, data validations + // and merged ranges later down. + (cell as XLCell).Clear(cellClearOptions, true); + } + + if (clearOptions.HasFlag(XLClearOptions.ConditionalFormats)) + RemoveConditionalFormatting(); + + if (clearOptions.HasFlag(XLClearOptions.DataValidation)) + { + var validation = NewDataValidation; + Worksheet.DataValidations.Delete(validation); + } + + if (clearOptions.HasFlag(XLClearOptions.MergedRanges)) + ClearMerged(); + + if (clearOptions.HasFlag(XLClearOptions.Sparklines)) + RemoveSparklines(); + + if (clearOptions == XLClearOptions.All) + { + Worksheet.Internals.CellsCollection.RemoveAll( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + ); + } + return this; + } + + public IXLRangeBase Relative(IXLRangeBase sourceBaseRange, IXLRangeBase targetBaseRange) + { + var xlSourceBaseRangeAddress = (XLRangeAddress)sourceBaseRange.RangeAddress; + var xlTargetBaseRangeAddress = (XLRangeAddress)targetBaseRange.RangeAddress; + var xlRangeAddress = this.RangeAddress.Relative(in xlSourceBaseRangeAddress, in xlTargetBaseRangeAddress); + + return ((XLRangeBase)targetBaseRange).Range(in xlRangeAddress); + } + + internal void RemoveConditionalFormatting() + { + var mf = RangeAddress.FirstAddress; + var ml = RangeAddress.LastAddress; + foreach (var format in Worksheet.ConditionalFormats.Where(x => x.Ranges.GetIntersectedRanges(RangeAddress).Any()).ToList()) + { + var cfRanges = format.Ranges.ToList(); + format.Ranges.RemoveAll(); + + foreach (var cfRange in cfRanges) + { + if (!cfRange.Intersects(this)) + { + format.Ranges.Add(cfRange); + continue; + } + + var f = cfRange.RangeAddress.FirstAddress; + var l = cfRange.RangeAddress.LastAddress; + bool byWidth = false, byHeight = false; + XLRange rng1 = null, rng2 = null; + if (mf.ColumnNumber <= f.ColumnNumber && ml.ColumnNumber >= l.ColumnNumber) + { + if (mf.RowNumber.Between(f.RowNumber, l.RowNumber) || ml.RowNumber.Between(f.RowNumber, l.RowNumber)) + { + if (mf.RowNumber > f.RowNumber) + rng1 = Worksheet.Range(f.RowNumber, f.ColumnNumber, mf.RowNumber - 1, l.ColumnNumber); + if (ml.RowNumber < l.RowNumber) + rng2 = Worksheet.Range(ml.RowNumber + 1, f.ColumnNumber, l.RowNumber, l.ColumnNumber); + } + byWidth = true; + } + + if (mf.RowNumber <= f.RowNumber && ml.RowNumber >= l.RowNumber) + { + if (mf.ColumnNumber.Between(f.ColumnNumber, l.ColumnNumber) || ml.ColumnNumber.Between(f.ColumnNumber, l.ColumnNumber)) + { + if (mf.ColumnNumber > f.ColumnNumber) + rng1 = Worksheet.Range(f.RowNumber, f.ColumnNumber, l.RowNumber, mf.ColumnNumber - 1); + if (ml.ColumnNumber < l.ColumnNumber) + rng2 = Worksheet.Range(f.RowNumber, ml.ColumnNumber + 1, l.RowNumber, l.ColumnNumber); + } + byHeight = true; + } + + if (rng1 != null) + { + format.Ranges.Add(rng1); + } + if (rng2 != null) + { + //TODO: reflect the formula for a new range + format.Ranges.Add(rng2); + } + + if (!byWidth && !byHeight) + format.Ranges.Add(cfRange); // Not split, preserve original + } + if (!format.Ranges.Any()) + Worksheet.ConditionalFormats.Remove(x => x == format); + } + } + + internal void RemoveSparklines() + { + Worksheet.SparklineGroups.GetSparklines(this).ToList() + .ForEach(sl => Worksheet.SparklineGroups.Remove(sl.Location)); + } + + public void DeleteComments() + { + Cells().DeleteComments(); + } + + public bool Contains(String rangeAddress) + { + string addressToUse = rangeAddress.Contains("!") + ? rangeAddress.Substring(rangeAddress.IndexOf("!") + 1) + : rangeAddress; + + XLAddress firstAddress; + XLAddress lastAddress; + if (addressToUse.Contains(':')) + { + var arrRange = addressToUse.Split(':'); + firstAddress = XLAddress.Create(Worksheet, arrRange[0]); + lastAddress = XLAddress.Create(Worksheet, arrRange[1]); + } + else + { + firstAddress = XLAddress.Create(Worksheet, addressToUse); + lastAddress = XLAddress.Create(Worksheet, addressToUse); + } + return Contains(firstAddress, lastAddress); + } + + public bool Contains(IXLRangeBase range) + { + return Contains((XLAddress)range.RangeAddress.FirstAddress, (XLAddress)range.RangeAddress.LastAddress); + } + + public bool Intersects(string rangeAddress) + { + return Intersects(Worksheet.Range(rangeAddress)); + } + + public bool Intersects(IXLRangeBase range) + { + if (!range.RangeAddress.IsValid || !RangeAddress.IsValid) + return false; + var ma = range.RangeAddress; + var ra = RangeAddress; + return ra.Intersects(ma); + } + + IXLRange IXLRangeBase.AsRange() + { + return AsRange(); + } + + public virtual XLRange AsRange() + { + return Worksheet.Range(RangeAddress); + } + + public IXLRange AddToNamed(String rangeName) + { + return AddToNamed(rangeName, XLScope.Workbook); + } + + public IXLRange AddToNamed(String rangeName, XLScope scope) + { + return AddToNamed(rangeName, scope, null); + } + + public IXLRange AddToNamed(String rangeName, XLScope scope, String comment) + { + var namedRanges = scope == XLScope.Workbook + ? Worksheet.Workbook.NamedRanges + : Worksheet.NamedRanges; + + if (namedRanges.TryGetValue(rangeName, out IXLNamedRange namedRange)) + namedRange.Add(Worksheet.Workbook, RangeAddress.ToStringFixed(XLReferenceStyle.A1, true)); + else + namedRanges.Add(rangeName, RangeAddress.ToStringFixed(XLReferenceStyle.A1, true), comment); + + return AsRange(); + } + + public IXLRangeBase SetValue(T value) + { + Cells().ForEach(c => c.SetValue(value)); + return this; + } + + public Boolean IsMerged() + { + return Cells().Any(c => c.IsMerged()); + } + + public virtual Boolean IsEmpty() + { + return !CellsUsed().Any() || CellsUsed().Any(c => c.IsEmpty()); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public virtual Boolean IsEmpty(Boolean includeFormats) + { + return IsEmpty(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public virtual Boolean IsEmpty(XLCellsUsedOptions options) + { + return CellsUsed(options).Cast().All(c => c.IsEmpty(options)); + } + + public virtual Boolean IsEntireRow() + { + return RangeAddress.IsEntireRow(); + } + + public virtual Boolean IsEntireColumn() + { + return RangeAddress.IsEntireColumn(); + } + + public Boolean IsEntireSheet() + { + return RangeAddress.IsEntireSheet(); + } + + #endregion IXLRangeBase Members + + public IXLCells Search(String searchText, CompareOptions compareOptions = CompareOptions.Ordinal, Boolean searchFormulae = false) + { + var culture = CultureInfo.CurrentCulture; + return CellsUsed(XLCellsUsedOptions.AllContents, c => + { + try + { + if (searchFormulae) + return c.HasFormula + && culture.CompareInfo.IndexOf(c.FormulaA1, searchText, compareOptions) >= 0 + || culture.CompareInfo.IndexOf(c.Value.ToString(), searchText, compareOptions) >= 0; + else + return culture.CompareInfo.IndexOf(c.GetFormattedString(), searchText, compareOptions) >= 0; + } + catch + { + return false; + } + }); + } + + internal XLCell FirstCell() + { + return Cell(1, 1); + } + + internal XLCell LastCell() + { + return Cell(RowCount(), ColumnCount()); + } + + internal XLCell FirstCellUsed() + { + return FirstCellUsed(XLCellsUsedOptions.AllContents, predicate: null); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + internal XLCell FirstCellUsed(Boolean includeFormats) + { + return FirstCellUsed(includeFormats, null); + } + + internal XLCell FirstCellUsed(Func predicate) + { + return FirstCellUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + internal XLCell FirstCellUsed(Boolean includeFormats, Func predicate) + { + return FirstCellUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + internal XLCell FirstCellUsed(XLCellsUsedOptions options, Func predicate) + { + predicate = predicate ?? (t => true); + + //To avoid unnecessary initialization of thousands cells + var opt = options + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation + & ~XLCellsUsedOptions.MergedRanges; + + IEnumerable cellsUsed = CellsUsed(opt, predicate); + + if (options.HasFlag(XLCellsUsedOptions.ConditionalFormats)) + { + cellsUsed = cellsUsed.Union( + Worksheet.ConditionalFormats + .SelectMany(cf => cf.Ranges.GetIntersectedRanges(RangeAddress)) + .Select(r => r.FirstCell()) + .Where(predicate) + ); + } + if (options.HasFlag(XLCellsUsedOptions.DataValidation)) + { + cellsUsed = cellsUsed.Union( + Worksheet.DataValidations + .GetAllInRange(RangeAddress) + .SelectMany(dv => dv.Ranges) + .Select(r => r.FirstCell()) + .Where(predicate) + ); + } + if (options.HasFlag(XLCellsUsedOptions.MergedRanges)) + { + cellsUsed = cellsUsed.Union( + Worksheet.MergedRanges.GetIntersectedRanges(RangeAddress) + .Select(r => r.FirstCell()) + .Where(predicate) + ); + } + + cellsUsed = cellsUsed.ToList(); + + if (!cellsUsed.Any()) + return null; + + var firstRow = cellsUsed.Min(c => c.Address.RowNumber); + var firstColumn = cellsUsed.Min(c => c.Address.ColumnNumber); + + if (firstRow < RangeAddress.FirstAddress.RowNumber) + firstRow = RangeAddress.FirstAddress.RowNumber; + + if (firstColumn < RangeAddress.FirstAddress.ColumnNumber) + firstColumn = RangeAddress.FirstAddress.ColumnNumber; + + return Worksheet.Cell(firstRow, firstColumn); + } + + internal XLCell LastCellUsed() + { + return LastCellUsed(XLCellsUsedOptions.AllContents, predicate: null); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + internal XLCell LastCellUsed(Boolean includeFormats) + { + return LastCellUsed(includeFormats, null); + } + + internal XLCell LastCellUsed(Func predicate) + { + return LastCellUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + internal XLCell LastCellUsed(Boolean includeFormats, Func predicate) + { + return LastCellUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + internal XLCell LastCellUsed(XLCellsUsedOptions options, Func predicate) + { + predicate = predicate ?? (t => true); + + //To avoid unnecessary initialization of thousands cells + var opt = options + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation + & ~XLCellsUsedOptions.MergedRanges; + + IEnumerable cellsUsed = CellsUsed(opt, predicate); + + if (options.HasFlag(XLCellsUsedOptions.ConditionalFormats)) + { + cellsUsed = cellsUsed.Union( + Worksheet.ConditionalFormats + .SelectMany(cf => cf.Ranges.GetIntersectedRanges(RangeAddress)) + .Select(r => r.LastCell()) + .Where(predicate) + ); + } + if (options.HasFlag(XLCellsUsedOptions.DataValidation)) + { + cellsUsed = cellsUsed.Union( + Worksheet.DataValidations + .GetAllInRange(RangeAddress) + .SelectMany(dv => dv.Ranges) + .Select(r => r.LastCell()) + .Where(predicate) + ); + } + if (options.HasFlag(XLCellsUsedOptions.MergedRanges)) + { + cellsUsed = cellsUsed.Union( + Worksheet.MergedRanges.GetIntersectedRanges(RangeAddress) + .Select(r => r.LastCell()) + .Where(predicate) + ); + } + + cellsUsed = cellsUsed.ToList(); + + if (!cellsUsed.Any()) + return null; + + var lastRow = cellsUsed.Max(c => c.Address.RowNumber); + var lastColumn = cellsUsed.Max(c => c.Address.ColumnNumber); + + if (lastRow > RangeAddress.LastAddress.RowNumber) + lastRow = RangeAddress.LastAddress.RowNumber; + + if (lastColumn > RangeAddress.LastAddress.ColumnNumber) + lastColumn = RangeAddress.LastAddress.ColumnNumber; + + return Worksheet.Cell(lastRow, lastColumn); + } + + public XLCell Cell(Int32 row, Int32 column) + { + return Cell(new XLAddress(Worksheet, row, column, false, false)); + } + + public virtual XLCell Cell(String cellAddressInRange) + { + if (XLHelper.IsValidA1Address(cellAddressInRange)) + return Cell(XLAddress.Create(Worksheet, cellAddressInRange)); + + if (Worksheet.NamedRanges.TryGetValue(cellAddressInRange, out IXLNamedRange namedRange)) + return namedRange.Ranges.First().FirstCell().CastTo(); + + return null; + } + + public XLCell Cell(Int32 row, String column) + { + return Cell(new XLAddress(Worksheet, row, column, false, false)); + } + + public XLCell Cell(IXLAddress cellAddressInRange) + { + return Cell(cellAddressInRange.RowNumber, cellAddressInRange.ColumnNumber); + } + + public XLCell Cell(in XLAddress cellAddressInRange) + { + Int32 absRow = cellAddressInRange.RowNumber + RangeAddress.FirstAddress.RowNumber - 1; + Int32 absColumn = cellAddressInRange.ColumnNumber + RangeAddress.FirstAddress.ColumnNumber - 1; + + if (absRow <= 0 || absRow > XLHelper.MaxRowNumber) + { + throw new ArgumentOutOfRangeException( + nameof(cellAddressInRange), + String.Format("Row number must be between 1 and {0}", XLHelper.MaxRowNumber) + ); + } + + if (absColumn <= 0 || absColumn > XLHelper.MaxColumnNumber) + { + throw new ArgumentOutOfRangeException( + nameof(cellAddressInRange), + String.Format("Column number must be between 1 and {0}", XLHelper.MaxColumnNumber) + ); + } + + var cell = Worksheet.Internals.CellsCollection.GetCell(absRow, + absColumn); + + if (cell != null) + return cell; + + var styleValue = this.StyleValue; + + if (styleValue == Worksheet.StyleValue) + { + if (Worksheet.Internals.RowsCollection.TryGetValue(absRow, out XLRow row) + && row.StyleValue != Worksheet.StyleValue) + styleValue = row.StyleValue; + else if (Worksheet.Internals.ColumnsCollection.TryGetValue(absColumn, out XLColumn column) + && column.StyleValue != Worksheet.StyleValue) + styleValue = column.StyleValue; + } + var absoluteAddress = new XLAddress(this.Worksheet, + absRow, + absColumn, + cellAddressInRange.FixedRow, + cellAddressInRange.FixedColumn); + + // If the default style for this range base is empty, but the worksheet + // has a default style, use the worksheet's default style + XLCell newCell = new XLCell(Worksheet, absoluteAddress, styleValue); + + Worksheet.Internals.CellsCollection.Add(absRow, absColumn, newCell); + return newCell; + } + + public Int32 RowCount() + { + return RangeAddress.LastAddress.RowNumber - RangeAddress.FirstAddress.RowNumber + 1; + } + + public Int32 RowNumber() + { + return RangeAddress.FirstAddress.RowNumber; + } + + public Int32 ColumnCount() + { + return RangeAddress.LastAddress.ColumnNumber - RangeAddress.FirstAddress.ColumnNumber + 1; + } + + public Int32 ColumnNumber() + { + return RangeAddress.FirstAddress.ColumnNumber; + } + + public String ColumnLetter() + { + return RangeAddress.FirstAddress.ColumnLetter; + } + + public virtual XLRange Range(String rangeAddressStr) + { + var rangeAddress = new XLRangeAddress(Worksheet, rangeAddressStr); + return Range(rangeAddress); + } + + internal abstract void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted); + + internal abstract void WorksheetRangeShiftedRows(XLRange range, int rowsShifted); + + public abstract XLRangeType RangeType { get; } + + public XLRange Range(IXLCell firstCell, IXLCell lastCell) + { + var newFirstCellAddress = (XLAddress)firstCell.Address; + var newLastCellAddress = (XLAddress)lastCell.Address; + + return GetRange(newFirstCellAddress, newLastCellAddress); + } + + private XLRange GetRange(XLAddress newFirstCellAddress, XLAddress newLastCellAddress) + { + if (!this.Worksheet.Equals(newFirstCellAddress.Worksheet)) + throw new ArgumentException("The address refers to a different worksheet.", nameof(newFirstCellAddress)); + + if (!this.Worksheet.Equals(newLastCellAddress.Worksheet)) + throw new ArgumentException("The address refers to a different worksheet.", nameof(newLastCellAddress)); + + var newRangeAddress = new XLRangeAddress(newFirstCellAddress, newLastCellAddress); + var xlRangeParameters = new XLRangeParameters(newRangeAddress, Style); + if ( + newFirstCellAddress.RowNumber < RangeAddress.FirstAddress.RowNumber + || newFirstCellAddress.RowNumber > RangeAddress.LastAddress.RowNumber + || newLastCellAddress.RowNumber > RangeAddress.LastAddress.RowNumber + || newFirstCellAddress.ColumnNumber < RangeAddress.FirstAddress.ColumnNumber + || newFirstCellAddress.ColumnNumber > RangeAddress.LastAddress.ColumnNumber + || newLastCellAddress.ColumnNumber > RangeAddress.LastAddress.ColumnNumber + ) + { + throw new ArgumentOutOfRangeException(String.Format( + "The cells {0} and {1} are outside the range '{2}'.", + newFirstCellAddress, + newLastCellAddress, + ToString())); + } + + if (newFirstCellAddress.Worksheet != null) + return newFirstCellAddress.Worksheet.GetOrCreateRange(xlRangeParameters); + else if (Worksheet != null) + return Worksheet.GetOrCreateRange(xlRangeParameters); + else + return new XLRange(xlRangeParameters); + } + + public XLRange Range(String firstCellAddress, String lastCellAddress) + { + var rangeAddress = new XLRangeAddress(XLAddress.Create(Worksheet, firstCellAddress), + XLAddress.Create(Worksheet, lastCellAddress)); + return Range(rangeAddress); + } + + public XLRange Range(Int32 firstCellRow, Int32 firstCellColumn, Int32 lastCellRow, Int32 lastCellColumn) + { + var rangeAddress = new XLRangeAddress + ( + new XLAddress + ( + Worksheet, + firstCellRow + RangeAddress.FirstAddress.RowNumber - 1, + firstCellColumn + RangeAddress.FirstAddress.ColumnNumber - 1, + fixedRow: false, + fixedColumn: false + ), + new XLAddress + ( + Worksheet, + lastCellRow + RangeAddress.FirstAddress.RowNumber - 1, + lastCellColumn + RangeAddress.FirstAddress.ColumnNumber - 1, + fixedRow: false, + fixedColumn: false + ) + ); + return Range(rangeAddress); + } + + public XLRange Range(IXLAddress firstCellAddress, IXLAddress lastCellAddress) + { + var rangeAddress = new XLRangeAddress((XLAddress)firstCellAddress, (XLAddress)lastCellAddress); + return Range(rangeAddress); + } + + public XLRange Range(IXLRangeAddress rangeAddress) + { + var xlRangeAddress = (XLRangeAddress)rangeAddress; + return Range(in xlRangeAddress); + } + + internal XLRange Range(in XLRangeAddress rangeAddress) + { + var ws = rangeAddress.FirstAddress.Worksheet ?? + rangeAddress.LastAddress.Worksheet ?? + Worksheet; + + var newFirstCellAddress = new XLAddress(ws, + rangeAddress.FirstAddress.RowNumber, + rangeAddress.FirstAddress.ColumnNumber, + rangeAddress.FirstAddress.FixedRow, + rangeAddress.FirstAddress.FixedColumn); + + var newLastCellAddress = new XLAddress(ws, + rangeAddress.LastAddress.RowNumber, + rangeAddress.LastAddress.ColumnNumber, + rangeAddress.LastAddress.FixedRow, + rangeAddress.LastAddress.FixedColumn); + + return GetRange(newFirstCellAddress, newLastCellAddress); + } + + public virtual IXLRanges Ranges(String ranges) + { + var retVal = new XLRanges(); + var rangePairs = ranges.Split(','); + foreach (string pair in rangePairs) + retVal.Add(Range(pair.Trim())); + return retVal; + } + + public IXLRanges Ranges(params String[] ranges) + { + var retVal = new XLRanges(); + foreach (string pair in ranges) + retVal.Add(Range(pair)); + return retVal; + } + + protected String FixColumnAddress(String address) + { + if (Int32.TryParse(address, out Int32 rowNumber)) + return RangeAddress.FirstAddress.ColumnLetter + (rowNumber + RangeAddress.FirstAddress.RowNumber - 1).ToInvariantString(); + return address; + } + + protected String FixRowAddress(String address) + { + if (Int32.TryParse(address, out Int32 columnNumber)) + return XLHelper.GetColumnLetterFromNumber(columnNumber + RangeAddress.FirstAddress.ColumnNumber - 1) + RangeAddress.FirstAddress.RowNumber.ToInvariantString(); + return address; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(bool includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(true, options) { RangeAddress }; + return cells; + } + + public IXLCells CellsUsed(Func predicate) + { + var cells = new XLCells(true, XLCellsUsedOptions.AllContents, predicate) { RangeAddress }; + return cells; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(Boolean includeFormats, Func predicate) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options, Func predicate) + { + var cells = new XLCells(true, options, predicate) { RangeAddress }; + return cells; + } + + public IXLRangeColumns InsertColumnsAfter(Int32 numberOfColumns) + { + return InsertColumnsAfter(numberOfColumns, true); + } + + public IXLRangeColumns InsertColumnsAfter(Int32 numberOfColumns, Boolean expandRange) + { + var retVal = InsertColumnsAfter(false, numberOfColumns); + // Adjust the range + if (expandRange) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber + numberOfColumns, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + return retVal; + } + + public IXLRangeColumns InsertColumnsAfter(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true) + { + return InsertColumnsAfterInternal(onlyUsedCells, numberOfColumns, formatFromLeft); + } + + public void InsertColumnsAfterVoid(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true) + { + InsertColumnsAfterInternal(onlyUsedCells, numberOfColumns, formatFromLeft, nullReturn: true); + } + + private IXLRangeColumns InsertColumnsAfterInternal(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true, Boolean nullReturn = false) + { + int columnCount = ColumnCount(); + int firstColumn = RangeAddress.FirstAddress.ColumnNumber + columnCount; + if (firstColumn > XLHelper.MaxColumnNumber) + firstColumn = XLHelper.MaxColumnNumber; + int lastColumn = firstColumn + ColumnCount() - 1; + if (lastColumn > XLHelper.MaxColumnNumber) + lastColumn = XLHelper.MaxColumnNumber; + + int firstRow = RangeAddress.FirstAddress.RowNumber; + int lastRow = firstRow + RowCount() - 1; + if (lastRow > XLHelper.MaxRowNumber) + lastRow = XLHelper.MaxRowNumber; + + var newRange = Worksheet.Range(firstRow, firstColumn, lastRow, lastColumn); + return newRange.InsertColumnsBeforeInternal(onlyUsedCells, numberOfColumns, formatFromLeft, nullReturn); + } + + public IXLRangeColumns InsertColumnsBefore(Int32 numberOfColumns) + { + return InsertColumnsBefore(numberOfColumns, false); + } + + public IXLRangeColumns InsertColumnsBefore(Int32 numberOfColumns, Boolean expandRange) + { + var retVal = InsertColumnsBefore(false, numberOfColumns); + // Adjust the range + if (expandRange) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber - numberOfColumns, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + return retVal; + } + + public IXLRangeColumns InsertColumnsBefore(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true) + { + return InsertColumnsBeforeInternal(onlyUsedCells, numberOfColumns, formatFromLeft); + } + + public void InsertColumnsBeforeVoid(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true) + { + InsertColumnsBeforeInternal(onlyUsedCells, numberOfColumns, formatFromLeft, nullReturn: true); + } + + private IXLRangeColumns InsertColumnsBeforeInternal(Boolean onlyUsedCells, Int32 numberOfColumns, Boolean formatFromLeft = true, Boolean nullReturn = false) + { + if (numberOfColumns <= 0 || numberOfColumns > XLHelper.MaxColumnNumber) + throw new ArgumentOutOfRangeException(nameof(numberOfColumns), + $"Number of columns to insert must be a positive number no more than {XLHelper.MaxColumnNumber}"); + + foreach (XLWorksheet ws in Worksheet.Workbook.WorksheetsInternal) + { + foreach (XLCell cell in ws.Internals.CellsCollection.GetCells(c => !String.IsNullOrWhiteSpace(c.FormulaA1))) + cell.ShiftFormulaColumns(AsRange(), numberOfColumns); + } + + var cellsToInsert = new Dictionary(); + var cellsToDelete = new List(); + int firstColumn = RangeAddress.FirstAddress.ColumnNumber; + int firstRow = RangeAddress.FirstAddress.RowNumber; + int lastRow = RangeAddress.FirstAddress.RowNumber + RowCount() - 1; + + if (!onlyUsedCells) + { + int lastColumn = Worksheet.Internals.CellsCollection.MaxColumnUsed; + if (lastColumn > 0) + { + for (int co = lastColumn; co >= firstColumn; co--) + { + int newColumn = co + numberOfColumns; + for (int ro = lastRow; ro >= firstRow; ro--) + { + var oldCell = Worksheet.Internals.CellsCollection.GetCell(ro, co); + if (oldCell == null) + continue; + + var oldKey = new XLAddress(Worksheet, ro, co, false, false); + var newKey = new XLAddress(Worksheet, ro, newColumn, false, false); + + oldCell.Address = newKey; + if (newKey.IsValid) + cellsToInsert.Add(newKey, oldCell); + cellsToDelete.Add(oldKey); + } + + if (this.IsEntireColumn()) + { + Worksheet.Column(newColumn).Width = Worksheet.Column(co).Width; + } + } + } + } + else + { + foreach ( + XLCell c in + Worksheet.Internals.CellsCollection.GetCells(firstRow, firstColumn, lastRow, + Worksheet.Internals.CellsCollection.MaxColumnUsed)) + { + int newColumn = c.Address.ColumnNumber + numberOfColumns; + var newKey = new XLAddress(Worksheet, c.Address.RowNumber, newColumn, false, false); + + cellsToDelete.Add(c.Address); + c.Address = newKey; + if (newKey.IsValid) + cellsToInsert.Add(newKey, c); + } + } + + cellsToDelete.ForEach(c => Worksheet.Internals.CellsCollection.Remove(c.RowNumber, c.ColumnNumber)); + cellsToInsert.ForEach( + c => Worksheet.Internals.CellsCollection.Add(c.Key.RowNumber, c.Key.ColumnNumber, c.Value)); + + Int32 firstRowReturn = RangeAddress.FirstAddress.RowNumber; + Int32 lastRowReturn = RangeAddress.LastAddress.RowNumber; + Int32 firstColumnReturn = RangeAddress.FirstAddress.ColumnNumber; + Int32 lastColumnReturn = RangeAddress.FirstAddress.ColumnNumber + numberOfColumns - 1; + + Worksheet.NotifyRangeShiftedColumns(AsRange(), numberOfColumns); + + var rangeToReturn = Worksheet.Range(firstRowReturn, firstColumnReturn, lastRowReturn, lastColumnReturn); + + // We deliberately ignore conditional formats and data validation here. Their shifting is handled elsewhere + var contentFlags = XLCellsUsedOptions.All + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation; + + if (formatFromLeft && rangeToReturn.RangeAddress.FirstAddress.ColumnNumber > 1) + { + var firstColumnUsed = rangeToReturn.FirstColumn(); + var model = firstColumnUsed.ColumnLeft(); + var modelFirstRow = (model as IXLRangeBase).FirstCellUsed(contentFlags); + var modelLastRow = (model as IXLRangeBase).LastCellUsed(contentFlags); + if (modelLastRow != null) + { + Int32 firstRoReturned = modelFirstRow.Address.RowNumber + - model.RangeAddress.FirstAddress.RowNumber + 1; + Int32 lastRoReturned = modelLastRow.Address.RowNumber + - model.RangeAddress.FirstAddress.RowNumber + 1; + for (Int32 ro = firstRoReturned; ro <= lastRoReturned; ro++) + { + rangeToReturn.Row(ro).Style = model.Cell(ro).Style; + } + } + } + else + { + var lastRoUsed = rangeToReturn.LastRowUsed(contentFlags); + if (lastRoUsed != null) + { + Int32 lastRoReturned = lastRoUsed.RowNumber(); + for (Int32 ro = 1; ro <= lastRoReturned; ro++) + { + var styleToUse = + Worksheet.Internals.RowsCollection.TryGetValue(ro, out XLRow row) + ? row.Style + : Worksheet.Style; + + rangeToReturn.Row(ro).Style = styleToUse; + } + } + } + + if (nullReturn) + return null; + + return rangeToReturn.Columns(); + } + + public IXLRangeRows InsertRowsBelow(Int32 numberOfRows) + { + return InsertRowsBelow(numberOfRows, true); + } + + public IXLRangeRows InsertRowsBelow(Int32 numberOfRows, Boolean expandRange) + { + var retVal = InsertRowsBelow(false, numberOfRows); + // Adjust the range + if (expandRange) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber + numberOfRows, + RangeAddress.LastAddress.ColumnNumber, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + return retVal; + } + + public IXLRangeRows InsertRowsBelow(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove = true) + { + return InsertRowsBelowInternal(onlyUsedCells, numberOfRows, formatFromAbove, nullReturn: false); + } + + public void InsertRowsBelowVoid(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove = true) + { + InsertRowsBelowInternal(onlyUsedCells, numberOfRows, formatFromAbove, nullReturn: true); + } + + private IXLRangeRows InsertRowsBelowInternal(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove, Boolean nullReturn) + { + int rowCount = RowCount(); + int firstRow = RangeAddress.FirstAddress.RowNumber + rowCount; + if (firstRow > XLHelper.MaxRowNumber) + firstRow = XLHelper.MaxRowNumber; + int lastRow = firstRow + RowCount() - 1; + if (lastRow > XLHelper.MaxRowNumber) + lastRow = XLHelper.MaxRowNumber; + + int firstColumn = RangeAddress.FirstAddress.ColumnNumber; + int lastColumn = firstColumn + ColumnCount() - 1; + if (lastColumn > XLHelper.MaxColumnNumber) + lastColumn = XLHelper.MaxColumnNumber; + + var newRange = Worksheet.Range(firstRow, firstColumn, lastRow, lastColumn); + return newRange.InsertRowsAboveInternal(onlyUsedCells, numberOfRows, formatFromAbove, nullReturn); + } + + public IXLRangeRows InsertRowsAbove(Int32 numberOfRows) + { + return InsertRowsAbove(numberOfRows, false); + } + + public IXLRangeRows InsertRowsAbove(Int32 numberOfRows, Boolean expandRange) + { + var retVal = InsertRowsAbove(false, numberOfRows); + // Adjust the range + if (expandRange) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber - numberOfRows, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + return retVal; + } + + public void InsertRowsAboveVoid(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove = true) + { + InsertRowsAboveInternal(onlyUsedCells, numberOfRows, formatFromAbove, nullReturn: true); + } + + public IXLRangeRows InsertRowsAbove(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove = true) + { + return InsertRowsAboveInternal(onlyUsedCells, numberOfRows, formatFromAbove, nullReturn: false); + } + + private IXLRangeRows InsertRowsAboveInternal(Boolean onlyUsedCells, Int32 numberOfRows, Boolean formatFromAbove, Boolean nullReturn) + { + if (numberOfRows <= 0 || numberOfRows > XLHelper.MaxRowNumber) + throw new ArgumentOutOfRangeException(nameof(numberOfRows), + $"Number of rows to insert must be a positive number no more than {XLHelper.MaxRowNumber}"); + + var asRange = AsRange(); + foreach (XLWorksheet ws in Worksheet.Workbook.WorksheetsInternal) + { + foreach (XLCell cell in ws.Internals.CellsCollection.GetCells(c => !String.IsNullOrWhiteSpace(c.FormulaA1))) + cell.ShiftFormulaRows(asRange, numberOfRows); + } + + var cellsToInsert = new Dictionary(); + var cellsToDelete = new List(); + int firstRow = RangeAddress.FirstAddress.RowNumber; + int firstColumn = RangeAddress.FirstAddress.ColumnNumber; + int lastColumn = Math.Min( + RangeAddress.FirstAddress.ColumnNumber + ColumnCount() - 1, + Worksheet.Internals.CellsCollection.MaxColumnUsed); + + if (!onlyUsedCells) + { + int lastRow = Worksheet.Internals.CellsCollection.MaxRowUsed; + if (lastRow > 0) + { + for (int ro = lastRow; ro >= firstRow; ro--) + { + int newRow = ro + numberOfRows; + + for (int co = lastColumn; co >= firstColumn; co--) + { + var oldCell = Worksheet.Internals.CellsCollection.GetCell(ro, co); + if (oldCell == null) + continue; + + var oldKey = new XLAddress(Worksheet, ro, co, false, false); + var newKey = new XLAddress(Worksheet, newRow, co, false, false); + + oldCell.Address = newKey; + if (newKey.IsValid) + cellsToInsert.Add(newKey, oldCell); + cellsToDelete.Add(oldKey); + } + if (this.IsEntireRow()) + { + Worksheet.Row(newRow).Height = Worksheet.Row(ro).Height; + } + } + } + } + else + { + foreach ( + XLCell c in + Worksheet.Internals.CellsCollection.GetCells(firstRow, firstColumn, + Worksheet.Internals.CellsCollection.MaxRowUsed, + lastColumn)) + { + int newRow = c.Address.RowNumber + numberOfRows; + var newKey = new XLAddress(Worksheet, newRow, c.Address.ColumnNumber, false, false); + cellsToDelete.Add(c.Address); + c.Address = newKey; + if (newKey.IsValid) + cellsToInsert.Add(newKey, c); + } + } + + cellsToDelete.ForEach(c => Worksheet.Internals.CellsCollection.Remove(c.RowNumber, c.ColumnNumber)); + cellsToInsert.ForEach(c => Worksheet.Internals.CellsCollection.Add(c.Key.RowNumber, c.Key.ColumnNumber, c.Value)); + + Int32 firstRowReturn = RangeAddress.FirstAddress.RowNumber; + Int32 lastRowReturn = RangeAddress.FirstAddress.RowNumber + numberOfRows - 1; + Int32 firstColumnReturn = RangeAddress.FirstAddress.ColumnNumber; + Int32 lastColumnReturn = RangeAddress.LastAddress.ColumnNumber; + + Worksheet.NotifyRangeShiftedRows(AsRange(), numberOfRows); + + var rangeToReturn = Worksheet.Range(firstRowReturn, firstColumnReturn, lastRowReturn, lastColumnReturn); + + // We deliberately ignore conditional formats and data validation here. Their shifting is handled elsewhere + var contentFlags = XLCellsUsedOptions.All + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation; + + if (formatFromAbove && rangeToReturn.RangeAddress.FirstAddress.RowNumber > 1) + { + var fr = rangeToReturn.FirstRow(); + var model = fr.RowAbove(); + var modelFirstColumn = (model as IXLRangeBase).FirstCellUsed(contentFlags); + var modelLastColumn = (model as IXLRangeBase).LastCellUsed(contentFlags); + if (modelFirstColumn != null && modelLastColumn != null) + { + Int32 firstCoReturned = modelFirstColumn.Address.ColumnNumber + - model.RangeAddress.FirstAddress.ColumnNumber + 1; + Int32 lastCoReturned = modelLastColumn.Address.ColumnNumber + - model.RangeAddress.FirstAddress.ColumnNumber + 1; + for (Int32 co = firstCoReturned; co <= lastCoReturned; co++) + { + rangeToReturn.Column(co).Style = model.Cell(co).Style; + } + } + } + else + { + var lastCoUsed = rangeToReturn.LastColumnUsed(contentFlags); + if (lastCoUsed != null) + { + Int32 lastCoReturned = lastCoUsed.ColumnNumber(); + for (Int32 co = 1; co <= lastCoReturned; co++) + { + var styleToUse = + Worksheet.Internals.ColumnsCollection.TryGetValue(co, out XLColumn column) + ? column.Style + : Worksheet.Style; + + rangeToReturn.Style = styleToUse; + } + } + } + + // Skip calling .Rows() for performance reasons if required. + if (nullReturn) + return null; + + return rangeToReturn.Rows(); + } + + private void ClearMerged() + { + var mergeToDelete = Worksheet.Internals.MergedRanges.GetIntersectedRanges(RangeAddress).ToList(); + mergeToDelete.ForEach(m => Worksheet.Internals.MergedRanges.Remove(m)); + } + + public Boolean Contains(IXLCell cell) + { + return Contains((XLAddress)cell.Address); + } + + public bool Contains(XLAddress first, XLAddress last) + { + return Contains(first) && Contains(last); + } + + public bool Contains(XLAddress address) + { + return RangeAddress.Contains(in address); + } + + public void Delete(XLShiftDeletedCells shiftDeleteCells) + { + int numberOfRows = RowCount(); + int numberOfColumns = ColumnCount(); + + if (!RangeAddress.IsValid) return; + + Worksheet.SparklineGroups.Remove(this); + + IXLRange shiftedRangeFormula = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber); + + // Shift formulas first + foreach (var cell in Worksheet + .Workbook + .Worksheets + .Cast() + .SelectMany(ws => ws + .Internals + .CellsCollection + .GetCells(c => c.HasFormula))) + { + if (shiftDeleteCells == XLShiftDeletedCells.ShiftCellsUp) + cell.ShiftFormulaRows((XLRange)shiftedRangeFormula, numberOfRows * -1); + else + cell.ShiftFormulaColumns((XLRange)shiftedRangeFormula, numberOfColumns * -1); + } + + // Range to shift... + var cellsToInsert = new Dictionary(); + var cellsToDelete = new List(); + + Int32 columnModifier = 0; + Int32 rowModifier = 0; + IEnumerable cellsQuery; + switch (shiftDeleteCells) + { + case XLShiftDeletedCells.ShiftCellsLeft: + cellsQuery = Worksheet.Internals.CellsCollection.GetCells( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + Worksheet.Internals.CellsCollection.MaxColumnUsed); + + columnModifier = ColumnCount(); + + break; + + case XLShiftDeletedCells.ShiftCellsUp: + cellsQuery = Worksheet.Internals.CellsCollection.GetCells( + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber, + Worksheet.Internals.CellsCollection.MaxRowUsed, + RangeAddress.LastAddress.ColumnNumber); + + rowModifier = RowCount(); + + break; + + default: + cellsQuery = new XLCell[] { }; + break; + } + + foreach (var c in cellsQuery) + { + // Schedule for removal from CellsCollection + cellsToDelete.Add(c.Address); + + // Generate new cell to insert into CellsCollection + var newCellAddress = new XLAddress(Worksheet, c.Address.RowNumber - rowModifier, + c.Address.ColumnNumber - columnModifier, + fixedRow: false, + fixedColumn: false); + + if (newCellAddress.IsValid) + { + bool canInsert = shiftDeleteCells == XLShiftDeletedCells.ShiftCellsLeft + ? c.Address.ColumnNumber > RangeAddress.LastAddress.ColumnNumber + : c.Address.RowNumber > RangeAddress.LastAddress.RowNumber; + + c.Address = newCellAddress; + + if (canInsert) + cellsToInsert.Add(newCellAddress, c); + } + } + + cellsToDelete.ForEach(c => Worksheet.Internals.CellsCollection.Remove(c.RowNumber, c.ColumnNumber)); + cellsToInsert.ForEach( + c => Worksheet.Internals.CellsCollection.Add(c.Key.RowNumber, c.Key.ColumnNumber, c.Value)); + + var mergesToRemove = Worksheet.Internals.MergedRanges.Where(Contains).ToList(); + mergesToRemove.ForEach(r => Worksheet.Internals.MergedRanges.Remove(r)); + + var hyperlinksToRemove = Worksheet.Hyperlinks.Where(hl => Contains(hl.Cell.AsRange())).ToList(); + hyperlinksToRemove.ForEach(hl => Worksheet.Hyperlinks.Delete(hl)); + + var shiftedRange = AsRange(); + if (shiftDeleteCells == XLShiftDeletedCells.ShiftCellsUp) + Worksheet.NotifyRangeShiftedRows(shiftedRange, rowModifier * -1); + else + Worksheet.NotifyRangeShiftedColumns(shiftedRange, columnModifier * -1); + + Worksheet.DeleteRange(RangeAddress); + } + + public override string ToString() + { + return String.Concat( + Worksheet.Name.EscapeSheetName(), + '!', + RangeAddress.FirstAddress, + ':', + RangeAddress.LastAddress); + } + + protected IXLRangeAddress ShiftColumns(IXLRangeAddress thisRangeAddress, XLRange shiftedRange, int columnsShifted) + { + if (!thisRangeAddress.IsValid || !shiftedRange.RangeAddress.IsValid) return thisRangeAddress; + + bool allRowsAreCovered = thisRangeAddress.FirstAddress.RowNumber >= shiftedRange.RangeAddress.FirstAddress.RowNumber && + thisRangeAddress.LastAddress.RowNumber <= shiftedRange.RangeAddress.LastAddress.RowNumber; + + if (!allRowsAreCovered) + return thisRangeAddress; + + bool shiftLeftBoundary = (columnsShifted > 0 && thisRangeAddress.FirstAddress.ColumnNumber >= shiftedRange.RangeAddress.FirstAddress.ColumnNumber) || + (columnsShifted < 0 && thisRangeAddress.FirstAddress.ColumnNumber > shiftedRange.RangeAddress.FirstAddress.ColumnNumber); + + bool shiftRightBoundary = thisRangeAddress.LastAddress.ColumnNumber >= shiftedRange.RangeAddress.FirstAddress.ColumnNumber; + + int newLeftBoundary = thisRangeAddress.FirstAddress.ColumnNumber; + if (shiftLeftBoundary) + { + if (newLeftBoundary + columnsShifted > shiftedRange.RangeAddress.FirstAddress.ColumnNumber) + newLeftBoundary = newLeftBoundary + columnsShifted; + else + newLeftBoundary = shiftedRange.RangeAddress.FirstAddress.ColumnNumber; + } + + int newRightBoundary = thisRangeAddress.LastAddress.ColumnNumber; + if (shiftRightBoundary) + newRightBoundary += columnsShifted; + + bool destroyedByShift = newRightBoundary < newLeftBoundary; + + var firstAddress = (XLAddress)thisRangeAddress.FirstAddress; + var lastAddress = (XLAddress)thisRangeAddress.LastAddress; + + if (destroyedByShift) + { + firstAddress = Worksheet.InvalidAddress; + lastAddress = Worksheet.InvalidAddress; + Worksheet.DeleteRange(RangeAddress); + } + + if (shiftLeftBoundary) + firstAddress = new XLAddress(Worksheet, + thisRangeAddress.FirstAddress.RowNumber, + newLeftBoundary, + thisRangeAddress.FirstAddress.FixedRow, + thisRangeAddress.FirstAddress.FixedColumn); + + if (shiftRightBoundary) + lastAddress = new XLAddress(Worksheet, + thisRangeAddress.LastAddress.RowNumber, + newRightBoundary, + thisRangeAddress.LastAddress.FixedRow, + thisRangeAddress.LastAddress.FixedColumn); + + return new XLRangeAddress(firstAddress, lastAddress); + } + + protected IXLRangeAddress ShiftRows(IXLRangeAddress thisRangeAddress, XLRange shiftedRange, int rowsShifted) + { + if (!thisRangeAddress.IsValid || !shiftedRange.RangeAddress.IsValid) return thisRangeAddress; + + bool allColumnsAreCovered = thisRangeAddress.FirstAddress.ColumnNumber >= shiftedRange.RangeAddress.FirstAddress.ColumnNumber && + thisRangeAddress.LastAddress.ColumnNumber <= shiftedRange.RangeAddress.LastAddress.ColumnNumber; + + if (!allColumnsAreCovered) + return thisRangeAddress; + + bool shiftTopBoundary = (rowsShifted > 0 && thisRangeAddress.FirstAddress.RowNumber >= shiftedRange.RangeAddress.FirstAddress.RowNumber) || + (rowsShifted < 0 && thisRangeAddress.FirstAddress.RowNumber > shiftedRange.RangeAddress.FirstAddress.RowNumber); + + bool shiftBottomBoundary = thisRangeAddress.LastAddress.RowNumber >= shiftedRange.RangeAddress.FirstAddress.RowNumber; + + int newTopBoundary = thisRangeAddress.FirstAddress.RowNumber; + if (shiftTopBoundary) + { + if (newTopBoundary + rowsShifted > shiftedRange.RangeAddress.FirstAddress.RowNumber) + newTopBoundary = newTopBoundary + rowsShifted; + else + newTopBoundary = shiftedRange.RangeAddress.FirstAddress.RowNumber; + } + + int newBottomBoundary = thisRangeAddress.LastAddress.RowNumber; + if (shiftBottomBoundary) + newBottomBoundary += rowsShifted; + + bool destroyedByShift = newBottomBoundary < newTopBoundary; + + var firstAddress = (XLAddress)thisRangeAddress.FirstAddress; + var lastAddress = (XLAddress)thisRangeAddress.LastAddress; + + if (destroyedByShift) + { + firstAddress = Worksheet.InvalidAddress; + lastAddress = Worksheet.InvalidAddress; + Worksheet.DeleteRange(RangeAddress); + } + + if (shiftTopBoundary) + firstAddress = new XLAddress(Worksheet, + newTopBoundary, + thisRangeAddress.FirstAddress.ColumnNumber, + thisRangeAddress.FirstAddress.FixedRow, + thisRangeAddress.FirstAddress.FixedColumn); + + if (shiftBottomBoundary) + lastAddress = new XLAddress(Worksheet, + newBottomBoundary, + thisRangeAddress.LastAddress.ColumnNumber, + thisRangeAddress.LastAddress.FixedRow, + thisRangeAddress.LastAddress.FixedColumn); + + return new XLRangeAddress(firstAddress, lastAddress); + } + + public IXLRange RangeUsed() + { + return RangeUsed(XLCellsUsedOptions.AllContents); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRange RangeUsed(bool includeFormats) + { + return RangeUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLRange RangeUsed(XLCellsUsedOptions options) + { + var firstCell = (this as IXLRangeBase).FirstCellUsed(options); + if (firstCell == null) + return null; + var lastCell = (this as IXLRangeBase).LastCellUsed(options); + return Worksheet.Range(firstCell, lastCell); + } + + public virtual void CopyTo(IXLRangeBase target) + { + CopyTo(target.FirstCell()); + } + + public virtual void CopyTo(IXLCell target) + { + target.Value = this; + } + + //public IXLChart CreateChart(Int32 firstRow, Int32 firstColumn, Int32 lastRow, Int32 lastColumn) + //{ + // IXLChart chart = new XLChartWorksheet; + // chart.FirstRow = firstRow; + // chart.LastRow = lastRow; + // chart.LastColumn = lastColumn; + // chart.FirstColumn = firstColumn; + // Worksheet.Charts.Add(chart); + // return chart; + //} + + IXLPivotTable IXLRangeBase.CreatePivotTable(IXLCell targetCell, String name) + { + return CreatePivotTable(targetCell, name); + } + + public XLPivotTable CreatePivotTable(IXLCell targetCell, String name) + { + return (XLPivotTable)targetCell.Worksheet.PivotTables.Add(name, targetCell, AsRange()); + } + + public virtual IXLAutoFilter SetAutoFilter() + { + return SetAutoFilter(true); + } + + public IXLAutoFilter SetAutoFilter(Boolean value) + { + if (value) + return Worksheet.AutoFilter.Set(this); + else + return Worksheet.AutoFilter.Clear(); + } + + #region Sort + + public IXLSortElements SortRows + { + get { return _sortRows ?? (_sortRows = new XLSortElements()); } + } + + public IXLSortElements SortColumns + { + get { return _sortColumns ?? (_sortColumns = new XLSortElements()); } + } + + private String DefaultSortString() + { + var sb = new StringBuilder(); + Int32 maxColumn = ColumnCount(); + if (maxColumn == XLHelper.MaxColumnNumber) + maxColumn = (this as IXLRangeBase).LastCellUsed(XLCellsUsedOptions.All).Address.ColumnNumber; + for (int i = 1; i <= maxColumn; i++) + { + if (sb.Length > 0) + sb.Append(','); + + sb.Append(i); + } + + return sb.ToString(); + } + + public IXLRangeBase Sort() + { + if (!SortColumns.Any()) + { + return Sort(DefaultSortString()); + } + + SortRangeRows(); + return this; + } + + public IXLRangeBase Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + SortColumns.Clear(); + if (String.IsNullOrWhiteSpace(columnsToSortBy)) + { + columnsToSortBy = DefaultSortString(); + } + + foreach (string coPairTrimmed in columnsToSortBy.Split(',').Select(coPair => coPair.Trim())) + { + String coString; + String order; + if (coPairTrimmed.Contains(' ')) + { + var pair = coPairTrimmed.Split(' '); + coString = pair[0]; + order = pair[1]; + } + else + { + coString = coPairTrimmed; + order = sortOrder == XLSortOrder.Ascending ? "ASC" : "DESC"; + } + + if (!Int32.TryParse(coString, out Int32 co)) + co = XLHelper.GetColumnNumberFromLetter(coString); + + SortColumns.Add(co, String.Compare(order, "ASC", true) == 0 ? XLSortOrder.Ascending : XLSortOrder.Descending, ignoreBlanks, matchCase); + } + + SortRangeRows(); + return this; + } + + public IXLRangeBase Sort(Int32 columnToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return Sort(columnToSortBy.ToString(), sortOrder, matchCase, ignoreBlanks); + } + + public IXLRangeBase SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + SortRows.Clear(); + Int32 maxColumn = ColumnCount(); + if (maxColumn == XLHelper.MaxColumnNumber) + maxColumn = (this as IXLRangeBase).LastCellUsed(XLCellsUsedOptions.All).Address.ColumnNumber; + + for (int i = 1; i <= maxColumn; i++) + { + SortRows.Add(i, sortOrder, ignoreBlanks, matchCase); + } + + SortRangeColumns(); + return this; + } + + #region Sort Rows + + private void SortRangeRows() + { + Int32 maxRow = RowCount(); + if (maxRow == XLHelper.MaxRowNumber) + maxRow = (this as IXLRangeBase).LastCellUsed(XLCellsUsedOptions.All).Address.RowNumber; + + SortingRangeRows(1, maxRow); + } + + private void SwapRows(Int32 row1, Int32 row2) + { + int row1InWs = RangeAddress.FirstAddress.RowNumber + row1 - 1; + int row2InWs = RangeAddress.FirstAddress.RowNumber + row2 - 1; + + Int32 firstColumn = RangeAddress.FirstAddress.ColumnNumber; + Int32 lastColumn = RangeAddress.LastAddress.ColumnNumber; + + var range1Sp1 = new XLSheetPoint(row1InWs, firstColumn); + var range1Sp2 = new XLSheetPoint(row1InWs, lastColumn); + var range2Sp1 = new XLSheetPoint(row2InWs, firstColumn); + var range2Sp2 = new XLSheetPoint(row2InWs, lastColumn); + + Worksheet.Internals.CellsCollection.SwapRanges(new XLSheetRange(range1Sp1, range1Sp2), + new XLSheetRange(range2Sp1, range2Sp2), Worksheet); + } + + private int SortRangeRows(int begPoint, int endPoint) + { + int pivot = begPoint; + int m = begPoint + 1; + int n = endPoint; + while ((m < endPoint) && RowQuick(pivot).CompareTo(RowQuick(m), SortColumns) >= 0) + m++; + + while (n > begPoint && RowQuick(pivot).CompareTo(RowQuick(n), SortColumns) <= 0) + n--; + + while (m < n) + { + SwapRows(m, n); + + while (m < endPoint && RowQuick(pivot).CompareTo(RowQuick(m), SortColumns) >= 0) + m++; + + while (n > begPoint && RowQuick(pivot).CompareTo(RowQuick(n), SortColumns) <= 0) + n--; + } + + if (pivot != n) + SwapRows(n, pivot); + + return n; + } + + private void SortingRangeRows(int beg, int end) + { + if (beg == end) + return; + int pivot = SortRangeRows(beg, end); + if (pivot > beg) + SortingRangeRows(beg, pivot - 1); + if (pivot < end) + SortingRangeRows(pivot + 1, end); + } + + #endregion Sort Rows + + #region Sort Columns + + private void SortRangeColumns() + { + Int32 maxColumn = ColumnCount(); + if (maxColumn == XLHelper.MaxColumnNumber) + maxColumn = (this as IXLRangeBase).LastCellUsed(XLCellsUsedOptions.All).Address.ColumnNumber; + SortingRangeColumns(1, maxColumn); + } + + private void SwapColumns(Int32 column1, Int32 column2) + { + int col1InWs = RangeAddress.FirstAddress.ColumnNumber + column1 - 1; + int col2InWs = RangeAddress.FirstAddress.ColumnNumber + column2 - 1; + + Int32 firstRow = RangeAddress.FirstAddress.RowNumber; + Int32 lastRow = RangeAddress.LastAddress.RowNumber; + + var range1Sp1 = new XLSheetPoint(firstRow, col1InWs); + var range1Sp2 = new XLSheetPoint(lastRow, col1InWs); + var range2Sp1 = new XLSheetPoint(firstRow, col2InWs); + var range2Sp2 = new XLSheetPoint(lastRow, col2InWs); + + Worksheet.Internals.CellsCollection.SwapRanges(new XLSheetRange(range1Sp1, range1Sp2), + new XLSheetRange(range2Sp1, range2Sp2), Worksheet); + } + + private int SortRangeColumns(int begPoint, int endPoint) + { + int pivot = begPoint; + int m = begPoint + 1; + int n = endPoint; + while ((m < endPoint) && ColumnQuick(pivot).CompareTo((ColumnQuick(m)), SortRows) >= 0) + m++; + + while ((n > begPoint) && ((ColumnQuick(pivot)).CompareTo((ColumnQuick(n)), SortRows) <= 0)) + n--; + while (m < n) + { + SwapColumns(m, n); + + while ((m < endPoint) && (ColumnQuick(pivot)).CompareTo((ColumnQuick(m)), SortRows) >= 0) + m++; + + while ((n > begPoint) && (ColumnQuick(pivot)).CompareTo((ColumnQuick(n)), SortRows) <= 0) + n--; + } + if (pivot != n) + SwapColumns(n, pivot); + return n; + } + + private void SortingRangeColumns(int beg, int end) + { + if (end == beg) + return; + int pivot = SortRangeColumns(beg, end); + if (pivot > beg) + SortingRangeColumns(beg, pivot - 1); + if (pivot < end) + SortingRangeColumns(pivot + 1, end); + } + + #endregion Sort Columns + + #endregion Sort + + public XLRangeColumn ColumnQuick(Int32 column) + { + var firstCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber + column - 1, + false, + false); + var lastCellAddress = new XLAddress(Worksheet, + RangeAddress.LastAddress.RowNumber, + RangeAddress.FirstAddress.ColumnNumber + column - 1, + false, + false); + return Worksheet.RangeColumn(new XLRangeAddress(firstCellAddress, lastCellAddress)); + } + + public XLRangeRow RowQuick(Int32 row) + { + var firstCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber + row - 1, + RangeAddress.FirstAddress.ColumnNumber, + false, + false); + var lastCellAddress = new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber + row - 1, + RangeAddress.LastAddress.ColumnNumber, + false, + false); + + return Worksheet.RangeRow(new XLRangeAddress(firstCellAddress, lastCellAddress)); + } + + public IXLDataValidation SetDataValidation() + { + var existingValidation = GetDataValidation(); + if (existingValidation != null && existingValidation.Ranges.Any(r => r == this)) + return existingValidation; + + IXLDataValidation dataValidationToCopy = Worksheet.DataValidations.GetAllInRange(RangeAddress) + .FirstOrDefault(); + + var newRange = AsRange(); + var dataValidation = new XLDataValidation(newRange); + if (dataValidationToCopy != null) + dataValidation.CopyFrom(dataValidationToCopy); + + Worksheet.DataValidations.Add(dataValidation); + return dataValidation; + } + + public IXLConditionalFormat AddConditionalFormat() + { + var cf = new XLConditionalFormat(AsRange()); + Worksheet.ConditionalFormats.Add(cf); + return cf; + } + + internal IXLConditionalFormat AddConditionalFormat(IXLConditionalFormat source) + { + var cf = new XLConditionalFormat(AsRange()); + cf.CopyFrom(source); + Worksheet.ConditionalFormats.Add(cf); + return cf; + } + + public void Select() + { + Worksheet.SelectedRanges.Add(AsRange()); + } + + public IXLRangeBase Grow() + { + return Grow(1); + } + + public IXLRangeBase Grow(int growCount) + { + var firstRow = Math.Max(1, this.RangeAddress.FirstAddress.RowNumber - growCount); + var firstColumn = Math.Max(1, this.RangeAddress.FirstAddress.ColumnNumber - growCount); + + var lastRow = Math.Min(XLHelper.MaxRowNumber, this.RangeAddress.LastAddress.RowNumber + growCount); + var lastColumn = Math.Min(XLHelper.MaxColumnNumber, this.RangeAddress.LastAddress.ColumnNumber + growCount); + + return this.Worksheet.Range(firstRow, firstColumn, lastRow, lastColumn); + } + + public IXLRangeBase Shrink() + { + return Shrink(1); + } + + public IXLRangeBase Shrink(int shrinkCount) + { + var firstRow = this.RangeAddress.FirstAddress.RowNumber + shrinkCount; + var firstColumn = this.RangeAddress.FirstAddress.ColumnNumber + shrinkCount; + + var lastRow = this.RangeAddress.LastAddress.RowNumber - shrinkCount; + var lastColumn = this.RangeAddress.LastAddress.ColumnNumber - shrinkCount; + + if (firstRow > lastRow || firstColumn > lastColumn) + return null; + + return this.Worksheet.Range(firstRow, firstColumn, lastRow, lastColumn); + } + + public IXLRangeAddress Intersection(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null) + { + if (otherRange == null) + return null; + + if (!this.Worksheet.Equals(otherRange.Worksheet)) + return null; + + if (thisRangePredicate == null && otherRangePredicate == null) + { + // Special case, no predicates. We can optimise this a bit then. + return this.RangeAddress.Intersection(otherRange.RangeAddress); + } + else + { + thisRangePredicate = thisRangePredicate ?? (c => true); + otherRangePredicate = otherRangePredicate ?? (c => true); + + var intersectionCells = this.Cells(c => thisRangePredicate(c) && otherRange.Cells(otherRangePredicate).Contains(c)); + + if (!intersectionCells.Any()) + return null; + + var firstRow = intersectionCells.Min(c => c.Address.RowNumber); + var firstColumn = intersectionCells.Min(c => c.Address.ColumnNumber); + + var lastRow = intersectionCells.Max(c => c.Address.RowNumber); + var lastColumn = intersectionCells.Max(c => c.Address.ColumnNumber); + + return new XLRangeAddress + ( + new XLAddress(this.Worksheet, firstRow, firstColumn, fixedRow: false, fixedColumn: false), + new XLAddress(this.Worksheet, lastRow, lastColumn, fixedRow: false, fixedColumn: false) + ); + } + } + + public IXLCells SurroundingCells(Func predicate = null) + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents, predicate); + this.Grow().Cells(c => !this.Contains(c)).ForEach(c => cells.Add(c as XLCell)); + return cells; + } + + public IXLCells Union(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null) + { + if (otherRange == null) + return this.Cells(thisRangePredicate); + + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + if (!this.Worksheet.Equals(otherRange.Worksheet)) + return cells; + + if (thisRangePredicate == null) thisRangePredicate = c => true; + if (otherRangePredicate == null) otherRangePredicate = c => true; + + this.Cells(thisRangePredicate).Concat(otherRange.Cells(otherRangePredicate)).Distinct().ForEach(c => cells.Add(c as XLCell)); + return cells; + } + + public IXLCells Difference(IXLRangeBase otherRange, Func thisRangePredicate = null, Func otherRangePredicate = null) + { + if (otherRange == null) + return this.Cells(thisRangePredicate); + + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + if (!this.Worksheet.Equals(otherRange.Worksheet)) + return cells; + + if (thisRangePredicate == null) thisRangePredicate = c => true; + if (otherRangePredicate == null) otherRangePredicate = c => true; + + this.Cells(c => thisRangePredicate(c) && !otherRange.Cells(otherRangePredicate).Contains(c)).ForEach(c => cells.Add(c as XLCell)); + return cells; + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeColumn.cs b/ClosedXML/Excel/Ranges/XLRangeColumn.cs new file mode 100644 index 0000000..97ee3dd --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeColumn.cs @@ -0,0 +1,392 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLRangeColumn : XLRangeBase, IXLRangeColumn + { + #region Constructor + + /// + /// The direct contructor should only be used in . + /// + public XLRangeColumn(XLRangeParameters rangeParameters) + : base(rangeParameters.RangeAddress, (rangeParameters.DefaultStyle as XLStyle).Value) + { + } + + #endregion Constructor + + #region IXLRangeColumn Members + + IXLCell IXLRangeColumn.Cell(int rowNumber) + { + return Cell(rowNumber); + } + + public override IXLCells Cells(string cellsInColumn) + { + var retVal = new XLCells(false, XLCellsUsedOptions.AllContents); + var rangePairs = cellsInColumn.Split(','); + foreach (string pair in rangePairs) + retVal.Add(Range(pair.Trim()).RangeAddress); + return retVal; + } + + public IXLCells Cells(int firstRow, int lastRow) + { + return Cells(firstRow + ":" + lastRow); + } + + public void Delete() + { + Delete(true); + } + + internal void Delete(Boolean deleteTableField) + { + if (deleteTableField && IsTableColumn()) + { + var table = Table as XLTable; + var firstCellValue = Cell(1).Value.ToString(); + if (!table.FieldNames.ContainsKey(firstCellValue)) + throw new ArgumentException(string.Format("Field {0} not found.", firstCellValue)); + + var field = table.Fields.Cast().Single(f => f.Name == firstCellValue); + field.Delete(false); + } + + Delete(XLShiftDeletedCells.ShiftCellsLeft); + } + + public IXLCells InsertCellsAbove(int numberOfRows) + { + return InsertCellsAbove(numberOfRows, false); + } + + public IXLCells InsertCellsAbove(int numberOfRows, bool expandRange) + { + return InsertRowsAbove(numberOfRows, expandRange).Cells(); + } + + public IXLCells InsertCellsBelow(int numberOfRows) + { + return InsertCellsBelow(numberOfRows, true); + } + + public IXLCells InsertCellsBelow(int numberOfRows, bool expandRange) + { + return InsertRowsBelow(numberOfRows, expandRange).Cells(); + } + + public int CellCount() + { + return RangeAddress.LastAddress.RowNumber - RangeAddress.FirstAddress.RowNumber + 1; + } + + public IXLRangeColumn Sort(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + base.Sort(1, sortOrder, matchCase, ignoreBlanks); + return this; + } + + public new IXLRangeColumn CopyTo(IXLCell target) + { + base.CopyTo(target); + + int lastRowNumber = target.Address.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + int lastColumnNumber = target.Address.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range( + target.Address.RowNumber, + target.Address.ColumnNumber, + lastRowNumber, + lastColumnNumber) + .Column(1); + } + + public new IXLRangeColumn CopyTo(IXLRangeBase target) + { + base.CopyTo(target); + + int lastRowNumber = target.RangeAddress.FirstAddress.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + int lastColumnNumber = target.RangeAddress.FirstAddress.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range( + target.RangeAddress.FirstAddress.RowNumber, + target.RangeAddress.FirstAddress.ColumnNumber, + lastRowNumber, + lastColumnNumber) + .Column(1); + } + + public IXLRangeColumn Column(int start, int end) + { + return Range(start, end).FirstColumn(); + } + + public IXLRangeColumn Column(IXLCell start, IXLCell end) + { + return Column(start.Address.RowNumber, end.Address.RowNumber); + } + + public IXLRangeColumns Columns(string columns) + { + var retVal = new XLRangeColumns(); + var rowPairs = columns.Split(','); + foreach (string trimmedPair in rowPairs.Select(pair => pair.Trim())) + { + string firstRow; + string lastRow; + if (trimmedPair.Contains(':') || trimmedPair.Contains('-')) + { + var rowRange = trimmedPair.Split(':', '-'); + + firstRow = rowRange[0]; + lastRow = rowRange[1]; + } + else + { + firstRow = trimmedPair; + lastRow = trimmedPair; + } + + retVal.Add(Range(firstRow, lastRow).FirstColumn()); + } + + return retVal; + } + + public IXLRangeColumn SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + public IXLColumn WorksheetColumn() + { + return Worksheet.Column(RangeAddress.FirstAddress.ColumnNumber); + } + + #endregion IXLRangeColumn Members + + public override XLRangeType RangeType + { + get { return XLRangeType.RangeColumn; } + } + + public XLCell Cell(int row) + { + return Cell(row, 1); + } + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + RangeAddress = (XLRangeAddress)ShiftColumns(RangeAddress, range, columnsShifted); + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + RangeAddress = (XLRangeAddress)ShiftRows(RangeAddress, range, rowsShifted); + } + + public XLRange Range(int firstRow, int lastRow) + { + return Range(firstRow, 1, lastRow, 1); + } + + public override XLRange Range(string rangeAddressStr) + { + string rangeAddressToUse; + if (rangeAddressStr.Contains(':') || rangeAddressStr.Contains('-')) + { + if (rangeAddressStr.Contains('-')) + rangeAddressStr = rangeAddressStr.Replace('-', ':'); + + var arrRange = rangeAddressStr.Split(':'); + string firstPart = arrRange[0]; + string secondPart = arrRange[1]; + rangeAddressToUse = FixColumnAddress(firstPart) + ":" + FixColumnAddress(secondPart); + } + else + rangeAddressToUse = FixColumnAddress(rangeAddressStr); + + var rangeAddress = new XLRangeAddress(Worksheet, rangeAddressToUse); + return Range(rangeAddress); + } + + public int CompareTo(XLRangeColumn otherColumn, IXLSortElements rowsToSort) + { + foreach (IXLSortElement e in rowsToSort) + { + var thisCell = Cell(e.ElementNumber); + var otherCell = otherColumn.Cell(e.ElementNumber); + int comparison; + bool thisCellIsBlank = thisCell.IsEmpty(); + bool otherCellIsBlank = otherCell.IsEmpty(); + if (e.IgnoreBlanks && (thisCellIsBlank || otherCellIsBlank)) + { + if (thisCellIsBlank && otherCellIsBlank) + comparison = 0; + else + { + if (thisCellIsBlank) + comparison = e.SortOrder == XLSortOrder.Ascending ? 1 : -1; + else + comparison = e.SortOrder == XLSortOrder.Ascending ? -1 : 1; + } + } + else + { + if (thisCell.DataType == otherCell.DataType) + { + if (thisCell.DataType == XLDataType.Text) + { + comparison = e.MatchCase + ? thisCell.InnerText.CompareTo(otherCell.InnerText) + : String.Compare(thisCell.InnerText, otherCell.InnerText, true); + } + else if (thisCell.DataType == XLDataType.TimeSpan) + comparison = thisCell.GetTimeSpan().CompareTo(otherCell.GetTimeSpan()); + else + comparison = Double.Parse(thisCell.InnerText, XLHelper.NumberStyle, XLHelper.ParseCulture).CompareTo(Double.Parse(otherCell.InnerText, XLHelper.NumberStyle, XLHelper.ParseCulture)); + } + else if (e.MatchCase) + comparison = String.Compare(thisCell.GetString(), otherCell.GetString(), true); + else + comparison = thisCell.GetString().CompareTo(otherCell.GetString()); + } + + if (comparison != 0) + return e.SortOrder == XLSortOrder.Ascending ? comparison : comparison * -1; + } + + return 0; + } + + private XLRangeColumn ColumnShift(Int32 columnsToShift) + { + Int32 columnNumber = ColumnNumber() + columnsToShift; + return Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, + columnNumber, + RangeAddress.LastAddress.RowNumber, + columnNumber).FirstColumn(); + } + + #region XLRangeColumn Left + + IXLRangeColumn IXLRangeColumn.ColumnLeft() + { + return ColumnLeft(); + } + + IXLRangeColumn IXLRangeColumn.ColumnLeft(Int32 step) + { + return ColumnLeft(step); + } + + public XLRangeColumn ColumnLeft() + { + return ColumnLeft(1); + } + + public XLRangeColumn ColumnLeft(Int32 step) + { + return ColumnShift(step * -1); + } + + #endregion XLRangeColumn Left + + #region XLRangeColumn Right + + IXLRangeColumn IXLRangeColumn.ColumnRight() + { + return ColumnRight(); + } + + IXLRangeColumn IXLRangeColumn.ColumnRight(Int32 step) + { + return ColumnRight(step); + } + + public XLRangeColumn ColumnRight() + { + return ColumnRight(1); + } + + public XLRangeColumn ColumnRight(Int32 step) + { + return ColumnShift(step); + } + + #endregion XLRangeColumn Right + + public IXLTable AsTable() + { + if (IsTableColumn()) + throw new InvalidOperationException("This column is already part of a table."); + + return AsRange().AsTable(); + } + + public IXLTable AsTable(string name) + { + if (IsTableColumn()) + throw new InvalidOperationException("This column is already part of a table."); + + return AsRange().AsTable(name); + } + + public IXLTable CreateTable() + { + if (IsTableColumn()) + throw new InvalidOperationException("This column is already part of a table."); + + return AsRange().CreateTable(); + } + + public IXLTable CreateTable(string name) + { + if (IsTableColumn()) + throw new InvalidOperationException("This column is already part of a table."); + + return AsRange().CreateTable(name); + } + + public new IXLRangeColumn Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRangeColumn ColumnUsed(Boolean includeFormats) + { + return ColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLRangeColumn ColumnUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents) + { + return Column((this as IXLRangeBase).FirstCellUsed(options), + (this as IXLRangeBase).LastCellUsed(options)); + } + + internal IXLTable Table { get; set; } + + public Boolean IsTableColumn() + { + return Table != null; + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeColumns.cs b/ClosedXML/Excel/Ranges/XLRangeColumns.cs new file mode 100644 index 0000000..cedf4a1 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeColumns.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLRangeColumns : XLStylizedBase, IXLRangeColumns, IXLStylized + { + private readonly List _ranges = new List(); + + public XLRangeColumns() : base(XLWorkbook.DefaultStyleValue) + { + } + + #region IXLRangeColumns Members + + public IXLRangeColumns Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _ranges.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void Delete() + { + _ranges.OrderByDescending(c => c.ColumnNumber()).ForEach(r => r.Delete()); + _ranges.Clear(); + } + + public void Add(IXLRangeColumn range) + { + _ranges.Add((XLRangeColumn)range); + } + + public IEnumerator GetEnumerator() + { + return _ranges.Cast() + .OrderBy(r => r.Worksheet.Position) + .ThenBy(r => r.ColumnNumber()) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLCells Cells() + { + var cells = new XLCells(usedCellsOnly: false, options: XLCellsUsedOptions.AllContents); + foreach (XLRangeColumn container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(usedCellsOnly: true, options: XLCellsUsedOptions.AllContents); + foreach (XLRangeColumn container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents + ); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(usedCellsOnly: true, options: options); + foreach (XLRangeColumn container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLRangeColumns SetDataType(XLDataType dataType) + { + _ranges.ForEach(c => c.DataType = dataType); + return this; + } + + #endregion IXLRangeColumns Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + foreach (XLRangeColumn rng in _ranges) + { + yield return rng.Style; + foreach (XLCell r in rng.Worksheet.Internals.CellsCollection.GetCells( + rng.RangeAddress.FirstAddress.RowNumber, + rng.RangeAddress.FirstAddress.ColumnNumber, + rng.RangeAddress.LastAddress.RowNumber, + rng.RangeAddress.LastAddress.ColumnNumber)) + yield return r.Style; + } + } + } + + protected override IEnumerable Children + { + get + { + foreach (var range in _ranges) + yield return range; + } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + public void Select() + { + foreach (var range in this) + range.Select(); + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeConsolidationEngine.cs b/ClosedXML/Excel/Ranges/XLRangeConsolidationEngine.cs new file mode 100644 index 0000000..8eb04a0 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeConsolidationEngine.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + /// + /// Engine for ranges consolidation. Supports IXLRanges including ranges from either one or multiple worksheets. + /// + internal class XLRangeConsolidationEngine + { + #region Public Constructors + + public XLRangeConsolidationEngine(IXLRanges ranges) + { + if (ranges == null) + throw new ArgumentNullException(nameof(ranges)); + _allRanges = ranges; + } + + #endregion Public Constructors + + #region Public Methods + + public IXLRanges Consolidate() + { + if (!_allRanges.Any()) + return _allRanges; + + var worksheets = _allRanges.Select(r => r.Worksheet).Distinct().OrderBy(ws => ws.Position); + + IXLRanges retVal = new XLRanges(); + foreach (var ws in worksheets) + { + var matrix = new XLRangeConsolidationMatrix(ws, _allRanges.Where(r => r.Worksheet == ws)); + var consRanges = matrix.GetConsolidatedRanges(); + foreach (var consRange in consRanges) + { + retVal.Add(consRange); + } + } + + return retVal; + } + + #endregion Public Methods + + #region Private Fields + + private readonly IXLRanges _allRanges; + + #endregion Private Fields + + #region Private Classes + + /// + /// Class representing the area covering ranges to be consolidated as a set of bit matrices. Does all the dirty job + /// of ranges consolidation. + /// + private class XLRangeConsolidationMatrix + { + #region Public Constructors + + /// + /// Constructor. + /// + /// Current worksheet. + /// Ranges to be consolidated. They are expected to belong to the current worksheet, no check is performed. + public XLRangeConsolidationMatrix(IXLWorksheet worksheet, IEnumerable ranges) + { + _worksheet = worksheet; + PrepareBitMatrix(ranges); + FillBitMatrix(ranges); + } + + #endregion Public Constructors + + #region Public Methods + + /// + /// Get consolidated ranges equivalent to the input ones. + /// + public IEnumerable GetConsolidatedRanges() + { + var rowNumbers = _bitMatrix.Keys.OrderBy(k => k).ToArray(); + for (int i = 0; i < rowNumbers.Length; i++) + { + var startRow = rowNumbers[i]; + var startings = GetRangesBoundariesStartingByRow(_bitMatrix[startRow]); + + foreach (var starting in startings) + { + int j = i + 1; + while (j < rowNumbers.Length && RowIncludesRange(_bitMatrix[rowNumbers[j]], starting)) j++; + + var endRow = rowNumbers[j - 1]; + var startColumn = starting.Item1 + _minColumn - 1; + var endColumn = starting.Item2 + _minColumn - 1; + + yield return _worksheet.Range(startRow, startColumn, endRow, endColumn); + + while (j > i) + { + ClearRangeInRow(_bitMatrix[rowNumbers[j - 1]], starting); + j--; + } + } + } + } + + #endregion Public Methods + + #region Private Fields + + private readonly IXLWorksheet _worksheet; + private Dictionary _bitMatrix; + private int _maxColumn = 0; + private int _minColumn = XLHelper.MaxColumnNumber + 1; + + #endregion Private Fields + + #region Private Methods + + private void AddToBitMatrix(IXLRangeAddress rangeAddress) + { + var rows = _bitMatrix.Keys + .Where(k => k >= rangeAddress.FirstAddress.RowNumber && + k <= rangeAddress.LastAddress.RowNumber); + + var minIndex = rangeAddress.FirstAddress.ColumnNumber - _minColumn + 1; + var maxIndex = rangeAddress.LastAddress.ColumnNumber - _minColumn + 1; + + foreach (var rowNum in rows) + { + for (int i = minIndex; i <= maxIndex; i++) + { + _bitMatrix[rowNum][i] = true; + } + } + } + + private void ClearRangeInRow(BitArray rowArray, Tuple rangeBoundaries) + { + for (int i = rangeBoundaries.Item1; i <= rangeBoundaries.Item2; i++) + { + rowArray[i] = false; + } + } + + private void FillBitMatrix(IEnumerable ranges) + { + foreach (var range in ranges) + { + AddToBitMatrix(range.RangeAddress); + } + + System.Diagnostics.Debug.Assert( + _bitMatrix.Values.All(r => r[0] == false && r[r.Length - 1] == false)); + } + + private IEnumerable> GetRangesBoundariesStartingByRow(BitArray rowArray) + { + int startIdx = 0; + for (int i = 1; i < rowArray.Length - 1; i++) + { + if (!rowArray[i - 1] && rowArray[i]) + startIdx = i; + if (rowArray[i] && !rowArray[i + 1]) + yield return new Tuple(startIdx, i); + } + } + + private void PrepareBitMatrix(IEnumerable ranges) + { + _bitMatrix = new Dictionary(); + foreach (var range in ranges) + { + var address = range.RangeAddress; + _minColumn = (_minColumn <= address.FirstAddress.ColumnNumber) + ? _minColumn + : address.FirstAddress.ColumnNumber; + _maxColumn = (_maxColumn >= address.LastAddress.ColumnNumber) + ? _maxColumn + : address.LastAddress.ColumnNumber; + + if (!_bitMatrix.ContainsKey(address.FirstAddress.RowNumber)) + _bitMatrix.Add(address.FirstAddress.RowNumber, null); + if (!_bitMatrix.ContainsKey(address.LastAddress.RowNumber)) + _bitMatrix.Add(address.LastAddress.RowNumber, null); + if (!_bitMatrix.ContainsKey(address.LastAddress.RowNumber + 1)) + _bitMatrix.Add(address.LastAddress.RowNumber + 1, null); + } + + var keys = _bitMatrix.Keys.ToList(); + foreach (var rowNum in keys) + { + _bitMatrix[rowNum] = new BitArray(_maxColumn - _minColumn + 3, false); + } + } + private bool RowIncludesRange(BitArray rowArray, Tuple rangeBoundaries) + { + for (int i = rangeBoundaries.Item1; i <= rangeBoundaries.Item2; i++) + { + if (!rowArray[i]) + return false; + } + + return true; + } + + #endregion Private Methods + } + + #endregion Private Classes + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeFactory.cs b/ClosedXML/Excel/Ranges/XLRangeFactory.cs new file mode 100644 index 0000000..b3ded64 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeFactory.cs @@ -0,0 +1,89 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLRangeFactory + { + #region Properties + + public XLWorksheet Worksheet { get; private set; } + + #endregion Properties + + #region Constructors + + public XLRangeFactory(XLWorksheet worksheet) + { + if (worksheet == null) + throw new ArgumentNullException(nameof(worksheet)); + Worksheet = worksheet; + } + + #endregion Constructors + + #region Methods + + public XLRangeBase Create(XLRangeKey key) + { + switch (key.RangeType) + { + case XLRangeType.Range: + return CreateRange(key.RangeAddress); + + case XLRangeType.Column: + return CreateColumn(key.RangeAddress.FirstAddress.ColumnNumber); + + case XLRangeType.Row: + return CreateColumn(key.RangeAddress.FirstAddress.RowNumber); + + case XLRangeType.RangeColumn: + return CreateRangeColumn(key.RangeAddress); + + case XLRangeType.RangeRow: + return CreateRangeRow(key.RangeAddress); + + case XLRangeType.Table: + return CreateTable(key.RangeAddress); + + case XLRangeType.Worksheet: + default: + throw new NotImplementedException(key.RangeType.ToString()); + } + } + + public XLRange CreateRange(XLRangeAddress rangeAddress) + { + var xlRangeParameters = new XLRangeParameters(rangeAddress, Worksheet.Style); + return new XLRange(xlRangeParameters); + } + + public XLColumn CreateColumn(int columnNumber) + { + return new XLColumn(Worksheet, columnNumber); + } + + public XLRow CreateRow(int rowNumber) + { + return new XLRow(Worksheet, rowNumber); + } + + public XLRangeColumn CreateRangeColumn(XLRangeAddress rangeAddress) + { + var xlRangeParameters = new XLRangeParameters(rangeAddress, Worksheet.Style); + return new XLRangeColumn(xlRangeParameters); + } + + public XLRangeRow CreateRangeRow(XLRangeAddress rangeAddress) + { + var xlRangeParameters = new XLRangeParameters(rangeAddress, Worksheet.Style); + return new XLRangeRow(xlRangeParameters); + } + + public XLTable CreateTable(XLRangeAddress rangeAddress) + { + return new XLTable(new XLRangeParameters(rangeAddress, Worksheet.Style)); + } + + #endregion Methods + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeKey.cs b/ClosedXML/Excel/Ranges/XLRangeKey.cs new file mode 100644 index 0000000..8fb4842 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeKey.cs @@ -0,0 +1,60 @@ +using System; + +namespace ClosedXML.Excel +{ + [System.Diagnostics.DebuggerDisplay("{RangeType} {RangeAddress}")] + internal struct XLRangeKey : IEquatable + { + public XLRangeType RangeType { get; private set; } + + public XLRangeAddress RangeAddress { get; private set; } + + public XLRangeKey(XLRangeType rangeType, XLRangeAddress address) + { + RangeType = rangeType; + RangeAddress = address; + } + + #region Overrides + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(XLRangeKey other) + { + return RangeType == other.RangeType && + RangeAddress.Equals(other.RangeAddress); + } + + /// Indicates whether this instance and a specified object are equal. + /// true if and this instance are the same type and represent the same value; otherwise, false. + /// Another object to compare to. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is XLRangeKey && Equals((XLRangeKey)obj); + } + + /// Returns the hash code for this instance. + /// A 32-bit signed integer that is the hash code for this instance. + public override int GetHashCode() + { + unchecked + { + return ((int)RangeType * 397) ^ RangeAddress.GetHashCode(); + } + } + + public static bool operator ==(XLRangeKey left, XLRangeKey right) + { + return left.Equals(right); + } + + public static bool operator !=(XLRangeKey left, XLRangeKey right) + { + return !(left == right); + } + + #endregion Overrides + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeParameters.cs b/ClosedXML/Excel/Ranges/XLRangeParameters.cs new file mode 100644 index 0000000..2c93bab --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeParameters.cs @@ -0,0 +1,22 @@ +namespace ClosedXML.Excel +{ + internal class XLRangeParameters + { + #region Constructor + + public XLRangeParameters(XLRangeAddress rangeAddress, IXLStyle defaultStyle) + { + RangeAddress = rangeAddress; + DefaultStyle = defaultStyle; + } + + #endregion + + #region Properties + + public XLRangeAddress RangeAddress { get; private set; } + + public IXLStyle DefaultStyle { get; private set; } + #endregion + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeRow.cs b/ClosedXML/Excel/Ranges/XLRangeRow.cs new file mode 100644 index 0000000..f8d0de9 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeRow.cs @@ -0,0 +1,372 @@ +namespace ClosedXML.Excel +{ + using System; + using System.Linq; + + internal class XLRangeRow : XLRangeBase, IXLRangeRow + { + #region Constructor + + /// + /// The direct contructor should only be used in . + /// + public XLRangeRow(XLRangeParameters rangeParameters) + : base(rangeParameters.RangeAddress, (rangeParameters.DefaultStyle as XLStyle).Value) + { + } + + #endregion Constructor + + #region IXLRangeRow Members + + public IXLCell Cell(int column) + { + return Cell(1, column); + } + + public override XLCell Cell(string columnLetter) + { + return Cell(1, columnLetter); + } + + IXLCell IXLRangeRow.Cell(string columnLetter) + { + return Cell(columnLetter); + } + + public void Delete() + { + Delete(XLShiftDeletedCells.ShiftCellsUp); + } + + public IXLCells InsertCellsAfter(int numberOfColumns) + { + return InsertCellsAfter(numberOfColumns, true); + } + + public IXLCells InsertCellsAfter(int numberOfColumns, bool expandRange) + { + return InsertColumnsAfter(numberOfColumns, expandRange).Cells(); + } + + public IXLCells InsertCellsBefore(int numberOfColumns) + { + return InsertCellsBefore(numberOfColumns, false); + } + + public IXLCells InsertCellsBefore(int numberOfColumns, bool expandRange) + { + return InsertColumnsBefore(numberOfColumns, expandRange).Cells(); + } + + public override IXLCells Cells(string cellsInRow) + { + var retVal = new XLCells(false, XLCellsUsedOptions.AllContents); + var rangePairs = cellsInRow.Split(','); + foreach (string pair in rangePairs) + retVal.Add(Range(pair.Trim()).RangeAddress); + return retVal; + } + + public IXLCells Cells(int firstColumn, int lastColumn) + { + return Cells(firstColumn + ":" + lastColumn); + } + + public IXLCells Cells(string firstColumn, string lastColumn) + { + return Cells(XLHelper.GetColumnNumberFromLetter(firstColumn) + ":" + + XLHelper.GetColumnNumberFromLetter(lastColumn)); + } + + public int CellCount() + { + return RangeAddress.LastAddress.ColumnNumber - RangeAddress.FirstAddress.ColumnNumber + 1; + } + + public new IXLRangeRow Sort() + { + return SortLeftToRight(); + } + + public new IXLRangeRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + base.SortLeftToRight(sortOrder, matchCase, ignoreBlanks); + return this; + } + + public new IXLRangeRow CopyTo(IXLCell target) + { + base.CopyTo(target); + + int lastRowNumber = target.Address.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + int lastColumnNumber = target.Address.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range( + target.Address.RowNumber, + target.Address.ColumnNumber, + lastRowNumber, + lastColumnNumber) + .Row(1); + } + + public new IXLRangeRow CopyTo(IXLRangeBase target) + { + base.CopyTo(target); + int lastRowNumber = target.RangeAddress.FirstAddress.RowNumber + RowCount() - 1; + if (lastRowNumber > XLHelper.MaxRowNumber) + lastRowNumber = XLHelper.MaxRowNumber; + int lastColumnNumber = target.RangeAddress.LastAddress.ColumnNumber + ColumnCount() - 1; + if (lastColumnNumber > XLHelper.MaxColumnNumber) + lastColumnNumber = XLHelper.MaxColumnNumber; + + return target.Worksheet.Range( + target.RangeAddress.FirstAddress.RowNumber, + target.RangeAddress.LastAddress.ColumnNumber, + lastRowNumber, + lastColumnNumber) + .Row(1); + } + + public IXLRangeRow Row(int start, int end) + { + return Range(1, start, 1, end).Row(1); + } + + public IXLRangeRow Row(IXLCell start, IXLCell end) + { + return Row(start.Address.ColumnNumber, end.Address.ColumnNumber); + } + + public IXLRangeRows Rows(string rows) + { + var retVal = new XLRangeRows(); + var columnPairs = rows.Split(','); + foreach (string trimmedPair in columnPairs.Select(pair => pair.Trim())) + { + string firstColumn; + string lastColumn; + if (trimmedPair.Contains(':') || trimmedPair.Contains('-')) + { + var columnRange = trimmedPair.Contains('-') + ? trimmedPair.Replace('-', ':').Split(':') + : trimmedPair.Split(':'); + firstColumn = columnRange[0]; + lastColumn = columnRange[1]; + } + else + { + firstColumn = trimmedPair; + lastColumn = trimmedPair; + } + + retVal.Add(Range(firstColumn, lastColumn).FirstRow()); + } + + return retVal; + } + + public IXLRangeRow SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + public IXLRow WorksheetRow() + { + return Worksheet.Row(RangeAddress.FirstAddress.RowNumber); + } + + #endregion IXLRangeRow Members + public override XLRangeType RangeType + { + get { return XLRangeType.RangeRow; } + } + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + RangeAddress = (XLRangeAddress)ShiftColumns(RangeAddress, range, columnsShifted); + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + RangeAddress = (XLRangeAddress)ShiftRows(RangeAddress, range, rowsShifted); + } + + public IXLRange Range(int firstColumn, int lastColumn) + { + return Range(1, firstColumn, 1, lastColumn); + } + + public override XLRange Range(string rangeAddressStr) + { + string rangeAddressToUse; + if (rangeAddressStr.Contains(':') || rangeAddressStr.Contains('-')) + { + if (rangeAddressStr.Contains('-')) + rangeAddressStr = rangeAddressStr.Replace('-', ':'); + + var arrRange = rangeAddressStr.Split(':'); + string firstPart = arrRange[0]; + string secondPart = arrRange[1]; + rangeAddressToUse = FixRowAddress(firstPart) + ":" + FixRowAddress(secondPart); + } + else + rangeAddressToUse = FixRowAddress(rangeAddressStr); + + var rangeAddress = new XLRangeAddress(Worksheet, rangeAddressToUse); + return Range(rangeAddress); + } + + public int CompareTo(XLRangeRow otherRow, IXLSortElements columnsToSort) + { + foreach (IXLSortElement e in columnsToSort) + { + var thisCell = (XLCell)Cell(e.ElementNumber); + var otherCell = (XLCell)otherRow.Cell(e.ElementNumber); + int comparison; + bool thisCellIsBlank = thisCell.IsEmpty(); + bool otherCellIsBlank = otherCell.IsEmpty(); + if (e.IgnoreBlanks && (thisCellIsBlank || otherCellIsBlank)) + { + if (thisCellIsBlank && otherCellIsBlank) + comparison = 0; + else + { + if (thisCellIsBlank) + comparison = e.SortOrder == XLSortOrder.Ascending ? 1 : -1; + else + comparison = e.SortOrder == XLSortOrder.Ascending ? -1 : 1; + } + } + else + { + if (thisCell.DataType == otherCell.DataType) + { + switch (thisCell.DataType) + { + case XLDataType.Text: + comparison = e.MatchCase + ? thisCell.InnerText.CompareTo(otherCell.InnerText) + : String.Compare(thisCell.InnerText, otherCell.InnerText, true); + break; + + case XLDataType.TimeSpan: + comparison = thisCell.GetTimeSpan().CompareTo(otherCell.GetTimeSpan()); + break; + + case XLDataType.DateTime: + comparison = thisCell.GetDateTime().CompareTo(otherCell.GetDateTime()); + break; + + case XLDataType.Number: + comparison = thisCell.GetDouble().CompareTo(otherCell.GetDouble()); + break; + + case XLDataType.Boolean: + comparison = thisCell.GetBoolean().CompareTo(otherCell.GetBoolean()); + break; + + default: + throw new NotImplementedException(); + } + } + else if (e.MatchCase) + comparison = String.Compare(thisCell.GetString(), otherCell.GetString(), true); + else + comparison = thisCell.GetString().CompareTo(otherCell.GetString()); + } + + if (comparison != 0) + return e.SortOrder == XLSortOrder.Ascending ? comparison : -comparison; + } + + return 0; + } + + private XLRangeRow RowShift(Int32 rowsToShift) + { + Int32 rowNum = RowNumber() + rowsToShift; + + var range = Worksheet.Range( + rowNum, + RangeAddress.FirstAddress.ColumnNumber, + rowNum, + RangeAddress.LastAddress.ColumnNumber); + + return range.FirstRow(); + } + + #region XLRangeRow Above + + IXLRangeRow IXLRangeRow.RowAbove() + { + return RowAbove(); + } + + IXLRangeRow IXLRangeRow.RowAbove(Int32 step) + { + return RowAbove(step); + } + + public XLRangeRow RowAbove() + { + return RowAbove(1); + } + + public XLRangeRow RowAbove(Int32 step) + { + return RowShift(step * -1); + } + + #endregion XLRangeRow Above + + #region XLRangeRow Below + + IXLRangeRow IXLRangeRow.RowBelow() + { + return RowBelow(); + } + + IXLRangeRow IXLRangeRow.RowBelow(Int32 step) + { + return RowBelow(step); + } + + public XLRangeRow RowBelow() + { + return RowBelow(1); + } + + public XLRangeRow RowBelow(Int32 step) + { + return RowShift(step); + } + + #endregion XLRangeRow Below + + public new IXLRangeRow Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRangeRow RowUsed(Boolean includeFormats) + { + return RowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLRangeRow RowUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents) + { + return Row((this as IXLRangeBase).FirstCellUsed(options), + (this as IXLRangeBase).LastCellUsed(options)); + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeRows.cs b/ClosedXML/Excel/Ranges/XLRangeRows.cs new file mode 100644 index 0000000..ba1e5ed --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeRows.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLRangeRows : XLStylizedBase, IXLRangeRows, IXLStylized + { + private readonly List _ranges = new List(); + + public XLRangeRows() : base(XLStyle.Default.Value) + { + } + + #region IXLRangeRows Members + + public IXLRangeRows Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _ranges.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void Delete() + { + _ranges.OrderByDescending(r => r.RowNumber()).ForEach(r => r.Delete()); + _ranges.Clear(); + } + + public void Add(IXLRangeRow range) + { + _ranges.Add((XLRangeRow)range); + } + + public IEnumerator GetEnumerator() + { + return _ranges.Cast() + .OrderBy(r => r.Worksheet.Position) + .ThenBy(r => r.RowNumber()) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLCells Cells() + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + foreach (XLRangeRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(true, XLCellsUsedOptions.AllContents); + foreach (XLRangeRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(true, options); + foreach (XLRangeRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLRangeRows SetDataType(XLDataType dataType) + { + _ranges.ForEach(c => c.DataType = dataType); + return this; + } + + #endregion IXLRangeRows Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + foreach (XLRangeRow rng in _ranges) + { + yield return rng.Style; + foreach (XLCell r in rng.Worksheet.Internals.CellsCollection.GetCells( + rng.RangeAddress.FirstAddress.RowNumber, + rng.RangeAddress.FirstAddress.ColumnNumber, + rng.RangeAddress.LastAddress.RowNumber, + rng.RangeAddress.LastAddress.ColumnNumber)) + yield return r.Style; + } + } + } + + protected override IEnumerable Children + { + get + { + foreach (var range in _ranges) + yield return range; + } + } + + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + public void Select() + { + foreach (var range in this) + range.Select(); + } + } +} diff --git a/ClosedXML/Excel/Ranges/XLRangeType.cs b/ClosedXML/Excel/Ranges/XLRangeType.cs new file mode 100644 index 0000000..1042d86 --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRangeType.cs @@ -0,0 +1,13 @@ +namespace ClosedXML.Excel +{ + internal enum XLRangeType : byte + { + Range = 0, + Column = 1, + Row = 2, + RangeColumn = 3, + RangeRow = 4, + Table = 5, + Worksheet = 6 + } +} diff --git a/ClosedXML/Excel/Ranges/XLRanges.cs b/ClosedXML/Excel/Ranges/XLRanges.cs new file mode 100644 index 0000000..ea2f98b --- /dev/null +++ b/ClosedXML/Excel/Ranges/XLRanges.cs @@ -0,0 +1,319 @@ +using ClosedXML.Excel.Ranges.Index; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLRanges : XLStylizedBase, IXLRanges, IXLStylized + { + /// + /// Normally, XLRanges collection includes ranges from a single worksheet, but not necessarily. + /// + private readonly Dictionary> _indexes; + private IEnumerable Ranges => _indexes.Values.SelectMany(index => index.GetAll()); + private bool _styleInitialized = false; + + private IXLRangeIndex GetRangeIndex(IXLWorksheet worksheet) + { + if (!_indexes.TryGetValue(worksheet, out IXLRangeIndex rangeIndex)) + { + rangeIndex = new XLRangeIndex(worksheet); + _indexes.Add(worksheet, rangeIndex); + } + + return rangeIndex; + } + + public XLRanges() : base(XLWorkbook.DefaultStyleValue) + { + _indexes = new Dictionary>(); + } + + #region IXLRanges Members + + public IXLRanges Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + Ranges.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void Add(XLRange range) + { + if (GetRangeIndex(range.Worksheet).Add(range)) + Count++; + + if (_styleInitialized) + return; + + var worksheetStyle = range?.Worksheet?.Style; + if (worksheetStyle == null) + return; + + InnerStyle = worksheetStyle; + _styleInitialized = true; + } + + public void Add(IXLRangeBase range) + { + Add(range.AsRange() as XLRange); + } + + public void Add(IXLCell cell) + { + Add(cell.AsRange()); + } + + public bool Remove(IXLRange range) + { + if (GetRangeIndex(range.Worksheet).Remove(range.RangeAddress)) + { + Count--; + return true; + } + + return false; + } + + /// + /// Removes ranges matching the criteria from the collection, optionally releasing their event handlers. + /// + /// Criteria to filter ranges. Only those ranges that satisfy the criteria will be removed. + /// Null means the entire collection should be cleared. + /// Specify whether or not should removed ranges be unsubscribed from + /// row/column shifting events. Until ranges are unsubscribed they cannot be collected by GC. + public void RemoveAll(Predicate match = null, bool releaseEventHandlers = true) + { + foreach (var index in _indexes.Values) + { + Count -= index.RemoveAll(match ?? (_ => true)); + } + } + + public int Count { get; private set; } + + public IEnumerator GetEnumerator() + { + return Ranges + .OrderBy(r => r.Worksheet.Position) + .ThenBy(r => r.RangeAddress.FirstAddress.RowNumber) + .ThenBy(r => r.RangeAddress.FirstAddress.ColumnNumber) + .Cast() + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Boolean Contains(IXLCell cell) + { + return GetIntersectedRanges((XLAddress)cell.Address).Any(); + } + + public Boolean Contains(IXLRange range) + { + return GetIntersectedRanges((XLRangeAddress)range.RangeAddress) + .Any(r => r.Contains(range)); + } + + /// + /// Filter ranges from a collection that intersect the specified address. Is much more efficient + /// that using Linq expression .Where(). + /// + public IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) + { + var xlRangeAddress = (XLRangeAddress)rangeAddress; + return GetIntersectedRanges(in xlRangeAddress); + } + + internal IEnumerable GetIntersectedRanges(in XLRangeAddress rangeAddress) + { + return GetRangeIndex(rangeAddress.Worksheet) + .GetIntersectedRanges(rangeAddress); + } + + /// + /// Filter ranges from a collection that intersect the specified address. Is much more efficient + /// that using Linq expression .Where(). + /// + public IEnumerable GetIntersectedRanges(IXLAddress address) + { + var xlAddress = (XLAddress)address; + return GetIntersectedRanges(in xlAddress); + } + + internal IEnumerable GetIntersectedRanges(in XLAddress address) + { + return GetRangeIndex(address.Worksheet) + .GetIntersectedRanges(address); + } + + public IEnumerable GetIntersectedRanges(IXLCell cell) + { + return GetIntersectedRanges(cell.Address); + } + + public IEnumerable DataValidation + { + get { return Ranges.Select(range => range.DataValidation).Where(dv => dv != null); } + } + + public IXLRanges AddToNamed(String rangeName) + { + return AddToNamed(rangeName, XLScope.Workbook); + } + + public IXLRanges AddToNamed(String rangeName, XLScope scope) + { + return AddToNamed(rangeName, XLScope.Workbook, null); + } + + public IXLRanges AddToNamed(String rangeName, XLScope scope, String comment) + { + Ranges.ForEach(r => r.AddToNamed(rangeName, scope, comment)); + return this; + } + + public Object Value + { + set { Ranges.ForEach(r => r.Value = value); } + } + + public IXLRanges SetValue(T value) + { + Ranges.ForEach(r => r.SetValue(value)); + return this; + } + + public IXLCells Cells() + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + foreach (XLRange container in Ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(true, XLCellsUsedOptions.AllContents); + foreach (XLRange container in Ranges) + cells.Add(container.RangeAddress); + return cells; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(true, options); + foreach (XLRange container in Ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLRanges SetDataType(XLDataType dataType) + { + Ranges.ForEach(c => c.DataType = dataType); + return this; + } + + #endregion IXLRanges Members + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + foreach (XLRange rng in Ranges) + { + yield return rng.Style; + foreach (XLCell r in rng.Worksheet.Internals.CellsCollection.GetCells( + rng.RangeAddress.FirstAddress.RowNumber, + rng.RangeAddress.FirstAddress.ColumnNumber, + rng.RangeAddress.LastAddress.RowNumber, + rng.RangeAddress.LastAddress.ColumnNumber)) + yield return r.Style; + } + } + } + + protected override IEnumerable Children + { + get + { + foreach (XLRange rng in Ranges) + yield return rng; + } + } + + public override IXLRanges RangesUsed + { + get { return this; } + } + + #endregion IXLStylized Members + + public override string ToString() + { + String retVal = Ranges.Aggregate(String.Empty, (agg, r) => agg + (r.ToString() + ",")); + if (retVal.Length > 0) retVal = retVal.Substring(0, retVal.Length - 1); + return retVal; + } + + public override bool Equals(object obj) + { + return Equals(obj as XLRanges); + } + + public bool Equals(XLRanges other) + { + if (other == null) + return false; + + return Ranges.Count() == other.Ranges.Count() && + Ranges.Select(thisRange => Enumerable.Contains(other.Ranges, thisRange)).All(foundOne => foundOne); + } + + public override int GetHashCode() + { + return Ranges.Aggregate(0, (current, r) => current ^ r.GetHashCode()); + } + + public IXLDataValidation SetDataValidation() + { + var firstRange = Ranges.First(); + var dataValidation = new XLDataValidation(firstRange); + foreach (var range in Ranges.Skip(1)) + { + dataValidation.AddRange(range); + } + + firstRange.Worksheet.DataValidations.Add(dataValidation); + return dataValidation; + } + + public void Select() + { + foreach (var range in this) + range.Select(); + } + + public IXLRanges Consolidate() + { + var engine = new XLRangeConsolidationEngine(this); + return engine.Consolidate(); + } + } +} diff --git a/ClosedXML/Excel/RichText/IXLFormattedText.cs b/ClosedXML/Excel/RichText/IXLFormattedText.cs new file mode 100644 index 0000000..3180bee --- /dev/null +++ b/ClosedXML/Excel/RichText/IXLFormattedText.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLFormattedText : IEnumerable, IEquatable>, IXLWithRichString + { + Boolean Bold { set; } + Boolean Italic { set; } + XLFontUnderlineValues Underline { set; } + Boolean Strikethrough { set; } + XLFontVerticalTextAlignmentValues VerticalAlignment { set; } + Boolean Shadow { set; } + Double FontSize { set; } + XLColor FontColor { set; } + String FontName { set; } + XLFontFamilyNumberingValues FontFamilyNumbering { set; } + + IXLFormattedText SetBold(); IXLFormattedText SetBold(Boolean value); + IXLFormattedText SetItalic(); IXLFormattedText SetItalic(Boolean value); + IXLFormattedText SetUnderline(); IXLFormattedText SetUnderline(XLFontUnderlineValues value); + IXLFormattedText SetStrikethrough(); IXLFormattedText SetStrikethrough(Boolean value); + IXLFormattedText SetVerticalAlignment(XLFontVerticalTextAlignmentValues value); + IXLFormattedText SetShadow(); IXLFormattedText SetShadow(Boolean value); + IXLFormattedText SetFontSize(Double value); + IXLFormattedText SetFontColor(XLColor value); + IXLFormattedText SetFontName(String value); + IXLFormattedText SetFontFamilyNumbering(XLFontFamilyNumberingValues value); + + IXLRichString AddText(String text, IXLFontBase font); + IXLFormattedText ClearText(); + IXLFormattedText ClearFont(); + IXLFormattedText Substring(Int32 index); + IXLFormattedText Substring(Int32 index, Int32 length); + Int32 Count { get; } + Int32 Length { get; } + + String Text { get; } + IXLPhonetics Phonetics { get; } + Boolean HasPhonetics { get; } + } +} diff --git a/ClosedXML/Excel/RichText/IXLPhonetic.cs b/ClosedXML/Excel/RichText/IXLPhonetic.cs new file mode 100644 index 0000000..56e20f9 --- /dev/null +++ b/ClosedXML/Excel/RichText/IXLPhonetic.cs @@ -0,0 +1,11 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLPhonetic: IEquatable + { + String Text { get; set; } + Int32 Start { get; set; } + Int32 End { get; set; } + } +} diff --git a/ClosedXML/Excel/RichText/IXLPhonetics.cs b/ClosedXML/Excel/RichText/IXLPhonetics.cs new file mode 100644 index 0000000..67cd96d --- /dev/null +++ b/ClosedXML/Excel/RichText/IXLPhonetics.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLPhoneticAlignment { Center, Distributed, Left, NoControl } + public enum XLPhoneticType { FullWidthKatakana, HalfWidthKatakana, Hiragana, NoConversion } + public interface IXLPhonetics : IXLFontBase, IEnumerable, IEquatable + { + IXLPhonetics SetBold(); IXLPhonetics SetBold(Boolean value); + IXLPhonetics SetItalic(); IXLPhonetics SetItalic(Boolean value); + IXLPhonetics SetUnderline(); IXLPhonetics SetUnderline(XLFontUnderlineValues value); + IXLPhonetics SetStrikethrough(); IXLPhonetics SetStrikethrough(Boolean value); + IXLPhonetics SetVerticalAlignment(XLFontVerticalTextAlignmentValues value); + IXLPhonetics SetShadow(); IXLPhonetics SetShadow(Boolean value); + IXLPhonetics SetFontSize(Double value); + IXLPhonetics SetFontColor(XLColor value); + IXLPhonetics SetFontName(String value); + IXLPhonetics SetFontFamilyNumbering(XLFontFamilyNumberingValues value); + IXLPhonetics SetFontCharSet(XLFontCharSet value); + + IXLPhonetics Add(String text, Int32 start, Int32 end); + IXLPhonetics ClearText(); + IXLPhonetics ClearFont(); + Int32 Count { get; } + + XLPhoneticAlignment Alignment { get; set; } + XLPhoneticType Type { get; set; } + + IXLPhonetics SetAlignment(XLPhoneticAlignment phoneticAlignment); + IXLPhonetics SetType(XLPhoneticType phoneticType); + } +} diff --git a/ClosedXML/Excel/RichText/IXLRichString.cs b/ClosedXML/Excel/RichText/IXLRichString.cs new file mode 100644 index 0000000..a090460 --- /dev/null +++ b/ClosedXML/Excel/RichText/IXLRichString.cs @@ -0,0 +1,27 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLWithRichString + { + IXLRichString AddText(String text); + IXLRichString AddNewLine(); + } + public interface IXLRichString: IXLFontBase, IEquatable, IXLWithRichString + { + String Text { get; set; } + + + IXLRichString SetBold(); IXLRichString SetBold(Boolean value); + IXLRichString SetItalic(); IXLRichString SetItalic(Boolean value); + IXLRichString SetUnderline(); IXLRichString SetUnderline(XLFontUnderlineValues value); + IXLRichString SetStrikethrough(); IXLRichString SetStrikethrough(Boolean value); + IXLRichString SetVerticalAlignment(XLFontVerticalTextAlignmentValues value); + IXLRichString SetShadow(); IXLRichString SetShadow(Boolean value); + IXLRichString SetFontSize(Double value); + IXLRichString SetFontColor(XLColor value); + IXLRichString SetFontName(String value); + IXLRichString SetFontFamilyNumbering(XLFontFamilyNumberingValues value); + IXLRichString SetFontCharSet(XLFontCharSet value); + } +} diff --git a/ClosedXML/Excel/RichText/IXLRichText.cs b/ClosedXML/Excel/RichText/IXLRichText.cs new file mode 100644 index 0000000..46efd41 --- /dev/null +++ b/ClosedXML/Excel/RichText/IXLRichText.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLRichText : IXLFormattedText + { + } +} diff --git a/ClosedXML/Excel/RichText/XLFormattedText.cs b/ClosedXML/Excel/RichText/XLFormattedText.cs new file mode 100644 index 0000000..3748478 --- /dev/null +++ b/ClosedXML/Excel/RichText/XLFormattedText.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLFormattedText: IXLFormattedText + { + List _richTexts = new List(); + + protected T Container; + readonly IXLFontBase _defaultFont; + public XLFormattedText(IXLFontBase defaultFont) + { + Length = 0; + _defaultFont = defaultFont; + } + + public XLFormattedText(IXLFormattedText defaultRichText, IXLFontBase defaultFont) + : this(defaultFont) + { + foreach (var rt in defaultRichText) + AddText(rt.Text, rt); + if (defaultRichText.HasPhonetics) + { + _phonetics = new XLPhonetics(defaultRichText.Phonetics, defaultFont); + } + } + + public XLFormattedText(String text, IXLFontBase defaultFont) + :this(defaultFont) + { + AddText(text); + } + + public Int32 Count { get { return _richTexts.Count; } } + public int Length { get; private set; } + + public IXLRichString AddText(String text) + { + return AddText(text, _defaultFont); + } + public IXLRichString AddText(String text, IXLFontBase font) + { + var richText = new XLRichString(text, font, this); + return AddText(richText); + } + + public IXLRichString AddText(IXLRichString richText) + { + _richTexts.Add(richText); + Length += richText.Text.Length; + return richText; + } + + public IXLRichString AddNewLine() + { + return AddText(Environment.NewLine); + } + + public IXLFormattedText ClearText() + { + _richTexts.Clear(); + Length = 0; + return this; + } + public IXLFormattedText ClearFont() + { + String text = Text; + ClearText(); + AddText(text); + return this; + } + + public override string ToString() + { + var sb = new StringBuilder(_richTexts.Count); + _richTexts.ForEach(rt => sb.Append(rt.Text)); + return sb.ToString(); + } + + public IXLFormattedText Substring(Int32 index) + { + return Substring(index, Length - index); + } + public IXLFormattedText Substring(Int32 index, Int32 length) + { + if (index + 1 > Length || (Length - index + 1) < length || length <= 0) + throw new IndexOutOfRangeException("Index and length must refer to a location within the string."); + + List newRichTexts = new List(); + var retVal = new XLFormattedText(_defaultFont); + + Int32 lastPosition = 0; + foreach (var rt in _richTexts) + { + if (lastPosition >= index + 1 + length) // We already have what we need + { + newRichTexts.Add(rt); + } + else if (lastPosition + rt.Text.Length >= index + 1) // Eureka! + { + Int32 startIndex = index - lastPosition; + + if (startIndex > 0) + newRichTexts.Add(new XLRichString(rt.Text.Substring(0, startIndex), rt, this)); + else if (startIndex < 0) + startIndex = 0; + + Int32 leftToTake = length - retVal.Length; + if (leftToTake > rt.Text.Length - startIndex) + leftToTake = rt.Text.Length - startIndex; + + XLRichString newRt = new XLRichString(rt.Text.Substring(startIndex, leftToTake), rt, this); + newRichTexts.Add(newRt); + retVal.AddText(newRt); + + if (startIndex + leftToTake < rt.Text.Length) + newRichTexts.Add(new XLRichString(rt.Text.Substring(startIndex + leftToTake), rt, this)); + } + else // We haven't reached the desired position yet + { + newRichTexts.Add(rt); + } + lastPosition += rt.Text.Length; + } + _richTexts = newRichTexts; + return retVal; + } + + public IEnumerator GetEnumerator() + { + return _richTexts.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Boolean Bold { set { _richTexts.ForEach(rt => rt.Bold = value); } } + public Boolean Italic { set { _richTexts.ForEach(rt => rt.Italic = value); } } + public XLFontUnderlineValues Underline { set { _richTexts.ForEach(rt => rt.Underline = value); } } + public Boolean Strikethrough { set { _richTexts.ForEach(rt => rt.Strikethrough = value); } } + public XLFontVerticalTextAlignmentValues VerticalAlignment { set { _richTexts.ForEach(rt => rt.VerticalAlignment = value); } } + public Boolean Shadow { set { _richTexts.ForEach(rt => rt.Shadow = value); } } + public Double FontSize { set { _richTexts.ForEach(rt => rt.FontSize = value); } } + public XLColor FontColor { set { _richTexts.ForEach(rt => rt.FontColor = value); } } + public String FontName { set { _richTexts.ForEach(rt => rt.FontName = value); } } + public XLFontFamilyNumberingValues FontFamilyNumbering { set { _richTexts.ForEach(rt => rt.FontFamilyNumbering = value); } } + + public IXLFormattedText SetBold() { Bold = true; return this; } public IXLFormattedText SetBold(Boolean value) { Bold = value; return this; } + public IXLFormattedText SetItalic() { Italic = true; return this; } public IXLFormattedText SetItalic(Boolean value) { Italic = value; return this; } + public IXLFormattedText SetUnderline() { Underline = XLFontUnderlineValues.Single; return this; } public IXLFormattedText SetUnderline(XLFontUnderlineValues value) { Underline = value; return this; } + public IXLFormattedText SetStrikethrough() { Strikethrough = true; return this; } public IXLFormattedText SetStrikethrough(Boolean value) { Strikethrough = value; return this; } + public IXLFormattedText SetVerticalAlignment(XLFontVerticalTextAlignmentValues value) { VerticalAlignment = value; return this; } + public IXLFormattedText SetShadow() { Shadow = true; return this; } public IXLFormattedText SetShadow(Boolean value) { Shadow = value; return this; } + public IXLFormattedText SetFontSize(Double value) { FontSize = value; return this; } + public IXLFormattedText SetFontColor(XLColor value) { FontColor = value; return this; } + public IXLFormattedText SetFontName(String value) { FontName = value; return this; } + public IXLFormattedText SetFontFamilyNumbering(XLFontFamilyNumberingValues value) { FontFamilyNumbering = value; return this; } + + public bool Equals(IXLFormattedText other) + { + Int32 count = Count; + if (count != other.Count) + return false; + + for (Int32 i = 0; i < count; i++) + { + if (_richTexts.ElementAt(i) != other.ElementAt(i)) + return false; + } + + return _phonetics == null || Phonetics.Equals(other.Phonetics); + } + + public String Text { get { return ToString(); } } + + private IXLPhonetics _phonetics; + public IXLPhonetics Phonetics + { + get { return _phonetics ?? (_phonetics = new XLPhonetics(_defaultFont)); } + } + + public Boolean HasPhonetics { get { return _phonetics != null; } } + } +} diff --git a/ClosedXML/Excel/RichText/XLPhonetic.cs b/ClosedXML/Excel/RichText/XLPhonetic.cs new file mode 100644 index 0000000..253a24f --- /dev/null +++ b/ClosedXML/Excel/RichText/XLPhonetic.cs @@ -0,0 +1,25 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLPhonetic: IXLPhonetic + { + public XLPhonetic(String text, Int32 start, Int32 end) + { + Text = text; + Start = start; + End = end; + } + public String Text { get; set; } + public Int32 Start { get; set; } + public Int32 End { get; set; } + + public bool Equals(IXLPhonetic other) + { + if (other == null) + return false; + + return Text == other.Text && Start == other.Start && End == other.End; + } + } +} diff --git a/ClosedXML/Excel/RichText/XLPhonetics.cs b/ClosedXML/Excel/RichText/XLPhonetics.cs new file mode 100644 index 0000000..b048267 --- /dev/null +++ b/ClosedXML/Excel/RichText/XLPhonetics.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLPhonetics : IXLPhonetics + { + private readonly List _phonetics = new List(); + + private readonly IXLFontBase _defaultFont; + + public XLPhonetics(IXLFontBase defaultFont) + { + _defaultFont = defaultFont; + Type = XLPhoneticType.FullWidthKatakana; + Alignment = XLPhoneticAlignment.Left; + this.CopyFont(_defaultFont); + } + + public XLPhonetics(IXLPhonetics defaultPhonetics, IXLFontBase defaultFont) + { + _defaultFont = defaultFont; + Type = defaultPhonetics.Type; + Alignment = defaultPhonetics.Alignment; + + this.CopyFont(defaultPhonetics); + } + + public Boolean Bold { get; set; } + public Boolean Italic { get; set; } + public XLFontUnderlineValues Underline { get; set; } + public Boolean Strikethrough { get; set; } + public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; } + public Boolean Shadow { get; set; } + public Double FontSize { get; set; } + public XLColor FontColor { get; set; } + public String FontName { get; set; } + public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; } + public XLFontCharSet FontCharSet { get; set; } + + public IXLPhonetics SetBold() { Bold = true; return this; } + + public IXLPhonetics SetBold(Boolean value) { Bold = value; return this; } + + public IXLPhonetics SetItalic() { Italic = true; return this; } + + public IXLPhonetics SetItalic(Boolean value) { Italic = value; return this; } + + public IXLPhonetics SetUnderline() { Underline = XLFontUnderlineValues.Single; return this; } + + public IXLPhonetics SetUnderline(XLFontUnderlineValues value) { Underline = value; return this; } + + public IXLPhonetics SetStrikethrough() { Strikethrough = true; return this; } + + public IXLPhonetics SetStrikethrough(Boolean value) { Strikethrough = value; return this; } + + public IXLPhonetics SetVerticalAlignment(XLFontVerticalTextAlignmentValues value) { VerticalAlignment = value; return this; } + + public IXLPhonetics SetShadow() { Shadow = true; return this; } + + public IXLPhonetics SetShadow(Boolean value) { Shadow = value; return this; } + + public IXLPhonetics SetFontSize(Double value) { FontSize = value; return this; } + + public IXLPhonetics SetFontColor(XLColor value) { FontColor = value; return this; } + + public IXLPhonetics SetFontName(String value) { FontName = value; return this; } + + public IXLPhonetics SetFontFamilyNumbering(XLFontFamilyNumberingValues value) { FontFamilyNumbering = value; return this; } + + public IXLPhonetics SetFontCharSet(XLFontCharSet value) { FontCharSet = value; return this; } + + public IXLPhonetics Add(String text, Int32 start, Int32 end) + { + _phonetics.Add(new XLPhonetic(text, start, end)); + return this; + } + + public IXLPhonetics ClearText() + { + _phonetics.Clear(); + return this; + } + + public IXLPhonetics ClearFont() + { + this.CopyFont(_defaultFont); + return this; + } + + public Int32 Count { get { return _phonetics.Count; } } + + public XLPhoneticAlignment Alignment { get; set; } + public XLPhoneticType Type { get; set; } + + public IXLPhonetics SetAlignment(XLPhoneticAlignment phoneticAlignment) { Alignment = phoneticAlignment; return this; } + + public IXLPhonetics SetType(XLPhoneticType phoneticType) { Type = phoneticType; return this; } + + public IEnumerator GetEnumerator() + { + return _phonetics.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool Equals(IXLPhonetics other) + { + if (other == null) + return false; + + Int32 phoneticsCount = _phonetics.Count; + for (Int32 i = 0; i < phoneticsCount; i++) + { + if (!_phonetics[i].Equals(other.ElementAt(i))) + return false; + } + + return + Bold == other.Bold + && Italic == other.Italic + && Underline == other.Underline + && Strikethrough == other.Strikethrough + && VerticalAlignment == other.VerticalAlignment + && Shadow == other.Shadow + && FontSize == other.FontSize + && FontColor.Equals(other.FontColor) + && FontName == other.FontName + && FontFamilyNumbering == other.FontFamilyNumbering; + } + } +} diff --git a/ClosedXML/Excel/RichText/XLRichString.cs b/ClosedXML/Excel/RichText/XLRichString.cs new file mode 100644 index 0000000..5a82aab --- /dev/null +++ b/ClosedXML/Excel/RichText/XLRichString.cs @@ -0,0 +1,159 @@ +using System; +using System.Diagnostics; + +namespace ClosedXML.Excel +{ + [DebuggerDisplay("{Text}")] + internal class XLRichString : IXLRichString + { + private IXLWithRichString _withRichString; + + public XLRichString(String text, IXLFontBase font, IXLWithRichString withRichString) + { + Text = text; + this.CopyFont(font); + _withRichString = withRichString; + } + + public String Text { get; set; } + + public IXLRichString AddText(String text) + { + return _withRichString.AddText(text); + } + + public IXLRichString AddNewLine() + { + return AddText(Environment.NewLine); + } + + public Boolean Bold { get; set; } + public Boolean Italic { get; set; } + public XLFontUnderlineValues Underline { get; set; } + public Boolean Strikethrough { get; set; } + public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; } + public Boolean Shadow { get; set; } + public Double FontSize { get; set; } + public XLColor FontColor { get; set; } + public String FontName { get; set; } + public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; } + public XLFontCharSet FontCharSet { get; set; } + + public IXLRichString SetBold() + { + Bold = true; return this; + } + + public IXLRichString SetBold(Boolean value) + { + Bold = value; return this; + } + + public IXLRichString SetItalic() + { + Italic = true; return this; + } + + public IXLRichString SetItalic(Boolean value) + { + Italic = value; return this; + } + + public IXLRichString SetUnderline() + { + Underline = XLFontUnderlineValues.Single; return this; + } + + public IXLRichString SetUnderline(XLFontUnderlineValues value) + { + Underline = value; return this; + } + + public IXLRichString SetStrikethrough() + { + Strikethrough = true; return this; + } + + public IXLRichString SetStrikethrough(Boolean value) + { + Strikethrough = value; return this; + } + + public IXLRichString SetVerticalAlignment(XLFontVerticalTextAlignmentValues value) + { + VerticalAlignment = value; return this; + } + + public IXLRichString SetShadow() + { + Shadow = true; return this; + } + + public IXLRichString SetShadow(Boolean value) + { + Shadow = value; return this; + } + + public IXLRichString SetFontSize(Double value) + { + FontSize = value; return this; + } + + public IXLRichString SetFontColor(XLColor value) + { + FontColor = value; return this; + } + + public IXLRichString SetFontName(String value) + { + FontName = value; return this; + } + + public IXLRichString SetFontFamilyNumbering(XLFontFamilyNumberingValues value) + { + FontFamilyNumbering = value; return this; + } + + public IXLRichString SetFontCharSet(XLFontCharSet value) + { + FontCharSet = value; return this; + } + + public Boolean Equals(IXLRichString other) + { + return + Text == other.Text + && Bold.Equals(other.Bold) + && Italic.Equals(other.Italic) + && Underline.Equals(other.Underline) + && Strikethrough.Equals(other.Strikethrough) + && VerticalAlignment.Equals(other.VerticalAlignment) + && Shadow.Equals(other.Shadow) + && FontSize.Equals(other.FontSize) + && FontColor.Equals(other.FontColor) + && FontName.Equals(other.FontName) + && FontFamilyNumbering.Equals(other.FontFamilyNumbering) + ; + } + + public override bool Equals(object obj) + { + return Equals((XLRichString)obj); + } + + public override int GetHashCode() + { + return Text.GetHashCode() + ^ Bold.GetHashCode() + ^ Italic.GetHashCode() + ^ (Int32)Underline + ^ Strikethrough.GetHashCode() + ^ (Int32)VerticalAlignment + ^ Shadow.GetHashCode() + ^ FontSize.GetHashCode() + ^ FontColor.GetHashCode() + ^ FontName.GetHashCode() + ^ (Int32)FontFamilyNumbering; + } + } +} diff --git a/ClosedXML/Excel/RichText/XLRichText.cs b/ClosedXML/Excel/RichText/XLRichText.cs new file mode 100644 index 0000000..aaad29a --- /dev/null +++ b/ClosedXML/Excel/RichText/XLRichText.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLRichText: XLFormattedText, IXLRichText + { + + public XLRichText(IXLFontBase defaultFont) + :base(defaultFont) + { + Container = this; + } + + public XLRichText(XLFormattedText defaultRichText, IXLFontBase defaultFont) + :base(defaultRichText, defaultFont) + { + Container = this; + } + + public XLRichText(String text, IXLFontBase defaultFont) + :base(text, defaultFont) + { + Container = this; + } + + } +} diff --git a/ClosedXML/Excel/Rows/IXLRow.cs b/ClosedXML/Excel/Rows/IXLRow.cs new file mode 100644 index 0000000..045ab5b --- /dev/null +++ b/ClosedXML/Excel/Rows/IXLRow.cs @@ -0,0 +1,207 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLRow : IXLRangeBase + { + /// + /// Gets or sets the height of this row. + /// + /// + /// The width of this row. + /// + Double Height { get; set; } + + /// + /// Clears the height for the row and defaults it to the spreadsheet row height. + /// + void ClearHeight(); + + /// + /// Deletes this row and shifts the rows below this one accordingly. + /// + void Delete(); + + /// + /// Gets this row's number + /// + Int32 RowNumber(); + + /// + /// Inserts X number of rows below this one. + /// All rows below will be shifted accordingly. + /// + /// The number of rows to insert. + IXLRows InsertRowsBelow(Int32 numberOfRows); + + /// + /// Inserts X number of rows above this one. + /// This row and all below will be shifted accordingly. + /// + /// The number of rows to insert. + IXLRows InsertRowsAbove(Int32 numberOfRows); + + IXLRow AdjustToContents(); + + /// + /// Adjusts the height of the row based on its contents, starting from the startColumn. + /// + /// The column to start calculating the row height. + IXLRow AdjustToContents(Int32 startColumn); + + /// + /// Adjusts the height of the row based on its contents, starting from the startColumn and ending at endColumn. + /// + /// The column to start calculating the row height. + /// The column to end calculating the row height. + IXLRow AdjustToContents(Int32 startColumn, Int32 endColumn); + + IXLRow AdjustToContents(Double minHeight, Double maxHeight); + + IXLRow AdjustToContents(Int32 startColumn, Double minHeight, Double maxHeight); + + IXLRow AdjustToContents(Int32 startColumn, Int32 endColumn, Double minHeight, Double maxHeight); + + /// Hides this row. + IXLRow Hide(); + + /// Unhides this row. + IXLRow Unhide(); + + /// + /// Gets a value indicating whether this row is hidden or not. + /// + /// + /// true if this row is hidden; otherwise, false. + /// + Boolean IsHidden { get; } + + /// + /// Gets or sets the outline level of this row. + /// + /// + /// The outline level of this row. + /// + Int32 OutlineLevel { get; set; } + + /// + /// Adds this row to the next outline level (Increments the outline level for this row by 1). + /// + IXLRow Group(); + + /// + /// Adds this row to the next outline level (Increments the outline level for this row by 1). + /// + /// If set to true the row will be shown collapsed. + IXLRow Group(Boolean collapse); + + /// + /// Sets outline level for this row. + /// + /// The outline level. + IXLRow Group(Int32 outlineLevel); + + /// + /// Sets outline level for this row. + /// + /// The outline level. + /// If set to true the row will be shown collapsed. + IXLRow Group(Int32 outlineLevel, Boolean collapse); + + /// + /// Adds this row to the previous outline level (decrements the outline level for this row by 1). + /// + IXLRow Ungroup(); + + /// + /// Adds this row to the previous outline level (decrements the outline level for this row by 1). + /// + /// If set to true it will remove this row from all outline levels. + IXLRow Ungroup(Boolean fromAll); + + /// + /// Show this row as collapsed. + /// + IXLRow Collapse(); + + /// + /// Gets the cell in the specified column. + /// + /// The cell's column. + IXLCell Cell(Int32 columnNumber); + + /// + /// Gets the cell in the specified column. + /// + /// The cell's column. + IXLCell Cell(String columnLetter); + + /// + /// Returns the specified group of cells, separated by commas. + /// e.g. Cells("1"), Cells("1:5"), Cells("1,3:5") + /// + /// The row's cells to return. + new IXLCells Cells(String cellsInRow); + + /// + /// Returns the specified group of cells. + /// + /// The first column in the group of cells to return. + /// The last column in the group of cells to return. + IXLCells Cells(Int32 firstColumn, Int32 lastColumn); + + /// + /// Returns the specified group of cells. + /// + /// The first column in the group of cells to return. + /// The last column in the group of cells to return. + IXLCells Cells(String firstColumn, String lastColumn); + + /// Expands this row (if it's collapsed). + IXLRow Expand(); + + Int32 CellCount(); + + IXLRangeRow CopyTo(IXLCell cell); + + IXLRangeRow CopyTo(IXLRangeBase range); + + IXLRow CopyTo(IXLRow row); + + IXLRow Sort(); + + IXLRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + IXLRangeRow Row(Int32 start, Int32 end); + + IXLRangeRow Row(IXLCell start, IXLCell end); + + IXLRangeRows Rows(String columns); + + /// + /// Adds a horizontal page break after this row. + /// + IXLRow AddHorizontalPageBreak(); + + IXLRow SetDataType(XLDataType dataType); + + IXLRow RowAbove(); + + IXLRow RowAbove(Int32 step); + + IXLRow RowBelow(); + + IXLRow RowBelow(Int32 step); + + /// + /// Clears the contents of this row. + /// + /// Specify what you want to clear. + new IXLRow Clear(XLClearOptions clearOptions = XLClearOptions.All); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRangeRow RowUsed(Boolean includeFormats); + + IXLRangeRow RowUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents); + } +} diff --git a/ClosedXML/Excel/Rows/IXLRows.cs b/ClosedXML/Excel/Rows/IXLRows.cs new file mode 100644 index 0000000..bf8adf7 --- /dev/null +++ b/ClosedXML/Excel/Rows/IXLRows.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLRows : IEnumerable + { + /// + /// Sets the height of all rows. + /// + /// + /// The height of all rows. + /// + Double Height { set; } + + /// + /// Deletes all rows and shifts the rows below them accordingly. + /// + void Delete(); + + /// + /// Adjusts the height of all rows based on its contents. + /// + IXLRows AdjustToContents(); + + /// + /// Adjusts the height of all rows based on its contents, starting from the startColumn. + /// + /// The column to start calculating the row height. + IXLRows AdjustToContents(Int32 startColumn); + + /// + /// Adjusts the height of all rows based on its contents, starting from the startColumn and ending at endColumn. + /// + /// The column to start calculating the row height. + /// The column to end calculating the row height. + IXLRows AdjustToContents(Int32 startColumn, Int32 endColumn); + + IXLRows AdjustToContents(Double minHeight, Double maxHeight); + + IXLRows AdjustToContents(Int32 startColumn, Double minHeight, Double maxHeight); + + IXLRows AdjustToContents(Int32 startColumn, Int32 endColumn, Double minHeight, Double maxHeight); + + /// + /// Hides all rows. + /// + void Hide(); + + /// Unhides all rows. + void Unhide(); + + /// + /// Increments the outline level of all rows by 1. + /// + void Group(); + + /// + /// Increments the outline level of all rows by 1. + /// + /// If set to true the rows will be shown collapsed. + void Group(Boolean collapse); + + /// + /// Sets outline level for all rows. + /// + /// The outline level. + void Group(Int32 outlineLevel); + + /// + /// Sets outline level for all rows. + /// + /// The outline level. + /// If set to true the rows will be shown collapsed. + void Group(Int32 outlineLevel, Boolean collapse); + + /// + /// Decrements the outline level of all rows by 1. + /// + void Ungroup(); + + /// + /// Decrements the outline level of all rows by 1. + /// + /// If set to true it will remove the rows from all outline levels. + void Ungroup(Boolean fromAll); + + /// + /// Show all rows as collapsed. + /// + void Collapse(); + + /// Expands all rows (if they're collapsed). + void Expand(); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + IXLStyle Style { get; set; } + + /// + /// Adds a horizontal page break after these rows. + /// + IXLRows AddHorizontalPageBreaks(); + + IXLRows SetDataType(XLDataType dataType); + + /// + /// Clears the contents of these rows. + /// + /// Specify what you want to clear. + IXLRows Clear(XLClearOptions clearOptions = XLClearOptions.All); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Rows/XLRow.cs b/ClosedXML/Excel/Rows/XLRow.cs new file mode 100644 index 0000000..6f31877 --- /dev/null +++ b/ClosedXML/Excel/Rows/XLRow.cs @@ -0,0 +1,609 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLRow : XLRangeBase, IXLRow + { + #region Private fields + + private Double _height; + private Int32 _outlineLevel; + + #endregion Private fields + + #region Constructor + + /// + /// The direct contructor should only be used in . + /// + public XLRow(XLWorksheet worksheet, Int32 row) + : base(XLRangeAddress.EntireRow(worksheet, row), worksheet.StyleValue) + { + SetRowNumber(row); + + _height = worksheet.RowHeight; + } + + #endregion Constructor + + public override XLRangeType RangeType + { + get { return XLRangeType.Row; } + } + + public override IEnumerable Styles + { + get + { + yield return Style; + + int row = RowNumber(); + + foreach (XLCell cell in Worksheet.Internals.CellsCollection.GetCellsInRow(row)) + yield return cell.Style; + } + } + + protected override IEnumerable Children + { + get + { + int row = RowNumber(); + + foreach (XLCell cell in Worksheet.Internals.CellsCollection.GetCellsInRow(row)) + yield return cell; + } + } + + public Boolean Collapsed { get; set; } + + #region IXLRow Members + + public Boolean Loading { get; set; } + + public Boolean HeightChanged { get; private set; } + + public Double Height + { + get { return _height; } + set + { + if (!Loading) + HeightChanged = true; + + _height = value; + } + } + + public void ClearHeight() + { + Height = Worksheet.RowHeight; + HeightChanged = false; + } + + public void Delete() + { + int rowNumber = RowNumber(); + AsRange().Delete(XLShiftDeletedCells.ShiftCellsUp); + Worksheet.DeleteRow(rowNumber); + } + + public new IXLRows InsertRowsBelow(Int32 numberOfRows) + { + int rowNum = RowNumber(); + Worksheet.Internals.RowsCollection.ShiftRowsDown(rowNum + 1, numberOfRows); + var asRange = Worksheet.Row(rowNum).AsRange(); + asRange.InsertRowsBelowVoid(true, numberOfRows); + + var newRows = Worksheet.Rows(rowNum + 1, rowNum + numberOfRows); + + CopyRows(newRows); + + return newRows; + } + + private void CopyRows(IXLRows newRows) + { + foreach (var newRow in newRows) + { + var internalRow = Worksheet.Internals.RowsCollection[newRow.RowNumber()]; + internalRow._height = Height; + internalRow.InnerStyle = InnerStyle; + internalRow.Collapsed = Collapsed; + internalRow.IsHidden = IsHidden; + internalRow._outlineLevel = OutlineLevel; + } + } + + public new IXLRows InsertRowsAbove(Int32 numberOfRows) + { + int rowNum = RowNumber(); + if (rowNum > 1) + { + return Worksheet.Row(rowNum - 1).InsertRowsBelow(numberOfRows); + } + + Worksheet.Internals.RowsCollection.ShiftRowsDown(rowNum, numberOfRows); + var asRange = Worksheet.Row(rowNum).AsRange(); + asRange.InsertRowsAboveVoid(true, numberOfRows); + + return Worksheet.Rows(rowNum, rowNum + numberOfRows - 1); + } + + public new IXLRow Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + public IXLCell Cell(Int32 columnNumber) + { + return Cell(1, columnNumber); + } + + public override XLCell Cell(String columnLetter) + { + return Cell(1, columnLetter); + } + + IXLCell IXLRow.Cell(string columnLetter) + { + return Cell(columnLetter); + } + + public override IXLCells Cells() + { + return Cells(true, XLCellsUsedOptions.All); + } + + public override IXLCells Cells(Boolean usedCellsOnly) + { + if (usedCellsOnly) + return Cells(true, XLCellsUsedOptions.AllContents); + else + return Cells(FirstCellUsed().Address.ColumnNumber, LastCellUsed().Address.ColumnNumber); + } + + public override IXLCells Cells(String cellsInRow) + { + var retVal = new XLCells(false, XLCellsUsedOptions.AllContents); + var rangePairs = cellsInRow.Split(','); + foreach (string pair in rangePairs) + retVal.Add(Range(pair.Trim()).RangeAddress); + return retVal; + } + + public IXLCells Cells(Int32 firstColumn, Int32 lastColumn) + { + return Cells(firstColumn + ":" + lastColumn); + } + + public IXLCells Cells(String firstColumn, String lastColumn) + { + return Cells(XLHelper.GetColumnNumberFromLetter(firstColumn) + ":" + + XLHelper.GetColumnNumberFromLetter(lastColumn)); + } + + public IXLRow AdjustToContents(Int32 startColumn) + { + return AdjustToContents(startColumn, XLHelper.MaxColumnNumber); + } + + public IXLRow AdjustToContents(Int32 startColumn, Int32 endColumn) + { + return AdjustToContents(startColumn, endColumn, 0, Double.MaxValue); + } + + public IXLRow AdjustToContents(Double minHeight, Double maxHeight) + { + return AdjustToContents(1, XLHelper.MaxColumnNumber, minHeight, maxHeight); + } + + public IXLRow AdjustToContents(Int32 startColumn, Double minHeight, Double maxHeight) + { + return AdjustToContents(startColumn, XLHelper.MaxColumnNumber, minHeight, maxHeight); + } + + public IXLRow AdjustToContents(Int32 startColumn, Int32 endColumn, Double minHeight, Double maxHeight) + { + var fontCache = new Dictionary(); + + Double rowMaxHeight = minHeight; + foreach (XLCell c in from XLCell c in Row(startColumn, endColumn).CellsUsed() where !c.IsMerged() select c) + { + Double thisHeight; + Int32 textRotation = c.StyleValue.Alignment.TextRotation; + if (c.HasRichText || textRotation != 0 || c.InnerText.Contains(Environment.NewLine)) + { + var kpList = new List>(); + if (c.HasRichText) + { + foreach (IXLRichString rt in c.RichText) + { + String formattedString = rt.Text; + var arr = formattedString.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + Int32 arrCount = arr.Count(); + for (Int32 i = 0; i < arrCount; i++) + { + String s = arr[i]; + if (i < arrCount - 1) + s += Environment.NewLine; + kpList.Add(new KeyValuePair(rt, s)); + } + } + } + else + { + String formattedString = c.GetFormattedString(); + var arr = formattedString.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + Int32 arrCount = arr.Count(); + for (Int32 i = 0; i < arrCount; i++) + { + String s = arr[i]; + if (i < arrCount - 1) + s += Environment.NewLine; + kpList.Add(new KeyValuePair(c.Style.Font, s)); + } + } + + Double maxLongCol = kpList.Max(kp => kp.Value.Length); + Double maxHeightCol = kpList.Max(kp => kp.Key.GetHeight(fontCache)); + Int32 lineCount = kpList.Count(kp => kp.Value.Contains(Environment.NewLine)) + 1; + if (textRotation == 0) + thisHeight = maxHeightCol * lineCount; + else + { + if (textRotation == 255) + thisHeight = maxLongCol * maxHeightCol; + else + { + Double rotation; + if (textRotation == 90 || textRotation == 180) + rotation = 90; + else + rotation = textRotation % 90; + + thisHeight = (rotation / 90.0) * maxHeightCol * maxLongCol * 0.5; + } + } + } + else + thisHeight = c.Style.Font.GetHeight(fontCache); + + if (thisHeight >= maxHeight) + { + rowMaxHeight = maxHeight; + break; + } + if (thisHeight > rowMaxHeight) + rowMaxHeight = thisHeight; + } + + if (rowMaxHeight <= 0) + rowMaxHeight = Worksheet.RowHeight; + + Height = rowMaxHeight; + + foreach (IDisposable font in fontCache.Values) + { + font.Dispose(); + } + return this; + } + + public IXLRow Hide() + { + IsHidden = true; + return this; + } + + public IXLRow Unhide() + { + IsHidden = false; + return this; + } + + public Boolean IsHidden { get; set; } + + public Int32 OutlineLevel + { + get { return _outlineLevel; } + set + { + if (value < 0 || value > 8) + throw new ArgumentOutOfRangeException("value", "Outline level must be between 0 and 8."); + + Worksheet.IncrementColumnOutline(value); + Worksheet.DecrementColumnOutline(_outlineLevel); + _outlineLevel = value; + } + } + + public IXLRow Group() + { + return Group(false); + } + + public IXLRow Group(Int32 outlineLevel) + { + return Group(outlineLevel, false); + } + + public IXLRow Ungroup() + { + return Ungroup(false); + } + + public IXLRow Group(Boolean collapse) + { + if (OutlineLevel < 8) + OutlineLevel += 1; + + Collapsed = collapse; + return this; + } + + public IXLRow Group(Int32 outlineLevel, Boolean collapse) + { + OutlineLevel = outlineLevel; + Collapsed = collapse; + return this; + } + + public IXLRow Ungroup(Boolean ungroupFromAll) + { + if (ungroupFromAll) + OutlineLevel = 0; + else + { + if (OutlineLevel > 0) + OutlineLevel -= 1; + } + return this; + } + + public IXLRow Collapse() + { + Collapsed = true; + return Hide(); + } + + public IXLRow Expand() + { + Collapsed = false; + return Unhide(); + } + + public Int32 CellCount() + { + return RangeAddress.LastAddress.ColumnNumber - RangeAddress.FirstAddress.ColumnNumber + 1; + } + + public new IXLRow Sort() + { + return SortLeftToRight(); + } + + public new IXLRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, + Boolean ignoreBlanks = true) + { + base.SortLeftToRight(sortOrder, matchCase, ignoreBlanks); + return this; + } + + IXLRangeRow IXLRow.CopyTo(IXLCell target) + { + var copy = AsRange().CopyTo(target); + return copy.Row(1); + } + + IXLRangeRow IXLRow.CopyTo(IXLRangeBase target) + { + var copy = AsRange().CopyTo(target); + return copy.Row(1); + } + + public IXLRow CopyTo(IXLRow row) + { + row.Clear(); + var newRow = (XLRow)row; + newRow._height = _height; + newRow.HeightChanged = HeightChanged; + newRow.InnerStyle = GetStyle(); + newRow.IsHidden = IsHidden; + + AsRange().CopyTo(row); + + return newRow; + } + + public IXLRangeRow Row(Int32 start, Int32 end) + { + return Range(1, start, 1, end).Row(1); + } + + public IXLRangeRow Row(IXLCell start, IXLCell end) + { + return Row(start.Address.ColumnNumber, end.Address.ColumnNumber); + } + + public IXLRangeRows Rows(String rows) + { + var retVal = new XLRangeRows(); + var rowPairs = rows.Split(','); + foreach (string pair in rowPairs) + AsRange().Rows(pair.Trim()).ForEach(retVal.Add); + + return retVal; + } + + public IXLRow AddHorizontalPageBreak() + { + Worksheet.PageSetup.AddHorizontalPageBreak(RowNumber()); + return this; + } + + public IXLRow SetDataType(XLDataType dataType) + { + DataType = dataType; + return this; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRangeRow RowUsed(Boolean includeFormats) + { + return RowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLRangeRow RowUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents) + { + return Row((this as IXLRangeBase).FirstCellUsed(options), + (this as IXLRangeBase).LastCellUsed(options)); + } + + #endregion IXLRow Members + + public override XLRange AsRange() + { + return Range(1, 1, 1, XLHelper.MaxColumnNumber); + } + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + //do nothing + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + return; // rows are shifted by XLRowCollection + } + + internal void SetRowNumber(Int32 row) + { + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, row, 1, RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + new XLAddress(Worksheet, + row, + XLHelper.MaxColumnNumber, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + + public override XLRange Range(String rangeAddressStr) + { + String rangeAddressToUse; + if (rangeAddressStr.Contains(':') || rangeAddressStr.Contains('-')) + { + if (rangeAddressStr.Contains('-')) + rangeAddressStr = rangeAddressStr.Replace('-', ':'); + + var arrRange = rangeAddressStr.Split(':'); + string firstPart = arrRange[0]; + string secondPart = arrRange[1]; + rangeAddressToUse = FixRowAddress(firstPart) + ":" + FixRowAddress(secondPart); + } + else + rangeAddressToUse = FixRowAddress(rangeAddressStr); + + var rangeAddress = new XLRangeAddress(Worksheet, rangeAddressToUse); + return Range(rangeAddress); + } + + public IXLRow AdjustToContents() + { + return AdjustToContents(1); + } + + internal void SetStyleNoColumns(IXLStyle value) + { + InnerStyle = value; + + int row = RowNumber(); + foreach (XLCell c in Worksheet.Internals.CellsCollection.GetCellsInRow(row)) + c.InnerStyle = value; + } + + private XLRow RowShift(Int32 rowsToShift) + { + return Worksheet.Row(RowNumber() + rowsToShift); + } + + #region XLRow Above + + IXLRow IXLRow.RowAbove() + { + return RowAbove(); + } + + IXLRow IXLRow.RowAbove(Int32 step) + { + return RowAbove(step); + } + + public XLRow RowAbove() + { + return RowAbove(1); + } + + public XLRow RowAbove(Int32 step) + { + return RowShift(step * -1); + } + + #endregion XLRow Above + + #region XLRow Below + + IXLRow IXLRow.RowBelow() + { + return RowBelow(); + } + + IXLRow IXLRow.RowBelow(Int32 step) + { + return RowBelow(step); + } + + public XLRow RowBelow() + { + return RowBelow(1); + } + + public XLRow RowBelow(Int32 step) + { + return RowShift(step); + } + + #endregion XLRow Below + + public override Boolean IsEmpty() + { + return IsEmpty(XLCellsUsedOptions.AllContents); + } + + public override Boolean IsEmpty(XLCellsUsedOptions options) + { + if (options.HasFlag(XLCellsUsedOptions.NormalFormats) && + !StyleValue.Equals(Worksheet.StyleValue)) + return false; + + return base.IsEmpty(options); + } + + public override Boolean IsEntireRow() + { + return true; + } + + public override Boolean IsEntireColumn() + { + return false; + } + } +} diff --git a/ClosedXML/Excel/Rows/XLRowCollection.cs b/ClosedXML/Excel/Rows/XLRowCollection.cs new file mode 100644 index 0000000..1059b3a --- /dev/null +++ b/ClosedXML/Excel/Rows/XLRowCollection.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLRowsCollection : IDictionary + { + private readonly Dictionary _dictionary = new Dictionary(); + + public Dictionary Deleted { get; } = new Dictionary(); + + public Int32 MaxRowUsed; + + #region IDictionary Members + + public void Add(int key, XLRow value) + { + if (key > MaxRowUsed) MaxRowUsed = key; + + if (Deleted.ContainsKey(key)) + Deleted.Remove(key); + + _dictionary.Add(key, value); + } + + public bool ContainsKey(int key) + { + return _dictionary.ContainsKey(key); + } + + public ICollection Keys + { + get { return _dictionary.Keys; } + } + + public bool Remove(int key) + { + if (!Deleted.ContainsKey(key)) + Deleted.Add(key, _dictionary[key]); + + return _dictionary.Remove(key); + } + + public bool TryGetValue(int key, out XLRow value) + { + return _dictionary.TryGetValue(key, out value); + } + + public ICollection Values + { + get { return _dictionary.Values; } + } + + public XLRow this[int key] + { + get { return _dictionary[key]; } + set { _dictionary[key] = value; } + } + + public void Add(KeyValuePair item) + { + if (item.Key > MaxRowUsed) MaxRowUsed = item.Key; + + if (Deleted.ContainsKey(item.Key)) + Deleted.Remove(item.Key); + + _dictionary.Add(item.Key, item.Value); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _dictionary.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public int Count + { + get { return _dictionary.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool Remove(KeyValuePair item) + { + if (!Deleted.ContainsKey(item.Key)) + Deleted.Add(item.Key, _dictionary[item.Key]); + + return _dictionary.Remove(item.Key); + } + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + #endregion IDictionary Members + + public void ShiftRowsDown(Int32 startingRow, Int32 rowsToShift) + { + foreach (int ro in _dictionary.Keys.Where(k => k >= startingRow).OrderByDescending(k => k)) + { + var rowToMove = _dictionary[ro]; + _dictionary.Remove(ro); + Int32 newRowNum = ro + rowsToShift; + if (newRowNum <= XLHelper.MaxRowNumber) + { + rowToMove.SetRowNumber(newRowNum); + _dictionary.Add(newRowNum, rowToMove); + } + } + } + + public void RemoveAll(Func predicate) + { + foreach (var row in _dictionary.Values.Where(predicate).Where(row1 => !Deleted.ContainsKey(row1.RowNumber()))) + { + Deleted.Add(row.RowNumber(), row); + } + + _dictionary.RemoveAll(predicate); + } + } +} diff --git a/ClosedXML/Excel/Rows/XLRows.cs b/ClosedXML/Excel/Rows/XLRows.cs new file mode 100644 index 0000000..3259b5a --- /dev/null +++ b/ClosedXML/Excel/Rows/XLRows.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLRows : XLStylizedBase, IXLRows, IXLStylized + { + private readonly List _rows = new List(); + private readonly XLWorksheet _worksheet; + + + /// + /// Create a new instance of . + /// + /// If worksheet is specified it means that the created instance represents + /// all rows on a worksheet so changing its height will affect all rows. + /// Default style to use when initializing child entries. + public XLRows(XLWorksheet worksheet, XLStyleValue defaultStyle = null) + : base(defaultStyle) + { + _worksheet = worksheet; + } + + #region IXLRows Members + + public IEnumerator GetEnumerator() + { + return _rows.Cast().OrderBy(r => r.RowNumber()).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public double Height + { + set + { + _rows.ForEach(c => c.Height = value); + if (_worksheet == null) return; + _worksheet.RowHeight = value; + _worksheet.Internals.RowsCollection.ForEach(r => r.Value.Height = value); + } + } + + public void Delete() + { + if (_worksheet != null) + { + _worksheet.Internals.RowsCollection.Clear(); + _worksheet.Internals.CellsCollection.Clear(); + } + else + { + var toDelete = new Dictionary>(); + foreach (XLRow r in _rows) + { + if (!toDelete.TryGetValue(r.Worksheet, out List list)) + { + list = new List(); + toDelete.Add(r.Worksheet, list); + } + + list.Add(r.RowNumber()); + } + + foreach (KeyValuePair> kp in toDelete) + { + foreach (int r in kp.Value.OrderByDescending(r => r)) + kp.Key.Row(r).Delete(); + } + } + } + + public IXLRows AdjustToContents() + { + _rows.ForEach(r => r.AdjustToContents()); + return this; + } + + public IXLRows AdjustToContents(Int32 startColumn) + { + _rows.ForEach(r => r.AdjustToContents(startColumn)); + return this; + } + + public IXLRows AdjustToContents(Int32 startColumn, Int32 endColumn) + { + _rows.ForEach(r => r.AdjustToContents(startColumn, endColumn)); + return this; + } + + public IXLRows AdjustToContents(Double minHeight, Double maxHeight) + { + _rows.ForEach(r => r.AdjustToContents(minHeight, maxHeight)); + return this; + } + + public IXLRows AdjustToContents(Int32 startColumn, Double minHeight, Double maxHeight) + { + _rows.ForEach(r => r.AdjustToContents(startColumn, minHeight, maxHeight)); + return this; + } + + public IXLRows AdjustToContents(Int32 startColumn, Int32 endColumn, Double minHeight, Double maxHeight) + { + _rows.ForEach(r => r.AdjustToContents(startColumn, endColumn, minHeight, maxHeight)); + return this; + } + + public void Hide() + { + _rows.ForEach(r => r.Hide()); + } + + public void Unhide() + { + _rows.ForEach(r => r.Unhide()); + } + + public void Group() + { + Group(false); + } + + public void Group(Int32 outlineLevel) + { + Group(outlineLevel, false); + } + + public void Ungroup() + { + Ungroup(false); + } + + public void Group(Boolean collapse) + { + _rows.ForEach(r => r.Group(collapse)); + } + + public void Group(Int32 outlineLevel, Boolean collapse) + { + _rows.ForEach(r => r.Group(outlineLevel, collapse)); + } + + public void Ungroup(Boolean ungroupFromAll) + { + _rows.ForEach(r => r.Ungroup(ungroupFromAll)); + } + + public void Collapse() + { + _rows.ForEach(r => r.Collapse()); + } + + public void Expand() + { + _rows.ForEach(r => r.Expand()); + } + + public IXLCells Cells() + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + foreach (XLRow container in _rows) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(true, XLCellsUsedOptions.AllContents); + foreach (XLRow container in _rows) + cells.Add(container.RangeAddress); + return cells; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(true, options); + foreach (XLRow container in _rows) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLRows AddHorizontalPageBreaks() + { + foreach (XLRow row in _rows) + row.Worksheet.PageSetup.AddHorizontalPageBreak(row.RowNumber()); + return this; + } + + public IXLRows SetDataType(XLDataType dataType) + { + _rows.ForEach(c => c.DataType = dataType); + return this; + } + + #endregion IXLRows Members + + #region IXLStylized Members + protected override IEnumerable Children + { + get + { + if (_worksheet != null) + yield return _worksheet; + else + { + foreach (XLRow row in _rows) + yield return row; + } + } + } + + public override IEnumerable Styles + { + get + { + yield return Style; + if (_worksheet != null) + yield return _worksheet.Style; + else + { + foreach (IXLStyle s in _rows.SelectMany(row => row.Styles)) + { + yield return s; + } + } + } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + public void Add(XLRow row) + { + _rows.Add(row); + } + + public IXLRows Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _rows.ForEach(c => c.Clear(clearOptions)); + return this; + } + + public void Select() + { + foreach (var range in this) + range.Select(); + } + } +} diff --git a/ClosedXML/Excel/SaveOptions.cs b/ClosedXML/Excel/SaveOptions.cs new file mode 100644 index 0000000..83cf982 --- /dev/null +++ b/ClosedXML/Excel/SaveOptions.cs @@ -0,0 +1,29 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public class SaveOptions + { + public SaveOptions() + { +#if DEBUG + this.ValidatePackage = true; +#else + this.ValidatePackage = false; +#endif + } + + public Boolean ConsolidateConditionalFormatRanges { get; set; } = true; + public Boolean ConsolidateDataValidationRanges { get; set; } = true; + public Boolean EvaluateFormulasBeforeSaving { get; set; } = false; + + /// + /// Gets or sets the filter privacy flag. Set to null to leave the current property in saved workbook unchanged + /// + public Boolean? FilterPrivacy { get; set; } + + public Boolean GenerateCalculationChain { get; set; } = true; + public Boolean ValidatePackage { get; set; } + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparkLineGroup.cs b/ClosedXML/Excel/Sparkline/IXLSparkLineGroup.cs new file mode 100644 index 0000000..e3ad631 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparkLineGroup.cs @@ -0,0 +1,105 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public enum XLDisplayBlanksAsValues + { + Interpolate = 0, + NotPlotted = 1, + Zero = 2 + } + + public enum XLSparklineAxisMinMax + { + Automatic = 0, + SameForAll = 1, + Custom = 2 + } + + [Flags] + public enum XLSparklineMarkers + { + None = 0, + HighPoint = 1 << 1, + LowPoint = 1 << 2, + FirstPoint = 1 << 3, + LastPoint = 1 << 4, + NegativePoints = 1 << 5, + Markers = 1 << 6, + All = HighPoint | LowPoint | FirstPoint | LastPoint | NegativePoints | Markers + } + + public enum XLSparklineType + { + Line = 0, + Column = 1, + Stacked = 2 + } + + public interface IXLSparklineGroup : IEnumerable + { + #region Public Properties + + IXLRange DateRange { get; set; } + + XLDisplayBlanksAsValues DisplayEmptyCellsAs { get; set; } + + Boolean DisplayHidden { get; set; } + + IXLSparklineHorizontalAxis HorizontalAxis { get; } + + Double LineWeight { get; set; } + + XLSparklineMarkers ShowMarkers { get; set; } + + IXLSparklineStyle Style { get; set; } + + XLSparklineType Type { get; set; } + + IXLSparklineVerticalAxis VerticalAxis { get; } + + IXLWorksheet Worksheet { get; } + + #endregion Public Properties + + #region Public Methods + + IXLSparkline Add(IXLCell location, IXLRange sourceData); + + IEnumerable Add(IXLRange locationRange, IXLRange sourceDataRange); + + IEnumerable Add(string locationRangeAddress, string sourceDataAddress); + + void CopyFrom(IXLSparklineGroup sparklineGroup); + + IXLSparklineGroup CopyTo(IXLWorksheet targetSheet); + + IXLSparkline GetSparkline(IXLCell cell); + + IEnumerable GetSparklines(IXLRangeBase searchRange); + + void Remove(IXLCell cell); + + void Remove(IXLSparkline sparkline); + + void RemoveAll(); + + IXLSparklineGroup SetDateRange(IXLRange value); + + IXLSparklineGroup SetDisplayEmptyCellsAs(XLDisplayBlanksAsValues value); + + IXLSparklineGroup SetDisplayHidden(Boolean value); + + IXLSparklineGroup SetLineWeight(Double value); + + IXLSparklineGroup SetShowMarkers(XLSparklineMarkers value); + + IXLSparklineGroup SetStyle(IXLSparklineStyle value); + + IXLSparklineGroup SetType(XLSparklineType value); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparkLineGroups.cs b/ClosedXML/Excel/Sparkline/IXLSparkLineGroups.cs new file mode 100644 index 0000000..ce2c4f2 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparkLineGroups.cs @@ -0,0 +1,33 @@ +// Keep this file CodeMaid organised and cleaned +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLSparklineGroups : IEnumerable + { + #region Public Properties + + IXLWorksheet Worksheet { get; } + + #endregion Public Properties + + #region Public Methods + + IXLSparklineGroup Add(IXLSparklineGroup sparklineGroup); + IXLSparklineGroup Add(string locationAddress, string sourceDataAddress); + IXLSparklineGroup Add(IXLCell location, IXLRange sourceData); + IXLSparklineGroup Add(IXLRange locationRange, IXLRange sourceDataRange); + + void CopyTo(IXLWorksheet targetSheet); + + IXLSparkline GetSparkline(IXLCell cell); + IEnumerable GetSparklines(IXLRangeBase rangeBase); + + void Remove(IXLCell cell); + void Remove(IXLRangeBase range); + void Remove(IXLSparklineGroup sparklineGroup); + void RemoveAll(); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparkline.cs b/ClosedXML/Excel/Sparkline/IXLSparkline.cs new file mode 100644 index 0000000..679d051 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparkline.cs @@ -0,0 +1,26 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + public interface IXLSparkline + { + #region Public Properties + + bool IsValid { get; } + + IXLCell Location { get; set; } + + IXLRange SourceData { get; set; } + + IXLSparklineGroup SparklineGroup { get; } + + #endregion Public Properties + + #region Public Methods + + IXLSparkline SetLocation(IXLCell value); + + IXLSparkline SetSourceData(IXLRange value); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparklineHorizontalAxis.cs b/ClosedXML/Excel/Sparkline/IXLSparklineHorizontalAxis.cs new file mode 100644 index 0000000..5d2873d --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparklineHorizontalAxis.cs @@ -0,0 +1,30 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLSparklineHorizontalAxis + { + #region Public Properties + + XLColor Color { get; set; } + + Boolean DateAxis { get; } + + Boolean IsVisible { get; set; } + + Boolean RightToLeft { get; set; } + + #endregion Public Properties + + #region Public Methods + + IXLSparklineHorizontalAxis SetColor(XLColor value); + + IXLSparklineHorizontalAxis SetRightToLeft(Boolean value); + + IXLSparklineHorizontalAxis SetVisible(Boolean value); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparklineStyle.cs b/ClosedXML/Excel/Sparkline/IXLSparklineStyle.cs new file mode 100644 index 0000000..30bc8ff --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparklineStyle.cs @@ -0,0 +1,43 @@ +// Keep this file CodeMaid organised and cleaned + +namespace ClosedXML.Excel +{ + public interface IXLSparklineStyle + { + #region Public Properties + + XLColor FirstMarkerColor { get; set; } + + XLColor HighMarkerColor { get; set; } + + XLColor LastMarkerColor { get; set; } + + XLColor LowMarkerColor { get; set; } + + XLColor MarkersColor { get; set; } + + XLColor NegativeColor { get; set; } + + XLColor SeriesColor { get; set; } + + #endregion Public Properties + + #region Public Methods + + IXLSparklineStyle SetFirstMarkerColor(XLColor value); + + IXLSparklineStyle SetHighMarkerColor(XLColor value); + + IXLSparklineStyle SetLastMarkerColor(XLColor value); + + IXLSparklineStyle SetLowMarkerColor(XLColor value); + + IXLSparklineStyle SetMarkersColor(XLColor value); + + IXLSparklineStyle SetNegativeColor(XLColor value); + + IXLSparklineStyle SetSeriesColor(XLColor value); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/IXLSparklineVerticalAxis.cs b/ClosedXML/Excel/Sparkline/IXLSparklineVerticalAxis.cs new file mode 100644 index 0000000..17e5d26 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/IXLSparklineVerticalAxis.cs @@ -0,0 +1,32 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLSparklineVerticalAxis + { + #region Public Properties + + Double? ManualMax { get; set; } + + Double? ManualMin { get; set; } + + XLSparklineAxisMinMax MaxAxisType { get; set; } + + XLSparklineAxisMinMax MinAxisType { get; set; } + + #endregion Public Properties + + #region Public Methods + + IXLSparklineVerticalAxis SetManualMax(Double? value); + + IXLSparklineVerticalAxis SetManualMin(Double? value); + + IXLSparklineVerticalAxis SetMaxAxisType(XLSparklineAxisMinMax value); + + IXLSparklineVerticalAxis SetMinAxisType(XLSparklineAxisMinMax value); + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparkLine.cs b/ClosedXML/Excel/Sparkline/XLSparkLine.cs new file mode 100644 index 0000000..e2dbe3a --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparkLine.cs @@ -0,0 +1,91 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLSparkline : IXLSparkline + { + #region Private Fields + + private IXLCell _location; + private IXLRange _sourceData; + + #endregion Private Fields + + #region Public Properties + + public bool IsValid => + Location != null && + SourceData != null && + ((XLAddress)Location.Address).IsValid && + ((XLRangeAddress)SourceData.RangeAddress).IsValid; + + public IXLCell Location + { + get => _location; + set => SetLocation(value); + } + + public IXLRange SourceData + { + get => _sourceData; + set => SetSourceData(value); + } + + public IXLSparklineGroup SparklineGroup { get; } + + #endregion Public Properties + + #region Public Constructors + + /// + /// Create a new sparkline + /// + /// The sparkline group to add the sparkline to + /// The cell to place the sparkline in + /// The range the sparkline gets data from + public XLSparkline(IXLSparklineGroup sparklineGroup, IXLCell cell, IXLRange sourceData) + { + if (sparklineGroup == null) + throw new ArgumentNullException(nameof(sparklineGroup)); + + if (cell == null) + throw new ArgumentNullException(nameof(cell)); + + if (sparklineGroup.Worksheet != cell.Worksheet) + throw new InvalidOperationException("Cell must belong to the same worksheet as the sparkline group"); + + SparklineGroup = sparklineGroup; + Location = cell; + SourceData = sourceData; + } + + #endregion Public Constructors + + #region Public Methods + + public IXLSparkline SetLocation(IXLCell cell) + { + if (cell != null && cell.Worksheet != SparklineGroup.Worksheet) + throw new InvalidOperationException("Cannot move the sparkline to a different worksheet"); + + if (_location != null) + SparklineGroup.Remove(_location); + + _location = cell; + (SparklineGroup as XLSparklineGroup).Add(this); + return this; + } + + public IXLSparkline SetSourceData(IXLRange range) + { + if (range != null && range.RowCount() != 1 && range.ColumnCount() != 1) + throw new ArgumentException("SourceData range must have either a single row or a single column"); + + _sourceData = range; + return this; + } + + #endregion Public Methods + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparkLineGroup.cs b/ClosedXML/Excel/Sparkline/XLSparkLineGroup.cs new file mode 100644 index 0000000..ba5847e --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparkLineGroup.cs @@ -0,0 +1,338 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLSparklineGroup : IXLSparklineGroup + { + #region Public Properties + + public IXLRange DateRange + { + get => _dateRange; + set => SetDateRange(value); + } + + public XLDisplayBlanksAsValues DisplayEmptyCellsAs { get; set; } + + public Boolean DisplayHidden { get; set; } + + public IXLSparklineHorizontalAxis HorizontalAxis { get; } + + public Double LineWeight { get; set; } + + public XLSparklineMarkers ShowMarkers { get; set; } + + private IXLSparklineGroups SparklineGroups => Worksheet.SparklineGroups; + + public IXLSparklineStyle Style + { + get => _style; + set => SetStyle(value); + } + + public XLSparklineType Type { get; set; } + + public IXLSparklineVerticalAxis VerticalAxis { get; } + + /// + /// The worksheet this sparkline group is associated with + /// + public IXLWorksheet Worksheet { get; } + + private IXLRange _dateRange; + private IXLSparklineStyle _style; + + #endregion Public Properties + + #region Public Constructors + + /// + /// Add a new sparkline group copied from an existing sparkline group to the specified worksheet + /// + /// The worksheet the sparkline group is being added to + /// The sparkline group to copy from + /// The new sparkline group added + public XLSparklineGroup(IXLWorksheet targetWorksheet, IXLSparklineGroup copyFrom) + : this(targetWorksheet) + { + CopyFrom(copyFrom); + } + + /// + /// Add a new sparkline group copied from an existing sparkline group to the specified worksheet + /// + /// The new sparkline group added + public XLSparklineGroup(IXLWorksheet targetWorksheet, string locationAddress, string sourceDataAddress) + : this(targetWorksheet) + { + Add(locationAddress, sourceDataAddress); + } + + /// + /// Add a new sparkline group copied from an existing sparkline group to the specified worksheet + /// + /// The new sparkline group added + public XLSparklineGroup(IXLCell location, IXLRange sourceData) + : this(location.Worksheet) + { + Add(location, sourceData); + } + + /// + /// Add a new sparkline group copied from an existing sparkline group to the specified worksheet + /// + /// The new sparkline group added + public XLSparklineGroup(IXLRange locationRange, IXLRange sourceDataRange) + : this(locationRange.Worksheet) + { + Add(locationRange, sourceDataRange); + } + + #endregion Public Constructors + + #region Public Methods + + public IEnumerable Add(IXLRange locationRange, IXLRange sourceDataRange) + { + var singleRow = locationRange.RowCount() == 1; + var singleColumn = locationRange.ColumnCount() == 1; + var newSparklines = new List(); + + if (singleRow && singleColumn) + { + newSparklines.Add(Add(locationRange.FirstCell(), sourceDataRange)); + } + else if (singleRow) + { + if (locationRange.ColumnCount() != sourceDataRange.ColumnCount()) + throw new ArgumentException("locationRange and sourceDataRange must have the same width"); + for (int i = 1; i <= locationRange.ColumnCount(); i++) + { + newSparklines.Add(Add(locationRange.Cell(1, i), sourceDataRange.Column(i).AsRange())); + } + } + else if (singleColumn) + { + if (locationRange.RowCount() != sourceDataRange.RowCount()) + throw new ArgumentException("locationRange and sourceDataRange must have the same height"); + + for (int i = 1; i <= locationRange.RowCount(); i++) + { + newSparklines.Add(Add(locationRange.Cell(i, 1), sourceDataRange.Row(i).AsRange())); + } + } + else + throw new ArgumentException("locationRange must have either a single row or a single column"); + + return newSparklines; + } + + /// + /// Add a sparkline to the group. + /// + /// The cell to add sparklines to. If it already contains a sparkline + /// it will be replaced. + /// The range the sparkline gets data from + /// A newly created sparkline. + public IXLSparkline Add(IXLCell location, IXLRange sourceData) + { + if (location.Worksheet != Worksheet) + throw new ArgumentException("The specified sparkline belongs to the different worksheet"); + + return new XLSparkline(this, location, sourceData); + } + + public IEnumerable Add(string locationRangeAddress, string sourceDataAddress) + { + var sourceDataRange = Worksheet.Workbook.Range(sourceDataAddress) ?? + Worksheet.Range(sourceDataAddress); + return Add(Worksheet.Range(locationRangeAddress), sourceDataRange); + } + + /// + /// Copy the details from a specified sparkline group + /// + /// The sparkline group to copy from + public void CopyFrom(IXLSparklineGroup sparklineGroup) + { + if (sparklineGroup.DateRange != null) + { + DateRange = sparklineGroup.DateRange.Worksheet == sparklineGroup.Worksheet + ? Worksheet.Range(sparklineGroup.DateRange.RangeAddress.ToString()) + : sparklineGroup.DateRange; + } + + DisplayEmptyCellsAs = sparklineGroup.DisplayEmptyCellsAs; + DisplayHidden = sparklineGroup.DisplayHidden; + LineWeight = sparklineGroup.LineWeight; + ShowMarkers = sparklineGroup.ShowMarkers; + Type = sparklineGroup.Type; + + XLSparklineStyle.Copy(sparklineGroup.Style, Style); + XLSparklineHorizontalAxis.Copy(sparklineGroup.HorizontalAxis, HorizontalAxis); + XLSparklineVerticalAxis.Copy(sparklineGroup.VerticalAxis, VerticalAxis); + } + + /// + /// Copy this sparkline group to the specified worksheet + /// + /// The worksheet to copy this sparkline group to + public IXLSparklineGroup CopyTo(IXLWorksheet targetSheet) + { + if (targetSheet == Worksheet) + throw new InvalidOperationException( + "Cannot copy the sparkline group to the same worksheet it belongs to"); + + var copy = targetSheet.SparklineGroups.Add(new XLSparklineGroup(targetSheet, this)); + foreach (var sparkline in _sparklines.Values) + { + var location = targetSheet.Cell(((XLAddress)sparkline.Location.Address).WithoutWorksheet()); + var sourceData = sparkline.SourceData.Worksheet == Worksheet + ? targetSheet.Range(sparkline.SourceData.RangeAddress.ToString()) + : sparkline.SourceData; + + copy.Add(location, sourceData); + } + return copy; + } + + public IEnumerator GetEnumerator() + { + return _sparklines.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLSparkline GetSparkline(IXLCell cell) + { + return _sparklines.TryGetValue(cell, out IXLSparkline sparkline) ? sparkline : null; + } + + public IEnumerable GetSparklines(IXLRangeBase searchRange) + { + foreach (var key in _sparklines.Keys.Where(searchRange.Contains)) + { + yield return GetSparkline(key); + } + } + + /// + /// Remove all sparklines in the specified cell from this group + /// + /// The cell to remove sparklines from + public void Remove(IXLCell cell) + { + if (_sparklines.ContainsKey(cell)) + _sparklines.Remove(cell); + } + + /// + /// Remove the sparkline from this group + /// + /// + public void Remove(IXLSparkline sparkline) + { + Remove(sparkline.Location); + } + + /// + /// Remove all sparklines from this group + /// + public void RemoveAll() + { + _sparklines.Clear(); + } + + public IXLSparklineGroup SetDateRange(IXLRange value) + { + if (value != null) + { + if (value.RowCount() != 1 && value.ColumnCount() != 1) + throw new ArgumentException("The date range must be either one row high or one column wide"); + } + + _dateRange = value; + return this; + } + + public IXLSparklineGroup SetDisplayEmptyCellsAs(XLDisplayBlanksAsValues displayEmptyCellsAs) + { + DisplayEmptyCellsAs = displayEmptyCellsAs; + return this; + } + + public IXLSparklineGroup SetDisplayHidden(Boolean displayHidden) + { + DisplayHidden = displayHidden; + return this; + } + + public IXLSparklineGroup SetLineWeight(Double lineWeight) + { + LineWeight = lineWeight; + return this; + } + + public IXLSparklineGroup SetShowMarkers(XLSparklineMarkers value) + { + ShowMarkers = value; + return this; + } + + public IXLSparklineGroup SetStyle(IXLSparklineStyle value) + { + _style = value ?? throw new ArgumentNullException(nameof(value)); + return this; + } + + public IXLSparklineGroup SetType(XLSparklineType type) + { + Type = type; + return this; + } + + internal IXLSparkline Add(IXLSparkline sparkline) + { + if (sparkline.Location.Worksheet != Worksheet) + throw new ArgumentException("The specified sparkline belongs to the different worksheet"); + + SparklineGroups.Remove(sparkline.Location); + _sparklines[sparkline.Location] = sparkline; + return sparkline; + } + + #endregion Public Methods + + #region Private Fields + + private readonly Dictionary _sparklines = new Dictionary(); + + #endregion Private Fields + + #region Private Constructors + + /// + /// Add a new sparkline group to the specified worksheet + /// + /// The worksheet the sparkline group is being added to + /// The new sparkline group added + internal XLSparklineGroup(IXLWorksheet targetWorksheet) + { + Worksheet = targetWorksheet ?? throw new ArgumentNullException(nameof(targetWorksheet)); + HorizontalAxis = new XLSparklineHorizontalAxis(this); + VerticalAxis = new XLSparklineVerticalAxis(this); + HorizontalAxis.Color = XLColor.Black; + Style = XLSparklineTheme.Default; + LineWeight = 0.75d; + } + + #endregion Private Constructors + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparkLineGroups.cs b/ClosedXML/Excel/Sparkline/XLSparkLineGroups.cs new file mode 100644 index 0000000..daee483 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparkLineGroups.cs @@ -0,0 +1,177 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLSparklineGroups : IXLSparklineGroups + { + #region Public Properties + + public IXLWorksheet Worksheet { get; } + + #endregion Public Properties + + #region Public Constructors + + public XLSparklineGroups(IXLWorksheet worksheet) + { + Worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + } + + #endregion Public Constructors + + #region Public Methods + + /// + /// Add empty sparkline group. + /// + internal IXLSparklineGroup Add() + { + return Add(new XLSparklineGroup(Worksheet)); + } + + /// + /// Add the sparkline group to the collection. + /// + /// The sparkline group to add to the collection + /// The same sparkline group + public IXLSparklineGroup Add(IXLSparklineGroup sparklineGroup) + { + if (sparklineGroup.Worksheet != Worksheet) + throw new ArgumentException("The specified sparkline group belongs to the different worksheet"); + + _sparklineGroups.Add(sparklineGroup); + return sparklineGroup; + } + + public IXLSparklineGroup Add(string locationAddress, string sourceDataAddress) + { + return Add(new XLSparklineGroup(Worksheet, locationAddress, sourceDataAddress)); + } + + public IXLSparklineGroup Add(IXLCell location, IXLRange sourceData) + { + return Add(new XLSparklineGroup(location, sourceData)); + } + + public IXLSparklineGroup Add(IXLRange locationRange, IXLRange sourceDataRange) + { + return Add(new XLSparklineGroup(locationRange, sourceDataRange)); + } + + /// + /// Add a copy of an existing sparkline group to the specified worksheet + /// + /// The sparkline group to copy + /// The worksheet the sparkline group is being added to + /// The new sparkline group added + public IXLSparklineGroup AddCopy(IXLSparklineGroup sparklineGroupToCopy, IXLWorksheet targetWorksheet) + { + var sparklineGroup = new XLSparklineGroup(targetWorksheet, sparklineGroupToCopy); + _sparklineGroups.Add(sparklineGroup); + return sparklineGroup; + } + + /// + /// Copy this sparkline group to a different worksheet + /// + /// The worksheet to copy the sparkline group to + public void CopyTo(IXLWorksheet targetSheet) + { + foreach (var slg in this) + { + slg.CopyTo(targetSheet); + } + } + + /// + /// Search for the first sparkline that is in the specified cell + /// + /// The cell to find the sparkline for + /// The sparkline in the cell or null if no sparklines are found + public IXLSparkline GetSparkline(IXLCell cell) + { + return _sparklineGroups + .Select(g => g.GetSparkline(cell)) + .FirstOrDefault(s => s != null); + } + + /// + /// Find all sparklines located in a given range + /// + /// The range to search + /// The sparkline in the cell or null if no sparklines are found + public IEnumerable GetSparklines(IXLRangeBase searchRange) + { + return _sparklineGroups + .SelectMany(g => g.GetSparklines(searchRange)); + } + + public IEnumerator GetEnumerator() + { + return _sparklineGroups.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Remove all sparklines in the specified cell + /// + /// The cell to remove sparklines from + public void Remove(IXLCell cell) + { + _sparklineGroups + .AsParallel() + .ForEach(g => g.Remove(cell)); + } + + public void Remove(IXLRangeBase range) + { + var sparklinesToRemove = _sparklineGroups + .SelectMany(g => g) + .Where(sparkline => range.Contains(sparkline.Location)) + .ToList(); + + sparklinesToRemove.ForEach(Remove); + } + + /// + /// Remove the sparkline group from the worksheet + /// + /// The sparkline group to remove + public void Remove(IXLSparklineGroup sparklineGroup) + { + _sparklineGroups.Remove(sparklineGroup); + } + + /// + /// Remove the sparkline from the worksheet + /// + /// The sparkline to remove + private void Remove(IXLSparkline sparkline) + { + sparkline.SparklineGroup.Remove(sparkline); + } + + /// + /// Remove all sparkline groups and their contents from the worksheet. + /// + public void RemoveAll() + { + _sparklineGroups.Clear(); + } + + #endregion Public Methods + + #region Private Fields + + private readonly List _sparklineGroups = new List(); + + #endregion Private Fields + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparklineHorizontalAxis.cs b/ClosedXML/Excel/Sparkline/XLSparklineHorizontalAxis.cs new file mode 100644 index 0000000..6570865 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparklineHorizontalAxis.cs @@ -0,0 +1,60 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLSparklineHorizontalAxis : IXLSparklineHorizontalAxis + { + #region Public Properties + + public XLColor Color { get; set; } + + public bool DateAxis => SparklineGroup.DateRange != null; + + public bool IsVisible { get; set; } + + public bool RightToLeft { get; set; } + + public IXLSparklineGroup SparklineGroup { get; } + + #endregion Public Properties + + #region Public Constructors + + public XLSparklineHorizontalAxis(IXLSparklineGroup sparklineGroup) + { + SparklineGroup = sparklineGroup ?? throw new ArgumentNullException(nameof(sparklineGroup)); + } + + #endregion Public Constructors + + #region Public Methods + + public IXLSparklineHorizontalAxis SetColor(XLColor value) + { + Color = value ?? throw new ArgumentNullException(nameof(value)); + return this; + } + + public IXLSparklineHorizontalAxis SetRightToLeft(bool value) + { + RightToLeft = value; + return this; + } + + public IXLSparklineHorizontalAxis SetVisible(bool value) + { + IsVisible = value; + return this; + } + + #endregion Public Methods + + public static void Copy(IXLSparklineHorizontalAxis from, IXLSparklineHorizontalAxis to) + { + to.Color = from.Color; + to.IsVisible = from.IsVisible; + to.RightToLeft = from.RightToLeft; + } + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparklineStyle.cs b/ClosedXML/Excel/Sparkline/XLSparklineStyle.cs new file mode 100644 index 0000000..1cdc30b --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparklineStyle.cs @@ -0,0 +1,149 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLSparklineStyle : IXLSparklineStyle, IEquatable + { + #region Public Properties + + public XLColor FirstMarkerColor { get; set; } + + public XLColor HighMarkerColor { get; set; } + + public XLColor LastMarkerColor { get; set; } + + public XLColor LowMarkerColor { get; set; } + + public XLColor MarkersColor { get; set; } + + public XLColor NegativeColor { get; set; } + + public XLColor SeriesColor { get; set; } + + #endregion Public Properties + + #region Public Methods + + public IXLSparklineStyle SetFirstMarkerColor(XLColor value) + { + FirstMarkerColor = value; + return this; + } + + public IXLSparklineStyle SetHighMarkerColor(XLColor value) + { + HighMarkerColor = value; + return this; + } + + public IXLSparklineStyle SetLastMarkerColor(XLColor value) + { + LastMarkerColor = value; + return this; + } + + public IXLSparklineStyle SetLowMarkerColor(XLColor value) + { + LowMarkerColor = value; + return this; + } + + public IXLSparklineStyle SetMarkersColor(XLColor value) + { + MarkersColor = value; + return this; + } + + public IXLSparklineStyle SetNegativeColor(XLColor value) + { + NegativeColor = value; + return this; + } + + public IXLSparklineStyle SetSeriesColor(XLColor value) + { + SeriesColor = value; + return this; + } + + #endregion Public Methods + + public static void Copy(IXLSparklineStyle from, IXLSparklineStyle to) + { + to.FirstMarkerColor = from.FirstMarkerColor; + to.HighMarkerColor = from.HighMarkerColor; + to.LastMarkerColor = from.LastMarkerColor; + to.LowMarkerColor = from.LowMarkerColor; + to.MarkersColor = from.MarkersColor; + to.NegativeColor = from.NegativeColor; + to.SeriesColor = from.SeriesColor; + } + + #region IEquatable implementation + + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(XLSparklineStyle left, XLSparklineStyle right) + { + return !Equals(left, right); + } + + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(XLSparklineStyle left, XLSparklineStyle right) + { + return Equals(left, right); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + public bool Equals(XLSparklineStyle other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return FirstMarkerColor.Equals(other.FirstMarkerColor) + && HighMarkerColor.Equals(other.HighMarkerColor) + && LastMarkerColor.Equals(other.LastMarkerColor) + && LowMarkerColor.Equals(other.LowMarkerColor) + && MarkersColor.Equals(other.MarkersColor) + && NegativeColor.Equals(other.NegativeColor) + && SeriesColor.Equals(other.SeriesColor); + } + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof(XLSparklineStyle)) return false; + return Equals((XLSparklineStyle)obj); + } + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode() + { + unchecked + { + var hashCode = FirstMarkerColor.GetHashCode(); + hashCode = (hashCode * 397) ^ HighMarkerColor.GetHashCode(); + hashCode = (hashCode * 397) ^ LastMarkerColor.GetHashCode(); + hashCode = (hashCode * 397) ^ LowMarkerColor.GetHashCode(); + hashCode = (hashCode * 397) ^ MarkersColor.GetHashCode(); + hashCode = (hashCode * 397) ^ NegativeColor.GetHashCode(); + hashCode = (hashCode * 397) ^ SeriesColor.GetHashCode(); + return hashCode; + } + } + + #endregion IEquatable implementation + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparklineTheme.cs b/ClosedXML/Excel/Sparkline/XLSparklineTheme.cs new file mode 100644 index 0000000..8e1dbe7 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparklineTheme.cs @@ -0,0 +1,432 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + public static class XLSparklineTheme + { + #region Public Properties + + public static IXLSparklineStyle Default => Dark5; + + #region Dark + + public static IXLSparklineStyle Dark1 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Text1, 0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893), + MarkersColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Text1, 0.249977111117893) + }; + + public static IXLSparklineStyle Dark2 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Text1, 0.34998626667073579), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893), + MarkersColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Background1, -0.249977111117893) + }; + + public static IXLSparklineStyle Dark3 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF323232"), + NegativeColor = XLColor.FromHtml("FFD00000"), + MarkersColor = XLColor.FromHtml("FFD00000"), + HighMarkerColor = XLColor.FromHtml("FFD00000"), + LowMarkerColor = XLColor.FromHtml("FFD00000"), + FirstMarkerColor = XLColor.FromHtml("FFD00000"), + LastMarkerColor = XLColor.FromHtml("FFD00000") + }; + + public static IXLSparklineStyle Dark4 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF000000"), + NegativeColor = XLColor.FromHtml("FF0070C0"), + MarkersColor = XLColor.FromHtml("FF0070C0"), + HighMarkerColor = XLColor.FromHtml("FF0070C0"), + LowMarkerColor = XLColor.FromHtml("FF0070C0"), + FirstMarkerColor = XLColor.FromHtml("FF0070C0"), + LastMarkerColor = XLColor.FromHtml("FF0070C0") + }; + + public static IXLSparklineStyle Dark5 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF376092"), + NegativeColor = XLColor.FromHtml("FFD00000"), + MarkersColor = XLColor.FromHtml("FFD00000"), + HighMarkerColor = XLColor.FromHtml("FFD00000"), + LowMarkerColor = XLColor.FromHtml("FFD00000"), + FirstMarkerColor = XLColor.FromHtml("FFD00000"), + LastMarkerColor = XLColor.FromHtml("FFD00000") + }; + + public static IXLSparklineStyle Dark6 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF0070C0"), + NegativeColor = XLColor.Black, + MarkersColor = XLColor.Black, + HighMarkerColor = XLColor.Black, + LowMarkerColor = XLColor.Black, + FirstMarkerColor = XLColor.Black, + LastMarkerColor = XLColor.Black + }; + + #endregion Dark + + #region Colorful + + public static IXLSparklineStyle Colorful1 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF5F5F5F"), + NegativeColor = XLColor.FromHtml("FFFFB620"), + MarkersColor = XLColor.FromHtml("FFD70077"), + HighMarkerColor = XLColor.FromHtml("FF56BE79"), + LowMarkerColor = XLColor.FromHtml("FFFF5055"), + FirstMarkerColor = XLColor.FromHtml("FF5687C2"), + LastMarkerColor = XLColor.FromHtml("FF359CEB") + }; + + public static IXLSparklineStyle Colorful2 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF5687C2"), + NegativeColor = XLColor.FromHtml("FFFFB620"), + MarkersColor = XLColor.FromHtml("FFD70077"), + HighMarkerColor = XLColor.FromHtml("FF56BE79"), + LowMarkerColor = XLColor.FromHtml("FFFF5055"), + FirstMarkerColor = XLColor.FromHtml("FF777777"), + LastMarkerColor = XLColor.FromHtml("FF359CEB") + }; + + public static IXLSparklineStyle Colorful3 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FFC6EFCE"), + NegativeColor = XLColor.FromHtml("FFFFC7CE"), + MarkersColor = XLColor.FromHtml("FF8CADD6"), + HighMarkerColor = XLColor.FromHtml("FF60D276"), + LowMarkerColor = XLColor.FromHtml("FFFF5367"), + FirstMarkerColor = XLColor.FromHtml("FFFFDC47"), + LastMarkerColor = XLColor.FromHtml("FFFFEB9C") + }; + + public static IXLSparklineStyle Colorful4 => new XLSparklineStyle + { + SeriesColor = XLColor.FromHtml("FF00B050"), + NegativeColor = XLColor.FromHtml("FFFF0000"), + MarkersColor = XLColor.FromHtml("FF0070C0"), + HighMarkerColor = XLColor.FromHtml("FF00B050"), + LowMarkerColor = XLColor.FromHtml("FFFF0000"), + FirstMarkerColor = XLColor.FromHtml("FFFFC000"), + LastMarkerColor = XLColor.FromHtml("FFFFC000") + }; + + public static IXLSparklineStyle Colorful5 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Text2), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent6), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2) + }; + + public static IXLSparklineStyle Colorful6 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Text1), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent6), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2) + }; + + #endregion Colorful + + #region Accent + + public static IXLSparklineStyle Accent1 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent1), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent2), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent2 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent2), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent3), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent3 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent3), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent4), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent4 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent4), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent5), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent5 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent5), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent6), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent6 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent6), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent1), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893) + }; + + #endregion Accent + + #region Accent Darker 25% + + public static IXLSparklineStyle Accent1Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent2), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent2Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent3), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent3Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent4), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent4Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent5), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent5Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent6), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent6Darker25 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent1), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893) + }; + + #endregion Accent Darker 25% + + #region Accent Darker 50% + + public static IXLSparklineStyle Accent1Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent2), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, 0.39997558519241921) + }; + + public static IXLSparklineStyle Accent2Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent3), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, 0.39997558519241921) + }; + + public static IXLSparklineStyle Accent3Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent4), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, 0.39997558519241921) + }; + + public static IXLSparklineStyle Accent4Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent5), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, 0.39997558519241921) + }; + + public static IXLSparklineStyle Accent5Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent6), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, 0.39997558519241921) + }; + + public static IXLSparklineStyle Accent6Darker50 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.499984740745262), + NegativeColor = XLColor.FromTheme(XLThemeColor.Accent1), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.499984740745262), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, 0.39997558519241921), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, 0.39997558519241921) + }; + + #endregion Accent Darker 50% + + #region Accent Lighter 40% + + public static IXLSparklineStyle Accent1Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent1, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent1, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent1, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent2Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent2, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent2, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent2, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent3Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent3, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent3, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent3, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent4Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent4, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent4, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent4, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent5Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent5, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent5, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent5, -0.249977111117893) + }; + + public static IXLSparklineStyle Accent6Lighter40 => new XLSparklineStyle + { + SeriesColor = XLColor.FromTheme(XLThemeColor.Accent6, 0.39997558519241921), + NegativeColor = XLColor.FromTheme(XLThemeColor.Background1, -0.499984740745262), + MarkersColor = XLColor.FromTheme(XLThemeColor.Accent6, 0.79998168889431442), + HighMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.499984740745262), + LowMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.499984740745262), + FirstMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893), + LastMarkerColor = XLColor.FromTheme(XLThemeColor.Accent6, -0.249977111117893) + }; + + #endregion Accent Lighter 40% + + #endregion Public Properties + } +} diff --git a/ClosedXML/Excel/Sparkline/XLSparklineVerticalAxis.cs b/ClosedXML/Excel/Sparkline/XLSparklineVerticalAxis.cs new file mode 100644 index 0000000..9831e47 --- /dev/null +++ b/ClosedXML/Excel/Sparkline/XLSparklineVerticalAxis.cs @@ -0,0 +1,100 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLSparklineVerticalAxis : IXLSparklineVerticalAxis + { + #region Public Properties + + public Double? ManualMax + { + get => _manualMax; + set => SetManualMax(value); + } + + public Double? ManualMin + { + get => _manualMin; + set => SetManualMin(value); + } + + public XLSparklineAxisMinMax MaxAxisType + { + get => _maxAxisType; + set => SetMaxAxisType(value); + } + + public XLSparklineAxisMinMax MinAxisType + { + get => _minAxisType; + set => SetMinAxisType(value); + } + + #endregion Public Properties + + #region Public Methods + + public IXLSparklineVerticalAxis SetManualMax(Double? manualMax) + { + if (manualMax != null) + MaxAxisType = XLSparklineAxisMinMax.Custom; + + _manualMax = manualMax; + return this; + } + + public IXLSparklineVerticalAxis SetManualMin(Double? manualMin) + { + if (manualMin != null) + MinAxisType = XLSparklineAxisMinMax.Custom; + + _manualMin = manualMin; + return this; + } + + public IXLSparklineVerticalAxis SetMaxAxisType(XLSparklineAxisMinMax maxAxisType) + { + if (maxAxisType != XLSparklineAxisMinMax.Custom) + _manualMax = null; + + _maxAxisType = maxAxisType; + return this; + } + + public IXLSparklineVerticalAxis SetMinAxisType(XLSparklineAxisMinMax minAxisType) + { + if (minAxisType != XLSparklineAxisMinMax.Custom) + _manualMin = null; + + _minAxisType = minAxisType; + return this; + } + + #endregion Public Methods + + #region Private Fields + + private Double? _manualMax; + private Double? _manualMin; + private XLSparklineAxisMinMax _maxAxisType; + private XLSparklineAxisMinMax _minAxisType; + + #endregion Private Fields + + public IXLSparklineGroup SparklineGroup { get; } + + public XLSparklineVerticalAxis(IXLSparklineGroup sparklineGroup) + { + SparklineGroup = sparklineGroup ?? throw new ArgumentNullException(nameof(sparklineGroup)); + } + + public static void Copy(IXLSparklineVerticalAxis from, IXLSparklineVerticalAxis to) + { + to.ManualMax = from.ManualMax; + to.ManualMin = from.ManualMin; + to.MaxAxisType = from.MaxAxisType; + to.MinAxisType = from.MinAxisType; + } + } +} diff --git a/ClosedXML/Excel/Style/Colors/XLColor_Internal.cs b/ClosedXML/Excel/Style/Colors/XLColor_Internal.cs new file mode 100644 index 0000000..3919bf8 --- /dev/null +++ b/ClosedXML/Excel/Style/Colors/XLColor_Internal.cs @@ -0,0 +1,58 @@ +using System; +using System.Drawing; + +namespace ClosedXML.Excel +{ + public partial class XLColor + { + internal XLColorKey Key { get; private set; } + + private XLColor(XLColor defaultColor) : this(defaultColor.Key) + { + } + + private XLColor() : this(new XLColorKey()) + { + HasValue = false; + } + + private XLColor(Color color) : this(new XLColorKey + { + Color = color, + ColorType = XLColorType.Color + }) + { + } + + private XLColor(Int32 index) : this(new XLColorKey + { + Indexed = index, + ColorType = XLColorType.Indexed + }) + { + } + + private XLColor(XLThemeColor themeColor) : this(new XLColorKey + { + ThemeColor = themeColor, + ColorType = XLColorType.Theme + }) + { + } + + private XLColor(XLThemeColor themeColor, Double themeTint) : this(new XLColorKey + { + ThemeColor = themeColor, + ThemeTint = themeTint, + ColorType = XLColorType.Theme + }) + { + } + + internal XLColor(XLColorKey key) + { + Key = key; + HasValue = true; + } + } +} diff --git a/ClosedXML/Excel/Style/Colors/XLColor_Public.cs b/ClosedXML/Excel/Style/Colors/XLColor_Public.cs new file mode 100644 index 0000000..a75edcf --- /dev/null +++ b/ClosedXML/Excel/Style/Colors/XLColor_Public.cs @@ -0,0 +1,154 @@ +using System; +using System.Drawing; + +namespace ClosedXML.Excel +{ + public enum XLColorType + { + Color, + Theme, + Indexed + } + + public enum XLThemeColor + { + Background1, + Text1, + Background2, + Text2, + Accent1, + Accent2, + Accent3, + Accent4, + Accent5, + Accent6, + Hyperlink, + FollowedHyperlink + } + + public partial class XLColor : IEquatable + { + /// + /// Usually indexed colors are limited to max 63 + /// Index 81 is some special case. + /// Some people claim it's the index for tooltip color. + /// We'll return normal black when index 81 is found. + /// + private const Int32 TOOLTIPCOLORINDEX = 81; + + public Boolean HasValue { get; private set; } + + public XLColorType ColorType + { + get { return Key.ColorType; } + } + + public Color Color + { + get + { + if (ColorType == XLColorType.Theme) + throw new InvalidOperationException("Cannot convert theme color to Color."); + + if (ColorType == XLColorType.Indexed) + if (Indexed == TOOLTIPCOLORINDEX) + return Color.FromArgb(255, Color.Black); + else + return IndexedColors[Indexed].Color; + + return Key.Color; + } + } + + public Int32 Indexed + { + get + { + if (ColorType == XLColorType.Theme) + throw new InvalidOperationException("Cannot convert theme color to indexed color."); + + if (ColorType == XLColorType.Indexed) + return Key.Indexed; + + throw new InvalidOperationException("Cannot convert Color to indexed color."); + } + } + + public XLThemeColor ThemeColor + { + get + { + if (ColorType == XLColorType.Theme) + return Key.ThemeColor; + + if (ColorType == XLColorType.Indexed) + throw new InvalidOperationException("Cannot convert indexed color to theme color."); + + throw new InvalidOperationException("Cannot convert Color to theme color."); + } + } + + public Double ThemeTint + { + get + { + if (ColorType == XLColorType.Theme) + return Key.ThemeTint; + + if (ColorType == XLColorType.Indexed) + throw new InvalidOperationException("Cannot extract theme tint from an indexed color."); + + return Color.A / 255.0; + } + } + + #region IEquatable Members + + public bool Equals(XLColor other) + { + return Key == other.Key; + } + + #endregion IEquatable Members + + public override bool Equals(object obj) + { + return Equals((XLColor)obj); + } + + public override int GetHashCode() + { + var hashCode = 229333804; + hashCode = hashCode * -1521134295 + HasValue.GetHashCode(); + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + public override string ToString() + { + if (ColorType == XLColorType.Color) + return Color.ToHex(); + + if (ColorType == XLColorType.Theme) + return String.Format("Color Theme: {0}, Tint: {1}", ThemeColor.ToString(), ThemeTint.ToString()); + + return "Color Index: " + Indexed; + } + + public static Boolean operator ==(XLColor left, XLColor right) + { + // If both are null, or both are same instance, return true. + if (ReferenceEquals(left, right)) return true; + + // If one is null, but not both, return false. + if ((left as object) == null || (right as Object) == null) return false; + + return left.Equals(right); + } + + public static Boolean operator !=(XLColor left, XLColor right) + { + return !(left == right); + } + } +} diff --git a/ClosedXML/Excel/Style/Colors/XLColor_Static.cs b/ClosedXML/Excel/Style/Colors/XLColor_Static.cs new file mode 100644 index 0000000..9fa5bc5 --- /dev/null +++ b/ClosedXML/Excel/Style/Colors/XLColor_Static.cs @@ -0,0 +1,1659 @@ +using ClosedXML.Excel.Caching; +using ClosedXML.Utils; +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ClosedXML.Excel +{ + public partial class XLColor + { + private static readonly XLColorRepository Repository = new XLColorRepository(key => new XLColor(key)); + + private static readonly Dictionary ByColor = new Dictionary(); + private static readonly Object ByColorLock = new Object(); + + internal static XLColor FromKey(ref XLColorKey key) + { + return Repository.GetOrCreate(ref key); + } + + public static XLColor FromColor(Color color) + { + var key = new XLColorKey + { + ColorType = XLColorType.Color, + Color = color + }; + return FromKey(ref key); + } + + public static XLColor FromArgb(Int32 argb) + { + return FromColor(Color.FromArgb(argb)); + } + + public static XLColor FromArgb(Int32 r, Int32 g, Int32 b) + { + return FromColor(Color.FromArgb(r, g, b)); + } + + public static XLColor FromArgb(Int32 a, Int32 r, Int32 g, Int32 b) + { + return FromColor(Color.FromArgb(a, r, g, b)); + } + +#if _NETFRAMEWORK_ + public static XLColor FromKnownColor(KnownColor color) + { + return FromColor(Color.FromKnownColor(color)); + } +#endif + + public static XLColor FromName(String name) + { + return FromColor(Color.FromName(name)); + } + + public static XLColor FromHtml(String htmlColor) + { + return FromColor(ColorStringParser.ParseFromHtml(htmlColor)); + } + + public static XLColor FromIndex(Int32 index) + { + var key = new XLColorKey + { + ColorType = XLColorType.Indexed, + Indexed = index + }; + return FromKey(ref key); + } + + public static XLColor FromTheme(XLThemeColor themeColor) + { + var key = new XLColorKey + { + ColorType = XLColorType.Theme, + ThemeColor = themeColor + }; + return FromKey(ref key); + } + + public static XLColor FromTheme(XLThemeColor themeColor, Double themeTint) + { + var key = new XLColorKey + { + ColorType = XLColorType.Theme, + ThemeColor = themeColor, + ThemeTint = themeTint + }; + return FromKey(ref key); + } + + private static Dictionary _indexedColors; + + public static Dictionary IndexedColors + { + get + { + if (_indexedColors == null) + { + var retVal = new Dictionary + { + {0, FromHtml("#FF000000")}, + {1, FromHtml("#FFFFFFFF")}, + {2, FromHtml("#FFFF0000")}, + {3, FromHtml("#FF00FF00")}, + {4, FromHtml("#FF0000FF")}, + {5, FromHtml("#FFFFFF00")}, + {6, FromHtml("#FFFF00FF")}, + {7, FromHtml("#FF00FFFF")}, + {8, FromHtml("#FF000000")}, + {9, FromHtml("#FFFFFFFF")}, + {10, FromHtml("#FFFF0000")}, + {11, FromHtml("#FF00FF00")}, + {12, FromHtml("#FF0000FF")}, + {13, FromHtml("#FFFFFF00")}, + {14, FromHtml("#FFFF00FF")}, + {15, FromHtml("#FF00FFFF")}, + {16, FromHtml("#FF800000")}, + {17, FromHtml("#FF008000")}, + {18, FromHtml("#FF000080")}, + {19, FromHtml("#FF808000")}, + {20, FromHtml("#FF800080")}, + {21, FromHtml("#FF008080")}, + {22, FromHtml("#FFC0C0C0")}, + {23, FromHtml("#FF808080")}, + {24, FromHtml("#FF9999FF")}, + {25, FromHtml("#FF993366")}, + {26, FromHtml("#FFFFFFCC")}, + {27, FromHtml("#FFCCFFFF")}, + {28, FromHtml("#FF660066")}, + {29, FromHtml("#FFFF8080")}, + {30, FromHtml("#FF0066CC")}, + {31, FromHtml("#FFCCCCFF")}, + {32, FromHtml("#FF000080")}, + {33, FromHtml("#FFFF00FF")}, + {34, FromHtml("#FFFFFF00")}, + {35, FromHtml("#FF00FFFF")}, + {36, FromHtml("#FF800080")}, + {37, FromHtml("#FF800000")}, + {38, FromHtml("#FF008080")}, + {39, FromHtml("#FF0000FF")}, + {40, FromHtml("#FF00CCFF")}, + {41, FromHtml("#FFCCFFFF")}, + {42, FromHtml("#FFCCFFCC")}, + {43, FromHtml("#FFFFFF99")}, + {44, FromHtml("#FF99CCFF")}, + {45, FromHtml("#FFFF99CC")}, + {46, FromHtml("#FFCC99FF")}, + {47, FromHtml("#FFFFCC99")}, + {48, FromHtml("#FF3366FF")}, + {49, FromHtml("#FF33CCCC")}, + {50, FromHtml("#FF99CC00")}, + {51, FromHtml("#FFFFCC00")}, + {52, FromHtml("#FFFF9900")}, + {53, FromHtml("#FFFF6600")}, + {54, FromHtml("#FF666699")}, + {55, FromHtml("#FF969696")}, + {56, FromHtml("#FF003366")}, + {57, FromHtml("#FF339966")}, + {58, FromHtml("#FF003300")}, + {59, FromHtml("#FF333300")}, + {60, FromHtml("#FF993300")}, + {61, FromHtml("#FF993366")}, + {62, FromHtml("#FF333399")}, + {63, FromHtml("#FF333333")}, + {64, FromColor(Color.Transparent)} + }; + _indexedColors = retVal; + } + return _indexedColors; + } + } + + internal static bool IsNullOrTransparent(XLColor color) + { + return color == null + || !color.HasValue + || IsTransparent(color.Key); + } + + internal static bool IsTransparent(in XLColorKey colorKey) + { + return colorKey == NoColor.Key + || (colorKey.ColorType == XLColorType.Indexed && colorKey.Indexed == 64); + } + + public static XLColor NoColor { get; } = new XLColor(); + + public static XLColor AliceBlue { get { return FromColor(Color.AliceBlue); } } + + public static XLColor AntiqueWhite { get { return FromColor(Color.AntiqueWhite); } } + + public static XLColor Aqua { get { return FromColor(Color.Aqua); } } + + public static XLColor Aquamarine { get { return FromColor(Color.Aquamarine); } } + + public static XLColor Azure { get { return FromColor(Color.Azure); } } + + public static XLColor Beige { get { return FromColor(Color.Beige); } } + + public static XLColor Bisque { get { return FromColor(Color.Bisque); } } + + public static XLColor Black { get { return FromColor(Color.Black); } } + + public static XLColor BlanchedAlmond { get { return FromColor(Color.BlanchedAlmond); } } + + public static XLColor Blue { get { return FromColor(Color.Blue); } } + + public static XLColor BlueViolet { get { return FromColor(Color.BlueViolet); } } + + public static XLColor Brown { get { return FromColor(Color.Brown); } } + + public static XLColor BurlyWood { get { return FromColor(Color.BurlyWood); } } + + public static XLColor CadetBlue { get { return FromColor(Color.CadetBlue); } } + + public static XLColor Chartreuse { get { return FromColor(Color.Chartreuse); } } + + public static XLColor Chocolate { get { return FromColor(Color.Chocolate); } } + + public static XLColor Coral { get { return FromColor(Color.Coral); } } + + public static XLColor CornflowerBlue { get { return FromColor(Color.CornflowerBlue); } } + + public static XLColor Cornsilk { get { return FromColor(Color.Cornsilk); } } + + public static XLColor Crimson { get { return FromColor(Color.Crimson); } } + + public static XLColor Cyan { get { return FromColor(Color.Cyan); } } + + public static XLColor DarkBlue { get { return FromColor(Color.DarkBlue); } } + + public static XLColor DarkCyan { get { return FromColor(Color.DarkCyan); } } + + public static XLColor DarkGoldenrod { get { return FromColor(Color.DarkGoldenrod); } } + + public static XLColor DarkGray { get { return FromColor(Color.DarkGray); } } + + public static XLColor DarkGreen { get { return FromColor(Color.DarkGreen); } } + + public static XLColor DarkKhaki { get { return FromColor(Color.DarkKhaki); } } + + public static XLColor DarkMagenta { get { return FromColor(Color.DarkMagenta); } } + + public static XLColor DarkOliveGreen { get { return FromColor(Color.DarkOliveGreen); } } + + public static XLColor DarkOrange { get { return FromColor(Color.DarkOrange); } } + + public static XLColor DarkOrchid { get { return FromColor(Color.DarkOrchid); } } + + public static XLColor DarkRed { get { return FromColor(Color.DarkRed); } } + + public static XLColor DarkSalmon { get { return FromColor(Color.DarkSalmon); } } + + public static XLColor DarkSeaGreen { get { return FromColor(Color.DarkSeaGreen); } } + + public static XLColor DarkSlateBlue { get { return FromColor(Color.DarkSlateBlue); } } + + public static XLColor DarkSlateGray { get { return FromColor(Color.DarkSlateGray); } } + + public static XLColor DarkTurquoise { get { return FromColor(Color.DarkTurquoise); } } + + public static XLColor DarkViolet { get { return FromColor(Color.DarkViolet); } } + + public static XLColor DeepPink { get { return FromColor(Color.DeepPink); } } + + public static XLColor DeepSkyBlue { get { return FromColor(Color.DeepSkyBlue); } } + + public static XLColor DimGray { get { return FromColor(Color.DimGray); } } + + public static XLColor DodgerBlue { get { return FromColor(Color.DodgerBlue); } } + + public static XLColor Firebrick { get { return FromColor(Color.Firebrick); } } + + public static XLColor FloralWhite { get { return FromColor(Color.FloralWhite); } } + + public static XLColor ForestGreen { get { return FromColor(Color.ForestGreen); } } + + public static XLColor Fuchsia { get { return FromColor(Color.Fuchsia); } } + + public static XLColor Gainsboro { get { return FromColor(Color.Gainsboro); } } + + public static XLColor GhostWhite { get { return FromColor(Color.GhostWhite); } } + + public static XLColor Gold { get { return FromColor(Color.Gold); } } + + public static XLColor Goldenrod { get { return FromColor(Color.Goldenrod); } } + + public static XLColor Gray { get { return FromColor(Color.Gray); } } + + public static XLColor Green { get { return FromColor(Color.Green); } } + + public static XLColor GreenYellow { get { return FromColor(Color.GreenYellow); } } + + public static XLColor Honeydew { get { return FromColor(Color.Honeydew); } } + + public static XLColor HotPink { get { return FromColor(Color.HotPink); } } + + public static XLColor IndianRed { get { return FromColor(Color.IndianRed); } } + + public static XLColor Indigo { get { return FromColor(Color.Indigo); } } + + public static XLColor Ivory { get { return FromColor(Color.Ivory); } } + + public static XLColor Khaki { get { return FromColor(Color.Khaki); } } + + public static XLColor Lavender { get { return FromColor(Color.Lavender); } } + + public static XLColor LavenderBlush { get { return FromColor(Color.LavenderBlush); } } + + public static XLColor LawnGreen { get { return FromColor(Color.LawnGreen); } } + + public static XLColor LemonChiffon { get { return FromColor(Color.LemonChiffon); } } + + public static XLColor LightBlue { get { return FromColor(Color.LightBlue); } } + + public static XLColor LightCoral { get { return FromColor(Color.LightCoral); } } + + public static XLColor LightCyan { get { return FromColor(Color.LightCyan); } } + + public static XLColor LightGoldenrodYellow { get { return FromColor(Color.LightGoldenrodYellow); } } + + public static XLColor LightGray { get { return FromColor(Color.LightGray); } } + + public static XLColor LightGreen { get { return FromColor(Color.LightGreen); } } + + public static XLColor LightPink { get { return FromColor(Color.LightPink); } } + + public static XLColor LightSalmon { get { return FromColor(Color.LightSalmon); } } + + public static XLColor LightSeaGreen { get { return FromColor(Color.LightSeaGreen); } } + + public static XLColor LightSkyBlue { get { return FromColor(Color.LightSkyBlue); } } + + public static XLColor LightSlateGray { get { return FromColor(Color.LightSlateGray); } } + + public static XLColor LightSteelBlue { get { return FromColor(Color.LightSteelBlue); } } + + public static XLColor LightYellow { get { return FromColor(Color.LightYellow); } } + + public static XLColor Lime { get { return FromColor(Color.Lime); } } + + public static XLColor LimeGreen { get { return FromColor(Color.LimeGreen); } } + + public static XLColor Linen { get { return FromColor(Color.Linen); } } + + public static XLColor Magenta { get { return FromColor(Color.Magenta); } } + + public static XLColor Maroon { get { return FromColor(Color.Maroon); } } + + public static XLColor MediumAquamarine { get { return FromColor(Color.MediumAquamarine); } } + + public static XLColor MediumBlue { get { return FromColor(Color.MediumBlue); } } + + public static XLColor MediumOrchid { get { return FromColor(Color.MediumOrchid); } } + + public static XLColor MediumPurple { get { return FromColor(Color.MediumPurple); } } + + public static XLColor MediumSeaGreen { get { return FromColor(Color.MediumSeaGreen); } } + + public static XLColor MediumSlateBlue { get { return FromColor(Color.MediumSlateBlue); } } + + public static XLColor MediumSpringGreen { get { return FromColor(Color.MediumSpringGreen); } } + + public static XLColor MediumTurquoise { get { return FromColor(Color.MediumTurquoise); } } + + public static XLColor MediumVioletRed { get { return FromColor(Color.MediumVioletRed); } } + + public static XLColor MidnightBlue { get { return FromColor(Color.MidnightBlue); } } + + public static XLColor MintCream { get { return FromColor(Color.MintCream); } } + + public static XLColor MistyRose { get { return FromColor(Color.MistyRose); } } + + public static XLColor Moccasin { get { return FromColor(Color.Moccasin); } } + + public static XLColor NavajoWhite { get { return FromColor(Color.NavajoWhite); } } + + public static XLColor Navy { get { return FromColor(Color.Navy); } } + + public static XLColor OldLace { get { return FromColor(Color.OldLace); } } + + public static XLColor Olive { get { return FromColor(Color.Olive); } } + + public static XLColor OliveDrab { get { return FromColor(Color.OliveDrab); } } + + public static XLColor Orange { get { return FromColor(Color.Orange); } } + + public static XLColor OrangeRed { get { return FromColor(Color.OrangeRed); } } + + public static XLColor Orchid { get { return FromColor(Color.Orchid); } } + + public static XLColor PaleGoldenrod { get { return FromColor(Color.PaleGoldenrod); } } + + public static XLColor PaleGreen { get { return FromColor(Color.PaleGreen); } } + + public static XLColor PaleTurquoise { get { return FromColor(Color.PaleTurquoise); } } + + public static XLColor PaleVioletRed { get { return FromColor(Color.PaleVioletRed); } } + + public static XLColor PapayaWhip { get { return FromColor(Color.PapayaWhip); } } + + public static XLColor PeachPuff { get { return FromColor(Color.PeachPuff); } } + + public static XLColor Peru { get { return FromColor(Color.Peru); } } + + public static XLColor Pink { get { return FromColor(Color.Pink); } } + + public static XLColor Plum { get { return FromColor(Color.Plum); } } + + public static XLColor PowderBlue { get { return FromColor(Color.PowderBlue); } } + + public static XLColor Purple { get { return FromColor(Color.Purple); } } + + public static XLColor Red { get { return FromColor(Color.Red); } } + + public static XLColor RosyBrown { get { return FromColor(Color.RosyBrown); } } + + public static XLColor RoyalBlue { get { return FromColor(Color.RoyalBlue); } } + + public static XLColor SaddleBrown { get { return FromColor(Color.SaddleBrown); } } + + public static XLColor Salmon { get { return FromColor(Color.Salmon); } } + + public static XLColor SandyBrown { get { return FromColor(Color.SandyBrown); } } + + public static XLColor SeaGreen { get { return FromColor(Color.SeaGreen); } } + + public static XLColor SeaShell { get { return FromColor(Color.SeaShell); } } + + public static XLColor Sienna { get { return FromColor(Color.Sienna); } } + + public static XLColor Silver { get { return FromColor(Color.Silver); } } + + public static XLColor SkyBlue { get { return FromColor(Color.SkyBlue); } } + + public static XLColor SlateBlue { get { return FromColor(Color.SlateBlue); } } + + public static XLColor SlateGray { get { return FromColor(Color.SlateGray); } } + + public static XLColor Snow { get { return FromColor(Color.Snow); } } + + public static XLColor SpringGreen { get { return FromColor(Color.SpringGreen); } } + + public static XLColor SteelBlue { get { return FromColor(Color.SteelBlue); } } + + public static XLColor Tan { get { return FromColor(Color.Tan); } } + + public static XLColor Teal { get { return FromColor(Color.Teal); } } + + public static XLColor Thistle { get { return FromColor(Color.Thistle); } } + + public static XLColor Tomato { get { return FromColor(Color.Tomato); } } + + public static XLColor Turquoise { get { return FromColor(Color.Turquoise); } } + + public static XLColor Violet { get { return FromColor(Color.Violet); } } + + public static XLColor Wheat { get { return FromColor(Color.Wheat); } } + + public static XLColor White { get { return FromColor(Color.White); } } + + public static XLColor WhiteSmoke { get { return FromColor(Color.WhiteSmoke); } } + + public static XLColor Yellow { get { return FromColor(Color.Yellow); } } + + public static XLColor YellowGreen { get { return FromColor(Color.YellowGreen); } } + + public static XLColor AirForceBlue { get { return FromHtml("#FF5D8AA8"); } } + + public static XLColor Alizarin { get { return FromHtml("#FFE32636"); } } + + public static XLColor Almond { get { return FromHtml("#FFEFDECD"); } } + + public static XLColor Amaranth { get { return FromHtml("#FFE52B50"); } } + + public static XLColor Amber { get { return FromHtml("#FFFFBF00"); } } + + public static XLColor AmberSaeEce { get { return FromHtml("#FFFF7E00"); } } + + public static XLColor AmericanRose { get { return FromHtml("#FFFF033E"); } } + + public static XLColor Amethyst { get { return FromHtml("#FF9966CC"); } } + + public static XLColor AntiFlashWhite { get { return FromHtml("#FFF2F3F4"); } } + + public static XLColor AntiqueBrass { get { return FromHtml("#FFCD9575"); } } + + public static XLColor AntiqueFuchsia { get { return FromHtml("#FF915C83"); } } + + public static XLColor AppleGreen { get { return FromHtml("#FF8DB600"); } } + + public static XLColor Apricot { get { return FromHtml("#FFFBCEB1"); } } + + public static XLColor Aquamarine1 { get { return FromHtml("#FF7FFFD0"); } } + + public static XLColor ArmyGreen { get { return FromHtml("#FF4B5320"); } } + + public static XLColor Arsenic { get { return FromHtml("#FF3B444B"); } } + + public static XLColor ArylideYellow { get { return FromHtml("#FFE9D66B"); } } + + public static XLColor AshGrey { get { return FromHtml("#FFB2BEB5"); } } + + public static XLColor Asparagus { get { return FromHtml("#FF87A96B"); } } + + public static XLColor AtomicTangerine { get { return FromHtml("#FFFF9966"); } } + + public static XLColor Auburn { get { return FromHtml("#FF6D351A"); } } + + public static XLColor Aureolin { get { return FromHtml("#FFFDEE00"); } } + + public static XLColor Aurometalsaurus { get { return FromHtml("#FF6E7F80"); } } + + public static XLColor Awesome { get { return FromHtml("#FFFF2052"); } } + + public static XLColor AzureColorWheel { get { return FromHtml("#FF007FFF"); } } + + public static XLColor BabyBlue { get { return FromHtml("#FF89CFF0"); } } + + public static XLColor BabyBlueEyes { get { return FromHtml("#FFA1CAF1"); } } + + public static XLColor BabyPink { get { return FromHtml("#FFF4C2C2"); } } + + public static XLColor BallBlue { get { return FromHtml("#FF21ABCD"); } } + + public static XLColor BananaMania { get { return FromHtml("#FFFAE7B5"); } } + + public static XLColor BattleshipGrey { get { return FromHtml("#FF848482"); } } + + public static XLColor Bazaar { get { return FromHtml("#FF98777B"); } } + + public static XLColor BeauBlue { get { return FromHtml("#FFBCD4E6"); } } + + public static XLColor Beaver { get { return FromHtml("#FF9F8170"); } } + + public static XLColor Bistre { get { return FromHtml("#FF3D2B1F"); } } + + public static XLColor Bittersweet { get { return FromHtml("#FFFE6F5E"); } } + + public static XLColor BleuDeFrance { get { return FromHtml("#FF318CE7"); } } + + public static XLColor BlizzardBlue { get { return FromHtml("#FFACE5EE"); } } + + public static XLColor Blond { get { return FromHtml("#FFFAF0BE"); } } + + public static XLColor BlueBell { get { return FromHtml("#FFA2A2D0"); } } + + public static XLColor BlueGray { get { return FromHtml("#FF6699CC"); } } + + public static XLColor BlueGreen { get { return FromHtml("#FF00DDDD"); } } + + public static XLColor BluePigment { get { return FromHtml("#FF333399"); } } + + public static XLColor BlueRyb { get { return FromHtml("#FF0247FE"); } } + + public static XLColor Blush { get { return FromHtml("#FFDE5D83"); } } + + public static XLColor Bole { get { return FromHtml("#FF79443B"); } } + + public static XLColor BondiBlue { get { return FromHtml("#FF0095B6"); } } + + public static XLColor BostonUniversityRed { get { return FromHtml("#FFCC0000"); } } + + public static XLColor BrandeisBlue { get { return FromHtml("#FF0070FF"); } } + + public static XLColor Brass { get { return FromHtml("#FFB5A642"); } } + + public static XLColor BrickRed { get { return FromHtml("#FFCB4154"); } } + + public static XLColor BrightCerulean { get { return FromHtml("#FF1DACD6"); } } + + public static XLColor BrightGreen { get { return FromHtml("#FF66FF00"); } } + + public static XLColor BrightLavender { get { return FromHtml("#FFBF94E4"); } } + + public static XLColor BrightMaroon { get { return FromHtml("#FFC32148"); } } + + public static XLColor BrightPink { get { return FromHtml("#FFFF007F"); } } + + public static XLColor BrightTurquoise { get { return FromHtml("#FF08E8DE"); } } + + public static XLColor BrightUbe { get { return FromHtml("#FFD19FE8"); } } + + public static XLColor BrilliantLavender { get { return FromHtml("#FFF4BBFF"); } } + + public static XLColor BrilliantRose { get { return FromHtml("#FFFF55A3"); } } + + public static XLColor BrinkPink { get { return FromHtml("#FFFB607F"); } } + + public static XLColor BritishRacingGreen { get { return FromHtml("#FF004225"); } } + + public static XLColor Bronze { get { return FromHtml("#FFCD7F32"); } } + + public static XLColor BrownTraditional { get { return FromHtml("#FF964B00"); } } + + public static XLColor BubbleGum { get { return FromHtml("#FFFFC1CC"); } } + + public static XLColor Bubbles { get { return FromHtml("#FFE7FEFF"); } } + + public static XLColor Buff { get { return FromHtml("#FFF0DC82"); } } + + public static XLColor BulgarianRose { get { return FromHtml("#FF480607"); } } + + public static XLColor Burgundy { get { return FromHtml("#FF800020"); } } + + public static XLColor BurntOrange { get { return FromHtml("#FFCC5500"); } } + + public static XLColor BurntSienna { get { return FromHtml("#FFE97451"); } } + + public static XLColor BurntUmber { get { return FromHtml("#FF8A3324"); } } + + public static XLColor Byzantine { get { return FromHtml("#FFBD33A4"); } } + + public static XLColor Byzantium { get { return FromHtml("#FF702963"); } } + + public static XLColor Cadet { get { return FromHtml("#FF536872"); } } + + public static XLColor CadetGrey { get { return FromHtml("#FF91A3B0"); } } + + public static XLColor CadmiumGreen { get { return FromHtml("#FF006B3C"); } } + + public static XLColor CadmiumOrange { get { return FromHtml("#FFED872D"); } } + + public static XLColor CadmiumRed { get { return FromHtml("#FFE30022"); } } + + public static XLColor CadmiumYellow { get { return FromHtml("#FFFFF600"); } } + + public static XLColor CalPolyPomonaGreen { get { return FromHtml("#FF1E4D2B"); } } + + public static XLColor CambridgeBlue { get { return FromHtml("#FFA3C1AD"); } } + + public static XLColor Camel { get { return FromHtml("#FFC19A6B"); } } + + public static XLColor CamouflageGreen { get { return FromHtml("#FF78866B"); } } + + public static XLColor CanaryYellow { get { return FromHtml("#FFFFEF00"); } } + + public static XLColor CandyAppleRed { get { return FromHtml("#FFFF0800"); } } + + public static XLColor CandyPink { get { return FromHtml("#FFE4717A"); } } + + public static XLColor CaputMortuum { get { return FromHtml("#FF592720"); } } + + public static XLColor Cardinal { get { return FromHtml("#FFC41E3A"); } } + + public static XLColor CaribbeanGreen { get { return FromHtml("#FF00CC99"); } } + + public static XLColor Carmine { get { return FromHtml("#FF960018"); } } + + public static XLColor CarminePink { get { return FromHtml("#FFEB4C42"); } } + + public static XLColor CarmineRed { get { return FromHtml("#FFFF0038"); } } + + public static XLColor CarnationPink { get { return FromHtml("#FFFFA6C9"); } } + + public static XLColor Carnelian { get { return FromHtml("#FFB31B1B"); } } + + public static XLColor CarolinaBlue { get { return FromHtml("#FF99BADD"); } } + + public static XLColor CarrotOrange { get { return FromHtml("#FFED9121"); } } + + public static XLColor Ceil { get { return FromHtml("#FF92A1CF"); } } + + public static XLColor Celadon { get { return FromHtml("#FFACE1AF"); } } + + public static XLColor CelestialBlue { get { return FromHtml("#FF4997D0"); } } + + public static XLColor Cerise { get { return FromHtml("#FFDE3163"); } } + + public static XLColor CerisePink { get { return FromHtml("#FFEC3B83"); } } + + public static XLColor Cerulean { get { return FromHtml("#FF007BA7"); } } + + public static XLColor CeruleanBlue { get { return FromHtml("#FF2A52BE"); } } + + public static XLColor Chamoisee { get { return FromHtml("#FFA0785A"); } } + + public static XLColor Champagne { get { return FromHtml("#FFF7E7CE"); } } + + public static XLColor Charcoal { get { return FromHtml("#FF36454F"); } } + + public static XLColor ChartreuseTraditional { get { return FromHtml("#FFDFFF00"); } } + + public static XLColor CherryBlossomPink { get { return FromHtml("#FFFFB7C5"); } } + + public static XLColor Chocolate1 { get { return FromHtml("#FF7B3F00"); } } + + public static XLColor ChromeYellow { get { return FromHtml("#FFFFA700"); } } + + public static XLColor Cinereous { get { return FromHtml("#FF98817B"); } } + + public static XLColor Cinnabar { get { return FromHtml("#FFE34234"); } } + + public static XLColor Citrine { get { return FromHtml("#FFE4D00A"); } } + + public static XLColor ClassicRose { get { return FromHtml("#FFFBCCE7"); } } + + public static XLColor Cobalt { get { return FromHtml("#FF0047AB"); } } + + public static XLColor ColumbiaBlue { get { return FromHtml("#FF9BDDFF"); } } + + public static XLColor CoolBlack { get { return FromHtml("#FF002E63"); } } + + public static XLColor CoolGrey { get { return FromHtml("#FF8C92AC"); } } + + public static XLColor Copper { get { return FromHtml("#FFB87333"); } } + + public static XLColor CopperRose { get { return FromHtml("#FF996666"); } } + + public static XLColor Coquelicot { get { return FromHtml("#FFFF3800"); } } + + public static XLColor CoralPink { get { return FromHtml("#FFF88379"); } } + + public static XLColor CoralRed { get { return FromHtml("#FFFF4040"); } } + + public static XLColor Cordovan { get { return FromHtml("#FF893F45"); } } + + public static XLColor Corn { get { return FromHtml("#FFFBEC5D"); } } + + public static XLColor CornellRed { get { return FromHtml("#FFB31B1B"); } } + + public static XLColor CosmicLatte { get { return FromHtml("#FFFFF8E7"); } } + + public static XLColor CottonCandy { get { return FromHtml("#FFFFBCD9"); } } + + public static XLColor Cream { get { return FromHtml("#FFFFFDD0"); } } + + public static XLColor CrimsonGlory { get { return FromHtml("#FFBE0032"); } } + + public static XLColor CyanProcess { get { return FromHtml("#FF00B7EB"); } } + + public static XLColor Daffodil { get { return FromHtml("#FFFFFF31"); } } + + public static XLColor Dandelion { get { return FromHtml("#FFF0E130"); } } + + public static XLColor DarkBrown { get { return FromHtml("#FF654321"); } } + + public static XLColor DarkByzantium { get { return FromHtml("#FF5D3954"); } } + + public static XLColor DarkCandyAppleRed { get { return FromHtml("#FFA40000"); } } + + public static XLColor DarkCerulean { get { return FromHtml("#FF08457E"); } } + + public static XLColor DarkChampagne { get { return FromHtml("#FFC2B280"); } } + + public static XLColor DarkChestnut { get { return FromHtml("#FF986960"); } } + + public static XLColor DarkCoral { get { return FromHtml("#FFCD5B45"); } } + + public static XLColor DarkElectricBlue { get { return FromHtml("#FF536878"); } } + + public static XLColor DarkGreen1 { get { return FromHtml("#FF013220"); } } + + public static XLColor DarkJungleGreen { get { return FromHtml("#FF1A2421"); } } + + public static XLColor DarkLava { get { return FromHtml("#FF483C32"); } } + + public static XLColor DarkLavender { get { return FromHtml("#FF734F96"); } } + + public static XLColor DarkMidnightBlue { get { return FromHtml("#FF003366"); } } + + public static XLColor DarkPastelBlue { get { return FromHtml("#FF779ECB"); } } + + public static XLColor DarkPastelGreen { get { return FromHtml("#FF03C03C"); } } + + public static XLColor DarkPastelPurple { get { return FromHtml("#FF966FD6"); } } + + public static XLColor DarkPastelRed { get { return FromHtml("#FFC23B22"); } } + + public static XLColor DarkPink { get { return FromHtml("#FFE75480"); } } + + public static XLColor DarkPowderBlue { get { return FromHtml("#FF003399"); } } + + public static XLColor DarkRaspberry { get { return FromHtml("#FF872657"); } } + + public static XLColor DarkScarlet { get { return FromHtml("#FF560319"); } } + + public static XLColor DarkSienna { get { return FromHtml("#FF3C1414"); } } + + public static XLColor DarkSpringGreen { get { return FromHtml("#FF177245"); } } + + public static XLColor DarkTan { get { return FromHtml("#FF918151"); } } + + public static XLColor DarkTangerine { get { return FromHtml("#FFFFA812"); } } + + public static XLColor DarkTaupe { get { return FromHtml("#FF483C32"); } } + + public static XLColor DarkTerraCotta { get { return FromHtml("#FFCC4E5C"); } } + + public static XLColor DartmouthGreen { get { return FromHtml("#FF00693E"); } } + + public static XLColor DavysGrey { get { return FromHtml("#FF555555"); } } + + public static XLColor DebianRed { get { return FromHtml("#FFD70A53"); } } + + public static XLColor DeepCarmine { get { return FromHtml("#FFA9203E"); } } + + public static XLColor DeepCarminePink { get { return FromHtml("#FFEF3038"); } } + + public static XLColor DeepCarrotOrange { get { return FromHtml("#FFE9692C"); } } + + public static XLColor DeepCerise { get { return FromHtml("#FFDA3287"); } } + + public static XLColor DeepChampagne { get { return FromHtml("#FFFAD6A5"); } } + + public static XLColor DeepChestnut { get { return FromHtml("#FFB94E48"); } } + + public static XLColor DeepFuchsia { get { return FromHtml("#FFC154C1"); } } + + public static XLColor DeepJungleGreen { get { return FromHtml("#FF004B49"); } } + + public static XLColor DeepLilac { get { return FromHtml("#FF9955BB"); } } + + public static XLColor DeepMagenta { get { return FromHtml("#FFCC00CC"); } } + + public static XLColor DeepPeach { get { return FromHtml("#FFFFCBA4"); } } + + public static XLColor DeepSaffron { get { return FromHtml("#FFFF9933"); } } + + public static XLColor Denim { get { return FromHtml("#FF1560BD"); } } + + public static XLColor Desert { get { return FromHtml("#FFC19A6B"); } } + + public static XLColor DesertSand { get { return FromHtml("#FFEDC9AF"); } } + + public static XLColor DogwoodRose { get { return FromHtml("#FFD71868"); } } + + public static XLColor DollarBill { get { return FromHtml("#FF85BB65"); } } + + public static XLColor Drab { get { return FromHtml("#FF967117"); } } + + public static XLColor DukeBlue { get { return FromHtml("#FF00009C"); } } + + public static XLColor EarthYellow { get { return FromHtml("#FFE1A95F"); } } + + public static XLColor Ecru { get { return FromHtml("#FFC2B280"); } } + + public static XLColor Eggplant { get { return FromHtml("#FF614051"); } } + + public static XLColor Eggshell { get { return FromHtml("#FFF0EAD6"); } } + + public static XLColor EgyptianBlue { get { return FromHtml("#FF1034A6"); } } + + public static XLColor ElectricBlue { get { return FromHtml("#FF7DF9FF"); } } + + public static XLColor ElectricCrimson { get { return FromHtml("#FFFF003F"); } } + + public static XLColor ElectricIndigo { get { return FromHtml("#FF6F00FF"); } } + + public static XLColor ElectricLavender { get { return FromHtml("#FFF4BBFF"); } } + + public static XLColor ElectricLime { get { return FromHtml("#FFCCFF00"); } } + + public static XLColor ElectricPurple { get { return FromHtml("#FFBF00FF"); } } + + public static XLColor ElectricUltramarine { get { return FromHtml("#FF3F00FF"); } } + + public static XLColor ElectricViolet { get { return FromHtml("#FF8F00FF"); } } + + public static XLColor Emerald { get { return FromHtml("#FF50C878"); } } + + public static XLColor EtonBlue { get { return FromHtml("#FF96C8A2"); } } + + public static XLColor Fallow { get { return FromHtml("#FFC19A6B"); } } + + public static XLColor FaluRed { get { return FromHtml("#FF801818"); } } + + public static XLColor Fandango { get { return FromHtml("#FFB53389"); } } + + public static XLColor FashionFuchsia { get { return FromHtml("#FFF400A1"); } } + + public static XLColor Fawn { get { return FromHtml("#FFE5AA70"); } } + + public static XLColor Feldgrau { get { return FromHtml("#FF4D5D53"); } } + + public static XLColor FernGreen { get { return FromHtml("#FF4F7942"); } } + + public static XLColor FerrariRed { get { return FromHtml("#FFFF2800"); } } + + public static XLColor FieldDrab { get { return FromHtml("#FF6C541E"); } } + + public static XLColor FireEngineRed { get { return FromHtml("#FFCE2029"); } } + + public static XLColor Flame { get { return FromHtml("#FFE25822"); } } + + public static XLColor FlamingoPink { get { return FromHtml("#FFFC8EAC"); } } + + public static XLColor Flavescent { get { return FromHtml("#FFF7E98E"); } } + + public static XLColor Flax { get { return FromHtml("#FFEEDC82"); } } + + public static XLColor FluorescentOrange { get { return FromHtml("#FFFFBF00"); } } + + public static XLColor FluorescentYellow { get { return FromHtml("#FFCCFF00"); } } + + public static XLColor Folly { get { return FromHtml("#FFFF004F"); } } + + public static XLColor ForestGreenTraditional { get { return FromHtml("#FF014421"); } } + + public static XLColor FrenchBeige { get { return FromHtml("#FFA67B5B"); } } + + public static XLColor FrenchBlue { get { return FromHtml("#FF0072BB"); } } + + public static XLColor FrenchLilac { get { return FromHtml("#FF86608E"); } } + + public static XLColor FrenchRose { get { return FromHtml("#FFF64A8A"); } } + + public static XLColor FuchsiaPink { get { return FromHtml("#FFFF77FF"); } } + + public static XLColor Fulvous { get { return FromHtml("#FFE48400"); } } + + public static XLColor FuzzyWuzzy { get { return FromHtml("#FFCC6666"); } } + + public static XLColor Gamboge { get { return FromHtml("#FFE49B0F"); } } + + public static XLColor Ginger { get { return FromHtml("#FFF9F9FF"); } } + + public static XLColor Glaucous { get { return FromHtml("#FF6082B6"); } } + + public static XLColor GoldenBrown { get { return FromHtml("#FF996515"); } } + + public static XLColor GoldenPoppy { get { return FromHtml("#FFFCC200"); } } + + public static XLColor GoldenYellow { get { return FromHtml("#FFFFDF00"); } } + + public static XLColor GoldMetallic { get { return FromHtml("#FFD4AF37"); } } + + public static XLColor GrannySmithApple { get { return FromHtml("#FFA8E4A0"); } } + + public static XLColor GrayAsparagus { get { return FromHtml("#FF465945"); } } + + public static XLColor GreenPigment { get { return FromHtml("#FF00A550"); } } + + public static XLColor GreenRyb { get { return FromHtml("#FF66B032"); } } + + public static XLColor Grullo { get { return FromHtml("#FFA99A86"); } } + + public static XLColor HalayaUbe { get { return FromHtml("#FF663854"); } } + + public static XLColor HanBlue { get { return FromHtml("#FF446CCF"); } } + + public static XLColor HanPurple { get { return FromHtml("#FF5218FA"); } } + + public static XLColor HansaYellow { get { return FromHtml("#FFE9D66B"); } } + + public static XLColor Harlequin { get { return FromHtml("#FF3FFF00"); } } + + public static XLColor HarvardCrimson { get { return FromHtml("#FFC90016"); } } + + public static XLColor HarvestGold { get { return FromHtml("#FFDA9100"); } } + + public static XLColor Heliotrope { get { return FromHtml("#FFDF73FF"); } } + + public static XLColor HollywoodCerise { get { return FromHtml("#FFF400A1"); } } + + public static XLColor HookersGreen { get { return FromHtml("#FF007000"); } } + + public static XLColor HotMagenta { get { return FromHtml("#FFFF1DCE"); } } + + public static XLColor HunterGreen { get { return FromHtml("#FF355E3B"); } } + + public static XLColor Iceberg { get { return FromHtml("#FF71A6D2"); } } + + public static XLColor Icterine { get { return FromHtml("#FFFCF75E"); } } + + public static XLColor Inchworm { get { return FromHtml("#FFB2EC5D"); } } + + public static XLColor IndiaGreen { get { return FromHtml("#FF138808"); } } + + public static XLColor IndianYellow { get { return FromHtml("#FFE3A857"); } } + + public static XLColor IndigoDye { get { return FromHtml("#FF00416A"); } } + + public static XLColor InternationalKleinBlue { get { return FromHtml("#FF002FA7"); } } + + public static XLColor InternationalOrange { get { return FromHtml("#FFFF4F00"); } } + + public static XLColor Iris { get { return FromHtml("#FF5A4FCF"); } } + + public static XLColor Isabelline { get { return FromHtml("#FFF4F0EC"); } } + + public static XLColor IslamicGreen { get { return FromHtml("#FF009000"); } } + + public static XLColor Jade { get { return FromHtml("#FF00A86B"); } } + + public static XLColor Jasper { get { return FromHtml("#FFD73B3E"); } } + + public static XLColor JazzberryJam { get { return FromHtml("#FFA50B5E"); } } + + public static XLColor Jonquil { get { return FromHtml("#FFFADA5E"); } } + + public static XLColor JuneBud { get { return FromHtml("#FFBDDA57"); } } + + public static XLColor JungleGreen { get { return FromHtml("#FF29AB87"); } } + + public static XLColor KellyGreen { get { return FromHtml("#FF4CBB17"); } } + + public static XLColor KhakiHtmlCssKhaki { get { return FromHtml("#FFC3B091"); } } + + public static XLColor LanguidLavender { get { return FromHtml("#FFD6CADD"); } } + + public static XLColor LapisLazuli { get { return FromHtml("#FF26619C"); } } + + public static XLColor LaSalleGreen { get { return FromHtml("#FF087830"); } } + + public static XLColor LaserLemon { get { return FromHtml("#FFFEFE22"); } } + + public static XLColor Lava { get { return FromHtml("#FFCF1020"); } } + + public static XLColor LavenderBlue { get { return FromHtml("#FFCCCCFF"); } } + + public static XLColor LavenderFloral { get { return FromHtml("#FFB57EDC"); } } + + public static XLColor LavenderGray { get { return FromHtml("#FFC4C3D0"); } } + + public static XLColor LavenderIndigo { get { return FromHtml("#FF9457EB"); } } + + public static XLColor LavenderPink { get { return FromHtml("#FFFBAED2"); } } + + public static XLColor LavenderPurple { get { return FromHtml("#FF967BB6"); } } + + public static XLColor LavenderRose { get { return FromHtml("#FFFBA0E3"); } } + + public static XLColor Lemon { get { return FromHtml("#FFFFF700"); } } + + public static XLColor LightApricot { get { return FromHtml("#FFFDD5B1"); } } + + public static XLColor LightBrown { get { return FromHtml("#FFB5651D"); } } + + public static XLColor LightCarminePink { get { return FromHtml("#FFE66771"); } } + + public static XLColor LightCornflowerBlue { get { return FromHtml("#FF93CCEA"); } } + + public static XLColor LightFuchsiaPink { get { return FromHtml("#FFF984EF"); } } + + public static XLColor LightMauve { get { return FromHtml("#FFDCD0FF"); } } + + public static XLColor LightPastelPurple { get { return FromHtml("#FFB19CD9"); } } + + public static XLColor LightSalmonPink { get { return FromHtml("#FFFF9999"); } } + + public static XLColor LightTaupe { get { return FromHtml("#FFB38B6D"); } } + + public static XLColor LightThulianPink { get { return FromHtml("#FFE68FAC"); } } + + public static XLColor LightYellow1 { get { return FromHtml("#FFFFFFED"); } } + + public static XLColor Lilac { get { return FromHtml("#FFC8A2C8"); } } + + public static XLColor LimeColorWheel { get { return FromHtml("#FFBFFF00"); } } + + public static XLColor LincolnGreen { get { return FromHtml("#FF195905"); } } + + public static XLColor Liver { get { return FromHtml("#FF534B4F"); } } + + public static XLColor Lust { get { return FromHtml("#FFE62020"); } } + + public static XLColor MacaroniAndCheese { get { return FromHtml("#FFFFBD88"); } } + + public static XLColor MagentaDye { get { return FromHtml("#FFCA1F7B"); } } + + public static XLColor MagentaProcess { get { return FromHtml("#FFFF0090"); } } + + public static XLColor MagicMint { get { return FromHtml("#FFAAF0D1"); } } + + public static XLColor Magnolia { get { return FromHtml("#FFF8F4FF"); } } + + public static XLColor Mahogany { get { return FromHtml("#FFC04000"); } } + + public static XLColor Maize { get { return FromHtml("#FFFBEC5D"); } } + + public static XLColor MajorelleBlue { get { return FromHtml("#FF6050DC"); } } + + public static XLColor Malachite { get { return FromHtml("#FF0BDA51"); } } + + public static XLColor Manatee { get { return FromHtml("#FF979AAA"); } } + + public static XLColor MangoTango { get { return FromHtml("#FFFF8243"); } } + + public static XLColor MaroonX11 { get { return FromHtml("#FFB03060"); } } + + public static XLColor Mauve { get { return FromHtml("#FFE0B0FF"); } } + + public static XLColor Mauvelous { get { return FromHtml("#FFEF98AA"); } } + + public static XLColor MauveTaupe { get { return FromHtml("#FF915F6D"); } } + + public static XLColor MayaBlue { get { return FromHtml("#FF73C2FB"); } } + + public static XLColor MeatBrown { get { return FromHtml("#FFE5B73B"); } } + + public static XLColor MediumAquamarine1 { get { return FromHtml("#FF66DDAA"); } } + + public static XLColor MediumCandyAppleRed { get { return FromHtml("#FFE2062C"); } } + + public static XLColor MediumCarmine { get { return FromHtml("#FFAF4035"); } } + + public static XLColor MediumChampagne { get { return FromHtml("#FFF3E5AB"); } } + + public static XLColor MediumElectricBlue { get { return FromHtml("#FF035096"); } } + + public static XLColor MediumJungleGreen { get { return FromHtml("#FF1C352D"); } } + + public static XLColor MediumPersianBlue { get { return FromHtml("#FF0067A5"); } } + + public static XLColor MediumRedViolet { get { return FromHtml("#FFBB3385"); } } + + public static XLColor MediumSpringBud { get { return FromHtml("#FFC9DC87"); } } + + public static XLColor MediumTaupe { get { return FromHtml("#FF674C47"); } } + + public static XLColor Melon { get { return FromHtml("#FFFDBCB4"); } } + + public static XLColor MidnightGreenEagleGreen { get { return FromHtml("#FF004953"); } } + + public static XLColor MikadoYellow { get { return FromHtml("#FFFFC40C"); } } + + public static XLColor Mint { get { return FromHtml("#FF3EB489"); } } + + public static XLColor MintGreen { get { return FromHtml("#FF98FF98"); } } + + public static XLColor ModeBeige { get { return FromHtml("#FF967117"); } } + + public static XLColor MoonstoneBlue { get { return FromHtml("#FF73A9C2"); } } + + public static XLColor MordantRed19 { get { return FromHtml("#FFAE0C00"); } } + + public static XLColor MossGreen { get { return FromHtml("#FFADDFAD"); } } + + public static XLColor MountainMeadow { get { return FromHtml("#FF30BA8F"); } } + + public static XLColor MountbattenPink { get { return FromHtml("#FF997A8D"); } } + + public static XLColor MsuGreen { get { return FromHtml("#FF18453B"); } } + + public static XLColor Mulberry { get { return FromHtml("#FFC54B8C"); } } + + public static XLColor Mustard { get { return FromHtml("#FFFFDB58"); } } + + public static XLColor Myrtle { get { return FromHtml("#FF21421E"); } } + + public static XLColor NadeshikoPink { get { return FromHtml("#FFF6ADC6"); } } + + public static XLColor NapierGreen { get { return FromHtml("#FF2A8000"); } } + + public static XLColor NaplesYellow { get { return FromHtml("#FFFADA5E"); } } + + public static XLColor NeonCarrot { get { return FromHtml("#FFFFA343"); } } + + public static XLColor NeonFuchsia { get { return FromHtml("#FFFE59C2"); } } + + public static XLColor NeonGreen { get { return FromHtml("#FF39FF14"); } } + + public static XLColor NonPhotoBlue { get { return FromHtml("#FFA4DDED"); } } + + public static XLColor OceanBoatBlue { get { return FromHtml("#FFCC7422"); } } + + public static XLColor Ochre { get { return FromHtml("#FFCC7722"); } } + + public static XLColor OldGold { get { return FromHtml("#FFCFB53B"); } } + + public static XLColor OldLavender { get { return FromHtml("#FF796878"); } } + + public static XLColor OldMauve { get { return FromHtml("#FF673147"); } } + + public static XLColor OldRose { get { return FromHtml("#FFC08081"); } } + + public static XLColor OliveDrab7 { get { return FromHtml("#FF3C341F"); } } + + public static XLColor Olivine { get { return FromHtml("#FF9AB973"); } } + + public static XLColor Onyx { get { return FromHtml("#FF0F0F0F"); } } + + public static XLColor OperaMauve { get { return FromHtml("#FFB784A7"); } } + + public static XLColor OrangeColorWheel { get { return FromHtml("#FFFF7F00"); } } + + public static XLColor OrangePeel { get { return FromHtml("#FFFF9F00"); } } + + public static XLColor OrangeRyb { get { return FromHtml("#FFFB9902"); } } + + public static XLColor OtterBrown { get { return FromHtml("#FF654321"); } } + + public static XLColor OuCrimsonRed { get { return FromHtml("#FF990000"); } } + + public static XLColor OuterSpace { get { return FromHtml("#FF414A4C"); } } + + public static XLColor OutrageousOrange { get { return FromHtml("#FFFF6E4A"); } } + + public static XLColor OxfordBlue { get { return FromHtml("#FF002147"); } } + + public static XLColor PakistanGreen { get { return FromHtml("#FF00421B"); } } + + public static XLColor PalatinateBlue { get { return FromHtml("#FF273BE2"); } } + + public static XLColor PalatinatePurple { get { return FromHtml("#FF682860"); } } + + public static XLColor PaleAqua { get { return FromHtml("#FFBCD4E6"); } } + + public static XLColor PaleBrown { get { return FromHtml("#FF987654"); } } + + public static XLColor PaleCarmine { get { return FromHtml("#FFAF4035"); } } + + public static XLColor PaleCerulean { get { return FromHtml("#FF9BC4E2"); } } + + public static XLColor PaleChestnut { get { return FromHtml("#FFDDADAF"); } } + + public static XLColor PaleCopper { get { return FromHtml("#FFDA8A67"); } } + + public static XLColor PaleCornflowerBlue { get { return FromHtml("#FFABCDEF"); } } + + public static XLColor PaleGold { get { return FromHtml("#FFE6BE8A"); } } + + public static XLColor PaleMagenta { get { return FromHtml("#FFF984E5"); } } + + public static XLColor PalePink { get { return FromHtml("#FFFADADD"); } } + + public static XLColor PaleRobinEggBlue { get { return FromHtml("#FF96DED1"); } } + + public static XLColor PaleSilver { get { return FromHtml("#FFC9C0BB"); } } + + public static XLColor PaleSpringBud { get { return FromHtml("#FFECEBBD"); } } + + public static XLColor PaleTaupe { get { return FromHtml("#FFBC987E"); } } + + public static XLColor PansyPurple { get { return FromHtml("#FF78184A"); } } + + public static XLColor ParisGreen { get { return FromHtml("#FF50C878"); } } + + public static XLColor PastelBlue { get { return FromHtml("#FFAEC6CF"); } } + + public static XLColor PastelBrown { get { return FromHtml("#FF836953"); } } + + public static XLColor PastelGray { get { return FromHtml("#FFCFCFC4"); } } + + public static XLColor PastelGreen { get { return FromHtml("#FF77DD77"); } } + + public static XLColor PastelMagenta { get { return FromHtml("#FFF49AC2"); } } + + public static XLColor PastelOrange { get { return FromHtml("#FFFFB347"); } } + + public static XLColor PastelPink { get { return FromHtml("#FFFFD1DC"); } } + + public static XLColor PastelPurple { get { return FromHtml("#FFB39EB5"); } } + + public static XLColor PastelRed { get { return FromHtml("#FFFF6961"); } } + + public static XLColor PastelViolet { get { return FromHtml("#FFCB99C9"); } } + + public static XLColor PastelYellow { get { return FromHtml("#FFFDFD96"); } } + + public static XLColor PaynesGrey { get { return FromHtml("#FF40404F"); } } + + public static XLColor Peach { get { return FromHtml("#FFFFE5B4"); } } + + public static XLColor PeachOrange { get { return FromHtml("#FFFFCC99"); } } + + public static XLColor PeachYellow { get { return FromHtml("#FFFADFAD"); } } + + public static XLColor Pear { get { return FromHtml("#FFD1E231"); } } + + public static XLColor Pearl { get { return FromHtml("#FFF0EAD6"); } } + + public static XLColor Peridot { get { return FromHtml("#FFE6E200"); } } + + public static XLColor Periwinkle { get { return FromHtml("#FFCCCCFF"); } } + + public static XLColor PersianBlue { get { return FromHtml("#FF1C39BB"); } } + + public static XLColor PersianGreen { get { return FromHtml("#FF00A693"); } } + + public static XLColor PersianIndigo { get { return FromHtml("#FF32127A"); } } + + public static XLColor PersianOrange { get { return FromHtml("#FFD99058"); } } + + public static XLColor PersianPink { get { return FromHtml("#FFF77FBE"); } } + + public static XLColor PersianPlum { get { return FromHtml("#FF701C1C"); } } + + public static XLColor PersianRed { get { return FromHtml("#FFCC3333"); } } + + public static XLColor PersianRose { get { return FromHtml("#FFFE28A2"); } } + + public static XLColor Persimmon { get { return FromHtml("#FFEC5800"); } } + + public static XLColor Phlox { get { return FromHtml("#FFDF00FF"); } } + + public static XLColor PhthaloBlue { get { return FromHtml("#FF000F89"); } } + + public static XLColor PhthaloGreen { get { return FromHtml("#FF123524"); } } + + public static XLColor PiggyPink { get { return FromHtml("#FFFDDDE6"); } } + + public static XLColor PineGreen { get { return FromHtml("#FF01796F"); } } + + public static XLColor PinkOrange { get { return FromHtml("#FFFF9966"); } } + + public static XLColor PinkPearl { get { return FromHtml("#FFE7ACCF"); } } + + public static XLColor PinkSherbet { get { return FromHtml("#FFF78FA7"); } } + + public static XLColor Pistachio { get { return FromHtml("#FF93C572"); } } + + public static XLColor Platinum { get { return FromHtml("#FFE5E4E2"); } } + + public static XLColor PlumTraditional { get { return FromHtml("#FF8E4585"); } } + + public static XLColor PortlandOrange { get { return FromHtml("#FFFF5A36"); } } + + public static XLColor PrincetonOrange { get { return FromHtml("#FFFF8F00"); } } + + public static XLColor Prune { get { return FromHtml("#FF701C1C"); } } + + public static XLColor PrussianBlue { get { return FromHtml("#FF003153"); } } + + public static XLColor PsychedelicPurple { get { return FromHtml("#FFDF00FF"); } } + + public static XLColor Puce { get { return FromHtml("#FFCC8899"); } } + + public static XLColor Pumpkin { get { return FromHtml("#FFFF7518"); } } + + public static XLColor PurpleHeart { get { return FromHtml("#FF69359C"); } } + + public static XLColor PurpleMountainMajesty { get { return FromHtml("#FF9678B6"); } } + + public static XLColor PurpleMunsell { get { return FromHtml("#FF9F00C5"); } } + + public static XLColor PurplePizzazz { get { return FromHtml("#FFFE4EDA"); } } + + public static XLColor PurpleTaupe { get { return FromHtml("#FF50404D"); } } + + public static XLColor PurpleX11 { get { return FromHtml("#FFA020F0"); } } + + public static XLColor RadicalRed { get { return FromHtml("#FFFF355E"); } } + + public static XLColor Raspberry { get { return FromHtml("#FFE30B5D"); } } + + public static XLColor RaspberryGlace { get { return FromHtml("#FF915F6D"); } } + + public static XLColor RaspberryPink { get { return FromHtml("#FFE25098"); } } + + public static XLColor RaspberryRose { get { return FromHtml("#FFB3446C"); } } + + public static XLColor RawUmber { get { return FromHtml("#FF826644"); } } + + public static XLColor RazzleDazzleRose { get { return FromHtml("#FFFF33CC"); } } + + public static XLColor Razzmatazz { get { return FromHtml("#FFE3256B"); } } + + public static XLColor RedMunsell { get { return FromHtml("#FFF2003C"); } } + + public static XLColor RedNcs { get { return FromHtml("#FFC40233"); } } + + public static XLColor RedPigment { get { return FromHtml("#FFED1C24"); } } + + public static XLColor RedRyb { get { return FromHtml("#FFFE2712"); } } + + public static XLColor Redwood { get { return FromHtml("#FFAB4E52"); } } + + public static XLColor Regalia { get { return FromHtml("#FF522D80"); } } + + public static XLColor RichBlack { get { return FromHtml("#FF004040"); } } + + public static XLColor RichBrilliantLavender { get { return FromHtml("#FFF1A7FE"); } } + + public static XLColor RichCarmine { get { return FromHtml("#FFD70040"); } } + + public static XLColor RichElectricBlue { get { return FromHtml("#FF0892D0"); } } + + public static XLColor RichLavender { get { return FromHtml("#FFA76BCF"); } } + + public static XLColor RichLilac { get { return FromHtml("#FFB666D2"); } } + + public static XLColor RichMaroon { get { return FromHtml("#FFB03060"); } } + + public static XLColor RifleGreen { get { return FromHtml("#FF414833"); } } + + public static XLColor RobinEggBlue { get { return FromHtml("#FF00CCCC"); } } + + public static XLColor Rose { get { return FromHtml("#FFFF007F"); } } + + public static XLColor RoseBonbon { get { return FromHtml("#FFF9429E"); } } + + public static XLColor RoseEbony { get { return FromHtml("#FF674846"); } } + + public static XLColor RoseGold { get { return FromHtml("#FFB76E79"); } } + + public static XLColor RoseMadder { get { return FromHtml("#FFE32636"); } } + + public static XLColor RosePink { get { return FromHtml("#FFFF66CC"); } } + + public static XLColor RoseQuartz { get { return FromHtml("#FFAA98A9"); } } + + public static XLColor RoseTaupe { get { return FromHtml("#FF905D5D"); } } + + public static XLColor RoseVale { get { return FromHtml("#FFAB4E52"); } } + + public static XLColor Rosewood { get { return FromHtml("#FF65000B"); } } + + public static XLColor RossoCorsa { get { return FromHtml("#FFD40000"); } } + + public static XLColor RoyalAzure { get { return FromHtml("#FF0038A8"); } } + + public static XLColor RoyalBlueTraditional { get { return FromHtml("#FF002366"); } } + + public static XLColor RoyalFuchsia { get { return FromHtml("#FFCA2C92"); } } + + public static XLColor RoyalPurple { get { return FromHtml("#FF7851A9"); } } + + public static XLColor Ruby { get { return FromHtml("#FFE0115F"); } } + + public static XLColor Ruddy { get { return FromHtml("#FFFF0028"); } } + + public static XLColor RuddyBrown { get { return FromHtml("#FFBB6528"); } } + + public static XLColor RuddyPink { get { return FromHtml("#FFE18E96"); } } + + public static XLColor Rufous { get { return FromHtml("#FFA81C07"); } } + + public static XLColor Russet { get { return FromHtml("#FF80461B"); } } + + public static XLColor Rust { get { return FromHtml("#FFB7410E"); } } + + public static XLColor SacramentoStateGreen { get { return FromHtml("#FF00563F"); } } + + public static XLColor SafetyOrangeBlazeOrange { get { return FromHtml("#FFFF6700"); } } + + public static XLColor Saffron { get { return FromHtml("#FFF4C430"); } } + + public static XLColor Salmon1 { get { return FromHtml("#FFFF8C69"); } } + + public static XLColor SalmonPink { get { return FromHtml("#FFFF91A4"); } } + + public static XLColor Sand { get { return FromHtml("#FFC2B280"); } } + + public static XLColor SandDune { get { return FromHtml("#FF967117"); } } + + public static XLColor Sandstorm { get { return FromHtml("#FFECD540"); } } + + public static XLColor SandyTaupe { get { return FromHtml("#FF967117"); } } + + public static XLColor Sangria { get { return FromHtml("#FF92000A"); } } + + public static XLColor SapGreen { get { return FromHtml("#FF507D2A"); } } + + public static XLColor Sapphire { get { return FromHtml("#FF082567"); } } + + public static XLColor SatinSheenGold { get { return FromHtml("#FFCBA135"); } } + + public static XLColor Scarlet { get { return FromHtml("#FFFF2000"); } } + + public static XLColor SchoolBusYellow { get { return FromHtml("#FFFFD800"); } } + + public static XLColor ScreaminGreen { get { return FromHtml("#FF76FF7A"); } } + + public static XLColor SealBrown { get { return FromHtml("#FF321414"); } } + + public static XLColor SelectiveYellow { get { return FromHtml("#FFFFBA00"); } } + + public static XLColor Sepia { get { return FromHtml("#FF704214"); } } + + public static XLColor Shadow { get { return FromHtml("#FF8A795D"); } } + + public static XLColor ShamrockGreen { get { return FromHtml("#FF009E60"); } } + + public static XLColor ShockingPink { get { return FromHtml("#FFFC0FC0"); } } + + public static XLColor Sienna1 { get { return FromHtml("#FF882D17"); } } + + public static XLColor Sinopia { get { return FromHtml("#FFCB410B"); } } + + public static XLColor Skobeloff { get { return FromHtml("#FF007474"); } } + + public static XLColor SkyMagenta { get { return FromHtml("#FFCF71AF"); } } + + public static XLColor SmaltDarkPowderBlue { get { return FromHtml("#FF003399"); } } + + public static XLColor SmokeyTopaz { get { return FromHtml("#FF933D41"); } } + + public static XLColor SmokyBlack { get { return FromHtml("#FF100C08"); } } + + public static XLColor SpiroDiscoBall { get { return FromHtml("#FF0FC0FC"); } } + + public static XLColor SplashedWhite { get { return FromHtml("#FFFEFDFF"); } } + + public static XLColor SpringBud { get { return FromHtml("#FFA7FC00"); } } + + public static XLColor StPatricksBlue { get { return FromHtml("#FF23297A"); } } + + public static XLColor StilDeGrainYellow { get { return FromHtml("#FFFADA5E"); } } + + public static XLColor Straw { get { return FromHtml("#FFE4D96F"); } } + + public static XLColor Sunglow { get { return FromHtml("#FFFFCC33"); } } + + public static XLColor Sunset { get { return FromHtml("#FFFAD6A5"); } } + + public static XLColor Tangelo { get { return FromHtml("#FFF94D00"); } } + + public static XLColor Tangerine { get { return FromHtml("#FFF28500"); } } + + public static XLColor TangerineYellow { get { return FromHtml("#FFFFCC00"); } } + + public static XLColor Taupe { get { return FromHtml("#FF483C32"); } } + + public static XLColor TaupeGray { get { return FromHtml("#FF8B8589"); } } + + public static XLColor TeaGreen { get { return FromHtml("#FFD0F0C0"); } } + + public static XLColor TealBlue { get { return FromHtml("#FF367588"); } } + + public static XLColor TealGreen { get { return FromHtml("#FF006D5B"); } } + + public static XLColor TeaRoseOrange { get { return FromHtml("#FFF88379"); } } + + public static XLColor TeaRoseRose { get { return FromHtml("#FFF4C2C2"); } } + + public static XLColor TennéTawny { get { return FromHtml("#FFCD5700"); } } + + public static XLColor TerraCotta { get { return FromHtml("#FFE2725B"); } } + + public static XLColor ThulianPink { get { return FromHtml("#FFDE6FA1"); } } + + public static XLColor TickleMePink { get { return FromHtml("#FFFC89AC"); } } + + public static XLColor TiffanyBlue { get { return FromHtml("#FF0ABAB5"); } } + + public static XLColor TigersEye { get { return FromHtml("#FFE08D3C"); } } + + public static XLColor Timberwolf { get { return FromHtml("#FFDBD7D2"); } } + + public static XLColor TitaniumYellow { get { return FromHtml("#FFEEE600"); } } + + public static XLColor Toolbox { get { return FromHtml("#FF746CC0"); } } + + public static XLColor TractorRed { get { return FromHtml("#FFFD0E35"); } } + + public static XLColor TropicalRainForest { get { return FromHtml("#FF00755E"); } } + + public static XLColor TuftsBlue { get { return FromHtml("#FF417DC1"); } } + + public static XLColor Tumbleweed { get { return FromHtml("#FFDEAA88"); } } + + public static XLColor TurkishRose { get { return FromHtml("#FFB57281"); } } + + public static XLColor Turquoise1 { get { return FromHtml("#FF30D5C8"); } } + + public static XLColor TurquoiseBlue { get { return FromHtml("#FF00FFEF"); } } + + public static XLColor TurquoiseGreen { get { return FromHtml("#FFA0D6B4"); } } + + public static XLColor TuscanRed { get { return FromHtml("#FF823535"); } } + + public static XLColor TwilightLavender { get { return FromHtml("#FF8A496B"); } } + + public static XLColor TyrianPurple { get { return FromHtml("#FF66023C"); } } + + public static XLColor UaBlue { get { return FromHtml("#FF0033AA"); } } + + public static XLColor UaRed { get { return FromHtml("#FFD9004C"); } } + + public static XLColor Ube { get { return FromHtml("#FF8878C3"); } } + + public static XLColor UclaBlue { get { return FromHtml("#FF536895"); } } + + public static XLColor UclaGold { get { return FromHtml("#FFFFB300"); } } + + public static XLColor UfoGreen { get { return FromHtml("#FF3CD070"); } } + + public static XLColor Ultramarine { get { return FromHtml("#FF120A8F"); } } + + public static XLColor UltramarineBlue { get { return FromHtml("#FF4166F5"); } } + + public static XLColor UltraPink { get { return FromHtml("#FFFF6FFF"); } } + + public static XLColor Umber { get { return FromHtml("#FF635147"); } } + + public static XLColor UnitedNationsBlue { get { return FromHtml("#FF5B92E5"); } } + + public static XLColor UnmellowYellow { get { return FromHtml("#FFFFFF66"); } } + + public static XLColor UpForestGreen { get { return FromHtml("#FF014421"); } } + + public static XLColor UpMaroon { get { return FromHtml("#FF7B1113"); } } + + public static XLColor UpsdellRed { get { return FromHtml("#FFAE2029"); } } + + public static XLColor Urobilin { get { return FromHtml("#FFE1AD21"); } } + + public static XLColor UscCardinal { get { return FromHtml("#FF990000"); } } + + public static XLColor UscGold { get { return FromHtml("#FFFFCC00"); } } + + public static XLColor UtahCrimson { get { return FromHtml("#FFD3003F"); } } + + public static XLColor Vanilla { get { return FromHtml("#FFF3E5AB"); } } + + public static XLColor VegasGold { get { return FromHtml("#FFC5B358"); } } + + public static XLColor VenetianRed { get { return FromHtml("#FFC80815"); } } + + public static XLColor Verdigris { get { return FromHtml("#FF43B3AE"); } } + + public static XLColor Vermilion { get { return FromHtml("#FFE34234"); } } + + public static XLColor Veronica { get { return FromHtml("#FFA020F0"); } } + + public static XLColor Violet1 { get { return FromHtml("#FF8F00FF"); } } + + public static XLColor VioletColorWheel { get { return FromHtml("#FF7F00FF"); } } + + public static XLColor VioletRyb { get { return FromHtml("#FF8601AF"); } } + + public static XLColor Viridian { get { return FromHtml("#FF40826D"); } } + + public static XLColor VividAuburn { get { return FromHtml("#FF922724"); } } + + public static XLColor VividBurgundy { get { return FromHtml("#FF9F1D35"); } } + + public static XLColor VividCerise { get { return FromHtml("#FFDA1D81"); } } + + public static XLColor VividTangerine { get { return FromHtml("#FFFFA089"); } } + + public static XLColor VividViolet { get { return FromHtml("#FF9F00FF"); } } + + public static XLColor WarmBlack { get { return FromHtml("#FF004242"); } } + + public static XLColor Wenge { get { return FromHtml("#FF645452"); } } + + public static XLColor WildBlueYonder { get { return FromHtml("#FFA2ADD0"); } } + + public static XLColor WildStrawberry { get { return FromHtml("#FFFF43A4"); } } + + public static XLColor WildWatermelon { get { return FromHtml("#FFFC6C85"); } } + + public static XLColor Wisteria { get { return FromHtml("#FFC9A0DC"); } } + + public static XLColor Xanadu { get { return FromHtml("#FF738678"); } } + + public static XLColor YaleBlue { get { return FromHtml("#FF0F4D92"); } } + + public static XLColor YellowMunsell { get { return FromHtml("#FFEFCC00"); } } + + public static XLColor YellowNcs { get { return FromHtml("#FFFFD300"); } } + + public static XLColor YellowProcess { get { return FromHtml("#FFFFEF00"); } } + + public static XLColor YellowRyb { get { return FromHtml("#FFFEFE33"); } } + + public static XLColor Zaffre { get { return FromHtml("#FF0014A8"); } } + + public static XLColor ZinnwalditeBrown { get { return FromHtml("#FF2C1608"); } } + + public static XLColor Transparent { get { return FromColor(Color.Transparent); } } + } +} diff --git a/ClosedXML/Excel/Style/IXLAlignment.cs b/ClosedXML/Excel/Style/IXLAlignment.cs new file mode 100644 index 0000000..61a3301 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLAlignment.cs @@ -0,0 +1,106 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLAlignmentReadingOrderValues + { + ContextDependent, + LeftToRight, + RightToLeft + } + + public enum XLAlignmentHorizontalValues + { + Center, + CenterContinuous, + Distributed, + Fill, + General, + Justify, + Left, + Right + } + + public enum XLAlignmentVerticalValues + { + Bottom, + Center, + Distributed, + Justify, + Top + } + + public interface IXLAlignment : IEquatable + { + /// + /// Gets or sets the cell's horizontal alignment. + /// + XLAlignmentHorizontalValues Horizontal { get; set; } + + /// + /// Gets or sets the cell's vertical alignment. + /// + XLAlignmentVerticalValues Vertical { get; set; } + + /// + /// Gets or sets the cell's text indentation. + /// + Int32 Indent { get; set; } + + /// + /// Gets or sets whether the cell's last line is justified or not. + /// + Boolean JustifyLastLine { get; set; } + + /// + /// Gets or sets the cell's reading order. + /// + XLAlignmentReadingOrderValues ReadingOrder { get; set; } + + /// + /// Gets or sets the cell's relative indent. + /// + Int32 RelativeIndent { get; set; } + + /// + /// Gets or sets whether the cell's font size should decrease to fit the contents. + /// + Boolean ShrinkToFit { get; set; } + + /// + /// Gets or sets the cell's text rotation. + /// + Int32 TextRotation { get; set; } + + /// + /// Gets or sets whether the cell's text should wrap if it doesn't fit. + /// + Boolean WrapText { get; set; } + + /// + /// Gets or sets wheter the cell's text should be displayed from to to bottom + /// (as opposed to the normal left to right). + /// + Boolean TopToBottom { get; set; } + + IXLStyle SetHorizontal(XLAlignmentHorizontalValues value); + + IXLStyle SetVertical(XLAlignmentVerticalValues value); + + IXLStyle SetIndent(Int32 value); + + IXLStyle SetJustifyLastLine(); IXLStyle SetJustifyLastLine(Boolean value); + + IXLStyle SetReadingOrder(XLAlignmentReadingOrderValues value); + + IXLStyle SetRelativeIndent(Int32 value); + + IXLStyle SetShrinkToFit(); IXLStyle SetShrinkToFit(Boolean value); + + IXLStyle SetTextRotation(Int32 value); + + IXLStyle SetWrapText(); IXLStyle SetWrapText(Boolean value); + + IXLStyle SetTopToBottom(); IXLStyle SetTopToBottom(Boolean value); + } +} diff --git a/ClosedXML/Excel/Style/IXLBorder.cs b/ClosedXML/Excel/Style/IXLBorder.cs new file mode 100644 index 0000000..5c49df4 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLBorder.cs @@ -0,0 +1,89 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLBorderStyleValues + { + DashDot, + DashDotDot, + Dashed, + Dotted, + Double, + Hair, + Medium, + MediumDashDot, + MediumDashDotDot, + MediumDashed, + None, + SlantDashDot, + Thick, + Thin + } + + public interface IXLBorder : IEquatable + { + XLBorderStyleValues OutsideBorder { set; } + + XLColor OutsideBorderColor { set; } + + XLBorderStyleValues InsideBorder { set; } + + XLColor InsideBorderColor { set; } + + XLBorderStyleValues LeftBorder { get; set; } + + XLColor LeftBorderColor { get; set; } + + XLBorderStyleValues RightBorder { get; set; } + + XLColor RightBorderColor { get; set; } + + XLBorderStyleValues TopBorder { get; set; } + + XLColor TopBorderColor { get; set; } + + XLBorderStyleValues BottomBorder { get; set; } + + XLColor BottomBorderColor { get; set; } + + Boolean DiagonalUp { get; set; } + + Boolean DiagonalDown { get; set; } + + XLBorderStyleValues DiagonalBorder { get; set; } + + XLColor DiagonalBorderColor { get; set; } + + IXLStyle SetOutsideBorder(XLBorderStyleValues value); + + IXLStyle SetOutsideBorderColor(XLColor value); + + IXLStyle SetInsideBorder(XLBorderStyleValues value); + + IXLStyle SetInsideBorderColor(XLColor value); + + IXLStyle SetLeftBorder(XLBorderStyleValues value); + + IXLStyle SetLeftBorderColor(XLColor value); + + IXLStyle SetRightBorder(XLBorderStyleValues value); + + IXLStyle SetRightBorderColor(XLColor value); + + IXLStyle SetTopBorder(XLBorderStyleValues value); + + IXLStyle SetTopBorderColor(XLColor value); + + IXLStyle SetBottomBorder(XLBorderStyleValues value); + + IXLStyle SetBottomBorderColor(XLColor value); + + IXLStyle SetDiagonalUp(); IXLStyle SetDiagonalUp(Boolean value); + + IXLStyle SetDiagonalDown(); IXLStyle SetDiagonalDown(Boolean value); + + IXLStyle SetDiagonalBorder(XLBorderStyleValues value); + + IXLStyle SetDiagonalBorderColor(XLColor value); + } +} diff --git a/ClosedXML/Excel/Style/IXLFill.cs b/ClosedXML/Excel/Style/IXLFill.cs new file mode 100644 index 0000000..69ac75f --- /dev/null +++ b/ClosedXML/Excel/Style/IXLFill.cs @@ -0,0 +1,42 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLFillPatternValues + { + DarkDown, + DarkGray, + DarkGrid, + DarkHorizontal, + DarkTrellis, + DarkUp, + DarkVertical, + Gray0625, + Gray125, + LightDown, + LightGray, + LightGrid, + LightHorizontal, + LightTrellis, + LightUp, + LightVertical, + MediumGray, + None, + Solid + } + + public interface IXLFill : IEquatable + { + XLColor BackgroundColor { get; set; } + + XLColor PatternColor { get; set; } + + XLFillPatternValues PatternType { get; set; } + + IXLStyle SetBackgroundColor(XLColor value); + + IXLStyle SetPatternColor(XLColor value); + + IXLStyle SetPatternType(XLFillPatternValues value); + } +} diff --git a/ClosedXML/Excel/Style/IXLFont.cs b/ClosedXML/Excel/Style/IXLFont.cs new file mode 100644 index 0000000..a996b62 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLFont.cs @@ -0,0 +1,158 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLFontUnderlineValues + { + Double, + DoubleAccounting, + None, + Single, + SingleAccounting + } + + public enum XLFontVerticalTextAlignmentValues + { + Baseline, + Subscript, + Superscript + } + + public enum XLFontFamilyNumberingValues + { + NotApplicable = 0, + Roman = 1, + Swiss = 2, + Modern = 3, + Script = 4, + Decorative = 5 + } + + public enum XLFontCharSet + { + /// + /// ASCII character set. + /// + Ansi = 0, + + /// + /// System default character set. + /// + Default = 1, + + /// + /// Symbol character set. + /// + Symbol = 2, + + /// + /// Characters used by Macintosh. + /// + Mac = 77, + + /// + /// Japanese character set. + /// + ShiftJIS = 128, + + /// + /// Korean character set. + /// + Hangul = 129, + + /// + /// Another common spelling of the Korean character set. + /// + Hangeul = 129, + + /// + /// Korean character set. + /// + Johab = 130, + + /// + /// Chinese character set used in mainland China. + /// + GB2312 = 134, + + /// + /// Chinese character set used mostly in Hong Kong SAR and Taiwan. + /// + ChineseBig5 = 136, + + /// + /// Greek character set. + /// + Greek = 161, + + /// + /// Turkish character set. + /// + Turkish = 162, + + /// + /// Vietnamese character set. + /// + Vietnamese = 163, + + /// + /// Hebrew character set. + /// + Hebrew = 177, + + /// + /// Arabic character set. + /// + Arabic = 178, + + /// + /// Baltic character set. + /// + Baltic = 186, + + /// + /// Russian character set. + /// + Russian = 204, + + /// + /// Thai character set. + /// + Thai = 222, + + /// + /// Eastern European character set. + /// + EastEurope = 238, + + /// + /// Extended ASCII character set used with disk operating system (DOS) and some Microsoft Windows fonts. + /// + Oem = 255 + } + + public interface IXLFont : IXLFontBase, IEquatable + { + IXLStyle SetBold(); IXLStyle SetBold(Boolean value); + + IXLStyle SetItalic(); IXLStyle SetItalic(Boolean value); + + IXLStyle SetUnderline(); IXLStyle SetUnderline(XLFontUnderlineValues value); + + IXLStyle SetStrikethrough(); IXLStyle SetStrikethrough(Boolean value); + + IXLStyle SetVerticalAlignment(XLFontVerticalTextAlignmentValues value); + + IXLStyle SetShadow(); IXLStyle SetShadow(Boolean value); + + IXLStyle SetFontSize(Double value); + + IXLStyle SetFontColor(XLColor value); + + IXLStyle SetFontName(String value); + + IXLStyle SetFontFamilyNumbering(XLFontFamilyNumberingValues value); + + IXLStyle SetFontCharSet(XLFontCharSet value); + } +} diff --git a/ClosedXML/Excel/Style/IXLFontBase.cs b/ClosedXML/Excel/Style/IXLFontBase.cs new file mode 100644 index 0000000..21b8288 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLFontBase.cs @@ -0,0 +1,29 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLFontBase + { + Boolean Bold { get; set; } + + Boolean Italic { get; set; } + + XLFontUnderlineValues Underline { get; set; } + + Boolean Strikethrough { get; set; } + + XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; } + + Boolean Shadow { get; set; } + + Double FontSize { get; set; } + + XLColor FontColor { get; set; } + + String FontName { get; set; } + + XLFontFamilyNumberingValues FontFamilyNumbering { get; set; } + + XLFontCharSet FontCharSet { get; set; } + } +} diff --git a/ClosedXML/Excel/Style/IXLNumberFormat.cs b/ClosedXML/Excel/Style/IXLNumberFormat.cs new file mode 100644 index 0000000..cb55e51 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLNumberFormat.cs @@ -0,0 +1,11 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLNumberFormat : IXLNumberFormatBase, IEquatable + { + IXLStyle SetNumberFormatId(Int32 value); + + IXLStyle SetFormat(String value); + } +} diff --git a/ClosedXML/Excel/Style/IXLNumberFormatBase.cs b/ClosedXML/Excel/Style/IXLNumberFormatBase.cs new file mode 100644 index 0000000..d334a93 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLNumberFormatBase.cs @@ -0,0 +1,11 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLNumberFormatBase + { + Int32 NumberFormatId { get; set; } + + String Format { get; set; } + } +} diff --git a/ClosedXML/Excel/Style/IXLProtection.cs b/ClosedXML/Excel/Style/IXLProtection.cs new file mode 100644 index 0000000..94d9153 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLProtection.cs @@ -0,0 +1,15 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLProtection : IEquatable + { + Boolean Locked { get; set; } + + Boolean Hidden { get; set; } + + IXLStyle SetLocked(); IXLStyle SetLocked(Boolean value); + + IXLStyle SetHidden(); IXLStyle SetHidden(Boolean value); + } +} diff --git a/ClosedXML/Excel/Style/IXLStyle.cs b/ClosedXML/Excel/Style/IXLStyle.cs new file mode 100644 index 0000000..7634e61 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLStyle.cs @@ -0,0 +1,26 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLStyle : IEquatable + { + IXLAlignment Alignment { get; set; } + + IXLBorder Border { get; set; } + + IXLNumberFormat DateFormat { get; } + + IXLFill Fill { get; set; } + + IXLFont Font { get; set; } + + Boolean IncludeQuotePrefix { get; set; } + + IXLNumberFormat NumberFormat { get; set; } + + IXLProtection Protection { get; set; } + + IXLStyle SetIncludeQuotePrefix(Boolean includeQuotePrefix = true); + } +} diff --git a/ClosedXML/Excel/Style/IXLStylized.cs b/ClosedXML/Excel/Style/IXLStylized.cs new file mode 100644 index 0000000..c056e04 --- /dev/null +++ b/ClosedXML/Excel/Style/IXLStylized.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal interface IXLStylized + { + IXLStyle Style { get; set; } + + IEnumerable Styles { get; } + + IXLStyle InnerStyle { get; set; } + + IXLRanges RangesUsed { get; } + + /// + /// Immutable style + /// + XLStyleValue StyleValue { get; } + + void ModifyStyle(Func modification); + } +} diff --git a/ClosedXML/Excel/Style/XLAlignment.cs b/ClosedXML/Excel/Style/XLAlignment.cs new file mode 100644 index 0000000..3605776 --- /dev/null +++ b/ClosedXML/Excel/Style/XLAlignment.cs @@ -0,0 +1,327 @@ +#region + +using System; +using System.Text; + +#endregion + +namespace ClosedXML.Excel +{ + internal class XLAlignment : IXLAlignment + { + #region Static members + + internal static XLAlignmentKey GenerateKey(IXLAlignment d) + { + XLAlignmentKey key; + if (d == null) + { + key = XLAlignmentValue.Default.Key; + } + else if (d is XLAlignment) + { + key = (d as XLAlignment).Key; + } + else + { + key = new XLAlignmentKey + { + Horizontal = d.Horizontal, + Vertical = d.Vertical, + Indent = d.Indent, + JustifyLastLine = d.JustifyLastLine, + ReadingOrder = d.ReadingOrder, + RelativeIndent = d.RelativeIndent, + ShrinkToFit = d.ShrinkToFit, + TextRotation = d.TextRotation, + WrapText = d.WrapText + }; + } + return key; + } + + #endregion Static members + + #region Properties + private readonly XLStyle _style; + + private XLAlignmentValue _value; + + internal XLAlignmentKey Key + { + get { return _value.Key; } + private set { _value = XLAlignmentValue.FromKey(ref value); } + } + + #endregion Properties + + #region Constructors + + /// + /// Create an instance of XLAlignment initializing it with the specified value. + /// + /// Style to attach the new instance to. + /// Style value to use. + public XLAlignment(XLStyle style, XLAlignmentValue value) + { + _style = style ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLAlignment(XLStyle style, XLAlignmentKey key) : this(style, XLAlignmentValue.FromKey(ref key)) + { + } + + public XLAlignment(XLStyle style = null, IXLAlignment d = null) : this(style, GenerateKey(d)) + { + } + + #endregion Constructors + + #region IXLAlignment Members + + public XLAlignmentHorizontalValues Horizontal + { + get { return Key.Horizontal; } + set + { + Boolean updateIndent = !( + value == XLAlignmentHorizontalValues.Left + || value == XLAlignmentHorizontalValues.Right + || value == XLAlignmentHorizontalValues.Distributed + ); + + Modify(k => { k.Horizontal = value; return k; }); + if (updateIndent) + Indent = 0; + } + } + + public XLAlignmentVerticalValues Vertical + { + get { return Key.Vertical; } + set { Modify(k => { k.Vertical = value; return k; }); } + } + + public Int32 Indent + { + get { return Key.Indent; } + set + { + if (Indent != value) + { + if (Horizontal == XLAlignmentHorizontalValues.General) + Horizontal = XLAlignmentHorizontalValues.Left; + + if (value > 0 && !( + Horizontal == XLAlignmentHorizontalValues.Left + || Horizontal == XLAlignmentHorizontalValues.Right + || Horizontal == XLAlignmentHorizontalValues.Distributed + )) + { + throw new ArgumentException( + "For indents, only left, right, and distributed horizontal alignments are supported."); + } + } + Modify(k => { k.Indent = value; return k; }); + } + } + + public Boolean JustifyLastLine + { + get { return Key.JustifyLastLine; } + set { Modify(k => { k.JustifyLastLine = value; return k; }); } + } + + public XLAlignmentReadingOrderValues ReadingOrder + { + get { return Key.ReadingOrder; } + set { Modify(k => { k.ReadingOrder = value; return k; }); } + } + + public Int32 RelativeIndent + { + get { return Key.RelativeIndent; } + set { Modify(k => { k.RelativeIndent = value; return k; }); } + } + + public Boolean ShrinkToFit + { + get { return Key.ShrinkToFit; } + set { Modify(k => { k.ShrinkToFit = value; return k; }); } + } + + public Int32 TextRotation + { + get { return Key.TextRotation; } + set + { + Int32 rotation = value; + + if (rotation != 255 && (rotation < -90 || rotation > 180)) + throw new ArgumentException("TextRotation must be between -90 and 180 degrees, or 255."); + + Modify(k => { k.TextRotation = rotation; return k; }); + } + } + + public Boolean WrapText + { + get { return Key.WrapText; } + set { Modify(k => { k.WrapText = value; return k; }); } + } + + public Boolean TopToBottom + { + get { return TextRotation == 255; } + set { TextRotation = value ? 255 : 0; } + } + + public IXLStyle SetHorizontal(XLAlignmentHorizontalValues value) + { + Horizontal = value; + return _style; + } + + public IXLStyle SetVertical(XLAlignmentVerticalValues value) + { + Vertical = value; + return _style; + } + + public IXLStyle SetIndent(Int32 value) + { + Indent = value; + return _style; + } + + public IXLStyle SetJustifyLastLine() + { + JustifyLastLine = true; + return _style; + } + + public IXLStyle SetJustifyLastLine(Boolean value) + { + JustifyLastLine = value; + return _style; + } + + public IXLStyle SetReadingOrder(XLAlignmentReadingOrderValues value) + { + ReadingOrder = value; + return _style; + } + + public IXLStyle SetRelativeIndent(Int32 value) + { + RelativeIndent = value; + return _style; + } + + public IXLStyle SetShrinkToFit() + { + ShrinkToFit = true; + return _style; + } + + public IXLStyle SetShrinkToFit(Boolean value) + { + ShrinkToFit = value; + return _style; + } + + public IXLStyle SetTextRotation(Int32 value) + { + TextRotation = value; + return _style; + } + + public IXLStyle SetWrapText() + { + WrapText = true; + return _style; + } + + public IXLStyle SetWrapText(Boolean value) + { + WrapText = value; + return _style; + } + + public IXLStyle SetTopToBottom() + { + TopToBottom = true; + return _style; + } + + public IXLStyle SetTopToBottom(Boolean value) + { + TopToBottom = value; + return _style; + } + + #endregion + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var align = styleKey.Alignment; + styleKey.Alignment = modification(align); + return styleKey; + }); + } + + #region Overridden + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Horizontal); + sb.Append("-"); + sb.Append(Vertical); + sb.Append("-"); + sb.Append(Indent); + sb.Append("-"); + sb.Append(JustifyLastLine); + sb.Append("-"); + sb.Append(ReadingOrder); + sb.Append("-"); + sb.Append(RelativeIndent); + sb.Append("-"); + sb.Append(ShrinkToFit); + sb.Append("-"); + sb.Append(TextRotation); + sb.Append("-"); + sb.Append(WrapText); + sb.Append("-"); + return sb.ToString(); + } + + public override bool Equals(object obj) + { + return Equals(obj as XLAlignment); + } + + public bool Equals(IXLAlignment other) + { + var otherA = other as XLAlignment; + if (otherA == null) + return false; + + return Key == otherA.Key; + } + + public override int GetHashCode() + { + var hashCode = 1214962009; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLAlignmentKey.cs b/ClosedXML/Excel/Style/XLAlignmentKey.cs new file mode 100644 index 0000000..c167224 --- /dev/null +++ b/ClosedXML/Excel/Style/XLAlignmentKey.cs @@ -0,0 +1,78 @@ +using System; + +namespace ClosedXML.Excel +{ + public struct XLAlignmentKey : IEquatable + { + public XLAlignmentHorizontalValues Horizontal { get; set; } + + public XLAlignmentVerticalValues Vertical { get; set; } + + public int Indent { get; set; } + + public bool JustifyLastLine { get; set; } + + public XLAlignmentReadingOrderValues ReadingOrder { get; set; } + + public int RelativeIndent { get; set; } + + public bool ShrinkToFit { get; set; } + + public int TextRotation { get; set; } + + public bool WrapText { get; set; } + + public bool TopToBottom { get; set; } + + public bool Equals(XLAlignmentKey other) + { + return + Horizontal == other.Horizontal + && Vertical == other.Vertical + && Indent == other.Indent + && JustifyLastLine == other.JustifyLastLine + && ReadingOrder == other.ReadingOrder + && RelativeIndent == other.RelativeIndent + && ShrinkToFit == other.ShrinkToFit + && TextRotation == other.TextRotation + && WrapText == other.WrapText + && TopToBottom == other.TopToBottom; + } + + public override bool Equals(object obj) + { + if (obj is XLAlignment) + return Equals((XLAlignment)obj); + return base.Equals(obj); + } + + public override int GetHashCode() + { + var hashCode = -596887160; + hashCode = hashCode * -1521134295 + (int)Horizontal; + hashCode = hashCode * -1521134295 + (int)Vertical; + hashCode = hashCode * -1521134295 + Indent; + hashCode = hashCode * -1521134295 + JustifyLastLine.GetHashCode(); + hashCode = hashCode * -1521134295 + (int)ReadingOrder; + hashCode = hashCode * -1521134295 + RelativeIndent; + hashCode = hashCode * -1521134295 + ShrinkToFit.GetHashCode(); + hashCode = hashCode * -1521134295 + TextRotation; + hashCode = hashCode * -1521134295 + WrapText.GetHashCode(); + hashCode = hashCode * -1521134295 + TopToBottom.GetHashCode(); + return hashCode; + } + + public override string ToString() + { + return + $"{Horizontal} {Vertical} {ReadingOrder} Indent: {Indent} RelativeIndent: {RelativeIndent} TextRotation: {TextRotation} " + + (WrapText ? "WrapText" : "") + + (JustifyLastLine ? "JustifyLastLine" : "") + + (TopToBottom ? "TopToBottom" : ""); + } + + public static bool operator ==(XLAlignmentKey left, XLAlignmentKey right) => left.Equals(right); + + public static bool operator !=(XLAlignmentKey left, XLAlignmentKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLAlignmentValue.cs b/ClosedXML/Excel/Style/XLAlignmentValue.cs new file mode 100644 index 0000000..758f0ac --- /dev/null +++ b/ClosedXML/Excel/Style/XLAlignmentValue.cs @@ -0,0 +1,68 @@ +using ClosedXML.Excel.Caching; + +namespace ClosedXML.Excel +{ + public class XLAlignmentValue + { + private static readonly XLAlignmentRepository Repository = new XLAlignmentRepository(key => new XLAlignmentValue(key)); + + public static XLAlignmentValue FromKey(ref XLAlignmentKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLAlignmentKey DefaultKey = new XLAlignmentKey + { + Indent = 0, + Horizontal = XLAlignmentHorizontalValues.General, + JustifyLastLine = false, + ReadingOrder = XLAlignmentReadingOrderValues.ContextDependent, + RelativeIndent = 0, + ShrinkToFit = false, + TextRotation = 0, + Vertical = XLAlignmentVerticalValues.Bottom, + WrapText = false + }; + + internal static readonly XLAlignmentValue Default = FromKey(ref DefaultKey); + + public XLAlignmentKey Key { get; private set; } + + public XLAlignmentHorizontalValues Horizontal { get { return Key.Horizontal; } } + + public XLAlignmentVerticalValues Vertical { get { return Key.Vertical; } } + + public int Indent { get { return Key.Indent; } } + + public bool JustifyLastLine { get { return Key.JustifyLastLine; } } + + public XLAlignmentReadingOrderValues ReadingOrder { get { return Key.ReadingOrder; } } + + public int RelativeIndent { get { return Key.RelativeIndent; } } + + public bool ShrinkToFit { get { return Key.ShrinkToFit; } } + + public int TextRotation { get { return Key.TextRotation; } } + + public bool WrapText { get { return Key.WrapText; } } + + public bool TopToBottom { get { return Key.TopToBottom; } } + + private XLAlignmentValue(XLAlignmentKey key) + { + Key = key; + } + + public override bool Equals(object obj) + { + var cached = obj as XLAlignmentValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return 990326508 + Key.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Style/XLBorder.cs b/ClosedXML/Excel/Style/XLBorder.cs new file mode 100644 index 0000000..a856027 --- /dev/null +++ b/ClosedXML/Excel/Style/XLBorder.cs @@ -0,0 +1,611 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLBorder : IXLBorder + { + #region Static members + + internal static XLBorderKey GenerateKey(IXLBorder defaultBorder) + { + XLBorderKey key; + if (defaultBorder == null) + { + key = XLBorderValue.Default.Key; + } + else if (defaultBorder is XLBorder) + { + key = (defaultBorder as XLBorder).Key; + } + else + { + key = new XLBorderKey + { + LeftBorder = defaultBorder.LeftBorder, + LeftBorderColor = defaultBorder.LeftBorderColor.Key, + RightBorder = defaultBorder.RightBorder, + RightBorderColor = defaultBorder.RightBorderColor.Key, + TopBorder = defaultBorder.TopBorder, + TopBorderColor = defaultBorder.TopBorderColor.Key, + BottomBorder = defaultBorder.BottomBorder, + BottomBorderColor = defaultBorder.BottomBorderColor.Key, + DiagonalBorder = defaultBorder.DiagonalBorder, + DiagonalBorderColor = defaultBorder.DiagonalBorderColor.Key, + DiagonalUp = defaultBorder.DiagonalUp, + DiagonalDown = defaultBorder.DiagonalDown, + }; + } + return key; + } + + #endregion Static members + + private readonly XLStyle _style; + + private readonly IXLStylized _container; + + private XLBorderValue _value; + + internal XLBorderKey Key + { + get { return _value.Key; } + private set { _value = XLBorderValue.FromKey(ref value); } + } + + #region Constructors + + /// + /// Create an instance of XLBorder initializing it with the specified value. + /// + /// Container the border is applied to. + /// Style to attach the new instance to. + /// Style value to use. + public XLBorder(IXLStylized container, XLStyle style, XLBorderValue value) + { + _container = container; + _style = style ?? _container.Style as XLStyle ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLBorder(IXLStylized container, XLStyle style, XLBorderKey key) : this(container, style, XLBorderValue.FromKey(ref key)) + { + } + + public XLBorder(IXLStylized container, XLStyle style = null, IXLBorder d = null) : this(container, style, GenerateKey(d)) + { + } + + #endregion Constructors + + #region IXLBorder Members + + public XLBorderStyleValues OutsideBorder + { + set + { + if (_container == null) return; + + if (_container is XLWorksheet || _container is XLConditionalFormat) + { + Modify(k => + { + k.TopBorder = + k.BottomBorder = + k.LeftBorder = + k.RightBorder = value; + return k; + }); + } + else + { + foreach (IXLRange r in _container.RangesUsed) + { + r.FirstColumn().Style.Border.LeftBorder = value; + r.LastColumn().Style.Border.RightBorder = value; + r.FirstRow().Style.Border.TopBorder = value; + r.LastRow().Style.Border.BottomBorder = value; + } + } + } + } + + public XLColor OutsideBorderColor + { + set + { + if (_container == null) return; + + if (_container is XLWorksheet || _container is XLConditionalFormat) + { + Modify(k => + { + k.TopBorderColor = + k.BottomBorderColor = + k.LeftBorderColor = + k.RightBorderColor = value.Key; + return k; + }); + } + else + { + foreach (IXLRange r in _container.RangesUsed) + { + r.FirstColumn().Style.Border.LeftBorderColor = value; + r.LastColumn().Style.Border.RightBorderColor = value; + r.FirstRow().Style.Border.TopBorderColor = value; + r.LastRow().Style.Border.BottomBorderColor = value; + } + } + } + } + + public XLBorderStyleValues InsideBorder + { + set + { + if (_container == null) return; + + var wsContainer = _container as XLWorksheet; + if (wsContainer != null) + { + Modify(k => + { + k.TopBorder = + k.BottomBorder = + k.LeftBorder = + k.RightBorder = value; + return k; + }); + } + else + { + foreach (IXLRange r in _container.RangesUsed) + { + using (new RestoreOutsideBorder(r)) + { + foreach (var cell in r.Cells()) + { + (cell.Style.Border as XLBorder) + .Modify(k => + { + k.TopBorder = + k.BottomBorder = + k.LeftBorder = + k.RightBorder = value; + return k; + }); + } + } + } + } + } + } + + public XLColor InsideBorderColor + { + set + { + if (_container == null) return; + + var wsContainer = _container as XLWorksheet; + if (wsContainer != null) + { + Modify(k => + { + k.TopBorderColor = + k.BottomBorderColor = + k.LeftBorderColor = + k.RightBorderColor = value.Key; + return k; + }); + } + else + { + foreach (IXLRange r in _container.RangesUsed) + { + using (new RestoreOutsideBorder(r)) + { + foreach (var cell in r.Cells()) + { + (cell.Style.Border as XLBorder) + .Modify(k => + { + k.TopBorderColor = + k.BottomBorderColor = + k.LeftBorderColor = + k.RightBorderColor = value.Key; + return k; + }); + } + } + } + } + } + } + + public XLBorderStyleValues LeftBorder + { + get { return Key.LeftBorder; } + set { Modify(k => { k.LeftBorder = value; return k; }); } + } + + public XLColor LeftBorderColor + { + get + { + var colorKey = Key.LeftBorderColor; + return XLColor.FromKey(ref colorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.LeftBorderColor = value.Key; return k; }); + } + } + + public XLBorderStyleValues RightBorder + { + get { return Key.RightBorder; } + set { Modify(k => { k.RightBorder = value; return k; }); } + } + + public XLColor RightBorderColor + { + get + { + var colorKey = Key.RightBorderColor; + return XLColor.FromKey(ref colorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.RightBorderColor = value.Key; return k; }); + } + } + + public XLBorderStyleValues TopBorder + { + get { return Key.TopBorder; } + set { Modify(k => { k.TopBorder = value; return k; }); } + } + + public XLColor TopBorderColor + { + get + { + var colorKey = Key.TopBorderColor; + return XLColor.FromKey(ref colorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.TopBorderColor = value.Key; return k; }); + } + } + + public XLBorderStyleValues BottomBorder + { + get { return Key.BottomBorder; } + set { Modify(k => { k.BottomBorder = value; return k; }); } + } + + public XLColor BottomBorderColor + { + get + { + var colorKey = Key.BottomBorderColor; + return XLColor.FromKey(ref colorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.BottomBorderColor = value.Key; return k; }); + } + } + + public XLBorderStyleValues DiagonalBorder + { + get { return Key.DiagonalBorder; } + set { Modify(k => { k.DiagonalBorder = value; return k; }); } + } + + public XLColor DiagonalBorderColor + { + get + { + var colorKey = Key.DiagonalBorderColor; + return XLColor.FromKey(ref colorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.DiagonalBorderColor = value.Key; return k; }); + } + } + + public Boolean DiagonalUp + { + get { return Key.DiagonalUp; } + set { Modify(k => { k.DiagonalUp = value; return k; }); } + } + + public Boolean DiagonalDown + { + get { return Key.DiagonalDown; } + set { Modify(k => { k.DiagonalDown = value; return k; }); } + } + + public IXLStyle SetOutsideBorder(XLBorderStyleValues value) + { + OutsideBorder = value; + return _container.Style; + } + + public IXLStyle SetOutsideBorderColor(XLColor value) + { + OutsideBorderColor = value; + return _container.Style; + } + + public IXLStyle SetInsideBorder(XLBorderStyleValues value) + { + InsideBorder = value; + return _container.Style; + } + + public IXLStyle SetInsideBorderColor(XLColor value) + { + InsideBorderColor = value; + return _container.Style; + } + + public IXLStyle SetLeftBorder(XLBorderStyleValues value) + { + LeftBorder = value; + return _container.Style; + } + + public IXLStyle SetLeftBorderColor(XLColor value) + { + LeftBorderColor = value; + return _container.Style; + } + + public IXLStyle SetRightBorder(XLBorderStyleValues value) + { + RightBorder = value; + return _container.Style; + } + + public IXLStyle SetRightBorderColor(XLColor value) + { + RightBorderColor = value; + return _container.Style; + } + + public IXLStyle SetTopBorder(XLBorderStyleValues value) + { + TopBorder = value; + return _container.Style; + } + + public IXLStyle SetTopBorderColor(XLColor value) + { + TopBorderColor = value; + return _container.Style; + } + + public IXLStyle SetBottomBorder(XLBorderStyleValues value) + { + BottomBorder = value; + return _container.Style; + } + + public IXLStyle SetBottomBorderColor(XLColor value) + { + BottomBorderColor = value; + return _container.Style; + } + + public IXLStyle SetDiagonalUp() + { + DiagonalUp = true; + return _container.Style; + } + + public IXLStyle SetDiagonalUp(Boolean value) + { + DiagonalUp = value; + return _container.Style; + } + + public IXLStyle SetDiagonalDown() + { + DiagonalDown = true; + return _container.Style; + } + + public IXLStyle SetDiagonalDown(Boolean value) + { + DiagonalDown = value; + return _container.Style; + } + + public IXLStyle SetDiagonalBorder(XLBorderStyleValues value) + { + DiagonalBorder = value; + return _container.Style; + } + + public IXLStyle SetDiagonalBorderColor(XLColor value) + { + DiagonalBorderColor = value; + return _container.Style; + } + + #endregion IXLBorder Members + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var border = styleKey.Border; + styleKey.Border = modification(border); + return styleKey; + }); + } + + #region Overridden + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(LeftBorder.ToString()); + sb.Append("-"); + sb.Append(LeftBorderColor); + sb.Append("-"); + sb.Append(RightBorder.ToString()); + sb.Append("-"); + sb.Append(RightBorderColor); + sb.Append("-"); + sb.Append(TopBorder.ToString()); + sb.Append("-"); + sb.Append(TopBorderColor); + sb.Append("-"); + sb.Append(BottomBorder.ToString()); + sb.Append("-"); + sb.Append(BottomBorderColor); + sb.Append("-"); + sb.Append(DiagonalBorder.ToString()); + sb.Append("-"); + sb.Append(DiagonalBorderColor); + sb.Append("-"); + sb.Append(DiagonalUp.ToString()); + sb.Append("-"); + sb.Append(DiagonalDown.ToString()); + return sb.ToString(); + } + + public override bool Equals(object obj) + { + return Equals(obj as XLBorder); + } + + public bool Equals(IXLBorder other) + { + var otherB = other as XLBorder; + if (otherB == null) + return false; + + return Key == otherB.Key; + } + + public override int GetHashCode() + { + var hashCode = 416600561; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + + /// + /// Helper class that remembers outside border state before editing (in constructor) and restore afterwards (on disposing). + /// It presumes that size of the range does not change during the editing, else it will fail. + /// + private class RestoreOutsideBorder : IDisposable + { + private readonly IXLRange _range; + private readonly Dictionary _topBorders; + private readonly Dictionary _bottomBorders; + private readonly Dictionary _leftBorders; + private readonly Dictionary _rightBorders; + + public RestoreOutsideBorder(IXLRange range) + { + _range = range ?? throw new ArgumentNullException(nameof(range)); + + _topBorders = range.FirstRow().Cells().ToDictionary( + c => c.Address.ColumnNumber - range.RangeAddress.FirstAddress.ColumnNumber + 1, + c => (c.Style as XLStyle).Key.Border); + + _bottomBorders = range.LastRow().Cells().ToDictionary( + c => c.Address.ColumnNumber - range.RangeAddress.FirstAddress.ColumnNumber + 1, + c => (c.Style as XLStyle).Key.Border); + + _leftBorders = range.FirstColumn().Cells().ToDictionary( + c => c.Address.RowNumber - range.RangeAddress.FirstAddress.RowNumber + 1, + c => (c.Style as XLStyle).Key.Border); + + _rightBorders = range.LastColumn().Cells().ToDictionary( + c => c.Address.RowNumber - range.RangeAddress.FirstAddress.RowNumber + 1, + c => (c.Style as XLStyle).Key.Border); + } + + // Used by Janitor.Fody + private void DisposeManaged() + { + _topBorders.ForEach(kp => (_range.FirstRow().Cell(kp.Key).Style + .Border as XLBorder).Modify(k => + { + k.TopBorder = kp.Value.TopBorder; + k.TopBorderColor = kp.Value.TopBorderColor; + return k; + })); + _bottomBorders.ForEach(kp => (_range.LastRow().Cell(kp.Key).Style + .Border as XLBorder).Modify(k => + { + k.BottomBorder = kp.Value.BottomBorder; + k.BottomBorderColor = kp.Value.BottomBorderColor; + return k; + })); + _leftBorders.ForEach(kp => (_range.FirstColumn().Cell(kp.Key).Style + .Border as XLBorder).Modify(k => + { + k.LeftBorder = kp.Value.LeftBorder; + k.LeftBorderColor = kp.Value.LeftBorderColor; + return k; + })); + _rightBorders.ForEach(kp => (_range.LastColumn().Cell(kp.Key).Style + .Border as XLBorder).Modify(k => + { + k.RightBorder = kp.Value.RightBorder; + k.RightBorderColor = kp.Value.RightBorderColor; + return k; + })); + } + +#if _NET40_ + + public void Dispose() + { + // net40 doesn't support Janitor.Fody, so let's dispose manually + DisposeManaged(); + } + +#else + + public void Dispose() + { + // Leave this empty (for non net40 targets) so that Janitor.Fody can do its work + } + +#endif + } + } +} diff --git a/ClosedXML/Excel/Style/XLBorderKey.cs b/ClosedXML/Excel/Style/XLBorderKey.cs new file mode 100644 index 0000000..facb097 --- /dev/null +++ b/ClosedXML/Excel/Style/XLBorderKey.cs @@ -0,0 +1,97 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLBorderKey : IEquatable + { + public XLBorderStyleValues LeftBorder { get; set; } + + public XLColorKey LeftBorderColor { get; set; } + + public XLBorderStyleValues RightBorder { get; set; } + + public XLColorKey RightBorderColor { get; set; } + + public XLBorderStyleValues TopBorder { get; set; } + + public XLColorKey TopBorderColor { get; set; } + + public XLBorderStyleValues BottomBorder { get; set; } + + public XLColorKey BottomBorderColor { get; set; } + + public XLBorderStyleValues DiagonalBorder { get; set; } + + public XLColorKey DiagonalBorderColor { get; set; } + + public bool DiagonalUp { get; set; } + + public bool DiagonalDown { get; set; } + + public override int GetHashCode() + { + var hashCode = -198124310; + hashCode = hashCode * -1521134295 + (int)LeftBorder; + hashCode = hashCode * -1521134295 + (int)RightBorder; + hashCode = hashCode * -1521134295 + (int)TopBorder; + hashCode = hashCode * -1521134295 + (int)BottomBorder; + hashCode = hashCode * -1521134295 + (int)DiagonalBorder; + hashCode = hashCode * -1521134295 + DiagonalUp.GetHashCode(); + hashCode = hashCode * -1521134295 + DiagonalDown.GetHashCode(); + + if (LeftBorder != XLBorderStyleValues.None) + hashCode = hashCode * -1521134295 + LeftBorderColor.GetHashCode(); + if (RightBorder != XLBorderStyleValues.None) + hashCode = hashCode * -1521134295 + RightBorderColor.GetHashCode(); + if (TopBorder != XLBorderStyleValues.None) + hashCode = hashCode * -1521134295 + TopBorderColor.GetHashCode(); + if (BottomBorder != XLBorderStyleValues.None) + hashCode = hashCode * -1521134295 + BottomBorderColor.GetHashCode(); + if (DiagonalBorder != XLBorderStyleValues.None) + hashCode = hashCode * -1521134295 + DiagonalBorderColor.GetHashCode(); + + return hashCode; + } + + public bool Equals(XLBorderKey other) + { + return + AreEquivalent(LeftBorder, LeftBorderColor, other.LeftBorder, other.LeftBorderColor) + && AreEquivalent(RightBorder, RightBorderColor, other.RightBorder, other.RightBorderColor) + && AreEquivalent(TopBorder, TopBorderColor, other.TopBorder, other.TopBorderColor) + && AreEquivalent(BottomBorder, BottomBorderColor, other.BottomBorder, other.BottomBorderColor) + && AreEquivalent(DiagonalBorder, DiagonalBorderColor, other.DiagonalBorder, other.DiagonalBorderColor) + && DiagonalUp == other.DiagonalUp + && DiagonalDown == other.DiagonalDown; + } + + private bool AreEquivalent( + XLBorderStyleValues borderStyle1, XLColorKey color1, + XLBorderStyleValues borderStyle2, XLColorKey color2) + { + return (borderStyle1 == XLBorderStyleValues.None && + borderStyle2 == XLBorderStyleValues.None) || + borderStyle1 == borderStyle2 && + color1 == color2; + } + + public override bool Equals(object obj) + { + if (obj is XLBorderKey) + return Equals((XLBorderKey)obj); + return base.Equals(obj); + } + + public override string ToString() + { + return $"{LeftBorder} {LeftBorderColor} {RightBorder} {RightBorderColor} {TopBorder} {TopBorderColor} " + + $"{BottomBorder} {BottomBorderColor} {DiagonalBorder} {DiagonalBorderColor} " + + (DiagonalUp ? "DiagonalUp" : "") + + (DiagonalDown ? "DiagonalDown" : ""); + } + + public static bool operator ==(XLBorderKey left, XLBorderKey right) => left.Equals(right); + + public static bool operator !=(XLBorderKey left, XLBorderKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLBorderValue.cs b/ClosedXML/Excel/Style/XLBorderValue.cs new file mode 100644 index 0000000..f5cfd6c --- /dev/null +++ b/ClosedXML/Excel/Style/XLBorderValue.cs @@ -0,0 +1,85 @@ +using ClosedXML.Excel.Caching; + +namespace ClosedXML.Excel +{ + internal class XLBorderValue + { + private static readonly XLBorderRepository Repository = new XLBorderRepository(key => new XLBorderValue(key)); + + public static XLBorderValue FromKey(ref XLBorderKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLBorderKey DefaultKey = new XLBorderKey + { + BottomBorder = XLBorderStyleValues.None, + DiagonalBorder = XLBorderStyleValues.None, + DiagonalDown = false, + DiagonalUp = false, + LeftBorder = XLBorderStyleValues.None, + RightBorder = XLBorderStyleValues.None, + TopBorder = XLBorderStyleValues.None, + BottomBorderColor = XLColor.Black.Key, + DiagonalBorderColor = XLColor.Black.Key, + LeftBorderColor = XLColor.Black.Key, + RightBorderColor = XLColor.Black.Key, + TopBorderColor = XLColor.Black.Key + }; + + internal static readonly XLBorderValue Default = FromKey(ref DefaultKey); + + public XLBorderKey Key { get; private set; } + + public XLBorderStyleValues LeftBorder { get { return Key.LeftBorder; } } + + public XLColor LeftBorderColor { get; private set; } + + public XLBorderStyleValues RightBorder { get { return Key.RightBorder; } } + + public XLColor RightBorderColor { get; private set; } + + public XLBorderStyleValues TopBorder { get { return Key.TopBorder; } } + + public XLColor TopBorderColor { get; private set; } + + public XLBorderStyleValues BottomBorder { get { return Key.BottomBorder; } } + + public XLColor BottomBorderColor { get; private set; } + + public XLBorderStyleValues DiagonalBorder { get { return Key.DiagonalBorder; } } + + public XLColor DiagonalBorderColor { get; private set; } + + public bool DiagonalUp { get { return Key.DiagonalUp; } } + + public bool DiagonalDown { get { return Key.DiagonalDown; } } + + private XLBorderValue(XLBorderKey key) + { + Key = key; + var leftBorderColor = Key.LeftBorderColor; + var rightBorderColor = Key.RightBorderColor; + var topBorderColor = Key.TopBorderColor; + var bottomBorderColor = Key.BottomBorderColor; + var diagonalBorderColor = Key.DiagonalBorderColor; + LeftBorderColor = XLColor.FromKey(ref leftBorderColor); + RightBorderColor = XLColor.FromKey(ref rightBorderColor); + TopBorderColor = XLColor.FromKey(ref topBorderColor); + BottomBorderColor = XLColor.FromKey(ref bottomBorderColor); + DiagonalBorderColor = XLColor.FromKey(ref diagonalBorderColor); + } + + public override bool Equals(object obj) + { + var cached = obj as XLBorderValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return -280332839 + Key.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Style/XLColorKey.cs b/ClosedXML/Excel/Style/XLColorKey.cs new file mode 100644 index 0000000..a30822b --- /dev/null +++ b/ClosedXML/Excel/Style/XLColorKey.cs @@ -0,0 +1,76 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLColorKey : IEquatable + { + public XLColorType ColorType { get; set; } + + public System.Drawing.Color Color { get; set; } + + public int Indexed { get; set; } + + public XLThemeColor ThemeColor { get; set; } + + public double ThemeTint { get; set; } + + public override int GetHashCode() + { + var hashCode = -331517974; + hashCode = hashCode * -1521134295 + (int)ColorType; + hashCode = hashCode * -1521134295 + (ColorType == XLColorType.Indexed ? Indexed : 0); + hashCode = hashCode * -1521134295 + (ColorType == XLColorType.Theme ? (int)ThemeColor : 0); + hashCode = hashCode * -1521134295 + (ColorType == XLColorType.Theme ? ThemeTint.GetHashCode() : 0); + hashCode = hashCode * -1521134295 + (ColorType == XLColorType.Color ? Color.ToArgb() : 0); + return hashCode; + } + + public bool Equals(XLColorKey other) + { + if (ColorType == other.ColorType) + { + if (ColorType == XLColorType.Color) + { + // .NET Color.Equals() will return false for Color.FromArgb(255, 255, 255, 255) == Color.White + // Therefore we compare the ToArgb() values + return Color.ToArgb() == other.Color.ToArgb(); + } + if (ColorType == XLColorType.Theme) + { + return + ThemeColor == other.ThemeColor + && Math.Abs(ThemeTint - other.ThemeTint) < XLHelper.Epsilon; + } + return Indexed == other.Indexed; + } + + return false; + } + + public override bool Equals(object obj) + { + if (obj is XLColorKey) + return Equals((XLColorKey)obj); + return base.Equals(obj); + } + + public override string ToString() + { + switch (ColorType) + { + case XLColorType.Color: + return Color.ToString(); + case XLColorType.Theme: + return $"{ThemeColor} ({ThemeTint})"; + case XLColorType.Indexed: + return $"Indexed: {Indexed}"; + default: + return base.ToString(); + } + } + + public static bool operator ==(XLColorKey left, XLColorKey right) => left.Equals(right); + + public static bool operator !=(XLColorKey left, XLColorKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLFill.cs b/ClosedXML/Excel/Style/XLFill.cs new file mode 100644 index 0000000..0835fe5 --- /dev/null +++ b/ClosedXML/Excel/Style/XLFill.cs @@ -0,0 +1,213 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLFill : IXLFill + { + #region static members + + internal static XLFillKey GenerateKey(IXLFill defaultFill) + { + XLFillKey key; + if (defaultFill == null) + { + key = XLFillValue.Default.Key; + } + else if (defaultFill is XLFill) + { + key = (defaultFill as XLFill).Key; + } + else + { + key = new XLFillKey + { + PatternType = defaultFill.PatternType, + BackgroundColor = defaultFill.BackgroundColor.Key, + PatternColor = defaultFill.PatternColor.Key + }; + } + return key; + } + + #endregion static members + + #region Properties + + private readonly XLStyle _style; + + private XLFillValue _value; + + internal XLFillKey Key + { + get { return _value.Key; } + private set { _value = XLFillValue.FromKey(ref value); } + } + + #endregion Properties + + #region Constructors + + /// + /// Create an instance of XLFill initializing it with the specified value. + /// + /// Style to attach the new instance to. + /// Style value to use. + public XLFill(XLStyle style, XLFillValue value) + { + _style = style ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLFill(XLStyle style, XLFillKey key) : this(style, XLFillValue.FromKey(ref key)) + { + } + + public XLFill(XLStyle style = null, IXLFill d = null) : this(style, GenerateKey(d)) + { + } + + #endregion Constructors + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var fill = styleKey.Fill; + styleKey.Fill = modification(fill); + return styleKey; + }); + } + + #region IXLFill Members + + public XLColor BackgroundColor + { + get + { + var backgroundColorKey = Key.BackgroundColor; + return XLColor.FromKey(ref backgroundColorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + if ((PatternType == XLFillPatternValues.None || + PatternType == XLFillPatternValues.Solid) + && XLColor.IsNullOrTransparent(BackgroundColor)) + { + var patternType = value.HasValue ? XLFillPatternValues.Solid : XLFillPatternValues.None; + Modify(k => + { + k.BackgroundColor = value.Key; + k.PatternType = patternType; + return k; + }); + } + else + Modify(k => { k.BackgroundColor = value.Key; return k; }); + } + } + + public XLColor PatternColor + { + get + { + var patternColorKey = Key.PatternColor; + return XLColor.FromKey(ref patternColorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + + Modify(k => { k.PatternColor = value.Key; return k; }); + } + } + + public XLFillPatternValues PatternType + { + get { return Key.PatternType; } + set + { + if (PatternType == XLFillPatternValues.None && + value != XLFillPatternValues.None) + { + // If fill was empty and the pattern changes to non-empty we have to specify a background color too. + // Otherwise the fill will be considered empty and pattern won't update (the cached empty fill will be used). + Modify(k => + { + k.BackgroundColor = XLColor.FromTheme(XLThemeColor.Text1).Key; + k.PatternType = value; + return k; + }); + } + else + Modify(k => { k.PatternType = value; return k; }); + } + } + + public IXLStyle SetBackgroundColor(XLColor value) + { + BackgroundColor = value; + return _style; + } + + public IXLStyle SetPatternColor(XLColor value) + { + PatternColor = value; + return _style; + } + + public IXLStyle SetPatternType(XLFillPatternValues value) + { + PatternType = value; + return _style; + } + + #endregion IXLFill Members + + #region Overridden + + public override bool Equals(object obj) + { + return Equals(obj as XLFill); + } + + public bool Equals(IXLFill other) + { + var otherF = other as XLFill; + if (otherF == null) + return false; + + return Key == otherF.Key; + } + + public override string ToString() + { + switch (PatternType) + { + case XLFillPatternValues.None: + return "None"; + + case XLFillPatternValues.Solid: + return string.Concat("Solid ", BackgroundColor.ToString()); + + default: + return string.Concat(PatternType.ToString(), " pattern: ", PatternColor.ToString(), " on ", BackgroundColor.ToString()); + } + } + + public override int GetHashCode() + { + var hashCode = -1938644919; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLFillKey.cs b/ClosedXML/Excel/Style/XLFillKey.cs new file mode 100644 index 0000000..de1a2cf --- /dev/null +++ b/ClosedXML/Excel/Style/XLFillKey.cs @@ -0,0 +1,68 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLFillKey : IEquatable + { + public XLColorKey BackgroundColor { get; set; } + + public XLColorKey PatternColor { get; set; } + + public XLFillPatternValues PatternType { get; set; } + + public override int GetHashCode() + { + var hashCode = 2043579837; + + if (HasNoFill()) return hashCode; + + hashCode = hashCode * -1521134295 + (int)PatternType; + hashCode = hashCode * -1521134295 + BackgroundColor.GetHashCode(); + + if (HasNoForeground()) return hashCode; + + hashCode = hashCode * -1521134295 + PatternColor.GetHashCode(); + + return hashCode; + } + + public bool Equals(XLFillKey other) + { + if (HasNoFill() && other.HasNoFill()) + return true; + + return BackgroundColor == other.BackgroundColor + && PatternType == other.PatternType + && (HasNoForeground() && other.HasNoForeground() || + PatternColor == other.PatternColor); + } + + private bool HasNoFill() + { + return PatternType == XLFillPatternValues.None + || (PatternType == XLFillPatternValues.Solid && XLColor.IsTransparent(BackgroundColor)); + } + + private bool HasNoForeground() + { + return PatternType == XLFillPatternValues.Solid || + PatternType == XLFillPatternValues.None; + } + + public override bool Equals(object obj) + { + if (obj is XLFillKey) + return Equals((XLFillKey)obj); + return base.Equals(obj); + } + + public override string ToString() + { + return $"{PatternType} {BackgroundColor}/{PatternColor}"; + } + + public static bool operator ==(XLFillKey left, XLFillKey right) => left.Equals(right); + + public static bool operator !=(XLFillKey left, XLFillKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLFillValue.cs b/ClosedXML/Excel/Style/XLFillValue.cs new file mode 100644 index 0000000..f7c2eb0 --- /dev/null +++ b/ClosedXML/Excel/Style/XLFillValue.cs @@ -0,0 +1,52 @@ +using ClosedXML.Excel.Caching; + +namespace ClosedXML.Excel +{ + internal sealed class XLFillValue + { + private static readonly XLFillRepository Repository = new XLFillRepository(key => new XLFillValue(key)); + + public static XLFillValue FromKey(ref XLFillKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLFillKey DefaultKey = new XLFillKey + { + BackgroundColor = XLColor.FromIndex(64).Key, + PatternType = XLFillPatternValues.None, + PatternColor = XLColor.FromIndex(64).Key + }; + + internal static readonly XLFillValue Default = FromKey(ref DefaultKey); + + public XLFillKey Key { get; private set; } + + public XLColor BackgroundColor { get; private set; } + + public XLColor PatternColor { get; private set; } + + public XLFillPatternValues PatternType { get { return Key.PatternType; } } + + private XLFillValue(XLFillKey key) + { + Key = key; + var backgroundColorKey = Key.BackgroundColor; + var patternColorKey = Key.PatternColor; + BackgroundColor = XLColor.FromKey(ref backgroundColorKey); + PatternColor = XLColor.FromKey(ref patternColorKey); + } + + public override bool Equals(object obj) + { + var cached = obj as XLFillValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return -280332839 + Key.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Style/XLFont.cs b/ClosedXML/Excel/Style/XLFont.cs new file mode 100644 index 0000000..22a45d8 --- /dev/null +++ b/ClosedXML/Excel/Style/XLFont.cs @@ -0,0 +1,357 @@ +using System; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLFont : IXLFont + { + #region Static members + + public static IXLFontBase DefaultCommentFont + { + get + { + // MS Excel uses Tahoma 9 Swiss no matter what current style font + var defaultCommentFont = new XLFont + { + FontName = "Tahoma", + FontSize = 9, + FontFamilyNumbering = XLFontFamilyNumberingValues.Swiss + }; + + return defaultCommentFont; + } + } + + internal static XLFontKey GenerateKey(IXLFontBase defaultFont) + { + if (defaultFont == null) + { + return XLFontValue.Default.Key; + } + else if (defaultFont is XLFont) + { + return (defaultFont as XLFont).Key; + } + else + { + return new XLFontKey + { + Bold = defaultFont.Bold, + Italic = defaultFont.Italic, + Underline = defaultFont.Underline, + Strikethrough = defaultFont.Strikethrough, + VerticalAlignment = defaultFont.VerticalAlignment, + Shadow = defaultFont.Shadow, + FontSize = defaultFont.FontSize, + FontColor = defaultFont.FontColor.Key, + FontName = defaultFont.FontName, + FontFamilyNumbering = defaultFont.FontFamilyNumbering, + FontCharSet = defaultFont.FontCharSet + }; + } + } + + #endregion Static members + + private readonly XLStyle _style; + + private XLFontValue _value; + + internal XLFontKey Key + { + get { return _value.Key; } + private set { _value = XLFontValue.FromKey(ref value); } + } + + #region Constructors + + /// + /// Create an instance of XLFont initializing it with the specified value. + /// + /// Style to attach the new instance to. + /// Style value to use. + public XLFont(XLStyle style, XLFontValue value) + { + _style = style ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLFont(XLStyle style, XLFontKey key) : this(style, XLFontValue.FromKey(ref key)) + { + } + + public XLFont(XLStyle style = null, IXLFont d = null) : this(style, GenerateKey(d)) + { + } + + #endregion Constructors + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var font = styleKey.Font; + styleKey.Font = modification(font); + return styleKey; + }); + } + + #region IXLFont Members + + public Boolean Bold + { + get { return Key.Bold; } + set + { + Modify(k => { k.Bold = value; return k; }); + } + } + + public Boolean Italic + { + get { return Key.Italic; } + set + { + Modify(k => { k.Italic = value; return k; }); + } + } + + public XLFontUnderlineValues Underline + { + get { return Key.Underline; } + set + { + Modify(k => { k.Underline = value; return k; }); + } + } + + public Boolean Strikethrough + { + get { return Key.Strikethrough; } + set + { + Modify(k => { k.Strikethrough = value; return k; }); + } + } + + public XLFontVerticalTextAlignmentValues VerticalAlignment + { + get { return Key.VerticalAlignment; } + set + { + Modify(k => { k.VerticalAlignment = value; return k; }); + } + } + + public Boolean Shadow + { + get { return Key.Shadow; } + set + { + Modify(k => { k.Shadow = value; return k; }); + } + } + + public Double FontSize + { + get { return Key.FontSize; } + set + { + Modify(k => { k.FontSize = value; return k; }); + } + } + + public XLColor FontColor + { + get + { + var fontColorKey = Key.FontColor; + return XLColor.FromKey(ref fontColorKey); + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Color cannot be null"); + Modify(k => { k.FontColor = value.Key; return k; }); + } + } + + public String FontName + { + get { return Key.FontName; } + set + { + Modify(k => { k.FontName = value; return k; }); + } + } + + public XLFontFamilyNumberingValues FontFamilyNumbering + { + get { return Key.FontFamilyNumbering; } + set + { + Modify(k => { k.FontFamilyNumbering = value; return k; }); + } + } + + public XLFontCharSet FontCharSet + { + get { return Key.FontCharSet; } + set + { + Modify(k => { k.FontCharSet = value; return k; }); + } + } + + public IXLStyle SetBold() + { + Bold = true; + return _style; + } + + public IXLStyle SetBold(Boolean value) + { + Bold = value; + return _style; + } + + public IXLStyle SetItalic() + { + Italic = true; + return _style; + } + + public IXLStyle SetItalic(Boolean value) + { + Italic = value; + return _style; + } + + public IXLStyle SetUnderline() + { + Underline = XLFontUnderlineValues.Single; + return _style; + } + + public IXLStyle SetUnderline(XLFontUnderlineValues value) + { + Underline = value; + return _style; + } + + public IXLStyle SetStrikethrough() + { + Strikethrough = true; + return _style; + } + + public IXLStyle SetStrikethrough(Boolean value) + { + Strikethrough = value; + return _style; + } + + public IXLStyle SetVerticalAlignment(XLFontVerticalTextAlignmentValues value) + { + VerticalAlignment = value; + return _style; + } + + public IXLStyle SetShadow() + { + Shadow = true; + return _style; + } + + public IXLStyle SetShadow(Boolean value) + { + Shadow = value; + return _style; + } + + public IXLStyle SetFontSize(Double value) + { + FontSize = value; + return _style; + } + + public IXLStyle SetFontColor(XLColor value) + { + FontColor = value; + return _style; + } + + public IXLStyle SetFontName(String value) + { + FontName = value; + return _style; + } + + public IXLStyle SetFontFamilyNumbering(XLFontFamilyNumberingValues value) + { + FontFamilyNumbering = value; + return _style; + } + + public IXLStyle SetFontCharSet(XLFontCharSet value) + { + FontCharSet = value; + return _style; + } + + #endregion IXLFont Members + + #region Overridden + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Bold.ToString()); + sb.Append("-"); + sb.Append(Italic.ToString()); + sb.Append("-"); + sb.Append(Underline.ToString()); + sb.Append("-"); + sb.Append(Strikethrough.ToString()); + sb.Append("-"); + sb.Append(VerticalAlignment.ToString()); + sb.Append("-"); + sb.Append(Shadow.ToString()); + sb.Append("-"); + sb.Append(FontSize.ToString()); + sb.Append("-"); + sb.Append(FontColor); + sb.Append("-"); + sb.Append(FontName); + sb.Append("-"); + sb.Append(FontFamilyNumbering.ToString()); + return sb.ToString(); + } + + public override bool Equals(object obj) + { + return Equals(obj as XLFont); + } + + public Boolean Equals(IXLFont other) + { + var otherF = other as XLFont; + if (otherF == null) + return false; + + return Key == otherF.Key; + } + + public override int GetHashCode() + { + var hashCode = 416600561; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLFontKey.cs b/ClosedXML/Excel/Style/XLFontKey.cs new file mode 100644 index 0000000..1afbbf1 --- /dev/null +++ b/ClosedXML/Excel/Style/XLFontKey.cs @@ -0,0 +1,81 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLFontKey : IEquatable + { + public bool Bold { get; set; } + + public bool Italic { get; set; } + + public XLFontUnderlineValues Underline { get; set; } + + public bool Strikethrough { get; set; } + + public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; } + + public bool Shadow { get; set; } + + public double FontSize { get; set; } + + public XLColorKey FontColor { get; set; } + + public string FontName { get; set; } + + public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; } + + public XLFontCharSet FontCharSet { get; set; } + + public bool Equals(XLFontKey other) + { + return + Bold == other.Bold + && Italic == other.Italic + && Underline == other.Underline + && Strikethrough == other.Strikethrough + && VerticalAlignment == other.VerticalAlignment + && Shadow == other.Shadow + && FontSize == other.FontSize + && FontColor == other.FontColor + && FontFamilyNumbering == other.FontFamilyNumbering + && FontCharSet == other.FontCharSet + && string.Equals(FontName, other.FontName, StringComparison.InvariantCultureIgnoreCase); + } + + public override bool Equals(object obj) + { + if (obj is XLFontKey) + return Equals((XLFontKey)obj); + return base.Equals(obj); + } + + public override int GetHashCode() + { + var hashCode = 1158783753; + hashCode = hashCode * -1521134295 + Bold.GetHashCode(); + hashCode = hashCode * -1521134295 + Italic.GetHashCode(); + hashCode = hashCode * -1521134295 + (int)Underline; + hashCode = hashCode * -1521134295 + Strikethrough.GetHashCode(); + hashCode = hashCode * -1521134295 + (int)VerticalAlignment; + hashCode = hashCode * -1521134295 + Shadow.GetHashCode(); + hashCode = hashCode * -1521134295 + FontSize.GetHashCode(); + hashCode = hashCode * -1521134295 + FontColor.GetHashCode(); + hashCode = hashCode * -1521134295 + StringComparer.InvariantCultureIgnoreCase.GetHashCode(FontName); + hashCode = hashCode * -1521134295 + (int)FontFamilyNumbering; + hashCode = hashCode * -1521134295 + (int)FontCharSet; + return hashCode; + } + + public override string ToString() + { + return $"{FontName} {FontSize}pt {FontColor} " + + (Bold ? "Bold" : "") + (Italic ? "Italic" : "") + (Strikethrough ? "Strikethrough" : "") + + (Underline == XLFontUnderlineValues.None ? "" : Underline.ToString()) + + $"{FontFamilyNumbering} {FontCharSet}"; + } + + public static bool operator ==(XLFontKey left, XLFontKey right) => left.Equals(right); + + public static bool operator !=(XLFontKey left, XLFontKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLFontValue.cs b/ClosedXML/Excel/Style/XLFontValue.cs new file mode 100644 index 0000000..37b4d74 --- /dev/null +++ b/ClosedXML/Excel/Style/XLFontValue.cs @@ -0,0 +1,73 @@ +using ClosedXML.Excel.Caching; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal sealed class XLFontValue + { + private static readonly XLFontRepository Repository = new XLFontRepository(key => new XLFontValue(key)); + + public static XLFontValue FromKey(ref XLFontKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLFontKey DefaultKey = new XLFontKey + { + Bold = false, + Italic = false, + Underline = XLFontUnderlineValues.None, + Strikethrough = false, + VerticalAlignment = XLFontVerticalTextAlignmentValues.Baseline, + FontSize = 11, + FontColor = XLColor.FromArgb(0, 0, 0).Key, + FontName = "Calibri", + FontFamilyNumbering = XLFontFamilyNumberingValues.Swiss, + FontCharSet = XLFontCharSet.Default + }; + internal static readonly XLFontValue Default = FromKey(ref DefaultKey); + + public XLFontKey Key { get; private set; } + + public bool Bold { get { return Key.Bold; } } + + public bool Italic { get { return Key.Italic; } } + + public XLFontUnderlineValues Underline { get { return Key.Underline; } } + + public bool Strikethrough { get { return Key.Strikethrough; } } + + public XLFontVerticalTextAlignmentValues VerticalAlignment { get { return Key.VerticalAlignment; } } + + public bool Shadow { get { return Key.Shadow; } } + + public double FontSize { get { return Key.FontSize; } } + + public XLColor FontColor { get; private set; } + + public string FontName { get { return Key.FontName; } } + + public XLFontFamilyNumberingValues FontFamilyNumbering { get { return Key.FontFamilyNumbering; } } + + public XLFontCharSet FontCharSet { get { return Key.FontCharSet; } } + + private XLFontValue(XLFontKey key) + { + Key = key; + var fontColorKey = Key.FontColor; + FontColor = XLColor.FromKey(ref fontColorKey); + } + + public override bool Equals(object obj) + { + var cached = obj as XLFontValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return -280332839 + EqualityComparer.Default.GetHashCode(Key); + } + } +} diff --git a/ClosedXML/Excel/Style/XLNumberFormat.cs b/ClosedXML/Excel/Style/XLNumberFormat.cs new file mode 100644 index 0000000..54ffb77 --- /dev/null +++ b/ClosedXML/Excel/Style/XLNumberFormat.cs @@ -0,0 +1,152 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLNumberFormat : IXLNumberFormat + { + #region Static members + + internal static XLNumberFormatKey GenerateKey(IXLNumberFormat defaultNumberFormat) + { + if (defaultNumberFormat == null) + return XLNumberFormatValue.Default.Key; + + if (defaultNumberFormat is XLNumberFormat) + return (defaultNumberFormat as XLNumberFormat).Key; + + return new XLNumberFormatKey + { + NumberFormatId = defaultNumberFormat.NumberFormatId, + Format = defaultNumberFormat.Format + }; + } + + #endregion Static members + + #region Properties + + private readonly XLStyle _style; + + private XLNumberFormatValue _value; + + internal XLNumberFormatKey Key + { + get { return _value.Key; } + private set { _value = XLNumberFormatValue.FromKey(ref value); } + } + + #endregion Properties + + #region Constructors + + /// + /// Create an instance of XLNumberFormat initializing it with the specified value. + /// + /// Style to attach the new instance to. + /// Style value to use. + public XLNumberFormat(XLStyle style, XLNumberFormatValue value) + { + _style = style ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLNumberFormat(XLStyle style, XLNumberFormatKey key) : this(style, XLNumberFormatValue.FromKey(ref key)) + { + } + + public XLNumberFormat(XLStyle style = null, IXLNumberFormat d = null) : this(style, GenerateKey(d)) + { + } + + #endregion Constructors + + #region IXLNumberFormat Members + + public Int32 NumberFormatId + { + get { return Key.NumberFormatId; } + set + { + Modify(k => + { + k.Format = XLNumberFormatValue.Default.Format; + k.NumberFormatId = value; + return k; + }); + } + } + + public String Format + { + get { return Key.Format; } + set + { + Modify(k => + { + k.Format = value; + if (string.IsNullOrWhiteSpace(k.Format)) + k.NumberFormatId = XLNumberFormatValue.Default.NumberFormatId; + else + k.NumberFormatId = -1; + return k; + }); + } + } + + public IXLStyle SetNumberFormatId(Int32 value) + { + NumberFormatId = value; + return _style; + } + + public IXLStyle SetFormat(String value) + { + Format = value; + return _style; + } + + #endregion IXLNumberFormat Members + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var numberFormat = styleKey.NumberFormat; + styleKey.NumberFormat = modification(numberFormat); + return styleKey; + }); + } + + #region Overridden + + public override string ToString() + { + return NumberFormatId + "-" + Format; + } + + public override bool Equals(object obj) + { + return Equals(obj as IXLNumberFormatBase); + } + + public bool Equals(IXLNumberFormatBase other) + { + var otherN = other as XLNumberFormat; + if (otherN == null) + return false; + + return Key == otherN.Key; + } + + public override int GetHashCode() + { + var hashCode = 416600561; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLNumberFormatKey.cs b/ClosedXML/Excel/Style/XLNumberFormatKey.cs new file mode 100644 index 0000000..7e979a9 --- /dev/null +++ b/ClosedXML/Excel/Style/XLNumberFormatKey.cs @@ -0,0 +1,42 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLNumberFormatKey : IEquatable + { + public int NumberFormatId { get; set; } + + public string Format { get; set; } + + public override int GetHashCode() + { + var hashCode = -759193072; + hashCode = hashCode * -1521134295 + NumberFormatId; + hashCode = hashCode * -1521134295 + (Format == null ? 0 : Format.GetHashCode()); + return hashCode; + } + + public bool Equals(XLNumberFormatKey other) + { + return + NumberFormatId == other.NumberFormatId + && string.Equals(Format, other.Format); + } + + public override bool Equals(object obj) + { + if (obj is XLNumberFormatKey) + return Equals((XLNumberFormatKey)obj); + return base.Equals(obj); + } + + public override string ToString() + { + return $"{Format}/{NumberFormatId}"; + } + + public static bool operator ==(XLNumberFormatKey left, XLNumberFormatKey right) => left.Equals(right); + + public static bool operator !=(XLNumberFormatKey left, XLNumberFormatKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLNumberFormatValue.cs b/ClosedXML/Excel/Style/XLNumberFormatValue.cs new file mode 100644 index 0000000..360ae8c --- /dev/null +++ b/ClosedXML/Excel/Style/XLNumberFormatValue.cs @@ -0,0 +1,45 @@ +using ClosedXML.Excel.Caching; + +namespace ClosedXML.Excel +{ + internal sealed class XLNumberFormatValue + { + private static readonly XLNumberFormatRepository Repository = new XLNumberFormatRepository(key => new XLNumberFormatValue(key)); + + public static XLNumberFormatValue FromKey(ref XLNumberFormatKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLNumberFormatKey DefaultKey = new XLNumberFormatKey + { + NumberFormatId = 0, + Format = string.Empty + }; + + internal static readonly XLNumberFormatValue Default = FromKey(ref DefaultKey); + + public XLNumberFormatKey Key { get; private set; } + + public int NumberFormatId { get { return Key.NumberFormatId; } } + + public string Format { get { return Key.Format; } } + + private XLNumberFormatValue(XLNumberFormatKey key) + { + Key = key; + } + + public override bool Equals(object obj) + { + var cached = obj as XLNumberFormatValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return 1507230172 + Key.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Style/XLPredefinedFormat.cs b/ClosedXML/Excel/Style/XLPredefinedFormat.cs new file mode 100644 index 0000000..e256a30 --- /dev/null +++ b/ClosedXML/Excel/Style/XLPredefinedFormat.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + /// + /// Reference point of date/number formats available. + /// See more at: https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.numberingformat.aspx + /// + public static class XLPredefinedFormat + { + /// + /// General + /// + public static int General { get { return 0; } } + + public enum Number + { + /// + /// General + /// + General = 0, + + /// + /// 0 + /// + Integer = 1, + + /// + /// 0.00 + /// + Precision2 = 2, + + /// + /// #,##0 + /// + IntegerWithSeparator = 3, + + /// + /// #,##0.00 + /// + Precision2WithSeparator = 4, + + /// + /// 0% + /// + PercentInteger = 9, + + /// + /// 0.00% + /// + PercentPrecision2 = 10, + + /// + /// 0.00E+00 + /// + ScientificPrecision2 = 11, + + /// + /// # ?/? + /// + FractionPrecision1 = 12, + + /// + /// # ??/?? + /// + FractionPrecision2 = 13, + + /// + /// #,##0 ,(#,##0) + /// + IntegerWithSeparatorAndParens = 37, + + /// + /// #,##0 ,[Red](#,##0) + /// + IntegerWithSeparatorAndParensRed = 38, + + /// + /// #,##0.00,(#,##0.00) + /// + Precision2WithSeparatorAndParens = 39, + + /// + /// #,##0.00,[Red](#,##0.00) + /// + Precision2WithSeparatorAndParensRed = 40, + + /// + /// ##0.0E+0 + /// + ScientificUpToHundredsAndPrecision1 = 48, + + /// + /// @ + /// + Text = 49 + } + + public enum DateTime + { + /// + /// General + /// + General = 0, + + /// + /// d/m/yyyy + /// + DayMonthYear4WithSlashes = 14, + + /// + /// d-mmm-yy + /// + DayMonthAbbrYear2WithDashes = 15, + + /// + /// d-mmm + /// + DayMonthAbbrWithDash = 16, + + /// + /// mmm-yy + /// + MonthAbbrYear2WithDash = 17, + + /// + /// h:mm tt + /// + Hour12MinutesAmPm = 18, + + /// + /// h:mm:ss tt + /// + Hour12MinutesSecondsAmPm = 19, + + /// + /// H:mm + /// + Hour24Minutes = 20, + + /// + /// H:mm:ss + /// + Hour24MinutesSeconds = 21, + + /// + /// m/d/yyyy H:mm + /// + MonthDayYear4WithDashesHour24Minutes = 22, + + /// + /// mm:ss + /// + MinutesSeconds = 45, + + /// + /// [h]:mm:ss + /// + Hour12MinutesSeconds = 46, + + /// + /// mmss.0 + /// + MinutesSecondsMillis1 = 47, + + /// + /// @ + /// + Text = 49 + } + + private static IDictionary _formatCodes; + + internal static IDictionary FormatCodes + { + get + { + if (_formatCodes == null) + { + var fCodes = new Dictionary + { + {0, string.Empty}, + {1, "0"}, + {2, "0.00"}, + {3, "#,##0"}, + {4, "#,##0.00"}, + {7, "$#,##0.00_);($#,##0.00)"}, + {9, "0%"}, + {10, "0.00%"}, + {11, "0.00E+00"}, + {12, "# ?/?"}, + {13, "# ??/??"}, + {14, "M/d/yyyy"}, + {15, "d-MMM-yy"}, + {16, "d-MMM"}, + {17, "MMM-yy"}, + {18, "h:mm tt"}, + {19, "h:mm:ss tt"}, + {20, "H:mm"}, + {21, "H:mm:ss"}, + {22, "M/d/yyyy H:mm"}, + {37, "#,##0 ;(#,##0)"}, + {38, "#,##0 ;[Red](#,##0)"}, + {39, "#,##0.00;(#,##0.00)"}, + {40, "#,##0.00;[Red](#,##0.00)"}, + {45, "mm:ss"}, + {46, "[h]:mm:ss"}, + {47, "mmss.0"}, + {48, "##0.0E+0"}, + {49, "@"} + }; + _formatCodes = fCodes; + } + + return _formatCodes; + } + } + } +} diff --git a/ClosedXML/Excel/Style/XLProtection.cs b/ClosedXML/Excel/Style/XLProtection.cs new file mode 100644 index 0000000..81045c4 --- /dev/null +++ b/ClosedXML/Excel/Style/XLProtection.cs @@ -0,0 +1,153 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLProtection : IXLProtection + { + #region Static members + + internal static XLProtectionKey GenerateKey(IXLProtection defaultProtection) + { + if (defaultProtection == null) + return XLProtectionValue.Default.Key; + if (defaultProtection is XLProtection) + return (defaultProtection as XLProtection).Key; + + return new XLProtectionKey + { + Locked = defaultProtection.Locked, + Hidden = defaultProtection.Hidden + }; + } + + #endregion Static members + + #region Properties + + private readonly XLStyle _style; + + private XLProtectionValue _value; + + internal XLProtectionKey Key + { + get { return _value.Key; } + private set { _value = XLProtectionValue.FromKey(ref value); } + } + + #endregion Properties + + #region Constructors + + /// + /// Create an instance of XLProtection initializing it with the specified value. + /// + /// Style to attach the new instance to. + /// Style value to use. + public XLProtection(XLStyle style, XLProtectionValue value) + { + _style = style ?? XLStyle.CreateEmptyStyle(); + _value = value; + } + + public XLProtection(XLStyle style, XLProtectionKey key) : this(style, XLProtectionValue.FromKey(ref key)) + { + } + + public XLProtection(XLStyle style = null, IXLProtection d = null) : this(style, GenerateKey(d)) + { + } + + #endregion Constructors + + #region IXLProtection Members + + public Boolean Locked + { + get { return Key.Locked; } + set + { + Modify(k => { k.Locked = value; return k; }); + } + } + + public Boolean Hidden + { + get { return Key.Hidden; } + set + { + Modify(k => { k.Hidden = value; return k; }); + } + } + + public IXLStyle SetLocked() + { + Locked = true; + return _style; + } + + public IXLStyle SetLocked(Boolean value) + { + Locked = value; + return _style; + } + + public IXLStyle SetHidden() + { + Hidden = true; + return _style; + } + + public IXLStyle SetHidden(Boolean value) + { + Hidden = value; + return _style; + } + + #endregion IXLProtection Members + + private void Modify(Func modification) + { + Key = modification(Key); + + _style.Modify(styleKey => + { + var protection = styleKey.Protection; + styleKey.Protection = modification(protection); + return styleKey; + }); + } + + #region Overridden + + public override bool Equals(object obj) + { + return Equals((IXLProtection)obj); + } + + public bool Equals(IXLProtection other) + { + var otherP = other as XLProtection; + if (otherP == null) + return false; + + return Key == otherP.Key; + } + + public override string ToString() + { + if (Locked) + return Hidden ? "Locked-Hidden" : "Locked"; + + return Hidden ? "Hidden" : "None"; + } + + public override int GetHashCode() + { + var hashCode = 416600561; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLProtectionKey.cs b/ClosedXML/Excel/Style/XLProtectionKey.cs new file mode 100644 index 0000000..ba4f013 --- /dev/null +++ b/ClosedXML/Excel/Style/XLProtectionKey.cs @@ -0,0 +1,42 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLProtectionKey : IEquatable + { + public bool Locked { get; set; } + + public bool Hidden { get; set; } + + public override int GetHashCode() + { + var hashCode = -1357408252; + hashCode = hashCode * -1521134295 + Locked.GetHashCode(); + hashCode = hashCode * -1521134295 + Hidden.GetHashCode(); + return hashCode; + } + + public bool Equals(XLProtectionKey other) + { + return + Locked == other.Locked + && Hidden == other.Hidden; + } + + public override bool Equals(object obj) + { + if (obj is XLProtectionKey) + return Equals((XLProtectionKey)obj); + return base.Equals(obj); + } + + public override string ToString() + { + return (Locked ? "Locked" : "") + (Hidden ? "Hidden" : ""); + } + + public static bool operator ==(XLProtectionKey left, XLProtectionKey right) => left.Equals(right); + + public static bool operator !=(XLProtectionKey left, XLProtectionKey right) => !(left.Equals(right)); + } +} diff --git a/ClosedXML/Excel/Style/XLProtectionValue.cs b/ClosedXML/Excel/Style/XLProtectionValue.cs new file mode 100644 index 0000000..fba99e2 --- /dev/null +++ b/ClosedXML/Excel/Style/XLProtectionValue.cs @@ -0,0 +1,45 @@ +using ClosedXML.Excel.Caching; + +namespace ClosedXML.Excel +{ + internal sealed class XLProtectionValue + { + private static readonly XLProtectionRepository Repository = new XLProtectionRepository(key => new XLProtectionValue(key)); + + public static XLProtectionValue FromKey(ref XLProtectionKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLProtectionKey DefaultKey = new XLProtectionKey + { + Locked = true, + Hidden = false + }; + + internal static readonly XLProtectionValue Default = FromKey(ref DefaultKey); + + public XLProtectionKey Key { get; private set; } + + public bool Locked { get { return Key.Locked; } } + + public bool Hidden { get { return Key.Hidden; } } + + private XLProtectionValue(XLProtectionKey key) + { + Key = key; + } + + public override bool Equals(object obj) + { + var cached = obj as XLProtectionValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + return 909014992 + Key.GetHashCode(); + } + } +} diff --git a/ClosedXML/Excel/Style/XLStyle.cs b/ClosedXML/Excel/Style/XLStyle.cs new file mode 100644 index 0000000..a09864d --- /dev/null +++ b/ClosedXML/Excel/Style/XLStyle.cs @@ -0,0 +1,214 @@ +using System; +using System.Text; + +namespace ClosedXML.Excel +{ + internal class XLStyle : IXLStyle + { + #region Static members + + public static XLStyle Default { get { return new XLStyle(XLStyleValue.Default); } } + + internal static XLStyleKey GenerateKey(IXLStyle initialStyle) + { + if (initialStyle == null) + return Default.Key; + if (initialStyle is XLStyle) + return (initialStyle as XLStyle).Key; + + return new XLStyleKey + { + Font = XLFont.GenerateKey(initialStyle.Font), + Alignment = XLAlignment.GenerateKey(initialStyle.Alignment), + Border = XLBorder.GenerateKey(initialStyle.Border), + Fill = XLFill.GenerateKey(initialStyle.Fill), + NumberFormat = XLNumberFormat.GenerateKey(initialStyle.NumberFormat), + Protection = XLProtection.GenerateKey(initialStyle.Protection) + }; + } + + internal static XLStyle CreateEmptyStyle() + { + return new XLStyle(new XLStylizedEmpty(null)); + } + + #endregion Static members + + #region properties + + private readonly IXLStylized _container; + + internal XLStyleValue Value { get; private set; } + + internal XLStyleKey Key + { + get { return Value.Key; } + private set + { + Value = XLStyleValue.FromKey(ref value); + } + } + + #endregion properties + + #region constructors + + public XLStyle(IXLStylized container, IXLStyle initialStyle = null, Boolean useDefaultModify = true) : this(container, GenerateKey(initialStyle)) + { + } + + public XLStyle(IXLStylized container, XLStyleKey key) : this(container, XLStyleValue.FromKey(ref key)) + { + } + + internal XLStyle(IXLStylized container, XLStyleValue value) + { + _container = container ?? new XLStylizedEmpty(XLStyle.Default); + Value = value; + } + + /// + /// To initialize XLStyle.Default only + /// + private XLStyle(XLStyleValue value) + { + _container = null; + Value = value; + } + + #endregion constructors + + internal void Modify(Func modification) + { + Key = modification(Key); + + if (_container != null) + { + _container.ModifyStyle(modification); + } + } + + #region IXLStyle members + + public IXLFont Font + { + get { return new XLFont(this, Value.Font); } + set + { + Modify(k => { k.Font = XLFont.GenerateKey(value); return k; }); + } + } + + public IXLAlignment Alignment + { + get { return new XLAlignment(this, Value.Alignment); } + set + { + Modify(k => { k.Alignment = XLAlignment.GenerateKey(value); return k; }); + } + } + + public IXLBorder Border + { + get { return new XLBorder(_container, this, Value.Border); } + set + { + Modify(k => { k.Border = XLBorder.GenerateKey(value); return k; }); + } + } + + public IXLFill Fill + { + get { return new XLFill(this, Value.Fill); } + set + { + Modify(k => { k.Fill = XLFill.GenerateKey(value); return k; }); + } + } + + public Boolean IncludeQuotePrefix + { + get { return Value.IncludeQuotePrefix; } + set + { + Modify(k => { k.IncludeQuotePrefix = value; return k; }); + } + } + + public IXLStyle SetIncludeQuotePrefix(Boolean includeQuotePrefix = true) + { + IncludeQuotePrefix = includeQuotePrefix; + return this; + } + + public IXLNumberFormat NumberFormat + { + get { return new XLNumberFormat(this, Value.NumberFormat); } + set + { + Modify(k => { k.NumberFormat = XLNumberFormat.GenerateKey(value); return k; }); + } + } + + public IXLProtection Protection + { + get { return new XLProtection(this, Value.Protection); } + set + { + Modify(k => { k.Protection = XLProtection.GenerateKey(value); return k; }); + } + } + + public IXLNumberFormat DateFormat + { + get { return NumberFormat; } + } + + #endregion IXLStyle members + + #region Overridden + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("Font:"); + sb.Append(Font); + sb.Append(" Fill:"); + sb.Append(Fill); + sb.Append(" Border:"); + sb.Append(Border); + sb.Append(" NumberFormat: "); + sb.Append(NumberFormat); + sb.Append(" Alignment: "); + sb.Append(Alignment); + sb.Append(" Protection: "); + sb.Append(Protection); + return sb.ToString(); + } + + public bool Equals(IXLStyle other) + { + var otherS = other as XLStyle; + + if (otherS == null) + return false; + + return Key == otherS.Key && + _container == otherS._container; + } + + public override bool Equals(object obj) + { + return Equals(obj as XLStyle); + } + + public override int GetHashCode() + { + var hashCode = 416600561; + hashCode = hashCode * -1521134295 + Key.GetHashCode(); + return hashCode; + } + + #endregion Overridden + } +} diff --git a/ClosedXML/Excel/Style/XLStyleKey.cs b/ClosedXML/Excel/Style/XLStyleKey.cs new file mode 100644 index 0000000..93fc2b7 --- /dev/null +++ b/ClosedXML/Excel/Style/XLStyleKey.cs @@ -0,0 +1,88 @@ +using System; + +namespace ClosedXML.Excel +{ + internal struct XLStyleKey : IEquatable + { + public XLAlignmentKey Alignment { get; set; } + + public XLBorderKey Border { get; set; } + + public XLFillKey Fill { get; set; } + + public XLFontKey Font { get; set; } + + public Boolean IncludeQuotePrefix { get; set; } + + public XLNumberFormatKey NumberFormat { get; set; } + + public XLProtectionKey Protection { get; set; } + + public override int GetHashCode() + { + var hashCode = -476701294; + hashCode = hashCode * -1521134295 + Alignment.GetHashCode(); + hashCode = hashCode * -1521134295 + Border.GetHashCode(); + hashCode = hashCode * -1521134295 + Fill.GetHashCode(); + hashCode = hashCode * -1521134295 + Font.GetHashCode(); + hashCode = hashCode * -1521134295 + IncludeQuotePrefix.GetHashCode(); + hashCode = hashCode * -1521134295 + NumberFormat.GetHashCode(); + hashCode = hashCode * -1521134295 + Protection.GetHashCode(); + return hashCode; + } + + public bool Equals(XLStyleKey other) + { + return Alignment == other.Alignment && + Border == other.Border && + Fill == other.Fill && + Font == other.Font && + IncludeQuotePrefix == other.IncludeQuotePrefix && + NumberFormat == other.NumberFormat && + Protection == other.Protection; + } + + public override string ToString() + { + return + this == XLStyle.Default.Key ? "Default" : + string.Format("Alignment: {0} Border: {1} Fill: {2} Font: {3} IncludeQuotePrefix: {4} NumberFormat: {5} Protection: {6}", + Alignment == XLStyle.Default.Key.Alignment ? "Default" : Alignment.ToString(), + Border == XLStyle.Default.Key.Border ? "Default" : Border.ToString(), + Fill == XLStyle.Default.Key.Fill ? "Default" : Fill.ToString(), + Font == XLStyle.Default.Key.Font ? "Default" : Font.ToString(), + IncludeQuotePrefix == XLStyle.Default.Key.IncludeQuotePrefix ? "Default" : IncludeQuotePrefix.ToString(), + NumberFormat == XLStyle.Default.Key.NumberFormat ? "Default" : NumberFormat.ToString(), + Protection == XLStyle.Default.Key.Protection ? "Default" : Protection.ToString()); + } + + public override bool Equals(object obj) + { + if (obj is XLStyleKey) + return Equals((XLStyleKey)obj); + return base.Equals(obj); + } + + public static bool operator ==(XLStyleKey left, XLStyleKey right) => left.Equals(right); + + public static bool operator !=(XLStyleKey left, XLStyleKey right) => !(left.Equals(right)); + + public void Deconstruct( + out XLAlignmentKey alignment, + out XLBorderKey border, + out XLFillKey fill, + out XLFontKey font, + out Boolean includeQuotePrefix, + out XLNumberFormatKey numberFormat, + out XLProtectionKey protection) + { + alignment = Alignment; + border = Border; + fill = Fill; + font = Font; + includeQuotePrefix = IncludeQuotePrefix; + numberFormat = NumberFormat; + protection = Protection; + } + } +} diff --git a/ClosedXML/Excel/Style/XLStyleValue.cs b/ClosedXML/Excel/Style/XLStyleValue.cs new file mode 100644 index 0000000..a196519 --- /dev/null +++ b/ClosedXML/Excel/Style/XLStyleValue.cs @@ -0,0 +1,97 @@ +using ClosedXML.Excel.Caching; +using System; + +namespace ClosedXML.Excel +{ + internal class XLStyleValue + { + private static readonly XLStyleRepository Repository = new XLStyleRepository(key => new XLStyleValue(key)); + + public static XLStyleValue FromKey(ref XLStyleKey key) + { + return Repository.GetOrCreate(ref key); + } + + private static readonly XLStyleKey DefaultKey = new XLStyleKey + { + Alignment = XLAlignmentValue.Default.Key, + Border = XLBorderValue.Default.Key, + Fill = XLFillValue.Default.Key, + Font = XLFontValue.Default.Key, + IncludeQuotePrefix = false, + NumberFormat = XLNumberFormatValue.Default.Key, + Protection = XLProtectionValue.Default.Key + }; + + internal static readonly XLStyleValue Default = FromKey(ref DefaultKey); + + public XLStyleKey Key { get; private set; } + + public XLAlignmentValue Alignment { get; private set; } + + public XLBorderValue Border { get; private set; } + + public XLFillValue Fill { get; private set; } + + public XLFontValue Font { get; private set; } + + public Boolean IncludeQuotePrefix { get; private set; } + + public XLNumberFormatValue NumberFormat { get; private set; } + + public XLProtectionValue Protection { get; private set; } + + internal XLStyleValue(XLStyleKey key) + { + Key = key; + var (alignment, border, fill, font, _, numberFormat, protection) = Key; + Alignment = XLAlignmentValue.FromKey(ref alignment); + Border = XLBorderValue.FromKey(ref border); + Fill = XLFillValue.FromKey(ref fill); + Font = XLFontValue.FromKey(ref font); + IncludeQuotePrefix = key.IncludeQuotePrefix; + NumberFormat = XLNumberFormatValue.FromKey(ref numberFormat); + Protection = XLProtectionValue.FromKey(ref protection); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + return true; + + var cached = obj as XLStyleValue; + return cached != null && + Key.Equals(cached.Key); + } + + public override int GetHashCode() + { + if (_hashCode.HasValue) + return _hashCode.Value; + + _hashCode = -280332839 + Key.GetHashCode(); + return _hashCode.Value; + } + + public static bool operator ==(XLStyleValue left, XLStyleValue right) + { + if (ReferenceEquals(left, right)) + return true; + if (ReferenceEquals(left, null) && ReferenceEquals(right, null)) + return true; + if (ReferenceEquals(left, null) || ReferenceEquals(right, null)) + return false; + if (left._hashCode.HasValue && right._hashCode.HasValue && + left._hashCode != right._hashCode) + return false; + return left.Key.Equals(right.Key); + } + + public static bool operator !=(XLStyleValue left, XLStyleValue right) + { + return !(left == right); + } + + private int? _hashCode; + } +} diff --git a/ClosedXML/Excel/Style/XLStylizedBase.cs b/ClosedXML/Excel/Style/XLStylizedBase.cs new file mode 100644 index 0000000..325dd58 --- /dev/null +++ b/ClosedXML/Excel/Style/XLStylizedBase.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace ClosedXML.Excel +{ + /// + /// Base class for any workbook element that has or may have a style. + /// + internal abstract class XLStylizedBase : IXLStylized + { + #region Properties + + /// + /// Read-only style property. + /// + internal XLStyleValue StyleValue { get; private protected set; } + XLStyleValue IXLStylized.StyleValue + { + get { return StyleValue; } + } + + /// + /// Editable style of the workbook element. Modification of this property DOES affect styles of child objects as well - they will + /// be changed accordingly. Accessing this property causes a new instance generated so use this property + /// with caution. If you need only _read_ the style consider using property instead. + /// + public IXLStyle Style + { + get { return InnerStyle; } + set { SetStyle(value, true); } + } + + /// + /// Editable style of the workbook element. Modification of this property DOES NOT affect styles of child objects. + /// Accessing this property causes a new instance generated so use this property with caution. If you need + /// only _read_ the style consider using property instead. + /// + public IXLStyle InnerStyle + { + get { return new XLStyle(this, StyleValue.Key); } + set { SetStyle(value, false); } + } + + /// + /// Get a collection of stylized entities which current entity's style changes should be propagated to. + /// + protected abstract IEnumerable Children { get; } + + public abstract IXLRanges RangesUsed { get; } + + public abstract IEnumerable Styles { get; } + + #endregion Properties + + protected XLStylizedBase(XLStyleValue styleValue = null) + { + StyleValue = styleValue ?? XLWorkbook.DefaultStyleValue; + } + + #region Private methods + + private void SetStyle(IXLStyle style, bool propagate = false) + { + if (style is XLStyle xlStyle) + SetStyle(xlStyle.Value, propagate); + else + { + var styleKey = XLStyle.GenerateKey(style); + SetStyle(XLStyleValue.FromKey(ref styleKey), propagate); + } + } + + /// + /// Apply specified style to the container. + /// + /// Style to apply. + /// Whether or not propagate the style to inner ranges. + private void SetStyle(XLStyleValue value, bool propagate = false) + { + StyleValue = value; + if (propagate) + { + Children.ForEach(child => child.SetStyle(StyleValue, true)); + } + } + + private static ReferenceEqualityComparer _comparer = new ReferenceEqualityComparer(); + + void IXLStylized.ModifyStyle(Func modification) + { + var children = GetChildrenRecursively(this) + .GroupBy(child => child.StyleValue, _comparer); + + foreach (var group in children) + { + var styleKey = modification(group.Key.Key); + var styleValue = XLStyleValue.FromKey(ref styleKey); + foreach (var child in group) + { + child.StyleValue = styleValue; + } + } + } + + private IEnumerable GetChildrenRecursively(XLStylizedBase parent) + { + return new List { parent } + .Union(parent.Children.Where(child => child != parent).SelectMany(child => GetChildrenRecursively(child))); + } + + #endregion Private methods + + #region Nested classes + + public sealed class ReferenceEqualityComparer : IEqualityComparer where T : class + { + public bool Equals(T x, T y) => ReferenceEquals(x, y); + + public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + #endregion Nested classes + } +} diff --git a/ClosedXML/Excel/Style/XLStylizedEmpty.cs b/ClosedXML/Excel/Style/XLStylizedEmpty.cs new file mode 100644 index 0000000..9f45588 --- /dev/null +++ b/ClosedXML/Excel/Style/XLStylizedEmpty.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal class XLStylizedEmpty : XLStylizedBase, IXLStylized + { + public XLStylizedEmpty(IXLStyle defaultStyle) + : base((defaultStyle as XLStyle)?.Value) + { + } + + public override IEnumerable Styles + { + get + { + yield return Style; + } + } + + public override IXLRanges RangesUsed + { + get { return new XLRanges(); } + } + + protected override IEnumerable Children + { + get { yield break; } + } + } +} diff --git a/ClosedXML/Excel/Tables/IXLTable.cs b/ClosedXML/Excel/Tables/IXLTable.cs new file mode 100644 index 0000000..e6c2912 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTable.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; + +namespace ClosedXML.Excel +{ + public interface IXLTable : IXLRange + { + IXLAutoFilter AutoFilter { get; } + IXLTableRange DataRange { get; } + Boolean EmphasizeFirstColumn { get; set; } + Boolean EmphasizeLastColumn { get; set; } + IEnumerable Fields { get; } + string Name { get; set; } + Boolean ShowAutoFilter { get; set; } + Boolean ShowColumnStripes { get; set; } + Boolean ShowHeaderRow { get; set; } + Boolean ShowRowStripes { get; set; } + Boolean ShowTotalsRow { get; set; } + XLTableTheme Theme { get; set; } + + /// + /// Clears the contents of this table. + /// + /// Specify what you want to clear. + new IXLTable Clear(XLClearOptions clearOptions = XLClearOptions.All); + + IXLTableField Field(string fieldName); + + IXLTableField Field(int fieldIndex); + + IXLRangeRow HeadersRow(); + + /// + /// Appends the IEnumerable data elements and returns the range of the new rows. + /// + /// The IEnumerable data. + /// if set to true propagate extra columns' values and formulas. + /// + /// The range of the new rows. + /// + IXLRange AppendData(IEnumerable data, Boolean propagateExtraColumns = false); + + /// + /// Appends the IEnumerable data elements and returns the range of the new rows. + /// + /// The IEnumerable data. + /// if set to true the data will be transposed before inserting. + /// if set to true propagate extra columns' values and formulas. + /// + /// The range of the new rows. + /// + IXLRange AppendData(IEnumerable data, Boolean transpose, Boolean propagateExtraColumns = false); + + /// + /// Appends the data of a data table and returns the range of the new rows. + /// + /// The data table. + /// if set to true propagate extra columns' values and formulas. + /// + /// The range of the new rows. + /// + IXLRange AppendData(DataTable dataTable, Boolean propagateExtraColumns = false); + + /// + /// Appends the IEnumerable data elements and returns the range of the new rows. + /// + /// + /// The table data. + /// if set to true propagate extra columns' values and formulas. + /// + /// The range of the new rows. + /// + IXLRange AppendData(IEnumerable data, Boolean propagateExtraColumns = false); + + /// + /// Replaces the IEnumerable data elements and returns the table's data range. + /// + /// The IEnumerable data. + /// if set to true propagate extra columns' values and formulas. + /// + /// The table's data range. + /// + IXLRange ReplaceData(IEnumerable data, Boolean propagateExtraColumns = false); + + /// + /// Replaces the IEnumerable data elements and returns the table's data range. + /// + /// The IEnumerable data. + /// if set to true the data will be transposed before inserting. + /// if set to true propagate extra columns' values and formulas. + /// + /// The table's data range. + /// + IXLRange ReplaceData(IEnumerable data, Boolean transpose, Boolean propagateExtraColumns = false); + + /// + /// Replaces the data from the records of a data table and returns the table's data range. + /// + /// The data table. + /// if set to true propagate extra columns' values and formulas. + /// + /// The table's data range. + /// + IXLRange ReplaceData(DataTable dataTable, Boolean propagateExtraColumns = false); + + /// + /// Replaces the IEnumerable data elements as a table and the table's data range. + /// + /// + /// The table data. + /// if set to true propagate extra columns' values and formulas. + /// + /// The table's data range. + /// + IXLRange ReplaceData(IEnumerable data, Boolean propagateExtraColumns = false); + + /// + /// Resizes the table to the specified range address. + /// + /// The new table range. + /// + IXLTable Resize(IXLRange range); + + /// + /// Resizes the table to the specified range address. + /// + /// The range boundaries. + /// + IXLTable Resize(IXLRangeAddress rangeAddress); + + /// + /// Resizes the table to the specified range address. + /// + /// The range boundaries. + /// + IXLTable Resize(string rangeAddress); + + /// + /// Resizes the table to the specified range. + /// + /// The first cell in the range. + /// The last cell in the range. + /// + IXLTable Resize(IXLCell firstCell, IXLCell lastCell); + + /// + /// Resizes the table to the specified range. + /// + /// The first cell address in the worksheet. + /// The last cell address in the worksheet. + /// + IXLTable Resize(string firstCellAddress, string lastCellAddress); + + /// + /// Resizes the table to the specified range. + /// + /// The first cell address in the worksheet. + /// The last cell address in the worksheet. + /// + IXLTable Resize(IXLAddress firstCellAddress, IXLAddress lastCellAddress); + + /// + /// Resizes the table to the specified range. + /// + /// The first cell's row of the range to return. + /// The first cell's column of the range to return. + /// The last cell's row of the range to return. + /// The last cell's column of the range to return. + /// + IXLTable Resize(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn); + + new IXLAutoFilter SetAutoFilter(); + + IXLTable SetEmphasizeFirstColumn(); + + IXLTable SetEmphasizeFirstColumn(Boolean value); + + IXLTable SetEmphasizeLastColumn(); + + IXLTable SetEmphasizeLastColumn(Boolean value); + + IXLTable SetShowAutoFilter(); + + IXLTable SetShowAutoFilter(Boolean value); + + IXLTable SetShowColumnStripes(); + + IXLTable SetShowColumnStripes(Boolean value); + + IXLTable SetShowHeaderRow(); + + IXLTable SetShowHeaderRow(Boolean value); + + IXLTable SetShowRowStripes(); + + IXLTable SetShowRowStripes(Boolean value); + + IXLTable SetShowTotalsRow(); + + IXLTable SetShowTotalsRow(Boolean value); + + IXLRangeRow TotalsRow(); + + /// + /// Converts the table to an enumerable of dynamic objects + /// + /// + IEnumerable AsDynamicEnumerable(); + + /// + /// Converts the table to a standard .NET System.Data.DataTable + /// + /// + DataTable AsNativeDataTable(); + + IXLTable CopyTo(IXLWorksheet targetSheet); + } +} diff --git a/ClosedXML/Excel/Tables/IXLTableField.cs b/ClosedXML/Excel/Tables/IXLTableField.cs new file mode 100644 index 0000000..c1e9107 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTableField.cs @@ -0,0 +1,129 @@ +using System; + +namespace ClosedXML.Excel +{ + public enum XLTotalsRowFunction + { + None, + Sum, + Minimum, + Maximum, + Average, + Count, + CountNumbers, + StandardDeviation, + Variance, + Custom + } + + public interface IXLTableField + { + /// + /// Gets the corresponding column for this table field. + /// Includes the header and footer cells + /// + /// + /// The column. + /// + IXLRangeColumn Column { get; } + + /// + /// Gets the collection of data cells for this field + /// Excludes the header and footer cells + /// + /// + /// The data cells + /// + IXLCells DataCells { get; } + + /// + /// Gets the footer cell for the table field. + /// + /// + /// The footer cell. + /// + IXLCell TotalsCell { get; } + + /// + /// Gets the header cell for the table field. + /// + /// + /// The header cell. + /// + IXLCell HeaderCell { get; } + + /// + /// Gets the index of the column (0-based). + /// + /// + /// The index. + /// + Int32 Index { get; } + + /// + /// Gets or sets the name/header of this table field. + /// The corresponding header cell's value will change if you set this. + /// + /// + /// The name. + /// + String Name { get; set; } + + /// + /// Gets the underlying table for this table field. + /// + IXLTable Table { get; } + + /// + /// Gets or sets the totals row formula in A1 format. + /// + /// + /// The totals row formula a1. + /// + String TotalsRowFormulaA1 { get; set; } + + /// + /// Gets or sets the totals row formula in R1C1 format. + /// + /// + /// The totals row formula r1 c1. + /// + String TotalsRowFormulaR1C1 { get; set; } + + /// + /// Gets or sets the totals row function. + /// + /// + /// The totals row function. + /// + XLTotalsRowFunction TotalsRowFunction { get; set; } + + /// + /// Gets or sets the totals row label. + /// + /// + /// The totals row label. + /// + String TotalsRowLabel { get; set; } + + /// + /// Deletes this table field from the table. + /// + void Delete(); + + /// + /// Determines whether all cells this table field have a consistent data type. + /// + Boolean IsConsistentDataType(); + + /// + /// Determines whether all cells this table field have a consistent formula. + /// + Boolean IsConsistentFormula(); + + /// + /// Determines whether all cells this table field have a consistent style. + /// + Boolean IsConsistentStyle(); + } +} diff --git a/ClosedXML/Excel/Tables/IXLTableRange.cs b/ClosedXML/Excel/Tables/IXLTableRange.cs new file mode 100644 index 0000000..aeac6b8 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTableRange.cs @@ -0,0 +1,58 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + public interface IXLTableRange : IXLRange + { + IXLTable Table { get; } + + IXLTableRow FirstRow(Func predicate = null); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLTableRow FirstRowUsed(Boolean includeFormats, Func predicate = null); + + IXLTableRow FirstRowUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLTableRow FirstRowUsed(Func predicate = null); + + new IXLTableRows InsertRowsAbove(int numberOfRows); + + new IXLTableRows InsertRowsBelow(int numberOfRows); + + IXLTableRow LastRow(Func predicate = null); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLTableRow LastRowUsed(Boolean includeFormats, Func predicate = null); + + IXLTableRow LastRowUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLTableRow LastRowUsed(Func predicate = null); + + /// + /// Rows the specified row. + /// + /// 1-based row number relative to the first row of this range. + /// + new IXLTableRow Row(int row); + + IXLTableRows Rows(Func predicate = null); + + /// + /// Returns a subset of the rows + /// + /// The first row to return. 1-based row number relative to the first row of this range. + /// The last row to return. 1-based row number relative to the first row of this range. + /// + new IXLTableRows Rows(int firstRow, int lastRow); + + new IXLTableRows Rows(string rows); + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLTableRows RowsUsed(Boolean includeFormats, Func predicate = null); + + IXLTableRows RowsUsed(XLCellsUsedOptions options, Func predicate = null); + + IXLTableRows RowsUsed(Func predicate = null); + } +} diff --git a/ClosedXML/Excel/Tables/IXLTableRow.cs b/ClosedXML/Excel/Tables/IXLTableRow.cs new file mode 100644 index 0000000..5722624 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTableRow.cs @@ -0,0 +1,33 @@ +using System; + +namespace ClosedXML.Excel +{ + public interface IXLTableRow : IXLRangeRow + { + IXLCell Field(Int32 index); + + IXLCell Field(String name); + + new IXLTableRow Sort(); + + new IXLTableRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true); + + new IXLTableRow RowAbove(); + + new IXLTableRow RowAbove(Int32 step); + + new IXLTableRow RowBelow(); + + new IXLTableRow RowBelow(Int32 step); + + /// + /// Clears the contents of this row. + /// + /// Specify what you want to clear. + new IXLTableRow Clear(XLClearOptions clearOptions = XLClearOptions.All); + + new IXLTableRows InsertRowsAbove(int numberOfRows); + + new IXLTableRows InsertRowsBelow(int numberOfRows); + } +} diff --git a/ClosedXML/Excel/Tables/IXLTableRows.cs b/ClosedXML/Excel/Tables/IXLTableRows.cs new file mode 100644 index 0000000..9573684 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTableRows.cs @@ -0,0 +1,46 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLTableRows : IEnumerable + { + IXLStyle Style { get; set; } + + /// + /// Adds a table row to this group. + /// + /// The row table to add. + void Add(IXLTableRow tableRow); + + /// + /// Returns the collection of cells. + /// + IXLCells Cells(); + + /// + /// Returns the collection of cells that have a value. + /// + IXLCells CellsUsed(); + + /// + /// Returns the collection of cells that have a value. + /// + /// if set to true will return all cells with a value or a style different than the default. + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLCells CellsUsed(Boolean includeFormats); + + IXLCells CellsUsed(XLCellsUsedOptions options); + + /// + /// Clears the contents of these rows. + /// + /// Specify what you want to clear. + IXLTableRows Clear(XLClearOptions clearOptions = XLClearOptions.All); + + void Delete(); + + void Select(); + } +} diff --git a/ClosedXML/Excel/Tables/IXLTables.cs b/ClosedXML/Excel/Tables/IXLTables.cs new file mode 100644 index 0000000..a9ec662 --- /dev/null +++ b/ClosedXML/Excel/Tables/IXLTables.cs @@ -0,0 +1,29 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + public interface IXLTables : IEnumerable + { + void Add(IXLTable table); + + /// + /// Clears the contents of these tables. + /// + /// Specify what you want to clear. + IXLTables Clear(XLClearOptions clearOptions = XLClearOptions.All); + + Boolean Contains(String name); + + void Remove(Int32 index); + + void Remove(String name); + + IXLTable Table(Int32 index); + + IXLTable Table(String name); + + Boolean TryGetTable(String tableName, out IXLTable table); + } +} diff --git a/ClosedXML/Excel/Tables/XLTable.cs b/ClosedXML/Excel/Tables/XLTable.cs new file mode 100644 index 0000000..2741ab6 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTable.cs @@ -0,0 +1,1039 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Dynamic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel +{ + [DebuggerDisplay("{Name}")] + internal class XLTable : XLRange, IXLTable + { + #region Private fields + + private string _name; + internal bool _showTotalsRow; + internal HashSet _uniqueNames; + + #endregion Private fields + + #region Constructor + + /// + /// The direct contructor should only be used in . + /// + public XLTable(XLRangeParameters xlRangeParameters) + : base(xlRangeParameters) + { + InitializeValues(false); + } + + #endregion Constructor + + public override XLRangeType RangeType + { + get { return XLRangeType.Table; } + } + + private IXLRangeAddress _lastRangeAddress; + private Dictionary _fieldNames = null; + + public Dictionary FieldNames + { + get + { + if (_fieldNames != null && _lastRangeAddress != null && _lastRangeAddress.Equals(RangeAddress)) + return _fieldNames; + + if (_fieldNames == null) + { + _fieldNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + _lastRangeAddress = RangeAddress; + HeadersRow(); + } + else + { + HeadersRow(false); + } + + RescanFieldNames(); + + _lastRangeAddress = RangeAddress; + + return _fieldNames; + } + } + + private void RescanFieldNames() + { + if (ShowHeaderRow) + { + var detectedFieldNames = new Dictionary(); + var headersRow = HeadersRow(false); + Int32 cellPos = 0; + foreach (var cell in headersRow.Cells()) + { + var name = cell.GetString(); + if (_fieldNames.TryGetValue(name, out IXLTableField tableField) && tableField.Column.ColumnNumber() == cell.Address.ColumnNumber) + { + (tableField as XLTableField).Index = cellPos; + detectedFieldNames.Add(name, _fieldNames[name]); + cellPos++; + continue; + } + + // Be careful here. Fields names may actually be whitespace, but not empty + if (String.IsNullOrEmpty(name)) + { + name = GetUniqueName("Column", cellPos + 1, true); + cell.SetValue(name); + cell.DataType = XLDataType.Text; + } + if (_fieldNames.ContainsKey(name)) + throw new ArgumentException("The header row contains more than one field name '" + name + "'."); + + _fieldNames.Add(name, new XLTableField(this, name) { Index = cellPos++ }); + detectedFieldNames.Add(name, _fieldNames[name]); + } + + _fieldNames.Keys + .Where(key => !detectedFieldNames.ContainsKey(key)) + .ToArray() + .ForEach(key => _fieldNames.Remove(key)); + } + else + { + Int32 colCount = ColumnCount(); + for (Int32 i = 1; i <= colCount; i++) + { + if (_fieldNames.Values.All(f => f.Index != i - 1)) + { + var name = "Column" + i; + + _fieldNames.Add(name, new XLTableField(this, name) { Index = i - 1 }); + } + } + } + } + + internal void AddFields(IEnumerable fieldNames) + { + _fieldNames = new Dictionary(); + + Int32 cellPos = 0; + foreach (var name in fieldNames) + { + _fieldNames.Add(name, new XLTableField(this, name) { Index = cellPos++ }); + } + } + + internal void RenameField(String oldName, String newName) + { + if (!_fieldNames.TryGetValue(oldName, out IXLTableField field)) + throw new ArgumentException("The field does not exist in this table", "oldName"); + + _fieldNames.Remove(oldName); + _fieldNames.Add(newName, field); + } + + internal String RelId { get; set; } + + public IXLTableRange DataRange + { + get + { + XLRange range; + + var firstDataRowNumber = 1; + var lastDataRowNumber = RowCount(); + + if (_showHeaderRow) + firstDataRowNumber++; + + if (_showTotalsRow) + lastDataRowNumber--; + + if (firstDataRowNumber > lastDataRowNumber) + return null; + + range = Range(firstDataRowNumber, 1, lastDataRowNumber, ColumnCount()); + + return new XLTableRange(range, this); + } + } + + private XLAutoFilter _autoFilter; + + public XLAutoFilter AutoFilter + { + get + { + if (_autoFilter == null) + _autoFilter = new XLAutoFilter(); + + _autoFilter.Range = ShowTotalsRow ? Range(1, 1, RowCount() - 1, ColumnCount()) : AsRange(); + return _autoFilter; + } + } + + public override IXLAutoFilter SetAutoFilter() + { + return AutoFilter; + } + + protected override void OnRangeAddressChanged(XLRangeAddress oldAddress, XLRangeAddress newAddress) + { + //Do nothing for table + } + + #region IXLTable Members + + public Boolean EmphasizeFirstColumn { get; set; } + public Boolean EmphasizeLastColumn { get; set; } + public Boolean ShowRowStripes { get; set; } + public Boolean ShowColumnStripes { get; set; } + + private Boolean _showAutoFilter; + + public Boolean ShowAutoFilter + { + get { return _showHeaderRow && _showAutoFilter; } + set { _showAutoFilter = value; } + } + + public XLTableTheme Theme { get; set; } + + public String Name + { + get { return _name; } + set + { + if (_name == value) return; + + // Validation rules for table names + var oldname = _name ?? string.Empty; + + if (!XLHelper.ValidateName("table", value, oldname, Worksheet.Tables.Select(t => t.Name), out String message)) + throw new ArgumentException(message, nameof(value)); + + _name = value; + + // Some totals row formula depend on the table name. Update them. + if (_fieldNames?.Any() ?? false) + this.Fields.ForEach(f => (f as XLTableField).UpdateTableFieldTotalsRowFormula()); + + if (!String.IsNullOrWhiteSpace(oldname) && !String.Equals(oldname, _name, StringComparison.OrdinalIgnoreCase)) + { + Worksheet.Tables.Add(this); + if (Worksheet.Tables.Contains(oldname)) + Worksheet.Tables.Remove(oldname); + } + } + } + + public Boolean ShowTotalsRow + { + get { return _showTotalsRow; } + set + { + if (value && !_showTotalsRow) + InsertRowsBelow(1); + else if (!value && _showTotalsRow) + TotalsRow().Delete(); + + _showTotalsRow = value; + + // Invalidate fields' columns + this.Fields.Cast().ForEach(f => f.Column = null); + + if (_showTotalsRow) + { + AutoFilter.Range = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber, RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber - 1, RangeAddress.LastAddress.ColumnNumber); + } + else + AutoFilter.Range = Worksheet.Range(RangeAddress); + } + } + + public IXLRangeRow HeadersRow() + { + return HeadersRow(true); + } + + internal IXLRangeRow HeadersRow(Boolean scanForNewFieldsNames) + { + if (!ShowHeaderRow) return null; + + if (scanForNewFieldsNames) + { + var tempResult = FieldNames; + } + + return FirstRow(); + } + + public IXLRangeRow TotalsRow() + { + return ShowTotalsRow ? LastRow() : null; + } + + public IXLTableField Field(String fieldName) + { + return Field(GetFieldIndex(fieldName)); + } + + public IXLTableField Field(Int32 fieldIndex) + { + return FieldNames.Values.First(f => f.Index == fieldIndex); + } + + public IEnumerable Fields + { + get + { + Int32 columnCount = ColumnCount(); + for (int co = 0; co < columnCount; co++) + yield return Field(co); + } + } + + public IXLTable Resize(IXLRangeAddress rangeAddress) + { + return Resize(Worksheet.Range(RangeAddress)); + } + + public IXLTable Resize(string rangeAddress) + { + return Resize(Worksheet.Range(RangeAddress)); + } + + public IXLTable Resize(IXLCell firstCell, IXLCell lastCell) + { + return Resize(Worksheet.Range(firstCell, lastCell)); + } + + public IXLTable Resize(string firstCellAddress, string lastCellAddress) + { + return Resize(Worksheet.Range(firstCellAddress, lastCellAddress)); + } + + public IXLTable Resize(IXLAddress firstCellAddress, IXLAddress lastCellAddress) + { + return Resize(Worksheet.Range(firstCellAddress, lastCellAddress)); + } + + public IXLTable Resize(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn) + { + return Resize(Worksheet.Range(firstCellRow, firstCellColumn, lastCellRow, lastCellColumn)); + } + + public IXLTable Resize(IXLRange range) + { + if (!this.ShowHeaderRow) + throw new NotImplementedException("Resizing of tables with no headers not supported yet."); + + if (this.Worksheet != range.Worksheet) + throw new InvalidOperationException("You cannot resize a table to a range on a different sheet."); + + var totalsRowChanged = this.ShowTotalsRow ? range.LastRow().RowNumber() - this.TotalsRow().RowNumber() : 0; + var oldTotalsRowNumber = this.ShowTotalsRow ? this.TotalsRow().RowNumber() : -1; + + var existingHeaders = this.FieldNames.Keys; + var newHeaders = new HashSet(); + + // Force evaluation of f.Column field + var tempArray = this.Fields.Select(f => f.Column).ToArray(); + + var firstRow = range.Row(1); + if (!firstRow.FirstCell().Address.Equals(this.HeadersRow().FirstCell().Address) + || !firstRow.LastCell().Address.Equals(this.HeadersRow().LastCell().Address)) + { + _uniqueNames.Clear(); + var co = 1; + foreach (var c in firstRow.Cells()) + { + if (String.IsNullOrWhiteSpace(((XLCell)c).InnerText)) + c.Value = GetUniqueName("Column", co, true); + + var header = c.GetString(); + _uniqueNames.Add(header); + + if (!existingHeaders.Contains(header)) + newHeaders.Add(header); + + co++; + } + } + + if (totalsRowChanged < 0) + { + range.Rows(r => r.RowNumber().Equals(this.TotalsRow().RowNumber() + totalsRowChanged)).Single().InsertRowsAbove(1); + range = Worksheet.Range(range.FirstCell(), range.LastCell().CellAbove()); + oldTotalsRowNumber++; + } + else if (totalsRowChanged > 0) + { + this.TotalsRow().RowBelow(totalsRowChanged + 1).InsertRowsAbove(1); + this.TotalsRow().AsRange().Delete(XLShiftDeletedCells.ShiftCellsUp); + } + + this.RangeAddress = (XLRangeAddress)range.RangeAddress; + RescanFieldNames(); + + if (this.ShowTotalsRow) + { + foreach (var f in this._fieldNames.Values) + { + var c = this.TotalsRow().Cell(f.Index + 1); + if (!c.IsEmpty() && newHeaders.Contains(f.Name)) + { + f.TotalsRowLabel = c.GetFormattedString(); + c.DataType = XLDataType.Text; + } + } + + if (totalsRowChanged != 0) + { + foreach (var f in this._fieldNames.Values.Cast()) + { + f.UpdateTableFieldTotalsRowFormula(); + var c = this.TotalsRow().Cell(f.Index + 1); + if (!String.IsNullOrWhiteSpace(f.TotalsRowLabel)) + { + c.DataType = XLDataType.Text; + + //Remove previous row's label + var oldTotalsCell = this.Worksheet.Cell(oldTotalsRowNumber, f.Column.ColumnNumber()); + if (oldTotalsCell.Value.ToString() == f.TotalsRowLabel) + oldTotalsCell.Value = null; + } + + if (f.TotalsRowFunction != XLTotalsRowFunction.None) + c.DataType = XLDataType.Number; + } + } + } + + return this; + } + + public IXLTable SetEmphasizeFirstColumn() + { + EmphasizeFirstColumn = true; + return this; + } + + public IXLTable SetEmphasizeFirstColumn(Boolean value) + { + EmphasizeFirstColumn = value; + return this; + } + + public IXLTable SetEmphasizeLastColumn() + { + EmphasizeLastColumn = true; + return this; + } + + public IXLTable SetEmphasizeLastColumn(Boolean value) + { + EmphasizeLastColumn = value; + return this; + } + + public IXLTable SetShowRowStripes() + { + ShowRowStripes = true; + return this; + } + + public IXLTable SetShowRowStripes(Boolean value) + { + ShowRowStripes = value; + return this; + } + + public IXLTable SetShowColumnStripes() + { + ShowColumnStripes = true; + return this; + } + + public IXLTable SetShowColumnStripes(Boolean value) + { + ShowColumnStripes = value; + return this; + } + + public IXLTable SetShowTotalsRow() + { + ShowTotalsRow = true; + return this; + } + + public IXLTable SetShowTotalsRow(Boolean value) + { + ShowTotalsRow = value; + return this; + } + + public IXLTable SetShowAutoFilter() + { + ShowAutoFilter = true; + return this; + } + + public IXLTable SetShowAutoFilter(Boolean value) + { + ShowAutoFilter = value; + return this; + } + + public new IXLRange Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, + Boolean matchCase = false, Boolean ignoreBlanks = true) + { + var toSortBy = new StringBuilder(); + foreach (string coPairTrimmed in columnsToSortBy.Split(',').Select(coPair => coPair.Trim())) + { + String coString; + String order; + if (coPairTrimmed.Contains(' ')) + { + var pair = coPairTrimmed.Split(' '); + coString = pair[0]; + order = pair[1]; + } + else + { + coString = coPairTrimmed; + order = sortOrder == XLSortOrder.Ascending ? "ASC" : "DESC"; + } + + if (!Int32.TryParse(coString, out Int32 co)) + co = Field(coString).Index + 1; + + if (toSortBy.Length > 0) + toSortBy.Append(','); + + toSortBy.Append(co); + toSortBy.Append(' '); + toSortBy.Append(order); + } + return DataRange.Sort(toSortBy.ToString(), sortOrder, matchCase, ignoreBlanks); + } + + public new IXLTable Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + IXLAutoFilter IXLTable.AutoFilter + { + get { return AutoFilter; } + } + + #endregion IXLTable Members + + private void InitializeValues(Boolean setAutofilter) + { + ShowRowStripes = true; + _showHeaderRow = true; + Theme = XLTableTheme.TableStyleMedium2; + if (setAutofilter) + InitializeAutoFilter(); + + AsRange().Row(1).DataType = XLDataType.Text; + + if (RowCount() == 1) + InsertRowsBelow(1); + } + + public void InitializeAutoFilter() + { + ShowAutoFilter = true; + } + + internal void OnAddedToTables() + { + _uniqueNames = new HashSet(); + Int32 co = 1; + foreach (IXLCell c in Row(1).Cells()) + { + // Be careful here. Fields names may actually be whitespace, but not empty + if (String.IsNullOrEmpty(((XLCell)c).InnerText)) + c.Value = GetUniqueName("Column", co, true); + _uniqueNames.Add(c.GetString()); + co++; + } + } + + private String GetUniqueName(String originalName, Int32 initialOffset, Boolean enforceOffset) + { + String name = String.Concat(originalName, enforceOffset ? initialOffset.ToInvariantString() : string.Empty); + if (_uniqueNames?.Contains(name) ?? false) + { + Int32 i = initialOffset; + name = originalName + i.ToInvariantString(); + while (_uniqueNames.Contains(name)) + { + i++; + name = originalName + i.ToInvariantString(); + } + } + + return name; + } + + public Int32 GetFieldIndex(String name) + { + // There is a discrepancy in the way headers with line breaks are stored. + // The entry in the table definition will contain \r\n + // but the shared string value of the actual cell will contain only \n + name = name.Replace("\r\n", "\n"); + if (FieldNames.TryGetValue(name, out IXLTableField tableField)) + return tableField.Index; + + throw new ArgumentOutOfRangeException("The header row doesn't contain field name '" + name + "'."); + } + + internal Boolean _showHeaderRow; + + public Boolean ShowHeaderRow + { + get { return _showHeaderRow; } + set + { + if (_showHeaderRow == value) return; + + if (_showHeaderRow) + { + var headersRow = HeadersRow(); + _uniqueNames = new HashSet(); + Int32 co = 1; + foreach (IXLCell c in headersRow.Cells()) + { + if (String.IsNullOrWhiteSpace(((XLCell)c).InnerText)) + c.Value = GetUniqueName("Column", co, true); + _uniqueNames.Add(c.GetString()); + co++; + } + + headersRow.Clear(); + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, RangeAddress.FirstAddress.RowNumber + 1, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + RangeAddress.LastAddress); + } + else + { + var asRange = Worksheet.Range( + RangeAddress.FirstAddress.RowNumber - 1, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.LastAddress.RowNumber, + RangeAddress.LastAddress.ColumnNumber); + var firstRow = asRange.FirstRow(); + IXLRangeRow rangeRow; + if (firstRow.IsEmpty(XLCellsUsedOptions.All)) + { + rangeRow = firstRow; + RangeAddress = new XLRangeAddress( + new XLAddress(Worksheet, + RangeAddress.FirstAddress.RowNumber - 1, + RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, + RangeAddress.FirstAddress.FixedColumn), + RangeAddress.LastAddress); + } + else + { + var fAddress = RangeAddress.FirstAddress; + //var lAddress = RangeAddress.LastAddress; + + rangeRow = firstRow.InsertRowsBelow(1, false).First(); + + RangeAddress = new XLRangeAddress( + fAddress, + RangeAddress.LastAddress); + } + + Int32 co = 1; + foreach (var name in FieldNames.Values.Select(f => f.Name)) + { + rangeRow.Cell(co).SetValue(name); + co++; + } + } + + _showHeaderRow = value; + + // Invalidate fields' columns + this.Fields.Cast().ForEach(f => f.Column = null); + + if (_showHeaderRow) + HeadersRow().DataType = XLDataType.Text; + } + } + + public IXLTable SetShowHeaderRow() + { + return SetShowHeaderRow(true); + } + + public IXLTable SetShowHeaderRow(Boolean value) + { + ShowHeaderRow = value; + return this; + } + + public void ExpandTableRows(Int32 rows) + { + RangeAddress = new XLRangeAddress( + RangeAddress.FirstAddress, + new XLAddress(Worksheet, RangeAddress.LastAddress.RowNumber + rows, + RangeAddress.LastAddress.ColumnNumber, + RangeAddress.LastAddress.FixedRow, + RangeAddress.LastAddress.FixedColumn)); + } + + public override XLRangeColumn Column(int columnNumber) + { + var column = base.Column(columnNumber); + column.Table = this; + return column; + } + + public override XLRangeColumn Column(string columnName) + { + var column = base.Column(columnName); + column.Table = this; + return column; + } + + public override IXLRangeColumns Columns(int firstColumn, int lastColumn) + { + var columns = base.Columns(firstColumn, lastColumn); + columns.Cast().ForEach(column => column.Table = this); + return columns; + } + + public override IXLRangeColumns Columns(Func predicate = null) + { + var columns = base.Columns(predicate); + columns.Cast().ForEach(column => column.Table = this); + return columns; + } + + public override IXLRangeColumns Columns(string columns) + { + var cols = base.Columns(columns); + cols.Cast().ForEach(column => column.Table = this); + return cols; + } + + public override IXLRangeColumns Columns(string firstColumn, string lastColumn) + { + var columns = base.Columns(firstColumn, lastColumn); + columns.Cast().ForEach(column => column.Table = this); + return columns; + } + + internal override XLRangeColumns ColumnsUsed(XLCellsUsedOptions options, Func predicate = null) + { + var columns = base.ColumnsUsed(options, predicate); + columns.Cast().ForEach(column => column.Table = this); + return columns; + } + + internal override XLRangeColumns ColumnsUsed(Func predicate = null) + { + var columns = base.ColumnsUsed(predicate); + columns.Cast().ForEach(column => column.Table = this); + return columns; + } + + IXLPivotTable IXLRangeBase.CreatePivotTable(IXLCell targetCell, String name) + { + return CreatePivotTable(targetCell, name); + } + + internal new XLPivotTable CreatePivotTable(IXLCell targetCell, String name) + { + return (XLPivotTable)targetCell.Worksheet.PivotTables.Add(name, targetCell, this); + } + + public IEnumerable AsDynamicEnumerable() + { + foreach (var row in this.DataRange.Rows()) + { + dynamic expando = new ExpandoObject(); + foreach (var f in this.Fields) + { + var value = row.Cell(f.Index + 1).Value; + // ExpandoObject supports IDictionary so we can extend it like this + var expandoDict = expando as IDictionary; + expandoDict[f.Name] = value; + } + + yield return expando; + } + } + + public DataTable AsNativeDataTable() + { + var table = new DataTable(this.Name); + + foreach (var f in Fields.Cast()) + { + Type type = typeof(object); + if (f.IsConsistentDataType()) + { + var c = f.Column.Cells().Skip(this.ShowHeaderRow ? 1 : 0).First(); + switch (c.DataType) + { + case XLDataType.Text: + type = typeof(String); + break; + + case XLDataType.Boolean: + type = typeof(Boolean); + break; + + case XLDataType.DateTime: + type = typeof(DateTime); + break; + + case XLDataType.TimeSpan: + type = typeof(TimeSpan); + break; + + case XLDataType.Number: + type = typeof(Double); + break; + } + } + + table.Columns.Add(f.Name, type); + } + + foreach (var row in this.DataRange.Rows()) + { + var dr = table.NewRow(); + + foreach (var f in this.Fields) + { + dr[f.Name] = row.Cell(f.Index + 1).Value; + } + + table.Rows.Add(dr); + } + + return table; + } + + public IXLTable CopyTo(IXLWorksheet targetSheet) + { + return CopyTo((XLWorksheet)targetSheet); + } + + internal IXLTable CopyTo(XLWorksheet targetSheet, bool copyData = true) + { + if (targetSheet == Worksheet) + throw new InvalidOperationException("Cannot copy table to the worksheet it already belongs to."); + + var targetRange = targetSheet.Range(RangeAddress.WithoutWorksheet()); + if (copyData) + RangeUsed().CopyTo(targetRange); + else + HeadersRow().CopyTo(targetRange.FirstRow()); + + String tableName = Name; + var newTable = (XLTable)targetSheet.Table(targetRange, tableName, true); + + newTable.RelId = null; + newTable.EmphasizeFirstColumn = EmphasizeFirstColumn; + newTable.EmphasizeLastColumn = EmphasizeLastColumn; + newTable.ShowRowStripes = ShowRowStripes; + newTable.ShowColumnStripes = ShowColumnStripes; + newTable.ShowAutoFilter = ShowAutoFilter; + newTable.Theme = Theme; + newTable._showTotalsRow = ShowTotalsRow; + + Int32 fieldCount = ColumnCount(); + for (Int32 f = 0; f < fieldCount; f++) + { + var tableField = newTable.Field(f) as XLTableField; + var tField = Field(f) as XLTableField; + tableField.Index = tField.Index; + tableField.Name = tField.Name; + tableField.totalsRowLabel = tField.totalsRowLabel; + tableField.totalsRowFunction = tField.totalsRowFunction; + } + return newTable; + } + + #region Append and replace data + + public IXLRange AppendData(IEnumerable data, Boolean propagateExtraColumns = false) + { + return AppendData(data, transpose: false, propagateExtraColumns: propagateExtraColumns); + } + + public IXLRange AppendData(IEnumerable data, bool transpose, Boolean propagateExtraColumns = false) + { + var castedData = data?.Cast(); + if (!(castedData?.Any() ?? false) || data is String) + return null; + + var numberOfNewRows = castedData.Count(); + + var lastRowOfOldRange = this.DataRange.LastRow(); + lastRowOfOldRange.InsertRowsBelow(numberOfNewRows); + this.Fields.Cast().ForEach(f => f.Column = null); + + var insertedRange = lastRowOfOldRange.RowBelow().FirstCell().InsertData(castedData, transpose); + + PropagateExtraColumns(insertedRange.ColumnCount(), lastRowOfOldRange.RowNumber()); + + return insertedRange; + } + + public IXLRange AppendData(DataTable dataTable, Boolean propagateExtraColumns = false) + { + return AppendData(dataTable.Rows.Cast(), propagateExtraColumns: propagateExtraColumns); + } + + public IXLRange AppendData(IEnumerable data, Boolean propagateExtraColumns = false) + { + if (!(data?.Any() ?? false) || data is String) + return null; + + var numberOfNewRows = data.Count(); + + if (numberOfNewRows == 0) + return null; + + var lastRowOfOldRange = this.DataRange.LastRow(); + lastRowOfOldRange.InsertRowsBelow(numberOfNewRows); + this.Fields.Cast().ForEach(f => f.Column = null); + + var insertedRange = lastRowOfOldRange.RowBelow().FirstCell().InsertData(data); + + PropagateExtraColumns(insertedRange.ColumnCount(), lastRowOfOldRange.RowNumber()); + + return insertedRange; + } + + public IXLRange ReplaceData(IEnumerable data, Boolean propagateExtraColumns = false) + { + return ReplaceData(data, transpose: false, propagateExtraColumns: propagateExtraColumns); + } + + public IXLRange ReplaceData(IEnumerable data, bool transpose, Boolean propagateExtraColumns = false) + { + var castedData = data?.Cast(); + if (!(castedData?.Any() ?? false) || data is String) + throw new InvalidOperationException("Cannot replace table data with empty enumerable."); + + var firstDataRowNumber = this.DataRange.FirstRow().RowNumber(); + var lastDataRowNumber = this.DataRange.LastRow().RowNumber(); + + // Resize table + var sizeDifference = castedData.Count() - this.DataRange.RowCount(); + if (sizeDifference > 0) + this.DataRange.LastRow().InsertRowsBelow(sizeDifference); + else if (sizeDifference < 0) + { + this.DataRange.Rows + ( + lastDataRowNumber + sizeDifference + 1 - firstDataRowNumber + 1, + lastDataRowNumber - firstDataRowNumber + 1 + ) + .Delete(); + + // No propagation needed when reducing the number of rows + propagateExtraColumns = false; + } + + if (sizeDifference != 0) + // Invalidate table fields' columns + this.Fields.Cast().ForEach(f => f.Column = null); + + var replacedRange = this.DataRange.FirstCell().InsertData(castedData, transpose); + + if (propagateExtraColumns) + PropagateExtraColumns(replacedRange.ColumnCount(), lastDataRowNumber); + + return replacedRange; + } + + public IXLRange ReplaceData(DataTable dataTable, Boolean propagateExtraColumns = false) + { + return ReplaceData(dataTable.Rows.Cast(), propagateExtraColumns: propagateExtraColumns); + } + + public IXLRange ReplaceData(IEnumerable data, Boolean propagateExtraColumns = false) + { + if (!(data?.Any() ?? false) || data is String) + throw new InvalidOperationException("Cannot replace table data with empty enumerable."); + + var firstDataRowNumber = this.DataRange.FirstRow().RowNumber(); + var lastDataRowNumber = this.DataRange.LastRow().RowNumber(); + + // Resize table + var sizeDifference = data.Count() - this.DataRange.RowCount(); + if (sizeDifference > 0) + this.DataRange.LastRow().InsertRowsBelow(sizeDifference); + else if (sizeDifference < 0) + { + this.DataRange.Rows + ( + lastDataRowNumber + sizeDifference + 1 - firstDataRowNumber + 1, + lastDataRowNumber - firstDataRowNumber + 1 + ) + .Delete(); + + // No propagation needed when reducing the number of rows + propagateExtraColumns = false; + } + + if (sizeDifference != 0) + // Invalidate table fields' columns + this.Fields.Cast().ForEach(f => f.Column = null); + + var replacedRange = this.DataRange.FirstCell().InsertData(data); + + if (propagateExtraColumns) + PropagateExtraColumns(replacedRange.ColumnCount(), lastDataRowNumber); + + return replacedRange; + } + + private void PropagateExtraColumns(int numberOfNonExtraColumns, int previousLastDataRow) + { + for (var i = numberOfNonExtraColumns; i < this.Fields.Count(); i++) + { + var field = this.Field(i); + + var cell = this.Worksheet.Cell(previousLastDataRow, field.Column.ColumnNumber()); + field.Column.Cells(c => c.Address.RowNumber > previousLastDataRow) + .ForEach(c => + { + if (cell.HasFormula) + c.FormulaR1C1 = cell.FormulaR1C1; + else + c.Value = cell.Value; + }); + } + } + + #endregion Append and replace data + } +} diff --git a/ClosedXML/Excel/Tables/XLTableField.cs b/ClosedXML/Excel/Tables/XLTableField.cs new file mode 100644 index 0000000..c074518 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTableField.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ClosedXML.Excel +{ + [DebuggerDisplay("{Name}")] + internal class XLTableField : IXLTableField + { + internal XLTotalsRowFunction totalsRowFunction; + internal String totalsRowLabel; + private readonly XLTable table; + + private IXLRangeColumn _column; + private Int32 index; + private String name; + + public XLTableField(XLTable table, String name) + { + this.table = table; + this.name = name; + } + + public IXLRangeColumn Column + { + get + { + if (_column == null) + { + _column = table.AsRange().Column(this.Index + 1); + } + return _column; + } + internal set + { + _column = value; + } + } + + public IXLCells DataCells + { + get + { + return Column.Cells(c => + { + if (table.ShowHeaderRow && c == HeaderCell) + return false; + if (table.ShowTotalsRow && c == TotalsCell) + return false; + return true; + }); + } + } + + public IXLCell HeaderCell + { + get + { + if (!table.ShowHeaderRow) + return null; + + return Column.FirstCell(); + } + } + + public Int32 Index + { + get { return index; } + internal set + { + if (index == value) return; + index = value; + _column = null; + } + } + + public String Name + { + get + { + return name; + } + set + { + if (name == value) return; + + if (table.ShowHeaderRow) + (table.HeadersRow(false).Cell(Index + 1) as XLCell).SetValue(value, false); + + table.RenameField(name, value); + name = value; + } + } + + public IXLTable Table { get { return table; } } + + public IXLCell TotalsCell + { + get + { + if (!table.ShowTotalsRow) + return null; + + return Column.LastCell(); + } + } + + public String TotalsRowFormulaA1 + { + get { return table.TotalsRow().Cell(Index + 1).FormulaA1; } + set + { + totalsRowFunction = XLTotalsRowFunction.Custom; + table.TotalsRow().Cell(Index + 1).FormulaA1 = value; + } + } + + public String TotalsRowFormulaR1C1 + { + get { return table.TotalsRow().Cell(Index + 1).FormulaR1C1; } + set + { + totalsRowFunction = XLTotalsRowFunction.Custom; + table.TotalsRow().Cell(Index + 1).FormulaR1C1 = value; + } + } + + public XLTotalsRowFunction TotalsRowFunction + { + get { return totalsRowFunction; } + set + { + totalsRowFunction = value; + UpdateTableFieldTotalsRowFormula(); + } + } + + public String TotalsRowLabel + { + get { return totalsRowLabel; } + set + { + totalsRowFunction = XLTotalsRowFunction.None; + (table.TotalsRow().Cell(Index + 1) as XLCell).SetValue(value, false); + totalsRowLabel = value; + } + } + + public void Delete() + { + Delete(true); + } + + internal void Delete(Boolean deleteUnderlyingRangeColumn) + { + var fields = table.Fields.Cast().ToArray(); + + if (deleteUnderlyingRangeColumn) + { + table.AsRange().ColumnQuick(this.Index + 1).Delete(); + } + + fields.Where(f => f.Index > this.Index).ForEach(f => f.Index--); + table.FieldNames.Remove(this.Name); + } + + public bool IsConsistentDataType() + { + var dataTypes = this.Column + .Cells() + .Skip(this.table.ShowHeaderRow ? 1 : 0) + .Select(c => c.DataType); + + if (this.table.ShowTotalsRow) + dataTypes = dataTypes.Take(dataTypes.Count() - 1); + + var distinctDataTypes = dataTypes + .GroupBy(dt => dt) + .Select(g => new { Key = g.Key, Count = g.Count() }); + + return distinctDataTypes.Count() == 1; + } + + public Boolean IsConsistentFormula() + { + var formulas = this.Column + .Cells() + .Skip(this.table.ShowHeaderRow ? 1 : 0) + .Select(c => c.FormulaR1C1); + + if (this.table.ShowTotalsRow) + formulas = formulas.Take(formulas.Count() - 1); + + var distinctFormulas = formulas + .GroupBy(f => f) + .Select(g => new { Key = g.Key, Count = g.Count() }); + + return distinctFormulas.Count() == 1; + } + + public bool IsConsistentStyle() + { + var styles = this.Column + .Cells() + .Skip(this.table.ShowHeaderRow ? 1 : 0) + .OfType() + .Select(c => c.StyleValue); + + if (this.table.ShowTotalsRow) + styles = styles.Take(styles.Count() - 1); + + var distinctStyles = styles + .Distinct(); + + return distinctStyles.Count() == 1; + } + + private static IEnumerable QuotedTableFieldCharacters = new[] { "'", "#" }; + + internal void UpdateTableFieldTotalsRowFormula() + { + if (TotalsRowFunction != XLTotalsRowFunction.None && TotalsRowFunction != XLTotalsRowFunction.Custom) + { + var cell = table.TotalsRow().Cell(Index + 1); + var formulaCode = String.Empty; + switch (TotalsRowFunction) + { + case XLTotalsRowFunction.Sum: formulaCode = "109"; break; + case XLTotalsRowFunction.Minimum: formulaCode = "105"; break; + case XLTotalsRowFunction.Maximum: formulaCode = "104"; break; + case XLTotalsRowFunction.Average: formulaCode = "101"; break; + case XLTotalsRowFunction.Count: formulaCode = "103"; break; + case XLTotalsRowFunction.CountNumbers: formulaCode = "102"; break; + case XLTotalsRowFunction.StandardDeviation: formulaCode = "107"; break; + case XLTotalsRowFunction.Variance: formulaCode = "110"; break; + } + + var modifiedName = Name; + QuotedTableFieldCharacters.ForEach(c => modifiedName = modifiedName.Replace(c, "'" + c)); + + if (modifiedName.StartsWith(" ") || modifiedName.EndsWith(" ")) + { + modifiedName = "[" + modifiedName + "]"; + } + + var prependTableName = modifiedName.Contains(" "); + + cell.FormulaA1 = $"SUBTOTAL({formulaCode},{(prependTableName ? table.Name : string.Empty)}[{modifiedName}])"; + var lastCell = table.LastRow().Cell(Index + 1); + if (lastCell.DataType != XLDataType.Text) + { + cell.DataType = lastCell.DataType; + cell.Style.NumberFormat = lastCell.Style.NumberFormat; + } + } + } + } +} diff --git a/ClosedXML/Excel/Tables/XLTableRange.cs b/ClosedXML/Excel/Tables/XLTableRange.cs new file mode 100644 index 0000000..717f149 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTableRange.cs @@ -0,0 +1,270 @@ +using System; + +namespace ClosedXML.Excel +{ + using System.Linq; + + internal class XLTableRange : XLRange, IXLTableRange + { + private readonly XLTable _table; + private readonly XLRange _range; + public XLTableRange(XLRange range, XLTable table) + : base(new XLRangeParameters(range.RangeAddress, range.Style)) + { + _table = table; + _range = range; + } + + IXLTableRow IXLTableRange.FirstRow(Func predicate) + { + return FirstRow(predicate); + } + public XLTableRow FirstRow(Func predicate = null) + { + if (predicate == null) + return new XLTableRow(this, (_range.FirstRow())); + + Int32 rowCount = _range.RowCount(); + + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = new XLTableRow(this, (_range.Row(ro))); + if (predicate(row)) return row; + } + + return null; + } + + IXLTableRow IXLTableRange.FirstRowUsed(Func predicate) + { + return FirstRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + public XLTableRow FirstRowUsed(Func predicate = null) + { + return FirstRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLTableRow IXLTableRange.FirstRowUsed(Boolean includeFormats, Func predicate) + { + return FirstRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + IXLTableRow IXLTableRange.FirstRowUsed(XLCellsUsedOptions options, Func predicate) + { + return FirstRowUsed(options, predicate); + } + + internal XLTableRow FirstRowUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + return new XLTableRow(this, (_range.FirstRowUsed(options))); + + Int32 rowCount = _range.RowCount(); + + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = new XLTableRow(this, (_range.Row(ro))); + + if (!row.IsEmpty(options) && predicate(row)) + return row; + } + + return null; + } + + + IXLTableRow IXLTableRange.LastRow(Func predicate) + { + return LastRow(predicate); + } + public XLTableRow LastRow(Func predicate = null) + { + if (predicate == null) + return new XLTableRow(this, (_range.LastRow())); + + Int32 rowCount = _range.RowCount(); + + for (Int32 ro = rowCount; ro >= 1; ro--) + { + var row = new XLTableRow(this, (_range.Row(ro))); + if (predicate(row)) return row; + } + return null; + } + + IXLTableRow IXLTableRange.LastRowUsed(Func predicate) + { + return LastRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + public XLTableRow LastRowUsed(Func predicate = null) + { + return LastRowUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLTableRow IXLTableRange.LastRowUsed(Boolean includeFormats, Func predicate) + { + return LastRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + IXLTableRow IXLTableRange.LastRowUsed(XLCellsUsedOptions options, Func predicate) + { + return LastRowUsed(options, predicate); + } + + + internal XLTableRow LastRowUsed(XLCellsUsedOptions options, Func predicate = null) + { + if (predicate == null) + return new XLTableRow(this, (_range.LastRowUsed(options))); + + Int32 rowCount = _range.RowCount(); + + for (Int32 ro = rowCount; ro >= 1; ro--) + { + var row = new XLTableRow(this, (_range.Row(ro))); + + if (!row.IsEmpty(options) && predicate(row)) + return row; + } + + return null; + } + + IXLTableRow IXLTableRange.Row(int row) + { + return Row(row); + } + public new XLTableRow Row(int row) + { + if (row <= 0 || row > XLHelper.MaxRowNumber + RangeAddress.FirstAddress.RowNumber - 1) + { + throw new ArgumentOutOfRangeException( + nameof(row), + String.Format("Row number must be between 1 and {0}", XLHelper.MaxRowNumber + RangeAddress.FirstAddress.RowNumber - 1) + ); + } + + return new XLTableRow(this, base.Row(row)); + } + + public IXLTableRows Rows(Func predicate = null) + { + var retVal = new XLTableRows(Worksheet.Style); + Int32 rowCount = _range.RowCount(); + + for (int r = 1; r <= rowCount; r++) + { + var row = Row(r); + if (predicate == null || predicate(row)) + retVal.Add(row); + } + return retVal; + } + + public new IXLTableRows Rows(int firstRow, int lastRow) + { + var retVal = new XLTableRows(Worksheet.Style); + + for (int rowNumber = firstRow; rowNumber <= lastRow; rowNumber++) + retVal.Add(Row(rowNumber)); + + return retVal; + } + + public new IXLTableRows Rows(string rows) + { + var retVal = new XLTableRows(Worksheet.Style); + var rowPairs = rows.Split(','); + foreach (string tPair in rowPairs.Select(pair => pair.Trim())) + { + String firstRow; + String lastRow; + if (tPair.Contains(':') || tPair.Contains('-')) + { + var rowRange = XLHelper.SplitRange(tPair); + + firstRow = rowRange[0]; + lastRow = rowRange[1]; + } + else + { + firstRow = tPair; + lastRow = tPair; + } + foreach (IXLTableRow row in Rows(Int32.Parse(firstRow), Int32.Parse(lastRow))) + retVal.Add(row); + } + return retVal; + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLTableRows RowsUsed(Boolean includeFormats, Func predicate = null) + { + return RowsUsed(includeFormats + ? XLCellsUsedOptions.AllContents + : XLCellsUsedOptions.All, + predicate); + } + + IXLTableRows IXLTableRange.RowsUsed(XLCellsUsedOptions options, Func predicate) + { + return RowsUsed(options, predicate); + } + + internal XLTableRows RowsUsed(XLCellsUsedOptions options, Func predicate = null) + { + var rows = new XLTableRows(Worksheet.Style); + Int32 rowCount = RowCount(); + + for (Int32 ro = 1; ro <= rowCount; ro++) + { + var row = Row(ro); + + if (!row.IsEmpty(options) && (predicate == null || predicate(row))) + rows.Add(row); + } + return rows; + } + + IXLTableRows IXLTableRange.RowsUsed(Func predicate) + { + return RowsUsed(predicate); + } + public IXLTableRows RowsUsed(Func predicate = null) + { + return RowsUsed(XLCellsUsedOptions.AllContents, predicate); + } + + IXLTable IXLTableRange.Table { get { return _table; } } + public XLTable Table { get { return _table; } } + + public new IXLTableRows InsertRowsAbove(int numberOfRows) + { + return XLHelper.InsertRowsWithoutEvents(base.InsertRowsAbove, this, numberOfRows, !Table.ShowTotalsRow ); + } + public new IXLTableRows InsertRowsBelow(int numberOfRows) + { + return XLHelper.InsertRowsWithoutEvents(base.InsertRowsBelow, this, numberOfRows, !Table.ShowTotalsRow); + } + + + public new IXLRangeColumn Column(String column) + { + if (XLHelper.IsValidColumn(column)) + { + Int32 coNum = XLHelper.GetColumnNumberFromLetter(column); + return coNum > ColumnCount() ? Column(_table.GetFieldIndex(column) + 1) : Column(coNum); + } + + return Column(_table.GetFieldIndex(column) + 1); + } + } +} diff --git a/ClosedXML/Excel/Tables/XLTableRow.cs b/ClosedXML/Excel/Tables/XLTableRow.cs new file mode 100644 index 0000000..f863b67 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTableRow.cs @@ -0,0 +1,115 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLTableRow : XLRangeRow, IXLTableRow + { + private readonly XLTableRange _tableRange; + + public XLTableRow(XLTableRange tableRange, XLRangeRow rangeRow) + : base(new XLRangeParameters(rangeRow.RangeAddress, rangeRow.Style)) + { + _tableRange = tableRange; + } + + #region IXLTableRow Members + + public IXLCell Field(Int32 index) + { + return Cell(index + 1); + } + + public IXLCell Field(String name) + { + Int32 fieldIndex = _tableRange.Table.GetFieldIndex(name); + return Cell(fieldIndex + 1); + } + + public new IXLTableRow Sort() + { + return SortLeftToRight(); + } + + public new IXLTableRow SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, Boolean ignoreBlanks = true) + { + base.SortLeftToRight(sortOrder, matchCase, ignoreBlanks); + return this; + } + + #endregion IXLTableRow Members + + private XLTableRow RowShift(Int32 rowsToShift) + { + return _tableRange.Row(RowNumber() - _tableRange.FirstRow().RowNumber() + 1 + rowsToShift); + } + + #region XLTableRow Above + + IXLTableRow IXLTableRow.RowAbove() + { + return RowAbove(); + } + + IXLTableRow IXLTableRow.RowAbove(Int32 step) + { + return RowAbove(step); + } + + public new XLTableRow RowAbove() + { + return RowAbove(1); + } + + public new XLTableRow RowAbove(Int32 step) + { + return RowShift(step * -1); + } + + #endregion XLTableRow Above + + #region XLTableRow Below + + IXLTableRow IXLTableRow.RowBelow() + { + return RowBelow(); + } + + IXLTableRow IXLTableRow.RowBelow(Int32 step) + { + return RowBelow(step); + } + + public new XLTableRow RowBelow() + { + return RowBelow(1); + } + + public new XLTableRow RowBelow(Int32 step) + { + return RowShift(step); + } + + #endregion XLTableRow Below + + public new IXLTableRow Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + base.Clear(clearOptions); + return this; + } + + public new IXLTableRows InsertRowsAbove(int numberOfRows) + { + return XLHelper.InsertRowsWithoutEvents(base.InsertRowsAbove, _tableRange, numberOfRows, !_tableRange.Table.ShowTotalsRow); + } + + public new IXLTableRows InsertRowsBelow(int numberOfRows) + { + return XLHelper.InsertRowsWithoutEvents(base.InsertRowsBelow, _tableRange, numberOfRows, !_tableRange.Table.ShowTotalsRow); + } + + public new void Delete() + { + Delete(XLShiftDeletedCells.ShiftCellsUp); + } + } +} diff --git a/ClosedXML/Excel/Tables/XLTableRows.cs b/ClosedXML/Excel/Tables/XLTableRows.cs new file mode 100644 index 0000000..b3cff49 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTableRows.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLTableRows : XLStylizedBase, IXLTableRows, IXLStylized + { + private readonly List _ranges = new List(); + + public XLTableRows(IXLStyle defaultStyle) : base((defaultStyle as XLStyle).Value) + { + } + + #region IXLStylized Members + + public override IEnumerable Styles + { + get + { + yield return Style; + foreach (XLTableRow rng in _ranges) + { + yield return rng.Style; + foreach (XLCell r in rng.Worksheet.Internals.CellsCollection.GetCells( + rng.RangeAddress.FirstAddress.RowNumber, + rng.RangeAddress.FirstAddress.ColumnNumber, + rng.RangeAddress.LastAddress.RowNumber, + rng.RangeAddress.LastAddress.ColumnNumber)) + yield return r.Style; + } + } + } + + protected override IEnumerable Children + { + get + { + foreach (var range in _ranges) + { + yield return range; + } + } + } + + public override IXLRanges RangesUsed + { + get + { + var retVal = new XLRanges(); + this.ForEach(c => retVal.Add(c.AsRange())); + return retVal; + } + } + + #endregion IXLStylized Members + + #region IXLTableRows Members + + public IXLTableRows Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _ranges.ForEach(r => r.Clear(clearOptions)); + return this; + } + + public void Delete() + { + _ranges.OrderByDescending(r => r.RowNumber()).ForEach(r => r.Delete()); + _ranges.Clear(); + } + + public void Add(IXLTableRow tableRow) + { + _ranges.Add((XLTableRow)tableRow); + } + + public IEnumerator GetEnumerator() + { + var retList = new List(); + _ranges.ForEach(retList.Add); + return retList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLCells Cells() + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + foreach (XLTableRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed() + { + var cells = new XLCells(true, XLCellsUsedOptions.AllContents); + foreach (XLTableRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + public IXLCells CellsUsed(Boolean includeFormats) + { + return CellsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + public IXLCells CellsUsed(XLCellsUsedOptions options) + { + var cells = new XLCells(false, options); + foreach (XLTableRow container in _ranges) + cells.Add(container.RangeAddress); + return cells; + } + + #endregion IXLTableRows Members + + public void Select() + { + foreach (var range in this) + range.Select(); + } + } +} diff --git a/ClosedXML/Excel/Tables/XLTableTheme.cs b/ClosedXML/Excel/Tables/XLTableTheme.cs new file mode 100644 index 0000000..ca70d17 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTableTheme.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ClosedXML.Excel +{ + public sealed class XLTableTheme + { + public static readonly XLTableTheme None = new XLTableTheme("None"); + public static readonly XLTableTheme TableStyleMedium28 = new XLTableTheme("TableStyleMedium28"); + public static readonly XLTableTheme TableStyleMedium27 = new XLTableTheme("TableStyleMedium27"); + public static readonly XLTableTheme TableStyleMedium26 = new XLTableTheme("TableStyleMedium26"); + public static readonly XLTableTheme TableStyleMedium25 = new XLTableTheme("TableStyleMedium25"); + public static readonly XLTableTheme TableStyleMedium24 = new XLTableTheme("TableStyleMedium24"); + public static readonly XLTableTheme TableStyleMedium23 = new XLTableTheme("TableStyleMedium23"); + public static readonly XLTableTheme TableStyleMedium22 = new XLTableTheme("TableStyleMedium22"); + public static readonly XLTableTheme TableStyleMedium21 = new XLTableTheme("TableStyleMedium21"); + public static readonly XLTableTheme TableStyleMedium20 = new XLTableTheme("TableStyleMedium20"); + public static readonly XLTableTheme TableStyleMedium19 = new XLTableTheme("TableStyleMedium19"); + public static readonly XLTableTheme TableStyleMedium18 = new XLTableTheme("TableStyleMedium18"); + public static readonly XLTableTheme TableStyleMedium17 = new XLTableTheme("TableStyleMedium17"); + public static readonly XLTableTheme TableStyleMedium16 = new XLTableTheme("TableStyleMedium16"); + public static readonly XLTableTheme TableStyleMedium15 = new XLTableTheme("TableStyleMedium15"); + public static readonly XLTableTheme TableStyleMedium14 = new XLTableTheme("TableStyleMedium14"); + public static readonly XLTableTheme TableStyleMedium13 = new XLTableTheme("TableStyleMedium13"); + public static readonly XLTableTheme TableStyleMedium12 = new XLTableTheme("TableStyleMedium12"); + public static readonly XLTableTheme TableStyleMedium11 = new XLTableTheme("TableStyleMedium11"); + public static readonly XLTableTheme TableStyleMedium10 = new XLTableTheme("TableStyleMedium10"); + public static readonly XLTableTheme TableStyleMedium9 = new XLTableTheme("TableStyleMedium9"); + public static readonly XLTableTheme TableStyleMedium8 = new XLTableTheme("TableStyleMedium8"); + public static readonly XLTableTheme TableStyleMedium7 = new XLTableTheme("TableStyleMedium7"); + public static readonly XLTableTheme TableStyleMedium6 = new XLTableTheme("TableStyleMedium6"); + public static readonly XLTableTheme TableStyleMedium5 = new XLTableTheme("TableStyleMedium5"); + public static readonly XLTableTheme TableStyleMedium4 = new XLTableTheme("TableStyleMedium4"); + public static readonly XLTableTheme TableStyleMedium3 = new XLTableTheme("TableStyleMedium3"); + public static readonly XLTableTheme TableStyleMedium2 = new XLTableTheme("TableStyleMedium2"); + public static readonly XLTableTheme TableStyleMedium1 = new XLTableTheme("TableStyleMedium1"); + public static readonly XLTableTheme TableStyleLight21 = new XLTableTheme("TableStyleLight21"); + public static readonly XLTableTheme TableStyleLight20 = new XLTableTheme("TableStyleLight20"); + public static readonly XLTableTheme TableStyleLight19 = new XLTableTheme("TableStyleLight19"); + public static readonly XLTableTheme TableStyleLight18 = new XLTableTheme("TableStyleLight18"); + public static readonly XLTableTheme TableStyleLight17 = new XLTableTheme("TableStyleLight17"); + public static readonly XLTableTheme TableStyleLight16 = new XLTableTheme("TableStyleLight16"); + public static readonly XLTableTheme TableStyleLight15 = new XLTableTheme("TableStyleLight15"); + public static readonly XLTableTheme TableStyleLight14 = new XLTableTheme("TableStyleLight14"); + public static readonly XLTableTheme TableStyleLight13 = new XLTableTheme("TableStyleLight13"); + public static readonly XLTableTheme TableStyleLight12 = new XLTableTheme("TableStyleLight12"); + public static readonly XLTableTheme TableStyleLight11 = new XLTableTheme("TableStyleLight11"); + public static readonly XLTableTheme TableStyleLight10 = new XLTableTheme("TableStyleLight10"); + public static readonly XLTableTheme TableStyleLight9 = new XLTableTheme("TableStyleLight9"); + public static readonly XLTableTheme TableStyleLight8 = new XLTableTheme("TableStyleLight8"); + public static readonly XLTableTheme TableStyleLight7 = new XLTableTheme("TableStyleLight7"); + public static readonly XLTableTheme TableStyleLight6 = new XLTableTheme("TableStyleLight6"); + public static readonly XLTableTheme TableStyleLight5 = new XLTableTheme("TableStyleLight5"); + public static readonly XLTableTheme TableStyleLight4 = new XLTableTheme("TableStyleLight4"); + public static readonly XLTableTheme TableStyleLight3 = new XLTableTheme("TableStyleLight3"); + public static readonly XLTableTheme TableStyleLight2 = new XLTableTheme("TableStyleLight2"); + public static readonly XLTableTheme TableStyleLight1 = new XLTableTheme("TableStyleLight1"); + public static readonly XLTableTheme TableStyleDark11 = new XLTableTheme("TableStyleDark11"); + public static readonly XLTableTheme TableStyleDark10 = new XLTableTheme("TableStyleDark10"); + public static readonly XLTableTheme TableStyleDark9 = new XLTableTheme("TableStyleDark9"); + public static readonly XLTableTheme TableStyleDark8 = new XLTableTheme("TableStyleDark8"); + public static readonly XLTableTheme TableStyleDark7 = new XLTableTheme("TableStyleDark7"); + public static readonly XLTableTheme TableStyleDark6 = new XLTableTheme("TableStyleDark6"); + public static readonly XLTableTheme TableStyleDark5 = new XLTableTheme("TableStyleDark5"); + public static readonly XLTableTheme TableStyleDark4 = new XLTableTheme("TableStyleDark4"); + public static readonly XLTableTheme TableStyleDark3 = new XLTableTheme("TableStyleDark3"); + public static readonly XLTableTheme TableStyleDark2 = new XLTableTheme("TableStyleDark2"); + public static readonly XLTableTheme TableStyleDark1 = new XLTableTheme("TableStyleDark1"); + + public string Name { get; private set; } + + public XLTableTheme(string name) + { + this.Name = name; + } + + private static IEnumerable allThemes; + + public static IEnumerable GetAllThemes() + { + return (allThemes ?? (allThemes = typeof(XLTableTheme).GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(fi => fi.FieldType.Equals(typeof(XLTableTheme))) + .Select(fi => (XLTableTheme)fi.GetValue(null)) + .ToArray())); + } + + public static XLTableTheme FromName(string name) + { + return GetAllThemes().FirstOrDefault(s => s.Name == name); + } + + #region Overrides + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + XLTableTheme theme = obj as XLTableTheme; + if (theme == null) + { + return false; + } + return this.Name.Equals(theme.Name); + } + + public override int GetHashCode() + { + return this.Name.GetHashCode(); + } + + public override string ToString() + { + return this.Name; + } + + #endregion Overrides + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/Tables/XLTables.cs b/ClosedXML/Excel/Tables/XLTables.cs new file mode 100644 index 0000000..5d2fed9 --- /dev/null +++ b/ClosedXML/Excel/Tables/XLTables.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + using System.Collections; + + internal class XLTables : IXLTables + { + private readonly Dictionary _tables; + + public XLTables() + { + _tables = new Dictionary(StringComparer.OrdinalIgnoreCase); + Deleted = new HashSet(); + } + + internal ICollection Deleted { get; private set; } + + #region IXLTables Members + + public void Add(IXLTable table) + { + _tables.Add(table.Name, table); + (table as XLTable)?.OnAddedToTables(); + } + + public IXLTables Clear(XLClearOptions clearOptions = XLClearOptions.All) + { + _tables.Values.ForEach(t => t.Clear(clearOptions)); + return this; + } + + public Boolean Contains(String name) + { + return _tables.ContainsKey(name); + } + + public IEnumerator GetEnumerator() + { + return _tables.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Remove(Int32 index) + { + this.Remove(_tables.ElementAt(index).Key); + } + + public void Remove(String name) + { + if (!_tables.TryGetValue(name, out IXLTable table)) + throw new ArgumentOutOfRangeException(nameof(name), $"Unable to delete table because the table name {name} could not be found."); + + _tables.Remove(name); + + var relId = (table as XLTable)?.RelId; + + if (relId != null) + Deleted.Add(relId); + } + + public IXLTable Table(Int32 index) + { + return _tables.ElementAt(index).Value; + } + + public IXLTable Table(String name) + { + if (TryGetTable(name, out IXLTable table)) + return table; + + throw new ArgumentOutOfRangeException(nameof(name), $"Table {name} was not found."); + } + + public bool TryGetTable(string tableName, out IXLTable table) + { + return _tables.TryGetValue(tableName, out table); + } + + #endregion IXLTables Members + } +} diff --git a/ClosedXML/Excel/XLCellsUsedOptions.cs b/ClosedXML/Excel/XLCellsUsedOptions.cs new file mode 100644 index 0000000..6342062 --- /dev/null +++ b/ClosedXML/Excel/XLCellsUsedOptions.cs @@ -0,0 +1,29 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + public enum XLCellsUsedOptions + { + Contents = 1 << 0, + DataType = 1 << 1, + NormalFormats = 1 << 2, + ConditionalFormats = 1 << 3, + Comments = 1 << 4, + DataValidation = 1 << 5, + MergedRanges = 1 << 6, + Sparklines = 1 << 7, + + AllFormats = NormalFormats | ConditionalFormats, + AllContents = Contents | DataType | Comments, + All = Contents | DataType | NormalFormats | ConditionalFormats | Comments | DataValidation | MergedRanges | Sparklines + } + + internal static class XLCellsUsedOptionsExtensions + { + public static XLClearOptions ToClearOptions(this XLCellsUsedOptions options) + { + return (XLClearOptions)options; + } + } +} diff --git a/ClosedXML/Excel/XLClearOptions.cs b/ClosedXML/Excel/XLClearOptions.cs new file mode 100644 index 0000000..c6cbfd2 --- /dev/null +++ b/ClosedXML/Excel/XLClearOptions.cs @@ -0,0 +1,29 @@ +using System; + +namespace ClosedXML.Excel +{ + [Flags] + public enum XLClearOptions + { + Contents = 1 << 0, + DataType = 1 << 1, + NormalFormats = 1 << 2, + ConditionalFormats = 1 << 3, + Comments = 1 << 4, + DataValidation = 1 << 5, + MergedRanges = 1 << 6, + Sparklines = 1 << 7, + + AllFormats = NormalFormats | ConditionalFormats, + AllContents = Contents | DataType | Comments, + All = Contents | DataType | NormalFormats | ConditionalFormats | Comments | DataValidation | MergedRanges | Sparklines + } + + internal static class XLClearOptionsExtensions + { + public static XLCellsUsedOptions ToCellsUsedOptions(this XLClearOptions options) + { + return (XLCellsUsedOptions)options; + } + } +} diff --git a/ClosedXML/Excel/XLConstants.cs b/ClosedXML/Excel/XLConstants.cs new file mode 100644 index 0000000..e016d0d --- /dev/null +++ b/ClosedXML/Excel/XLConstants.cs @@ -0,0 +1,28 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + //Use the class to store magic strings or variables. + public static class XLConstants + { + internal const int NumberOfBuiltInStyles = 164; // But they are stored as 0-based (0 - 163) + + #region Pivot Table constants + + public static class PivotTable + { + internal const byte CreatedVersion = 5; + internal const byte RefreshedVersion = 5; + + //TODO: Needs to be refactored to be more user-friendly. + public const string ValuesSentinalLabel = "{{Values}}"; + } + + #endregion Pivot Table constants + + internal static class Comment + { + internal const string AlternateShapeTypeId = "_xssf_cell_comment"; + internal const string ShapeTypeId = "_x0000_t202"; + } + } +} diff --git a/ClosedXML/Excel/XLFileSharing.cs b/ClosedXML/Excel/XLFileSharing.cs new file mode 100644 index 0000000..8449770 --- /dev/null +++ b/ClosedXML/Excel/XLFileSharing.cs @@ -0,0 +1,10 @@ +// Keep this file CodeMaid organised and cleaned + +namespace ClosedXML.Excel +{ + internal class XLFileSharing : IXLFileSharing + { + public bool ReadOnlyRecommended { get; set; } + public string UserName { get; set; } + } +} diff --git a/ClosedXML/Excel/XLOutline.cs b/ClosedXML/Excel/XLOutline.cs new file mode 100644 index 0000000..1e296b7 --- /dev/null +++ b/ClosedXML/Excel/XLOutline.cs @@ -0,0 +1,17 @@ + +namespace ClosedXML.Excel +{ + internal class XLOutline:IXLOutline + { + public XLOutline(IXLOutline outline) + { + if (outline != null) + { + SummaryHLocation = outline.SummaryHLocation; + SummaryVLocation = outline.SummaryVLocation; + } + } + public XLOutlineSummaryVLocation SummaryVLocation { get; set; } + public XLOutlineSummaryHLocation SummaryHLocation { get; set; } + } +} diff --git a/ClosedXML/Excel/XLSheetView.cs b/ClosedXML/Excel/XLSheetView.cs new file mode 100644 index 0000000..6c6c08c --- /dev/null +++ b/ClosedXML/Excel/XLSheetView.cs @@ -0,0 +1,108 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal class XLSheetView : IXLSheetView + { + private XLAddress _topLeftCellAddress; + private int _zoomScale; + + public XLSheetView(XLWorksheet worksheet) + { + Worksheet = worksheet; + View = XLSheetViewOptions.Normal; + + ZoomScale = 100; + ZoomScaleNormal = 100; + ZoomScalePageLayoutView = 100; + ZoomScaleSheetLayoutView = 100; + } + + public XLSheetView(XLWorksheet worksheet, XLSheetView sheetView) + : this(worksheet) + { + this.SplitRow = sheetView.SplitRow; + this.SplitColumn = sheetView.SplitColumn; + this.FreezePanes = sheetView.FreezePanes; + this.TopLeftCellAddress = new XLAddress(this.Worksheet, sheetView.TopLeftCellAddress.RowNumber, sheetView.TopLeftCellAddress.ColumnNumber, sheetView.TopLeftCellAddress.FixedRow, sheetView.TopLeftCellAddress.FixedColumn); + } + + public Boolean FreezePanes { get; set; } + public Int32 SplitColumn { get; set; } + public Int32 SplitRow { get; set; } + + IXLAddress IXLSheetView.TopLeftCellAddress { get => TopLeftCellAddress; set => TopLeftCellAddress = (XLAddress)value; } + + public XLAddress TopLeftCellAddress + { + get => _topLeftCellAddress; + set + { + if (value.HasWorksheet && !value.Worksheet.Equals(this.Worksheet)) + throw new ArgumentException($"The value should be on the same worksheet as the sheet view."); + + _topLeftCellAddress = value; + } + } + + public XLSheetViewOptions View { get; set; } + + IXLWorksheet IXLSheetView.Worksheet { get => Worksheet; } + public XLWorksheet Worksheet { get; internal set; } + + public int ZoomScale + { + get { return _zoomScale; } + set + { + _zoomScale = value; + switch (View) + { + case XLSheetViewOptions.Normal: + ZoomScaleNormal = value; + break; + + case XLSheetViewOptions.PageBreakPreview: + ZoomScalePageLayoutView = value; + break; + + case XLSheetViewOptions.PageLayout: + ZoomScaleSheetLayoutView = value; + break; + } + } + } + + public int ZoomScaleNormal { get; set; } + + public int ZoomScalePageLayoutView { get; set; } + + public int ZoomScaleSheetLayoutView { get; set; } + + public void Freeze(Int32 rows, Int32 columns) + { + SplitRow = rows; + SplitColumn = columns; + FreezePanes = true; + } + + public void FreezeColumns(Int32 columns) + { + SplitColumn = columns; + FreezePanes = true; + } + + public void FreezeRows(Int32 rows) + { + SplitRow = rows; + FreezePanes = true; + } + + public IXLSheetView SetView(XLSheetViewOptions value) + { + View = value; + return this; + } + } +} diff --git a/ClosedXML/Excel/XLTheme.cs b/ClosedXML/Excel/XLTheme.cs new file mode 100644 index 0000000..c8677a2 --- /dev/null +++ b/ClosedXML/Excel/XLTheme.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLTheme : IXLTheme + { + public XLColor Background1 { get; set; } + public XLColor Text1 { get; set; } + public XLColor Background2 { get; set; } + public XLColor Text2 { get; set; } + public XLColor Accent1 { get; set; } + public XLColor Accent2 { get; set; } + public XLColor Accent3 { get; set; } + public XLColor Accent4 { get; set; } + public XLColor Accent5 { get; set; } + public XLColor Accent6 { get; set; } + public XLColor Hyperlink { get; set; } + public XLColor FollowedHyperlink { get; set; } + + public XLColor ResolveThemeColor(XLThemeColor themeColor) + { + switch (themeColor) + { + case XLThemeColor.Background1: + return Background1; + + case XLThemeColor.Text1: + return Text1; + + case XLThemeColor.Background2: + return Background2; + + case XLThemeColor.Text2: + return Text2; + + case XLThemeColor.Accent1: + return Accent1; + + case XLThemeColor.Accent2: + return Accent2; + + case XLThemeColor.Accent3: + return Accent3; + + case XLThemeColor.Accent4: + return Accent4; + + case XLThemeColor.Accent5: + return Accent5; + + case XLThemeColor.Accent6: + return Accent6; + + case XLThemeColor.Hyperlink: + return Hyperlink; + + case XLThemeColor.FollowedHyperlink: + return FollowedHyperlink; + + default: + return null; + } + } + } +} diff --git a/ClosedXML/Excel/XLWorkbook.cs b/ClosedXML/Excel/XLWorkbook.cs new file mode 100644 index 0000000..488beb3 --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook.cs @@ -0,0 +1,1072 @@ +using ClosedXML.Excel.CalcEngine; +using DocumentFormat.OpenXml; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + public enum XLEventTracking { Enabled, Disabled } + + public enum XLCalculateMode + { + Auto, + AutoNoTable, + Manual, + Default + }; + + public enum XLReferenceStyle + { + R1C1, + A1, + Default + }; + + public enum XLCellSetValueBehavior + { + /// + /// Analyze input string and convert value. For avoid analyzing use escape symbol ' + /// + Smart = 0, + + /// + /// Direct set value. If value has unsupported type - value will be stored as string returned by + /// + Simple = 1, + } + + public partial class XLWorkbook : IXLWorkbook + { + #region Static + + public static IXLStyle DefaultStyle + { + get + { + return XLStyle.Default; + } + } + + internal static XLStyleValue DefaultStyleValue + { + get + { + return XLStyleValue.Default; + } + } + + public static Double DefaultRowHeight { get; private set; } + public static Double DefaultColumnWidth { get; private set; } + + public static IXLPageSetup DefaultPageOptions + { + get + { + var defaultPageOptions = new XLPageSetup(null, null) + { + PageOrientation = XLPageOrientation.Default, + Scale = 100, + PaperSize = XLPaperSize.LetterPaper, + Margins = new XLMargins + { + Top = 0.75, + Bottom = 0.5, + Left = 0.75, + Right = 0.75, + Header = 0.5, + Footer = 0.75 + }, + ScaleHFWithDocument = true, + AlignHFWithMargins = true, + PrintErrorValue = XLPrintErrorValues.Displayed, + ShowComments = XLShowCommentsValues.None + }; + return defaultPageOptions; + } + } + + public static IXLOutline DefaultOutline + { + get + { + return new XLOutline(null) + { + SummaryHLocation = XLOutlineSummaryHLocation.Right, + SummaryVLocation = XLOutlineSummaryVLocation.Bottom + }; + } + } + + /// + /// Behavior for + /// + public static XLCellSetValueBehavior CellSetValueBehavior { get; set; } + + public static XLWorkbook OpenFromTemplate(String path) + { + return new XLWorkbook(path, asTemplate: true); + } + + #endregion Static + + internal readonly List UnsupportedSheets = + new List(); + + public XLEventTracking EventTracking { get; set; } + + /// + /// Counter increasing at workbook data change. Serves to determine if the cell formula + /// has to be recalculated. + /// + internal long RecalculationCounter { get; private set; } + + /// + /// Notify that workbook data has been changed which means that cached formula values + /// need to be re-evaluated. + /// + internal void InvalidateFormulas() + { + RecalculationCounter++; + } + + #region Nested Type : XLLoadSource + + private enum XLLoadSource + { + New, + File, + Stream + }; + + #endregion Nested Type : XLLoadSource + + internal XLWorksheets WorksheetsInternal { get; private set; } + + /// + /// Gets an object to manipulate the worksheets. + /// + public IXLWorksheets Worksheets + { + get { return WorksheetsInternal; } + } + + /// + /// Gets an object to manipulate this workbook's named ranges. + /// + public IXLNamedRanges NamedRanges { get; private set; } + + /// + /// Gets an object to manipulate this workbook's theme. + /// + public IXLTheme Theme { get; private set; } + + /// + /// Gets or sets the default style for the workbook. + /// All new worksheets will use this style. + /// + public IXLStyle Style { get; set; } + + /// + /// Gets or sets the default row height for the workbook. + /// All new worksheets will use this row height. + /// + public Double RowHeight { get; set; } + + /// + /// Gets or sets the default column width for the workbook. + /// All new worksheets will use this column width. + /// + public Double ColumnWidth { get; set; } + + /// + /// Gets or sets the default page options for the workbook. + /// All new worksheets will use these page options. + /// + public IXLPageSetup PageOptions { get; set; } + + /// + /// Gets or sets the default outline options for the workbook. + /// All new worksheets will use these outline options. + /// + public IXLOutline Outline { get; set; } + + /// + /// Gets or sets the workbook's properties. + /// + public XLWorkbookProperties Properties { get; set; } + + /// + /// Gets or sets the workbook's calculation mode. + /// + public XLCalculateMode CalculateMode { get; set; } + + public Boolean CalculationOnSave { get; set; } + public Boolean ForceFullCalculation { get; set; } + public Boolean FullCalculationOnLoad { get; set; } + public Boolean FullPrecision { get; set; } + + /// + /// Gets or sets the workbook's reference style. + /// + public XLReferenceStyle ReferenceStyle { get; set; } + + public IXLCustomProperties CustomProperties { get; private set; } + + public Boolean ShowFormulas { get; set; } + public Boolean ShowGridLines { get; set; } + public Boolean ShowOutlineSymbols { get; set; } + public Boolean ShowRowColHeaders { get; set; } + public Boolean ShowRuler { get; set; } + public Boolean ShowWhiteSpace { get; set; } + public Boolean ShowZeros { get; set; } + public Boolean RightToLeft { get; set; } + + public Boolean DefaultShowFormulas + { + get { return false; } + } + + public Boolean DefaultShowGridLines + { + get { return true; } + } + + public Boolean DefaultShowOutlineSymbols + { + get { return true; } + } + + public Boolean DefaultShowRowColHeaders + { + get { return true; } + } + + public Boolean DefaultShowRuler + { + get { return true; } + } + + public Boolean DefaultShowWhiteSpace + { + get { return true; } + } + + public Boolean DefaultShowZeros + { + get { return true; } + } + + public IXLFileSharing FileSharing { get; } = new XLFileSharing(); + + public Boolean DefaultRightToLeft + { + get { return false; } + } + + private void InitializeTheme() + { + Theme = new XLTheme + { + Text1 = XLColor.FromHtml("#FF000000"), + Background1 = XLColor.FromHtml("#FFFFFFFF"), + Text2 = XLColor.FromHtml("#FF1F497D"), + Background2 = XLColor.FromHtml("#FFEEECE1"), + Accent1 = XLColor.FromHtml("#FF4F81BD"), + Accent2 = XLColor.FromHtml("#FFC0504D"), + Accent3 = XLColor.FromHtml("#FF9BBB59"), + Accent4 = XLColor.FromHtml("#FF8064A2"), + Accent5 = XLColor.FromHtml("#FF4BACC6"), + Accent6 = XLColor.FromHtml("#FFF79646"), + Hyperlink = XLColor.FromHtml("#FF0000FF"), + FollowedHyperlink = XLColor.FromHtml("#FF800080") + }; + } + + public IXLNamedRange NamedRange(String rangeName) + { + if (rangeName.Contains("!")) + { + var split = rangeName.Split('!'); + var first = split[0]; + var wsName = first.StartsWith("'") ? first.Substring(1, first.Length - 2) : first; + var name = split[1]; + if (TryGetWorksheet(wsName, out IXLWorksheet ws)) + { + var range = ws.NamedRange(name); + return range ?? NamedRange(name); + } + return null; + } + return NamedRanges.NamedRange(rangeName); + } + + public Boolean TryGetWorksheet(String name, out IXLWorksheet worksheet) + { + return Worksheets.TryGetWorksheet(name, out worksheet); + } + + public IXLRange RangeFromFullAddress(String rangeAddress, out IXLWorksheet ws) + { + ws = null; + if (!rangeAddress.Contains('!')) return null; + + var split = rangeAddress.Split('!'); + var wsName = split[0].UnescapeSheetName(); + if (TryGetWorksheet(wsName, out ws)) + { + return ws.Range(split[1]); + } + return null; + } + + public IXLCell CellFromFullAddress(String cellAddress, out IXLWorksheet ws) + { + ws = null; + if (!cellAddress.Contains('!')) return null; + + var split = cellAddress.Split('!'); + var wsName = split[0].UnescapeSheetName(); + if (TryGetWorksheet(wsName, out ws)) + { + return ws.Cell(split[1]); + } + return null; + } + + /// + /// Saves the current workbook. + /// + public void Save() + { +#if DEBUG + Save(true, false); +#else + Save(false, false); +#endif + } + + /// + /// Saves the current workbook and optionally performs validation + /// + public void Save(Boolean validate, Boolean evaluateFormulae = false) + { + Save(new SaveOptions + { + ValidatePackage = validate, + EvaluateFormulasBeforeSaving = evaluateFormulae, + GenerateCalculationChain = true + }); + } + + public void Save(SaveOptions options) + { + checkForWorksheetsPresent(); + if (_loadSource == XLLoadSource.New) + throw new InvalidOperationException("This is a new file. Please use one of the 'SaveAs' methods."); + + if (_loadSource == XLLoadSource.Stream) + { + CreatePackage(_originalStream, false, _spreadsheetDocumentType, options); + } + else + CreatePackage(_originalFile, _spreadsheetDocumentType, options); + } + + /// + /// Saves the current workbook to a file. + /// + public void SaveAs(String file) + { +#if DEBUG + SaveAs(file, true, false); +#else + SaveAs(file, false, false); +#endif + } + + /// + /// Saves the current workbook to a file and optionally validates it. + /// + public void SaveAs(String file, Boolean validate, Boolean evaluateFormulae = false) + { + SaveAs(file, new SaveOptions + { + ValidatePackage = validate, + EvaluateFormulasBeforeSaving = evaluateFormulae, + GenerateCalculationChain = true + }); + } + + public void SaveAs(String file, SaveOptions options) + { + checkForWorksheetsPresent(); + PathHelper.CreateDirectory(Path.GetDirectoryName(file)); + if (_loadSource == XLLoadSource.New) + { + if (File.Exists(file)) + File.Delete(file); + + CreatePackage(file, GetSpreadsheetDocumentType(file), options); + } + else if (_loadSource == XLLoadSource.File) + { + if (String.Compare(_originalFile.Trim(), file.Trim(), true) != 0) + { + File.Copy(_originalFile, file, true); + File.SetAttributes(file, FileAttributes.Normal); + } + + CreatePackage(file, GetSpreadsheetDocumentType(file), options); + } + else if (_loadSource == XLLoadSource.Stream) + { + _originalStream.Position = 0; + + using (var fileStream = File.Create(file)) + { + CopyStream(_originalStream, fileStream); + CreatePackage(fileStream, false, _spreadsheetDocumentType, options); + } + } + + _loadSource = XLLoadSource.File; + _originalFile = file; + _originalStream = null; + } + + private static SpreadsheetDocumentType GetSpreadsheetDocumentType(string filePath) + { + var extension = Path.GetExtension(filePath); + + if (string.IsNullOrEmpty(extension)) throw new ArgumentException("Empty extension is not supported."); + extension = extension.Substring(1).ToLowerInvariant(); + + switch (extension) + { + case "xlsm": + return SpreadsheetDocumentType.MacroEnabledWorkbook; + + case "xltm": + return SpreadsheetDocumentType.MacroEnabledTemplate; + + case "xlsx": + return SpreadsheetDocumentType.Workbook; + + case "xltx": + return SpreadsheetDocumentType.Template; + + default: + throw new ArgumentException(String.Format("Extension '{0}' is not supported. Supported extensions are '.xlsx', '.xlsm', '.xltx' and '.xltm'.", extension)); + } + } + + private void checkForWorksheetsPresent() + { + if (!Worksheets.Any()) + throw new InvalidOperationException("Workbooks need at least one worksheet."); + } + + /// + /// Saves the current workbook to a stream. + /// + public void SaveAs(Stream stream) + { +#if DEBUG + SaveAs(stream, true, false); +#else + SaveAs(stream, false, false); +#endif + } + + /// + /// Saves the current workbook to a stream and optionally validates it. + /// + public void SaveAs(Stream stream, Boolean validate, Boolean evaluateFormulae = false) + { + SaveAs(stream, new SaveOptions + { + ValidatePackage = validate, + EvaluateFormulasBeforeSaving = evaluateFormulae, + GenerateCalculationChain = true + }); + } + + public void SaveAs(Stream stream, SaveOptions options) + { + checkForWorksheetsPresent(); + if (_loadSource == XLLoadSource.New) + { + // dm 20130422, this method or better the method SpreadsheetDocument.Create which is called + // inside of 'CreatePackage' need a stream which CanSeek & CanRead + // and an ordinary Response stream of a webserver can't do this + // so we have to ask and provide a way around this + if (stream.CanRead && stream.CanSeek && stream.CanWrite) + { + // all is fine the package can be created in a direct way + CreatePackage(stream, true, _spreadsheetDocumentType, options); + } + else + { + // the harder way + using (var ms = new MemoryStream()) + { + CreatePackage(ms, true, _spreadsheetDocumentType, options); + // not really nessesary, because I changed CopyStream too. + // but for better understanding and if somebody in the future + // provide an changed version of CopyStream + ms.Position = 0; + CopyStream(ms, stream); + } + } + } + else if (_loadSource == XLLoadSource.File) + { + using (var fileStream = new FileStream(_originalFile, FileMode.Open, FileAccess.Read)) + { + CopyStream(fileStream, stream); + } + CreatePackage(stream, false, _spreadsheetDocumentType, options); + } + else if (_loadSource == XLLoadSource.Stream) + { + _originalStream.Position = 0; + if (_originalStream != stream) + CopyStream(_originalStream, stream); + + CreatePackage(stream, false, _spreadsheetDocumentType, options); + } + + _loadSource = XLLoadSource.Stream; + _originalStream = stream; + _originalFile = null; + } + + internal static void CopyStream(Stream input, Stream output) + { + var buffer = new byte[8 * 1024]; + int len; + // dm 20130422, it is always a good idea to rewind the input stream, or not? + if (input.CanSeek) + input.Seek(0, SeekOrigin.Begin); + while ((len = input.Read(buffer, 0, buffer.Length)) > 0) + output.Write(buffer, 0, len); + // dm 20130422, and flushing the output after write + output.Flush(); + } + + public IXLTable Table(string tableName, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) + { + var table = this.Worksheets + .SelectMany(ws => ws.Tables) + .FirstOrDefault(t => t.Name.Equals(tableName, comparisonType)); + + if (table == null) + throw new ArgumentOutOfRangeException($"Table {tableName} was not found."); + + return table; + } + + public IXLWorksheet Worksheet(String name) + { + return WorksheetsInternal.Worksheet(name); + } + + public IXLWorksheet Worksheet(Int32 position) + { + return WorksheetsInternal.Worksheet(position); + } + + public IXLCustomProperty CustomProperty(String name) + { + return CustomProperties.CustomProperty(name); + } + + public IXLCells FindCells(Func predicate) + { + var cells = new XLCells(false, XLCellsUsedOptions.AllContents); + foreach (XLWorksheet ws in WorksheetsInternal) + { + foreach (XLCell cell in ws.CellsUsed(XLCellsUsedOptions.All)) + { + if (predicate(cell)) + cells.Add(cell); + } + } + return cells; + } + + public IXLRows FindRows(Func predicate) + { + var rows = new XLRows(worksheet: null); + foreach (XLWorksheet ws in WorksheetsInternal) + { + foreach (IXLRow row in ws.Rows().Where(predicate)) + rows.Add(row as XLRow); + } + return rows; + } + + public IXLColumns FindColumns(Func predicate) + { + var columns = new XLColumns(worksheet: null); + foreach (XLWorksheet ws in WorksheetsInternal) + { + foreach (IXLColumn column in ws.Columns().Where(predicate)) + columns.Add(column as XLColumn); + } + return columns; + } + + /// + /// Searches the cells' contents for a given piece of text + /// + /// The search text. + /// The compare options. + /// if set to true search formulae instead of cell values. + /// + public IEnumerable Search(String searchText, CompareOptions compareOptions = CompareOptions.Ordinal, Boolean searchFormulae = false) + { + foreach (var ws in WorksheetsInternal) + { + foreach (var cell in ws.Search(searchText, compareOptions, searchFormulae)) + yield return cell; + } + } + + #region Fields + + private XLLoadSource _loadSource = XLLoadSource.New; + private String _originalFile; + private Stream _originalStream; + private XLWorkbookProtection _workbookProtection; + + #endregion Fields + + #region Constructor + + /// + /// Creates a new Excel workbook. + /// + public XLWorkbook() + : this(XLEventTracking.Enabled) + { + } + + internal XLWorkbook(String file, Boolean asTemplate) + : this(XLEventTracking.Enabled) + { + LoadSheetsFromTemplate(file); + } + + public XLWorkbook(XLEventTracking eventTracking) + { + EventTracking = eventTracking; + Protection = new XLWorkbookProtection(DefaultProtectionAlgorithm); + DefaultRowHeight = 15; + DefaultColumnWidth = 8.43; + Style = new XLStyle(null, DefaultStyle); + RowHeight = DefaultRowHeight; + ColumnWidth = DefaultColumnWidth; + PageOptions = DefaultPageOptions; + Outline = DefaultOutline; + Properties = new XLWorkbookProperties(); + CalculateMode = XLCalculateMode.Default; + ReferenceStyle = XLReferenceStyle.Default; + InitializeTheme(); + ShowFormulas = DefaultShowFormulas; + ShowGridLines = DefaultShowGridLines; + ShowOutlineSymbols = DefaultShowOutlineSymbols; + ShowRowColHeaders = DefaultShowRowColHeaders; + ShowRuler = DefaultShowRuler; + ShowWhiteSpace = DefaultShowWhiteSpace; + ShowZeros = DefaultShowZeros; + RightToLeft = DefaultRightToLeft; + WorksheetsInternal = new XLWorksheets(this); + NamedRanges = new XLNamedRanges(this); + CustomProperties = new XLCustomProperties(this); + ShapeIdManager = new XLIdManager(); + Author = Environment.UserName; + } + + public XLWorkbook(LoadOptions loadOptions) + : this(loadOptions.EventTracking) + { + } + + /// + /// Opens an existing workbook from a file. + /// + /// The file to open. + public XLWorkbook(String file) + : this(file, XLEventTracking.Enabled) + { + } + + public XLWorkbook(String file, XLEventTracking eventTracking) + : this(eventTracking) + { + _loadSource = XLLoadSource.File; + _originalFile = file; + _spreadsheetDocumentType = GetSpreadsheetDocumentType(_originalFile); + Load(file); + } + + public XLWorkbook(String file, LoadOptions loadOptions) + : this(file, loadOptions.EventTracking) + { + if (loadOptions.RecalculateAllFormulas) + this.RecalculateAllFormulas(); + } + + /// + /// Opens an existing workbook from a stream. + /// + /// The stream to open. + public XLWorkbook(Stream stream) + : this(stream, XLEventTracking.Enabled) + { + } + + public XLWorkbook(Stream stream, XLEventTracking eventTracking) + : this(eventTracking) + { + _loadSource = XLLoadSource.Stream; + _originalStream = stream; + Load(stream); + } + + public XLWorkbook(Stream stream, LoadOptions loadOptions) + : this(stream, loadOptions.EventTracking) + { + if (loadOptions.RecalculateAllFormulas) + this.RecalculateAllFormulas(); + } + + #endregion Constructor + + #region Nested type: UnsupportedSheet + + internal sealed class UnsupportedSheet + { + public Boolean IsActive; + public UInt32 SheetId; + public Int32 Position; + } + + #endregion Nested type: UnsupportedSheet + + public IXLCell Cell(String namedCell) + { + var namedRange = NamedRange(namedCell); + if (namedRange != null) + { + return namedRange.Ranges?.FirstOrDefault()?.FirstCell(); + } + else + return CellFromFullAddress(namedCell, out _); + } + + public IXLCells Cells(String namedCells) + { + return Ranges(namedCells).Cells(); + } + + public IXLRange Range(String range) + { + var namedRange = NamedRange(range); + if (namedRange != null) + return namedRange.Ranges.FirstOrDefault(); + else + return RangeFromFullAddress(range, out _); + } + + public IXLRanges Ranges(String ranges) + { + var retVal = new XLRanges(); + var rangePairs = ranges.Split(','); + foreach (var range in rangePairs.Select(r => Range(r.Trim())).Where(range => range != null)) + { + retVal.Add(range); + } + return retVal; + } + + internal XLIdManager ShapeIdManager { get; private set; } + + // Used by Janitor.Fody + private void DisposeManaged() + { + Worksheets.ForEach(w => (w as XLWorksheet).Cleanup()); + } + +#if _NET40_ + public void Dispose() + { + // net40 doesn't support Janitor.Fody, so let's dispose manually + DisposeManaged(); + } +#else + + public void Dispose() + { + // Leave this empty (for non net40 targets) so that Janitor.Fody can do its work + } + +#endif + + public Boolean Use1904DateSystem { get; set; } + + public XLWorkbook SetUse1904DateSystem() + { + return SetUse1904DateSystem(true); + } + + public XLWorkbook SetUse1904DateSystem(Boolean value) + { + Use1904DateSystem = value; + return this; + } + + public IXLWorksheet AddWorksheet() + { + return Worksheets.Add(); + } + + public IXLWorksheet AddWorksheet(Int32 position) + { + return Worksheets.Add(position); + } + + public IXLWorksheet AddWorksheet(String sheetName) + { + return Worksheets.Add(sheetName); + } + + public IXLWorksheet AddWorksheet(String sheetName, Int32 position) + { + return Worksheets.Add(sheetName, position); + } + + public IXLWorksheet AddWorksheet(DataTable dataTable) + { + return Worksheets.Add(dataTable); + } + + public void AddWorksheet(DataSet dataSet) + { + Worksheets.Add(dataSet); + } + + public void AddWorksheet(IXLWorksheet worksheet) + { + worksheet.CopyTo(this, worksheet.Name); + } + + public IXLWorksheet AddWorksheet(DataTable dataTable, String sheetName) + { + return Worksheets.Add(dataTable, sheetName); + } + + private XLCalcEngine _calcEngine; + + private XLCalcEngine CalcEngine + { + get { return _calcEngine ?? (_calcEngine = new XLCalcEngine(this)); } + } + + public Object Evaluate(String expression) + { + return CalcEngine.Evaluate(expression); + } + + /// + /// Force recalculation of all cell formulas. + /// + public void RecalculateAllFormulas() + { + InvalidateFormulas(); + Worksheets.ForEach(sheet => sheet.RecalculateAllFormulas()); + } + + private static XLCalcEngine _calcEngineExpr; + private SpreadsheetDocumentType _spreadsheetDocumentType; + + private static XLCalcEngine CalcEngineExpr + { + get { return _calcEngineExpr ?? (_calcEngineExpr = new XLCalcEngine()); } + } + + public static Object EvaluateExpr(String expression) + { + return CalcEngineExpr.Evaluate(expression); + } + + public String Author { get; set; } + + public Boolean LockStructure + { + get => Protection.IsProtected && !Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure); + set + { + if (!Protection.IsProtected) + throw new InvalidOperationException($"Enable workbook protection before setting the {nameof(LockStructure)} property"); + + Protection.AllowElement(XLWorkbookProtectionElements.Structure, value); + } + } + + public XLWorkbook SetLockStructure(Boolean value) + { + LockStructure = value; return this; + } + + public Boolean LockWindows + { + get => Protection.IsProtected && !Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows); + set + { + if (!Protection.IsProtected) + throw new InvalidOperationException($"Enable workbook protection before setting the {nameof(LockWindows)} property"); + + Protection.AllowElement(XLWorkbookProtectionElements.Windows, value); + } + } + + public XLWorkbook SetLockWindows(Boolean value) + { + LockWindows = value; return this; + } + + public Boolean IsPasswordProtected => Protection.IsPasswordProtected; + public Boolean IsProtected => Protection.IsProtected; + + IXLWorkbookProtection IXLProtectable.Protection + { + get => Protection; + set => Protection = value as XLWorkbookProtection; + } + + internal XLWorkbookProtection Protection + { + get => _workbookProtection; + set + { + _workbookProtection = value.Clone().CastTo(); + } + } + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + public IXLWorkbookProtection Protect(Boolean lockStructure, Boolean lockWindows, String password) + { + var allowedElements = XLWorkbookProtectionElements.Everything; + + var protection = Protection.Protect(password, DefaultProtectionAlgorithm, allowedElements); + + if (lockStructure) + protection.DisallowElement(XLWorkbookProtectionElements.Structure); + + if (lockWindows) + protection.DisallowElement(XLWorkbookProtectionElements.Windows); + + return protection; + } + + public IXLWorkbookProtection Protect() + { + return Protection.Protect(); + } + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + public IXLWorkbookProtection Protect(Boolean lockStructure) + { + return Protect(lockStructure, lockWindows: false, password: null); + } + + [Obsolete("Use Protect(String password, Algorithm algorithm, TElement allowedElements)")] + public IXLWorkbookProtection Protect(Boolean lockStructure, Boolean lockWindows) + { + return Protect(lockStructure, lockWindows, null); + } + + public IXLWorkbookProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm) + + { + return Protect(password, algorithm, XLWorkbookProtectionElements.Windows); + } + + public IXLWorkbookProtection Protect(String password, Algorithm algorithm, XLWorkbookProtectionElements allowedElements) + { + return Protection.Protect(password, algorithm, allowedElements); + } + + IXLElementProtection IXLProtectable.Protect() + { + return Protect(); + } + + IXLElementProtection IXLProtectable.Protect(string password, Algorithm algorithm) + { + return Protect(password, algorithm); + } + + public IXLWorkbookProtection Unprotect() + { + return Protection.Unprotect(); + } + + public IXLWorkbookProtection Unprotect(String password) + { + return Protection.Unprotect(password); + } + + IXLElementProtection IXLProtectable.Unprotect() + { + return Unprotect(); + } + + IXLElementProtection IXLProtectable.Unprotect(String password) + { + return Unprotect(password); + } + + public override string ToString() + { + switch (_loadSource) + { + case XLLoadSource.New: + return "XLWorkbook(new)"; + + case XLLoadSource.File: + return String.Format("XLWorkbook({0})", _originalFile); + + case XLLoadSource.Stream: + return String.Format("XLWorkbook({0})", _originalStream.ToString()); + + default: + throw new NotImplementedException(); + } + } + + public void SuspendEvents() + { + foreach (var ws in WorksheetsInternal) + { + ws.SuspendEvents(); + } + } + + public void ResumeEvents() + { + foreach (var ws in WorksheetsInternal) + { + ws.ResumeEvents(); + } + } + } +} diff --git a/ClosedXML/Excel/XLWorkbookProperties.cs b/ClosedXML/Excel/XLWorkbookProperties.cs new file mode 100644 index 0000000..e9d2bcb --- /dev/null +++ b/ClosedXML/Excel/XLWorkbookProperties.cs @@ -0,0 +1,25 @@ +using System; + +namespace ClosedXML.Excel +{ + public class XLWorkbookProperties + { + public XLWorkbookProperties() + { + Company = null; + Manager = null; + } + public String Author { get; set; } + public String Title { get; set; } + public String Subject { get; set; } + public String Category { get; set; } + public String Keywords { get; set; } + public String Comments { get; set; } + public String Status { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public String LastModifiedBy { get; set; } + public String Company { get; set; } + public String Manager { get; set; } + } +} diff --git a/ClosedXML/Excel/XLWorkbook_ImageHandling.cs b/ClosedXML/Excel/XLWorkbook_ImageHandling.cs new file mode 100644 index 0000000..8ea8d29 --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook_ImageHandling.cs @@ -0,0 +1,72 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Drawing.Spreadsheet; +using DocumentFormat.OpenXml.Packaging; +using System; +using System.Linq; + +using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet; + +namespace ClosedXML.Excel +{ + public partial class XLWorkbook + { + public static OpenXmlElement GetAnchorFromImageId(WorksheetPart worksheetPart, string relId) + { + var drawingsPart = worksheetPart.DrawingsPart; + var matchingAnchor = drawingsPart.WorksheetDrawing + .Where(wsdr => wsdr.Descendants() + .Any(x => x?.Blip?.Embed?.Value.Equals(relId) ?? false) + ); + + if (!matchingAnchor.Any()) + return null; + else + return matchingAnchor.First(); + } + + public static OpenXmlElement GetAnchorFromImageIndex(WorksheetPart worksheetPart, Int32 index) + { + var drawingsPart = worksheetPart.DrawingsPart; + var matchingAnchor = drawingsPart.WorksheetDrawing + .Where(wsdr => wsdr.Descendants() + .Any(x => x.Id.Value.Equals(Convert.ToUInt32(index + 1))) + ); + + if (!matchingAnchor.Any()) + return null; + else + return matchingAnchor.First(); + } + + public static NonVisualDrawingProperties GetPropertiesFromAnchor(OpenXmlElement anchor) + { + if (!IsAllowedAnchor(anchor)) + return null; + + // Maybe we should not restrict here, and just search for all NonVisualDrawingProperties in an anchor? + var shape = anchor.Descendants().Cast().FirstOrDefault() + ?? anchor.Descendants().Cast().FirstOrDefault(); + + if (shape == null) return null; + + return shape + .Descendants() + .FirstOrDefault(); + } + + public static String GetImageRelIdFromAnchor(OpenXmlElement anchor) + { + if (!IsAllowedAnchor(anchor)) + return null; + + var blipFill = anchor.Descendants().FirstOrDefault(); + return blipFill?.Blip?.Embed?.Value; + } + + private static bool IsAllowedAnchor(OpenXmlElement anchor) + { + var allowedAnchorTypes = new Type[] { typeof(AbsoluteAnchor), typeof(OneCellAnchor), typeof(TwoCellAnchor) }; + return (allowedAnchorTypes.Any(t => t == anchor.GetType())); + } + } +} diff --git a/ClosedXML/Excel/XLWorkbook_Load.cs b/ClosedXML/Excel/XLWorkbook_Load.cs new file mode 100644 index 0000000..1c1bac9 --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook_Load.cs @@ -0,0 +1,3269 @@ +using ClosedXML.Extensions; +using ClosedXML.Utils; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Ap = DocumentFormat.OpenXml.ExtendedProperties; +using Op = DocumentFormat.OpenXml.CustomProperties; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; +using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet; + +namespace ClosedXML.Excel +{ + using Ap; + using Drawings; + using Op; + using System.Drawing; + + public partial class XLWorkbook + { + private readonly Dictionary _colorList = new Dictionary(); + + private void Load(String file) + { + LoadSheets(file); + } + + private void Load(Stream stream) + { + LoadSheets(stream); + } + + private void LoadSheets(String fileName) + { + using (var dSpreadsheet = SpreadsheetDocument.Open(fileName, false)) + LoadSpreadsheetDocument(dSpreadsheet); + } + + private void LoadSheets(Stream stream) + { + using (var dSpreadsheet = SpreadsheetDocument.Open(stream, false)) + LoadSpreadsheetDocument(dSpreadsheet); + } + + private void LoadSheetsFromTemplate(String fileName) + { + using (var dSpreadsheet = SpreadsheetDocument.CreateFromTemplate(fileName)) + LoadSpreadsheetDocument(dSpreadsheet); + + // If we load a workbook as a template, we have to treat it as a "new" workbook. + // The original file will NOT be copied into place before changes are applied + // Hence all loaded RelIds have to be cleared + ResetAllRelIds(); + } + + private void ResetAllRelIds() + { + foreach (var ws in Worksheets.Cast()) + { + ws.SheetId = 0; + ws.RelId = null; + + foreach (var pt in ws.PivotTables.Cast()) + { + pt.WorkbookCacheRelId = null; + pt.CacheDefinitionRelId = null; + pt.RelId = null; + } + + foreach (var picture in ws.Pictures.Cast()) + picture.RelId = null; + + foreach (var table in ws.Tables.Cast()) + table.RelId = null; + } + } + + private void LoadSpreadsheetDocument(SpreadsheetDocument dSpreadsheet) + { + ShapeIdManager = new XLIdManager(); + SetProperties(dSpreadsheet); + + SharedStringItem[] sharedStrings = null; + if (dSpreadsheet.WorkbookPart.GetPartsOfType().Any()) + { + var shareStringPart = dSpreadsheet.WorkbookPart.GetPartsOfType().First(); + sharedStrings = shareStringPart.SharedStringTable.Elements().ToArray(); + } + + if (dSpreadsheet.CustomFilePropertiesPart != null) + { + foreach (var m in dSpreadsheet.CustomFilePropertiesPart.Properties.Elements()) + { + String name = m.Name?.Value; + + if (string.IsNullOrWhiteSpace(name)) + continue; + + if (m.VTLPWSTR != null) + CustomProperties.Add(name, m.VTLPWSTR.Text); + else if (m.VTFileTime != null) + { + CustomProperties.Add(name, + DateTime.ParseExact(m.VTFileTime.Text, "yyyy'-'MM'-'dd'T'HH':'mm':'ssK", + CultureInfo.InvariantCulture)); + } + else if (m.VTDouble != null) + CustomProperties.Add(name, Double.Parse(m.VTDouble.Text, CultureInfo.InvariantCulture)); + else if (m.VTBool != null) + CustomProperties.Add(name, m.VTBool.Text == "true"); + } + } + + var wbProps = dSpreadsheet.WorkbookPart.Workbook.WorkbookProperties; + if (wbProps != null) + Use1904DateSystem = OpenXmlHelper.GetBooleanValueAsBool(wbProps.Date1904, false); + + var wbFilesharing = dSpreadsheet.WorkbookPart.Workbook.FileSharing; + if (wbFilesharing != null) + { + FileSharing.ReadOnlyRecommended = OpenXmlHelper.GetBooleanValueAsBool(wbFilesharing.ReadOnlyRecommended, false); + FileSharing.UserName = wbFilesharing.UserName?.Value; + } + + LoadWorkbookProtection(dSpreadsheet.WorkbookPart.Workbook.WorkbookProtection, this); + + var calculationProperties = dSpreadsheet.WorkbookPart.Workbook.CalculationProperties; + if (calculationProperties != null) + { + var calculateMode = calculationProperties.CalculationMode; + if (calculateMode != null) + CalculateMode = calculateMode.Value.ToClosedXml(); + + var calculationOnSave = calculationProperties.CalculationOnSave; + if (calculationOnSave != null) + CalculationOnSave = calculationOnSave.Value; + + var forceFullCalculation = calculationProperties.ForceFullCalculation; + if (forceFullCalculation != null) + ForceFullCalculation = forceFullCalculation.Value; + + var fullCalculationOnLoad = calculationProperties.FullCalculationOnLoad; + if (fullCalculationOnLoad != null) + FullCalculationOnLoad = fullCalculationOnLoad.Value; + + var fullPrecision = calculationProperties.FullPrecision; + if (fullPrecision != null) + FullPrecision = fullPrecision.Value; + + var referenceMode = calculationProperties.ReferenceMode; + if (referenceMode != null) + ReferenceStyle = referenceMode.Value.ToClosedXml(); + } + + var efp = dSpreadsheet.ExtendedFilePropertiesPart; + if (efp != null && efp.Properties != null) + { + if (efp.Properties.Elements().Any()) + Properties.Company = efp.Properties.GetFirstChild().Text; + + if (efp.Properties.Elements().Any()) + Properties.Manager = efp.Properties.GetFirstChild().Text; + } + + Stylesheet s = null; + if (dSpreadsheet.WorkbookPart.WorkbookStylesPart != null && + dSpreadsheet.WorkbookPart.WorkbookStylesPart.Stylesheet != null) + { + s = dSpreadsheet.WorkbookPart.WorkbookStylesPart.Stylesheet; + } + + NumberingFormats numberingFormats = s == null ? null : s.NumberingFormats; + Fills fills = s == null ? null : s.Fills; + Borders borders = s == null ? null : s.Borders; + Fonts fonts = s == null ? null : s.Fonts; + Int32 dfCount = 0; + Dictionary differentialFormats; + if (s != null && s.DifferentialFormats != null) + differentialFormats = s.DifferentialFormats.Elements().ToDictionary(k => dfCount++); + else + differentialFormats = new Dictionary(); + + var sheets = dSpreadsheet.WorkbookPart.Workbook.Sheets; + Int32 position = 0; + foreach (var dSheet in sheets.OfType()) + { + position++; + var sharedFormulasR1C1 = new Dictionary(); + + var worksheetPart = dSpreadsheet.WorkbookPart.GetPartById(dSheet.Id) as WorksheetPart; + + if (worksheetPart == null) + { + UnsupportedSheets.Add(new UnsupportedSheet { SheetId = dSheet.SheetId.Value, Position = position }); + continue; + } + + var sheetName = dSheet.Name; + + var ws = (XLWorksheet)WorksheetsInternal.Add(sheetName, position); + ws.RelId = dSheet.Id; + ws.SheetId = (Int32)dSheet.SheetId.Value; + + if (dSheet.State != null) + ws.Visibility = dSheet.State.Value.ToClosedXml(); + + ApplyStyle(ws, 0, s, fills, borders, fonts, numberingFormats); + + var styleList = new Dictionary();// {{0, ws.Style}}; + PageSetupProperties pageSetupProperties = null; + + lastRow = 0; + + using (var reader = OpenXmlReader.Create(worksheetPart)) + { + Type[] ignoredElements = new Type[] + { + typeof(CustomSheetViews) // Custom sheet views contain its own auto filter data, and more, which should be ignored for now + }; + + while (reader.Read()) + { + while (ignoredElements.Contains(reader.ElementType)) + reader.ReadNextSibling(); + + if (reader.ElementType == typeof(SheetFormatProperties)) + { + var sheetFormatProperties = (SheetFormatProperties)reader.LoadCurrentElement(); + if (sheetFormatProperties != null) + { + if (sheetFormatProperties.DefaultRowHeight != null) + ws.RowHeight = sheetFormatProperties.DefaultRowHeight; + + ws.RowHeightChanged = (sheetFormatProperties.CustomHeight != null && + sheetFormatProperties.CustomHeight.Value); + + if (sheetFormatProperties.DefaultColumnWidth != null) + { + ws.ColumnWidth = sheetFormatProperties.DefaultColumnWidth; + } + } + } + else if (reader.ElementType == typeof(SheetViews)) + LoadSheetViews((SheetViews)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(MergeCells)) + { + var mergedCells = (MergeCells)reader.LoadCurrentElement(); + if (mergedCells != null) + { + foreach (MergeCell mergeCell in mergedCells.Elements()) + ws.Range(mergeCell.Reference).Merge(false); + } + } + else if (reader.ElementType == typeof(Columns)) + LoadColumns(s, numberingFormats, fills, borders, fonts, ws, + (Columns)reader.LoadCurrentElement()); + else if (reader.ElementType == typeof(Row)) + { + LoadRows(s, numberingFormats, fills, borders, fonts, ws, sharedStrings, sharedFormulasR1C1, + styleList, (Row)reader.LoadCurrentElement()); + } + else if (reader.ElementType == typeof(AutoFilter)) + LoadAutoFilter((AutoFilter)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(SheetProtection)) + LoadSheetProtection((SheetProtection)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(DataValidations)) + LoadDataValidations((DataValidations)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(ConditionalFormatting)) + LoadConditionalFormatting((ConditionalFormatting)reader.LoadCurrentElement(), ws, differentialFormats); + else if (reader.ElementType == typeof(Hyperlinks)) + LoadHyperlinks((Hyperlinks)reader.LoadCurrentElement(), worksheetPart, ws); + else if (reader.ElementType == typeof(PrintOptions)) + LoadPrintOptions((PrintOptions)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(PageMargins)) + LoadPageMargins((PageMargins)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(PageSetup)) + LoadPageSetup((PageSetup)reader.LoadCurrentElement(), ws, pageSetupProperties); + else if (reader.ElementType == typeof(HeaderFooter)) + LoadHeaderFooter((HeaderFooter)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(SheetProperties)) + LoadSheetProperties((SheetProperties)reader.LoadCurrentElement(), ws, out pageSetupProperties); + else if (reader.ElementType == typeof(RowBreaks)) + LoadRowBreaks((RowBreaks)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(ColumnBreaks)) + LoadColumnBreaks((ColumnBreaks)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(WorksheetExtensionList)) + LoadExtensions((WorksheetExtensionList)reader.LoadCurrentElement(), ws); + else if (reader.ElementType == typeof(LegacyDrawing)) + ws.LegacyDrawingId = (reader.LoadCurrentElement() as LegacyDrawing).Id.Value; + } + reader.Close(); + } + + (ws.ConditionalFormats as XLConditionalFormats).ReorderAccordingToOriginalPriority(); + + #region LoadTables + + foreach (var tableDefinitionPart in worksheetPart.TableDefinitionParts) + { + var relId = worksheetPart.GetIdOfPart(tableDefinitionPart); + var dTable = tableDefinitionPart.Table; + + String reference = dTable.Reference.Value; + String tableName = dTable.Name ?? dTable.DisplayName ?? string.Empty; + if (String.IsNullOrWhiteSpace(tableName)) + throw new InvalidDataException("The table name is missing."); + + var xlTable = ws.Range(reference).CreateTable(tableName, false) as XLTable; + xlTable.RelId = relId; + + if (dTable.HeaderRowCount != null && dTable.HeaderRowCount == 0) + { + xlTable._showHeaderRow = false; + //foreach (var tableColumn in dTable.TableColumns.Cast()) + xlTable.AddFields(dTable.TableColumns.Cast().Select(t => GetTableColumnName(t.Name.Value))); + } + else + { + xlTable.InitializeAutoFilter(); + } + + if (dTable.TotalsRowCount != null && dTable.TotalsRowCount.Value > 0) + ((XLTable)xlTable)._showTotalsRow = true; + + if (dTable.TableStyleInfo != null) + { + if (dTable.TableStyleInfo.ShowFirstColumn != null) + xlTable.EmphasizeFirstColumn = dTable.TableStyleInfo.ShowFirstColumn.Value; + if (dTable.TableStyleInfo.ShowLastColumn != null) + xlTable.EmphasizeLastColumn = dTable.TableStyleInfo.ShowLastColumn.Value; + if (dTable.TableStyleInfo.ShowRowStripes != null) + xlTable.ShowRowStripes = dTable.TableStyleInfo.ShowRowStripes.Value; + if (dTable.TableStyleInfo.ShowColumnStripes != null) + xlTable.ShowColumnStripes = dTable.TableStyleInfo.ShowColumnStripes.Value; + if (dTable.TableStyleInfo.Name != null) + { + var theme = XLTableTheme.FromName(dTable.TableStyleInfo.Name.Value); + if (theme != null) + xlTable.Theme = theme; + else + xlTable.Theme = new XLTableTheme(dTable.TableStyleInfo.Name.Value); + } + else + xlTable.Theme = XLTableTheme.None; + } + + if (dTable.AutoFilter != null) + { + xlTable.ShowAutoFilter = true; + LoadAutoFilterColumns(dTable.AutoFilter, xlTable.AutoFilter); + } + else + xlTable.ShowAutoFilter = false; + + if (xlTable.ShowTotalsRow) + { + foreach (var tableColumn in dTable.TableColumns.Cast()) + { + var tableColumnName = GetTableColumnName(tableColumn.Name.Value); + if (tableColumn.TotalsRowFunction != null) + xlTable.Field(tableColumnName).TotalsRowFunction = + tableColumn.TotalsRowFunction.Value.ToClosedXml(); + + if (tableColumn.TotalsRowFormula != null) + xlTable.Field(tableColumnName).TotalsRowFormulaA1 = + tableColumn.TotalsRowFormula.Text; + + if (tableColumn.TotalsRowLabel != null) + xlTable.Field(tableColumnName).TotalsRowLabel = tableColumn.TotalsRowLabel.Value; + } + if (xlTable.AutoFilter != null) + xlTable.AutoFilter.Range = xlTable.Worksheet.Range( + xlTable.RangeAddress.FirstAddress.RowNumber, xlTable.RangeAddress.FirstAddress.ColumnNumber, + xlTable.RangeAddress.LastAddress.RowNumber - 1, xlTable.RangeAddress.LastAddress.ColumnNumber); + } + else if (xlTable.AutoFilter != null) + xlTable.AutoFilter.Range = xlTable.Worksheet.Range(xlTable.RangeAddress); + } + + #endregion LoadTables + + LoadDrawings(worksheetPart, ws); + + #region LoadComments + + if (worksheetPart.WorksheetCommentsPart != null) + { + var root = worksheetPart.WorksheetCommentsPart.Comments; + var authors = root.GetFirstChild().ChildElements.OfType().ToList(); + var comments = root.GetFirstChild().ChildElements.OfType().ToList(); + + // **** MAYBE FUTURE SHAPE SIZE SUPPORT + var shapes = GetCommentShapes(worksheetPart); + + for (var i = 0; i < comments.Count; i++) + { + var c = comments[i]; + + XElement shape = null; + if (i < shapes.Count) + shape = shapes[i]; + + // find cell by reference + var cell = ws.Cell(c.Reference); + + var shapeIdString = shape?.Attribute("id")?.Value; + if (shapeIdString?.StartsWith("_x0000_s") ?? false) + shapeIdString = shapeIdString.Substring(8); + + int? shapeId = int.TryParse(shapeIdString, out int sid) ? (int?)sid : null; + var xlComment = cell.CreateComment(shapeId); + + xlComment.Author = authors[(int)c.AuthorId.Value].InnerText; + ShapeIdManager.Add(xlComment.ShapeId); + + var runs = c.GetFirstChild().Elements(); + foreach (var run in runs) + { + var runProperties = run.RunProperties; + String text = run.Text.InnerText.FixNewLines(); + var rt = xlComment.AddText(text); + LoadFont(runProperties, rt); + } + + if (shape != null) + { + LoadShapeProperties(xlComment, shape); + + var clientData = shape.Elements().First(e => e.Name.LocalName == "ClientData"); + LoadClientData(xlComment, clientData); + + var textBox = shape.Elements().First(e => e.Name.LocalName == "textbox"); + LoadTextBox(xlComment, textBox); + + var alt = shape.Attribute("alt"); + if (alt != null) xlComment.Style.Web.SetAlternateText(alt.Value); + + LoadColorsAndLines(xlComment, shape); + + //var insetmode = (string)shape.Attributes().First(a=> a.Name.LocalName == "insetmode"); + //xlComment.Style.Margins.Automatic = insetmode != null && insetmode.Equals("auto"); + } + } + } + + #endregion LoadComments + } + + var workbook = dSpreadsheet.WorkbookPart.Workbook; + + var bookViews = workbook.BookViews; + if (bookViews != null && bookViews.FirstOrDefault() is WorkbookView workbookView) + { + if (workbookView.ActiveTab == null) + { + Worksheets.First().SetTabActive().Unhide(); + } + else + { + UnsupportedSheet unsupportedSheet = + UnsupportedSheets.FirstOrDefault(us => us.Position == (Int32)(workbookView.ActiveTab.Value + 1)); + if (unsupportedSheet != null) + unsupportedSheet.IsActive = true; + else + { + Worksheet((Int32)(workbookView.ActiveTab.Value + 1)).SetTabActive(); + } + } + } + LoadDefinedNames(workbook); + + #region Pivot tables + + // Delay loading of pivot tables until all sheets have been loaded + foreach (var dSheet in sheets.OfType()) + { + var worksheetPart = dSpreadsheet.WorkbookPart.GetPartById(dSheet.Id) as WorksheetPart; + + if (worksheetPart != null) + { + var ws = (XLWorksheet)WorksheetsInternal.Worksheet(dSheet.Name); + + foreach (var pivotTablePart in worksheetPart.PivotTableParts) + { + var pivotTableCacheDefinitionPart = pivotTablePart.PivotTableCacheDefinitionPart; + var pivotTableDefinition = pivotTablePart.PivotTableDefinition; + + var target = ws.FirstCell(); + if (pivotTableDefinition?.Location?.Reference?.HasValue ?? false) + { + ws.Range(pivotTableDefinition.Location.Reference.Value).Clear(XLClearOptions.All); + target = ws.Range(pivotTableDefinition.Location.Reference.Value).FirstCell(); + } + + IXLRange source = null; + XLPivotTableSourceType sourceType = XLPivotTableSourceType.Range; + if (pivotTableCacheDefinitionPart?.PivotCacheDefinition?.CacheSource?.WorksheetSource != null) + { + // TODO: Implement other sources besides worksheetSource + // But for now assume names and references point directly to a range + var wss = pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheSource.WorksheetSource; + + if (!String.IsNullOrEmpty(wss.Id)) + { + var externalRelationship = pivotTableCacheDefinitionPart.ExternalRelationships.FirstOrDefault(er => er.Id.Equals(wss.Id)); + if (externalRelationship?.IsExternal ?? false) + { + // We don't support external sources + continue; + } + } + + if (wss.Name != null) + { + var table = ws + .Workbook + .Worksheets + .SelectMany(ws1 => ws1.Tables) + .FirstOrDefault(t => t.Name.Equals(wss.Name.Value)); + + if (table != null) + { + sourceType = XLPivotTableSourceType.Table; + source = table; + } + else + { + sourceType = XLPivotTableSourceType.Range; + source = this.Range(wss.Name.Value); + } + } + else + { + sourceType = XLPivotTableSourceType.Range; + + IXLWorksheet sourceSheet; + if (wss.Sheet == null) + sourceSheet = ws; + else if (WorksheetsInternal.TryGetWorksheet(wss.Sheet.Value, out sourceSheet)) + source = this.Range(sourceSheet.Range(wss.Reference.Value).RangeAddress.ToStringRelative(includeSheet: true)); + } + + if (source == null) + continue; + } + + if (target != null && source != null) + { + XLPivotTable pt; + switch (sourceType) + { + case XLPivotTableSourceType.Range: + pt = ws.PivotTables.Add(pivotTableDefinition.Name, target, source) as XLPivotTable; + break; + + case XLPivotTableSourceType.Table: + pt = ws.PivotTables.Add(pivotTableDefinition.Name, target, source as XLTable) as XLPivotTable; + break; + + default: + throw new NotSupportedException($"Pivot table source type {sourceType} is not supported."); + } + + if (!String.IsNullOrWhiteSpace(StringValue.ToString(pivotTableDefinition?.ColumnHeaderCaption ?? String.Empty))) + pt.SetColumnHeaderCaption(StringValue.ToString(pivotTableDefinition.ColumnHeaderCaption)); + + if (!String.IsNullOrWhiteSpace(StringValue.ToString(pivotTableDefinition?.RowHeaderCaption ?? String.Empty))) + pt.SetRowHeaderCaption(StringValue.ToString(pivotTableDefinition.RowHeaderCaption)); + + pt.RelId = worksheetPart.GetIdOfPart(pivotTablePart); + pt.CacheDefinitionRelId = pivotTablePart.GetIdOfPart(pivotTableCacheDefinitionPart); + pt.WorkbookCacheRelId = dSpreadsheet.WorkbookPart.GetIdOfPart(pivotTableCacheDefinitionPart); + + if (pivotTableDefinition.MergeItem != null) pt.MergeAndCenterWithLabels = pivotTableDefinition.MergeItem.Value; + if (pivotTableDefinition.Indent != null) pt.RowLabelIndent = (int)pivotTableDefinition.Indent.Value; + if (pivotTableDefinition.PageOverThenDown != null) pt.FilterAreaOrder = pivotTableDefinition.PageOverThenDown.Value ? XLFilterAreaOrder.OverThenDown : XLFilterAreaOrder.DownThenOver; + if (pivotTableDefinition.PageWrap != null) pt.FilterFieldsPageWrap = (int)pivotTableDefinition.PageWrap.Value; + if (pivotTableDefinition.UseAutoFormatting != null) pt.AutofitColumns = pivotTableDefinition.UseAutoFormatting.Value; + if (pivotTableDefinition.PreserveFormatting != null) pt.PreserveCellFormatting = pivotTableDefinition.PreserveFormatting.Value; + if (pivotTableDefinition.RowGrandTotals != null) pt.ShowGrandTotalsRows = pivotTableDefinition.RowGrandTotals.Value; + if (pivotTableDefinition.ColumnGrandTotals != null) pt.ShowGrandTotalsColumns = pivotTableDefinition.ColumnGrandTotals.Value; + if (pivotTableDefinition.SubtotalHiddenItems != null) pt.FilteredItemsInSubtotals = pivotTableDefinition.SubtotalHiddenItems.Value; + if (pivotTableDefinition.MultipleFieldFilters != null) pt.AllowMultipleFilters = pivotTableDefinition.MultipleFieldFilters.Value; + if (pivotTableDefinition.CustomListSort != null) pt.UseCustomListsForSorting = pivotTableDefinition.CustomListSort.Value; + if (pivotTableDefinition.ShowDrill != null) pt.ShowExpandCollapseButtons = pivotTableDefinition.ShowDrill.Value; + if (pivotTableDefinition.ShowDataTips != null) pt.ShowContextualTooltips = pivotTableDefinition.ShowDataTips.Value; + if (pivotTableDefinition.ShowMemberPropertyTips != null) pt.ShowPropertiesInTooltips = pivotTableDefinition.ShowMemberPropertyTips.Value; + if (pivotTableDefinition.ShowHeaders != null) pt.DisplayCaptionsAndDropdowns = pivotTableDefinition.ShowHeaders.Value; + if (pivotTableDefinition.GridDropZones != null) pt.ClassicPivotTableLayout = pivotTableDefinition.GridDropZones.Value; + if (pivotTableDefinition.ShowEmptyRow != null) pt.ShowEmptyItemsOnRows = pivotTableDefinition.ShowEmptyRow.Value; + if (pivotTableDefinition.ShowEmptyColumn != null) pt.ShowEmptyItemsOnColumns = pivotTableDefinition.ShowEmptyColumn.Value; + if (pivotTableDefinition.ShowItems != null) pt.DisplayItemLabels = pivotTableDefinition.ShowItems.Value; + if (pivotTableDefinition.FieldListSortAscending != null) pt.SortFieldsAtoZ = pivotTableDefinition.FieldListSortAscending.Value; + if (pivotTableDefinition.PrintDrill != null) pt.PrintExpandCollapsedButtons = pivotTableDefinition.PrintDrill.Value; + if (pivotTableDefinition.ItemPrintTitles != null) pt.RepeatRowLabels = pivotTableDefinition.ItemPrintTitles.Value; + if (pivotTableDefinition.FieldPrintTitles != null) pt.PrintTitles = pivotTableDefinition.FieldPrintTitles.Value; + if (pivotTableDefinition.EnableDrill != null) pt.EnableShowDetails = pivotTableDefinition.EnableDrill.Value; + if (pivotTableCacheDefinitionPart.PivotCacheDefinition.SaveData != null) pt.SaveSourceData = pivotTableCacheDefinitionPart.PivotCacheDefinition.SaveData.Value; + + if (pivotTableCacheDefinitionPart.PivotCacheDefinition.MissingItemsLimit != null) + { + if (pivotTableCacheDefinitionPart.PivotCacheDefinition.MissingItemsLimit == 0U) + pt.ItemsToRetainPerField = XLItemsToRetain.None; + else if (pivotTableCacheDefinitionPart.PivotCacheDefinition.MissingItemsLimit == XLHelper.MaxRowNumber) + pt.ItemsToRetainPerField = XLItemsToRetain.Max; + } + + if (pivotTableDefinition.ShowMissing != null && pivotTableDefinition.MissingCaption != null) + pt.EmptyCellReplacement = pivotTableDefinition.MissingCaption.Value; + + if (pivotTableDefinition.ShowError != null && pivotTableDefinition.ErrorCaption != null) + pt.ErrorValueReplacement = pivotTableDefinition.ErrorCaption.Value; + + var pivotTableDefinitionExtensionList = pivotTableDefinition.GetFirstChild(); + var pivotTableDefinitionExtension = pivotTableDefinitionExtensionList?.GetFirstChild(); + var pivotTableDefinition2 = pivotTableDefinitionExtension?.GetFirstChild(); + if (pivotTableDefinition2 != null) + { + if (pivotTableDefinition2.EnableEdit != null) pt.EnableCellEditing = pivotTableDefinition2.EnableEdit.Value; + if (pivotTableDefinition2.HideValuesRow != null) pt.ShowValuesRow = !pivotTableDefinition2.HideValuesRow.Value; + } + + var pivotTableStyle = pivotTableDefinition.GetFirstChild(); + if (pivotTableStyle != null) + { + if (pivotTableStyle.Name != null) + pt.Theme = (XLPivotTableTheme)Enum.Parse(typeof(XLPivotTableTheme), pivotTableStyle.Name); + else + pt.Theme = XLPivotTableTheme.None; + + pt.ShowRowHeaders = OpenXmlHelper.GetBooleanValueAsBool(pivotTableStyle.ShowRowHeaders, false); + pt.ShowColumnHeaders = OpenXmlHelper.GetBooleanValueAsBool(pivotTableStyle.ShowColumnHeaders, false); + pt.ShowRowStripes = OpenXmlHelper.GetBooleanValueAsBool(pivotTableStyle.ShowRowStripes, false); + pt.ShowColumnStripes = OpenXmlHelper.GetBooleanValueAsBool(pivotTableStyle.ShowColumnStripes, false); + } + + // Subtotal configuration + if (pivotTableDefinition.PivotFields.Cast().All(pf => (pf.DefaultSubtotal == null || pf.DefaultSubtotal.Value) + && (pf.SubtotalTop == null || pf.SubtotalTop == true))) + pt.SetSubtotals(XLPivotSubtotals.AtTop); + else if (pivotTableDefinition.PivotFields.Cast().All(pf => (pf.DefaultSubtotal == null || pf.DefaultSubtotal.Value) + && (pf.SubtotalTop != null && pf.SubtotalTop.Value == false))) + pt.SetSubtotals(XLPivotSubtotals.AtBottom); + else + pt.SetSubtotals(XLPivotSubtotals.DoNotShow); + + // Row labels + if (pivotTableDefinition.RowFields != null) + { + foreach (var rf in pivotTableDefinition.RowFields.Cast()) + { + if (rf.Index < pivotTableDefinition.PivotFields.Count) + { + IXLPivotField pivotField = null; + if (rf.Index.Value == -2) + pivotField = pt.RowLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + else + { + var pf = pivotTableDefinition.PivotFields.ElementAt(rf.Index.Value) as PivotField; + if (pf == null) + continue; + + var cacheField = pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheFields.ElementAt(rf.Index.Value) as CacheField; + if (pt.SourceRangeFieldsAvailable.Contains(cacheField.Name?.Value)) + pivotField = pf.Name != null + ? pt.RowLabels.Add(cacheField.Name, pf.Name.Value) + : pt.RowLabels.Add(cacheField.Name.Value); + else + continue; + + if (pivotField != null) + { + LoadFieldOptions(pf, pivotField); + LoadSubtotals(pf, pivotField); + + if (pf.SortType != null) + { + pivotField.SetSort((XLPivotSortType)pf.SortType.Value); + } + } + } + } + } + } + + // Column labels + if (pivotTableDefinition.ColumnFields != null) + { + foreach (var cf in pivotTableDefinition.ColumnFields.Cast()) + { + IXLPivotField pivotField = null; + if (cf.Index.Value == -2) + pivotField = pt.ColumnLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + else if (cf.Index < pivotTableDefinition.PivotFields.Count) + { + var pf = pivotTableDefinition.PivotFields.ElementAt(cf.Index.Value) as PivotField; + if (pf == null) + continue; + + var cacheField = pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheFields.ElementAt(cf.Index.Value) as CacheField; + if (pt.SourceRangeFieldsAvailable.Contains(cacheField.Name?.Value)) + pivotField = pf.Name != null + ? pt.ColumnLabels.Add(cacheField.Name, pf.Name.Value) + : pt.ColumnLabels.Add(cacheField.Name.Value); + else + continue; + + if (pivotField != null) + { + LoadFieldOptions(pf, pivotField); + LoadSubtotals(pf, pivotField); + + if (pf.SortType != null) + { + pivotField.SetSort((XLPivotSortType)pf.SortType.Value); + } + } + } + } + } + + // Values + if (pivotTableDefinition.DataFields != null) + { + foreach (var df in pivotTableDefinition.DataFields.Cast()) + { + IXLPivotValue pivotValue = null; + if ((int)df.Field.Value == -2) + pivotValue = pt.Values.Add(XLConstants.PivotTable.ValuesSentinalLabel); + else if (df.Field.Value < pivotTableDefinition.PivotFields.Count) + { + var pf = pivotTableDefinition.PivotFields.ElementAt((int)df.Field.Value) as PivotField; + if (pf == null) + continue; + + var cacheField = pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheFields.ElementAt((int)df.Field.Value) as CacheField; + + if (pf.Name != null) + pivotValue = pt.Values.Add(pf.Name.Value, df.Name.Value); + else if (cacheField.Name != null && pt.SourceRangeFieldsAvailable.Contains(cacheField.Name)) + pivotValue = pt.Values.Add(cacheField.Name.Value, df.Name.Value); + else + continue; + + if (df.NumberFormatId != null) pivotValue.NumberFormat.SetNumberFormatId((int)df.NumberFormatId.Value); + if (df.Subtotal != null) pivotValue = pivotValue.SetSummaryFormula(df.Subtotal.Value.ToClosedXml()); + if (df.ShowDataAs != null) + { + var calculation = df.ShowDataAs.Value.ToClosedXml(); + pivotValue = pivotValue.SetCalculation(calculation); + } + + if (df.BaseField?.Value != null) + { + var col = pt.SourceRange.Column(df.BaseField.Value + 1); + + var items = col.CellsUsed() + .Select(c => c.Value) + .Skip(1) // Skip header column + .Distinct().ToList(); + + pivotValue.BaseField = col.FirstCell().GetValue(); + + if (df.BaseItem?.Value != null) + { + var bi = (int)df.BaseItem.Value; + if (bi.Between(0, items.Count - 1)) + pivotValue.BaseItem = items[(int)df.BaseItem.Value].ToString(); + } + } + } + } + } + + // Filters + if (pivotTableDefinition.PageFields != null) + { + foreach (var pageField in pivotTableDefinition.PageFields.Cast()) + { + var pf = pivotTableDefinition.PivotFields.ElementAt(pageField.Field.Value) as PivotField; + if (pf == null) + continue; + + var cacheField = pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheFields.ElementAt(pageField.Field.Value) as CacheField; + + if (!pt.SourceRangeFieldsAvailable.Contains(cacheField.Name?.Value)) + continue; + + var filterName = pf.Name?.Value ?? cacheField.Name?.Value; + + IXLPivotField rf; + if (pageField.Name?.Value != null) + rf = pt.ReportFilters.Add(filterName, pageField.Name.Value); + else + rf = pt.ReportFilters.Add(filterName); + + var openXmlItems = new List(); + if ((pageField.Item?.HasValue ?? false) + && pf.Items.Any() && cacheField.SharedItems.Any()) + { + if (!(pf.Items.ElementAt(Convert.ToInt32(pageField.Item.Value)) is Item item)) + continue; + + openXmlItems.Add(item); + } + else if (OpenXmlHelper.GetBooleanValueAsBool(pf.MultipleItemSelectionAllowed, false)) + { + openXmlItems.AddRange(pf.Items.Cast()); + } + + foreach (var item in openXmlItems) + { + if (!OpenXmlHelper.GetBooleanValueAsBool(item.Hidden, false) + && (item.Index?.HasValue ?? false)) + { + var sharedItem = cacheField.SharedItems.ElementAt(Convert.ToInt32((uint)item.Index)); + // https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.shareditems.aspx + switch (sharedItem) + { + case NumberItem numberItem: + rf.AddSelectedValue(Convert.ToDouble(numberItem.Val.Value)); + break; + + case DateTimeItem dateTimeItem: + rf.AddSelectedValue(Convert.ToDateTime(dateTimeItem.Val.Value)); + break; + + case BooleanItem booleanItem: + rf.AddSelectedValue(Convert.ToBoolean(booleanItem.Val.Value)); + break; + + case StringItem stringItem: + rf.AddSelectedValue(stringItem.Val.Value); + break; + + case MissingItem missingItem: + case ErrorItem errorItem: + // Ignore missing and error items + break; + + default: + throw new NotImplementedException(); + } + } + } + } + + pt.TargetCell = pt.TargetCell.CellAbove(pt.ReportFilters.Count() + 1); + } + + LoadPivotStyleFormats(pt, pivotTableDefinition, pivotTableCacheDefinitionPart.PivotCacheDefinition, differentialFormats); + } + } + } + } + + #endregion Pivot tables + } + + private void LoadPivotStyleFormats(XLPivotTable pt, PivotTableDefinition ptd, PivotCacheDefinition pcd, Dictionary differentialFormats) + { + if (ptd.Formats == null) + return; + + foreach (var format in ptd.Formats.OfType()) + { + var pivotArea = format.PivotArea; + if (pivotArea == null) + continue; + + var type = pivotArea.Type ?? PivotAreaValues.Normal; + var dataOnly = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.DataOnly, true); + var labelOnly = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.LabelOnly, false); + + if (dataOnly && labelOnly) + throw new InvalidOperationException("Cannot have dataOnly and labelOnly both set to true."); + + XLPivotStyleFormat styleFormat; + + if (pivotArea.Field == null && !(pivotArea.PivotAreaReferences?.OfType()?.Any() ?? false)) + { + // If the pivot field is null and doesn't have children (references), we assume this format is a grand total + // Example: + // + + var appliesTo = XLPivotStyleFormatElement.All; + if (dataOnly) + appliesTo = XLPivotStyleFormatElement.Data; + else if (labelOnly) + appliesTo = XLPivotStyleFormatElement.Label; + + var isRow = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.GrandRow, false); + var isColumn = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.GrandColumn, false); + + // Either of the two should be true, else this is an unsupported format + if (!isRow && !isColumn) + continue; + //throw new NotImplementedException(); + + if (isRow) + styleFormat = pt.StyleFormats.RowGrandTotalFormats.ForElement(appliesTo) as XLPivotStyleFormat; + else + styleFormat = pt.StyleFormats.ColumnGrandTotalFormats.ForElement(appliesTo) as XLPivotStyleFormat; + } + else + { + Int32 fieldIndex; + Boolean defaultSubtotal = false; + + if (pivotArea.Field != null) + fieldIndex = (Int32)pivotArea.Field; + else if (pivotArea.PivotAreaReferences?.OfType()?.Any() ?? false) + { + // The field we want does NOT have any children + var r = pivotArea.PivotAreaReferences.OfType().FirstOrDefault(r1 => !r1.Any()); + if (r == null) + continue; + + fieldIndex = Convert.ToInt32((UInt32)r.Field); + defaultSubtotal = OpenXmlHelper.GetBooleanValueAsBool(r.DefaultSubtotal, false); + } + else + throw new NotImplementedException(); + + XLPivotField field = null; + if (fieldIndex == -2) + { + var axis = pivotArea.Axis.Value; + if (axis == PivotTableAxisValues.AxisRow) + field = (XLPivotField)pt.RowLabels.Single(f => f.SourceName == "{{Values}}"); + else if (axis == PivotTableAxisValues.AxisColumn) + field = (XLPivotField)pt.ColumnLabels.Single(f => f.SourceName == "{{Values}}"); + else + continue; + } + else + { + var fieldName = pt.SourceRangeFieldsAvailable.ElementAt(fieldIndex); + field = (XLPivotField)pt.ImplementedFields.SingleOrDefault(f => f.SourceName.Equals(fieldName)); + + if (field is null) + continue; + } + + if (defaultSubtotal) + { + // Subtotal format + // Example: + // + // + // + // + // + + styleFormat = field.StyleFormats.Subtotal as XLPivotStyleFormat; + } + else if (type == PivotAreaValues.Button) + { + // Header format + // Example: + // + styleFormat = field.StyleFormats.Header as XLPivotStyleFormat; + } + else if (labelOnly) + { + // Label format + // Example: + // + // + // + // + // + styleFormat = field.StyleFormats.Label as XLPivotStyleFormat; + } + else + { + // Assume DataValues format + // Example: + // + // + // + // + // + // + // + // + // + // + // + styleFormat = field.StyleFormats.DataValuesFormat as XLPivotStyleFormat; + + foreach (var reference in pivotArea.PivotAreaReferences.OfType()) + { + fieldIndex = unchecked((int)reference.Field.Value); + if (field.Offset == fieldIndex) + continue; // already handled + + var fieldItem = reference.OfType().First(); + var fieldItemValue = (int)fieldItem.Val.Value; + + if (fieldIndex == -2) + { + styleFormat = (styleFormat as XLPivotValueStyleFormat) + .ForValueField(pt.Values.ElementAt(fieldItemValue)) + as XLPivotValueStyleFormat; + } + else + { + var additionalFieldName = pt.SourceRangeFieldsAvailable.ElementAt(fieldIndex); + var additionalField = pt.ImplementedFields + .Single(f => f.SourceName == additionalFieldName); + + var cacheField = pcd.CacheFields.OfType() + .FirstOrDefault(cf => cf.Name == additionalFieldName); + + Predicate predicate = null; + if ((cacheField?.SharedItems?.Any() ?? false) + && fieldItemValue < cacheField.SharedItems.Count) + { + var value = cacheField.SharedItems.OfType().ElementAt(fieldItemValue).Val?.Value; + predicate = o => o.ToString() == value; + } + + styleFormat = (styleFormat as XLPivotValueStyleFormat) + .AndWith(additionalField, predicate) + as XLPivotValueStyleFormat; + } + } + } + + styleFormat.AreaType = type.Value.ToClosedXml(); + styleFormat.Outline = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.Outline, true); + styleFormat.CollapsedLevelsAreSubtotals = OpenXmlHelper.GetBooleanValueAsBool(pivotArea.CollapsedLevelsAreSubtotals, false); + } + + IXLStyle style = XLStyle.Default; + if (format.FormatId != null) + { + var df = differentialFormats[(Int32)format.FormatId.Value]; + LoadFont(df.Font, style.Font); + LoadFill(df.Fill, style.Fill, differentialFillFormat: true); + LoadBorder(df.Border, style.Border); + LoadNumberFormat(df.NumberingFormat, style.NumberFormat); + } + + styleFormat.Style = style; + } + } + + private static void LoadFieldOptions(PivotField pf, IXLPivotField pivotField) + { + if (pf.SubtotalCaption != null) pivotField.SubtotalCaption = pf.SubtotalCaption; + if (pf.IncludeNewItemsInFilter != null) pivotField.IncludeNewItemsInFilter = pf.IncludeNewItemsInFilter.Value; + if (pf.Outline != null) pivotField.Outline = pf.Outline.Value; + if (pf.Compact != null) pivotField.Compact = pf.Compact.Value; + if (pf.InsertBlankRow != null) pivotField.InsertBlankLines = pf.InsertBlankRow.Value; + pivotField.ShowBlankItems = OpenXmlHelper.GetBooleanValueAsBool(pf.ShowAll, true); + if (pf.InsertPageBreak != null) pivotField.InsertPageBreaks = pf.InsertPageBreak.Value; + if (pf.SubtotalTop != null) pivotField.SubtotalsAtTop = pf.SubtotalTop.Value; + if (pf.AllDrilled != null) pivotField.Collapsed = !pf.AllDrilled.Value; + + var pivotFieldExtensionList = pf.GetFirstChild(); + var pivotFieldExtension = pivotFieldExtensionList?.GetFirstChild(); + var field2010 = pivotFieldExtension?.GetFirstChild(); + if (field2010?.FillDownLabels != null) pivotField.RepeatItemLabels = field2010.FillDownLabels.Value; + } + + private static void LoadSubtotals(PivotField pf, IXLPivotField pivotField) + { + if (pf.AverageSubTotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Average); + if (pf.CountASubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Count); + if (pf.CountSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.CountNumbers); + if (pf.MaxSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Maximum); + if (pf.MinSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Minimum); + if (pf.ApplyStandardDeviationPInSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.PopulationStandardDeviation); + if (pf.ApplyVariancePInSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.PopulationVariance); + if (pf.ApplyProductInSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Product); + if (pf.ApplyStandardDeviationInSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.StandardDeviation); + if (pf.SumSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Sum); + if (pf.ApplyVarianceInSubtotal != null) + pivotField.AddSubtotal(XLSubtotalFunction.Variance); + + if (pf.Items?.Any() ?? false) + { + var items = pf.Items.OfType().Where(i => i.Index != null && i.Index.HasValue); + if (!items.Any(i => i.HideDetails == null || BooleanValue.ToBoolean(i.HideDetails))) + pivotField.SetCollapsed(); + } + } + + private void LoadDrawings(WorksheetPart wsPart, IXLWorksheet ws) + { + if (wsPart.DrawingsPart != null) + { + var drawingsPart = wsPart.DrawingsPart; + + foreach (var anchor in drawingsPart.WorksheetDrawing.ChildElements) + { + var imgId = GetImageRelIdFromAnchor(anchor); + + //If imgId is null, we're probably dealing with a TextBox (or another shape) instead of a picture + if (imgId == null) continue; + + var imagePart = drawingsPart.GetPartById(imgId); + using (var stream = imagePart.GetStream()) + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + var vsdp = GetPropertiesFromAnchor(anchor); + + var picture = (ws as XLWorksheet).AddPicture(ms, vsdp.Name, Convert.ToInt32(vsdp.Id.Value)) as XLPicture; + picture.RelId = imgId; + + Xdr.ShapeProperties spPr = anchor.Descendants().First(); + picture.Placement = XLPicturePlacement.FreeFloating; + + if (spPr?.Transform2D?.Extents?.Cx.HasValue ?? false) + picture.Width = ConvertFromEnglishMetricUnits(spPr.Transform2D.Extents.Cx, GraphicsUtils.Graphics.DpiX); + + if (spPr?.Transform2D?.Extents?.Cy.HasValue ?? false) + picture.Height = ConvertFromEnglishMetricUnits(spPr.Transform2D.Extents.Cy, GraphicsUtils.Graphics.DpiY); + + if (anchor is Xdr.AbsoluteAnchor) + { + var absoluteAnchor = anchor as Xdr.AbsoluteAnchor; + picture.MoveTo( + ConvertFromEnglishMetricUnits(absoluteAnchor.Position.X.Value, GraphicsUtils.Graphics.DpiX), + ConvertFromEnglishMetricUnits(absoluteAnchor.Position.Y.Value, GraphicsUtils.Graphics.DpiY) + ); + } + else if (anchor is Xdr.OneCellAnchor) + { + var oneCellAnchor = anchor as Xdr.OneCellAnchor; + var from = LoadMarker(ws, oneCellAnchor.FromMarker); + picture.MoveTo(from.Cell, from.Offset); + } + else if (anchor is Xdr.TwoCellAnchor) + { + var twoCellAnchor = anchor as Xdr.TwoCellAnchor; + var from = LoadMarker(ws, twoCellAnchor.FromMarker); + var to = LoadMarker(ws, twoCellAnchor.ToMarker); + + if (twoCellAnchor.EditAs == null || !twoCellAnchor.EditAs.HasValue || twoCellAnchor.EditAs.Value == Xdr.EditAsValues.TwoCell) + { + picture.MoveTo(from.Cell, from.Offset, to.Cell, to.Offset); + } + else if (twoCellAnchor.EditAs.Value == Xdr.EditAsValues.Absolute) + { + var shapeProperties = twoCellAnchor.Descendants().FirstOrDefault(); + if (shapeProperties != null) + { + picture.MoveTo( + ConvertFromEnglishMetricUnits(spPr.Transform2D.Offset.X, GraphicsUtils.Graphics.DpiX), + ConvertFromEnglishMetricUnits(spPr.Transform2D.Offset.Y, GraphicsUtils.Graphics.DpiY) + ); + } + } + else if (twoCellAnchor.EditAs.Value == Xdr.EditAsValues.OneCell) + { + picture.MoveTo(from.Cell, from.Offset); + } + } + } + } + } + } + + private static Int32 ConvertFromEnglishMetricUnits(long emu, float resolution) + { + return Convert.ToInt32(emu * resolution / 914400); + } + + private static XLMarker LoadMarker(IXLWorksheet ws, Xdr.MarkerType marker) + { + var row = Math.Min(XLHelper.MaxRowNumber, Math.Max(1, Convert.ToInt32(marker.RowId.InnerText) + 1)); + var column = Math.Min(XLHelper.MaxColumnNumber, Math.Max(1, Convert.ToInt32(marker.ColumnId.InnerText) + 1)); + return new XLMarker( + ws.Cell(row, column), + new Point( + ConvertFromEnglishMetricUnits(Convert.ToInt32(marker.ColumnOffset.InnerText), GraphicsUtils.Graphics.DpiX), + ConvertFromEnglishMetricUnits(Convert.ToInt32(marker.RowOffset.InnerText), GraphicsUtils.Graphics.DpiY) + ) + ); + } + + #region Comment Helpers + + private static IList GetCommentShapes(WorksheetPart worksheetPart) + { + // Cannot get this to return Vml.Shape elements + foreach (var vmlPart in worksheetPart.VmlDrawingParts) + { + using (var stream = vmlPart.GetStream(FileMode.Open)) + { + var xdoc = XDocumentExtensions.Load(stream); + if (xdoc == null) + continue; + + var root = xdoc.Root.Element("xml") ?? xdoc.Root; + + if (root == null) + continue; + + var shapes = root + .Elements(XName.Get("shape", "urn:schemas-microsoft-com:vml")) + .Where(e => new[] + { + "#" + XLConstants.Comment.ShapeTypeId , + "#" + XLConstants.Comment.AlternateShapeTypeId + }.Contains(e.Attribute("type")?.Value)) + .ToList(); + + if (shapes != null) + return shapes; + } + } + + throw new ArgumentException("Could not load comments file"); + } + + #endregion Comment Helpers + + private String GetTableColumnName(string name) + { + return name.Replace("_x000a_", Environment.NewLine).Replace("_x005f_x000a_", "_x000a_"); + } + + // This may be part of XLHelper or XLColor + // Leaving it here for now. Can't decide what to call it and where to put it. + private XLColor ExtractColor(String color) + { + if (color.IndexOf("[") >= 0) + { + int start = color.IndexOf("[") + 1; + int end = color.IndexOf("]", start); + return XLColor.FromIndex(Int32.Parse(color.Substring(start, end - start))); + } + else + { + return XLColor.FromHtml(color); + } + } + + private void LoadColorsAndLines(IXLDrawing drawing, XElement shape) + { + var strokeColor = shape.Attribute("strokecolor"); + if (strokeColor != null) drawing.Style.ColorsAndLines.LineColor = ExtractColor(strokeColor.Value); + + var strokeWeight = shape.Attribute("strokeweight"); + if (strokeWeight != null && TryGetPtValue(strokeWeight.Value, out var lineWeight)) + drawing.Style.ColorsAndLines.LineWeight = lineWeight; + + var fillColor = shape.Attribute("fillcolor"); + if (fillColor != null && !fillColor.Value.ToLower().Contains("infobackground")) drawing.Style.ColorsAndLines.FillColor = ExtractColor(fillColor.Value); + + var fill = shape.Elements().FirstOrDefault(e => e.Name.LocalName == "fill"); + if (fill != null) + { + var opacity = fill.Attribute("opacity"); + if (opacity != null) + { + String opacityVal = opacity.Value; + if (opacityVal.EndsWith("f")) + drawing.Style.ColorsAndLines.FillTransparency = + Double.Parse(opacityVal.Substring(0, opacityVal.Length - 1), CultureInfo.InvariantCulture) / 65536.0; + else + drawing.Style.ColorsAndLines.FillTransparency = Double.Parse(opacityVal, CultureInfo.InvariantCulture); + } + } + + var stroke = shape.Elements().FirstOrDefault(e => e.Name.LocalName == "stroke"); + if (stroke != null) + { + var opacity = stroke.Attribute("opacity"); + if (opacity != null) + { + String opacityVal = opacity.Value; + if (opacityVal.EndsWith("f")) + drawing.Style.ColorsAndLines.LineTransparency = + Double.Parse(opacityVal.Substring(0, opacityVal.Length - 1), CultureInfo.InvariantCulture) / 65536.0; + else + drawing.Style.ColorsAndLines.LineTransparency = Double.Parse(opacityVal, CultureInfo.InvariantCulture); + } + + var dashStyle = stroke.Attribute("dashstyle"); + if (dashStyle != null) + { + String dashStyleVal = dashStyle.Value.ToLower(); + if (dashStyleVal == "1 1" || dashStyleVal == "shortdot") + { + var endCap = stroke.Attribute("endcap"); + if (endCap != null && endCap.Value == "round") + drawing.Style.ColorsAndLines.LineDash = XLDashStyle.RoundDot; + else + drawing.Style.ColorsAndLines.LineDash = XLDashStyle.SquareDot; + } + else + { + switch (dashStyleVal) + { + case "dash": drawing.Style.ColorsAndLines.LineDash = XLDashStyle.Dash; break; + case "dashdot": drawing.Style.ColorsAndLines.LineDash = XLDashStyle.DashDot; break; + case "longdash": drawing.Style.ColorsAndLines.LineDash = XLDashStyle.LongDash; break; + case "longdashdot": drawing.Style.ColorsAndLines.LineDash = XLDashStyle.LongDashDot; break; + case "longdashdotdot": drawing.Style.ColorsAndLines.LineDash = XLDashStyle.LongDashDotDot; break; + } + } + } + + var lineStyle = stroke.Attribute("linestyle"); + if (lineStyle != null) + { + String lineStyleVal = lineStyle.Value.ToLower(); + switch (lineStyleVal) + { + case "single": drawing.Style.ColorsAndLines.LineStyle = XLLineStyle.Single; break; + case "thickbetweenthin": drawing.Style.ColorsAndLines.LineStyle = XLLineStyle.ThickBetweenThin; break; + case "thickthin": drawing.Style.ColorsAndLines.LineStyle = XLLineStyle.ThickThin; break; + case "thinthick": drawing.Style.ColorsAndLines.LineStyle = XLLineStyle.ThinThick; break; + case "thinthin": drawing.Style.ColorsAndLines.LineStyle = XLLineStyle.ThinThin; break; + } + } + } + } + + private void LoadTextBox(IXLDrawing xlDrawing, XElement textBox) + { + var attStyle = textBox.Attribute("style"); + if (attStyle != null) LoadTextBoxStyle(xlDrawing, attStyle); + + var attInset = textBox.Attribute("inset"); + if (attInset != null) LoadTextBoxInset(xlDrawing, attInset); + } + + private void LoadTextBoxInset(IXLDrawing xlDrawing, XAttribute attInset) + { + var split = attInset.Value.Split(','); + xlDrawing.Style.Margins.Left = GetInsetValue(split[0]); + xlDrawing.Style.Margins.Top = GetInsetValue(split[1]); + xlDrawing.Style.Margins.Right = GetInsetValue(split[2]); + xlDrawing.Style.Margins.Bottom = GetInsetValue(split[3]); + } + + private double GetInsetValue(string value) + { + String v = value.Trim(); + if (v.EndsWith("pt")) + return Double.Parse(v.Substring(0, v.Length - 2), CultureInfo.InvariantCulture) / 72.0; + else + return Double.Parse(v.Substring(0, v.Length - 2), CultureInfo.InvariantCulture); + } + + private static void LoadTextBoxStyle(IXLDrawing xlDrawing, XAttribute attStyle) + { + var style = attStyle.Value; + var attributes = style.Split(';'); + foreach (String pair in attributes) + { + var split = pair.Split(':'); + if (split.Length != 2) continue; + + var attribute = split[0].Trim().ToLower(); + var value = split[1].Trim(); + Boolean isVertical = false; + switch (attribute) + { + case "mso-fit-shape-to-text": xlDrawing.Style.Size.SetAutomaticSize(value.Equals("t")); break; + case "mso-layout-flow-alt": + if (value.Equals("bottom-to-top")) xlDrawing.Style.Alignment.SetOrientation(XLDrawingTextOrientation.BottomToTop); + else if (value.Equals("top-to-bottom")) xlDrawing.Style.Alignment.SetOrientation(XLDrawingTextOrientation.Vertical); + break; + + case "layout-flow": isVertical = value.Equals("vertical"); break; + case "mso-direction-alt": if (value == "auto") xlDrawing.Style.Alignment.Direction = XLDrawingTextDirection.Context; break; + case "direction": if (value == "RTL") xlDrawing.Style.Alignment.Direction = XLDrawingTextDirection.RightToLeft; break; + } + if (isVertical && xlDrawing.Style.Alignment.Orientation == XLDrawingTextOrientation.LeftToRight) + xlDrawing.Style.Alignment.Orientation = XLDrawingTextOrientation.TopToBottom; + } + } + + private void LoadClientData(IXLDrawing drawing, XElement clientData) + { + var anchor = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "Anchor"); + if (anchor != null) LoadClientDataAnchor(drawing, anchor); + + LoadDrawingPositioning(drawing, clientData); + LoadDrawingProtection(drawing, clientData); + + var visible = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "Visible"); + drawing.Visible = visible != null && + (string.IsNullOrEmpty(visible.Value) || + visible.Value.StartsWith("t", StringComparison.OrdinalIgnoreCase)); + + LoadDrawingHAlignment(drawing, clientData); + LoadDrawingVAlignment(drawing, clientData); + } + + private void LoadDrawingHAlignment(IXLDrawing drawing, XElement clientData) + { + var textHAlign = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "TextHAlign"); + if (textHAlign != null) + drawing.Style.Alignment.Horizontal = (XLDrawingHorizontalAlignment)Enum.Parse(typeof(XLDrawingHorizontalAlignment), textHAlign.Value.ToProper()); + } + + private void LoadDrawingVAlignment(IXLDrawing drawing, XElement clientData) + { + var textVAlign = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "TextVAlign"); + if (textVAlign != null) + drawing.Style.Alignment.Vertical = (XLDrawingVerticalAlignment)Enum.Parse(typeof(XLDrawingVerticalAlignment), textVAlign.Value.ToProper()); + } + + private void LoadDrawingProtection(IXLDrawing drawing, XElement clientData) + { + var lockedElement = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "Locked"); + var lockTextElement = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "LockText"); + Boolean locked = lockedElement != null && lockedElement.Value.ToLower() == "true"; + Boolean lockText = lockTextElement != null && lockTextElement.Value.ToLower() == "true"; + drawing.Style.Protection.Locked = locked; + drawing.Style.Protection.LockText = lockText; + } + + private static void LoadDrawingPositioning(IXLDrawing drawing, XElement clientData) + { + var moveWithCellsElement = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "MoveWithCells"); + var sizeWithCellsElement = clientData.Elements().FirstOrDefault(e => e.Name.LocalName == "SizeWithCells"); + Boolean moveWithCells = !(moveWithCellsElement != null && moveWithCellsElement.Value.ToLower() == "true"); + Boolean sizeWithCells = !(sizeWithCellsElement != null && sizeWithCellsElement.Value.ToLower() == "true"); + if (moveWithCells && !sizeWithCells) + drawing.Style.Properties.Positioning = XLDrawingAnchor.MoveWithCells; + else if (moveWithCells && sizeWithCells) + drawing.Style.Properties.Positioning = XLDrawingAnchor.MoveAndSizeWithCells; + else + drawing.Style.Properties.Positioning = XLDrawingAnchor.Absolute; + } + + private static void LoadClientDataAnchor(IXLDrawing drawing, XElement anchor) + { + var location = anchor.Value.Split(','); + drawing.Position.Column = int.Parse(location[0]) + 1; + drawing.Position.ColumnOffset = Double.Parse(location[1], CultureInfo.InvariantCulture) / 7.5; + drawing.Position.Row = int.Parse(location[2]) + 1; + drawing.Position.RowOffset = Double.Parse(location[3], CultureInfo.InvariantCulture); + } + + private void LoadShapeProperties(IXLDrawing xlDrawing, XElement shape) + { + if (shape.Attribute("style") == null) + return; + + foreach (var attributePair in shape.Attribute("style").Value.Split(';')) + { + var split = attributePair.Split(':'); + if (split.Length != 2) continue; + + var attribute = split[0].Trim().ToLower(); + var value = split[1].Trim(); + + switch (attribute) + { + case "visibility": xlDrawing.Visible = string.Equals("visible", value, StringComparison.OrdinalIgnoreCase); break; + case "width": + if (TryGetPtValue(value, out var ptWidth)) + { + xlDrawing.Style.Size.Width = ptWidth / 7.5; + } + break; + + case "height": + if (TryGetPtValue(value, out var ptHeight)) + { + xlDrawing.Style.Size.Height = ptHeight; + } + break; + + case "z-index": + if (Int32.TryParse(value, out var zOrder)) + { + xlDrawing.ZOrder = zOrder; + } + break; + } + } + } + + private readonly Dictionary knownUnits = new Dictionary + { + {"pt", 1.0}, + {"in", 72.0}, + {"mm", 72.0/25.4} + }; + + private bool TryGetPtValue(string value, out double result) + { + var knownUnit = knownUnits.FirstOrDefault(ku => value.Contains(ku.Key)); + + if (knownUnit.Key == null) + return Double.TryParse(value, out result); + + value = value.Replace(knownUnit.Key, String.Empty); + + if (Double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + result *= knownUnit.Value; + return true; + } + + result = 0d; + return false; + } + + private void LoadDefinedNames(Workbook workbook) + { + if (workbook.DefinedNames == null) return; + + foreach (var definedName in workbook.DefinedNames.OfType()) + { + var name = definedName.Name; + var visible = true; + if (definedName.Hidden != null) visible = !BooleanValue.ToBoolean(definedName.Hidden); + + var localSheetId = -1; + if (definedName.LocalSheetId?.HasValue ?? false) localSheetId = Convert.ToInt32(definedName.LocalSheetId.Value); + + if (name == "_xlnm.Print_Area") + { + var fixedNames = validateDefinedNames(definedName.Text.Split(',')); + foreach (string area in fixedNames) + { + if (area.Contains("[")) + { + var ws = Worksheets.FirstOrDefault(w => (w as XLWorksheet).SheetId == (localSheetId + 1)); + if (ws != null) + { + ws.PageSetup.PrintAreas.Add(area); + } + } + else + { + ParseReference(area, out String sheetName, out String sheetArea); + if (!(sheetArea.Equals("#REF") || sheetArea.EndsWith("#REF!") || sheetArea.Length == 0 || sheetName.Length == 0)) + WorksheetsInternal.Worksheet(sheetName).PageSetup.PrintAreas.Add(sheetArea); + } + } + } + else if (name == "_xlnm.Print_Titles") + { + LoadPrintTitles(definedName); + } + else + { + string text = definedName.Text; + + var comment = definedName.Comment; + if (localSheetId == -1) + { + if (NamedRanges.All(nr => nr.Name != name)) + (NamedRanges as XLNamedRanges).Add(name, text, comment, validateName: false, validateRangeAddress: false).Visible = visible; + } + else + { + if (Worksheet(localSheetId + 1).NamedRanges.All(nr => nr.Name != name)) + (Worksheet(localSheetId + 1).NamedRanges as XLNamedRanges).Add(name, text, comment, validateName: false, validateRangeAddress: false).Visible = visible; + } + } + } + } + + private static Regex definedNameRegex = new Regex(@"\A'.*'!.*\z", RegexOptions.Compiled); + + private IEnumerable validateDefinedNames(IEnumerable definedNames) + { + var sb = new StringBuilder(); + foreach (string testName in definedNames) + { + if (sb.Length > 0) + sb.Append(','); + + sb.Append(testName); + + Match matchedValidPattern = definedNameRegex.Match(sb.ToString()); + if (matchedValidPattern.Success) + { + yield return sb.ToString(); + sb = new StringBuilder(); + } + } + + if (sb.Length > 0) + yield return sb.ToString(); + } + + private void LoadPrintTitles(DefinedName definedName) + { + var areas = validateDefinedNames(definedName.Text.Split(',')); + foreach (var item in areas) + { + if (this.Range(item) != null) + SetColumnsOrRowsToRepeat(item); + } + } + + private void SetColumnsOrRowsToRepeat(string area) + { + ParseReference(area, out String sheetName, out String sheetArea); + sheetArea = sheetArea.Replace("$", ""); + + if (sheetArea.Equals("#REF")) return; + if (IsColReference(sheetArea)) + WorksheetsInternal.Worksheet(sheetName).PageSetup.SetColumnsToRepeatAtLeft(sheetArea); + if (IsRowReference(sheetArea)) + WorksheetsInternal.Worksheet(sheetName).PageSetup.SetRowsToRepeatAtTop(sheetArea); + } + + // either $A:$X => true or $1:$99 => false + private static bool IsColReference(string sheetArea) + { + return sheetArea.All(c => c == ':' || char.IsLetter(c)); + } + + private static bool IsRowReference(string sheetArea) + { + return sheetArea.All(c => c == ':' || char.IsNumber(c)); + } + + private static void ParseReference(string item, out string sheetName, out string sheetArea) + { + var sections = item.Trim().Split('!'); + if (sections.Count() == 1) + { + sheetName = string.Empty; + sheetArea = item; + } + else + { + sheetName = sections[0].UnescapeSheetName(); + sheetArea = sections[1]; + } + } + + private Int32 lastColumnNumber; + + private void LoadCells(SharedStringItem[] sharedStrings, Stylesheet s, NumberingFormats numberingFormats, + Fills fills, Borders borders, Fonts fonts, Dictionary sharedFormulasR1C1, + XLWorksheet ws, Dictionary styleList, Cell cell, Int32 rowIndex) + { + Int32 styleIndex = cell.StyleIndex != null ? Int32.Parse(cell.StyleIndex.InnerText) : 0; + + XLAddress cellAddress; + if (cell.CellReference == null) + { + cellAddress = new XLAddress(ws, rowIndex, ++lastColumnNumber, false, false); + } + else + { + cellAddress = XLAddress.Create(ws, cell.CellReference.Value); + lastColumnNumber = cellAddress.ColumnNumber; + } + + var xlCell = ws.Cell(in cellAddress); + + if (styleList.TryGetValue(styleIndex, out IXLStyle style)) + { + xlCell.InnerStyle = style; + } + else + { + ApplyStyle(xlCell, styleIndex, s, fills, borders, fonts, numberingFormats); + } + + if (cell.CellFormula?.SharedIndex != null && cell.CellFormula?.Reference != null) + { + String formula; + if (cell.CellFormula.FormulaType != null && cell.CellFormula.FormulaType == CellFormulaValues.Array) + formula = "{" + cell.CellFormula.Text + "}"; + else + formula = cell.CellFormula.Text; + + // Parent cell of shared formulas + // Child cells will use this shared index to set its R1C1 style formula + xlCell.FormulaReference = ws.Range(cell.CellFormula.Reference.Value).RangeAddress; + + xlCell.FormulaA1 = formula; + sharedFormulasR1C1.Add(cell.CellFormula.SharedIndex.Value, xlCell.FormulaR1C1); + + if (cell.DataType != null) + { + switch (cell.DataType.Value) + { + case CellValues.Boolean: + xlCell.SetDataTypeFast(XLDataType.Boolean); + break; + + case CellValues.Number: + xlCell.SetDataTypeFast(XLDataType.Number); + break; + + case CellValues.Date: + xlCell.SetDataTypeFast(XLDataType.DateTime); + break; + + case CellValues.InlineString: + case CellValues.SharedString: + case CellValues.String: + xlCell.SetDataTypeFast(XLDataType.Text); + break; + } + } + } + else if (cell.CellFormula != null) + { + if (cell.CellFormula.SharedIndex != null) + xlCell.FormulaR1C1 = sharedFormulasR1C1[cell.CellFormula.SharedIndex.Value]; + else if (!String.IsNullOrWhiteSpace(cell.CellFormula.Text)) + { + String formula; + if (cell.CellFormula.FormulaType != null && cell.CellFormula.FormulaType == CellFormulaValues.Array) + formula = "{" + cell.CellFormula.Text + "}"; + else + formula = cell.CellFormula.Text; + + xlCell.FormulaA1 = formula; + } + + if (cell.CellFormula.Reference != null) + { + foreach (var childCell in ws.Range(cell.CellFormula.Reference.Value).Cells(c => c.FormulaReference == null || !c.HasFormula)) + { + if (childCell.FormulaReference == null) + childCell.FormulaReference = ws.Range(cell.CellFormula.Reference.Value).RangeAddress; + + if (!childCell.HasFormula) + childCell.FormulaA1 = xlCell.FormulaA1; + } + } + + if (cell.DataType != null) + { + switch (cell.DataType.Value) + { + case CellValues.Boolean: + xlCell.SetDataTypeFast(XLDataType.Boolean); + break; + + case CellValues.Number: + xlCell.SetDataTypeFast(XLDataType.Number); + break; + + case CellValues.Date: + xlCell.SetDataTypeFast(XLDataType.DateTime); + break; + + case CellValues.InlineString: + case CellValues.SharedString: + case CellValues.String: + xlCell.SetDataTypeFast(XLDataType.Text); + break; + } + } + } + else if (cell.DataType != null) + { + if (cell.DataType == CellValues.InlineString) + { + xlCell.SetDataTypeFast(XLDataType.Text); + xlCell.ShareString = false; + + if (cell.InlineString != null) + { + if (cell.InlineString.Text != null) + xlCell.SetInternalCellValueString(cell.InlineString.Text.Text.FixNewLines()); + else + ParseCellValue(cell.InlineString, xlCell); + } + else + xlCell.SetInternalCellValueString(String.Empty); + } + else if (cell.DataType == CellValues.SharedString) + { + xlCell.SetDataTypeFast(XLDataType.Text); + + if (cell.CellValue != null + && Int32.TryParse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture, out Int32 sharedStringId) + && sharedStringId >= 0 && sharedStringId < sharedStrings.Length) + { + xlCell.SharedStringId = sharedStringId; + var sharedString = sharedStrings[sharedStringId]; + ParseCellValue(sharedString, xlCell); + } + else + xlCell.SetInternalCellValueString(String.Empty); + } + else if (cell.DataType == CellValues.String) + { + xlCell.SetDataTypeFast(XLDataType.Text); + + if (!String.IsNullOrEmpty(cell.CellValue?.Text)) + xlCell.SetInternalCellValueString(cell.CellValue.Text); + else + xlCell.SetInternalCellValueString(String.Empty); + } + else if (cell.DataType == CellValues.Date) + { + xlCell.SetDataTypeFast(XLDataType.DateTime); + + if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString()); + } + else if (cell.DataType == CellValues.Boolean) + { + xlCell.SetDataTypeFast(XLDataType.Boolean); + if (cell.CellValue != null) + xlCell.SetInternalCellValueString(cell.CellValue.Text); + } + else if (cell.DataType == CellValues.Number) + { + if (s == null) + xlCell.SetDataTypeFast(XLDataType.Number); + else + xlCell.DataType = GetDataTypeFromCell(xlCell.StyleValue.NumberFormat); + + if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString()); + } + } + else if (cell.CellValue != null) + { + if (s == null) + { + xlCell.SetDataTypeFast(XLDataType.Number); + } + else + { + xlCell.DataType = GetDataTypeFromCell(xlCell.StyleValue.NumberFormat); + + if (!String.IsNullOrWhiteSpace(cell.CellValue.Text)) + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, CultureInfo.InvariantCulture).ToInvariantString()); + + var numberFormatId = ((CellFormat)(s.CellFormats).ElementAt(styleIndex)).NumberFormatId; + + if (numberFormatId?.HasValue ?? false) + { + var format = s.NumberingFormats? + .Cast() + .Where(nf => nf.NumberFormatId.Value == numberFormatId) + .Select(nf => nf.FormatCode.Value) + .FirstOrDefault(); + + if (format == null) + xlCell.InnerStyle.NumberFormat.NumberFormatId = Int32.Parse(numberFormatId); + else + xlCell.InnerStyle.NumberFormat.Format = format; + } + } + } + + if (xlCell.HasFormula) + { + if (cell.CellValue != null) + xlCell.SetInternalCellValueString(cell.CellValue.Text); + + xlCell.NeedsRecalculation = (xlCell.CachedValue == null); + } + + if (Use1904DateSystem && xlCell.DataType == XLDataType.DateTime) + { + // Internally ClosedXML stores cells as standard 1900-based style + // so if a workbook is in 1904-format, we do that adjustment here and when saving. + xlCell.SetValue(xlCell.GetDateTime().AddDays(1462)); + } + + if (!styleList.ContainsKey(styleIndex)) + styleList.Add(styleIndex, xlCell.Style); + } + + /// + /// Parses the cell value for normal or rich text + /// Input element should either be a shared string or inline string + /// + /// The element (either a shared string or inline string) + /// The cell. + private void ParseCellValue(RstType element, XLCell xlCell) + { + var runs = element.Elements(); + var phoneticRuns = element.Elements(); + var phoneticProperties = element.Elements(); + Boolean hasRuns = false; + foreach (Run run in runs) + { + var runProperties = run.RunProperties; + String text = run.Text.InnerText.FixNewLines(); + + if (runProperties == null) + xlCell.RichText.AddText(text, xlCell.Style.Font); + else + { + var rt = xlCell.RichText.AddText(text); + LoadFont(runProperties, rt); + } + if (!hasRuns) + hasRuns = true; + } + + if (!hasRuns) + xlCell.SetInternalCellValueString(XmlEncoder.DecodeString(element.Text.InnerText)); + + #region Load PhoneticProperties + + var pp = phoneticProperties.FirstOrDefault(); + if (pp != null) + { + if (pp.Alignment != null) + xlCell.RichText.Phonetics.Alignment = pp.Alignment.Value.ToClosedXml(); + if (pp.Type != null) + xlCell.RichText.Phonetics.Type = pp.Type.Value.ToClosedXml(); + + LoadFont(pp, xlCell.RichText.Phonetics); + } + + #endregion Load PhoneticProperties + + #region Load Phonetic Runs + + foreach (PhoneticRun pr in phoneticRuns) + { + xlCell.RichText.Phonetics.Add(pr.Text.InnerText.FixNewLines(), (Int32)pr.BaseTextStartIndex.Value, + (Int32)pr.EndingBaseIndex.Value); + } + + #endregion Load Phonetic Runs + } + + private void LoadNumberFormat(NumberingFormat nfSource, IXLNumberFormat nf) + { + if (nfSource == null) return; + + if (nfSource.NumberFormatId != null && nfSource.NumberFormatId.Value < XLConstants.NumberOfBuiltInStyles) + nf.NumberFormatId = (Int32)nfSource.NumberFormatId.Value; + else if (nfSource.FormatCode != null) + nf.Format = nfSource.FormatCode.Value; + } + + private void LoadBorder(Border borderSource, IXLBorder border) + { + if (borderSource == null) return; + + LoadBorderValues(borderSource.DiagonalBorder, border.SetDiagonalBorder, border.SetDiagonalBorderColor); + + if (borderSource.DiagonalUp != null) + border.DiagonalUp = borderSource.DiagonalUp.Value; + if (borderSource.DiagonalDown != null) + border.DiagonalDown = borderSource.DiagonalDown.Value; + + LoadBorderValues(borderSource.LeftBorder, border.SetLeftBorder, border.SetLeftBorderColor); + LoadBorderValues(borderSource.RightBorder, border.SetRightBorder, border.SetRightBorderColor); + LoadBorderValues(borderSource.TopBorder, border.SetTopBorder, border.SetTopBorderColor); + LoadBorderValues(borderSource.BottomBorder, border.SetBottomBorder, border.SetBottomBorderColor); + } + + private void LoadBorderValues(BorderPropertiesType source, Func setBorder, Func setColor) + { + if (source != null) + { + if (source.Style != null) + setBorder(source.Style.Value.ToClosedXml()); + if (source.Color != null) + setColor(source.Color.ToClosedXMLColor(_colorList)); + } + } + + // Differential fills store the patterns differently than other fills + // Actually differential fills make more sense. bg is bg and fg is fg + // 'Other' fills store the bg color in the fg field when pattern type is solid + private void LoadFill(Fill openXMLFill, IXLFill closedXMLFill, Boolean differentialFillFormat) + { + if (openXMLFill == null || openXMLFill.PatternFill == null) return; + + if (openXMLFill.PatternFill.PatternType != null) + closedXMLFill.PatternType = openXMLFill.PatternFill.PatternType.Value.ToClosedXml(); + else + closedXMLFill.PatternType = XLFillPatternValues.Solid; + + switch (closedXMLFill.PatternType) + { + case XLFillPatternValues.None: + break; + + case XLFillPatternValues.Solid: + if (differentialFillFormat) + { + if (openXMLFill.PatternFill.BackgroundColor != null) + closedXMLFill.BackgroundColor = openXMLFill.PatternFill.BackgroundColor.ToClosedXMLColor(_colorList); + else + closedXMLFill.BackgroundColor = XLColor.FromIndex(64); + } + else + { + // yes, source is foreground! + if (openXMLFill.PatternFill.ForegroundColor != null) + closedXMLFill.BackgroundColor = openXMLFill.PatternFill.ForegroundColor.ToClosedXMLColor(_colorList); + else + closedXMLFill.BackgroundColor = XLColor.FromIndex(64); + } + break; + + default: + if (openXMLFill.PatternFill.ForegroundColor != null) + closedXMLFill.PatternColor = openXMLFill.PatternFill.ForegroundColor.ToClosedXMLColor(_colorList); + + if (openXMLFill.PatternFill.BackgroundColor != null) + closedXMLFill.BackgroundColor = openXMLFill.PatternFill.BackgroundColor.ToClosedXMLColor(_colorList); + else + closedXMLFill.BackgroundColor = XLColor.FromIndex(64); + break; + } + } + + private void LoadFont(OpenXmlElement fontSource, IXLFontBase fontBase) + { + if (fontSource == null) return; + + fontBase.Bold = GetBoolean(fontSource.Elements().FirstOrDefault()); + var fontColor = fontSource.Elements().FirstOrDefault(); + if (fontColor != null) + fontBase.FontColor = fontColor.ToClosedXMLColor(_colorList); + + var fontFamilyNumbering = + fontSource.Elements().FirstOrDefault(); + if (fontFamilyNumbering != null && fontFamilyNumbering.Val != null) + fontBase.FontFamilyNumbering = + (XLFontFamilyNumberingValues)Int32.Parse(fontFamilyNumbering.Val.ToString()); + var runFont = fontSource.Elements().FirstOrDefault(); + if (runFont != null) + { + if (runFont.Val != null) + fontBase.FontName = runFont.Val; + } + var fontSize = fontSource.Elements().FirstOrDefault(); + if (fontSize != null) + { + if ((fontSize).Val != null) + fontBase.FontSize = (fontSize).Val; + } + + fontBase.Italic = GetBoolean(fontSource.Elements().FirstOrDefault()); + fontBase.Shadow = GetBoolean(fontSource.Elements().FirstOrDefault()); + fontBase.Strikethrough = GetBoolean(fontSource.Elements().FirstOrDefault()); + + var underline = fontSource.Elements().FirstOrDefault(); + if (underline != null) + { + fontBase.Underline = underline.Val != null ? underline.Val.Value.ToClosedXml() : XLFontUnderlineValues.Single; + } + + var verticalTextAlignment = fontSource.Elements().FirstOrDefault(); + + if (verticalTextAlignment == null) return; + + fontBase.VerticalAlignment = verticalTextAlignment.Val != null ? verticalTextAlignment.Val.Value.ToClosedXml() : XLFontVerticalTextAlignmentValues.Baseline; + } + + private Int32 lastRow; + + private void LoadRows(Stylesheet s, NumberingFormats numberingFormats, Fills fills, Borders borders, Fonts fonts, + XLWorksheet ws, SharedStringItem[] sharedStrings, + Dictionary sharedFormulasR1C1, Dictionary styleList, + Row row) + { + Int32 rowIndex = row.RowIndex == null ? ++lastRow : (Int32)row.RowIndex.Value; + var xlRow = ws.Row(rowIndex, false); + + if (row.Height != null) + xlRow.Height = row.Height; + else + { + xlRow.Loading = true; + xlRow.Height = ws.RowHeight; + xlRow.Loading = false; + } + + if (row.Hidden != null && row.Hidden) + xlRow.Hide(); + + if (row.Collapsed != null && row.Collapsed) + xlRow.Collapsed = true; + + if (row.OutlineLevel != null && row.OutlineLevel > 0) + xlRow.OutlineLevel = row.OutlineLevel; + + if (row.CustomFormat != null) + { + Int32 styleIndex = row.StyleIndex != null ? Int32.Parse(row.StyleIndex.InnerText) : -1; + if (styleIndex >= 0) + { + ApplyStyle(xlRow, styleIndex, s, fills, borders, fonts, numberingFormats); + } + else + { + xlRow.Style = ws.Style; + } + } + + lastColumnNumber = 0; + foreach (Cell cell in row.Elements()) + LoadCells(sharedStrings, s, numberingFormats, fills, borders, fonts, sharedFormulasR1C1, ws, styleList, + cell, rowIndex); + } + + private void LoadColumns(Stylesheet s, NumberingFormats numberingFormats, Fills fills, Borders borders, + Fonts fonts, XLWorksheet ws, Columns columns) + { + if (columns == null) return; + + var wsDefaultColumn = + columns.Elements().FirstOrDefault(c => c.Max == XLHelper.MaxColumnNumber); + + if (wsDefaultColumn != null && wsDefaultColumn.Width != null) + ws.ColumnWidth = wsDefaultColumn.Width - ColumnWidthOffset; + + Int32 styleIndexDefault = wsDefaultColumn != null && wsDefaultColumn.Style != null + ? Int32.Parse(wsDefaultColumn.Style.InnerText) + : -1; + if (styleIndexDefault >= 0) + ApplyStyle(ws, styleIndexDefault, s, fills, borders, fonts, numberingFormats); + + foreach (Column col in columns.Elements()) + { + //IXLStylized toApply; + if (col.Max == XLHelper.MaxColumnNumber) continue; + + var xlColumns = (XLColumns)ws.Columns(col.Min, col.Max); + if (col.Width != null) + { + Double width = col.Width - ColumnWidthOffset; + //if (width < 0) width = 0; + xlColumns.Width = width; + } + else + xlColumns.Width = ws.ColumnWidth; + + if (col.Hidden != null && col.Hidden) + xlColumns.Hide(); + + if (col.Collapsed != null && col.Collapsed) + xlColumns.CollapseOnly(); + + if (col.OutlineLevel != null) + { + var outlineLevel = col.OutlineLevel; + xlColumns.ForEach(c => c.OutlineLevel = outlineLevel); + } + + Int32 styleIndex = col.Style != null ? Int32.Parse(col.Style.InnerText) : -1; + if (styleIndex >= 0) + { + ApplyStyle(xlColumns, styleIndex, s, fills, borders, fonts, numberingFormats); + } + else + { + xlColumns.Style = ws.Style; + } + } + } + + private static XLDataType GetDataTypeFromCell(XLNumberFormatValue numberFormat) + { + var numberFormatId = numberFormat.NumberFormatId; + if (numberFormatId == 46U) + return XLDataType.TimeSpan; + else if ((numberFormatId >= 14 && numberFormatId <= 22) || + (numberFormatId >= 45 && numberFormatId <= 47)) + return XLDataType.DateTime; + else if (numberFormatId == 49) + return XLDataType.Text; + else + { + if (!String.IsNullOrWhiteSpace(numberFormat.Format)) + { + var dataType = GetDataTypeFromFormat(numberFormat.Format); + return dataType.HasValue ? dataType.Value : XLDataType.Number; + } + else + return XLDataType.Number; + } + } + + private static XLDataType? GetDataTypeFromFormat(String format) + { + int length = format.Length; + String f = format.ToLower(); + for (Int32 i = 0; i < length; i++) + { + Char c = f[i]; + if (c == '"') + i = f.IndexOf('"', i + 1); + else if (c == '0' || c == '#' || c == '?') + return XLDataType.Number; + else if (c == 'y' || c == 'm' || c == 'd' || c == 'h' || c == 's') + return XLDataType.DateTime; + } + return null; + } + + private static void LoadAutoFilter(AutoFilter af, XLWorksheet ws) + { + if (af != null) + { + ws.Range(af.Reference.Value).SetAutoFilter(); + var autoFilter = ws.AutoFilter; + LoadAutoFilterSort(af, ws, autoFilter); + LoadAutoFilterColumns(af, autoFilter); + } + } + + private static void LoadAutoFilterColumns(AutoFilter af, XLAutoFilter autoFilter) + { + foreach (var filterColumn in af.Elements()) + { + Int32 column = (int)filterColumn.ColumnId.Value + 1; + if (filterColumn.CustomFilters != null) + { + var filterList = new List(); + autoFilter.Column(column).FilterType = XLFilterType.Custom; + autoFilter.Filters.Add(column, filterList); + XLConnector connector = filterColumn.CustomFilters.And != null && filterColumn.CustomFilters.And.Value ? XLConnector.And : XLConnector.Or; + + Boolean isText = false; + foreach (var filter in filterColumn.CustomFilters.OfType()) + { + String val = filter.Val.Value; + if (!Double.TryParse(val, out Double dTest)) + { + isText = true; + break; + } + } + + foreach (var filter in filterColumn.CustomFilters.OfType()) + { + var xlFilter = new XLFilter { Connector = connector }; + if (isText) + { + // TODO: Treat text BETWEEN functions better + if (filter.Val.Value.StartsWith("*") && filter.Val.Value.EndsWith("*")) + { + var value = filter.Val.Value.Substring(1, filter.Val.Value.Length - 2); + xlFilter.Value = filter.Val.Value; + xlFilter.Condition = s => XLFilterColumn.ContainsFunction(value, s); + } + else if (filter.Val.Value.StartsWith("*")) + { + var value = filter.Val.Value.Substring(1); + xlFilter.Value = filter.Val.Value; + xlFilter.Condition = s => XLFilterColumn.EndsWithFunction(value, s); + } + else if (filter.Val.Value.EndsWith("*")) + { + var value = filter.Val.Value.Substring(0, filter.Val.Value.Length - 1); + xlFilter.Value = filter.Val.Value; + xlFilter.Condition = s => XLFilterColumn.BeginsWithFunction(value, s); + } + else + xlFilter.Value = filter.Val.Value; + } + else + xlFilter.Value = Double.Parse(filter.Val.Value, CultureInfo.InvariantCulture); + + if (filter.Operator != null) + xlFilter.Operator = filter.Operator.Value.ToClosedXml(); + else + xlFilter.Operator = XLFilterOperator.Equal; + + // Unhandled instances - we should actually improve this + if (xlFilter.Condition == null) + { + Func condition = null; + switch (xlFilter.Operator) + { + case XLFilterOperator.Equal: + if (isText) + condition = o => o.ToString().Equals(xlFilter.Value.ToString(), StringComparison.OrdinalIgnoreCase); + else + condition = o => (o as IComparable).CompareTo(xlFilter.Value) == 0; + break; + + case XLFilterOperator.EqualOrGreaterThan: condition = o => (o as IComparable).CompareTo(xlFilter.Value) >= 0; break; + case XLFilterOperator.EqualOrLessThan: condition = o => (o as IComparable).CompareTo(xlFilter.Value) <= 0; break; + case XLFilterOperator.GreaterThan: condition = o => (o as IComparable).CompareTo(xlFilter.Value) > 0; break; + case XLFilterOperator.LessThan: condition = o => (o as IComparable).CompareTo(xlFilter.Value) < 0; break; + case XLFilterOperator.NotEqual: + if (isText) + condition = o => !o.ToString().Equals(xlFilter.Value.ToString(), StringComparison.OrdinalIgnoreCase); + else + condition = o => (o as IComparable).CompareTo(xlFilter.Value) != 0; + break; + } + + xlFilter.Condition = condition; + } + + filterList.Add(xlFilter); + } + } + else if (filterColumn.Filters != null) + { + if (filterColumn.Filters.Elements().All(element => element is Filter)) + autoFilter.Column(column).FilterType = XLFilterType.Regular; + else if (filterColumn.Filters.Elements().All(element => element is DateGroupItem)) + autoFilter.Column(column).FilterType = XLFilterType.DateTimeGrouping; + else + throw new NotSupportedException(String.Format("Mixing regular filters and date group filters in a single autofilter column is not supported. Column {0} of {1}", column, autoFilter.Range.ToString())); + + var filterList = new List(); + + autoFilter.Filters.Add((int)filterColumn.ColumnId.Value + 1, filterList); + + Boolean isText = false; + foreach (var filter in filterColumn.Filters.OfType()) + { + String val = filter.Val.Value; + if (!Double.TryParse(val, NumberStyles.Any, null, out Double dTest)) + { + isText = true; + break; + } + } + + foreach (var filter in filterColumn.Filters.OfType()) + { + var xlFilter = new XLFilter { Connector = XLConnector.Or, Operator = XLFilterOperator.Equal }; + + Func condition; + if (isText) + { + xlFilter.Value = filter.Val.Value; + condition = o => o.ToString().Equals(xlFilter.Value.ToString(), StringComparison.OrdinalIgnoreCase); + } + else + { + xlFilter.Value = Double.Parse(filter.Val.Value, NumberStyles.Any); + condition = o => (o as IComparable).CompareTo(xlFilter.Value) == 0; + } + + xlFilter.Condition = condition; + filterList.Add(xlFilter); + } + + foreach (var dateGroupItem in filterColumn.Filters.OfType()) + { + bool valid = true; + + if (!(dateGroupItem.DateTimeGrouping?.HasValue ?? false)) + continue; + + var xlDateGroupFilter = new XLFilter + { + Connector = XLConnector.Or, + Operator = XLFilterOperator.Equal, + DateTimeGrouping = dateGroupItem.DateTimeGrouping?.Value.ToClosedXml() ?? XLDateTimeGrouping.Year + }; + + int year = 1900; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Year) + { + if (dateGroupItem?.Year?.HasValue ?? false) + year = (int)dateGroupItem.Year?.Value; + else + valid &= false; + } + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Month) + { + if (dateGroupItem?.Month?.HasValue ?? false) + month = (int)dateGroupItem.Month?.Value; + else + valid &= false; + } + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Day) + { + if (dateGroupItem?.Day?.HasValue ?? false) + day = (int)dateGroupItem.Day?.Value; + else + valid &= false; + } + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Hour) + { + if (dateGroupItem?.Hour?.HasValue ?? false) + hour = (int)dateGroupItem.Hour?.Value; + else + valid &= false; + } + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Minute) + { + if (dateGroupItem?.Minute?.HasValue ?? false) + minute = (int)dateGroupItem.Minute?.Value; + else + valid &= false; + } + + if (xlDateGroupFilter.DateTimeGrouping >= XLDateTimeGrouping.Second) + { + if (dateGroupItem?.Second?.HasValue ?? false) + second = (int)dateGroupItem.Second?.Value; + else + valid &= false; + } + + var date = new DateTime(year, month, day, hour, minute, second); + xlDateGroupFilter.Value = date; + + xlDateGroupFilter.Condition = date2 => XLDateTimeGroupFilteredColumn.IsMatch(date, (DateTime)date2, xlDateGroupFilter.DateTimeGrouping); + + if (valid) + filterList.Add(xlDateGroupFilter); + } + } + else if (filterColumn.Top10 != null) + { + var xlFilterColumn = autoFilter.Column(column); + autoFilter.Filters.Add(column, null); + xlFilterColumn.FilterType = XLFilterType.TopBottom; + if (filterColumn.Top10.Percent != null && filterColumn.Top10.Percent.Value) + xlFilterColumn.TopBottomType = XLTopBottomType.Percent; + else + xlFilterColumn.TopBottomType = XLTopBottomType.Items; + + if (filterColumn.Top10.Top != null && !filterColumn.Top10.Top.Value) + xlFilterColumn.TopBottomPart = XLTopBottomPart.Bottom; + else + xlFilterColumn.TopBottomPart = XLTopBottomPart.Top; + + xlFilterColumn.TopBottomValue = (int)filterColumn.Top10.Val.Value; + } + else if (filterColumn.DynamicFilter != null) + { + autoFilter.Filters.Add(column, null); + var xlFilterColumn = autoFilter.Column(column); + xlFilterColumn.FilterType = XLFilterType.Dynamic; + if (filterColumn.DynamicFilter.Type != null) + xlFilterColumn.DynamicType = filterColumn.DynamicFilter.Type.Value.ToClosedXml(); + else + xlFilterColumn.DynamicType = XLFilterDynamicType.AboveAverage; + + xlFilterColumn.DynamicValue = filterColumn.DynamicFilter.Val.Value; + } + } + } + + private static void LoadAutoFilterSort(AutoFilter af, XLWorksheet ws, IXLAutoFilter autoFilter) + { + var sort = af.Elements().FirstOrDefault(); + if (sort != null) + { + var condition = sort.Elements().FirstOrDefault(); + if (condition != null) + { + Int32 column = ws.Range(condition.Reference.Value).FirstCell().Address.ColumnNumber - autoFilter.Range.FirstCell().Address.ColumnNumber + 1; + autoFilter.SortColumn = column; + autoFilter.Sorted = true; + autoFilter.SortOrder = condition.Descending != null && condition.Descending.Value ? XLSortOrder.Descending : XLSortOrder.Ascending; + } + } + } + + private static void LoadSheetProtection(SheetProtection sp, XLWorksheet ws) + { + if (sp == null) return; + + ws.Protection.IsProtected = OpenXmlHelper.GetBooleanValueAsBool(sp.Sheet, false); + + var algorithmName = sp.AlgorithmName?.Value ?? string.Empty; + if (String.IsNullOrEmpty(algorithmName)) + { + ws.Protection.PasswordHash = sp.Password?.Value ?? string.Empty; + ws.Protection.Base64EncodedSalt = string.Empty; + } + else if (DescribedEnumParser.IsValidDescription(algorithmName)) + { + ws.Protection.Algorithm = DescribedEnumParser.FromDescription(algorithmName); + ws.Protection.PasswordHash = sp.HashValue?.Value ?? string.Empty; + ws.Protection.SpinCount = sp.SpinCount?.Value ?? 0; + ws.Protection.Base64EncodedSalt = sp.SaltValue?.Value ?? string.Empty; + } + + ws.Protection.AllowElement(XLSheetProtectionElements.FormatCells, !OpenXmlHelper.GetBooleanValueAsBool(sp.FormatCells, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.FormatColumns, !OpenXmlHelper.GetBooleanValueAsBool(sp.FormatColumns, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.FormatRows, !OpenXmlHelper.GetBooleanValueAsBool(sp.FormatRows, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.InsertColumns, !OpenXmlHelper.GetBooleanValueAsBool(sp.InsertColumns, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.InsertHyperlinks, !OpenXmlHelper.GetBooleanValueAsBool(sp.InsertHyperlinks, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.InsertRows, !OpenXmlHelper.GetBooleanValueAsBool(sp.InsertRows, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.DeleteColumns, !OpenXmlHelper.GetBooleanValueAsBool(sp.DeleteColumns, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.DeleteRows, !OpenXmlHelper.GetBooleanValueAsBool(sp.DeleteRows, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.AutoFilter, !OpenXmlHelper.GetBooleanValueAsBool(sp.AutoFilter, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.PivotTables, !OpenXmlHelper.GetBooleanValueAsBool(sp.PivotTables, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.Sort, !OpenXmlHelper.GetBooleanValueAsBool(sp.Sort, true)); + ws.Protection.AllowElement(XLSheetProtectionElements.EditScenarios, !OpenXmlHelper.GetBooleanValueAsBool(sp.Scenarios, true)); + + ws.Protection.AllowElement(XLSheetProtectionElements.EditObjects, !OpenXmlHelper.GetBooleanValueAsBool(sp.Objects, false)); + ws.Protection.AllowElement(XLSheetProtectionElements.SelectLockedCells, !OpenXmlHelper.GetBooleanValueAsBool(sp.SelectLockedCells, false)); + ws.Protection.AllowElement(XLSheetProtectionElements.SelectUnlockedCells, !OpenXmlHelper.GetBooleanValueAsBool(sp.SelectUnlockedCells, false)); + } + + private static void LoadDataValidations(DataValidations dataValidations, XLWorksheet ws) + { + if (dataValidations == null) return; + + foreach (DataValidation dvs in dataValidations.Elements()) + { + String txt = dvs.SequenceOfReferences.InnerText; + if (String.IsNullOrWhiteSpace(txt)) continue; + foreach (var rangeAddress in txt.Split(' ')) + { + var dvt = new XLDataValidation(ws.Range(rangeAddress)); + ws.DataValidations.Add(dvt, skipIntersectionsCheck: true); + if (dvs.AllowBlank != null) dvt.IgnoreBlanks = dvs.AllowBlank; + if (dvs.ShowDropDown != null) dvt.InCellDropdown = !dvs.ShowDropDown.Value; + if (dvs.ShowErrorMessage != null) dvt.ShowErrorMessage = dvs.ShowErrorMessage; + if (dvs.ShowInputMessage != null) dvt.ShowInputMessage = dvs.ShowInputMessage; + if (dvs.PromptTitle != null) dvt.InputTitle = dvs.PromptTitle; + if (dvs.Prompt != null) dvt.InputMessage = dvs.Prompt; + if (dvs.ErrorTitle != null) dvt.ErrorTitle = dvs.ErrorTitle; + if (dvs.Error != null) dvt.ErrorMessage = dvs.Error; + if (dvs.ErrorStyle != null) dvt.ErrorStyle = dvs.ErrorStyle.Value.ToClosedXml(); + if (dvs.Type != null) dvt.AllowedValues = dvs.Type.Value.ToClosedXml(); + if (dvs.Operator != null) dvt.Operator = dvs.Operator.Value.ToClosedXml(); + if (dvs.Formula1 != null) dvt.MinValue = dvs.Formula1.Text; + if (dvs.Formula2 != null) dvt.MaxValue = dvs.Formula2.Text; + } + } + } + + /// + /// Loads the conditional formatting. + /// + // https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.conditionalformattingrule%28v=office.15%29.aspx?f=255&MSPPError=-2147217396 + private void LoadConditionalFormatting(ConditionalFormatting conditionalFormatting, XLWorksheet ws, + Dictionary differentialFormats) + { + if (conditionalFormatting == null) return; + + foreach (var fr in conditionalFormatting.Elements()) + { + var ranges = conditionalFormatting.SequenceOfReferences.Items + .Select(sor => ws.Range(sor.Value)); + var conditionalFormat = new XLConditionalFormat(ranges); + + conditionalFormat.StopIfTrue = OpenXmlHelper.GetBooleanValueAsBool(fr.StopIfTrue, false); + + if (fr.FormatId != null) + { + LoadFont(differentialFormats[(Int32)fr.FormatId.Value].Font, conditionalFormat.Style.Font); + LoadFill(differentialFormats[(Int32)fr.FormatId.Value].Fill, conditionalFormat.Style.Fill, + differentialFillFormat: true); + LoadBorder(differentialFormats[(Int32)fr.FormatId.Value].Border, conditionalFormat.Style.Border); + LoadNumberFormat(differentialFormats[(Int32)fr.FormatId.Value].NumberingFormat, + conditionalFormat.Style.NumberFormat); + } + + // The conditional formatting type is compulsory. If it doesn't exist, skip the entire rule. + if (fr.Type == null) continue; + conditionalFormat.ConditionalFormatType = fr.Type.Value.ToClosedXml(); + conditionalFormat.OriginalPriority = fr.Priority?.Value ?? Int32.MaxValue; + + if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.CellIs && fr.Operator != null) + conditionalFormat.Operator = fr.Operator.Value.ToClosedXml(); + + if (!String.IsNullOrWhiteSpace(fr.Text)) + conditionalFormat.Values.Add(GetFormula(fr.Text.Value)); + + if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.Top10) + { + if (fr.Percent != null) + conditionalFormat.Percent = fr.Percent.Value; + if (fr.Bottom != null) + conditionalFormat.Bottom = fr.Bottom.Value; + if (fr.Rank != null) + conditionalFormat.Values.Add(GetFormula(fr.Rank.Value.ToString())); + } + else if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.TimePeriod) + { + if (fr.TimePeriod != null) + conditionalFormat.TimePeriod = fr.TimePeriod.Value.ToClosedXml(); + else + conditionalFormat.TimePeriod = XLTimePeriod.Yesterday; + } + + if (fr.Elements().Any()) + { + var colorScale = fr.Elements().First(); + ExtractConditionalFormatValueObjects(conditionalFormat, colorScale); + } + else if (fr.Elements().Any()) + { + var dataBar = fr.Elements().First(); + if (dataBar.ShowValue != null) + conditionalFormat.ShowBarOnly = !dataBar.ShowValue.Value; + + var id = fr.Descendants().FirstOrDefault(); + if (id != null && id.Text != null && !String.IsNullOrWhiteSpace(id.Text)) + conditionalFormat.Id = new Guid(id.Text.Substring(1, id.Text.Length - 2)); + + ExtractConditionalFormatValueObjects(conditionalFormat, dataBar); + } + else if (fr.Elements().Any()) + { + var iconSet = fr.Elements().First(); + if (iconSet.ShowValue != null) + conditionalFormat.ShowIconOnly = !iconSet.ShowValue.Value; + if (iconSet.Reverse != null) + conditionalFormat.ReverseIconOrder = iconSet.Reverse.Value; + + if (iconSet.IconSetValue != null) + conditionalFormat.IconSetStyle = iconSet.IconSetValue.Value.ToClosedXml(); + else + conditionalFormat.IconSetStyle = XLIconSetStyle.ThreeTrafficLights1; + + ExtractConditionalFormatValueObjects(conditionalFormat, iconSet); + } + else + { + foreach (var formula in fr.Elements()) + { + if (formula.Text != null + && (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.CellIs + || conditionalFormat.ConditionalFormatType == XLConditionalFormatType.Expression)) + { + conditionalFormat.Values.Add(GetFormula(formula.Text)); + } + } + } + + ws.ConditionalFormats.Add(conditionalFormat); + } + } + + private void LoadExtensions(WorksheetExtensionList extensions, XLWorksheet ws) + { + if (extensions == null) + { + return; + } + + foreach (var conditionalFormattingRule in extensions + .Descendants() + .Where(cf => + cf.Type != null + && cf.Type.HasValue + && cf.Type.Value == ConditionalFormatValues.DataBar)) + { + var xlConditionalFormat = ws.ConditionalFormats + .Cast() + .SingleOrDefault(cf => cf.Id.WrapInBraces() == conditionalFormattingRule.Id); + if (xlConditionalFormat != null) + { + var negativeFillColor = conditionalFormattingRule.Descendants().SingleOrDefault(); + xlConditionalFormat.Colors.Add(negativeFillColor.ToClosedXMLColor(_colorList)); + } + } + + foreach (var slg in extensions + .Descendants() + .SelectMany(sparklineGroups => sparklineGroups.Descendants())) + { + var xlSparklineGroup = (ws.SparklineGroups as XLSparklineGroups).Add(); + + if (slg.Formula != null) + xlSparklineGroup.DateRange = Range(slg.Formula.Text); + + var xlSparklineStyle = xlSparklineGroup.Style; + if (slg.FirstMarkerColor != null) xlSparklineStyle.FirstMarkerColor = slg.FirstMarkerColor.ToClosedXMLColor(); + if (slg.LastMarkerColor != null) xlSparklineStyle.LastMarkerColor = slg.LastMarkerColor.ToClosedXMLColor(); + if (slg.HighMarkerColor != null) xlSparklineStyle.HighMarkerColor = slg.HighMarkerColor.ToClosedXMLColor(); + if (slg.LowMarkerColor != null) xlSparklineStyle.LowMarkerColor = slg.LowMarkerColor.ToClosedXMLColor(); + if (slg.SeriesColor != null) xlSparklineStyle.SeriesColor = slg.SeriesColor.ToClosedXMLColor(); + if (slg.NegativeColor != null) xlSparklineStyle.NegativeColor = slg.NegativeColor.ToClosedXMLColor(); + if (slg.MarkersColor != null) xlSparklineStyle.MarkersColor = slg.MarkersColor.ToClosedXMLColor(); + xlSparklineGroup.Style = xlSparklineStyle; + + if (slg.DisplayHidden != null) xlSparklineGroup.DisplayHidden = slg.DisplayHidden; + if (slg.LineWeight != null) xlSparklineGroup.LineWeight = slg.LineWeight; + if (slg.Type != null) xlSparklineGroup.Type = slg.Type.Value.ToClosedXml(); + if (slg.DisplayEmptyCellsAs != null) xlSparklineGroup.DisplayEmptyCellsAs = slg.DisplayEmptyCellsAs.Value.ToClosedXml(); + + xlSparklineGroup.ShowMarkers = XLSparklineMarkers.None; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.Markers, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.Markers; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.High, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.HighPoint; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.Low, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.LowPoint; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.First, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.FirstPoint; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.Last, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.LastPoint; + if (OpenXmlHelper.GetBooleanValueAsBool(slg.Negative, false)) xlSparklineGroup.ShowMarkers |= XLSparklineMarkers.NegativePoints; + + if (slg.AxisColor != null) xlSparklineGroup.HorizontalAxis.Color = ExtractColor(slg.AxisColor.Rgb.Value); + if (slg.DisplayXAxis != null) xlSparklineGroup.HorizontalAxis.IsVisible = slg.DisplayXAxis; + if (slg.RightToLeft != null) xlSparklineGroup.HorizontalAxis.RightToLeft = slg.RightToLeft; + + if (slg.ManualMax != null) xlSparklineGroup.VerticalAxis.ManualMax = slg.ManualMax; + if (slg.ManualMin != null) xlSparklineGroup.VerticalAxis.ManualMin = slg.ManualMin; + if (slg.MinAxisType != null) xlSparklineGroup.VerticalAxis.MinAxisType = slg.MinAxisType.Value.ToClosedXml(); + if (slg.MaxAxisType != null) xlSparklineGroup.VerticalAxis.MaxAxisType = slg.MaxAxisType.Value.ToClosedXml(); + + slg.Descendants().SelectMany(sls => sls.Descendants()) + .ForEach(sl => xlSparklineGroup.Add(sl.ReferenceSequence?.Text, sl.Formula?.Text)); + } + } + + private static void LoadWorkbookProtection(WorkbookProtection wp, XLWorkbook wb) + { + if (wp == null) return; + + wb.Protection.IsProtected = true; + + var algorithmName = wp.WorkbookAlgorithmName?.Value ?? string.Empty; + if (String.IsNullOrEmpty(algorithmName)) + { + wb.Protection.PasswordHash = wp.WorkbookPassword?.Value ?? string.Empty; + wb.Protection.Base64EncodedSalt = string.Empty; + } + else if (DescribedEnumParser.IsValidDescription(algorithmName)) + { + wb.Protection.Algorithm = DescribedEnumParser.FromDescription(algorithmName); + wb.Protection.PasswordHash = wp.WorkbookHashValue?.Value ?? string.Empty; + wb.Protection.SpinCount = wp.WorkbookSpinCount?.Value ?? 0; + wb.Protection.Base64EncodedSalt = wp.WorkbookSaltValue?.Value ?? string.Empty; + } + + wb.Protection.AllowElement(XLWorkbookProtectionElements.Structure, !OpenXmlHelper.GetBooleanValueAsBool(wp.LockStructure, false)); + wb.Protection.AllowElement(XLWorkbookProtectionElements.Windows, !OpenXmlHelper.GetBooleanValueAsBool(wp.LockWindows, false)); + } + + private static XLFormula GetFormula(String value) + { + var formula = new XLFormula(); + formula._value = value; + formula.IsFormula = !(value[0] == '"' && value.EndsWith("\"")); + return formula; + } + + private void ExtractConditionalFormatValueObjects(XLConditionalFormat conditionalFormat, OpenXmlElement element) + { + foreach (var c in element.Elements()) + { + if (c.Type != null) + conditionalFormat.ContentTypes.Add(c.Type.Value.ToClosedXml()); + if (c.Val != null) + conditionalFormat.Values.Add(new XLFormula { Value = c.Val.Value }); + else + conditionalFormat.Values.Add(null); + + if (c.GreaterThanOrEqual != null) + conditionalFormat.IconSetOperators.Add(c.GreaterThanOrEqual.Value ? XLCFIconSetOperator.EqualOrGreaterThan : XLCFIconSetOperator.GreaterThan); + else + conditionalFormat.IconSetOperators.Add(XLCFIconSetOperator.EqualOrGreaterThan); + } + foreach (var c in element.Elements()) + { + conditionalFormat.Colors.Add(c.ToClosedXMLColor(_colorList)); + } + } + + private static void LoadHyperlinks(Hyperlinks hyperlinks, WorksheetPart worksheetPart, XLWorksheet ws) + { + var hyperlinkDictionary = new Dictionary(); + if (worksheetPart.HyperlinkRelationships != null) + hyperlinkDictionary = worksheetPart.HyperlinkRelationships.ToDictionary(hr => hr.Id, hr => hr.Uri); + + if (hyperlinks == null) return; + + foreach (Hyperlink hl in hyperlinks.Elements()) + { + if (hl.Reference.Value.Equals("#REF")) continue; + String tooltip = hl.Tooltip != null ? hl.Tooltip.Value : String.Empty; + var xlRange = ws.Range(hl.Reference.Value); + foreach (XLCell xlCell in xlRange.Cells()) + { + xlCell.SettingHyperlink = true; + + if (hl.Id != null) + xlCell.Hyperlink = new XLHyperlink(hyperlinkDictionary[hl.Id], tooltip); + else if (hl.Location != null) + xlCell.Hyperlink = new XLHyperlink(hl.Location.Value, tooltip); + else + xlCell.Hyperlink = new XLHyperlink(hl.Reference.Value, tooltip); + + xlCell.SettingHyperlink = false; + } + } + } + + private static void LoadColumnBreaks(ColumnBreaks columnBreaks, XLWorksheet ws) + { + if (columnBreaks == null) return; + + foreach (Break columnBreak in columnBreaks.Elements().Where(columnBreak => columnBreak.Id != null)) + { + ws.PageSetup.ColumnBreaks.Add(Int32.Parse(columnBreak.Id.InnerText)); + } + } + + private static void LoadRowBreaks(RowBreaks rowBreaks, XLWorksheet ws) + { + if (rowBreaks == null) return; + + foreach (Break rowBreak in rowBreaks.Elements()) + ws.PageSetup.RowBreaks.Add(Int32.Parse(rowBreak.Id.InnerText)); + } + + private void LoadSheetProperties(SheetProperties sheetProperty, XLWorksheet ws, out PageSetupProperties pageSetupProperties) + { + pageSetupProperties = null; + if (sheetProperty == null) return; + + if (sheetProperty.TabColor != null) + ws.TabColor = sheetProperty.TabColor.ToClosedXMLColor(_colorList); + + if (sheetProperty.OutlineProperties != null) + { + if (sheetProperty.OutlineProperties.SummaryBelow != null) + { + ws.Outline.SummaryVLocation = sheetProperty.OutlineProperties.SummaryBelow + ? XLOutlineSummaryVLocation.Bottom + : XLOutlineSummaryVLocation.Top; + } + + if (sheetProperty.OutlineProperties.SummaryRight != null) + { + ws.Outline.SummaryHLocation = sheetProperty.OutlineProperties.SummaryRight + ? XLOutlineSummaryHLocation.Right + : XLOutlineSummaryHLocation.Left; + } + } + + if (sheetProperty.PageSetupProperties != null) + pageSetupProperties = sheetProperty.PageSetupProperties; + } + + private static void LoadHeaderFooter(HeaderFooter headerFooter, XLWorksheet ws) + { + if (headerFooter == null) return; + + if (headerFooter.AlignWithMargins != null) + ws.PageSetup.AlignHFWithMargins = headerFooter.AlignWithMargins; + if (headerFooter.ScaleWithDoc != null) + ws.PageSetup.ScaleHFWithDocument = headerFooter.ScaleWithDoc; + + if (headerFooter.DifferentFirst != null) + ws.PageSetup.DifferentFirstPageOnHF = headerFooter.DifferentFirst; + if (headerFooter.DifferentOddEven != null) + ws.PageSetup.DifferentOddEvenPagesOnHF = headerFooter.DifferentOddEven; + + // Footers + var xlFooter = (XLHeaderFooter)ws.PageSetup.Footer; + var evenFooter = headerFooter.EvenFooter; + if (evenFooter != null) + xlFooter.SetInnerText(XLHFOccurrence.EvenPages, evenFooter.Text); + var oddFooter = headerFooter.OddFooter; + if (oddFooter != null) + xlFooter.SetInnerText(XLHFOccurrence.OddPages, oddFooter.Text); + var firstFooter = headerFooter.FirstFooter; + if (firstFooter != null) + xlFooter.SetInnerText(XLHFOccurrence.FirstPage, firstFooter.Text); + // Headers + var xlHeader = (XLHeaderFooter)ws.PageSetup.Header; + var evenHeader = headerFooter.EvenHeader; + if (evenHeader != null) + xlHeader.SetInnerText(XLHFOccurrence.EvenPages, evenHeader.Text); + var oddHeader = headerFooter.OddHeader; + if (oddHeader != null) + xlHeader.SetInnerText(XLHFOccurrence.OddPages, oddHeader.Text); + var firstHeader = headerFooter.FirstHeader; + if (firstHeader != null) + xlHeader.SetInnerText(XLHFOccurrence.FirstPage, firstHeader.Text); + + ((XLHeaderFooter)ws.PageSetup.Header).SetAsInitial(); + ((XLHeaderFooter)ws.PageSetup.Footer).SetAsInitial(); + } + + private static void LoadPageSetup(PageSetup pageSetup, XLWorksheet ws, PageSetupProperties pageSetupProperties) + { + if (pageSetup == null) return; + + if (pageSetup.PaperSize != null) + ws.PageSetup.PaperSize = (XLPaperSize)Int32.Parse(pageSetup.PaperSize.InnerText); + if (pageSetup.Scale != null) + ws.PageSetup.Scale = Int32.Parse(pageSetup.Scale.InnerText); + if (pageSetupProperties != null && pageSetupProperties.FitToPage != null && pageSetupProperties.FitToPage.Value) + { + if (pageSetup.FitToWidth == null) + ws.PageSetup.PagesWide = 1; + else + ws.PageSetup.PagesWide = Int32.Parse(pageSetup.FitToWidth.InnerText); + + if (pageSetup.FitToHeight == null) + ws.PageSetup.PagesTall = 1; + else + ws.PageSetup.PagesTall = Int32.Parse(pageSetup.FitToHeight.InnerText); + } + if (pageSetup.PageOrder != null) + ws.PageSetup.PageOrder = pageSetup.PageOrder.Value.ToClosedXml(); + if (pageSetup.Orientation != null) + ws.PageSetup.PageOrientation = pageSetup.Orientation.Value.ToClosedXml(); + if (pageSetup.BlackAndWhite != null) + ws.PageSetup.BlackAndWhite = pageSetup.BlackAndWhite; + if (pageSetup.Draft != null) + ws.PageSetup.DraftQuality = pageSetup.Draft; + if (pageSetup.CellComments != null) + ws.PageSetup.ShowComments = pageSetup.CellComments.Value.ToClosedXml(); + if (pageSetup.Errors != null) + ws.PageSetup.PrintErrorValue = pageSetup.Errors.Value.ToClosedXml(); + if (pageSetup.HorizontalDpi != null) ws.PageSetup.HorizontalDpi = (Int32)pageSetup.HorizontalDpi.Value; + if (pageSetup.VerticalDpi != null) ws.PageSetup.VerticalDpi = (Int32)pageSetup.VerticalDpi.Value; + if (pageSetup.FirstPageNumber?.HasValue ?? false) + ws.PageSetup.FirstPageNumber = pageSetup.FirstPageNumber.Value; + } + + private static void LoadPageMargins(PageMargins pageMargins, XLWorksheet ws) + { + if (pageMargins == null) return; + + if (pageMargins.Bottom != null) + ws.PageSetup.Margins.Bottom = pageMargins.Bottom; + if (pageMargins.Footer != null) + ws.PageSetup.Margins.Footer = pageMargins.Footer; + if (pageMargins.Header != null) + ws.PageSetup.Margins.Header = pageMargins.Header; + if (pageMargins.Left != null) + ws.PageSetup.Margins.Left = pageMargins.Left; + if (pageMargins.Right != null) + ws.PageSetup.Margins.Right = pageMargins.Right; + if (pageMargins.Top != null) + ws.PageSetup.Margins.Top = pageMargins.Top; + } + + private static void LoadPrintOptions(PrintOptions printOptions, XLWorksheet ws) + { + if (printOptions == null) return; + + if (printOptions.GridLines != null) + ws.PageSetup.ShowGridlines = printOptions.GridLines; + if (printOptions.HorizontalCentered != null) + ws.PageSetup.CenterHorizontally = printOptions.HorizontalCentered; + if (printOptions.VerticalCentered != null) + ws.PageSetup.CenterVertically = printOptions.VerticalCentered; + if (printOptions.Headings != null) + ws.PageSetup.ShowRowAndColumnHeadings = printOptions.Headings; + } + + private static void LoadSheetViews(SheetViews sheetViews, XLWorksheet ws) + { + if (sheetViews == null) return; + + var sheetView = sheetViews.Elements().FirstOrDefault(); + + if (sheetView == null) return; + + if (sheetView.RightToLeft != null) ws.RightToLeft = sheetView.RightToLeft.Value; + if (sheetView.ShowFormulas != null) ws.ShowFormulas = sheetView.ShowFormulas.Value; + if (sheetView.ShowGridLines != null) ws.ShowGridLines = sheetView.ShowGridLines.Value; + if (sheetView.ShowOutlineSymbols != null) + ws.ShowOutlineSymbols = sheetView.ShowOutlineSymbols.Value; + if (sheetView.ShowRowColHeaders != null) ws.ShowRowColHeaders = sheetView.ShowRowColHeaders.Value; + if (sheetView.ShowRuler != null) ws.ShowRuler = sheetView.ShowRuler.Value; + if (sheetView.ShowWhiteSpace != null) ws.ShowWhiteSpace = sheetView.ShowWhiteSpace.Value; + if (sheetView.ShowZeros != null) ws.ShowZeros = sheetView.ShowZeros.Value; + if (sheetView.TabSelected != null) ws.TabSelected = sheetView.TabSelected.Value; + + var selection = sheetView.Elements().FirstOrDefault(); + if (selection != null) + { + if (selection.SequenceOfReferences != null) + ws.Ranges(selection.SequenceOfReferences.InnerText.Replace(" ", ",")).Select(); + + if (selection.ActiveCell != null) + ws.Cell(selection.ActiveCell).SetActive(); + } + + if (sheetView.ZoomScale != null) + ws.SheetView.ZoomScale = (int)UInt32Value.ToUInt32(sheetView.ZoomScale); + if (sheetView.ZoomScaleNormal != null) + ws.SheetView.ZoomScaleNormal = (int)UInt32Value.ToUInt32(sheetView.ZoomScaleNormal); + if (sheetView.ZoomScalePageLayoutView != null) + ws.SheetView.ZoomScalePageLayoutView = (int)UInt32Value.ToUInt32(sheetView.ZoomScalePageLayoutView); + if (sheetView.ZoomScaleSheetLayoutView != null) + ws.SheetView.ZoomScaleSheetLayoutView = (int)UInt32Value.ToUInt32(sheetView.ZoomScaleSheetLayoutView); + + var pane = sheetView.Elements().FirstOrDefault(); + if (new[] { PaneStateValues.Frozen, PaneStateValues.FrozenSplit }.Contains(pane?.State?.Value ?? PaneStateValues.Split)) + { + if (pane.HorizontalSplit != null) + ws.SheetView.SplitColumn = (Int32)pane.HorizontalSplit.Value; + if (pane.VerticalSplit != null) + ws.SheetView.SplitRow = (Int32)pane.VerticalSplit.Value; + } + + if (XLHelper.IsValidA1Address(sheetView.TopLeftCell)) + ws.SheetView.TopLeftCellAddress = ws.Cell(sheetView.TopLeftCell.Value).Address; + } + + private void SetProperties(SpreadsheetDocument dSpreadsheet) + { + var p = dSpreadsheet.PackageProperties; + Properties.Author = p.Creator; + Properties.Category = p.Category; + Properties.Comments = p.Description; + if (p.Created != null) + Properties.Created = p.Created.Value; + if (p.Modified != null) + Properties.Modified = p.Modified.Value; + Properties.Keywords = p.Keywords; + Properties.LastModifiedBy = p.LastModifiedBy; + Properties.Status = p.ContentStatus; + Properties.Subject = p.Subject; + Properties.Title = p.Title; + } + + private void ApplyStyle(IXLStylized xlStylized, Int32 styleIndex, Stylesheet s, Fills fills, Borders borders, + Fonts fonts, NumberingFormats numberingFormats) + { + if (s == null) return; //No Stylesheet, no Styles + + var cellFormat = (CellFormat)s.CellFormats.ElementAt(styleIndex); + + var xlStyle = XLStyle.Default.Key; + + xlStyle.IncludeQuotePrefix = OpenXmlHelper.GetBooleanValueAsBool(cellFormat.QuotePrefix, false); + + if (cellFormat.ApplyProtection != null) + { + var protection = cellFormat.Protection; + + if (protection == null) + xlStyle.Protection = XLProtectionValue.Default.Key; + else + { + xlStyle.Protection = new XLProtectionKey + { + Hidden = protection.Hidden != null && protection.Hidden.HasValue && + protection.Hidden.Value, + Locked = protection.Locked == null || + (protection.Locked.HasValue && protection.Locked.Value) + }; + } + } + + if (UInt32HasValue(cellFormat.FillId)) + { + var fill = (Fill)fills.ElementAt((Int32)cellFormat.FillId.Value); + if (fill.PatternFill != null) + { + LoadFill(fill, xlStylized.InnerStyle.Fill, differentialFillFormat: false); + } + xlStyle.Fill = (xlStylized.InnerStyle as XLStyle).Value.Key.Fill; + } + + var alignment = cellFormat.Alignment; + if (alignment != null) + { + var xlAlignment = xlStyle.Alignment; + if (alignment.Horizontal != null) + xlAlignment.Horizontal = alignment.Horizontal.Value.ToClosedXml(); + if (alignment.Indent != null && alignment.Indent != 0) + xlAlignment.Indent = Int32.Parse(alignment.Indent.ToString()); + if (alignment.JustifyLastLine != null) + xlAlignment.JustifyLastLine = alignment.JustifyLastLine; + if (alignment.ReadingOrder != null) + { + xlAlignment.ReadingOrder = + (XLAlignmentReadingOrderValues)Int32.Parse(alignment.ReadingOrder.ToString()); + } + if (alignment.RelativeIndent != null) + xlAlignment.RelativeIndent = alignment.RelativeIndent; + if (alignment.ShrinkToFit != null) + xlAlignment.ShrinkToFit = alignment.ShrinkToFit; + if (alignment.TextRotation != null) + xlAlignment.TextRotation = (Int32)alignment.TextRotation.Value; + if (alignment.Vertical != null) + xlAlignment.Vertical = alignment.Vertical.Value.ToClosedXml(); + if (alignment.WrapText != null) + xlAlignment.WrapText = alignment.WrapText; + + xlStyle.Alignment = xlAlignment; + } + + if (UInt32HasValue(cellFormat.BorderId)) + { + uint borderId = cellFormat.BorderId.Value; + var border = (Border)borders.ElementAt((Int32)borderId); + var xlBorder = xlStyle.Border; + if (border != null) + { + var bottomBorder = border.BottomBorder; + if (bottomBorder != null) + { + if (bottomBorder.Style != null) + xlBorder.BottomBorder = bottomBorder.Style.Value.ToClosedXml(); + + if (bottomBorder.Color != null) + xlBorder.BottomBorderColor = bottomBorder.Color.ToClosedXMLColor(_colorList).Key; + } + var topBorder = border.TopBorder; + if (topBorder != null) + { + if (topBorder.Style != null) + xlBorder.TopBorder = topBorder.Style.Value.ToClosedXml(); + if (topBorder.Color != null) + xlBorder.TopBorderColor = topBorder.Color.ToClosedXMLColor(_colorList).Key; + } + var leftBorder = border.LeftBorder; + if (leftBorder != null) + { + if (leftBorder.Style != null) + xlBorder.LeftBorder = leftBorder.Style.Value.ToClosedXml(); + if (leftBorder.Color != null) + xlBorder.LeftBorderColor = leftBorder.Color.ToClosedXMLColor(_colorList).Key; + } + var rightBorder = border.RightBorder; + if (rightBorder != null) + { + if (rightBorder.Style != null) + xlBorder.RightBorder = rightBorder.Style.Value.ToClosedXml(); + if (rightBorder.Color != null) + xlBorder.RightBorderColor = rightBorder.Color.ToClosedXMLColor(_colorList).Key; + } + var diagonalBorder = border.DiagonalBorder; + if (diagonalBorder != null) + { + if (diagonalBorder.Style != null) + xlBorder.DiagonalBorder = diagonalBorder.Style.Value.ToClosedXml(); + if (diagonalBorder.Color != null) + xlBorder.DiagonalBorderColor = diagonalBorder.Color.ToClosedXMLColor(_colorList).Key; + if (border.DiagonalDown != null) + xlBorder.DiagonalDown = border.DiagonalDown; + if (border.DiagonalUp != null) + xlBorder.DiagonalUp = border.DiagonalUp; + } + + xlStyle.Border = xlBorder; + } + } + + if (UInt32HasValue(cellFormat.FontId)) + { + var fontId = cellFormat.FontId; + var font = (DocumentFormat.OpenXml.Spreadsheet.Font)fonts.ElementAt((Int32)fontId.Value); + + var xlFont = xlStyle.Font; + if (font != null) + { + xlFont.Bold = GetBoolean(font.Bold); + + if (font.Color != null) + xlFont.FontColor = font.Color.ToClosedXMLColor(_colorList).Key; + + if (font.FontFamilyNumbering != null && (font.FontFamilyNumbering).Val != null) + { + xlFont.FontFamilyNumbering = + (XLFontFamilyNumberingValues)Int32.Parse((font.FontFamilyNumbering).Val.ToString()); + } + if (font.FontName != null) + { + if ((font.FontName).Val != null) + xlFont.FontName = (font.FontName).Val; + } + if (font.FontSize != null) + { + if ((font.FontSize).Val != null) + xlFont.FontSize = (font.FontSize).Val; + } + + xlFont.Italic = GetBoolean(font.Italic); + xlFont.Shadow = GetBoolean(font.Shadow); + xlFont.Strikethrough = GetBoolean(font.Strike); + + if (font.Underline != null) + { + xlFont.Underline = font.Underline.Val != null + ? (font.Underline).Val.Value.ToClosedXml() + : XLFontUnderlineValues.Single; + } + + if (font.VerticalTextAlignment != null) + { + xlFont.VerticalAlignment = font.VerticalTextAlignment.Val != null + ? (font.VerticalTextAlignment).Val.Value.ToClosedXml() + : XLFontVerticalTextAlignmentValues.Baseline; + } + + xlStyle.Font = xlFont; + } + } + + if (UInt32HasValue(cellFormat.NumberFormatId)) + { + var numberFormatId = cellFormat.NumberFormatId; + + string formatCode = String.Empty; + if (numberingFormats != null) + { + var numberingFormat = + numberingFormats.FirstOrDefault( + nf => + ((NumberingFormat)nf).NumberFormatId != null && + ((NumberingFormat)nf).NumberFormatId.Value == numberFormatId) as NumberingFormat; + + if (numberingFormat != null && numberingFormat.FormatCode != null) + formatCode = numberingFormat.FormatCode.Value; + } + + var xlNumberFormat = xlStyle.NumberFormat; + if (formatCode.Length > 0) + { + xlNumberFormat.Format = formatCode; + xlNumberFormat.NumberFormatId = -1; + } + else + xlNumberFormat.NumberFormatId = (Int32)numberFormatId.Value; + xlStyle.NumberFormat = xlNumberFormat; + } + + //When loading columns we must propagate style to each column but not deeper. In other cases we do not propagate at all. + if (xlStylized is IXLColumns columns) + columns.Cast().ForEach(col => col.InnerStyle = new XLStyle(col, xlStyle)); + else + xlStylized.InnerStyle = new XLStyle(xlStylized, xlStyle); + } + + private static Boolean UInt32HasValue(UInt32Value value) + { + return value != null && value.HasValue; + } + + private static Boolean GetBoolean(BooleanPropertyType property) + { + if (property != null) + { + if (property.Val != null) + return property.Val; + return true; + } + + return false; + } + } +} diff --git a/ClosedXML/Excel/XLWorkbook_Save.NestedTypes.cs b/ClosedXML/Excel/XLWorkbook_Save.NestedTypes.cs new file mode 100644 index 0000000..308ebe7 --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook_Save.NestedTypes.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + public partial class XLWorkbook + { + #region Nested type: SaveContext + + internal sealed class SaveContext + { + public SaveContext() + { + DifferentialFormats = new Dictionary(); + PivotTables = new Dictionary(); + RelIdGenerator = new RelIdGenerator(); + SharedFonts = new Dictionary(); + SharedNumberFormats = new Dictionary(); + SharedStyles = new Dictionary(); + TableId = 0; + TableNames = new HashSet(); + } + + public Dictionary DifferentialFormats { get; private set; } + public IDictionary PivotTables { get; private set; } + public RelIdGenerator RelIdGenerator { get; private set; } + public Dictionary SharedFonts { get; private set; } + public Dictionary SharedNumberFormats { get; private set; } + public Dictionary SharedStyles { get; private set; } + public uint TableId { get; set; } + public HashSet TableNames { get; private set; } + } + + #endregion Nested type: SaveContext + + #region Nested type: RelType + + internal enum RelType + { + Workbook//, Worksheet + } + + #endregion Nested type: RelType + + #region Nested type: RelIdGenerator + + internal sealed class RelIdGenerator + { + private readonly Dictionary> _relIds = new Dictionary>(); + + public void AddValues(IEnumerable values, RelType relType) + { + if (!_relIds.TryGetValue(relType, out List list)) + { + list = new List(); + _relIds.Add(relType, list); + } + list.AddRange(values.Where(v => !list.Contains(v))); + } + + public String GetNext() + { + return GetNext(RelType.Workbook); + } + + public String GetNext(RelType relType) + { + if (!_relIds.TryGetValue(relType, out List list)) + { + list = new List(); + _relIds.Add(relType, list); + } + + Int32 id = list.Count + 1; + while (true) + { + String relId = String.Concat("rId", id); + if (!list.Contains(relId)) + { + list.Add(relId); + return relId; + } + id++; + } + } + + public void Reset(RelType relType) + { + if (_relIds.ContainsKey(relType)) + _relIds.Remove(relType); + } + } + + #endregion Nested type: RelIdGenerator + + #region Nested type: FontInfo + + internal struct FontInfo + { + public XLFontValue Font; + public UInt32 FontId; + }; + + #endregion Nested type: FontInfo + + #region Nested type: FillInfo + + internal struct FillInfo + { + public XLFillValue Fill; + public UInt32 FillId; + } + + #endregion Nested type: FillInfo + + #region Nested type: BorderInfo + + internal struct BorderInfo + { + public XLBorderValue Border; + public UInt32 BorderId; + } + + #endregion Nested type: BorderInfo + + #region Nested type: NumberFormatInfo + + internal struct NumberFormatInfo + { + public XLNumberFormatValue NumberFormat; + public Int32 NumberFormatId; + } + + #endregion Nested type: NumberFormatInfo + + #region Nested type: StyleInfo + + internal struct StyleInfo + { + public UInt32 BorderId; + public UInt32 FillId; + public UInt32 FontId; + public Boolean IncludeQuotePrefix; + public Int32 NumberFormatId; + public XLStyleValue Style; + public UInt32 StyleId; + } + + #endregion Nested type: StyleInfo + + #region Nested type: Pivot tables + + internal struct PivotTableFieldInfo + { + public XLDataType DataType; + public Boolean MixedDataType; + public IEnumerable DistinctValues; + public Boolean IsTotallyBlankField; + } + + internal struct PivotTableInfo + { + public IDictionary Fields; + public Guid Guid; + } + + #endregion Nested type: Pivot tables + } +} diff --git a/ClosedXML/Excel/XLWorkbook_Save.cs b/ClosedXML/Excel/XLWorkbook_Save.cs new file mode 100644 index 0000000..8efbc8f --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook_Save.cs @@ -0,0 +1,6395 @@ +using ClosedXML.Excel.ContentManagers; +using ClosedXML.Excel.Exceptions; +using ClosedXML.Extensions; +using ClosedXML.Utils; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.CustomProperties; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.ExtendedProperties; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Spreadsheet; +using DocumentFormat.OpenXml.Validation; +using DocumentFormat.OpenXml.VariantTypes; +using DocumentFormat.OpenXml.Vml.Office; +using DocumentFormat.OpenXml.Vml.Spreadsheet; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml; +using System.Xml.Linq; +using Anchor = DocumentFormat.OpenXml.Vml.Spreadsheet.Anchor; +using BackgroundColor = DocumentFormat.OpenXml.Spreadsheet.BackgroundColor; +using BottomBorder = DocumentFormat.OpenXml.Spreadsheet.BottomBorder; +using Break = DocumentFormat.OpenXml.Spreadsheet.Break; +using Field = DocumentFormat.OpenXml.Spreadsheet.Field; +using Fill = DocumentFormat.OpenXml.Spreadsheet.Fill; +using Fonts = DocumentFormat.OpenXml.Spreadsheet.Fonts; +using FontScheme = DocumentFormat.OpenXml.Drawing.FontScheme; +using ForegroundColor = DocumentFormat.OpenXml.Spreadsheet.ForegroundColor; +using GradientFill = DocumentFormat.OpenXml.Drawing.GradientFill; +using GradientStop = DocumentFormat.OpenXml.Drawing.GradientStop; +using Hyperlink = DocumentFormat.OpenXml.Spreadsheet.Hyperlink; +using LeftBorder = DocumentFormat.OpenXml.Spreadsheet.LeftBorder; +using OfficeExcel = DocumentFormat.OpenXml.Office.Excel; +using Outline = DocumentFormat.OpenXml.Drawing.Outline; +using Path = System.IO.Path; +using PatternFill = DocumentFormat.OpenXml.Spreadsheet.PatternFill; +using Properties = DocumentFormat.OpenXml.ExtendedProperties.Properties; +using RightBorder = DocumentFormat.OpenXml.Spreadsheet.RightBorder; +using Run = DocumentFormat.OpenXml.Spreadsheet.Run; +using RunProperties = DocumentFormat.OpenXml.Spreadsheet.RunProperties; +using Table = DocumentFormat.OpenXml.Spreadsheet.Table; +using Text = DocumentFormat.OpenXml.Spreadsheet.Text; +using TopBorder = DocumentFormat.OpenXml.Spreadsheet.TopBorder; +using Underline = DocumentFormat.OpenXml.Spreadsheet.Underline; +using VerticalTextAlignment = DocumentFormat.OpenXml.Spreadsheet.VerticalTextAlignment; +using Vml = DocumentFormat.OpenXml.Vml; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; +using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet; + +namespace ClosedXML.Excel +{ + public partial class XLWorkbook + { + private const Double ColumnWidthOffset = 0.710625; + + //private Dictionary context.SharedStyles; + + private static readonly EnumValue CvSharedString = new EnumValue(CellValues.SharedString); + private static readonly EnumValue CvInlineString = new EnumValue(CellValues.InlineString); + private static readonly EnumValue CvNumber = new EnumValue(CellValues.Number); + private static readonly EnumValue CvDate = new EnumValue(CellValues.Date); + private static readonly EnumValue CvBoolean = new EnumValue(CellValues.Boolean); + + private static EnumValue GetCellValueType(XLCell xlCell) + { + switch (xlCell.DataType) + { + case XLDataType.Text: + return xlCell.ShareString ? CvSharedString : CvInlineString; + + case XLDataType.Number: + return CvNumber; + + case XLDataType.DateTime: + return CvDate; + + case XLDataType.Boolean: + return CvBoolean; + + case XLDataType.TimeSpan: + return CvNumber; + + default: + throw new NotImplementedException(); + } + } + + private Boolean Validate(SpreadsheetDocument package) + { + var backupCulture = Thread.CurrentThread.CurrentCulture; + + IEnumerable errors; + try + { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + var validator = new OpenXmlValidator(); + errors = validator.Validate(package); + } + finally + { + Thread.CurrentThread.CurrentCulture = backupCulture; + } + + if (errors.Any()) + { + var message = string.Join("\r\n", errors.Select(e => string.Format("Part {0}, Path {1}: {2}", e.Part.Uri, e.Path.XPath, e.Description)).ToArray()); + throw new ApplicationException(message); + } + return true; + } + + private void CreatePackage(String filePath, SpreadsheetDocumentType spreadsheetDocumentType, SaveOptions options) + { + PathHelper.CreateDirectory(Path.GetDirectoryName(filePath)); + var package = File.Exists(filePath) + ? SpreadsheetDocument.Open(filePath, true) + : SpreadsheetDocument.Create(filePath, spreadsheetDocumentType); + + using (package) + { + if (package.DocumentType != spreadsheetDocumentType) + { + package.ChangeDocumentType(spreadsheetDocumentType); + } + + CreateParts(package, options); + if (options.ValidatePackage) Validate(package); + } + } + + private void CreatePackage(Stream stream, bool newStream, SpreadsheetDocumentType spreadsheetDocumentType, SaveOptions options) + { + var package = newStream + ? SpreadsheetDocument.Create(stream, spreadsheetDocumentType) + : SpreadsheetDocument.Open(stream, true); + + using (package) + { + CreateParts(package, options); + if (options.ValidatePackage) Validate(package); + } + } + + // http://blogs.msdn.com/b/vsod/archive/2010/02/05/how-to-delete-a-worksheet-from-excel-using-open-xml-sdk-2-0.aspx + private void DeleteSheetAndDependencies(WorkbookPart wbPart, string sheetId) + { + //Get the SheetToDelete from workbook.xml + Sheet worksheet = wbPart.Workbook.Descendants().FirstOrDefault(s => s.Id == sheetId); + if (worksheet == null) + return; + + string sheetName = worksheet.Name; + // Get the pivot Table Parts + var pvtTableCacheParts = wbPart.PivotTableCacheDefinitionParts; + var pvtTableCacheDefinitionPart = new Dictionary(); + foreach (PivotTableCacheDefinitionPart Item in pvtTableCacheParts) + { + PivotCacheDefinition pvtCacheDef = Item.PivotCacheDefinition; + //Check if this CacheSource is linked to SheetToDelete + if (pvtCacheDef.Descendants().Any(cacheSource => cacheSource.WorksheetSource?.Sheet == sheetName)) + { + pvtTableCacheDefinitionPart.Add(Item, Item.ToString()); + } + } + foreach (var Item in pvtTableCacheDefinitionPart) + { + wbPart.DeletePart(Item.Key); + } + + // Remove the sheet reference from the workbook. + WorksheetPart worksheetPart = (WorksheetPart)(wbPart.GetPartById(sheetId)); + worksheet.Remove(); + + // Delete the worksheet part. + wbPart.DeletePart(worksheetPart); + + //Get the DefinedNames + var definedNames = wbPart.Workbook.Descendants().FirstOrDefault(); + if (definedNames != null) + { + List defNamesToDelete = new List(); + + foreach (var Item in definedNames.OfType()) + { + // This condition checks to delete only those names which are part of Sheet in question + if (Item.Text.Contains(worksheet.Name + "!")) + defNamesToDelete.Add(Item); + } + + foreach (DefinedName Item in defNamesToDelete) + { + Item.Remove(); + } + } + // Get the CalculationChainPart + //Note: An instance of this part type contains an ordered set of references to all cells in all worksheets in the + //workbook whose value is calculated from any formula + + CalculationChainPart calChainPart; + calChainPart = wbPart.CalculationChainPart; + if (calChainPart != null) + { + var calChainEntries = calChainPart.CalculationChain.Descendants().Where(c => c.SheetId == sheetId); + List calcsToDelete = new List(); + foreach (CalculationCell Item in calChainEntries) + calcsToDelete.Add(Item); + + foreach (CalculationCell Item in calcsToDelete) + Item.Remove(); + + if (!calChainPart.CalculationChain.Any()) + wbPart.DeletePart(calChainPart); + } + } + + // Adds child parts and generates content of the specified part. + private void CreateParts(SpreadsheetDocument document, SaveOptions options) + { + this.SuspendEvents(); + + var context = new SaveContext(); + + var workbookPart = document.WorkbookPart ?? document.AddWorkbookPart(); + + var worksheets = WorksheetsInternal; + + var partsToRemove = workbookPart.Parts.Where(s => worksheets.Deleted.Contains(s.RelationshipId)).ToList(); + + var pivotCacheDefinitionsToRemove = partsToRemove.SelectMany(s => ((WorksheetPart)s.OpenXmlPart).PivotTableParts.Select(pt => pt.PivotTableCacheDefinitionPart)).Distinct().ToList(); + pivotCacheDefinitionsToRemove.ForEach(c => workbookPart.DeletePart(c)); + + if (workbookPart.Workbook != null && workbookPart.Workbook.PivotCaches != null) + { + var pivotCachesToRemove = workbookPart.Workbook.PivotCaches.Where(pc => pivotCacheDefinitionsToRemove.Select(pcd => workbookPart.GetIdOfPart(pcd)).ToList().Contains(((PivotCache)pc).Id)).Distinct().ToList(); + pivotCachesToRemove.ForEach(c => workbookPart.Workbook.PivotCaches.RemoveChild(c)); + } + + worksheets.Deleted.ToList().ForEach(ws => DeleteSheetAndDependencies(workbookPart, ws)); + + // Ensure all RelId's have been added to the context + context.RelIdGenerator.AddValues(workbookPart.Parts.Select(p => p.RelationshipId), RelType.Workbook); + context.RelIdGenerator.AddValues(WorksheetsInternal.Cast().Where(ws => !String.IsNullOrWhiteSpace(ws.RelId)).Select(ws => ws.RelId), RelType.Workbook); + context.RelIdGenerator.AddValues(WorksheetsInternal.Cast().Where(ws => !String.IsNullOrWhiteSpace(ws.LegacyDrawingId)).Select(ws => ws.LegacyDrawingId), RelType.Workbook); + context.RelIdGenerator.AddValues(WorksheetsInternal + .Cast() + .SelectMany(ws => ws.Tables.Cast()) + .Where(t => !String.IsNullOrWhiteSpace(t.RelId)) + .Select(t => t.RelId), RelType.Workbook); + + var extendedFilePropertiesPart = document.ExtendedFilePropertiesPart ?? + document.AddNewPart( + context.RelIdGenerator.GetNext(RelType.Workbook)); + + GenerateExtendedFilePropertiesPartContent(extendedFilePropertiesPart); + + GenerateWorkbookPartContent(workbookPart, options, context); + + var sharedStringTablePart = workbookPart.SharedStringTablePart ?? + workbookPart.AddNewPart( + context.RelIdGenerator.GetNext(RelType.Workbook)); + + GenerateSharedStringTablePartContent(sharedStringTablePart, context); + + var workbookStylesPart = workbookPart.WorkbookStylesPart ?? + workbookPart.AddNewPart( + context.RelIdGenerator.GetNext(RelType.Workbook)); + + GenerateWorkbookStylesPartContent(workbookStylesPart, context); + + var cacheRelIds = WorksheetsInternal + .Cast() + .SelectMany(s => s.PivotTables.Cast().Select(pt => pt.WorkbookCacheRelId)) + .Where(relId => !string.IsNullOrWhiteSpace(relId)) + .Distinct(); + + foreach (var relId in cacheRelIds) + { + if (workbookPart.GetPartById(relId) is PivotTableCacheDefinitionPart pivotTableCacheDefinitionPart) + pivotTableCacheDefinitionPart.PivotCacheDefinition.CacheFields.RemoveAllChildren(); + } + + foreach (var worksheet in WorksheetsInternal.Cast().OrderBy(w => w.Position)) + { + WorksheetPart worksheetPart; + var wsRelId = worksheet.RelId; + if (workbookPart.Parts.Any(p => p.RelationshipId == wsRelId)) + worksheetPart = (WorksheetPart)workbookPart.GetPartById(wsRelId); + else + worksheetPart = workbookPart.AddNewPart(wsRelId); + + context.RelIdGenerator.AddValues(worksheetPart.HyperlinkRelationships.Select(hr => hr.Id), RelType.Workbook); + context.RelIdGenerator.AddValues(worksheetPart.Parts.Select(p => p.RelationshipId), RelType.Workbook); + if (worksheetPart.DrawingsPart != null) + context.RelIdGenerator.AddValues(worksheetPart.DrawingsPart.Parts.Select(p => p.RelationshipId), RelType.Workbook); + + var worksheetHasComments = worksheet.Internals.CellsCollection.GetCells(c => c.HasComment).Any(); + + var commentsPart = worksheetPart.WorksheetCommentsPart; + var vmlDrawingPart = worksheetPart.VmlDrawingParts.FirstOrDefault(); + var hasAnyVmlElements = DeleteExistingComments(worksheetPart, worksheet, commentsPart, vmlDrawingPart); + + if (worksheetHasComments) + { + if (commentsPart == null) + { + commentsPart = worksheetPart.AddNewPart(context.RelIdGenerator.GetNext(RelType.Workbook)); + commentsPart.Comments = new Comments(); + } + + if (vmlDrawingPart == null) + { + if (String.IsNullOrWhiteSpace(worksheet.LegacyDrawingId)) + { + worksheet.LegacyDrawingId = context.RelIdGenerator.GetNext(RelType.Workbook); + worksheet.LegacyDrawingIsNew = true; + } + + vmlDrawingPart = worksheetPart.AddNewPart(worksheet.LegacyDrawingId); + } + + GenerateWorksheetCommentsPartContent(commentsPart, worksheet); + hasAnyVmlElements = GenerateVmlDrawingPartContent(vmlDrawingPart, worksheet); + } + + // Remove empty parts + if (commentsPart != null && (commentsPart.RootElement?.ChildElements?.Count ?? 0) == 0) + worksheetPart.DeletePart(commentsPart); + + if (!hasAnyVmlElements && vmlDrawingPart != null) + worksheetPart.DeletePart(vmlDrawingPart); + + GenerateWorksheetPartContent(worksheetPart, worksheet, options, context); + + if (worksheet.PivotTables.Any()) + { + GeneratePivotTables(workbookPart, worksheetPart, worksheet, context); + } + + // Remove any orphaned references - maybe more types? + foreach (var orphan in worksheetPart.Worksheet.OfType().Where(lg => worksheetPart.Parts.All(p => p.RelationshipId != lg.Id))) + worksheetPart.Worksheet.RemoveChild(orphan); + + foreach (var orphan in worksheetPart.Worksheet.OfType().Where(d => worksheetPart.Parts.All(p => p.RelationshipId != d.Id))) + worksheetPart.Worksheet.RemoveChild(orphan); + } + + // Remove empty pivot cache part + if (workbookPart.Workbook.PivotCaches != null && !workbookPart.Workbook.PivotCaches.Any()) + workbookPart.Workbook.RemoveChild(workbookPart.Workbook.PivotCaches); + + if (options.GenerateCalculationChain) + GenerateCalculationChainPartContent(workbookPart, context); + else + DeleteCalculationChainPartContent(workbookPart, context); + + if (workbookPart.ThemePart == null) + { + var themePart = workbookPart.AddNewPart(context.RelIdGenerator.GetNext(RelType.Workbook)); + GenerateThemePartContent(themePart); + } + + // Custom properties + if (CustomProperties.Any()) + { + var customFilePropertiesPart = + document.CustomFilePropertiesPart ?? document.AddNewPart(context.RelIdGenerator.GetNext(RelType.Workbook)); + + GenerateCustomFilePropertiesPartContent(customFilePropertiesPart); + } + else + { + if (document.CustomFilePropertiesPart != null) + document.DeletePart(document.CustomFilePropertiesPart); + } + SetPackageProperties(document); + + // Clear list of deleted worksheets to prevent errors on multiple saves + worksheets.Deleted.Clear(); + + this.ResumeEvents(); + } + + private bool DeleteExistingComments(WorksheetPart worksheetPart, XLWorksheet worksheet, WorksheetCommentsPart commentsPart, VmlDrawingPart vmlDrawingPart) + { + // Nuke existing comments + if (commentsPart != null) + { + commentsPart.Comments = new Comments(); + } + + if (vmlDrawingPart == null) + return false; + + // Nuke the VmlDrawingPart elements for comments. + using (var vmlStream = vmlDrawingPart.GetStream(FileMode.Open)) + { + var xdoc = XDocumentExtensions.Load(vmlStream); + if (xdoc == null) + return false; + + // Remove existing shapes for comments + xdoc.Root + .Elements() + .Where(e => e.Name.LocalName == "shapetype" && e.Attribute("id").Value == XLConstants.Comment.ShapeTypeId) + .Remove(); + + xdoc.Root + .Elements() + .Where(e => e.Name.LocalName == "shape" && e.Attribute("type").Value == "#" + XLConstants.Comment.ShapeTypeId) + .Remove(); + + vmlStream.Position = 0; + + using (var writer = new XmlTextWriter(vmlStream, Encoding.UTF8)) + { + var contents = xdoc.ToString(); + writer.WriteRaw(contents); + vmlStream.SetLength(contents.Length); + } + + return xdoc.Root.HasElements; + } + } + + private static void GenerateTables(XLWorksheet worksheet, WorksheetPart worksheetPart, SaveContext context, XLWorksheetContentManager cm) + { + var tables = worksheet.Tables as XLTables; + + var emptyTable = tables.FirstOrDefault(t => t.DataRange == null); + if (emptyTable != null) + throw new EmptyTableException($"Table '{emptyTable.Name}' should have at least 1 row."); + + TableParts tableParts; + if (worksheetPart.Worksheet.Elements().Any()) + { + tableParts = worksheetPart.Worksheet.Elements().First(); + } + else + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.TableParts); + tableParts = new TableParts(); + worksheetPart.Worksheet.InsertAfter(tableParts, previousElement); + } + cm.SetElement(XLWorksheetContents.TableParts, tableParts); + + foreach (var deletedTableRelId in tables.Deleted) + { + if (worksheetPart.TableDefinitionParts != null) + { + var tableDefinitionPart = worksheetPart.GetPartById(deletedTableRelId); + worksheetPart.DeletePart(tableDefinitionPart); + + var tablePartsToRemove = tableParts.OfType().Where(tp => tp.Id?.Value == deletedTableRelId).ToList(); + tablePartsToRemove.ForEach(tp => tableParts.RemoveChild(tp)); + } + } + + tables.Deleted.Clear(); + + foreach (var xlTable in tables.Cast()) + { + if (String.IsNullOrEmpty(xlTable.RelId)) + xlTable.RelId = context.RelIdGenerator.GetNext(RelType.Workbook); + + var relId = xlTable.RelId; + + TableDefinitionPart tableDefinitionPart; + if (worksheetPart.HasPartWithId(relId)) + tableDefinitionPart = worksheetPart.GetPartById(relId) as TableDefinitionPart; + else + tableDefinitionPart = worksheetPart.AddNewPart(relId); + + GenerateTableDefinitionPartContent(tableDefinitionPart, xlTable, context); + + if (!tableParts.OfType().Any(tp => tp.Id == xlTable.RelId)) + { + var tablePart = new TablePart { Id = xlTable.RelId }; + tableParts.AppendChild(tablePart); + } + } + + tableParts.Count = (UInt32)tables.Count(); + } + + private void GenerateExtendedFilePropertiesPartContent(ExtendedFilePropertiesPart extendedFilePropertiesPart) + { + if (extendedFilePropertiesPart.Properties == null) + extendedFilePropertiesPart.Properties = new Properties(); + + var properties = extendedFilePropertiesPart.Properties; + if ( + !properties.NamespaceDeclarations.Contains(new KeyValuePair("vt", + "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"))) + { + properties.AddNamespaceDeclaration("vt", + "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"); + } + + if (properties.Application == null) + properties.AppendChild(new Application { Text = "Microsoft Excel" }); + + if (properties.DocumentSecurity == null) + properties.AppendChild(new DocumentSecurity { Text = "0" }); + + if (properties.ScaleCrop == null) + properties.AppendChild(new ScaleCrop { Text = "false" }); + + if (properties.HeadingPairs == null) + properties.HeadingPairs = new HeadingPairs(); + + if (properties.TitlesOfParts == null) + properties.TitlesOfParts = new TitlesOfParts(); + + properties.HeadingPairs.VTVector = new VTVector { BaseType = VectorBaseValues.Variant }; + + properties.TitlesOfParts.VTVector = new VTVector { BaseType = VectorBaseValues.Lpstr }; + + var vTVectorOne = properties.HeadingPairs.VTVector; + + var vTVectorTwo = properties.TitlesOfParts.VTVector; + + var modifiedWorksheets = + ((IEnumerable)WorksheetsInternal).Select(w => new { w.Name, Order = w.Position }).ToList(); + var modifiedNamedRanges = GetModifiedNamedRanges(); + var modifiedWorksheetsCount = modifiedWorksheets.Count; + var modifiedNamedRangesCount = modifiedNamedRanges.Count; + + InsertOnVtVector(vTVectorOne, "Worksheets", 0, modifiedWorksheetsCount.ToInvariantString()); + InsertOnVtVector(vTVectorOne, "Named Ranges", 2, modifiedNamedRangesCount.ToInvariantString()); + + vTVectorTwo.Size = (UInt32)(modifiedNamedRangesCount + modifiedWorksheetsCount); + + foreach ( + var vTlpstr3 in modifiedWorksheets.OrderBy(w => w.Order).Select(w => new VTLPSTR { Text = w.Name })) + vTVectorTwo.AppendChild(vTlpstr3); + + foreach (var vTlpstr7 in modifiedNamedRanges.Select(nr => new VTLPSTR { Text = nr })) + vTVectorTwo.AppendChild(vTlpstr7); + + if (Properties.Manager != null) + { + if (!String.IsNullOrWhiteSpace(Properties.Manager)) + { + if (properties.Manager == null) + properties.Manager = new Manager(); + + properties.Manager.Text = Properties.Manager; + } + else + properties.Manager = null; + } + + if (Properties.Company == null) return; + + if (!String.IsNullOrWhiteSpace(Properties.Company)) + { + if (properties.Company == null) + properties.Company = new Company(); + + properties.Company.Text = Properties.Company; + } + else + properties.Company = null; + } + + private static void InsertOnVtVector(VTVector vTVector, String property, Int32 index, String text) + { + var m = from e1 in vTVector.Elements() + where e1.Elements().Any(e2 => e2.Text == property) + select e1; + if (!m.Any()) + { + if (vTVector.Size == null) + vTVector.Size = new UInt32Value(0U); + + vTVector.Size += 2U; + var variant1 = new Variant(); + var vTlpstr1 = new VTLPSTR { Text = property }; + variant1.AppendChild(vTlpstr1); + vTVector.InsertAt(variant1, index); + + var variant2 = new Variant(); + var vTInt321 = new VTInt32(); + variant2.AppendChild(vTInt321); + vTVector.InsertAt(variant2, index + 1); + } + + var targetIndex = 0; + foreach (var e in vTVector.Elements()) + { + if (e.Elements().Any(e2 => e2.Text == property)) + { + vTVector.ElementAt(targetIndex + 1).GetFirstChild().Text = text; + break; + } + targetIndex++; + } + } + + private List GetModifiedNamedRanges() + { + var namedRanges = new List(); + foreach (var w in WorksheetsInternal) + { + var wName = w.Name; + namedRanges.AddRange(w.NamedRanges.Select(n => wName + "!" + n.Name)); + namedRanges.Add(w.Name + "!Print_Area"); + namedRanges.Add(w.Name + "!Print_Titles"); + } + namedRanges.AddRange(NamedRanges.Select(n => n.Name)); + return namedRanges; + } + + private void GenerateWorkbookPartContent(WorkbookPart workbookPart, SaveOptions options, SaveContext context) + { + if (workbookPart.Workbook == null) + workbookPart.Workbook = new Workbook(); + + var workbook = workbookPart.Workbook; + if ( + !workbook.NamespaceDeclarations.Contains(new KeyValuePair("r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"))) + { + workbook.AddNamespaceDeclaration("r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + } + + #region WorkbookProperties + + if (workbook.WorkbookProperties == null) + workbook.WorkbookProperties = new WorkbookProperties(); + + if (workbook.WorkbookProperties.CodeName == null) + workbook.WorkbookProperties.CodeName = "ThisWorkbook"; + + workbook.WorkbookProperties.Date1904 = OpenXmlHelper.GetBooleanValue(this.Use1904DateSystem, false); + + if (options.FilterPrivacy.HasValue) + workbook.WorkbookProperties.FilterPrivacy = OpenXmlHelper.GetBooleanValue(options.FilterPrivacy.Value, false); + + #endregion WorkbookProperties + + #region FileSharing + + if (workbook.FileSharing == null) + workbook.FileSharing = new FileSharing(); + + workbook.FileSharing.ReadOnlyRecommended = OpenXmlHelper.GetBooleanValue(this.FileSharing.ReadOnlyRecommended, false); + workbook.FileSharing.UserName = String.IsNullOrWhiteSpace(this.FileSharing.UserName) ? null : StringValue.FromString(this.FileSharing.UserName); + + if (!workbook.FileSharing.HasChildren && !workbook.FileSharing.HasAttributes) + workbook.FileSharing = null; + + #endregion FileSharing + + #region WorkbookProtection + + if (this.Protection.IsProtected) + { + if (workbook.WorkbookProtection == null) + workbook.WorkbookProtection = new WorkbookProtection(); + + var workbookProtection = workbook.WorkbookProtection; + + var protection = this.Protection; + + workbookProtection.WorkbookPassword = null; + workbookProtection.WorkbookAlgorithmName = null; + workbookProtection.WorkbookHashValue = null; + workbookProtection.WorkbookSpinCount = null; + workbookProtection.WorkbookSaltValue = null; + + if (protection.Algorithm == XLProtectionAlgorithm.Algorithm.SimpleHash) + { + if (!String.IsNullOrWhiteSpace(protection.PasswordHash)) + workbookProtection.WorkbookPassword = protection.PasswordHash; + } + else + { + workbookProtection.WorkbookAlgorithmName = DescribedEnumParser.ToDescription(protection.Algorithm); + workbookProtection.WorkbookHashValue = protection.PasswordHash; + workbookProtection.WorkbookSpinCount = protection.SpinCount; + workbookProtection.WorkbookSaltValue = protection.Base64EncodedSalt; + } + + workbookProtection.LockStructure = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure), false); + workbookProtection.LockWindows = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows), false); + } + else + { + workbook.WorkbookProtection = null; + } + + #endregion WorkbookProtection + + if (workbook.BookViews == null) + workbook.BookViews = new BookViews(); + + if (workbook.Sheets == null) + workbook.Sheets = new Sheets(); + + var worksheets = WorksheetsInternal; + workbook.Sheets.Elements().Where(s => worksheets.Deleted.Contains(s.Id)).ToList().ForEach( + s => s.Remove()); + + foreach (var sheet in workbook.Sheets.Elements()) + { + var sheetId = (Int32)sheet.SheetId.Value; + + if (WorksheetsInternal.All(w => w.SheetId != sheetId)) continue; + + var wks = WorksheetsInternal.Single(w => w.SheetId == sheetId); + wks.RelId = sheet.Id; + sheet.Name = wks.Name; + } + + foreach (var xlSheet in WorksheetsInternal.Cast().OrderBy(w => w.Position)) + { + string rId; + if (xlSheet.SheetId == 0 && String.IsNullOrWhiteSpace(xlSheet.RelId)) + { + rId = context.RelIdGenerator.GetNext(RelType.Workbook); + + while (WorksheetsInternal.Cast().Any(w => w.SheetId == Int32.Parse(rId.Substring(3)))) + rId = context.RelIdGenerator.GetNext(RelType.Workbook); + + xlSheet.SheetId = Int32.Parse(rId.Substring(3)); + xlSheet.RelId = rId; + } + else + { + if (String.IsNullOrWhiteSpace(xlSheet.RelId)) + { + rId = String.Concat("rId", xlSheet.SheetId); + context.RelIdGenerator.AddValues(new List { rId }, RelType.Workbook); + } + else + rId = xlSheet.RelId; + } + + if (workbook.Sheets.Cast().All(s => s.Id != rId)) + { + var newSheet = new Sheet + { + Name = xlSheet.Name, + Id = rId, + SheetId = (UInt32)xlSheet.SheetId + }; + + workbook.Sheets.AppendChild(newSheet); + } + } + + var sheetElements = from sheet in workbook.Sheets.Elements() + join worksheet in ((IEnumerable)WorksheetsInternal) on sheet.Id.Value + equals worksheet.RelId + orderby worksheet.Position + select sheet; + + UInt32 firstSheetVisible = 0; + var activeTab = + (from us in UnsupportedSheets where us.IsActive select (UInt32)us.Position - 1).FirstOrDefault(); + var foundVisible = false; + + var totalSheets = sheetElements.Count() + UnsupportedSheets.Count; + for (var p = 1; p <= totalSheets; p++) + { + if (UnsupportedSheets.All(us => us.Position != p)) + { + var sheet = sheetElements.ElementAt(p - UnsupportedSheets.Count(us => us.Position <= p) - 1); + workbook.Sheets.RemoveChild(sheet); + workbook.Sheets.AppendChild(sheet); + var xlSheet = Worksheet(sheet.Name); + if (xlSheet.Visibility != XLWorksheetVisibility.Visible) + sheet.State = xlSheet.Visibility.ToOpenXml(); + else + sheet.State = null; + + if (foundVisible) continue; + + if (sheet.State == null || sheet.State == SheetStateValues.Visible) + foundVisible = true; + else + firstSheetVisible++; + } + else + { + var sheetId = UnsupportedSheets.First(us => us.Position == p).SheetId; + var sheet = workbook.Sheets.Elements().First(s => s.SheetId == sheetId); + workbook.Sheets.RemoveChild(sheet); + workbook.Sheets.AppendChild(sheet); + } + } + + var workbookView = workbook.BookViews.Elements().FirstOrDefault(); + + if (activeTab == 0) + { + UInt32? firstActiveTab = null; + UInt32? firstSelectedTab = null; + foreach (var ws in worksheets) + { + if (ws.TabActive) + { + firstActiveTab = (UInt32)(ws.Position - 1); + break; + } + + if (ws.TabSelected) + { + firstSelectedTab = (UInt32)(ws.Position - 1); + } + } + + activeTab = firstActiveTab + ?? firstSelectedTab + ?? firstSheetVisible; + } + + if (workbookView == null) + { + workbookView = new WorkbookView { ActiveTab = activeTab, FirstSheet = firstSheetVisible }; + workbook.BookViews.AppendChild(workbookView); + } + else + { + workbookView.ActiveTab = activeTab; + workbookView.FirstSheet = firstSheetVisible; + } + + var definedNames = new DefinedNames(); + foreach (var worksheet in WorksheetsInternal) + { + var wsSheetId = (UInt32)worksheet.SheetId; + UInt32 sheetId = 0; + foreach (var s in workbook.Sheets.Elements().TakeWhile(s => s.SheetId != wsSheetId)) + { + sheetId++; + } + + if (worksheet.PageSetup.PrintAreas.Any()) + { + var definedName = new DefinedName { Name = "_xlnm.Print_Area", LocalSheetId = sheetId }; + var worksheetName = worksheet.Name; + var definedNameText = worksheet.PageSetup.PrintAreas.Aggregate(String.Empty, + (current, printArea) => + current + + (worksheetName.EscapeSheetName() + "!" + + printArea.RangeAddress. + FirstAddress.ToStringFixed( + XLReferenceStyle.A1) + + ":" + + printArea.RangeAddress. + LastAddress.ToStringFixed( + XLReferenceStyle.A1) + + ",")); + definedName.Text = definedNameText.Substring(0, definedNameText.Length - 1); + definedNames.AppendChild(definedName); + } + + if (worksheet.AutoFilter.IsEnabled) + { + var definedName = new DefinedName + { + Name = "_xlnm._FilterDatabase", + LocalSheetId = sheetId, + Text = worksheet.Name.EscapeSheetName() + "!" + + worksheet.AutoFilter.Range.RangeAddress.FirstAddress.ToStringFixed( + XLReferenceStyle.A1) + + ":" + + worksheet.AutoFilter.Range.RangeAddress.LastAddress.ToStringFixed( + XLReferenceStyle.A1), + Hidden = BooleanValue.FromBoolean(true) + }; + definedNames.AppendChild(definedName); + } + + foreach (var nr in worksheet.NamedRanges.Where(n => n.Name != "_xlnm._FilterDatabase")) + { + var definedName = new DefinedName + { + Name = nr.Name, + LocalSheetId = sheetId, + Text = nr.ToString() + }; + + if (!nr.Visible) + definedName.Hidden = BooleanValue.FromBoolean(true); + + if (!String.IsNullOrWhiteSpace(nr.Comment)) + definedName.Comment = nr.Comment; + definedNames.AppendChild(definedName); + } + + var definedNameTextRow = String.Empty; + var definedNameTextColumn = String.Empty; + if (worksheet.PageSetup.FirstRowToRepeatAtTop > 0) + { + definedNameTextRow = worksheet.Name.EscapeSheetName() + "!" + worksheet.PageSetup.FirstRowToRepeatAtTop + + ":" + worksheet.PageSetup.LastRowToRepeatAtTop; + } + if (worksheet.PageSetup.FirstColumnToRepeatAtLeft > 0) + { + var minColumn = worksheet.PageSetup.FirstColumnToRepeatAtLeft; + var maxColumn = worksheet.PageSetup.LastColumnToRepeatAtLeft; + definedNameTextColumn = worksheet.Name.EscapeSheetName() + "!" + + XLHelper.GetColumnLetterFromNumber(minColumn) + + ":" + XLHelper.GetColumnLetterFromNumber(maxColumn); + } + + string titles; + if (definedNameTextColumn.Length > 0) + { + titles = definedNameTextColumn; + if (definedNameTextRow.Length > 0) + titles += "," + definedNameTextRow; + } + else + titles = definedNameTextRow; + + if (titles.Length <= 0) continue; + + var definedName2 = new DefinedName + { + Name = "_xlnm.Print_Titles", + LocalSheetId = sheetId, + Text = titles + }; + + definedNames.AppendChild(definedName2); + } + + foreach (var nr in NamedRanges.OfType()) + { + var refersTo = string.Join(",", nr.RangeList + .Select(r => r.StartsWith("#REF!") ? "#REF!" : r)); + + var definedName = new DefinedName + { + Name = nr.Name, + Text = refersTo + }; + + if (!nr.Visible) + definedName.Hidden = BooleanValue.FromBoolean(true); + + if (!String.IsNullOrWhiteSpace(nr.Comment)) + definedName.Comment = nr.Comment; + definedNames.AppendChild(definedName); + } + + workbook.DefinedNames = definedNames; + + if (workbook.CalculationProperties == null) + workbook.CalculationProperties = new CalculationProperties { CalculationId = 125725U }; + + if (CalculateMode == XLCalculateMode.Default) + workbook.CalculationProperties.CalculationMode = null; + else + workbook.CalculationProperties.CalculationMode = CalculateMode.ToOpenXml(); + + if (ReferenceStyle == XLReferenceStyle.Default) + workbook.CalculationProperties.ReferenceMode = null; + else + workbook.CalculationProperties.ReferenceMode = ReferenceStyle.ToOpenXml(); + + if (CalculationOnSave) workbook.CalculationProperties.CalculationOnSave = CalculationOnSave; + if (ForceFullCalculation) workbook.CalculationProperties.ForceFullCalculation = ForceFullCalculation; + if (FullCalculationOnLoad) workbook.CalculationProperties.FullCalculationOnLoad = FullCalculationOnLoad; + if (FullPrecision) workbook.CalculationProperties.FullPrecision = FullPrecision; + } + + private void GenerateSharedStringTablePartContent(SharedStringTablePart sharedStringTablePart, + SaveContext context) + { + // Call all table headers to make sure their names are filled + var x = 0; + Worksheets.ForEach(w => w.Tables.ForEach(t => x = (t as XLTable).FieldNames.Count)); + + sharedStringTablePart.SharedStringTable = new SharedStringTable { Count = 0, UniqueCount = 0 }; + + var stringId = 0; + + var newStrings = new Dictionary(); + var newRichStrings = new Dictionary(); + + bool hasSharedString(IXLCell c) + { + if (c.DataType == XLDataType.Text && c.ShareString) + return (c as XLCell).StyleValue.IncludeQuotePrefix || String.IsNullOrWhiteSpace(c.FormulaA1) && (c as XLCell).InnerText.Length > 0; + else + return false; + } + + foreach (var c in Worksheets.Cast().SelectMany(w => w.Internals.CellsCollection.GetCells(hasSharedString))) + { + c.DataType = XLDataType.Text; + if (c.HasRichText) + { + if (newRichStrings.TryGetValue(c.RichText, out int id)) + c.SharedStringId = id; + else + { + var sharedStringItem = new SharedStringItem(); + PopulatedRichTextElements(sharedStringItem, c, context); + + sharedStringTablePart.SharedStringTable.Append(sharedStringItem); + sharedStringTablePart.SharedStringTable.Count += 1; + sharedStringTablePart.SharedStringTable.UniqueCount += 1; + + newRichStrings.Add(c.RichText, stringId); + c.SharedStringId = stringId; + + stringId++; + } + } + else + { + var value = c.Value.ObjectToInvariantString(); + if (newStrings.TryGetValue(value, out int id)) + c.SharedStringId = id; + else + { + var s = value; + var sharedStringItem = new SharedStringItem(); + var text = new Text { Text = XmlEncoder.EncodeString(s) }; + if (!s.Trim().Equals(s)) + text.Space = SpaceProcessingModeValues.Preserve; + sharedStringItem.Append(text); + sharedStringTablePart.SharedStringTable.Append(sharedStringItem); + sharedStringTablePart.SharedStringTable.Count += 1; + sharedStringTablePart.SharedStringTable.UniqueCount += 1; + + newStrings.Add(value, stringId); + c.SharedStringId = stringId; + + stringId++; + } + } + } + } + + private static void PopulatedRichTextElements(RstType rstType, IXLCell cell, SaveContext context) + { + foreach (var rt in cell.RichText.Where(r => !String.IsNullOrEmpty(r.Text))) + { + rstType.Append(GetRun(rt)); + } + + if (cell.RichText.HasPhonetics) + { + foreach (var p in cell.RichText.Phonetics) + { + var phoneticRun = new PhoneticRun + { + BaseTextStartIndex = (UInt32)p.Start, + EndingBaseIndex = (UInt32)p.End + }; + + var text = new Text { Text = p.Text }; + if (p.Text.PreserveSpaces()) + text.Space = SpaceProcessingModeValues.Preserve; + + phoneticRun.Append(text); + rstType.Append(phoneticRun); + } + + var fontKey = XLFont.GenerateKey(cell.RichText.Phonetics); + var f = XLFontValue.FromKey(ref fontKey); + + if (!context.SharedFonts.TryGetValue(f, out FontInfo fi)) + { + fi = new FontInfo { Font = f }; + context.SharedFonts.Add(f, fi); + } + + var phoneticProperties = new PhoneticProperties + { + FontId = fi.FontId + }; + + if (cell.RichText.Phonetics.Alignment != XLPhoneticAlignment.Left) + phoneticProperties.Alignment = cell.RichText.Phonetics.Alignment.ToOpenXml(); + if (cell.RichText.Phonetics.Type != XLPhoneticType.FullWidthKatakana) + phoneticProperties.Type = cell.RichText.Phonetics.Type.ToOpenXml(); + + rstType.Append(phoneticProperties); + } + } + + private static Run GetRun(IXLRichString rt) + { + var run = new Run(); + + var runProperties = new RunProperties(); + + var bold = rt.Bold ? new Bold() : null; + var italic = rt.Italic ? new Italic() : null; + var underline = rt.Underline != XLFontUnderlineValues.None + ? new Underline { Val = rt.Underline.ToOpenXml() } + : null; + var strike = rt.Strikethrough ? new Strike() : null; + var verticalAlignment = new VerticalTextAlignment + { Val = rt.VerticalAlignment.ToOpenXml() }; + var shadow = rt.Shadow ? new Shadow() : null; + var fontSize = new FontSize { Val = rt.FontSize }; + var color = new Color().FromClosedXMLColor(rt.FontColor); + var fontName = new RunFont { Val = rt.FontName }; + var fontFamilyNumbering = new FontFamily { Val = (Int32)rt.FontFamilyNumbering }; + + if (bold != null) runProperties.Append(bold); + if (italic != null) runProperties.Append(italic); + + if (strike != null) runProperties.Append(strike); + if (shadow != null) runProperties.Append(shadow); + if (underline != null) runProperties.Append(underline); + runProperties.Append(verticalAlignment); + + runProperties.Append(fontSize); + runProperties.Append(color); + runProperties.Append(fontName); + runProperties.Append(fontFamilyNumbering); + + var text = new Text { Text = rt.Text }; + if (rt.Text.PreserveSpaces()) + text.Space = SpaceProcessingModeValues.Preserve; + + run.Append(runProperties); + run.Append(text); + return run; + } + + private void DeleteCalculationChainPartContent(WorkbookPart workbookPart, SaveContext context) + { + if (workbookPart.CalculationChainPart != null) + workbookPart.DeletePart(workbookPart.CalculationChainPart); + } + + private void GenerateCalculationChainPartContent(WorkbookPart workbookPart, SaveContext context) + { + if (workbookPart.CalculationChainPart == null) + workbookPart.AddNewPart(context.RelIdGenerator.GetNext(RelType.Workbook)); + + if (workbookPart.CalculationChainPart.CalculationChain == null) + workbookPart.CalculationChainPart.CalculationChain = new CalculationChain(); + + var calculationChain = workbookPart.CalculationChainPart.CalculationChain; + calculationChain.RemoveAllChildren(); + + foreach (var worksheet in WorksheetsInternal) + { + foreach (var c in worksheet.Internals.CellsCollection.GetCells().Where(c => c.HasFormula)) + { + if (c.HasArrayFormula) + { + if (c.FormulaReference == null) + c.FormulaReference = c.AsRange().RangeAddress; + + if (c.FormulaReference.FirstAddress.Equals(c.Address)) + { + var cc = new CalculationCell + { + CellReference = c.Address.ToString(), + SheetId = worksheet.SheetId + }; + + cc.Array = true; + calculationChain.AppendChild(cc); + + foreach (var childCell in worksheet.Range(c.FormulaReference.ToString()).Cells()) + { + calculationChain.AppendChild( + new CalculationCell + { + CellReference = childCell.Address.ToString(), + SheetId = worksheet.SheetId, + InChildChain = true + } + ); + } + } + } + else + { + calculationChain.AppendChild(new CalculationCell + { + CellReference = c.Address.ToString(), + SheetId = worksheet.SheetId + }); + } + } + } + + if (!calculationChain.Any()) + workbookPart.DeletePart(workbookPart.CalculationChainPart); + } + + private void GenerateThemePartContent(ThemePart themePart) + { + var theme1 = new Theme { Name = "Office Theme" }; + theme1.AddNamespaceDeclaration("a", "http://schemas.openxmlformats.org/drawingml/2006/main"); + + var themeElements1 = new ThemeElements(); + + var colorScheme1 = new ColorScheme { Name = "Office" }; + + var dark1Color1 = new Dark1Color(); + var systemColor1 = new SystemColor + { + Val = SystemColorValues.WindowText, + LastColor = Theme.Text1.Color.ToHex().Substring(2) + }; + + dark1Color1.AppendChild(systemColor1); + + var light1Color1 = new Light1Color(); + var systemColor2 = new SystemColor + { + Val = SystemColorValues.Window, + LastColor = Theme.Background1.Color.ToHex().Substring(2) + }; + + light1Color1.AppendChild(systemColor2); + + var dark2Color1 = new Dark2Color(); + var rgbColorModelHex1 = new RgbColorModelHex { Val = Theme.Text2.Color.ToHex().Substring(2) }; + + dark2Color1.AppendChild(rgbColorModelHex1); + + var light2Color1 = new Light2Color(); + var rgbColorModelHex2 = new RgbColorModelHex { Val = Theme.Background2.Color.ToHex().Substring(2) }; + + light2Color1.AppendChild(rgbColorModelHex2); + + var accent1Color1 = new Accent1Color(); + var rgbColorModelHex3 = new RgbColorModelHex { Val = Theme.Accent1.Color.ToHex().Substring(2) }; + + accent1Color1.AppendChild(rgbColorModelHex3); + + var accent2Color1 = new Accent2Color(); + var rgbColorModelHex4 = new RgbColorModelHex { Val = Theme.Accent2.Color.ToHex().Substring(2) }; + + accent2Color1.AppendChild(rgbColorModelHex4); + + var accent3Color1 = new Accent3Color(); + var rgbColorModelHex5 = new RgbColorModelHex { Val = Theme.Accent3.Color.ToHex().Substring(2) }; + + accent3Color1.AppendChild(rgbColorModelHex5); + + var accent4Color1 = new Accent4Color(); + var rgbColorModelHex6 = new RgbColorModelHex { Val = Theme.Accent4.Color.ToHex().Substring(2) }; + + accent4Color1.AppendChild(rgbColorModelHex6); + + var accent5Color1 = new Accent5Color(); + var rgbColorModelHex7 = new RgbColorModelHex { Val = Theme.Accent5.Color.ToHex().Substring(2) }; + + accent5Color1.AppendChild(rgbColorModelHex7); + + var accent6Color1 = new Accent6Color(); + var rgbColorModelHex8 = new RgbColorModelHex { Val = Theme.Accent6.Color.ToHex().Substring(2) }; + + accent6Color1.AppendChild(rgbColorModelHex8); + + var hyperlink1 = new DocumentFormat.OpenXml.Drawing.Hyperlink(); + var rgbColorModelHex9 = new RgbColorModelHex { Val = Theme.Hyperlink.Color.ToHex().Substring(2) }; + + hyperlink1.AppendChild(rgbColorModelHex9); + + var followedHyperlinkColor1 = new FollowedHyperlinkColor(); + var rgbColorModelHex10 = new RgbColorModelHex { Val = Theme.FollowedHyperlink.Color.ToHex().Substring(2) }; + + followedHyperlinkColor1.AppendChild(rgbColorModelHex10); + + colorScheme1.AppendChild(dark1Color1); + colorScheme1.AppendChild(light1Color1); + colorScheme1.AppendChild(dark2Color1); + colorScheme1.AppendChild(light2Color1); + colorScheme1.AppendChild(accent1Color1); + colorScheme1.AppendChild(accent2Color1); + colorScheme1.AppendChild(accent3Color1); + colorScheme1.AppendChild(accent4Color1); + colorScheme1.AppendChild(accent5Color1); + colorScheme1.AppendChild(accent6Color1); + colorScheme1.AppendChild(hyperlink1); + colorScheme1.AppendChild(followedHyperlinkColor1); + + var fontScheme2 = new FontScheme { Name = "Office" }; + + var majorFont1 = new MajorFont(); + var latinFont1 = new LatinFont { Typeface = "Cambria" }; + var eastAsianFont1 = new EastAsianFont { Typeface = "" }; + var complexScriptFont1 = new ComplexScriptFont { Typeface = "" }; + var supplementalFont1 = new SupplementalFont { Script = "Jpan", Typeface = "MS Pゴシック" }; + var supplementalFont2 = new SupplementalFont { Script = "Hang", Typeface = "맑은 고딕" }; + var supplementalFont3 = new SupplementalFont { Script = "Hans", Typeface = "宋体" }; + var supplementalFont4 = new SupplementalFont { Script = "Hant", Typeface = "新細明體" }; + var supplementalFont5 = new SupplementalFont { Script = "Arab", Typeface = "Times New Roman" }; + var supplementalFont6 = new SupplementalFont { Script = "Hebr", Typeface = "Times New Roman" }; + var supplementalFont7 = new SupplementalFont { Script = "Thai", Typeface = "Tahoma" }; + var supplementalFont8 = new SupplementalFont { Script = "Ethi", Typeface = "Nyala" }; + var supplementalFont9 = new SupplementalFont { Script = "Beng", Typeface = "Vrinda" }; + var supplementalFont10 = new SupplementalFont { Script = "Gujr", Typeface = "Shruti" }; + var supplementalFont11 = new SupplementalFont { Script = "Khmr", Typeface = "MoolBoran" }; + var supplementalFont12 = new SupplementalFont { Script = "Knda", Typeface = "Tunga" }; + var supplementalFont13 = new SupplementalFont { Script = "Guru", Typeface = "Raavi" }; + var supplementalFont14 = new SupplementalFont { Script = "Cans", Typeface = "Euphemia" }; + var supplementalFont15 = new SupplementalFont { Script = "Cher", Typeface = "Plantagenet Cherokee" }; + var supplementalFont16 = new SupplementalFont { Script = "Yiii", Typeface = "Microsoft Yi Baiti" }; + var supplementalFont17 = new SupplementalFont { Script = "Tibt", Typeface = "Microsoft Himalaya" }; + var supplementalFont18 = new SupplementalFont { Script = "Thaa", Typeface = "MV Boli" }; + var supplementalFont19 = new SupplementalFont { Script = "Deva", Typeface = "Mangal" }; + var supplementalFont20 = new SupplementalFont { Script = "Telu", Typeface = "Gautami" }; + var supplementalFont21 = new SupplementalFont { Script = "Taml", Typeface = "Latha" }; + var supplementalFont22 = new SupplementalFont { Script = "Syrc", Typeface = "Estrangelo Edessa" }; + var supplementalFont23 = new SupplementalFont { Script = "Orya", Typeface = "Kalinga" }; + var supplementalFont24 = new SupplementalFont { Script = "Mlym", Typeface = "Kartika" }; + var supplementalFont25 = new SupplementalFont { Script = "Laoo", Typeface = "DokChampa" }; + var supplementalFont26 = new SupplementalFont { Script = "Sinh", Typeface = "Iskoola Pota" }; + var supplementalFont27 = new SupplementalFont { Script = "Mong", Typeface = "Mongolian Baiti" }; + var supplementalFont28 = new SupplementalFont { Script = "Viet", Typeface = "Times New Roman" }; + var supplementalFont29 = new SupplementalFont { Script = "Uigh", Typeface = "Microsoft Uighur" }; + + majorFont1.AppendChild(latinFont1); + majorFont1.AppendChild(eastAsianFont1); + majorFont1.AppendChild(complexScriptFont1); + majorFont1.AppendChild(supplementalFont1); + majorFont1.AppendChild(supplementalFont2); + majorFont1.AppendChild(supplementalFont3); + majorFont1.AppendChild(supplementalFont4); + majorFont1.AppendChild(supplementalFont5); + majorFont1.AppendChild(supplementalFont6); + majorFont1.AppendChild(supplementalFont7); + majorFont1.AppendChild(supplementalFont8); + majorFont1.AppendChild(supplementalFont9); + majorFont1.AppendChild(supplementalFont10); + majorFont1.AppendChild(supplementalFont11); + majorFont1.AppendChild(supplementalFont12); + majorFont1.AppendChild(supplementalFont13); + majorFont1.AppendChild(supplementalFont14); + majorFont1.AppendChild(supplementalFont15); + majorFont1.AppendChild(supplementalFont16); + majorFont1.AppendChild(supplementalFont17); + majorFont1.AppendChild(supplementalFont18); + majorFont1.AppendChild(supplementalFont19); + majorFont1.AppendChild(supplementalFont20); + majorFont1.AppendChild(supplementalFont21); + majorFont1.AppendChild(supplementalFont22); + majorFont1.AppendChild(supplementalFont23); + majorFont1.AppendChild(supplementalFont24); + majorFont1.AppendChild(supplementalFont25); + majorFont1.AppendChild(supplementalFont26); + majorFont1.AppendChild(supplementalFont27); + majorFont1.AppendChild(supplementalFont28); + majorFont1.AppendChild(supplementalFont29); + + var minorFont1 = new MinorFont(); + var latinFont2 = new LatinFont { Typeface = "Calibri" }; + var eastAsianFont2 = new EastAsianFont { Typeface = "" }; + var complexScriptFont2 = new ComplexScriptFont { Typeface = "" }; + var supplementalFont30 = new SupplementalFont { Script = "Jpan", Typeface = "MS Pゴシック" }; + var supplementalFont31 = new SupplementalFont { Script = "Hang", Typeface = "맑은 고딕" }; + var supplementalFont32 = new SupplementalFont { Script = "Hans", Typeface = "宋体" }; + var supplementalFont33 = new SupplementalFont { Script = "Hant", Typeface = "新細明體" }; + var supplementalFont34 = new SupplementalFont { Script = "Arab", Typeface = "Arial" }; + var supplementalFont35 = new SupplementalFont { Script = "Hebr", Typeface = "Arial" }; + var supplementalFont36 = new SupplementalFont { Script = "Thai", Typeface = "Tahoma" }; + var supplementalFont37 = new SupplementalFont { Script = "Ethi", Typeface = "Nyala" }; + var supplementalFont38 = new SupplementalFont { Script = "Beng", Typeface = "Vrinda" }; + var supplementalFont39 = new SupplementalFont { Script = "Gujr", Typeface = "Shruti" }; + var supplementalFont40 = new SupplementalFont { Script = "Khmr", Typeface = "DaunPenh" }; + var supplementalFont41 = new SupplementalFont { Script = "Knda", Typeface = "Tunga" }; + var supplementalFont42 = new SupplementalFont { Script = "Guru", Typeface = "Raavi" }; + var supplementalFont43 = new SupplementalFont { Script = "Cans", Typeface = "Euphemia" }; + var supplementalFont44 = new SupplementalFont { Script = "Cher", Typeface = "Plantagenet Cherokee" }; + var supplementalFont45 = new SupplementalFont { Script = "Yiii", Typeface = "Microsoft Yi Baiti" }; + var supplementalFont46 = new SupplementalFont { Script = "Tibt", Typeface = "Microsoft Himalaya" }; + var supplementalFont47 = new SupplementalFont { Script = "Thaa", Typeface = "MV Boli" }; + var supplementalFont48 = new SupplementalFont { Script = "Deva", Typeface = "Mangal" }; + var supplementalFont49 = new SupplementalFont { Script = "Telu", Typeface = "Gautami" }; + var supplementalFont50 = new SupplementalFont { Script = "Taml", Typeface = "Latha" }; + var supplementalFont51 = new SupplementalFont { Script = "Syrc", Typeface = "Estrangelo Edessa" }; + var supplementalFont52 = new SupplementalFont { Script = "Orya", Typeface = "Kalinga" }; + var supplementalFont53 = new SupplementalFont { Script = "Mlym", Typeface = "Kartika" }; + var supplementalFont54 = new SupplementalFont { Script = "Laoo", Typeface = "DokChampa" }; + var supplementalFont55 = new SupplementalFont { Script = "Sinh", Typeface = "Iskoola Pota" }; + var supplementalFont56 = new SupplementalFont { Script = "Mong", Typeface = "Mongolian Baiti" }; + var supplementalFont57 = new SupplementalFont { Script = "Viet", Typeface = "Arial" }; + var supplementalFont58 = new SupplementalFont { Script = "Uigh", Typeface = "Microsoft Uighur" }; + + minorFont1.AppendChild(latinFont2); + minorFont1.AppendChild(eastAsianFont2); + minorFont1.AppendChild(complexScriptFont2); + minorFont1.AppendChild(supplementalFont30); + minorFont1.AppendChild(supplementalFont31); + minorFont1.AppendChild(supplementalFont32); + minorFont1.AppendChild(supplementalFont33); + minorFont1.AppendChild(supplementalFont34); + minorFont1.AppendChild(supplementalFont35); + minorFont1.AppendChild(supplementalFont36); + minorFont1.AppendChild(supplementalFont37); + minorFont1.AppendChild(supplementalFont38); + minorFont1.AppendChild(supplementalFont39); + minorFont1.AppendChild(supplementalFont40); + minorFont1.AppendChild(supplementalFont41); + minorFont1.AppendChild(supplementalFont42); + minorFont1.AppendChild(supplementalFont43); + minorFont1.AppendChild(supplementalFont44); + minorFont1.AppendChild(supplementalFont45); + minorFont1.AppendChild(supplementalFont46); + minorFont1.AppendChild(supplementalFont47); + minorFont1.AppendChild(supplementalFont48); + minorFont1.AppendChild(supplementalFont49); + minorFont1.AppendChild(supplementalFont50); + minorFont1.AppendChild(supplementalFont51); + minorFont1.AppendChild(supplementalFont52); + minorFont1.AppendChild(supplementalFont53); + minorFont1.AppendChild(supplementalFont54); + minorFont1.AppendChild(supplementalFont55); + minorFont1.AppendChild(supplementalFont56); + minorFont1.AppendChild(supplementalFont57); + minorFont1.AppendChild(supplementalFont58); + + fontScheme2.AppendChild(majorFont1); + fontScheme2.AppendChild(minorFont1); + + var formatScheme1 = new FormatScheme { Name = "Office" }; + + var fillStyleList1 = new FillStyleList(); + + var solidFill1 = new SolidFill(); + var schemeColor1 = new SchemeColor { Val = SchemeColorValues.PhColor }; + + solidFill1.AppendChild(schemeColor1); + + var gradientFill1 = new GradientFill { RotateWithShape = true }; + + var gradientStopList1 = new GradientStopList(); + + var gradientStop1 = new GradientStop { Position = 0 }; + + var schemeColor2 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint1 = new Tint { Val = 50000 }; + var saturationModulation1 = new SaturationModulation { Val = 300000 }; + + schemeColor2.AppendChild(tint1); + schemeColor2.AppendChild(saturationModulation1); + + gradientStop1.AppendChild(schemeColor2); + + var gradientStop2 = new GradientStop { Position = 35000 }; + + var schemeColor3 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint2 = new Tint { Val = 37000 }; + var saturationModulation2 = new SaturationModulation { Val = 300000 }; + + schemeColor3.AppendChild(tint2); + schemeColor3.AppendChild(saturationModulation2); + + gradientStop2.AppendChild(schemeColor3); + + var gradientStop3 = new GradientStop { Position = 100000 }; + + var schemeColor4 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint3 = new Tint { Val = 15000 }; + var saturationModulation3 = new SaturationModulation { Val = 350000 }; + + schemeColor4.AppendChild(tint3); + schemeColor4.AppendChild(saturationModulation3); + + gradientStop3.AppendChild(schemeColor4); + + gradientStopList1.AppendChild(gradientStop1); + gradientStopList1.AppendChild(gradientStop2); + gradientStopList1.AppendChild(gradientStop3); + var linearGradientFill1 = new LinearGradientFill { Angle = 16200000, Scaled = true }; + + gradientFill1.AppendChild(gradientStopList1); + gradientFill1.AppendChild(linearGradientFill1); + + var gradientFill2 = new GradientFill { RotateWithShape = true }; + + var gradientStopList2 = new GradientStopList(); + + var gradientStop4 = new GradientStop { Position = 0 }; + + var schemeColor5 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade1 = new Shade { Val = 51000 }; + var saturationModulation4 = new SaturationModulation { Val = 130000 }; + + schemeColor5.AppendChild(shade1); + schemeColor5.AppendChild(saturationModulation4); + + gradientStop4.AppendChild(schemeColor5); + + var gradientStop5 = new GradientStop { Position = 80000 }; + + var schemeColor6 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade2 = new Shade { Val = 93000 }; + var saturationModulation5 = new SaturationModulation { Val = 130000 }; + + schemeColor6.AppendChild(shade2); + schemeColor6.AppendChild(saturationModulation5); + + gradientStop5.AppendChild(schemeColor6); + + var gradientStop6 = new GradientStop { Position = 100000 }; + + var schemeColor7 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade3 = new Shade { Val = 94000 }; + var saturationModulation6 = new SaturationModulation { Val = 135000 }; + + schemeColor7.AppendChild(shade3); + schemeColor7.AppendChild(saturationModulation6); + + gradientStop6.AppendChild(schemeColor7); + + gradientStopList2.AppendChild(gradientStop4); + gradientStopList2.AppendChild(gradientStop5); + gradientStopList2.AppendChild(gradientStop6); + var linearGradientFill2 = new LinearGradientFill { Angle = 16200000, Scaled = false }; + + gradientFill2.AppendChild(gradientStopList2); + gradientFill2.AppendChild(linearGradientFill2); + + fillStyleList1.AppendChild(solidFill1); + fillStyleList1.AppendChild(gradientFill1); + fillStyleList1.AppendChild(gradientFill2); + + var lineStyleList1 = new LineStyleList(); + + var outline1 = new Outline + { + Width = 9525, + CapType = LineCapValues.Flat, + CompoundLineType = CompoundLineValues.Single, + Alignment = PenAlignmentValues.Center + }; + + var solidFill2 = new SolidFill(); + + var schemeColor8 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade4 = new Shade { Val = 95000 }; + var saturationModulation7 = new SaturationModulation { Val = 105000 }; + + schemeColor8.AppendChild(shade4); + schemeColor8.AppendChild(saturationModulation7); + + solidFill2.AppendChild(schemeColor8); + var presetDash1 = new PresetDash { Val = PresetLineDashValues.Solid }; + + outline1.AppendChild(solidFill2); + outline1.AppendChild(presetDash1); + + var outline2 = new Outline + { + Width = 25400, + CapType = LineCapValues.Flat, + CompoundLineType = CompoundLineValues.Single, + Alignment = PenAlignmentValues.Center + }; + + var solidFill3 = new SolidFill(); + var schemeColor9 = new SchemeColor { Val = SchemeColorValues.PhColor }; + + solidFill3.AppendChild(schemeColor9); + var presetDash2 = new PresetDash { Val = PresetLineDashValues.Solid }; + + outline2.AppendChild(solidFill3); + outline2.AppendChild(presetDash2); + + var outline3 = new Outline + { + Width = 38100, + CapType = LineCapValues.Flat, + CompoundLineType = CompoundLineValues.Single, + Alignment = PenAlignmentValues.Center + }; + + var solidFill4 = new SolidFill(); + var schemeColor10 = new SchemeColor { Val = SchemeColorValues.PhColor }; + + solidFill4.AppendChild(schemeColor10); + var presetDash3 = new PresetDash { Val = PresetLineDashValues.Solid }; + + outline3.AppendChild(solidFill4); + outline3.AppendChild(presetDash3); + + lineStyleList1.AppendChild(outline1); + lineStyleList1.AppendChild(outline2); + lineStyleList1.AppendChild(outline3); + + var effectStyleList1 = new EffectStyleList(); + + var effectStyle1 = new EffectStyle(); + + var effectList1 = new EffectList(); + + var outerShadow1 = new OuterShadow + { + BlurRadius = 40000L, + Distance = 20000L, + Direction = 5400000, + RotateWithShape = false + }; + + var rgbColorModelHex11 = new RgbColorModelHex { Val = "000000" }; + var alpha1 = new Alpha { Val = 38000 }; + + rgbColorModelHex11.AppendChild(alpha1); + + outerShadow1.AppendChild(rgbColorModelHex11); + + effectList1.AppendChild(outerShadow1); + + effectStyle1.AppendChild(effectList1); + + var effectStyle2 = new EffectStyle(); + + var effectList2 = new EffectList(); + + var outerShadow2 = new OuterShadow + { + BlurRadius = 40000L, + Distance = 23000L, + Direction = 5400000, + RotateWithShape = false + }; + + var rgbColorModelHex12 = new RgbColorModelHex { Val = "000000" }; + var alpha2 = new Alpha { Val = 35000 }; + + rgbColorModelHex12.AppendChild(alpha2); + + outerShadow2.AppendChild(rgbColorModelHex12); + + effectList2.AppendChild(outerShadow2); + + effectStyle2.AppendChild(effectList2); + + var effectStyle3 = new EffectStyle(); + + var effectList3 = new EffectList(); + + var outerShadow3 = new OuterShadow + { + BlurRadius = 40000L, + Distance = 23000L, + Direction = 5400000, + RotateWithShape = false + }; + + var rgbColorModelHex13 = new RgbColorModelHex { Val = "000000" }; + var alpha3 = new Alpha { Val = 35000 }; + + rgbColorModelHex13.AppendChild(alpha3); + + outerShadow3.AppendChild(rgbColorModelHex13); + + effectList3.AppendChild(outerShadow3); + + var scene3DType1 = new Scene3DType(); + + var camera1 = new Camera { Preset = PresetCameraValues.OrthographicFront }; + var rotation1 = new Rotation { Latitude = 0, Longitude = 0, Revolution = 0 }; + + camera1.AppendChild(rotation1); + + var lightRig1 = new LightRig { Rig = LightRigValues.ThreePoints, Direction = LightRigDirectionValues.Top }; + var rotation2 = new Rotation { Latitude = 0, Longitude = 0, Revolution = 1200000 }; + + lightRig1.AppendChild(rotation2); + + scene3DType1.AppendChild(camera1); + scene3DType1.AppendChild(lightRig1); + + var shape3DType1 = new Shape3DType(); + var bevelTop1 = new BevelTop { Width = 63500L, Height = 25400L }; + + shape3DType1.AppendChild(bevelTop1); + + effectStyle3.AppendChild(effectList3); + effectStyle3.AppendChild(scene3DType1); + effectStyle3.AppendChild(shape3DType1); + + effectStyleList1.AppendChild(effectStyle1); + effectStyleList1.AppendChild(effectStyle2); + effectStyleList1.AppendChild(effectStyle3); + + var backgroundFillStyleList1 = new BackgroundFillStyleList(); + + var solidFill5 = new SolidFill(); + var schemeColor11 = new SchemeColor { Val = SchemeColorValues.PhColor }; + + solidFill5.AppendChild(schemeColor11); + + var gradientFill3 = new GradientFill { RotateWithShape = true }; + + var gradientStopList3 = new GradientStopList(); + + var gradientStop7 = new GradientStop { Position = 0 }; + + var schemeColor12 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint4 = new Tint { Val = 40000 }; + var saturationModulation8 = new SaturationModulation { Val = 350000 }; + + schemeColor12.AppendChild(tint4); + schemeColor12.AppendChild(saturationModulation8); + + gradientStop7.AppendChild(schemeColor12); + + var gradientStop8 = new GradientStop { Position = 40000 }; + + var schemeColor13 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint5 = new Tint { Val = 45000 }; + var shade5 = new Shade { Val = 99000 }; + var saturationModulation9 = new SaturationModulation { Val = 350000 }; + + schemeColor13.AppendChild(tint5); + schemeColor13.AppendChild(shade5); + schemeColor13.AppendChild(saturationModulation9); + + gradientStop8.AppendChild(schemeColor13); + + var gradientStop9 = new GradientStop { Position = 100000 }; + + var schemeColor14 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade6 = new Shade { Val = 20000 }; + var saturationModulation10 = new SaturationModulation { Val = 255000 }; + + schemeColor14.AppendChild(shade6); + schemeColor14.AppendChild(saturationModulation10); + + gradientStop9.AppendChild(schemeColor14); + + gradientStopList3.AppendChild(gradientStop7); + gradientStopList3.AppendChild(gradientStop8); + gradientStopList3.AppendChild(gradientStop9); + + var pathGradientFill1 = new PathGradientFill { Path = PathShadeValues.Circle }; + var fillToRectangle1 = new FillToRectangle { Left = 50000, Top = -80000, Right = 50000, Bottom = 180000 }; + + pathGradientFill1.AppendChild(fillToRectangle1); + + gradientFill3.AppendChild(gradientStopList3); + gradientFill3.AppendChild(pathGradientFill1); + + var gradientFill4 = new GradientFill { RotateWithShape = true }; + + var gradientStopList4 = new GradientStopList(); + + var gradientStop10 = new GradientStop { Position = 0 }; + + var schemeColor15 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var tint6 = new Tint { Val = 80000 }; + var saturationModulation11 = new SaturationModulation { Val = 300000 }; + + schemeColor15.AppendChild(tint6); + schemeColor15.AppendChild(saturationModulation11); + + gradientStop10.AppendChild(schemeColor15); + + var gradientStop11 = new GradientStop { Position = 100000 }; + + var schemeColor16 = new SchemeColor { Val = SchemeColorValues.PhColor }; + var shade7 = new Shade { Val = 30000 }; + var saturationModulation12 = new SaturationModulation { Val = 200000 }; + + schemeColor16.AppendChild(shade7); + schemeColor16.AppendChild(saturationModulation12); + + gradientStop11.AppendChild(schemeColor16); + + gradientStopList4.AppendChild(gradientStop10); + gradientStopList4.AppendChild(gradientStop11); + + var pathGradientFill2 = new PathGradientFill { Path = PathShadeValues.Circle }; + var fillToRectangle2 = new FillToRectangle { Left = 50000, Top = 50000, Right = 50000, Bottom = 50000 }; + + pathGradientFill2.AppendChild(fillToRectangle2); + + gradientFill4.AppendChild(gradientStopList4); + gradientFill4.AppendChild(pathGradientFill2); + + backgroundFillStyleList1.AppendChild(solidFill5); + backgroundFillStyleList1.AppendChild(gradientFill3); + backgroundFillStyleList1.AppendChild(gradientFill4); + + formatScheme1.AppendChild(fillStyleList1); + formatScheme1.AppendChild(lineStyleList1); + formatScheme1.AppendChild(effectStyleList1); + formatScheme1.AppendChild(backgroundFillStyleList1); + + themeElements1.AppendChild(colorScheme1); + themeElements1.AppendChild(fontScheme2); + themeElements1.AppendChild(formatScheme1); + var objectDefaults1 = new ObjectDefaults(); + var extraColorSchemeList1 = new ExtraColorSchemeList(); + + theme1.AppendChild(themeElements1); + theme1.AppendChild(objectDefaults1); + theme1.AppendChild(extraColorSchemeList1); + + themePart.Theme = theme1; + } + + private void GenerateCustomFilePropertiesPartContent(CustomFilePropertiesPart customFilePropertiesPart) + { + var properties = new DocumentFormat.OpenXml.CustomProperties.Properties(); + properties.AddNamespaceDeclaration("vt", + "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"); + var propertyId = 1; + foreach (var p in CustomProperties) + { + propertyId++; + var customDocumentProperty = new CustomDocumentProperty + { + FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", + PropertyId = propertyId, + Name = p.Name + }; + if (p.Type == XLCustomPropertyType.Text) + { + var vTlpwstr1 = new VTLPWSTR { Text = p.GetValue() }; + customDocumentProperty.AppendChild(vTlpwstr1); + } + else if (p.Type == XLCustomPropertyType.Date) + { + var vTFileTime1 = new VTFileTime + { + Text = + p.GetValue().ToUniversalTime().ToString( + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'") + }; + customDocumentProperty.AppendChild(vTFileTime1); + } + else if (p.Type == XLCustomPropertyType.Number) + { + var vTDouble1 = new VTDouble + { + Text = p.GetValue().ToInvariantString() + }; + customDocumentProperty.AppendChild(vTDouble1); + } + else + { + var vTBool1 = new VTBool { Text = p.GetValue().ToString().ToLower() }; + customDocumentProperty.AppendChild(vTBool1); + } + properties.AppendChild(customDocumentProperty); + } + + customFilePropertiesPart.Properties = properties; + } + + private void SetPackageProperties(OpenXmlPackage document) + { + var created = Properties.Created == DateTime.MinValue ? DateTime.Now : Properties.Created; + var modified = Properties.Modified == DateTime.MinValue ? DateTime.Now : Properties.Modified; + document.PackageProperties.Created = created; + document.PackageProperties.Modified = modified; + +#if true // Workaround: https://github.com/OfficeDev/Open-XML-SDK/issues/235 + + if (Properties.LastModifiedBy == null) document.PackageProperties.LastModifiedBy = ""; + if (Properties.Author == null) document.PackageProperties.Creator = ""; + if (Properties.Title == null) document.PackageProperties.Title = ""; + if (Properties.Subject == null) document.PackageProperties.Subject = ""; + if (Properties.Category == null) document.PackageProperties.Category = ""; + if (Properties.Keywords == null) document.PackageProperties.Keywords = ""; + if (Properties.Comments == null) document.PackageProperties.Description = ""; + if (Properties.Status == null) document.PackageProperties.ContentStatus = ""; + +#endif + + document.PackageProperties.LastModifiedBy = Properties.LastModifiedBy; + + document.PackageProperties.Creator = Properties.Author; + document.PackageProperties.Title = Properties.Title; + document.PackageProperties.Subject = Properties.Subject; + document.PackageProperties.Category = Properties.Category; + document.PackageProperties.Keywords = Properties.Keywords; + document.PackageProperties.Description = Properties.Comments; + document.PackageProperties.ContentStatus = Properties.Status; + } + + private static string GetTableName(String originalTableName, SaveContext context) + { + var tableName = originalTableName.RemoveSpecialCharacters(); + var name = tableName; + if (context.TableNames.Contains(name)) + { + var i = 1; + name = tableName + i.ToInvariantString(); + while (context.TableNames.Contains(name)) + { + i++; + name = tableName + i.ToInvariantString(); + } + } + + context.TableNames.Add(name); + return name; + } + + private static void GenerateTableDefinitionPartContent(TableDefinitionPart tableDefinitionPart, XLTable xlTable, SaveContext context) + { + context.TableId++; + var reference = xlTable.RangeAddress.FirstAddress + ":" + xlTable.RangeAddress.LastAddress; + var tableName = GetTableName(xlTable.Name, context); + var table = new Table + { + Id = context.TableId, + Name = tableName, + DisplayName = tableName, + Reference = reference + }; + + if (!xlTable.ShowHeaderRow) + table.HeaderRowCount = 0; + + if (xlTable.ShowTotalsRow) + table.TotalsRowCount = 1; + else + table.TotalsRowShown = false; + + var tableColumns = new TableColumns { Count = (UInt32)xlTable.ColumnCount() }; + + UInt32 columnId = 0; + foreach (var xlField in xlTable.Fields) + { + columnId++; + var fieldName = xlField.Name; + var tableColumn = new TableColumn + { + Id = columnId, + Name = fieldName.Replace("_x000a_", "_x005f_x000a_").Replace(Environment.NewLine, "_x000a_") + }; + + // https://github.com/ClosedXML/ClosedXML/issues/513 + if (xlField.IsConsistentStyle()) + { + var style = (xlField.Column.Cells() + .Skip(xlTable.ShowHeaderRow ? 1 : 0) + .First() + .Style as XLStyle).Value; + + if (!DefaultStyleValue.Equals(style) && context.DifferentialFormats.TryGetValue(style, out Int32 id)) + tableColumn.DataFormatId = UInt32Value.FromUInt32(Convert.ToUInt32(id)); + } + else + tableColumn.DataFormatId = null; + + if (xlField.IsConsistentFormula()) + { + string formula = xlField.Column.Cells() + .Skip(xlTable.ShowHeaderRow ? 1 : 0) + .First() + .FormulaA1; + + while (formula.StartsWith("=") && formula.Length > 1) + formula = formula.Substring(1); + + if (!String.IsNullOrWhiteSpace(formula)) + { + tableColumn.CalculatedColumnFormula = new CalculatedColumnFormula + { + Text = formula + }; + } + } + else + tableColumn.CalculatedColumnFormula = null; + + if (xlTable.ShowTotalsRow) + { + if (xlField.TotalsRowFunction != XLTotalsRowFunction.None) + { + tableColumn.TotalsRowFunction = xlField.TotalsRowFunction.ToOpenXml(); + + if (xlField.TotalsRowFunction == XLTotalsRowFunction.Custom) + tableColumn.TotalsRowFormula = new TotalsRowFormula(xlField.TotalsRowFormulaA1); + } + + if (!String.IsNullOrWhiteSpace(xlField.TotalsRowLabel)) + tableColumn.TotalsRowLabel = xlField.TotalsRowLabel; + } + tableColumns.AppendChild(tableColumn); + } + + var tableStyleInfo1 = new TableStyleInfo + { + ShowFirstColumn = xlTable.EmphasizeFirstColumn, + ShowLastColumn = xlTable.EmphasizeLastColumn, + ShowRowStripes = xlTable.ShowRowStripes, + ShowColumnStripes = xlTable.ShowColumnStripes + }; + + if (xlTable.Theme != XLTableTheme.None) + tableStyleInfo1.Name = xlTable.Theme.Name; + + if (xlTable.ShowAutoFilter) + { + var autoFilter1 = new AutoFilter(); + if (xlTable.ShowTotalsRow) + { + xlTable.AutoFilter.Range = xlTable.Worksheet.Range( + xlTable.RangeAddress.FirstAddress.RowNumber, xlTable.RangeAddress.FirstAddress.ColumnNumber, + xlTable.RangeAddress.LastAddress.RowNumber - 1, xlTable.RangeAddress.LastAddress.ColumnNumber); + } + else + xlTable.AutoFilter.Range = xlTable.Worksheet.Range(xlTable.RangeAddress); + + PopulateAutoFilter(xlTable.AutoFilter, autoFilter1); + + table.AppendChild(autoFilter1); + } + + table.AppendChild(tableColumns); + table.AppendChild(tableStyleInfo1); + + tableDefinitionPart.Table = table; + } + + private static void GeneratePivotTables(WorkbookPart workbookPart, WorksheetPart worksheetPart, + XLWorksheet xlWorksheet, + SaveContext context) + { + PivotCaches pivotCaches; + uint cacheId = 0; + if (workbookPart.Workbook.PivotCaches == null) + pivotCaches = workbookPart.Workbook.InsertAfter(new PivotCaches(), workbookPart.Workbook.CalculationProperties); + else + { + pivotCaches = workbookPart.Workbook.PivotCaches; + if (pivotCaches.Any()) + cacheId = pivotCaches.Cast().Max(pc => pc.CacheId.Value) + 1; + } + + foreach (var pt in xlWorksheet.PivotTables.Cast()) + { + context.PivotTables.Clear(); + + // TODO: Avoid duplicate pivot caches of same source range + + PivotCache pivotCache; + PivotTableCacheDefinitionPart pivotTableCacheDefinitionPart; + if (!String.IsNullOrWhiteSpace(pt.WorkbookCacheRelId)) + { + pivotCache = pivotCaches.Cast().Single(pc => pc.Id.Value == pt.WorkbookCacheRelId); + pivotTableCacheDefinitionPart = workbookPart.GetPartById(pt.WorkbookCacheRelId) as PivotTableCacheDefinitionPart; + } + else + { + var workbookCacheRelId = context.RelIdGenerator.GetNext(RelType.Workbook); + pt.WorkbookCacheRelId = workbookCacheRelId; + pivotCache = new PivotCache { CacheId = cacheId++, Id = workbookCacheRelId }; + pivotCaches.AppendChild(pivotCache); + pivotTableCacheDefinitionPart = workbookPart.AddNewPart(workbookCacheRelId); + } + + GeneratePivotTableCacheDefinitionPartContent(pivotTableCacheDefinitionPart, pt, context); + + PivotTablePart pivotTablePart; + var createNewPivotTablePart = String.IsNullOrWhiteSpace(pt.RelId); + if (createNewPivotTablePart) + { + var relId = context.RelIdGenerator.GetNext(RelType.Workbook); + pt.RelId = relId; + pivotTablePart = worksheetPart.AddNewPart(relId); + } + else + pivotTablePart = worksheetPart.GetPartById(pt.RelId) as PivotTablePart; + + GeneratePivotTablePartContent(pivotTablePart, pt, pivotCache.CacheId, context); + + if (createNewPivotTablePart) + pivotTablePart.AddPart(pivotTableCacheDefinitionPart, context.RelIdGenerator.GetNext(RelType.Workbook)); + } + } + + // Generates content of pivotTableCacheDefinitionPart + private static void GeneratePivotTableCacheDefinitionPartContent( + PivotTableCacheDefinitionPart pivotTableCacheDefinitionPart, XLPivotTable pt, + SaveContext context) + { + var pivotCacheDefinition = pivotTableCacheDefinitionPart.PivotCacheDefinition; + if (pivotCacheDefinition == null) + { + pivotCacheDefinition = new PivotCacheDefinition { Id = "rId1" }; + + pivotCacheDefinition.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + pivotTableCacheDefinitionPart.PivotCacheDefinition = pivotCacheDefinition; + } + + #region CreatedVersion + + byte createdVersion = XLConstants.PivotTable.CreatedVersion; + + if (pivotCacheDefinition.CreatedVersion?.HasValue ?? false) + pivotCacheDefinition.CreatedVersion = Math.Max(createdVersion, pivotCacheDefinition.CreatedVersion.Value); + else + pivotCacheDefinition.CreatedVersion = createdVersion; + + #endregion CreatedVersion + + #region RefreshedVersion + + byte refreshedVersion = XLConstants.PivotTable.RefreshedVersion; + if (pivotCacheDefinition.RefreshedVersion?.HasValue ?? false) + pivotCacheDefinition.RefreshedVersion = Math.Max(refreshedVersion, pivotCacheDefinition.RefreshedVersion.Value); + else + pivotCacheDefinition.RefreshedVersion = refreshedVersion; + + #endregion RefreshedVersion + + #region MinRefreshableVersion + + byte minRefreshableVersion = 3; + if (pivotCacheDefinition.MinRefreshableVersion?.HasValue ?? false) + pivotCacheDefinition.MinRefreshableVersion = Math.Max(minRefreshableVersion, pivotCacheDefinition.MinRefreshableVersion.Value); + else + pivotCacheDefinition.MinRefreshableVersion = minRefreshableVersion; + + #endregion MinRefreshableVersion + + pivotCacheDefinition.SaveData = pt.SaveSourceData; + pivotCacheDefinition.RefreshOnLoad = true; //pt.RefreshDataOnOpen + + var pti = new PivotTableInfo + { + Guid = pt.Guid, + Fields = new Dictionary() + }; + + var source = pt.SourceRange; + if (pt.ItemsToRetainPerField == XLItemsToRetain.None) + pivotCacheDefinition.MissingItemsLimit = 0U; + else if (pt.ItemsToRetainPerField == XLItemsToRetain.Max) + pivotCacheDefinition.MissingItemsLimit = XLHelper.MaxRowNumber; + + // Begin CacheSource + var cacheSource = new CacheSource { Type = SourceValues.Worksheet }; + var worksheetSource = new WorksheetSource(); + + switch (pt.SourceType) + { + case XLPivotTableSourceType.Range: + worksheetSource.Name = null; + worksheetSource.Reference = source.RangeAddress.ToStringRelative(includeSheet: false); + + // Do not quote worksheet name with whitespace here - issue #955 + worksheetSource.Sheet = source.RangeAddress.Worksheet.Name; + break; + + case XLPivotTableSourceType.Table: + worksheetSource.Name = pt.SourceTable.Name; + worksheetSource.Reference = null; + worksheetSource.Sheet = null; + break; + + default: + throw new NotSupportedException($"Pivot table source type {pt.SourceType} is not supported."); + } + + cacheSource.AppendChild(worksheetSource); + pivotCacheDefinition.CacheSource = cacheSource; + + // End CacheSource + + // Begin CacheFields + var cacheFields = pivotCacheDefinition.CacheFields; + if (cacheFields == null) + { + cacheFields = new CacheFields(); + pivotCacheDefinition.CacheFields = cacheFields; + } + + foreach (var c in source.Columns()) + { + var columnNumber = c.ColumnNumber(); + var columnName = c.FirstCell().Value.ObjectToInvariantString(); + + CacheField cacheField = null; + + // .CacheFields is cleared when workbook is begin saved + // So if there are any entries, it would be from previous pivot tables + // with an identical source range. + // When pivot sources get its refactoring, this will not be necessary + if (cacheFields != null) + { + cacheField = pivotCacheDefinition + .CacheFields + .Elements() + .FirstOrDefault(f => f.Name == columnName); + } + + if (cacheField == null) + { + cacheField = new CacheField + { + Name = columnName, + SharedItems = new SharedItems() + }; + cacheFields.AppendChild(cacheField); + } + var sharedItems = cacheField.SharedItems; + + XLPivotField xlpf; + if (pt.Fields.Contains(columnName)) + xlpf = pt.Fields.Get(columnName) as XLPivotField; + else + xlpf = pt.Fields.Add(columnName) as XLPivotField; + + var field = pt.RowLabels + .Union(pt.ColumnLabels) + .Union(pt.ReportFilters) + .FirstOrDefault(f => f.SourceName == columnName); + + if (field == null) + { + xlpf.ShowBlankItems = true; + } + else + { + xlpf.CustomName = field.CustomName; + xlpf.SortType = field.SortType; + xlpf.SubtotalCaption = field.SubtotalCaption; + xlpf.IncludeNewItemsInFilter = field.IncludeNewItemsInFilter; + xlpf.Outline = field.Outline; + xlpf.Compact = field.Compact; + xlpf.SubtotalsAtTop = field.SubtotalsAtTop; + xlpf.RepeatItemLabels = field.RepeatItemLabels; + xlpf.InsertBlankLines = field.InsertBlankLines; + xlpf.ShowBlankItems = field.ShowBlankItems; + xlpf.InsertPageBreaks = field.InsertPageBreaks; + xlpf.Collapsed = field.Collapsed; + xlpf.Subtotals.AddRange(field.Subtotals); + } + + var ptfi = new PivotTableFieldInfo + { + IsTotallyBlankField = false + }; + + var sourceHeaderRow = source.FirstRow().RowNumber(); + var fieldValueCells = source.CellsUsed(cell => cell.Address.ColumnNumber == columnNumber + && cell.Address.RowNumber > sourceHeaderRow); + var types = fieldValueCells.Select(cell => cell.DataType).Distinct().ToArray(); + var containsBlank = source.CellsUsed(XLCellsUsedOptions.All, + cell => cell.Address.ColumnNumber == columnNumber + && cell.Address.RowNumber > sourceHeaderRow + && cell.IsEmpty()).Any(); + + // For a totally blank column, we need to check that all cells in column are unused + if (!fieldValueCells.Any()) + { + ptfi.IsTotallyBlankField = true; + containsBlank = true; + } + + if (types.Any()) + { + if (types.Length == 1 && types.Single() == XLDataType.Number) + { + ptfi.DataType = XLDataType.Number; + ptfi.MixedDataType = false; + ptfi.DistinctValues = fieldValueCells + .Where(cell => cell.TryGetValue(out Double _)) + .Select(cell => cell.CachedValue.CastTo()) + .Distinct() + .Cast() + .ToArray(); + + pti.Fields.Add(xlpf.SourceName, ptfi); + } + else if (types.Length == 1 && types.Single() == XLDataType.DateTime) + { + ptfi.DataType = XLDataType.DateTime; + ptfi.MixedDataType = false; + ptfi.DistinctValues = fieldValueCells + .Where(cell => cell.TryGetValue(out DateTime _)) + .Select(cell => cell.CachedValue.CastTo()) + .Distinct() + .Cast() + .ToArray(); + + pti.Fields.Add(xlpf.SourceName, ptfi); + } + else + { + ptfi.DataType = types.First(); + ptfi.MixedDataType = types.Length > 1; + + if (!ptfi.MixedDataType && ptfi.DataType == XLDataType.Text) + ptfi.DistinctValues = fieldValueCells + .Where(cell => cell.TryGetValue(out String _)) + .Select(cell => cell.CachedValue.CastTo()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + else + ptfi.DistinctValues = fieldValueCells + .Where(cell => cell.TryGetValue(out String _)) + .Select(cell => cell.GetString()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + pti.Fields.Add(xlpf.SourceName, ptfi); + } + + // If this cache field exists and contains shared items, + // then we can assume that this as been populated by a previous pivot table + if (sharedItems.Any()) + continue; + + // Else we have to populate the items + if (types.Length == 1 && types.Single() == XLDataType.Number) + { + sharedItems.ContainsSemiMixedTypes = containsBlank; + sharedItems.ContainsString = false; + sharedItems.ContainsNumber = true; + + var allInteger = ptfi.DistinctValues.All(v => int.TryParse(v.ToString(), out int val)); + if (allInteger) sharedItems.ContainsInteger = true; + + // Output items only for row / column / filter fields + if (pt.RowLabels.Any(p => p.SourceName == xlpf.SourceName) + || pt.ColumnLabels.Any(p => p.SourceName == xlpf.SourceName) + || pt.ReportFilters.Any(p => p.SourceName == xlpf.SourceName)) + { + foreach (var value in ptfi.DistinctValues) + sharedItems.AppendChild(new NumberItem { Val = (double)value }); + + if (containsBlank) sharedItems.AppendChild(new MissingItem()); + } + + sharedItems.MinValue = (double)ptfi.DistinctValues.Min(); + sharedItems.MaxValue = (double)ptfi.DistinctValues.Max(); + } + else if (types.Length == 1 && types.Single() == XLDataType.DateTime) + { + sharedItems.ContainsSemiMixedTypes = containsBlank; + sharedItems.ContainsNonDate = false; + sharedItems.ContainsString = false; + sharedItems.ContainsDate = true; + + // Output items only for row / column / filter fields + if (pt.RowLabels.Any(p => p.SourceName == xlpf.SourceName) + || pt.ColumnLabels.Any(p => p.SourceName == xlpf.SourceName) + || pt.ReportFilters.Any(p => p.SourceName == xlpf.SourceName)) + { + foreach (var value in ptfi.DistinctValues) + sharedItems.AppendChild(new DateTimeItem { Val = (DateTime)value }); + + if (containsBlank) sharedItems.AppendChild(new MissingItem()); + } + + sharedItems.MinDate = (DateTime)ptfi.DistinctValues.Min(); + sharedItems.MaxDate = (DateTime)ptfi.DistinctValues.Max(); + } + else + { + if (ptfi.DistinctValues.Any(v => ((string)v).Length > 255)) + sharedItems.LongText = true; + + foreach (var value in ptfi.DistinctValues) + sharedItems.AppendChild(new StringItem { Val = (string)value }); + + if (containsBlank) sharedItems.AppendChild(new MissingItem()); + } + + sharedItems.Count = Convert.ToUInt32(sharedItems.Elements().Count()); + } + + if (containsBlank) sharedItems.ContainsBlank = true; + + if (ptfi.IsTotallyBlankField) + pti.Fields.Add(xlpf.SourceName, ptfi); + else if (ptfi.DistinctValues?.Any() ?? false) + sharedItems.Count = Convert.ToUInt32(ptfi.DistinctValues.Count()); + } + + // End CacheFields + + var pivotTableCacheRecordsPart = pivotTableCacheDefinitionPart.GetPartsOfType().Any() ? + pivotTableCacheDefinitionPart.GetPartsOfType().First() : + pivotTableCacheDefinitionPart.AddNewPart("rId1"); + + var pivotCacheRecords = new PivotCacheRecords(); + pivotCacheRecords.AddNamespaceDeclaration("r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + pivotTableCacheRecordsPart.PivotCacheRecords = pivotCacheRecords; + + context.PivotTables.Add(pti.Guid, pti); + } + + // Generates content of pivotTablePart + private static void GeneratePivotTablePartContent( + PivotTablePart pivotTablePart, XLPivotTable pt, + uint cacheId, SaveContext context) + { + var pti = context.PivotTables[(pt as XLPivotTable).Guid]; + + var pivotTableDefinition = new PivotTableDefinition + { + Name = pt.Name, + CacheId = cacheId, + DataCaption = "Values", + MergeItem = OpenXmlHelper.GetBooleanValue(pt.MergeAndCenterWithLabels, false), + Indent = Convert.ToUInt32(pt.RowLabelIndent), + PageOverThenDown = (pt.FilterAreaOrder == XLFilterAreaOrder.OverThenDown), + PageWrap = Convert.ToUInt32(pt.FilterFieldsPageWrap), + ShowError = String.IsNullOrEmpty(pt.ErrorValueReplacement), + UseAutoFormatting = OpenXmlHelper.GetBooleanValue(pt.AutofitColumns, false), + PreserveFormatting = OpenXmlHelper.GetBooleanValue(pt.PreserveCellFormatting, true), + RowGrandTotals = OpenXmlHelper.GetBooleanValue(pt.ShowGrandTotalsRows, true), + ColumnGrandTotals = OpenXmlHelper.GetBooleanValue(pt.ShowGrandTotalsColumns, true), + SubtotalHiddenItems = OpenXmlHelper.GetBooleanValue(pt.FilteredItemsInSubtotals, false), + MultipleFieldFilters = OpenXmlHelper.GetBooleanValue(pt.AllowMultipleFilters, true), + CustomListSort = OpenXmlHelper.GetBooleanValue(pt.UseCustomListsForSorting, true), + ShowDrill = OpenXmlHelper.GetBooleanValue(pt.ShowExpandCollapseButtons, true), + ShowDataTips = OpenXmlHelper.GetBooleanValue(pt.ShowContextualTooltips, true), + ShowMemberPropertyTips = OpenXmlHelper.GetBooleanValue(pt.ShowPropertiesInTooltips, true), + ShowHeaders = OpenXmlHelper.GetBooleanValue(pt.DisplayCaptionsAndDropdowns, true), + GridDropZones = OpenXmlHelper.GetBooleanValue(pt.ClassicPivotTableLayout, false), + ShowEmptyRow = OpenXmlHelper.GetBooleanValue(pt.ShowEmptyItemsOnRows, false), + ShowEmptyColumn = OpenXmlHelper.GetBooleanValue(pt.ShowEmptyItemsOnColumns, false), + ShowItems = OpenXmlHelper.GetBooleanValue(pt.DisplayItemLabels, true), + FieldListSortAscending = OpenXmlHelper.GetBooleanValue(pt.SortFieldsAtoZ, false), + PrintDrill = OpenXmlHelper.GetBooleanValue(pt.PrintExpandCollapsedButtons, false), + ItemPrintTitles = OpenXmlHelper.GetBooleanValue(pt.RepeatRowLabels, false), + FieldPrintTitles = OpenXmlHelper.GetBooleanValue(pt.PrintTitles, false), + EnableDrill = OpenXmlHelper.GetBooleanValue(pt.EnableShowDetails, true) + }; + + if (!String.IsNullOrEmpty(pt.ColumnHeaderCaption)) + pivotTableDefinition.ColumnHeaderCaption = StringValue.FromString(pt.ColumnHeaderCaption); + + if (!String.IsNullOrEmpty(pt.RowHeaderCaption)) + pivotTableDefinition.RowHeaderCaption = StringValue.FromString(pt.RowHeaderCaption); + + if (pt.ClassicPivotTableLayout) + { + pivotTableDefinition.Compact = false; + pivotTableDefinition.CompactData = false; + } + + if (pt.EmptyCellReplacement != null) + { + pivotTableDefinition.ShowMissing = true; + pivotTableDefinition.MissingCaption = pt.EmptyCellReplacement; + } + else + { + pivotTableDefinition.ShowMissing = false; + } + + if (pt.ErrorValueReplacement != null) + { + pivotTableDefinition.ShowError = true; + pivotTableDefinition.ErrorCaption = pt.ErrorValueReplacement; + } + else + { + pivotTableDefinition.ShowError = false; + } + + var location = new Location + { + FirstHeaderRow = 1U, + FirstDataRow = 1U, + FirstDataColumn = 1U + }; + + if (pt.ReportFilters.Any()) + { + // Reference cell is the part BELOW the report filters + location.Reference = pt.TargetCell.CellBelow(pt.ReportFilters.Count() + 1).Address.ToString(); + } + else + location.Reference = pt.TargetCell.Address.ToString(); + + var rowFields = new RowFields(); + var columnFields = new ColumnFields(); + var rowItems = new RowItems(); + var columnItems = new ColumnItems(); + var pageFields = new PageFields { Count = (uint)pt.ReportFilters.Count() }; + var pivotFields = new PivotFields { Count = Convert.ToUInt32(pt.SourceRange.ColumnCount()) }; + + var orderedPageFields = new SortedDictionary(); + var orderedColumnLabels = new SortedDictionary(); + var orderedRowLabels = new SortedDictionary(); + + // Add value fields first + if (pt.Values.Any()) + { + if (pt.RowLabels.Contains(XLConstants.PivotTable.ValuesSentinalLabel)) + { + var f = pt.RowLabels.First(f1 => f1.SourceName == XLConstants.PivotTable.ValuesSentinalLabel); + orderedRowLabels.Add(pt.RowLabels.IndexOf(f), new Field { Index = -2 }); + pivotTableDefinition.DataOnRows = true; + } + else if (pt.ColumnLabels.Contains(XLConstants.PivotTable.ValuesSentinalLabel)) + { + var f = pt.ColumnLabels.First(f1 => f1.SourceName == XLConstants.PivotTable.ValuesSentinalLabel); + orderedColumnLabels.Add(pt.ColumnLabels.IndexOf(f), new Field { Index = -2 }); + } + } + + // TODO: improve performance as per https://github.com/ClosedXML/ClosedXML/pull/984#discussion_r217266491 + foreach (var xlpf in pt.Fields) + { + var ptfi = pti.Fields[xlpf.SourceName]; + + if (pt.RowLabels.Contains(xlpf.SourceName)) + { + var rowLabelIndex = pt.RowLabels.IndexOf(xlpf); + var f = new Field { Index = pt.Fields.IndexOf(xlpf) }; + orderedRowLabels.Add(rowLabelIndex, f); + + if (ptfi.IsTotallyBlankField) + rowItems.AppendChild(new RowItem()); + else + { + for (var i = 0; i < ptfi.DistinctValues.Count(); i++) + { + var rowItem = new RowItem(); + rowItem.AppendChild(new MemberPropertyIndex { Val = i }); + rowItems.AppendChild(rowItem); + } + } + + var rowItemTotal = new RowItem { ItemType = ItemValues.Grand }; + rowItemTotal.AppendChild(new MemberPropertyIndex()); + rowItems.AppendChild(rowItemTotal); + } + else if (pt.ColumnLabels.Contains(xlpf.SourceName)) + { + var columnlabelIndex = pt.ColumnLabels.IndexOf(xlpf); + var f = new Field { Index = pt.Fields.IndexOf(xlpf) }; + orderedColumnLabels.Add(columnlabelIndex, f); + + if (ptfi.IsTotallyBlankField) + columnItems.AppendChild(new RowItem()); + else + { + for (var i = 0; i < ptfi.DistinctValues.Count(); i++) + { + var rowItem = new RowItem(); + rowItem.AppendChild(new MemberPropertyIndex { Val = i }); + columnItems.AppendChild(rowItem); + } + } + + var rowItemTotal = new RowItem { ItemType = ItemValues.Grand }; + rowItemTotal.AppendChild(new MemberPropertyIndex()); + columnItems.AppendChild(rowItemTotal); + } + } + + foreach (var xlpf in pt.Fields) + { + var ptfi = pti.Fields[xlpf.SourceName]; + IXLPivotField labelOrFilterField = null; + var pf = new PivotField + { + Name = xlpf.CustomName, + IncludeNewItemsInFilter = OpenXmlHelper.GetBooleanValue(xlpf.IncludeNewItemsInFilter, false), + InsertBlankRow = OpenXmlHelper.GetBooleanValue(xlpf.InsertBlankLines, false), + ShowAll = OpenXmlHelper.GetBooleanValue(xlpf.ShowBlankItems, true), + InsertPageBreak = OpenXmlHelper.GetBooleanValue(xlpf.InsertPageBreaks, false), + AllDrilled = OpenXmlHelper.GetBooleanValue(xlpf.Collapsed, false), + }; + if (!string.IsNullOrWhiteSpace(xlpf.SubtotalCaption)) + { + pf.SubtotalCaption = xlpf.SubtotalCaption; + } + + if (pt.ClassicPivotTableLayout) + { + pf.Outline = false; + pf.Compact = false; + } + else + { + pf.Outline = OpenXmlHelper.GetBooleanValue(xlpf.Outline, true); + pf.Compact = OpenXmlHelper.GetBooleanValue(xlpf.Compact, true); + } + + if (xlpf.SortType != XLPivotSortType.Default) + { + pf.SortType = new EnumValue((FieldSortValues)xlpf.SortType); + } + + switch (pt.Subtotals) + { + case XLPivotSubtotals.DoNotShow: + pf.DefaultSubtotal = false; + break; + + case XLPivotSubtotals.AtBottom: + pf.SubtotalTop = false; + break; + + case XLPivotSubtotals.AtTop: + // at top is by default + break; + } + + if (xlpf.SubtotalsAtTop.HasValue) + { + pf.SubtotalTop = OpenXmlHelper.GetBooleanValue(xlpf.SubtotalsAtTop.Value, true); + } + + if (pt.RowLabels.Contains(xlpf.SourceName)) + { + labelOrFilterField = pt.RowLabels.Get(xlpf.SourceName); + pf.Axis = PivotTableAxisValues.AxisRow; + } + else if (pt.ColumnLabels.Contains(xlpf.SourceName)) + { + labelOrFilterField = pt.ColumnLabels.Get(xlpf.SourceName); + pf.Axis = PivotTableAxisValues.AxisColumn; + } + else if (pt.ReportFilters.Contains(xlpf.SourceName)) + { + labelOrFilterField = pt.ReportFilters.Get(xlpf.SourceName); + var sortOrderIndex = pt.ReportFilters.IndexOf(labelOrFilterField); + + location.ColumnsPerPage = 1; + location.RowPageCount = 1; + pf.Axis = PivotTableAxisValues.AxisPage; + + var pageField = new PageField + { + Hierarchy = -1, + Field = pt.Fields.IndexOf(xlpf) + }; + + if (labelOrFilterField.SelectedValues.Count == 1) + { + if (ptfi.MixedDataType || ptfi.DataType == XLDataType.Text) + { + var values = ptfi.DistinctValues + .Select(v => v.ObjectToInvariantString().ToLower()) + .ToList(); + var selectedValue = labelOrFilterField.SelectedValues.Single().ObjectToInvariantString().ToLower(); + if (values.Contains(selectedValue)) + pageField.Item = Convert.ToUInt32(values.IndexOf(selectedValue)); + } + else if (ptfi.DataType == XLDataType.DateTime) + { + var values = ptfi.DistinctValues + .Select(v => Convert.ToDateTime(v)) + .ToList(); + var selectedValue = Convert.ToDateTime(labelOrFilterField.SelectedValues.Single()); + if (values.Contains(selectedValue)) + pageField.Item = Convert.ToUInt32(values.IndexOf(selectedValue)); + } + else if (ptfi.DataType == XLDataType.Number) + { + var values = ptfi.DistinctValues + .Select(v => Convert.ToDouble(v)) + .ToList(); + var selectedValue = Convert.ToDouble(labelOrFilterField.SelectedValues.Single()); + if (values.Contains(selectedValue)) + pageField.Item = Convert.ToUInt32(values.IndexOf(selectedValue)); + } + else if (ptfi.DataType == XLDataType.Boolean) + { + var values = ptfi.DistinctValues + .Select(v => Convert.ToBoolean(v)) + .ToList(); + var selectedValue = Convert.ToBoolean(labelOrFilterField.SelectedValues.Single()); + if (values.Contains(selectedValue)) + pageField.Item = Convert.ToUInt32(values.IndexOf(selectedValue)); + } + else if (ptfi.DataType == XLDataType.TimeSpan) + { + var values = ptfi.DistinctValues + .Cast() + .ToList(); + var selectedValue = (TimeSpan)labelOrFilterField.SelectedValues.Single(); + if (values.Contains(selectedValue)) + pageField.Item = Convert.ToUInt32(values.IndexOf(selectedValue)); + } + else + throw new NotImplementedException(); + } + + orderedPageFields.Add(sortOrderIndex, pageField); + } + + if ((labelOrFilterField?.SelectedValues?.Count ?? 0) > 1) + pf.MultipleItemSelectionAllowed = true; + + if (pt.Values.Any(p => p.SourceName == xlpf.SourceName)) + pf.DataField = true; + + var fieldItems = new Items(); + + // Output items only for row / column / filter fields + if (!ptfi.IsTotallyBlankField && + ptfi.DistinctValues.Any() + && (pt.RowLabels.Contains(xlpf.SourceName) + || pt.ColumnLabels.Contains(xlpf.SourceName) + || pt.ReportFilters.Contains(xlpf.SourceName))) + { + uint i = 0; + foreach (var value in ptfi.DistinctValues) + { + var item = new Item { Index = i }; + + if (labelOrFilterField != null && labelOrFilterField.Collapsed) + item.HideDetails = BooleanValue.FromBoolean(false); + + if (labelOrFilterField != null && + labelOrFilterField.SelectedValues.Count > 1 && + !labelOrFilterField.SelectedValues.Contains(value)) + item.Hidden = BooleanValue.FromBoolean(true); + + fieldItems.AppendChild(item); + + i++; + } + } + + if (xlpf.Subtotals.Any()) + { + foreach (var subtotal in xlpf.Subtotals) + { + var itemSubtotal = new Item(); + switch (subtotal) + { + case XLSubtotalFunction.Average: + pf.AverageSubTotal = true; + itemSubtotal.ItemType = ItemValues.Average; + break; + + case XLSubtotalFunction.Count: + pf.CountASubtotal = true; + itemSubtotal.ItemType = ItemValues.CountA; + break; + + case XLSubtotalFunction.CountNumbers: + pf.CountSubtotal = true; + itemSubtotal.ItemType = ItemValues.Count; + break; + + case XLSubtotalFunction.Maximum: + pf.MaxSubtotal = true; + itemSubtotal.ItemType = ItemValues.Maximum; + break; + + case XLSubtotalFunction.Minimum: + pf.MinSubtotal = true; + itemSubtotal.ItemType = ItemValues.Minimum; + break; + + case XLSubtotalFunction.PopulationStandardDeviation: + pf.ApplyStandardDeviationPInSubtotal = true; + itemSubtotal.ItemType = ItemValues.StandardDeviationP; + break; + + case XLSubtotalFunction.PopulationVariance: + pf.ApplyVariancePInSubtotal = true; + itemSubtotal.ItemType = ItemValues.VarianceP; + break; + + case XLSubtotalFunction.Product: + pf.ApplyProductInSubtotal = true; + itemSubtotal.ItemType = ItemValues.Product; + break; + + case XLSubtotalFunction.StandardDeviation: + pf.ApplyStandardDeviationInSubtotal = true; + itemSubtotal.ItemType = ItemValues.StandardDeviation; + break; + + case XLSubtotalFunction.Sum: + pf.SumSubtotal = true; + itemSubtotal.ItemType = ItemValues.Sum; + break; + + case XLSubtotalFunction.Variance: + pf.ApplyVarianceInSubtotal = true; + itemSubtotal.ItemType = ItemValues.Variance; + break; + } + fieldItems.AppendChild(itemSubtotal); + } + } + // If the field itself doesn't have subtotals, but the pivot table is set to show pivot tables, add the default item + else if (pt.Subtotals != XLPivotSubtotals.DoNotShow) + { + fieldItems.AppendChild(new Item { ItemType = ItemValues.Default }); + } + + if (fieldItems.Any()) + { + fieldItems.Count = Convert.ToUInt32(fieldItems.Count()); + pf.AppendChild(fieldItems); + } + + #region Excel 2010 Features + + if (xlpf.RepeatItemLabels) + { + var pivotFieldExtensionList = new PivotFieldExtensionList(); + pivotFieldExtensionList.RemoveNamespaceDeclaration("x"); + var pivotFieldExtension = new PivotFieldExtension { Uri = "{2946ED86-A175-432a-8AC1-64E0C546D7DE}" }; + pivotFieldExtension.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"); + + var pivotField2 = new DocumentFormat.OpenXml.Office2010.Excel.PivotField { FillDownLabels = true }; + + pivotFieldExtension.AppendChild(pivotField2); + + pivotFieldExtensionList.AppendChild(pivotFieldExtension); + pf.AppendChild(pivotFieldExtensionList); + } + + #endregion Excel 2010 Features + + pivotFields.AppendChild(pf); + } + + pivotTableDefinition.AppendChild(location); + pivotTableDefinition.AppendChild(pivotFields); + + if (pt.RowLabels.Any()) + { + rowFields.Append(orderedRowLabels.Values); + rowFields.Count = Convert.ToUInt32(rowFields.Count()); + pivotTableDefinition.AppendChild(rowFields); + } + else + { + rowItems.AppendChild(new RowItem()); + } + + if (rowItems.Any()) + { + rowItems.Count = Convert.ToUInt32(rowItems.Count()); + pivotTableDefinition.AppendChild(rowItems); + } + + if (pt.ColumnLabels.All(cl => cl.CustomName == XLConstants.PivotTable.ValuesSentinalLabel)) + { + for (int i = 0; i < pt.Values.Count(); i++) + { + var rowItem = new RowItem(); + rowItem.Index = Convert.ToUInt32(i); + rowItem.AppendChild(new MemberPropertyIndex() { Val = i }); + columnItems.AppendChild(rowItem); + } + } + + if (pt.ColumnLabels.Any()) + { + columnFields.Append(orderedColumnLabels.Values); + columnFields.Count = Convert.ToUInt32(columnFields.Count()); + pivotTableDefinition.AppendChild(columnFields); + } + + if (columnItems.Any()) + { + columnItems.Count = Convert.ToUInt32(columnItems.Count()); + pivotTableDefinition.AppendChild(columnItems); + } + + if (pt.ReportFilters.Any()) + { + pageFields.Append(orderedPageFields.Values); + pageFields.Count = Convert.ToUInt32(pageFields.Count()); + pivotTableDefinition.AppendChild(pageFields); + } + + var dataFields = new DataFields(); + foreach (var value in pt.Values) + { + var sourceColumn = + pt.SourceRange.Columns().FirstOrDefault(c => c.Cell(1).Value.ObjectToInvariantString() == value.SourceName); + if (sourceColumn == null) continue; + + UInt32 numberFormatId = 0; + if (value.NumberFormat.NumberFormatId != -1 || context.SharedNumberFormats.ContainsKey(value.NumberFormat.NumberFormatId)) + numberFormatId = (UInt32)value.NumberFormat.NumberFormatId; + else if (context.SharedNumberFormats.Any(snf => snf.Value.NumberFormat.Format == value.NumberFormat.Format)) + numberFormatId = (UInt32)context.SharedNumberFormats.First(snf => snf.Value.NumberFormat.Format == value.NumberFormat.Format).Key; + + var df = new DataField + { + Name = value.CustomName, + Field = (UInt32)(sourceColumn.ColumnNumber() - pt.SourceRange.RangeAddress.FirstAddress.ColumnNumber), + Subtotal = value.SummaryFormula.ToOpenXml(), + ShowDataAs = value.Calculation.ToOpenXml(), + NumberFormatId = numberFormatId + }; + + if (!String.IsNullOrEmpty(value.BaseField)) + { + var baseField = pt.SourceRange.Columns().FirstOrDefault(c => c.Cell(1).Value.ObjectToInvariantString() == value.BaseField); + if (baseField != null) + { + df.BaseField = baseField.ColumnNumber() - pt.SourceRange.RangeAddress.FirstAddress.ColumnNumber; + + var items = baseField.CellsUsed() + .Select(c => c.Value) + .Skip(1) // Skip header column + .Distinct().ToList(); + + if (items.Any(i => i.Equals(value.BaseItem))) + df.BaseItem = Convert.ToUInt32(items.IndexOf(value.BaseItem)); + } + } + else + { + df.BaseField = 0; + } + + if (value.CalculationItem == XLPivotCalculationItem.Previous) + df.BaseItem = 1048828U; + else if (value.CalculationItem == XLPivotCalculationItem.Next) + df.BaseItem = 1048829U; + else if (df.BaseItem == null || !df.BaseItem.HasValue) + df.BaseItem = 0U; + + dataFields.AppendChild(df); + } + + if (dataFields.Any()) + { + dataFields.Count = Convert.ToUInt32(dataFields.Count()); + pivotTableDefinition.AppendChild(dataFields); + } + + var pts = new PivotTableStyle + { + ShowRowHeaders = pt.ShowRowHeaders, + ShowColumnHeaders = pt.ShowColumnHeaders, + ShowRowStripes = pt.ShowRowStripes, + ShowColumnStripes = pt.ShowColumnStripes + }; + + if (pt.Theme != XLPivotTableTheme.None) + pts.Name = Enum.GetName(typeof(XLPivotTableTheme), pt.Theme); + + pivotTableDefinition.AppendChild(pts); + + // Pivot formats + if (pivotTableDefinition.Formats == null) + pivotTableDefinition.Formats = new Formats(); + else + pivotTableDefinition.Formats.RemoveAllChildren(); + + foreach (var styleFormat in pt.StyleFormats.RowGrandTotalFormats) + GeneratePivotTableFormat(isRow: true, (XLPivotStyleFormat)styleFormat, pivotTableDefinition, context); + + foreach (var styleFormat in pt.StyleFormats.ColumnGrandTotalFormats) + GeneratePivotTableFormat(isRow: false, (XLPivotStyleFormat)styleFormat, pivotTableDefinition, context); + + foreach (var pivotField in pt.ImplementedFields) + { + GeneratePivotFieldFormat(XLPivotStyleFormatTarget.Header, pt, (XLPivotField)pivotField, (XLPivotStyleFormat)pivotField.StyleFormats.Header, pivotTableDefinition, context); + GeneratePivotFieldFormat(XLPivotStyleFormatTarget.Subtotal, pt, (XLPivotField)pivotField, (XLPivotStyleFormat)pivotField.StyleFormats.Subtotal, pivotTableDefinition, context); + GeneratePivotFieldFormat(XLPivotStyleFormatTarget.Label, pt, (XLPivotField)pivotField, (XLPivotStyleFormat)pivotField.StyleFormats.Label, pivotTableDefinition, context); + GeneratePivotFieldFormat(XLPivotStyleFormatTarget.Data, pt, (XLPivotField)pivotField, (XLPivotStyleFormat)pivotField.StyleFormats.DataValuesFormat, pivotTableDefinition, context); + } + + if (pivotTableDefinition.Formats.Any()) + { + pivotTableDefinition.Formats.Count = new UInt32Value((uint)pivotTableDefinition.Formats.Count()); + } + else + pivotTableDefinition.Formats = null; + + #region Excel 2010 Features + + var pivotTableDefinitionExtensionList = new PivotTableDefinitionExtensionList(); + + var pivotTableDefinitionExtension = new PivotTableDefinitionExtension { Uri = "{962EF5D1-5CA2-4c93-8EF4-DBF5C05439D2}" }; + pivotTableDefinitionExtension.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"); + + var pivotTableDefinition2 = new DocumentFormat.OpenXml.Office2010.Excel.PivotTableDefinition + { + EnableEdit = pt.EnableCellEditing, + HideValuesRow = !pt.ShowValuesRow + }; + pivotTableDefinition2.AddNamespaceDeclaration("xm", "http://schemas.microsoft.com/office/excel/2006/main"); + + pivotTableDefinitionExtension.AppendChild(pivotTableDefinition2); + + pivotTableDefinitionExtensionList.AppendChild(pivotTableDefinitionExtension); + pivotTableDefinition.AppendChild(pivotTableDefinitionExtensionList); + + #endregion Excel 2010 Features + + pivotTablePart.PivotTableDefinition = pivotTableDefinition; + } + + private static void GeneratePivotTableFormat(Boolean isRow, XLPivotStyleFormat styleFormat, PivotTableDefinition pivotTableDefinition, SaveContext context) + { + if (DefaultStyle.Equals(styleFormat.Style) || !context.DifferentialFormats.ContainsKey(((XLStyle)styleFormat.Style).Value)) + return; + + var format = new Format(); + + format.FormatId = UInt32Value.FromUInt32(Convert.ToUInt32(context.DifferentialFormats[((XLStyle)styleFormat.Style).Value])); + + var pivotArea = GenerateDefaultPivotArea(XLPivotStyleFormatTarget.GrandTotal); + + pivotArea.LabelOnly = OpenXmlHelper.GetBooleanValue(styleFormat.AppliesTo == XLPivotStyleFormatElement.Label, false); + pivotArea.DataOnly = OpenXmlHelper.GetBooleanValue(styleFormat.AppliesTo == XLPivotStyleFormatElement.Data, true); + + pivotArea.GrandColumn = OpenXmlHelper.GetBooleanValue(!isRow, false); + pivotArea.GrandRow = OpenXmlHelper.GetBooleanValue(isRow, false); + pivotArea.Axis = isRow ? PivotTableAxisValues.AxisRow : PivotTableAxisValues.AxisColumn; + + format.PivotArea = pivotArea; + + pivotTableDefinition.Formats.AppendChild(format); + } + + private static void GeneratePivotFieldFormat(XLPivotStyleFormatTarget target, XLPivotTable pt, XLPivotField pivotField, XLPivotStyleFormat styleFormat, PivotTableDefinition pivotTableDefinition, SaveContext context) + { + if (target == XLPivotStyleFormatTarget.GrandTotal) + throw new ArgumentException($"Use {nameof(GeneratePivotTableFormat)} to populate grand total formats."); + + if (DefaultStyle.Equals(styleFormat.Style) || !context.DifferentialFormats.ContainsKey(((XLStyle)styleFormat.Style).Value)) + return; + + var format = new Format(); + + format.FormatId = UInt32Value.FromUInt32(Convert.ToUInt32(context.DifferentialFormats[((XLStyle)styleFormat.Style).Value])); + + var pivotArea = GenerateDefaultPivotArea(target); + + pivotArea.LabelOnly = OpenXmlHelper.GetBooleanValue(styleFormat.AppliesTo == XLPivotStyleFormatElement.Label, false); + pivotArea.DataOnly = OpenXmlHelper.GetBooleanValue(styleFormat.AppliesTo == XLPivotStyleFormatElement.Data, true); + + pivotArea.CollapsedLevelsAreSubtotals = OpenXmlHelper.GetBooleanValue(styleFormat.CollapsedLevelsAreSubtotals, false); + + if (target == XLPivotStyleFormatTarget.Header) + { + pivotArea.Field = pivotField.Offset; + + if (pivotField.IsOnRowAxis) + pivotArea.Axis = PivotTableAxisValues.AxisRow; + else if (pivotField.IsOnColumnAxis) + pivotArea.Axis = PivotTableAxisValues.AxisColumn; + else if (pivotField.IsInFilterList) + pivotArea.Axis = PivotTableAxisValues.AxisPage; + else + throw new NotImplementedException(); + } + + //Ensure referenced pivot field is added to field references + if (new[] + { + XLPivotStyleFormatTarget.Data, XLPivotStyleFormatTarget.Label, XLPivotStyleFormatTarget.Subtotal + }.Contains(target) + && !styleFormat.FieldReferences.OfType().Select(fr => fr.PivotField).Contains(pivotField)) + { + var fr = new PivotLabelFieldReference(pivotField); + fr.DefaultSubtotal = target == XLPivotStyleFormatTarget.Subtotal; + styleFormat.FieldReferences.Insert(0, fr); + } + + if (pivotArea.PivotAreaReferences == null) + pivotArea.PivotAreaReferences = new PivotAreaReferences(); + else + pivotArea.PivotAreaReferences.RemoveAllChildren(); + + foreach (var fr in styleFormat.FieldReferences) + { + GeneratePivotAreaReference(pt, pivotArea.PivotAreaReferences, fr, context); + } + + if (pivotArea.PivotAreaReferences.Any()) + { + pivotArea.PivotAreaReferences.Count = new UInt32Value((uint)pivotArea.PivotAreaReferences.Count()); + } + else + pivotArea.PivotAreaReferences = null; + + format.PivotArea = pivotArea; + pivotTableDefinition.Formats.AppendChild(format); + } + + private static PivotArea GenerateDefaultPivotArea(XLPivotStyleFormatTarget target) + { + switch (target) + { + case XLPivotStyleFormatTarget.Header: + return new PivotArea + { + Type = PivotAreaValues.Button, + FieldPosition = 0, + DataOnly = OpenXmlHelper.GetBooleanValue(false, true), + LabelOnly = OpenXmlHelper.GetBooleanValue(true, false), + Outline = OpenXmlHelper.GetBooleanValue(false, true), + }; + + case XLPivotStyleFormatTarget.Subtotal: + return new PivotArea + { + Type = PivotAreaValues.Normal, + FieldPosition = 0, + }; + + case XLPivotStyleFormatTarget.GrandTotal: + return new PivotArea + { + Type = PivotAreaValues.Normal, + FieldPosition = 0, + DataOnly = OpenXmlHelper.GetBooleanValue(false, true), + LabelOnly = OpenXmlHelper.GetBooleanValue(false, false), + }; + + case XLPivotStyleFormatTarget.Label: + return new PivotArea + { + Type = PivotAreaValues.Normal, + FieldPosition = 0, + DataOnly = OpenXmlHelper.GetBooleanValue(false, true), + LabelOnly = OpenXmlHelper.GetBooleanValue(true, false), + }; + + case XLPivotStyleFormatTarget.Data: + return new PivotArea + { + Type = PivotAreaValues.Normal, + FieldPosition = 0, + }; + + default: + throw new NotImplementedException(); + } + } + + private static void GeneratePivotAreaReference(XLPivotTable pt, PivotAreaReferences pivotAreaReferences, AbstractPivotFieldReference fieldReference, SaveContext context) + { + var pivotAreaReference = new PivotAreaReference(); + + pivotAreaReference.DefaultSubtotal = OpenXmlHelper.GetBooleanValue(fieldReference.DefaultSubtotal, false); + pivotAreaReference.Field = fieldReference.GetFieldOffset(); + + var matchedOffsets = fieldReference.Match(context.PivotTables[pt.Guid], pt); + foreach (var o in matchedOffsets) + { + pivotAreaReference.AppendChild(new FieldItem { Val = UInt32Value.FromUInt32((uint)o) }); + } + + pivotAreaReferences.AppendChild(pivotAreaReference); + } + + private static void GenerateWorksheetCommentsPartContent(WorksheetCommentsPart worksheetCommentsPart, + XLWorksheet xlWorksheet) + { + var commentList = new CommentList(); + var authorsDict = new Dictionary(); + foreach (var c in xlWorksheet.Internals.CellsCollection.GetCells(c => c.HasComment)) + { + var comment = new Comment { Reference = c.Address.ToStringRelative() }; + var authorName = c.Comment.Author; + + if (!authorsDict.TryGetValue(authorName, out int authorId)) + { + authorId = authorsDict.Count; + authorsDict.Add(authorName, authorId); + } + comment.AuthorId = (UInt32)authorId; + + var commentText = new CommentText(); + foreach (var rt in c.Comment) + { + commentText.Append(GetRun(rt)); + } + + comment.Append(commentText); + commentList.Append(comment); + } + + var authors = new Authors(); + foreach (var author in authorsDict.Select(a => new Author { Text = a.Key })) + { + authors.Append(author); + } + + worksheetCommentsPart.Comments.Append(authors); + worksheetCommentsPart.Comments.Append(commentList); + } + + // Generates content of vmlDrawingPart1. + private static bool GenerateVmlDrawingPartContent(VmlDrawingPart vmlDrawingPart, XLWorksheet xlWorksheet) + { + using (var ms = new MemoryStream()) + using (var stream = vmlDrawingPart.GetStream(FileMode.OpenOrCreate)) + { + CopyStream(stream, ms); + stream.Position = 0; + var writer = new XmlTextWriter(stream, Encoding.UTF8); + + writer.WriteStartElement("xml"); + + // https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.vml.shapetype?view=openxml-2.8.1#remarks + // This element defines a shape template that can be used to create other shapes. + // Shapetype is identical to the shape element(§14.1.2.19) except it cannot reference another shapetype element. + // The type attribute shall not be used with shapetype. + // Attributes defined in the shape override any that appear in the shapetype positioning attributes + // (such as top, width, z-index, rotation, flip) are not passed to a shape from a shapetype. + // To use this element, create a shapetype with a specific id attribute. + // Then create a shape and reference the shapetype's id using the type attribute. + new Vml.Shapetype( + new Vml.Stroke { JoinStyle = Vml.StrokeJoinStyleValues.Miter }, + new Vml.Path { AllowGradientShape = true, ConnectionPointType = ConnectValues.Rectangle } + ) + { + Id = XLConstants.Comment.ShapeTypeId, + CoordinateSize = "21600,21600", + OptionalNumber = 202, + EdgePath = "m,l,21600r21600,l21600,xe", + } + .WriteTo(writer); + + var cellWithComments = xlWorksheet.Internals.CellsCollection.GetCells(c => c.HasComment); + + var hasAnyVmlElements = false; + + foreach (var c in cellWithComments) + { + GenerateCommentShape(c).WriteTo(writer); + hasAnyVmlElements |= true; + } + + if (ms.Length > 0) + { + ms.Position = 0; + var xdoc = XDocumentExtensions.Load(ms); + xdoc.Root.Elements().ForEach(e => writer.WriteRaw(e.ToString())); + hasAnyVmlElements |= xdoc.Root.HasElements; + } + + writer.WriteEndElement(); + writer.Flush(); + writer.Close(); + + return hasAnyVmlElements; + } + } + + // VML Shape for Comment + private static Vml.Shape GenerateCommentShape(XLCell c) + { + var rowNumber = c.Address.RowNumber; + var columnNumber = c.Address.ColumnNumber; + + var shapeId = String.Concat("_x0000_s", c.Comment.ShapeId); + // Unique per cell (workbook?), e.g.: "_x0000_s1026" + var anchor = GetAnchor(c); + var textBox = GetTextBox(c.Comment.Style); + var fill = new Vml.Fill { Color2 = "#" + c.Comment.Style.ColorsAndLines.FillColor.Color.ToHex().Substring(2) }; + if (c.Comment.Style.ColorsAndLines.FillTransparency < 1) + fill.Opacity = + Math.Round(Convert.ToDouble(c.Comment.Style.ColorsAndLines.FillTransparency), 2).ToInvariantString(); + var stroke = GetStroke(c); + var shape = new Vml.Shape( + fill, + stroke, + new Vml.Shadow { Color = "black", Obscured = true }, + new Vml.Path { ConnectionPointType = ConnectValues.None }, + textBox, + new ClientData( + new MoveWithCells(c.Comment.Style.Properties.Positioning == XLDrawingAnchor.Absolute + ? "True" + : "False"), // Counterintuitive + new ResizeWithCells(c.Comment.Style.Properties.Positioning == XLDrawingAnchor.MoveAndSizeWithCells + ? "False" + : "True"), // Counterintuitive + anchor, + new HorizontalTextAlignment(c.Comment.Style.Alignment.Horizontal.ToString().ToCamel()), + new Vml.Spreadsheet.VerticalTextAlignment(c.Comment.Style.Alignment.Vertical.ToString().ToCamel()), + new AutoFill("False"), + new CommentRowTarget { Text = (rowNumber - 1).ToInvariantString() }, + new CommentColumnTarget { Text = (columnNumber - 1).ToInvariantString() }, + new Locked(c.Comment.Style.Protection.Locked ? "True" : "False"), + new LockText(c.Comment.Style.Protection.LockText ? "True" : "False"), + new Visible(c.Comment.Visible ? "True" : "False") + ) + { ObjectType = ObjectValues.Note } + ) + { + Id = shapeId, + Type = "#" + XLConstants.Comment.ShapeTypeId, + Style = GetCommentStyle(c), + FillColor = "#" + c.Comment.Style.ColorsAndLines.FillColor.Color.ToHex().Substring(2), + StrokeColor = "#" + c.Comment.Style.ColorsAndLines.LineColor.Color.ToHex().Substring(2), + StrokeWeight = String.Concat(c.Comment.Style.ColorsAndLines.LineWeight.ToInvariantString(), "pt"), + InsetMode = c.Comment.Style.Margins.Automatic ? InsetMarginValues.Auto : InsetMarginValues.Custom + }; + if (!String.IsNullOrWhiteSpace(c.Comment.Style.Web.AlternateText)) + shape.Alternate = c.Comment.Style.Web.AlternateText; + + return shape; + } + + private static Vml.Stroke GetStroke(XLCell c) + { + var lineDash = c.Comment.Style.ColorsAndLines.LineDash; + var stroke = new Vml.Stroke + { + LineStyle = c.Comment.Style.ColorsAndLines.LineStyle.ToOpenXml(), + DashStyle = + lineDash == XLDashStyle.RoundDot || lineDash == XLDashStyle.SquareDot + ? "shortDot" + : lineDash.ToString().ToCamel() + }; + if (lineDash == XLDashStyle.RoundDot) + stroke.EndCap = Vml.StrokeEndCapValues.Round; + if (c.Comment.Style.ColorsAndLines.LineTransparency < 1) + stroke.Opacity = + Math.Round(Convert.ToDouble(c.Comment.Style.ColorsAndLines.LineTransparency), 2).ToInvariantString(); + return stroke; + } + + // http://polymathprogrammer.com/2009/10/22/english-metric-units-and-open-xml/ + // http://archive.oreilly.com/pub/post/what_is_an_emu.html + // https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML + private static Int64 ConvertToEnglishMetricUnits(Int32 pixels, Double resolution) + { + return Convert.ToInt64(914400L * pixels / resolution); + } + + private static void AddPictureAnchor(WorksheetPart worksheetPart, Drawings.IXLPicture picture, SaveContext context) + { + var pic = picture as Drawings.XLPicture; + var drawingsPart = worksheetPart.DrawingsPart ?? + worksheetPart.AddNewPart(context.RelIdGenerator.GetNext(RelType.Workbook)); + + if (drawingsPart.WorksheetDrawing == null) + drawingsPart.WorksheetDrawing = new Xdr.WorksheetDrawing(); + + var worksheetDrawing = drawingsPart.WorksheetDrawing; + + // Add namespaces + if (!worksheetDrawing.NamespaceDeclarations.Any(nd => nd.Value.Equals("http://schemas.openxmlformats.org/drawingml/2006/main"))) + worksheetDrawing.AddNamespaceDeclaration("a", "http://schemas.openxmlformats.org/drawingml/2006/main"); + + if (!worksheetDrawing.NamespaceDeclarations.Any(nd => nd.Value.Equals("http://schemas.openxmlformats.org/officeDocument/2006/relationships"))) + worksheetDrawing.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + ///////// + + // Overwrite actual image binary data + ImagePart imagePart; + if (drawingsPart.HasPartWithId(pic.RelId)) + imagePart = drawingsPart.GetPartById(pic.RelId) as ImagePart; + else + { + pic.RelId = context.RelIdGenerator.GetNext(RelType.Workbook); + imagePart = drawingsPart.AddImagePart(pic.Format.ToOpenXml(), pic.RelId); + } + + using (var stream = new MemoryStream()) + { + pic.ImageStream.Position = 0; + pic.ImageStream.CopyTo(stream); + stream.Seek(0, SeekOrigin.Begin); + imagePart.FeedData(stream); + } + ///////// + + // Clear current anchors + var existingAnchor = GetAnchorFromImageId(worksheetPart, pic.RelId); + if (existingAnchor != null) + worksheetDrawing.RemoveChild(existingAnchor); + + var extentsCx = ConvertToEnglishMetricUnits(pic.Width, GraphicsUtils.Graphics.DpiX); + var extentsCy = ConvertToEnglishMetricUnits(pic.Height, GraphicsUtils.Graphics.DpiY); + + var nvps = worksheetDrawing.Descendants(); + var nvpId = nvps.Any() ? + (UInt32Value)worksheetDrawing.Descendants().Max(p => p.Id.Value) + 1 : + 1U; + + Xdr.FromMarker fMark; + Xdr.ToMarker tMark; + switch (pic.Placement) + { + case Drawings.XLPicturePlacement.FreeFloating: + var absoluteAnchor = new Xdr.AbsoluteAnchor( + new Xdr.Position + { + X = ConvertToEnglishMetricUnits(pic.Left, GraphicsUtils.Graphics.DpiX), + Y = ConvertToEnglishMetricUnits(pic.Top, GraphicsUtils.Graphics.DpiY) + }, + new Xdr.Extent + { + Cx = extentsCx, + Cy = extentsCy + }, + new Xdr.Picture( + new Xdr.NonVisualPictureProperties( + new Xdr.NonVisualDrawingProperties { Id = nvpId, Name = pic.Name }, + new Xdr.NonVisualPictureDrawingProperties(new PictureLocks { NoChangeAspect = true }) + ), + new Xdr.BlipFill( + new Blip { Embed = drawingsPart.GetIdOfPart(imagePart), CompressionState = BlipCompressionValues.Print }, + new Stretch(new FillRectangle()) + ), + new Xdr.ShapeProperties( + new Transform2D( + new Offset { X = 0, Y = 0 }, + new Extents { Cx = extentsCx, Cy = extentsCy } + ), + new PresetGeometry { Preset = ShapeTypeValues.Rectangle } + ) + ), + new Xdr.ClientData() + ); + + worksheetDrawing.Append(absoluteAnchor); + break; + + case Drawings.XLPicturePlacement.MoveAndSize: + var moveAndSizeFromMarker = pic.Markers[Drawings.XLMarkerPosition.TopLeft]; + if (moveAndSizeFromMarker == null) moveAndSizeFromMarker = new Drawings.XLMarker(picture.Worksheet.Cell("A1")); + fMark = new Xdr.FromMarker + { + ColumnId = new Xdr.ColumnId((moveAndSizeFromMarker.ColumnNumber - 1).ToInvariantString()), + RowId = new Xdr.RowId((moveAndSizeFromMarker.RowNumber - 1).ToInvariantString()), + ColumnOffset = new Xdr.ColumnOffset(ConvertToEnglishMetricUnits(moveAndSizeFromMarker.Offset.X, GraphicsUtils.Graphics.DpiX).ToInvariantString()), + RowOffset = new Xdr.RowOffset(ConvertToEnglishMetricUnits(moveAndSizeFromMarker.Offset.Y, GraphicsUtils.Graphics.DpiY).ToInvariantString()) + }; + + var moveAndSizeToMarker = pic.Markers[Drawings.XLMarkerPosition.BottomRight]; + if (moveAndSizeToMarker == null) moveAndSizeToMarker = new Drawings.XLMarker(picture.Worksheet.Cell("A1"), new System.Drawing.Point(picture.Width, picture.Height)); + tMark = new Xdr.ToMarker + { + ColumnId = new Xdr.ColumnId((moveAndSizeToMarker.ColumnNumber - 1).ToInvariantString()), + RowId = new Xdr.RowId((moveAndSizeToMarker.RowNumber - 1).ToInvariantString()), + ColumnOffset = new Xdr.ColumnOffset(ConvertToEnglishMetricUnits(moveAndSizeToMarker.Offset.X, GraphicsUtils.Graphics.DpiX).ToInvariantString()), + RowOffset = new Xdr.RowOffset(ConvertToEnglishMetricUnits(moveAndSizeToMarker.Offset.Y, GraphicsUtils.Graphics.DpiY).ToInvariantString()) + }; + + var twoCellAnchor = new Xdr.TwoCellAnchor( + fMark, + tMark, + new Xdr.Picture( + new Xdr.NonVisualPictureProperties( + new Xdr.NonVisualDrawingProperties { Id = nvpId, Name = pic.Name }, + new Xdr.NonVisualPictureDrawingProperties(new PictureLocks { NoChangeAspect = true }) + ), + new Xdr.BlipFill( + new Blip { Embed = drawingsPart.GetIdOfPart(imagePart), CompressionState = BlipCompressionValues.Print }, + new Stretch(new FillRectangle()) + ), + new Xdr.ShapeProperties( + new Transform2D( + new Offset { X = 0, Y = 0 }, + new Extents { Cx = extentsCx, Cy = extentsCy } + ), + new PresetGeometry { Preset = ShapeTypeValues.Rectangle } + ) + ), + new Xdr.ClientData() + ); + + worksheetDrawing.Append(twoCellAnchor); + break; + + case Drawings.XLPicturePlacement.Move: + var moveFromMarker = pic.Markers[Drawings.XLMarkerPosition.TopLeft]; + if (moveFromMarker == null) moveFromMarker = new Drawings.XLMarker(picture.Worksheet.Cell("A1")); + fMark = new Xdr.FromMarker + { + ColumnId = new Xdr.ColumnId((moveFromMarker.ColumnNumber - 1).ToInvariantString()), + RowId = new Xdr.RowId((moveFromMarker.RowNumber - 1).ToInvariantString()), + ColumnOffset = new Xdr.ColumnOffset(ConvertToEnglishMetricUnits(moveFromMarker.Offset.X, GraphicsUtils.Graphics.DpiX).ToInvariantString()), + RowOffset = new Xdr.RowOffset(ConvertToEnglishMetricUnits(moveFromMarker.Offset.Y, GraphicsUtils.Graphics.DpiY).ToInvariantString()) + }; + + var oneCellAnchor = new Xdr.OneCellAnchor( + fMark, + new Xdr.Extent + { + Cx = extentsCx, + Cy = extentsCy + }, + new Xdr.Picture( + new Xdr.NonVisualPictureProperties( + new Xdr.NonVisualDrawingProperties { Id = nvpId, Name = pic.Name }, + new Xdr.NonVisualPictureDrawingProperties(new PictureLocks { NoChangeAspect = true }) + ), + new Xdr.BlipFill( + new Blip { Embed = drawingsPart.GetIdOfPart(imagePart), CompressionState = BlipCompressionValues.Print }, + new Stretch(new FillRectangle()) + ), + new Xdr.ShapeProperties( + new Transform2D( + new Offset { X = 0, Y = 0 }, + new Extents { Cx = extentsCx, Cy = extentsCy } + ), + new PresetGeometry { Preset = ShapeTypeValues.Rectangle } + ) + ), + new Xdr.ClientData() + ); + + worksheetDrawing.Append(oneCellAnchor); + break; + } + } + + private static void RebaseNonVisualDrawingPropertiesIds(WorksheetPart worksheetPart) + { + var worksheetDrawing = worksheetPart.DrawingsPart.WorksheetDrawing; + + var toRebase = worksheetDrawing.Descendants() + .ToList(); + + toRebase.ForEach(nvdpr => nvdpr.Id = Convert.ToUInt32(toRebase.IndexOf(nvdpr) + 1)); + } + + private static Vml.TextBox GetTextBox(IXLDrawingStyle ds) + { + var sb = new StringBuilder(); + var a = ds.Alignment; + + if (a.Direction == XLDrawingTextDirection.Context) + sb.Append("mso-direction-alt:auto;"); + else if (a.Direction == XLDrawingTextDirection.RightToLeft) + sb.Append("direction:RTL;"); + + if (a.Orientation != XLDrawingTextOrientation.LeftToRight) + { + sb.Append("layout-flow:vertical;"); + if (a.Orientation == XLDrawingTextOrientation.BottomToTop) + sb.Append("mso-layout-flow-alt:bottom-to-top;"); + else if (a.Orientation == XLDrawingTextOrientation.Vertical) + sb.Append("mso-layout-flow-alt:top-to-bottom;"); + } + if (a.AutomaticSize) + sb.Append("mso-fit-shape-to-text:t;"); + + var tb = new Vml.TextBox(); + + if (sb.Length > 0) + tb.Style = sb.ToString(); + + var dm = ds.Margins; + if (!dm.Automatic) + tb.Inset = String.Concat( + dm.Left.ToInvariantString(), "in,", + dm.Top.ToInvariantString(), "in,", + dm.Right.ToInvariantString(), "in,", + dm.Bottom.ToInvariantString(), "in"); + + return tb; + } + + private static Anchor GetAnchor(XLCell cell) + { + var c = cell.Comment; + var cWidth = c.Style.Size.Width; + var fcNumber = c.Position.Column - 1; + var fcOffset = Convert.ToInt32(c.Position.ColumnOffset * 7.5); + var widthFromColumns = cell.Worksheet.Column(c.Position.Column).Width - c.Position.ColumnOffset; + var lastCell = cell.CellRight(c.Position.Column - cell.Address.ColumnNumber); + while (widthFromColumns <= cWidth) + { + lastCell = lastCell.CellRight(); + widthFromColumns += lastCell.WorksheetColumn().Width; + } + + var lcNumber = lastCell.WorksheetColumn().ColumnNumber() - 1; + var lcOffset = Convert.ToInt32((lastCell.WorksheetColumn().Width - (widthFromColumns - cWidth)) * 7.5); + + var cHeight = c.Style.Size.Height; //c.Style.Size.Height * 72.0; + var frNumber = c.Position.Row - 1; + var frOffset = Convert.ToInt32(c.Position.RowOffset); + var heightFromRows = cell.Worksheet.Row(c.Position.Row).Height - c.Position.RowOffset; + lastCell = cell.CellBelow(c.Position.Row - cell.Address.RowNumber); + while (heightFromRows <= cHeight) + { + lastCell = lastCell.CellBelow(); + heightFromRows += lastCell.WorksheetRow().Height; + } + + var lrNumber = lastCell.WorksheetRow().RowNumber() - 1; + var lrOffset = Convert.ToInt32(lastCell.WorksheetRow().Height - (heightFromRows - cHeight)); + return new Anchor + { + Text = string.Concat( + fcNumber, ", ", fcOffset, ", ", + frNumber, ", ", frOffset, ", ", + lcNumber, ", ", lcOffset, ", ", + lrNumber, ", ", lrOffset + ) + }; + } + + private static StringValue GetCommentStyle(XLCell cell) + { + var c = cell.Comment; + var sb = new StringBuilder("position:absolute; "); + + sb.Append("visibility:"); + sb.Append(c.Visible ? "visible" : "hidden"); + sb.Append(";"); + + sb.Append("width:"); + sb.Append(Math.Round(c.Style.Size.Width * 7.5, 2).ToInvariantString()); + sb.Append("pt;"); + sb.Append("height:"); + sb.Append(Math.Round(c.Style.Size.Height, 2).ToInvariantString()); + sb.Append("pt;"); + + sb.Append("z-index:"); + sb.Append(c.ZOrder.ToInvariantString()); + + return sb.ToString(); + } + + #region GenerateWorkbookStylesPartContent + + private void GenerateWorkbookStylesPartContent(WorkbookStylesPart workbookStylesPart, SaveContext context) + { + var defaultStyle = DefaultStyleValue; + + if (!context.SharedFonts.ContainsKey(defaultStyle.Font)) + context.SharedFonts.Add(defaultStyle.Font, new FontInfo { FontId = 0, Font = defaultStyle.Font }); + + if (workbookStylesPart.Stylesheet == null) + workbookStylesPart.Stylesheet = new Stylesheet(); + + // Cell styles = Named styles + if (workbookStylesPart.Stylesheet.CellStyles == null) + workbookStylesPart.Stylesheet.CellStyles = new CellStyles(); + + // To determine the default workbook style, we look for the style with builtInId = 0 (I hope that is the correct approach) + UInt32 defaultFormatId; + if (workbookStylesPart.Stylesheet.CellStyles.Elements().Any(c => c.BuiltinId != null && c.BuiltinId.HasValue && c.BuiltinId.Value == 0)) + { + // Possible to have duplicate default cell styles - occurs when file gets saved under different cultures. + // We prefer the style that is named Normal + var normalCellStyles = workbookStylesPart.Stylesheet.CellStyles.Elements() + .Where(c => c.BuiltinId != null && c.BuiltinId.HasValue && c.BuiltinId.Value == 0) + .OrderBy(c => c.Name != null && c.Name.HasValue && c.Name.Value == "Normal"); + + defaultFormatId = normalCellStyles.Last().FormatId.Value; + } + else if (workbookStylesPart.Stylesheet.CellStyles.Elements().Any()) + defaultFormatId = workbookStylesPart.Stylesheet.CellStyles.Elements().Max(c => c.FormatId.Value) + 1; + else + defaultFormatId = 0; + + context.SharedStyles.Add(defaultStyle, + new StyleInfo + { + StyleId = defaultFormatId, + Style = defaultStyle, + FontId = 0, + FillId = 0, + BorderId = 0, + IncludeQuotePrefix = false, + NumberFormatId = 0 + //AlignmentId = 0 + }); + + UInt32 styleCount = 1; + UInt32 fontCount = 1; + UInt32 fillCount = 3; + UInt32 borderCount = 1; + var numberFormatCount = 0; // 0-based + var pivotTableNumberFormats = new HashSet(); + var xlStyles = new HashSet(); + + foreach (var worksheet in WorksheetsInternal) + { + xlStyles.Add(worksheet.StyleValue); + foreach (var s in worksheet.Internals.ColumnsCollection.Select(c => c.Value.StyleValue)) + { + xlStyles.Add(s); + } + foreach (var s in worksheet.Internals.RowsCollection.Select(r => r.Value.StyleValue)) + { + xlStyles.Add(s); + } + + foreach (var s in worksheet.Internals.CellsCollection.GetCells().Select(c => c.StyleValue)) + { + xlStyles.Add(s); + } + + foreach (var ptnf in worksheet.PivotTables.SelectMany(pt => pt.Values.Select(ptv => ptv.NumberFormat)).Distinct().Where(nf => !pivotTableNumberFormats.Contains(nf))) + pivotTableNumberFormats.Add(ptnf); + } + + var alignments = xlStyles.Select(s => s.Alignment).Distinct().ToList(); + var borders = xlStyles.Select(s => s.Border).Distinct().ToList(); + var fonts = xlStyles.Select(s => s.Font).Distinct().ToList(); + var fills = xlStyles.Select(s => s.Fill).Distinct().ToList(); + var numberFormats = xlStyles.Select(s => s.NumberFormat).Distinct().ToList(); + var protections = xlStyles.Select(s => s.Protection).Distinct().ToList(); + + for (int i = 0; i < fonts.Count; i++) + { + if (!context.SharedFonts.ContainsKey(fonts[i])) + { + context.SharedFonts.Add(fonts[i], new FontInfo { FontId = (uint)fontCount++, Font = fonts[i] }); + } + } + + var sharedFills = fills.ToDictionary( + f => f, f => new FillInfo { FillId = fillCount++, Fill = f }); + + var sharedBorders = borders.ToDictionary( + b => b, b => new BorderInfo { BorderId = borderCount++, Border = b }); + + var sharedNumberFormats = numberFormats + .Where(nf => nf.NumberFormatId == -1) + .ToDictionary(nf => nf, nf => new NumberFormatInfo + { + NumberFormatId = XLConstants.NumberOfBuiltInStyles + numberFormatCount++, + NumberFormat = nf + }); + + foreach (var pivotNumberFormat in pivotTableNumberFormats.Where(nf => nf.NumberFormatId == -1)) + { + var numberFormatKey = new XLNumberFormatKey + { + NumberFormatId = -1, + Format = pivotNumberFormat.Format + }; + var numberFormat = XLNumberFormatValue.FromKey(ref numberFormatKey); + + if (sharedNumberFormats.ContainsKey(numberFormat)) + continue; + + sharedNumberFormats.Add(numberFormat, + new NumberFormatInfo + { + NumberFormatId = XLConstants.NumberOfBuiltInStyles + numberFormatCount++, + NumberFormat = numberFormat + }); + } + + var allSharedNumberFormats = ResolveNumberFormats(workbookStylesPart, sharedNumberFormats, defaultFormatId); + foreach (var nf in allSharedNumberFormats) + { + context.SharedNumberFormats.Add(nf.Value.NumberFormatId, nf.Value); + } + + ResolveFonts(workbookStylesPart, context); + var allSharedFills = ResolveFills(workbookStylesPart, sharedFills); + var allSharedBorders = ResolveBorders(workbookStylesPart, sharedBorders); + + foreach (var xlStyle in xlStyles) + { + var numberFormatId = xlStyle.NumberFormat.NumberFormatId >= 0 + ? xlStyle.NumberFormat.NumberFormatId + : allSharedNumberFormats[xlStyle.NumberFormat].NumberFormatId; + + if (!context.SharedStyles.ContainsKey(xlStyle)) + context.SharedStyles.Add(xlStyle, + new StyleInfo + { + StyleId = styleCount++, + Style = xlStyle, + FontId = context.SharedFonts[xlStyle.Font].FontId, + FillId = allSharedFills[xlStyle.Fill].FillId, + BorderId = allSharedBorders[xlStyle.Border].BorderId, + NumberFormatId = numberFormatId, + IncludeQuotePrefix = xlStyle.IncludeQuotePrefix + }); + } + + ResolveCellStyleFormats(workbookStylesPart, context); + ResolveRest(workbookStylesPart, context); + + if (!workbookStylesPart.Stylesheet.CellStyles.Elements().Any(c => c.BuiltinId != null && c.BuiltinId.HasValue && c.BuiltinId.Value == 0U)) + workbookStylesPart.Stylesheet.CellStyles.AppendChild(new CellStyle { Name = "Normal", FormatId = defaultFormatId, BuiltinId = 0U }); + + workbookStylesPart.Stylesheet.CellStyles.Count = (UInt32)workbookStylesPart.Stylesheet.CellStyles.Count(); + + var newSharedStyles = new Dictionary(); + foreach (var ss in context.SharedStyles) + { + var styleId = -1; + foreach (CellFormat f in workbookStylesPart.Stylesheet.CellFormats) + { + styleId++; + if (CellFormatsAreEqual(f, ss.Value, compareAlignment: true)) + break; + } + if (styleId == -1) + styleId = 0; + var si = ss.Value; + si.StyleId = (UInt32)styleId; + newSharedStyles.Add(ss.Key, si); + } + context.SharedStyles.Clear(); + newSharedStyles.ForEach(kp => context.SharedStyles.Add(kp.Key, kp.Value)); + + AddDifferentialFormats(workbookStylesPart, context); + } + + /// + /// Populates the differential formats that are currently in the file to the SaveContext + /// + /// The workbook styles part. + /// The context. + private void AddDifferentialFormats(WorkbookStylesPart workbookStylesPart, SaveContext context) + { + if (workbookStylesPart.Stylesheet.DifferentialFormats == null) + workbookStylesPart.Stylesheet.DifferentialFormats = new DifferentialFormats(); + + var differentialFormats = workbookStylesPart.Stylesheet.DifferentialFormats; + differentialFormats.RemoveAllChildren(); + FillDifferentialFormatsCollection(differentialFormats, context.DifferentialFormats); + + foreach (var ws in Worksheets) + { + foreach (var cf in ws.ConditionalFormats) + { + var styleValue = (cf.Style as XLStyle).Value; + if (!styleValue.Equals(DefaultStyleValue) && !context.DifferentialFormats.ContainsKey(styleValue)) + AddConditionalDifferentialFormat(workbookStylesPart.Stylesheet.DifferentialFormats, cf, context); + } + + foreach (var tf in ws.Tables.SelectMany(t => t.Fields)) + { + if (tf.IsConsistentStyle()) + { + var style = (tf.Column.Cells() + .Skip(tf.Table.ShowHeaderRow ? 1 : 0) + .First() + .Style as XLStyle).Value; + + if (!style.Equals(DefaultStyleValue) && !context.DifferentialFormats.ContainsKey(style)) + AddStyleAsDifferentialFormat(workbookStylesPart.Stylesheet.DifferentialFormats, style, context); + } + } + + foreach (var pt in ws.PivotTables.Cast()) + { + foreach (var styleFormat in pt.AllStyleFormats) + { + var xlStyle = (XLStyle)styleFormat.Style; + if (!xlStyle.Value.Equals(DefaultStyleValue) && !context.DifferentialFormats.ContainsKey(xlStyle.Value)) + AddStyleAsDifferentialFormat(workbookStylesPart.Stylesheet.DifferentialFormats, xlStyle.Value, context); + } + } + } + + differentialFormats.Count = (UInt32)differentialFormats.Count(); + if (differentialFormats.Count == 0) + workbookStylesPart.Stylesheet.DifferentialFormats = null; + } + + private void FillDifferentialFormatsCollection(DifferentialFormats differentialFormats, + Dictionary dictionary) + { + dictionary.Clear(); + var id = 0; + + foreach (var df in differentialFormats.Elements()) + { + var emptyContainer = new XLStylizedEmpty(DefaultStyle); + + var style = new XLStyle(emptyContainer, DefaultStyle); + LoadFont(df.Font, emptyContainer.Style.Font); + LoadBorder(df.Border, emptyContainer.Style.Border); + LoadNumberFormat(df.NumberingFormat, emptyContainer.Style.NumberFormat); + LoadFill(df.Fill, emptyContainer.Style.Fill, differentialFillFormat: true); + + if (!dictionary.ContainsKey(emptyContainer.StyleValue)) + dictionary.Add(emptyContainer.StyleValue, id++); + } + } + + private static void AddConditionalDifferentialFormat(DifferentialFormats differentialFormats, IXLConditionalFormat cf, + SaveContext context) + { + var differentialFormat = new DifferentialFormat(); + var styleValue = (cf.Style as XLStyle).Value; + + var diffFont = GetNewFont(new FontInfo { Font = styleValue.Font }, false); + if (diffFont?.HasChildren ?? false) + differentialFormat.Append(diffFont); + + if (!String.IsNullOrWhiteSpace(cf.Style.NumberFormat.Format)) + { + var numberFormat = new NumberingFormat + { + NumberFormatId = (UInt32)(XLConstants.NumberOfBuiltInStyles + differentialFormats.Count()), + FormatCode = cf.Style.NumberFormat.Format + }; + differentialFormat.Append(numberFormat); + } + + var diffFill = GetNewFill(new FillInfo { Fill = styleValue.Fill }, differentialFillFormat: true, ignoreMod: false); + if (diffFill?.HasChildren ?? false) + differentialFormat.Append(diffFill); + + var diffBorder = GetNewBorder(new BorderInfo { Border = styleValue.Border }, false); + if (diffBorder?.HasChildren ?? false) + differentialFormat.Append(diffBorder); + + differentialFormats.Append(differentialFormat); + + context.DifferentialFormats.Add(styleValue, differentialFormats.Count() - 1); + } + + private static void AddStyleAsDifferentialFormat(DifferentialFormats differentialFormats, XLStyleValue style, + SaveContext context) + { + var differentialFormat = new DifferentialFormat(); + + var diffFont = GetNewFont(new FontInfo { Font = style.Font }, false); + if (diffFont?.HasChildren ?? false) + differentialFormat.Append(diffFont); + + if (!String.IsNullOrWhiteSpace(style.NumberFormat.Format) || style.NumberFormat.NumberFormatId != 0) + { + var numberFormat = new NumberingFormat(); + + if (style.NumberFormat.NumberFormatId == -1) + { + numberFormat.FormatCode = style.NumberFormat.Format; + numberFormat.NumberFormatId = (UInt32)(XLConstants.NumberOfBuiltInStyles + + differentialFormats + .Descendants() + .Count(df => df.NumberingFormat != null && df.NumberingFormat.NumberFormatId != null && df.NumberingFormat.NumberFormatId.Value >= XLConstants.NumberOfBuiltInStyles)); + } + else + { + numberFormat.NumberFormatId = (UInt32)(style.NumberFormat.NumberFormatId); + if (!string.IsNullOrEmpty(style.NumberFormat.Format)) + numberFormat.FormatCode = style.NumberFormat.Format; + else if (XLPredefinedFormat.FormatCodes.TryGetValue(style.NumberFormat.NumberFormatId, out string formatCode)) + numberFormat.FormatCode = formatCode; + } + + differentialFormat.Append(numberFormat); + } + + var diffFill = GetNewFill(new FillInfo { Fill = style.Fill }, differentialFillFormat: true, ignoreMod: false); + if (diffFill?.HasChildren ?? false) + differentialFormat.Append(diffFill); + + var diffBorder = GetNewBorder(new BorderInfo { Border = style.Border }, false); + if (diffBorder?.HasChildren ?? false) + differentialFormat.Append(diffBorder); + + differentialFormats.Append(differentialFormat); + + context.DifferentialFormats.Add(style, differentialFormats.Count() - 1); + } + + private static void ResolveRest(WorkbookStylesPart workbookStylesPart, SaveContext context) + { + if (workbookStylesPart.Stylesheet.CellFormats == null) + workbookStylesPart.Stylesheet.CellFormats = new CellFormats(); + + foreach (var styleInfo in context.SharedStyles.Values) + { + var info = styleInfo; + var foundOne = + workbookStylesPart.Stylesheet.CellFormats.Cast().Any(f => CellFormatsAreEqual(f, info, compareAlignment: true)); + + if (foundOne) continue; + + var cellFormat = GetCellFormat(styleInfo); + cellFormat.FormatId = 0; + var alignment = new Alignment + { + Horizontal = styleInfo.Style.Alignment.Horizontal.ToOpenXml(), + Vertical = styleInfo.Style.Alignment.Vertical.ToOpenXml(), + Indent = (UInt32)styleInfo.Style.Alignment.Indent, + ReadingOrder = (UInt32)styleInfo.Style.Alignment.ReadingOrder, + WrapText = styleInfo.Style.Alignment.WrapText, + TextRotation = (UInt32)styleInfo.Style.Alignment.TextRotation, + ShrinkToFit = styleInfo.Style.Alignment.ShrinkToFit, + RelativeIndent = styleInfo.Style.Alignment.RelativeIndent, + JustifyLastLine = styleInfo.Style.Alignment.JustifyLastLine + }; + cellFormat.AppendChild(alignment); + + if (cellFormat.ApplyProtection.Value) + cellFormat.AppendChild(GetProtection(styleInfo)); + + workbookStylesPart.Stylesheet.CellFormats.AppendChild(cellFormat); + } + workbookStylesPart.Stylesheet.CellFormats.Count = (UInt32)workbookStylesPart.Stylesheet.CellFormats.Count(); + } + + private static void ResolveCellStyleFormats(WorkbookStylesPart workbookStylesPart, + SaveContext context) + { + if (workbookStylesPart.Stylesheet.CellStyleFormats == null) + workbookStylesPart.Stylesheet.CellStyleFormats = new CellStyleFormats(); + + foreach (var styleInfo in context.SharedStyles.Values) + { + var info = styleInfo; + var foundOne = + workbookStylesPart.Stylesheet.CellStyleFormats.Cast().Any( + f => CellFormatsAreEqual(f, info, compareAlignment: false)); + + if (foundOne) continue; + + var cellStyleFormat = GetCellFormat(styleInfo); + + if (cellStyleFormat.ApplyProtection.Value) + cellStyleFormat.AppendChild(GetProtection(styleInfo)); + + workbookStylesPart.Stylesheet.CellStyleFormats.AppendChild(cellStyleFormat); + } + workbookStylesPart.Stylesheet.CellStyleFormats.Count = + (UInt32)workbookStylesPart.Stylesheet.CellStyleFormats.Count(); + } + + private static bool ApplyFill(StyleInfo styleInfo) + { + return styleInfo.Style.Fill.PatternType.ToOpenXml() == PatternValues.None; + } + + private static bool ApplyBorder(StyleInfo styleInfo) + { + var opBorder = styleInfo.Style.Border; + return (opBorder.BottomBorder.ToOpenXml() != BorderStyleValues.None + || opBorder.DiagonalBorder.ToOpenXml() != BorderStyleValues.None + || opBorder.RightBorder.ToOpenXml() != BorderStyleValues.None + || opBorder.LeftBorder.ToOpenXml() != BorderStyleValues.None + || opBorder.TopBorder.ToOpenXml() != BorderStyleValues.None); + } + + private static bool ApplyProtection(StyleInfo styleInfo) + { + return styleInfo.Style.Protection != null; + } + + private static CellFormat GetCellFormat(StyleInfo styleInfo) + { + var cellFormat = new CellFormat + { + NumberFormatId = (UInt32)styleInfo.NumberFormatId, + FontId = styleInfo.FontId, + FillId = styleInfo.FillId, + BorderId = styleInfo.BorderId, + QuotePrefix = OpenXmlHelper.GetBooleanValue(styleInfo.IncludeQuotePrefix, false), + ApplyNumberFormat = true, + ApplyAlignment = true, + ApplyFill = ApplyFill(styleInfo), + ApplyBorder = ApplyBorder(styleInfo), + ApplyProtection = ApplyProtection(styleInfo) + }; + return cellFormat; + } + + private static Protection GetProtection(StyleInfo styleInfo) + { + return new Protection + { + Locked = styleInfo.Style.Protection.Locked, + Hidden = styleInfo.Style.Protection.Hidden + }; + } + + /// + /// Check if two style are equivalent. + /// + /// Style in the OpenXML format. + /// Style in the ClosedXML format. + /// Flag specifying whether or not compare the alignments of two styles. + /// Styles in x:cellStyleXfs section do not include alignment so we don't have to compare it in this case. + /// Styles in x:cellXfs section, on the opposite, do include alignments, and we must compare them. + /// + /// True if two formats are equivalent, false otherwise. + private static bool CellFormatsAreEqual(CellFormat f, StyleInfo styleInfo, bool compareAlignment) + { + return + f.BorderId != null && styleInfo.BorderId == f.BorderId + && f.FillId != null && styleInfo.FillId == f.FillId + && f.FontId != null && styleInfo.FontId == f.FontId + && f.NumberFormatId != null && styleInfo.NumberFormatId == f.NumberFormatId + && QuotePrefixesAreEqual(f.QuotePrefix, styleInfo.IncludeQuotePrefix) + && (f.ApplyFill == null && styleInfo.Style.Fill == XLFillValue.Default || + f.ApplyFill != null && f.ApplyFill == ApplyFill(styleInfo)) + && (f.ApplyBorder == null && styleInfo.Style.Border == XLBorderValue.Default || + f.ApplyBorder != null && f.ApplyBorder == ApplyBorder(styleInfo)) + && (!compareAlignment || AlignmentsAreEqual(f.Alignment, styleInfo.Style.Alignment)) + && ProtectionsAreEqual(f.Protection, styleInfo.Style.Protection) + ; + } + + private static bool ProtectionsAreEqual(Protection protection, XLProtectionValue xlProtection) + { + var p = XLProtectionValue.Default.Key; + if (protection != null) + { + if (protection.Locked != null) + p.Locked = protection.Locked.Value; + if (protection.Hidden != null) + p.Hidden = protection.Hidden.Value; + } + return p.Equals(xlProtection.Key); + } + + private static bool QuotePrefixesAreEqual(BooleanValue quotePrefix, Boolean includeQuotePrefix) + { + return OpenXmlHelper.GetBooleanValueAsBool(quotePrefix, false) == includeQuotePrefix; + } + + private static bool AlignmentsAreEqual(Alignment alignment, XLAlignmentValue xlAlignment) + { + if (alignment != null) + { + var a = XLAlignmentValue.Default.Key; + if (alignment.Indent != null) + a.Indent = (Int32)alignment.Indent.Value; + + if (alignment.Horizontal != null) + a.Horizontal = alignment.Horizontal.Value.ToClosedXml(); + if (alignment.Vertical != null) + a.Vertical = alignment.Vertical.Value.ToClosedXml(); + + if (alignment.ReadingOrder != null) + a.ReadingOrder = alignment.ReadingOrder.Value.ToClosedXml(); + if (alignment.WrapText != null) + a.WrapText = alignment.WrapText.Value; + if (alignment.TextRotation != null) + a.TextRotation = (Int32)alignment.TextRotation.Value; + if (alignment.ShrinkToFit != null) + a.ShrinkToFit = alignment.ShrinkToFit.Value; + if (alignment.RelativeIndent != null) + a.RelativeIndent = alignment.RelativeIndent.Value; + if (alignment.JustifyLastLine != null) + a.JustifyLastLine = alignment.JustifyLastLine.Value; + return a.Equals(xlAlignment.Key); + } + else + { + return XLStyle.Default.Value.Alignment.Equals(xlAlignment); + } + } + + private Dictionary ResolveBorders(WorkbookStylesPart workbookStylesPart, + Dictionary sharedBorders) + { + if (workbookStylesPart.Stylesheet.Borders == null) + workbookStylesPart.Stylesheet.Borders = new Borders(); + + var allSharedBorders = new Dictionary(); + foreach (var borderInfo in sharedBorders.Values) + { + var borderId = 0; + var foundOne = false; + foreach (Border f in workbookStylesPart.Stylesheet.Borders) + { + if (BordersAreEqual(f, borderInfo.Border)) + { + foundOne = true; + break; + } + borderId++; + } + if (!foundOne) + { + var border = GetNewBorder(borderInfo); + workbookStylesPart.Stylesheet.Borders.AppendChild(border); + } + allSharedBorders.Add(borderInfo.Border, + new BorderInfo { Border = borderInfo.Border, BorderId = (UInt32)borderId }); + } + workbookStylesPart.Stylesheet.Borders.Count = (UInt32)workbookStylesPart.Stylesheet.Borders.Count(); + return allSharedBorders; + } + + private static Border GetNewBorder(BorderInfo borderInfo, Boolean ignoreMod = true) + { + var border = new Border(); + if (borderInfo.Border.DiagonalUp != XLBorderValue.Default.DiagonalUp || ignoreMod) + border.DiagonalUp = borderInfo.Border.DiagonalUp; + + if (borderInfo.Border.DiagonalDown != XLBorderValue.Default.DiagonalDown || ignoreMod) + border.DiagonalDown = borderInfo.Border.DiagonalDown; + + if (borderInfo.Border.LeftBorder != XLBorderValue.Default.LeftBorder || ignoreMod) + { + var leftBorder = new LeftBorder { Style = borderInfo.Border.LeftBorder.ToOpenXml() }; + if (borderInfo.Border.LeftBorderColor != XLBorderValue.Default.LeftBorderColor || ignoreMod) + { + var leftBorderColor = new Color().FromClosedXMLColor(borderInfo.Border.LeftBorderColor); + leftBorder.AppendChild(leftBorderColor); + } + border.AppendChild(leftBorder); + } + + if (borderInfo.Border.RightBorder != XLBorderValue.Default.RightBorder || ignoreMod) + { + var rightBorder = new RightBorder { Style = borderInfo.Border.RightBorder.ToOpenXml() }; + if (borderInfo.Border.RightBorderColor != XLBorderValue.Default.RightBorderColor || ignoreMod) + { + var rightBorderColor = new Color().FromClosedXMLColor(borderInfo.Border.RightBorderColor); + rightBorder.AppendChild(rightBorderColor); + } + border.AppendChild(rightBorder); + } + + if (borderInfo.Border.TopBorder != XLBorderValue.Default.TopBorder || ignoreMod) + { + var topBorder = new TopBorder { Style = borderInfo.Border.TopBorder.ToOpenXml() }; + if (borderInfo.Border.TopBorderColor != XLBorderValue.Default.TopBorderColor || ignoreMod) + { + var topBorderColor = new Color().FromClosedXMLColor(borderInfo.Border.TopBorderColor); + topBorder.AppendChild(topBorderColor); + } + border.AppendChild(topBorder); + } + + if (borderInfo.Border.BottomBorder != XLBorderValue.Default.BottomBorder || ignoreMod) + { + var bottomBorder = new BottomBorder { Style = borderInfo.Border.BottomBorder.ToOpenXml() }; + if (borderInfo.Border.BottomBorderColor != XLBorderValue.Default.BottomBorderColor || ignoreMod) + { + var bottomBorderColor = new Color().FromClosedXMLColor(borderInfo.Border.BottomBorderColor); + bottomBorder.AppendChild(bottomBorderColor); + } + border.AppendChild(bottomBorder); + } + + if (borderInfo.Border.DiagonalBorder != XLBorderValue.Default.DiagonalBorder || ignoreMod) + { + var DiagonalBorder = new DiagonalBorder { Style = borderInfo.Border.DiagonalBorder.ToOpenXml() }; + if (borderInfo.Border.DiagonalBorderColor != XLBorderValue.Default.DiagonalBorderColor || ignoreMod) + if (borderInfo.Border.DiagonalBorderColor != null) + { + var DiagonalBorderColor = new Color().FromClosedXMLColor(borderInfo.Border.DiagonalBorderColor); + DiagonalBorder.AppendChild(DiagonalBorderColor); + } + border.AppendChild(DiagonalBorder); + } + + return border; + } + + private bool BordersAreEqual(Border b, XLBorderValue xlBorder) + { + var nb = XLBorderValue.Default.Key; + if (b.DiagonalUp != null) + nb.DiagonalUp = b.DiagonalUp.Value; + + if (b.DiagonalDown != null) + nb.DiagonalDown = b.DiagonalDown.Value; + + if (b.DiagonalBorder != null) + { + if (b.DiagonalBorder.Style != null) + nb.DiagonalBorder = b.DiagonalBorder.Style.Value.ToClosedXml(); + if (b.DiagonalBorder.Color != null) + nb.DiagonalBorderColor = b.DiagonalBorder.Color.ToClosedXMLColor(_colorList).Key; + } + + if (b.LeftBorder != null) + { + if (b.LeftBorder.Style != null) + nb.LeftBorder = b.LeftBorder.Style.Value.ToClosedXml(); + if (b.LeftBorder.Color != null) + nb.LeftBorderColor = b.LeftBorder.Color.ToClosedXMLColor(_colorList).Key; + } + + if (b.RightBorder != null) + { + if (b.RightBorder.Style != null) + nb.RightBorder = b.RightBorder.Style.Value.ToClosedXml(); + if (b.RightBorder.Color != null) + nb.RightBorderColor = b.RightBorder.Color.ToClosedXMLColor(_colorList).Key; + } + + if (b.TopBorder != null) + { + if (b.TopBorder.Style != null) + nb.TopBorder = b.TopBorder.Style.Value.ToClosedXml(); + if (b.TopBorder.Color != null) + nb.TopBorderColor = b.TopBorder.Color.ToClosedXMLColor(_colorList).Key; + } + + if (b.BottomBorder != null) + { + if (b.BottomBorder.Style != null) + nb.BottomBorder = b.BottomBorder.Style.Value.ToClosedXml(); + if (b.BottomBorder.Color != null) + nb.BottomBorderColor = b.BottomBorder.Color.ToClosedXMLColor(_colorList).Key; + } + + return nb.Equals(xlBorder.Key); + } + + private Dictionary ResolveFills(WorkbookStylesPart workbookStylesPart, + Dictionary sharedFills) + { + if (workbookStylesPart.Stylesheet.Fills == null) + workbookStylesPart.Stylesheet.Fills = new Fills(); + + ResolveFillWithPattern(workbookStylesPart.Stylesheet.Fills, PatternValues.None); + ResolveFillWithPattern(workbookStylesPart.Stylesheet.Fills, PatternValues.Gray125); + + var allSharedFills = new Dictionary(); + foreach (var fillInfo in sharedFills.Values) + { + var fillId = 0; + var foundOne = false; + foreach (Fill f in workbookStylesPart.Stylesheet.Fills) + { + if (FillsAreEqual(f, fillInfo.Fill, fromDifferentialFormat: false)) + { + foundOne = true; + break; + } + fillId++; + } + if (!foundOne) + { + var fill = GetNewFill(fillInfo, differentialFillFormat: false); + workbookStylesPart.Stylesheet.Fills.AppendChild(fill); + } + allSharedFills.Add(fillInfo.Fill, new FillInfo { Fill = fillInfo.Fill, FillId = (UInt32)fillId }); + } + + workbookStylesPart.Stylesheet.Fills.Count = (UInt32)workbookStylesPart.Stylesheet.Fills.Count(); + return allSharedFills; + } + + private static void ResolveFillWithPattern(Fills fills, PatternValues patternValues) + { + if (fills.Elements().Any(f => + f.PatternFill == null + || (f.PatternFill.PatternType == patternValues + && f.PatternFill.ForegroundColor == null + && f.PatternFill.BackgroundColor == null + ))) + return; + + var fill1 = new Fill(); + var patternFill1 = new PatternFill { PatternType = patternValues }; + fill1.AppendChild(patternFill1); + fills.AppendChild(fill1); + } + + private static Fill GetNewFill(FillInfo fillInfo, Boolean differentialFillFormat, Boolean ignoreMod = true) + { + var fill = new Fill(); + + var patternFill = new PatternFill(); + + patternFill.PatternType = fillInfo.Fill.PatternType.ToOpenXml(); + + BackgroundColor backgroundColor; + ForegroundColor foregroundColor; + + switch (fillInfo.Fill.PatternType) + { + case XLFillPatternValues.None: + break; + + case XLFillPatternValues.Solid: + + if (differentialFillFormat) + { + patternFill.AppendChild(new ForegroundColor { Auto = true }); + backgroundColor = new BackgroundColor().FromClosedXMLColor(fillInfo.Fill.BackgroundColor, true); + if (backgroundColor.HasAttributes) + patternFill.AppendChild(backgroundColor); + } + else + { + // ClosedXML Background color to be populated into OpenXML fgColor + foregroundColor = new ForegroundColor().FromClosedXMLColor(fillInfo.Fill.BackgroundColor); + if (foregroundColor.HasAttributes) + patternFill.AppendChild(foregroundColor); + } + break; + + default: + + foregroundColor = new ForegroundColor().FromClosedXMLColor(fillInfo.Fill.PatternColor); + if (foregroundColor.HasAttributes) + patternFill.AppendChild(foregroundColor); + + backgroundColor = new BackgroundColor().FromClosedXMLColor(fillInfo.Fill.BackgroundColor); + if (backgroundColor.HasAttributes) + patternFill.AppendChild(backgroundColor); + + break; + } + + if (patternFill.HasChildren) + fill.AppendChild(patternFill); + + return fill; + } + + private bool FillsAreEqual(Fill f, XLFillValue xlFill, Boolean fromDifferentialFormat) + { + var nF = new XLFill(null); + + LoadFill(f, nF, fromDifferentialFormat); + + return nF.Key.Equals(xlFill.Key); + } + + private void ResolveFonts(WorkbookStylesPart workbookStylesPart, SaveContext context) + { + if (workbookStylesPart.Stylesheet.Fonts == null) + workbookStylesPart.Stylesheet.Fonts = new Fonts(); + + var newFonts = new Dictionary(); + foreach (var fontInfo in context.SharedFonts.Values) + { + var fontId = 0; + var foundOne = false; + foreach (Font f in workbookStylesPart.Stylesheet.Fonts) + { + if (FontsAreEqual(f, fontInfo.Font)) + { + foundOne = true; + break; + } + fontId++; + } + if (!foundOne) + { + var font = GetNewFont(fontInfo); + workbookStylesPart.Stylesheet.Fonts.AppendChild(font); + } + newFonts.Add(fontInfo.Font, new FontInfo { Font = fontInfo.Font, FontId = (UInt32)fontId }); + } + context.SharedFonts.Clear(); + foreach (var kp in newFonts) + context.SharedFonts.Add(kp.Key, kp.Value); + + workbookStylesPart.Stylesheet.Fonts.Count = (UInt32)workbookStylesPart.Stylesheet.Fonts.Count(); + } + + private static Font GetNewFont(FontInfo fontInfo, Boolean ignoreMod = true) + { + var font = new Font(); + var bold = (fontInfo.Font.Bold != XLFontValue.Default.Bold || ignoreMod) && fontInfo.Font.Bold ? new Bold() : null; + var italic = (fontInfo.Font.Italic != XLFontValue.Default.Italic || ignoreMod) && fontInfo.Font.Italic ? new Italic() : null; + var underline = (fontInfo.Font.Underline != XLFontValue.Default.Underline || ignoreMod) && + fontInfo.Font.Underline != XLFontUnderlineValues.None + ? new Underline { Val = fontInfo.Font.Underline.ToOpenXml() } + : null; + var strike = (fontInfo.Font.Strikethrough != XLFontValue.Default.Strikethrough || ignoreMod) && fontInfo.Font.Strikethrough + ? new Strike() + : null; + var verticalAlignment = fontInfo.Font.VerticalAlignment != XLFontValue.Default.VerticalAlignment || ignoreMod + ? new VerticalTextAlignment { Val = fontInfo.Font.VerticalAlignment.ToOpenXml() } + : null; + + var shadow = (fontInfo.Font.Shadow != XLFontValue.Default.Shadow || ignoreMod) && fontInfo.Font.Shadow ? new Shadow() : null; + var fontSize = fontInfo.Font.FontSize != XLFontValue.Default.FontSize || ignoreMod + ? new FontSize { Val = fontInfo.Font.FontSize } + : null; + var color = fontInfo.Font.FontColor != XLFontValue.Default.FontColor || ignoreMod ? new Color().FromClosedXMLColor(fontInfo.Font.FontColor) : null; + + var fontName = fontInfo.Font.FontName != XLFontValue.Default.FontName || ignoreMod + ? new FontName { Val = fontInfo.Font.FontName } + : null; + var fontFamilyNumbering = fontInfo.Font.FontFamilyNumbering != XLFontValue.Default.FontFamilyNumbering || ignoreMod + ? new FontFamilyNumbering { Val = (Int32)fontInfo.Font.FontFamilyNumbering } + : null; + + var fontCharSet = (fontInfo.Font.FontCharSet != XLFontValue.Default.FontCharSet || ignoreMod) && fontInfo.Font.FontCharSet != XLFontCharSet.Default + ? new FontCharSet { Val = (Int32)fontInfo.Font.FontCharSet } + : null; + + if (bold != null) + font.AppendChild(bold); + if (italic != null) + font.AppendChild(italic); + if (underline != null) + font.AppendChild(underline); + if (strike != null) + font.AppendChild(strike); + if (verticalAlignment != null) + font.AppendChild(verticalAlignment); + if (shadow != null) + font.AppendChild(shadow); + if (fontSize != null) + font.AppendChild(fontSize); + if (color != null) + font.AppendChild(color); + if (fontName != null) + font.AppendChild(fontName); + if (fontFamilyNumbering != null) + font.AppendChild(fontFamilyNumbering); + if (fontCharSet != null) + font.AppendChild(fontCharSet); + + return font; + } + + private bool FontsAreEqual(Font f, XLFontValue xlFont) + { + var nf = XLFontValue.Default.Key; + nf.Bold = f.Bold != null; + nf.Italic = f.Italic != null; + + if (f.Underline != null) + { + nf.Underline = f.Underline.Val != null + ? f.Underline.Val.Value.ToClosedXml() + : XLFontUnderlineValues.Single; + } + nf.Strikethrough = f.Strike != null; + if (f.VerticalTextAlignment != null) + { + nf.VerticalAlignment = f.VerticalTextAlignment.Val != null + ? f.VerticalTextAlignment.Val.Value.ToClosedXml() + : XLFontVerticalTextAlignmentValues.Baseline; + } + nf.Shadow = f.Shadow != null; + if (f.FontSize != null) + nf.FontSize = f.FontSize.Val; + if (f.Color != null) + nf.FontColor = f.Color.ToClosedXMLColor(_colorList).Key; + if (f.FontName != null) + nf.FontName = f.FontName.Val; + if (f.FontFamilyNumbering != null) + nf.FontFamilyNumbering = (XLFontFamilyNumberingValues)f.FontFamilyNumbering.Val.Value; + + return nf.Equals(xlFont.Key); + } + + private static Dictionary ResolveNumberFormats( + WorkbookStylesPart workbookStylesPart, + Dictionary sharedNumberFormats, + UInt32 defaultFormatId) + { + if (workbookStylesPart.Stylesheet.NumberingFormats == null) + { + workbookStylesPart.Stylesheet.NumberingFormats = new NumberingFormats(); + workbookStylesPart.Stylesheet.NumberingFormats.AppendChild(new NumberingFormat() + { + NumberFormatId = 0, + FormatCode = "" + }); + } + + var allSharedNumberFormats = new Dictionary(); + foreach (var numberFormatInfo in sharedNumberFormats.Values.Where(nf => nf.NumberFormatId != defaultFormatId)) + { + var numberingFormatId = XLConstants.NumberOfBuiltInStyles; // 0-based + var foundOne = false; + foreach (NumberingFormat nf in workbookStylesPart.Stylesheet.NumberingFormats) + { + if (NumberFormatsAreEqual(nf, numberFormatInfo.NumberFormat)) + { + foundOne = true; + numberingFormatId = (Int32)nf.NumberFormatId.Value; + break; + } + numberingFormatId++; + } + if (!foundOne) + { + var numberingFormat = new NumberingFormat + { + NumberFormatId = (UInt32)numberingFormatId, + FormatCode = numberFormatInfo.NumberFormat.Format + }; + workbookStylesPart.Stylesheet.NumberingFormats.AppendChild(numberingFormat); + } + allSharedNumberFormats.Add(numberFormatInfo.NumberFormat, + new NumberFormatInfo + { + NumberFormat = numberFormatInfo.NumberFormat, + NumberFormatId = numberingFormatId + }); + } + workbookStylesPart.Stylesheet.NumberingFormats.Count = + (UInt32)workbookStylesPart.Stylesheet.NumberingFormats.Count(); + return allSharedNumberFormats; + } + + private static bool NumberFormatsAreEqual(NumberingFormat nf, XLNumberFormatValue xlNumberFormat) + { + if (nf.FormatCode != null && !String.IsNullOrWhiteSpace(nf.FormatCode.Value)) + return string.Equals(xlNumberFormat?.Format, nf.FormatCode.Value); + else if (nf.NumberFormatId != null) + return xlNumberFormat?.NumberFormatId == (Int32)nf.NumberFormatId.Value; + return false; + } + + #endregion GenerateWorkbookStylesPartContent + + #region GenerateWorksheetPartContent + + private static void GenerateWorksheetPartContent( + WorksheetPart worksheetPart, XLWorksheet xlWorksheet, SaveOptions options, SaveContext context) + { + if (options.ConsolidateConditionalFormatRanges) + { + ((XLConditionalFormats)xlWorksheet.ConditionalFormats).Consolidate(); + } + + #region Worksheet + + if (worksheetPart.Worksheet == null) + worksheetPart.Worksheet = new Worksheet(); + + if ( + !worksheetPart.Worksheet.NamespaceDeclarations.Contains(new KeyValuePair("r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"))) + { + worksheetPart.Worksheet.AddNamespaceDeclaration("r", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + } + + #endregion Worksheet + + var cm = new XLWorksheetContentManager(worksheetPart.Worksheet); + + #region SheetProperties + + if (worksheetPart.Worksheet.SheetProperties == null) + worksheetPart.Worksheet.SheetProperties = new SheetProperties(); + + worksheetPart.Worksheet.SheetProperties.TabColor = xlWorksheet.TabColor.HasValue + ? new TabColor().FromClosedXMLColor(xlWorksheet.TabColor) + : null; + + cm.SetElement(XLWorksheetContents.SheetProperties, worksheetPart.Worksheet.SheetProperties); + + if (worksheetPart.Worksheet.SheetProperties.OutlineProperties == null) + worksheetPart.Worksheet.SheetProperties.OutlineProperties = new OutlineProperties(); + + worksheetPart.Worksheet.SheetProperties.OutlineProperties.SummaryBelow = + (xlWorksheet.Outline.SummaryVLocation == + XLOutlineSummaryVLocation.Bottom); + worksheetPart.Worksheet.SheetProperties.OutlineProperties.SummaryRight = + (xlWorksheet.Outline.SummaryHLocation == + XLOutlineSummaryHLocation.Right); + + if (worksheetPart.Worksheet.SheetProperties.PageSetupProperties == null + && (xlWorksheet.PageSetup.PagesTall > 0 || xlWorksheet.PageSetup.PagesWide > 0)) + worksheetPart.Worksheet.SheetProperties.PageSetupProperties = new PageSetupProperties { FitToPage = true }; + + #endregion SheetProperties + + var maxColumn = 0; + + var sheetDimensionReference = "A1"; + if (xlWorksheet.Internals.CellsCollection.Count > 0) + { + maxColumn = xlWorksheet.Internals.CellsCollection.MaxColumnUsed; + var maxRow = xlWorksheet.Internals.CellsCollection.MaxRowUsed; + sheetDimensionReference = "A1:" + XLHelper.GetColumnLetterFromNumber(maxColumn) + + maxRow.ToInvariantString(); + } + + if (xlWorksheet.Internals.ColumnsCollection.Count > 0) + { + var maxColCollection = xlWorksheet.Internals.ColumnsCollection.Keys.Max(); + if (maxColCollection > maxColumn) + maxColumn = maxColCollection; + } + + #region SheetViews + + if (worksheetPart.Worksheet.SheetDimension == null) + worksheetPart.Worksheet.SheetDimension = new SheetDimension { Reference = sheetDimensionReference }; + + cm.SetElement(XLWorksheetContents.SheetDimension, worksheetPart.Worksheet.SheetDimension); + + if (worksheetPart.Worksheet.SheetViews == null) + worksheetPart.Worksheet.SheetViews = new SheetViews(); + + cm.SetElement(XLWorksheetContents.SheetViews, worksheetPart.Worksheet.SheetViews); + + var sheetView = (SheetView)worksheetPart.Worksheet.SheetViews.FirstOrDefault(); + if (sheetView == null) + { + sheetView = new SheetView { WorkbookViewId = 0U }; + worksheetPart.Worksheet.SheetViews.AppendChild(sheetView); + } + + var svcm = new XLSheetViewContentManager(sheetView); + + if (xlWorksheet.TabSelected) + sheetView.TabSelected = true; + else + sheetView.TabSelected = null; + + if (xlWorksheet.RightToLeft) + sheetView.RightToLeft = true; + else + sheetView.RightToLeft = null; + + if (xlWorksheet.ShowFormulas) + sheetView.ShowFormulas = true; + else + sheetView.ShowFormulas = null; + + if (xlWorksheet.ShowGridLines) + sheetView.ShowGridLines = null; + else + sheetView.ShowGridLines = false; + + if (xlWorksheet.ShowOutlineSymbols) + sheetView.ShowOutlineSymbols = null; + else + sheetView.ShowOutlineSymbols = false; + + if (xlWorksheet.ShowRowColHeaders) + sheetView.ShowRowColHeaders = null; + else + sheetView.ShowRowColHeaders = false; + + if (xlWorksheet.ShowRuler) + sheetView.ShowRuler = null; + else + sheetView.ShowRuler = false; + + if (xlWorksheet.ShowWhiteSpace) + sheetView.ShowWhiteSpace = null; + else + sheetView.ShowWhiteSpace = false; + + if (xlWorksheet.ShowZeros) + sheetView.ShowZeros = null; + else + sheetView.ShowZeros = false; + + if (xlWorksheet.RightToLeft) + sheetView.RightToLeft = true; + else + sheetView.RightToLeft = null; + + if (xlWorksheet.SheetView.View == XLSheetViewOptions.Normal) + sheetView.View = null; + else + sheetView.View = xlWorksheet.SheetView.View.ToOpenXml(); + + var pane = sheetView.Elements().FirstOrDefault(); + if (pane == null) + { + pane = new Pane(); + sheetView.InsertAt(pane, 0); + } + + svcm.SetElement(XLSheetViewContents.Pane, pane); + + pane.State = PaneStateValues.FrozenSplit; + int hSplit = xlWorksheet.SheetView.SplitColumn; + int ySplit = xlWorksheet.SheetView.SplitRow; + + pane.HorizontalSplit = hSplit; + pane.VerticalSplit = ySplit; + + pane.ActivePane = (ySplit == 0 ? PaneValues.TopRight : 0) + | (hSplit == 0 ? PaneValues.BottomLeft : 0); + + pane.TopLeftCell = XLHelper.GetColumnLetterFromNumber(xlWorksheet.SheetView.SplitColumn + 1) + + (xlWorksheet.SheetView.SplitRow + 1); + + if (hSplit == 0 && ySplit == 0) + { + // We don't have a pane. Just a regular sheet. + pane = null; + sheetView.RemoveAllChildren(); + svcm.SetElement(XLSheetViewContents.Pane, null); + } + + // Do sheet view. Whether it's for a regular sheet or for the bottom-right pane + if (!xlWorksheet.SheetView.TopLeftCellAddress.IsValid + || xlWorksheet.SheetView.TopLeftCellAddress == new XLAddress(1, 1, fixedRow: false, fixedColumn: false)) + sheetView.TopLeftCell = null; + else + sheetView.TopLeftCell = xlWorksheet.SheetView.TopLeftCellAddress.ToString(); + + if (xlWorksheet.SelectedRanges.Any() || xlWorksheet.ActiveCell != null) + { + sheetView.RemoveAllChildren(); + svcm.SetElement(XLSheetViewContents.Selection, null); + + var firstSelection = xlWorksheet.SelectedRanges.FirstOrDefault(); + + Action populateSelection = (Selection selection) => + { + if (xlWorksheet.ActiveCell != null) + selection.ActiveCell = xlWorksheet.ActiveCell.Address.ToStringRelative(false); + else if (firstSelection != null) + selection.ActiveCell = firstSelection.RangeAddress.FirstAddress.ToStringRelative(false); + + var seqRef = new List { selection.ActiveCell.Value }; + seqRef.AddRange(xlWorksheet.SelectedRanges + .Select(range => + { + if (range.RangeAddress.FirstAddress.Equals(range.RangeAddress.LastAddress)) + return range.RangeAddress.FirstAddress.ToStringRelative(false); + else + return range.RangeAddress.ToStringRelative(false); + }) + ); + + selection.SequenceOfReferences = new ListValue { InnerText = String.Join(" ", seqRef.Distinct().ToArray()) }; + + sheetView.InsertAfter(selection, svcm.GetPreviousElementFor(XLSheetViewContents.Selection)); + svcm.SetElement(XLSheetViewContents.Selection, selection); + }; + + // If a pane exists, we need to set the active pane too + // Yes, this might lead to 2 Selection elements! + if (pane != null) + { + populateSelection(new Selection() + { + Pane = pane.ActivePane + }); + } + populateSelection(new Selection()); + } + + if (xlWorksheet.SheetView.ZoomScale == 100) + sheetView.ZoomScale = null; + else + sheetView.ZoomScale = (UInt32)Math.Max(10, Math.Min(400, xlWorksheet.SheetView.ZoomScale)); + + if (xlWorksheet.SheetView.ZoomScaleNormal == 100) + sheetView.ZoomScaleNormal = null; + else + sheetView.ZoomScaleNormal = (UInt32)Math.Max(10, Math.Min(400, xlWorksheet.SheetView.ZoomScaleNormal)); + + if (xlWorksheet.SheetView.ZoomScalePageLayoutView == 100) + sheetView.ZoomScalePageLayoutView = null; + else + sheetView.ZoomScalePageLayoutView = (UInt32)Math.Max(10, Math.Min(400, xlWorksheet.SheetView.ZoomScalePageLayoutView)); + + if (xlWorksheet.SheetView.ZoomScaleSheetLayoutView == 100) + sheetView.ZoomScaleSheetLayoutView = null; + else + sheetView.ZoomScaleSheetLayoutView = (UInt32)Math.Max(10, Math.Min(400, xlWorksheet.SheetView.ZoomScaleSheetLayoutView)); + + #endregion SheetViews + + var maxOutlineColumn = 0; + if (xlWorksheet.ColumnCount() > 0) + maxOutlineColumn = xlWorksheet.GetMaxColumnOutline(); + + var maxOutlineRow = 0; + if (xlWorksheet.RowCount() > 0) + maxOutlineRow = xlWorksheet.GetMaxRowOutline(); + + #region SheetFormatProperties + + if (worksheetPart.Worksheet.SheetFormatProperties == null) + worksheetPart.Worksheet.SheetFormatProperties = new SheetFormatProperties(); + + cm.SetElement(XLWorksheetContents.SheetFormatProperties, + worksheetPart.Worksheet.SheetFormatProperties); + + worksheetPart.Worksheet.SheetFormatProperties.DefaultRowHeight = xlWorksheet.RowHeight.SaveRound(); + + if (xlWorksheet.RowHeightChanged) + worksheetPart.Worksheet.SheetFormatProperties.CustomHeight = true; + else + worksheetPart.Worksheet.SheetFormatProperties.CustomHeight = null; + + var worksheetColumnWidth = GetColumnWidth(xlWorksheet.ColumnWidth).SaveRound(); + if (xlWorksheet.ColumnWidthChanged) + worksheetPart.Worksheet.SheetFormatProperties.DefaultColumnWidth = worksheetColumnWidth; + else + worksheetPart.Worksheet.SheetFormatProperties.DefaultColumnWidth = null; + + if (maxOutlineColumn > 0) + worksheetPart.Worksheet.SheetFormatProperties.OutlineLevelColumn = (byte)maxOutlineColumn; + else + worksheetPart.Worksheet.SheetFormatProperties.OutlineLevelColumn = null; + + if (maxOutlineRow > 0) + worksheetPart.Worksheet.SheetFormatProperties.OutlineLevelRow = (byte)maxOutlineRow; + else + worksheetPart.Worksheet.SheetFormatProperties.OutlineLevelRow = null; + + #endregion SheetFormatProperties + + #region Columns + + var worksheetStyleId = context.SharedStyles[xlWorksheet.StyleValue].StyleId; + if (xlWorksheet.Internals.CellsCollection.Count == 0 && + xlWorksheet.Internals.ColumnsCollection.Count == 0 + && worksheetStyleId == 0) + worksheetPart.Worksheet.RemoveAllChildren(); + else + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.Columns); + worksheetPart.Worksheet.InsertAfter(new Columns(), previousElement); + } + + var columns = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.Columns, columns); + + var sheetColumnsByMin = columns.Elements().ToDictionary(c => c.Min.Value, c => c); + //Dictionary sheetColumnsByMax = columns.Elements().ToDictionary(c => c.Max.Value, c => c); + + Int32 minInColumnsCollection; + Int32 maxInColumnsCollection; + if (xlWorksheet.Internals.ColumnsCollection.Count > 0) + { + minInColumnsCollection = xlWorksheet.Internals.ColumnsCollection.Keys.Min(); + maxInColumnsCollection = xlWorksheet.Internals.ColumnsCollection.Keys.Max(); + } + else + { + minInColumnsCollection = 1; + maxInColumnsCollection = 0; + } + + if (minInColumnsCollection > 1) + { + UInt32Value min = 1; + UInt32Value max = (UInt32)(minInColumnsCollection - 1); + + for (var co = min; co <= max; co++) + { + var column = new Column + { + Min = co, + Max = co, + Style = worksheetStyleId, + Width = worksheetColumnWidth, + CustomWidth = true + }; + + UpdateColumn(column, columns, sheetColumnsByMin); //, sheetColumnsByMax); + } + } + + for (var co = minInColumnsCollection; co <= maxInColumnsCollection; co++) + { + UInt32 styleId; + Double columnWidth; + var isHidden = false; + var collapsed = false; + var outlineLevel = 0; + if (xlWorksheet.Internals.ColumnsCollection.TryGetValue(co, out XLColumn col)) + { + styleId = context.SharedStyles[col.StyleValue].StyleId; + columnWidth = GetColumnWidth(col.Width).SaveRound(); + isHidden = col.IsHidden; + collapsed = col.Collapsed; + outlineLevel = col.OutlineLevel; + } + else + { + styleId = context.SharedStyles[xlWorksheet.StyleValue].StyleId; + columnWidth = worksheetColumnWidth; + } + + var column = new Column + { + Min = (UInt32)co, + Max = (UInt32)co, + Style = styleId, + Width = columnWidth, + CustomWidth = true + }; + + if (isHidden) + column.Hidden = true; + if (collapsed) + column.Collapsed = true; + if (outlineLevel > 0) + column.OutlineLevel = (byte)outlineLevel; + + UpdateColumn(column, columns, sheetColumnsByMin); //, sheetColumnsByMax); + } + + var collection = maxInColumnsCollection; + foreach ( + var col in + columns.Elements().Where(c => c.Min > (UInt32)(collection)).OrderBy( + c => c.Min.Value)) + { + col.Style = worksheetStyleId; + col.Width = worksheetColumnWidth; + col.CustomWidth = true; + + if ((Int32)col.Max.Value > maxInColumnsCollection) + maxInColumnsCollection = (Int32)col.Max.Value; + } + + if (maxInColumnsCollection < XLHelper.MaxColumnNumber && worksheetStyleId != 0) + { + var column = new Column + { + Min = (UInt32)(maxInColumnsCollection + 1), + Max = (UInt32)(XLHelper.MaxColumnNumber), + Style = worksheetStyleId, + Width = worksheetColumnWidth, + CustomWidth = true + }; + columns.AppendChild(column); + } + + CollapseColumns(columns, sheetColumnsByMin); + + if (!columns.Any()) + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.Columns, null); + } + } + + #endregion Columns + + #region SheetData + + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.SheetData); + worksheetPart.Worksheet.InsertAfter(new SheetData(), previousElement); + } + + var sheetData = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.SheetData, sheetData); + + var lastRow = 0; + var existingSheetDataRows = + sheetData.Elements().ToDictionary(r => r.RowIndex == null ? ++lastRow : (Int32)r.RowIndex.Value, + r => r); + foreach ( + var r in + xlWorksheet.Internals.RowsCollection.Deleted.Where(r => existingSheetDataRows.ContainsKey(r.Key))) + { + sheetData.RemoveChild(existingSheetDataRows[r.Key]); + existingSheetDataRows.Remove(r.Key); + xlWorksheet.Internals.CellsCollection.Deleted.Remove(r.Key); + } + + var tableTotalCells = new HashSet( + xlWorksheet.Tables + .Where(table => table.ShowTotalsRow) + .SelectMany(table => + table.TotalsRow().CellsUsed()) + .Select(cell => cell.Address)); + + var distinctRows = xlWorksheet.Internals.CellsCollection.RowsCollection.Keys.Union(xlWorksheet.Internals.RowsCollection.Keys); + var noRows = !sheetData.Elements().Any(); + foreach (var distinctRow in distinctRows.OrderBy(r => r)) + { + Row row; + if (!existingSheetDataRows.TryGetValue(distinctRow, out row)) + row = new Row { RowIndex = (UInt32)distinctRow }; + + if (maxColumn > 0) + row.Spans = new ListValue { InnerText = "1:" + maxColumn.ToInvariantString() }; + + row.Height = null; + row.CustomHeight = null; + row.Hidden = null; + row.StyleIndex = null; + row.CustomFormat = null; + row.Collapsed = null; + if (xlWorksheet.Internals.RowsCollection.TryGetValue(distinctRow, out XLRow thisRow)) + { + if (thisRow.HeightChanged) + { + row.Height = thisRow.Height.SaveRound(); + row.CustomHeight = true; + row.CustomFormat = true; + } + + if (thisRow.StyleValue != xlWorksheet.StyleValue) + { + row.StyleIndex = context.SharedStyles[thisRow.StyleValue].StyleId; + row.CustomFormat = true; + } + + if (thisRow.IsHidden) + row.Hidden = true; + if (thisRow.Collapsed) + row.Collapsed = true; + if (thisRow.OutlineLevel > 0) + row.OutlineLevel = (byte)thisRow.OutlineLevel; + } + + var lastCell = 0; + var currentOpenXmlRowCells = row.Elements() + .ToDictionary + ( + c => c.CellReference?.Value ?? XLHelper.GetColumnLetterFromNumber(++lastCell) + distinctRow, + c => c + ); + + if (xlWorksheet.Internals.CellsCollection.Deleted.TryGetValue(distinctRow, out HashSet deletedColumns)) + { + foreach (var deletedColumn in deletedColumns.ToList()) + { + var key = XLHelper.GetColumnLetterFromNumber(deletedColumn) + distinctRow.ToInvariantString(); + + if (!currentOpenXmlRowCells.TryGetValue(key, out Cell cell)) + continue; + + row.RemoveChild(cell); + deletedColumns.Remove(deletedColumn); + } + if (deletedColumns.Count == 0) + xlWorksheet.Internals.CellsCollection.Deleted.Remove(distinctRow); + } + + if (xlWorksheet.Internals.CellsCollection.RowsCollection.TryGetValue(distinctRow, out Dictionary cells)) + { + var isNewRow = !row.Elements().Any(); + lastCell = 0; + var mRows = row.Elements().ToDictionary(c => XLHelper.GetColumnNumberFromAddress(c.CellReference == null + ? (XLHelper.GetColumnLetterFromNumber(++lastCell) + distinctRow) : c.CellReference.Value), c => c); + foreach (var xlCell in cells.Values + .OrderBy(c => c.Address.ColumnNumber) + .Select(c => c)) + { + XLTableField field = null; + + var styleId = context.SharedStyles[xlCell.StyleValue].StyleId; + var cellReference = (xlCell.Address).GetTrimmedAddress(); + + // For saving cells to file, ignore conditional formatting, data validation rules and merged + // ranges. They just bloat the file + var isEmpty = xlCell.IsEmpty(XLCellsUsedOptions.All + & ~XLCellsUsedOptions.ConditionalFormats + & ~XLCellsUsedOptions.DataValidation + & ~XLCellsUsedOptions.MergedRanges); + + if (currentOpenXmlRowCells.TryGetValue(cellReference, out Cell cell)) + { + if (isEmpty) + { + cell.Remove(); + } + + // reset some stuff that we'll populate later + cell.DataType = null; + cell.RemoveAllChildren(); + } + + if (!isEmpty) + { + if (cell == null) + { + cell = new Cell(); + cell.CellReference = new StringValue(cellReference); + + if (isNewRow) + row.AppendChild(cell); + else + { + var newColumn = XLHelper.GetColumnNumberFromAddress(cellReference); + + Cell cellBeforeInsert = null; + int[] lastCo = { Int32.MaxValue }; + foreach (var c in mRows.Where(kp => kp.Key > newColumn).Where(c => lastCo[0] > c.Key)) + { + cellBeforeInsert = c.Value; + lastCo[0] = c.Key; + } + if (cellBeforeInsert == null) + row.AppendChild(cell); + else + row.InsertBefore(cell, cellBeforeInsert); + } + } + + cell.StyleIndex = styleId; + if (xlCell.HasFormula) + { + var formula = xlCell.FormulaA1; + if (xlCell.HasArrayFormula) + { + formula = formula.Substring(1, formula.Length - 2); + var f = new CellFormula { FormulaType = CellFormulaValues.Array }; + + if (xlCell.FormulaReference == null) + xlCell.FormulaReference = xlCell.AsRange().RangeAddress; + + if (xlCell.FormulaReference.FirstAddress.Equals(xlCell.Address)) + { + f.Text = formula; + f.Reference = xlCell.FormulaReference.ToStringRelative(); + } + + cell.CellFormula = f; + } + else + { + cell.CellFormula = new CellFormula(); + cell.CellFormula.Text = formula; + } + + if (!options.EvaluateFormulasBeforeSaving || xlCell.CachedValue == null || xlCell.NeedsRecalculation) + cell.CellValue = null; + else + { + string valueCalculated; + if (xlCell.CachedValue is int) + valueCalculated = ((int)xlCell.CachedValue).ToInvariantString(); + else if (xlCell.CachedValue is double) + valueCalculated = ((double)xlCell.CachedValue).ToInvariantString(); + else + valueCalculated = xlCell.CachedValue.ToString(); + + cell.CellValue = new CellValue(valueCalculated); + } + } + else if (tableTotalCells.Contains(xlCell.Address)) + { + var table = xlWorksheet.Tables.First(t => t.AsRange().Contains(xlCell)); + field = table.Fields.First(f => f.Column.ColumnNumber() == xlCell.Address.ColumnNumber) as XLTableField; + + if (!String.IsNullOrWhiteSpace(field.TotalsRowLabel)) + { + cell.DataType = XLWorkbook.CvSharedString; + } + else + { + cell.DataType = null; + } + cell.CellFormula = null; + } + else + { + cell.CellFormula = null; + cell.DataType = xlCell.DataType == XLDataType.DateTime ? null : GetCellValueType(xlCell); + } + + if (options.EvaluateFormulasBeforeSaving || field != null || !xlCell.HasFormula) + SetCellValue(xlCell, field, cell, context); + } + } + xlWorksheet.Internals.CellsCollection.Deleted.Remove(distinctRow); + } + + // If we're adding a new row (not in sheet already and it's not "empty" + if (!existingSheetDataRows.ContainsKey(distinctRow)) + { + var invalidRow = row.Height == null + && row.CustomHeight == null + && row.Hidden == null + && row.StyleIndex == null + && row.CustomFormat == null + && row.Collapsed == null + && row.OutlineLevel == null + && !row.Elements().Any(); + + if (!invalidRow) + { + if (noRows) + { + sheetData.AppendChild(row); + noRows = false; + } + else + { + if (existingSheetDataRows.Any(r => r.Key > row.RowIndex.Value)) + { + var minRow = existingSheetDataRows.Where(r => r.Key > (Int32)row.RowIndex.Value).Min(r => r.Key); + var rowBeforeInsert = existingSheetDataRows[minRow]; + sheetData.InsertBefore(row, rowBeforeInsert); + } + else + sheetData.AppendChild(row); + } + } + } + } + + foreach (var r in xlWorksheet.Internals.CellsCollection.Deleted.Keys) + { + if (existingSheetDataRows.TryGetValue(r, out Row row)) + { + sheetData.RemoveChild(row); + existingSheetDataRows.Remove(r); + } + } + + #endregion SheetData + + #region SheetProtection + + if (xlWorksheet.Protection.IsProtected) + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.SheetProtection); + worksheetPart.Worksheet.InsertAfter(new SheetProtection(), previousElement); + } + + var sheetProtection = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.SheetProtection, sheetProtection); + + var protection = xlWorksheet.Protection; + sheetProtection.Sheet = OpenXmlHelper.GetBooleanValue(protection.IsProtected, false); + + sheetProtection.Password = null; + sheetProtection.AlgorithmName = null; + sheetProtection.HashValue = null; + sheetProtection.SpinCount = null; + sheetProtection.SaltValue = null; + + if (protection.Algorithm == XLProtectionAlgorithm.Algorithm.SimpleHash) + { + if (!String.IsNullOrWhiteSpace(protection.PasswordHash)) + sheetProtection.Password = protection.PasswordHash; + } + else + { + sheetProtection.AlgorithmName = DescribedEnumParser.ToDescription(protection.Algorithm); + sheetProtection.HashValue = protection.PasswordHash; + sheetProtection.SpinCount = protection.SpinCount; + sheetProtection.SaltValue = protection.Base64EncodedSalt; + } + + // default value of "1" + sheetProtection.FormatCells = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells), true); + sheetProtection.FormatColumns = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns), true); + sheetProtection.FormatRows = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows), true); + sheetProtection.InsertColumns = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.InsertColumns), true); + sheetProtection.InsertRows = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.InsertRows), true); + sheetProtection.InsertHyperlinks = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.InsertHyperlinks), true); + sheetProtection.DeleteColumns = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteColumns), true); + sheetProtection.DeleteRows = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteRows), true); + sheetProtection.Sort = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort), true); + sheetProtection.AutoFilter = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter), true); + sheetProtection.PivotTables = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.PivotTables), true); + sheetProtection.Scenarios = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.EditScenarios), true); + + // default value of "0" + sheetProtection.Objects = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.EditObjects), false); + sheetProtection.SelectLockedCells = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectLockedCells), false); + sheetProtection.SelectUnlockedCells = OpenXmlHelper.GetBooleanValue(!protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectUnlockedCells), false); + } + else + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.SheetProtection, null); + } + + #endregion SheetProtection + + #region AutoFilter + + worksheetPart.Worksheet.RemoveAllChildren(); + if (xlWorksheet.AutoFilter.IsEnabled) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.AutoFilter); + worksheetPart.Worksheet.InsertAfter(new AutoFilter(), previousElement); + + var autoFilter = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.AutoFilter, autoFilter); + + PopulateAutoFilter(xlWorksheet.AutoFilter, autoFilter); + } + else + { + cm.SetElement(XLWorksheetContents.AutoFilter, null); + } + + #endregion AutoFilter + + #region MergeCells + + if ((xlWorksheet).Internals.MergedRanges.Any()) + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.MergeCells); + worksheetPart.Worksheet.InsertAfter(new MergeCells(), previousElement); + } + + var mergeCells = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.MergeCells, mergeCells); + mergeCells.RemoveAllChildren(); + + foreach (var mergeCell in (xlWorksheet).Internals.MergedRanges.Select( + m => m.RangeAddress.FirstAddress.ToString() + ":" + m.RangeAddress.LastAddress.ToString()).Select( + merged => new MergeCell { Reference = merged })) + mergeCells.AppendChild(mergeCell); + + mergeCells.Count = (UInt32)mergeCells.Count(); + } + else + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.MergeCells, null); + } + + #endregion MergeCells + + #region Conditional Formatting + + if (!xlWorksheet.ConditionalFormats.Any()) + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.ConditionalFormatting, null); + } + else + { + worksheetPart.Worksheet.RemoveAllChildren(); + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.ConditionalFormatting); + + var conditionalFormats = xlWorksheet.ConditionalFormats.ToList(); // Required for IndexOf method + + foreach (var cfGroup in conditionalFormats + .GroupBy( + c => string.Join(" ", c.Ranges.Select(r => r.RangeAddress.ToStringRelative(false))), + c => c, + (key, g) => new { RangeId = key, CfList = g.ToList() } + ) + ) + { + var conditionalFormatting = new ConditionalFormatting + { + SequenceOfReferences = + new ListValue { InnerText = cfGroup.RangeId } + }; + foreach (var cf in cfGroup.CfList) + { + var priority = conditionalFormats.IndexOf(cf) + 1; + conditionalFormatting.Append(XLCFConverters.Convert(cf, priority, context)); + } + worksheetPart.Worksheet.InsertAfter(conditionalFormatting, previousElement); + previousElement = conditionalFormatting; + cm.SetElement(XLWorksheetContents.ConditionalFormatting, conditionalFormatting); + } + } + + var exlst = from c in xlWorksheet.ConditionalFormats where c.ConditionalFormatType == XLConditionalFormatType.DataBar && typeof(IXLConditionalFormat).IsAssignableFrom(c.GetType()) select c; + if (exlst != null && exlst.Any()) + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.WorksheetExtensionList); + worksheetPart.Worksheet.InsertAfter(new WorksheetExtensionList(), previousElement); + } + + WorksheetExtensionList worksheetExtensionList = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.WorksheetExtensionList, worksheetExtensionList); + + var conditionalFormattings = worksheetExtensionList.Descendants().SingleOrDefault(); + if (conditionalFormattings == null || !conditionalFormattings.Any()) + { + WorksheetExtension worksheetExtension1 = new WorksheetExtension { Uri = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" }; + worksheetExtension1.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"); + worksheetExtensionList.Append(worksheetExtension1); + + conditionalFormattings = new DocumentFormat.OpenXml.Office2010.Excel.ConditionalFormattings(); + worksheetExtension1.Append(conditionalFormattings); + } + + foreach (var cfGroup in exlst + .GroupBy( + c => string.Join(" ", c.Ranges.Select(r => r.RangeAddress.ToStringRelative(false))), + c => c, + (key, g) => new { RangeId = key, CfList = g.ToList() } + ) + ) + { + foreach (var xlConditionalFormat in cfGroup.CfList.Cast()) + { + var conditionalFormattingRule = conditionalFormattings.Descendants() + .SingleOrDefault(r => r.Id == xlConditionalFormat.Id.WrapInBraces()); + if (conditionalFormattingRule != null) + { + var conditionalFormat = conditionalFormattingRule.Ancestors().SingleOrDefault(); + conditionalFormattings.RemoveChild(conditionalFormat); + } + + var conditionalFormatting = new DocumentFormat.OpenXml.Office2010.Excel.ConditionalFormatting(); + conditionalFormatting.AddNamespaceDeclaration("xm", "http://schemas.microsoft.com/office/excel/2006/main"); + conditionalFormatting.Append(XLCFConvertersExtension.Convert(xlConditionalFormat, context)); + var referenceSequence = new DocumentFormat.OpenXml.Office.Excel.ReferenceSequence { Text = cfGroup.RangeId }; + conditionalFormatting.Append(referenceSequence); + + conditionalFormattings.Append(conditionalFormatting); + } + } + } + + #endregion Conditional Formatting + + #region Sparklines + + const string sparklineGroupsExtensionUri = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"; + + if (!xlWorksheet.SparklineGroups.Any()) + { + var worksheetExtensionList = worksheetPart.Worksheet.Elements().FirstOrDefault(); + var worksheetExtension = worksheetExtensionList?.Elements() + .FirstOrDefault(ext => string.Equals(ext.Uri, sparklineGroupsExtensionUri, StringComparison.InvariantCultureIgnoreCase)); + + worksheetExtension?.RemoveAllChildren(); + + if (worksheetExtensionList != null) + { + if (worksheetExtension != null && !worksheetExtension.HasChildren) + { + worksheetExtensionList.RemoveChild(worksheetExtension); + } + + if (!worksheetExtensionList.HasChildren) + { + worksheetPart.Worksheet.RemoveChild(worksheetExtensionList); + cm.SetElement(XLWorksheetContents.WorksheetExtensionList, null); + } + } + } + else + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.WorksheetExtensionList); + worksheetPart.Worksheet.InsertAfter(new WorksheetExtensionList(), previousElement); + } + + var worksheetExtensionList = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.WorksheetExtensionList, worksheetExtensionList); + + var sparklineGroups = worksheetExtensionList.Descendants().SingleOrDefault(); + + if (sparklineGroups == null || !sparklineGroups.Any()) + { + var worksheetExtension1 = new WorksheetExtension() { Uri = sparklineGroupsExtensionUri }; + worksheetExtension1.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"); + worksheetExtensionList.Append(worksheetExtension1); + + sparklineGroups = new X14.SparklineGroups(); + sparklineGroups.AddNamespaceDeclaration("xm", "http://schemas.microsoft.com/office/excel/2006/main"); + worksheetExtension1.Append(sparklineGroups); + } + else + { + sparklineGroups.RemoveAllChildren(); + } + + foreach (var xlSparklineGroup in xlWorksheet.SparklineGroups) + { + // Do not create an empty Sparkline group + if (!xlSparklineGroup.Any()) + continue; + + var sparklineGroup = new X14.SparklineGroup(); + sparklineGroup.SetAttribute(new OpenXmlAttribute("xr2", "uid", "http://schemas.microsoft.com/office/spreadsheetml/2015/revision2", "{A98FF5F8-AE60-43B5-8001-AD89004F45D3}")); + + sparklineGroup.FirstMarkerColor = new X14.FirstMarkerColor().FromClosedXMLColor(xlSparklineGroup.Style.FirstMarkerColor); + sparklineGroup.LastMarkerColor = new X14.LastMarkerColor().FromClosedXMLColor(xlSparklineGroup.Style.LastMarkerColor); + sparklineGroup.HighMarkerColor = new X14.HighMarkerColor().FromClosedXMLColor(xlSparklineGroup.Style.HighMarkerColor); + sparklineGroup.LowMarkerColor = new X14.LowMarkerColor().FromClosedXMLColor(xlSparklineGroup.Style.LowMarkerColor); + sparklineGroup.SeriesColor = new X14.SeriesColor().FromClosedXMLColor(xlSparklineGroup.Style.SeriesColor); + sparklineGroup.NegativeColor = new X14.NegativeColor().FromClosedXMLColor(xlSparklineGroup.Style.NegativeColor); + sparklineGroup.MarkersColor = new X14.MarkersColor().FromClosedXMLColor(xlSparklineGroup.Style.MarkersColor); + + sparklineGroup.High = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.HighPoint); + sparklineGroup.Low = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.LowPoint); + sparklineGroup.First = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.FirstPoint); + sparklineGroup.Last = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.LastPoint); + sparklineGroup.Negative = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.NegativePoints); + sparklineGroup.Markers = xlSparklineGroup.ShowMarkers.HasFlag(XLSparklineMarkers.Markers); + + sparklineGroup.DisplayHidden = xlSparklineGroup.DisplayHidden; + sparklineGroup.LineWeight = xlSparklineGroup.LineWeight; + sparklineGroup.Type = xlSparklineGroup.Type.ToOpenXml(); + sparklineGroup.DisplayEmptyCellsAs = xlSparklineGroup.DisplayEmptyCellsAs.ToOpenXml(); + + sparklineGroup.AxisColor = new X14.AxisColor() { Rgb = xlSparklineGroup.HorizontalAxis.Color.Color.ToHex() }; + sparklineGroup.DisplayXAxis = xlSparklineGroup.HorizontalAxis.IsVisible; + sparklineGroup.RightToLeft = xlSparklineGroup.HorizontalAxis.RightToLeft; + sparklineGroup.DateAxis = xlSparklineGroup.HorizontalAxis.DateAxis; + if (xlSparklineGroup.HorizontalAxis.DateAxis) + sparklineGroup.Formula = new OfficeExcel.Formula( + xlSparklineGroup.DateRange.RangeAddress.ToString(XLReferenceStyle.A1, true)); + + sparklineGroup.MinAxisType = xlSparklineGroup.VerticalAxis.MinAxisType.ToOpenXml(); + if (xlSparklineGroup.VerticalAxis.MinAxisType == XLSparklineAxisMinMax.Custom) + sparklineGroup.ManualMin = xlSparklineGroup.VerticalAxis.ManualMin; + + sparklineGroup.MaxAxisType = xlSparklineGroup.VerticalAxis.MaxAxisType.ToOpenXml(); + if (xlSparklineGroup.VerticalAxis.MaxAxisType == XLSparklineAxisMinMax.Custom) + sparklineGroup.ManualMax = xlSparklineGroup.VerticalAxis.ManualMax; + + var sparklines = new X14.Sparklines(xlSparklineGroup + .Select(xlSparkline => new X14.Sparkline + { + Formula = new OfficeExcel.Formula(xlSparkline.SourceData.RangeAddress.ToString(XLReferenceStyle.A1, true)), + ReferenceSequence = + new OfficeExcel.ReferenceSequence(xlSparkline.Location.Address.ToString()) + }) + ); + + sparklineGroup.Append(sparklines); + sparklineGroups.Append(sparklineGroup); + } + + // if all Sparkline groups had no Sparklines, remove the entire SparklineGroup element + if (sparklineGroups.ChildElements.Count == 0) + { + sparklineGroups.Remove(); + } + } + + #endregion Sparklines + + #region DataValidations + + if (!xlWorksheet.DataValidations.Any(d => d.IsDirty())) + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.DataValidations, null); + } + else + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.DataValidations); + worksheetPart.Worksheet.InsertAfter(new DataValidations(), previousElement); + } + + var dataValidations = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.DataValidations, dataValidations); + dataValidations.RemoveAllChildren(); + + if (options.ConsolidateDataValidationRanges) + { + xlWorksheet.DataValidations.Consolidate(); + } + + foreach (var dv in xlWorksheet.DataValidations) + { + var sequence = dv.Ranges.Aggregate(String.Empty, (current, r) => current + (r.RangeAddress + " ")); + + if (sequence.Length > 0) + sequence = sequence.Substring(0, sequence.Length - 1); + + var dataValidation = new DataValidation + { + AllowBlank = dv.IgnoreBlanks, + Formula1 = new Formula1(dv.MinValue), + Formula2 = new Formula2(dv.MaxValue), + Type = dv.AllowedValues.ToOpenXml(), + ShowErrorMessage = dv.ShowErrorMessage, + Prompt = dv.InputMessage, + PromptTitle = dv.InputTitle, + ErrorTitle = dv.ErrorTitle, + Error = dv.ErrorMessage, + ShowDropDown = !dv.InCellDropdown, + ShowInputMessage = dv.ShowInputMessage, + ErrorStyle = dv.ErrorStyle.ToOpenXml(), + Operator = dv.Operator.ToOpenXml(), + SequenceOfReferences = + new ListValue { InnerText = sequence } + }; + + dataValidations.AppendChild(dataValidation); + } + dataValidations.Count = (UInt32)xlWorksheet.DataValidations.Count(); + } + + #endregion DataValidations + + #region Hyperlinks + + var relToRemove = worksheetPart.HyperlinkRelationships.ToList(); + relToRemove.ForEach(worksheetPart.DeleteReferenceRelationship); + if (!xlWorksheet.Hyperlinks.Any()) + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.Hyperlinks, null); + } + else + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.Hyperlinks); + worksheetPart.Worksheet.InsertAfter(new Hyperlinks(), previousElement); + } + + var hyperlinks = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.Hyperlinks, hyperlinks); + hyperlinks.RemoveAllChildren(); + foreach (var hl in xlWorksheet.Hyperlinks) + { + Hyperlink hyperlink; + if (hl.IsExternal) + { + var rId = context.RelIdGenerator.GetNext(RelType.Workbook); + hyperlink = new Hyperlink { Reference = hl.Cell.Address.ToString(), Id = rId }; + worksheetPart.AddHyperlinkRelationship(hl.ExternalAddress, true, rId); + } + else + { + hyperlink = new Hyperlink + { + Reference = hl.Cell.Address.ToString(), + Location = hl.InternalAddress, + Display = hl.Cell.GetFormattedString() + }; + } + if (!String.IsNullOrWhiteSpace(hl.Tooltip)) + hyperlink.Tooltip = hl.Tooltip; + hyperlinks.AppendChild(hyperlink); + } + } + + #endregion Hyperlinks + + #region PrintOptions + + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.PrintOptions); + worksheetPart.Worksheet.InsertAfter(new PrintOptions(), previousElement); + } + + var printOptions = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.PrintOptions, printOptions); + + printOptions.HorizontalCentered = xlWorksheet.PageSetup.CenterHorizontally; + printOptions.VerticalCentered = xlWorksheet.PageSetup.CenterVertically; + printOptions.Headings = xlWorksheet.PageSetup.ShowRowAndColumnHeadings; + printOptions.GridLines = xlWorksheet.PageSetup.ShowGridlines; + + #endregion PrintOptions + + #region PageMargins + + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.PageMargins); + worksheetPart.Worksheet.InsertAfter(new PageMargins(), previousElement); + } + + var pageMargins = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.PageMargins, pageMargins); + pageMargins.Left = xlWorksheet.PageSetup.Margins.Left; + pageMargins.Right = xlWorksheet.PageSetup.Margins.Right; + pageMargins.Top = xlWorksheet.PageSetup.Margins.Top; + pageMargins.Bottom = xlWorksheet.PageSetup.Margins.Bottom; + pageMargins.Header = xlWorksheet.PageSetup.Margins.Header; + pageMargins.Footer = xlWorksheet.PageSetup.Margins.Footer; + + #endregion PageMargins + + #region PageSetup + + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.PageSetup); + worksheetPart.Worksheet.InsertAfter(new PageSetup(), previousElement); + } + + var pageSetup = worksheetPart.Worksheet.Elements().First(); + cm.SetElement(XLWorksheetContents.PageSetup, pageSetup); + + pageSetup.Orientation = xlWorksheet.PageSetup.PageOrientation.ToOpenXml(); + pageSetup.PaperSize = (UInt32)xlWorksheet.PageSetup.PaperSize; + pageSetup.BlackAndWhite = xlWorksheet.PageSetup.BlackAndWhite; + pageSetup.Draft = xlWorksheet.PageSetup.DraftQuality; + pageSetup.PageOrder = xlWorksheet.PageSetup.PageOrder.ToOpenXml(); + pageSetup.CellComments = xlWorksheet.PageSetup.ShowComments.ToOpenXml(); + pageSetup.Errors = xlWorksheet.PageSetup.PrintErrorValue.ToOpenXml(); + + if (xlWorksheet.PageSetup.FirstPageNumber.HasValue) + { + pageSetup.FirstPageNumber = UInt32Value.FromUInt32(xlWorksheet.PageSetup.FirstPageNumber.Value); + pageSetup.UseFirstPageNumber = true; + } + else + { + pageSetup.FirstPageNumber = null; + pageSetup.UseFirstPageNumber = null; + } + + if (xlWorksheet.PageSetup.HorizontalDpi > 0) + pageSetup.HorizontalDpi = (UInt32)xlWorksheet.PageSetup.HorizontalDpi; + else + pageSetup.HorizontalDpi = null; + + if (xlWorksheet.PageSetup.VerticalDpi > 0) + pageSetup.VerticalDpi = (UInt32)xlWorksheet.PageSetup.VerticalDpi; + else + pageSetup.VerticalDpi = null; + + if (xlWorksheet.PageSetup.Scale > 0) + { + pageSetup.Scale = (UInt32)xlWorksheet.PageSetup.Scale; + pageSetup.FitToWidth = null; + pageSetup.FitToHeight = null; + } + else + { + pageSetup.Scale = null; + + if (xlWorksheet.PageSetup.PagesWide >= 0 && xlWorksheet.PageSetup.PagesWide != 1) + pageSetup.FitToWidth = (UInt32)xlWorksheet.PageSetup.PagesWide; + + if (xlWorksheet.PageSetup.PagesTall >= 0 && xlWorksheet.PageSetup.PagesTall != 1) + pageSetup.FitToHeight = (UInt32)xlWorksheet.PageSetup.PagesTall; + } + + // For some reason some Excel files already contains pageSetup.Copies = 0 + // The validation fails for this + // Let's remove the attribute of that's the case. + if ((pageSetup?.Copies ?? 0) <= 0) + pageSetup.Copies = null; + + #endregion PageSetup + + #region HeaderFooter + + var headerFooter = worksheetPart.Worksheet.Elements().FirstOrDefault(); + if (headerFooter == null) + headerFooter = new HeaderFooter(); + else + worksheetPart.Worksheet.RemoveAllChildren(); + + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.HeaderFooter); + worksheetPart.Worksheet.InsertAfter(headerFooter, previousElement); + cm.SetElement(XLWorksheetContents.HeaderFooter, headerFooter); + } + if (((XLHeaderFooter)xlWorksheet.PageSetup.Header).Changed + || ((XLHeaderFooter)xlWorksheet.PageSetup.Footer).Changed) + { + headerFooter.RemoveAllChildren(); + + headerFooter.ScaleWithDoc = xlWorksheet.PageSetup.ScaleHFWithDocument; + headerFooter.AlignWithMargins = xlWorksheet.PageSetup.AlignHFWithMargins; + headerFooter.DifferentFirst = xlWorksheet.PageSetup.DifferentFirstPageOnHF; + headerFooter.DifferentOddEven = xlWorksheet.PageSetup.DifferentOddEvenPagesOnHF; + + var oddHeader = new OddHeader(xlWorksheet.PageSetup.Header.GetText(XLHFOccurrence.OddPages)); + headerFooter.AppendChild(oddHeader); + var oddFooter = new OddFooter(xlWorksheet.PageSetup.Footer.GetText(XLHFOccurrence.OddPages)); + headerFooter.AppendChild(oddFooter); + + var evenHeader = new EvenHeader(xlWorksheet.PageSetup.Header.GetText(XLHFOccurrence.EvenPages)); + headerFooter.AppendChild(evenHeader); + var evenFooter = new EvenFooter(xlWorksheet.PageSetup.Footer.GetText(XLHFOccurrence.EvenPages)); + headerFooter.AppendChild(evenFooter); + + var firstHeader = new FirstHeader(xlWorksheet.PageSetup.Header.GetText(XLHFOccurrence.FirstPage)); + headerFooter.AppendChild(firstHeader); + var firstFooter = new FirstFooter(xlWorksheet.PageSetup.Footer.GetText(XLHFOccurrence.FirstPage)); + headerFooter.AppendChild(firstFooter); + } + + #endregion HeaderFooter + + #region RowBreaks + + var rowBreakCount = xlWorksheet.PageSetup.RowBreaks.Count; + if (rowBreakCount > 0) + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.RowBreaks); + worksheetPart.Worksheet.InsertAfter(new RowBreaks(), previousElement); + } + + var rowBreaks = worksheetPart.Worksheet.Elements().First(); + + var existingBreaks = rowBreaks.ChildElements.OfType(); + var rowBreaksToDelete = existingBreaks + .Where(rb => !rb.Id.HasValue || + !xlWorksheet.PageSetup.RowBreaks.Contains((int)rb.Id.Value)) + .ToList(); + + foreach (var rb in rowBreaksToDelete) + { + rowBreaks.RemoveChild(rb); + } + + var rowBreaksToAdd = xlWorksheet.PageSetup.RowBreaks + .Where(xlRb => !existingBreaks.Any(rb => rb.Id.HasValue && rb.Id.Value == xlRb)); + + rowBreaks.Count = (UInt32)rowBreakCount; + rowBreaks.ManualBreakCount = (UInt32)rowBreakCount; + var lastRowNum = (UInt32)xlWorksheet.RangeAddress.LastAddress.RowNumber; + foreach (var break1 in rowBreaksToAdd.Select(rb => new Break + { + Id = (UInt32)rb, + Max = lastRowNum, + ManualPageBreak = true + })) + rowBreaks.AppendChild(break1); + cm.SetElement(XLWorksheetContents.RowBreaks, rowBreaks); + } + else + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.RowBreaks, null); + } + + #endregion RowBreaks + + #region ColumnBreaks + + var columnBreakCount = xlWorksheet.PageSetup.ColumnBreaks.Count; + if (columnBreakCount > 0) + { + if (!worksheetPart.Worksheet.Elements().Any()) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.ColumnBreaks); + worksheetPart.Worksheet.InsertAfter(new ColumnBreaks(), previousElement); + } + + var columnBreaks = worksheetPart.Worksheet.Elements().First(); + + var existingBreaks = columnBreaks.ChildElements.OfType(); + var columnBreaksToDelete = existingBreaks + .Where(cb => !cb.Id.HasValue || + !xlWorksheet.PageSetup.ColumnBreaks.Contains((int)cb.Id.Value)) + .ToList(); + + foreach (var rb in columnBreaksToDelete) + { + columnBreaks.RemoveChild(rb); + } + + var columnBreaksToAdd = xlWorksheet.PageSetup.ColumnBreaks + .Where(xlCb => !existingBreaks.Any(cb => cb.Id.HasValue && cb.Id.Value == xlCb)); + + columnBreaks.Count = (UInt32)columnBreakCount; + columnBreaks.ManualBreakCount = (UInt32)columnBreakCount; + var maxColumnNumber = (UInt32)xlWorksheet.RangeAddress.LastAddress.ColumnNumber; + foreach (var break1 in columnBreaksToAdd.Select(cb => new Break + { + Id = (UInt32)cb, + Max = maxColumnNumber, + ManualPageBreak = true + })) + columnBreaks.AppendChild(break1); + cm.SetElement(XLWorksheetContents.ColumnBreaks, columnBreaks); + } + else + { + worksheetPart.Worksheet.RemoveAllChildren(); + cm.SetElement(XLWorksheetContents.ColumnBreaks, null); + } + + #endregion ColumnBreaks + + #region Tables + + GenerateTables(xlWorksheet, worksheetPart, context, cm); + + #endregion Tables + + #region Drawings + + if (worksheetPart.DrawingsPart != null) + { + var xlPictures = xlWorksheet.Pictures as Drawings.XLPictures; + foreach (var removedPicture in xlPictures.Deleted) + { + worksheetPart.DrawingsPart.DeletePart(removedPicture); + } + xlPictures.Deleted.Clear(); + } + + foreach (var pic in xlWorksheet.Pictures) + { + AddPictureAnchor(worksheetPart, pic, context); + } + + if (xlWorksheet.Pictures.Any()) + RebaseNonVisualDrawingPropertiesIds(worksheetPart); + + var tableParts = worksheetPart.Worksheet.Elements().First(); + if (xlWorksheet.Pictures.Any() && !worksheetPart.Worksheet.OfType().Any()) + { + var worksheetDrawing = new Drawing { Id = worksheetPart.GetIdOfPart(worksheetPart.DrawingsPart) }; + worksheetDrawing.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + worksheetPart.Worksheet.InsertBefore(worksheetDrawing, tableParts); + cm.SetElement(XLWorksheetContents.Drawing, worksheetPart.Worksheet.Elements().First()); + } + + // Instead of saving a file with an empty Drawings.xml file, rather remove the .xml file + if (!xlWorksheet.Pictures.Any() && worksheetPart.DrawingsPart != null + && !worksheetPart.DrawingsPart.Parts.Any()) + { + var id = worksheetPart.GetIdOfPart(worksheetPart.DrawingsPart); + worksheetPart.Worksheet.RemoveChild(worksheetPart.Worksheet.OfType().FirstOrDefault(p => p.Id == id)); + worksheetPart.DeletePart(worksheetPart.DrawingsPart); + } + + #endregion Drawings + + #region LegacyDrawing + + if (xlWorksheet.LegacyDrawingIsNew) + { + worksheetPart.Worksheet.RemoveAllChildren(); + + if (!String.IsNullOrWhiteSpace(xlWorksheet.LegacyDrawingId)) + { + var previousElement = cm.GetPreviousElementFor(XLWorksheetContents.LegacyDrawing); + worksheetPart.Worksheet.InsertAfter(new LegacyDrawing { Id = xlWorksheet.LegacyDrawingId }, + previousElement); + + cm.SetElement(XLWorksheetContents.LegacyDrawing, worksheetPart.Worksheet.Elements().First()); + } + } + + #endregion LegacyDrawing + + #region LegacyDrawingHeaderFooter + + //LegacyDrawingHeaderFooter legacyHeaderFooter = worksheetPart.Worksheet.Elements().FirstOrDefault(); + //if (legacyHeaderFooter != null) + //{ + // worksheetPart.Worksheet.RemoveAllChildren(); + // { + // var previousElement = cm.GetPreviousElementFor(XLWSContentManager.XLWSContents.LegacyDrawingHeaderFooter); + // worksheetPart.Worksheet.InsertAfter(new LegacyDrawingHeaderFooter { Id = xlWorksheet.LegacyDrawingId }, + // previousElement); + // } + //} + + #endregion LegacyDrawingHeaderFooter + } + + private static void SetCellValue(XLCell xlCell, XLTableField field, Cell openXmlCell, SaveContext context) + { + if (field != null) + { + if (!String.IsNullOrWhiteSpace(field.TotalsRowLabel)) + { + var cellValue = new CellValue(); + cellValue.Text = xlCell.SharedStringId.ToInvariantString(); + openXmlCell.DataType = CvSharedString; + openXmlCell.CellValue = cellValue; + } + else if (field.TotalsRowFunction == XLTotalsRowFunction.None) + { + openXmlCell.DataType = CvSharedString; + openXmlCell.CellValue = null; + } + return; + } + + if (xlCell.HasFormula) + { + openXmlCell.InlineString = null; + var cellValue = new CellValue(); + try + { + var v = xlCell.Value; + cellValue.Text = v.ObjectToInvariantString(); + switch (v) + { + case String s: + openXmlCell.DataType = new EnumValue(CellValues.String); + break; + + case DateTime dt: + openXmlCell.DataType = new EnumValue(CellValues.Date); + break; + + case Boolean b: + openXmlCell.DataType = new EnumValue(CellValues.Boolean); + break; + + default: + if (v.IsNumber()) + openXmlCell.DataType = new EnumValue(CellValues.Number); + else + openXmlCell.DataType = null; + break; + } + } + catch + { + cellValue = null; + } + + openXmlCell.CellValue = cellValue; + return; + } + else + openXmlCell.CellValue = null; + + var dataType = xlCell.DataType; + + if (dataType != XLDataType.Text) + openXmlCell.InlineString = null; + + if (dataType == XLDataType.Text) + { + if (!xlCell.StyleValue.IncludeQuotePrefix && xlCell.InnerText.Length == 0) + openXmlCell.CellValue = null; + else + { + if (xlCell.ShareString) + { + var cellValue = new CellValue(); + cellValue.Text = xlCell.SharedStringId.ToInvariantString(); + openXmlCell.CellValue = cellValue; + + openXmlCell.InlineString = null; + } + else + { + var inlineString = new InlineString(); + if (xlCell.HasRichText) + { + PopulatedRichTextElements(inlineString, xlCell, context); + } + else + { + var text = xlCell.GetString(); + var t = new Text(text); + if (text.PreserveSpaces()) + t.Space = SpaceProcessingModeValues.Preserve; + + inlineString.Text = t; + } + + openXmlCell.InlineString = inlineString; + } + } + } + else if (dataType == XLDataType.TimeSpan) + { + var timeSpan = xlCell.GetTimeSpan(); + var cellValue = new CellValue(); + cellValue.Text = timeSpan.TotalDays.ToInvariantString(); + openXmlCell.CellValue = cellValue; + } + else if (dataType == XLDataType.DateTime || dataType == XLDataType.Number) + { + if (!String.IsNullOrWhiteSpace(xlCell.InnerText)) + { + var cellValue = new CellValue(); + var d = Double.Parse(xlCell.InnerText, XLHelper.NumberStyle, XLHelper.ParseCulture); + + if (xlCell.Worksheet.Workbook.Use1904DateSystem && xlCell.DataType == XLDataType.DateTime) + { + // Internally ClosedXML stores cells as standard 1900-based style + // so if a workbook is in 1904-format, we do that adjustment here and when loading. + d -= 1462; + } + + cellValue.Text = d.ToInvariantString(); + openXmlCell.CellValue = cellValue; + } + } + else + { + var cellValue = new CellValue(); + cellValue.Text = xlCell.InnerText; + openXmlCell.CellValue = cellValue; + } + } + + private static void PopulateAutoFilter(XLAutoFilter xlAutoFilter, AutoFilter autoFilter) + { + var filterRange = xlAutoFilter.Range; + autoFilter.Reference = filterRange.RangeAddress.ToString(); + + foreach (var kp in xlAutoFilter.Filters) + { + var filterColumn = new FilterColumn { ColumnId = (UInt32)kp.Key - 1 }; + var xlFilterColumn = xlAutoFilter.Column(kp.Key); + + switch (xlFilterColumn.FilterType) + { + case XLFilterType.Custom: + var customFilters = new CustomFilters(); + foreach (var filter in kp.Value) + { + var customFilter = new CustomFilter { Val = filter.Value.ObjectToInvariantString() }; + + if (filter.Operator != XLFilterOperator.Equal) + customFilter.Operator = filter.Operator.ToOpenXml(); + + if (filter.Connector == XLConnector.And) + customFilters.And = true; + + customFilters.Append(customFilter); + } + filterColumn.Append(customFilters); + break; + + case XLFilterType.TopBottom: + + var top101 = new Top10 { Val = (double)xlFilterColumn.TopBottomValue }; + if (xlFilterColumn.TopBottomType == XLTopBottomType.Percent) + top101.Percent = true; + if (xlFilterColumn.TopBottomPart == XLTopBottomPart.Bottom) + top101.Top = false; + + filterColumn.Append(top101); + break; + + case XLFilterType.Dynamic: + + var dynamicFilter = new DynamicFilter + { Type = xlFilterColumn.DynamicType.ToOpenXml(), Val = xlFilterColumn.DynamicValue }; + filterColumn.Append(dynamicFilter); + break; + + case XLFilterType.DateTimeGrouping: + var dateTimeGroupFilters = new Filters(); + foreach (var filter in kp.Value) + { + if (filter.Value is DateTime) + { + var d = (DateTime)filter.Value; + var dgi = new DateGroupItem + { + Year = (UInt16)d.Year, + DateTimeGrouping = filter.DateTimeGrouping.ToOpenXml() + }; + + if (filter.DateTimeGrouping >= XLDateTimeGrouping.Month) dgi.Month = (UInt16)d.Month; + if (filter.DateTimeGrouping >= XLDateTimeGrouping.Day) dgi.Day = (UInt16)d.Day; + if (filter.DateTimeGrouping >= XLDateTimeGrouping.Hour) dgi.Hour = (UInt16)d.Hour; + if (filter.DateTimeGrouping >= XLDateTimeGrouping.Minute) dgi.Minute = (UInt16)d.Minute; + if (filter.DateTimeGrouping >= XLDateTimeGrouping.Second) dgi.Second = (UInt16)d.Second; + + dateTimeGroupFilters.Append(dgi); + } + } + filterColumn.Append(dateTimeGroupFilters); + break; + + default: + var filters = new Filters(); + foreach (var filter in kp.Value) + { + filters.Append(new Filter { Val = filter.Value.ObjectToInvariantString() }); + } + + filterColumn.Append(filters); + break; + } + autoFilter.Append(filterColumn); + } + + if (xlAutoFilter.Sorted) + { + string reference = null; + + if (filterRange.FirstCell().Address.RowNumber < filterRange.LastCell().Address.RowNumber) + reference = filterRange.Range(filterRange.FirstCell().CellBelow(), filterRange.LastCell()).RangeAddress.ToString(); + else + reference = filterRange.RangeAddress.ToString(); + + var sortState = new SortState + { + Reference = reference + }; + + var sortCondition = new SortCondition + { + Reference = + filterRange.Range(1, xlAutoFilter.SortColumn, filterRange.RowCount(), + xlAutoFilter.SortColumn).RangeAddress.ToString() + }; + if (xlAutoFilter.SortOrder == XLSortOrder.Descending) + sortCondition.Descending = true; + + sortState.Append(sortCondition); + autoFilter.Append(sortState); + } + } + + private static void CollapseColumns(Columns columns, Dictionary sheetColumns) + { + UInt32 lastMin = 1; + var count = sheetColumns.Count; + var arr = sheetColumns.OrderBy(kp => kp.Key).ToArray(); + // sheetColumns[kp.Key + 1] + //Int32 i = 0; + //foreach (KeyValuePair kp in arr + // //.Where(kp => !(kp.Key < count && ColumnsAreEqual(kp.Value, ))) + // ) + for (var i = 0; i < count; i++) + { + var kp = arr[i]; + if (i + 1 != count && ColumnsAreEqual(kp.Value, arr[i + 1].Value)) continue; + + var newColumn = (Column)kp.Value.CloneNode(true); + newColumn.Min = lastMin; + var newColumnMax = newColumn.Max.Value; + var columnsToRemove = + columns.Elements().Where(co => co.Min >= lastMin && co.Max <= newColumnMax). + Select(co => co).ToList(); + columnsToRemove.ForEach(c => columns.RemoveChild(c)); + + columns.AppendChild(newColumn); + lastMin = kp.Key + 1; + //i++; + } + } + + private static double GetColumnWidth(double columnWidth) + { + return Math.Min(255.0, Math.Max(0.0, columnWidth + ColumnWidthOffset)); + } + + private static void UpdateColumn(Column column, Columns columns, Dictionary sheetColumnsByMin) + { + if (!sheetColumnsByMin.TryGetValue(column.Min.Value, out Column newColumn)) + { + newColumn = (Column)column.CloneNode(true); + columns.AppendChild(newColumn); + sheetColumnsByMin.Add(column.Min.Value, newColumn); + } + else + { + var existingColumn = sheetColumnsByMin[column.Min.Value]; + newColumn = (Column)existingColumn.CloneNode(true); + newColumn.Min = column.Min; + newColumn.Max = column.Max; + newColumn.Style = column.Style; + newColumn.Width = column.Width.SaveRound(); + newColumn.CustomWidth = column.CustomWidth; + + if (column.Hidden != null) + newColumn.Hidden = true; + else + newColumn.Hidden = null; + + if (column.Collapsed != null) + newColumn.Collapsed = true; + else + newColumn.Collapsed = null; + + if (column.OutlineLevel != null && column.OutlineLevel > 0) + newColumn.OutlineLevel = (byte)column.OutlineLevel; + else + newColumn.OutlineLevel = null; + + sheetColumnsByMin.Remove(column.Min.Value); + if (existingColumn.Min + 1 > existingColumn.Max) + { + //existingColumn.Min = existingColumn.Min + 1; + //columns.InsertBefore(existingColumn, newColumn); + //existingColumn.Remove(); + columns.RemoveChild(existingColumn); + columns.AppendChild(newColumn); + sheetColumnsByMin.Add(newColumn.Min.Value, newColumn); + } + else + { + //columns.InsertBefore(existingColumn, newColumn); + columns.AppendChild(newColumn); + sheetColumnsByMin.Add(newColumn.Min.Value, newColumn); + existingColumn.Min = existingColumn.Min + 1; + sheetColumnsByMin.Add(existingColumn.Min.Value, existingColumn); + } + } + } + + private static bool ColumnsAreEqual(Column left, Column right) + { + return + ((left.Style == null && right.Style == null) + || (left.Style != null && right.Style != null && left.Style.Value == right.Style.Value)) + && ((left.Width == null && right.Width == null) + || (left.Width != null && right.Width != null && (Math.Abs(left.Width.Value - right.Width.Value) < XLHelper.Epsilon))) + && ((left.Hidden == null && right.Hidden == null) + || (left.Hidden != null && right.Hidden != null && left.Hidden.Value == right.Hidden.Value)) + && ((left.Collapsed == null && right.Collapsed == null) + || + (left.Collapsed != null && right.Collapsed != null && left.Collapsed.Value == right.Collapsed.Value)) + && ((left.OutlineLevel == null && right.OutlineLevel == null) + || + (left.OutlineLevel != null && right.OutlineLevel != null && + left.OutlineLevel.Value == right.OutlineLevel.Value)); + } + + #endregion GenerateWorksheetPartContent + } +} diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs new file mode 100644 index 0000000..5a27ad2 --- /dev/null +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -0,0 +1,1986 @@ +using ClosedXML.Excel.Caching; +using ClosedXML.Excel.CalcEngine; +using ClosedXML.Excel.Drawings; +using ClosedXML.Excel.Ranges.Index; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Excel +{ + internal class XLWorksheet : XLRangeBase, IXLWorksheet + { + #region Fields + + private readonly Dictionary _columnOutlineCount = new Dictionary(); + private readonly Dictionary _rowOutlineCount = new Dictionary(); + private readonly XLRangeFactory _rangeFactory; + private readonly XLRangeRepository _rangeRepository; + private readonly List _rangeIndices; + internal Int32 ZOrder = 1; + private String _name; + internal Int32 _position; + + private Double _rowHeight; + private Boolean _tabActive; + private XLSheetProtection _protection; + internal Boolean EventTrackingEnabled; + + /// + /// Fake address to be used everywhere the invalid address is needed. + /// + internal readonly XLAddress InvalidAddress; + + #endregion Fields + + #region Constructor + + public XLWorksheet(String sheetName, XLWorkbook workbook) + : base( + new XLRangeAddress( + new XLAddress(null, XLHelper.MinRowNumber, XLHelper.MinColumnNumber, false, false), + new XLAddress(null, XLHelper.MaxRowNumber, XLHelper.MaxColumnNumber, false, false)), + (workbook.Style as XLStyle).Value) + { + EventTrackingEnabled = workbook.EventTracking == XLEventTracking.Enabled; + + Workbook = workbook; + InvalidAddress = new XLAddress(this, 0, 0, false, false); + + var firstAddress = new XLAddress(this, RangeAddress.FirstAddress.RowNumber, RangeAddress.FirstAddress.ColumnNumber, + RangeAddress.FirstAddress.FixedRow, RangeAddress.FirstAddress.FixedColumn); + var lastAddress = new XLAddress(this, RangeAddress.LastAddress.RowNumber, RangeAddress.LastAddress.ColumnNumber, + RangeAddress.LastAddress.FixedRow, RangeAddress.LastAddress.FixedColumn); + RangeAddress = new XLRangeAddress(firstAddress, lastAddress); + _rangeFactory = new XLRangeFactory(this); + _rangeRepository = new XLRangeRepository(workbook, _rangeFactory.Create); + _rangeIndices = new List(); + + Pictures = new XLPictures(this); + NamedRanges = new XLNamedRanges(this); + SheetView = new XLSheetView(this); + Tables = new XLTables(); + Hyperlinks = new XLHyperlinks(); + DataValidations = new XLDataValidations(this); + PivotTables = new XLPivotTables(this); + Protection = new XLSheetProtection(DefaultProtectionAlgorithm); + AutoFilter = new XLAutoFilter(); + ConditionalFormats = new XLConditionalFormats(); + SparklineGroups = new XLSparklineGroups(this); + Internals = new XLWorksheetInternals(new XLCellsCollection(), new XLColumnsCollection(), + new XLRowsCollection(), new XLRanges()); + PageSetup = new XLPageSetup((XLPageSetup)workbook.PageOptions, this); + Outline = new XLOutline(workbook.Outline); + _columnWidth = workbook.ColumnWidth; + _rowHeight = workbook.RowHeight; + RowHeightChanged = Math.Abs(workbook.RowHeight - XLWorkbook.DefaultRowHeight) > XLHelper.Epsilon; + Name = sheetName; + Charts = new XLCharts(); + ShowFormulas = workbook.ShowFormulas; + ShowGridLines = workbook.ShowGridLines; + ShowOutlineSymbols = workbook.ShowOutlineSymbols; + ShowRowColHeaders = workbook.ShowRowColHeaders; + ShowRuler = workbook.ShowRuler; + ShowWhiteSpace = workbook.ShowWhiteSpace; + ShowZeros = workbook.ShowZeros; + RightToLeft = workbook.RightToLeft; + TabColor = XLColor.NoColor; + SelectedRanges = new XLRanges(); + + Author = workbook.Author; + } + + #endregion Constructor + + public override XLRangeType RangeType + { + get { return XLRangeType.Worksheet; } + } + + //private IXLStyle _style; + private const String InvalidNameChars = @":\/?*[]"; + + public string LegacyDrawingId; + public Boolean LegacyDrawingIsNew; + private Double _columnWidth; + + public XLWorksheetInternals Internals { get; private set; } + + public XLRangeFactory RangeFactory + { + get { return _rangeFactory; } + } + + public override IEnumerable Styles + { + get + { + yield return GetStyle(); + foreach (XLCell c in Internals.CellsCollection.GetCells()) + yield return c.Style; + } + } + + protected override IEnumerable Children + { + get + { + var columnsUsed = Internals.ColumnsCollection.Keys + .Union(Internals.CellsCollection.ColumnsUsed.Keys) + .Distinct() + .OrderBy(c => c) + .ToList(); + foreach (var col in columnsUsed) + yield return Column(col); + + var rowsUsed = Internals.RowsCollection.Keys + .Union(Internals.CellsCollection.RowsUsed.Keys) + .Distinct() + .OrderBy(r => r) + .ToList(); + foreach (var row in rowsUsed) + yield return Row(row); + } + } + + internal Boolean RowHeightChanged { get; set; } + + internal Boolean ColumnWidthChanged { get; set; } + + public Int32 SheetId { get; set; } + + internal String RelId { get; set; } + + public XLDataValidations DataValidations { get; private set; } + + public IXLCharts Charts { get; private set; } + + public XLSheetProtection Protection + { + get => _protection; + set + { + _protection = value.Clone().CastTo(); + } + } + + public XLAutoFilter AutoFilter { get; private set; } + + public bool IsDeleted { get; private set; } + + #region IXLWorksheet Members + + public XLWorkbook Workbook { get; private set; } + + public Double ColumnWidth + { + get { return _columnWidth; } + set + { + ColumnWidthChanged = true; + _columnWidth = value; + } + } + + public Double RowHeight + { + get { return _rowHeight; } + set + { + RowHeightChanged = true; + _rowHeight = value; + } + } + + public String Name + { + get { return _name; } + set + { + if (_name == value) return; + + if (String.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Worksheet names cannot be empty"); + + if (value.IndexOfAny(InvalidNameChars.ToCharArray()) != -1) + throw new ArgumentException("Worksheet names cannot contain any of the following characters: " + + InvalidNameChars); + + if (value.Length > 31) + throw new ArgumentException("Worksheet names cannot be more than 31 characters"); + + if (value.StartsWith("'", StringComparison.Ordinal)) + throw new ArgumentException("Worksheet names cannot start with an apostrophe"); + + if (value.EndsWith("'", StringComparison.Ordinal)) + throw new ArgumentException("Worksheet names cannot end with an apostrophe"); + + Workbook.WorksheetsInternal.Rename(_name, value); + _name = value; + } + } + + public Int32 Position + { + get { return _position; } + set + { + if (value > Workbook.WorksheetsInternal.Count + Workbook.UnsupportedSheets.Count + 1) + throw new ArgumentOutOfRangeException(nameof(value), "Index must be equal or less than the number of worksheets + 1."); + + if (value < _position) + { + Workbook.WorksheetsInternal + .Where(w => w.Position >= value && w.Position < _position) + .ForEach(w => w._position += 1); + } + + if (value > _position) + { + Workbook.WorksheetsInternal + .Where(w => w.Position <= value && w.Position > _position) + .ForEach(w => (w)._position -= 1); + } + + _position = value; + } + } + + public IXLPageSetup PageSetup { get; private set; } + + public IXLOutline Outline { get; private set; } + + IXLRow IXLWorksheet.FirstRowUsed() + { + return FirstRowUsed(); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRow IXLWorksheet.FirstRowUsed(Boolean includeFormats) + { + return FirstRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + IXLRow IXLWorksheet.FirstRowUsed(XLCellsUsedOptions options) + { + return FirstRowUsed(options); + } + + IXLRow IXLWorksheet.LastRowUsed() + { + return LastRowUsed(); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLRow IXLWorksheet.LastRowUsed(Boolean includeFormats) + { + return LastRowUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + IXLRow IXLWorksheet.LastRowUsed(XLCellsUsedOptions options) + { + return LastRowUsed(options); + } + + IXLColumn IXLWorksheet.LastColumn() + { + return LastColumn(); + } + + IXLColumn IXLWorksheet.FirstColumn() + { + return FirstColumn(); + } + + IXLRow IXLWorksheet.FirstRow() + { + return FirstRow(); + } + + IXLRow IXLWorksheet.LastRow() + { + return LastRow(); + } + + IXLColumn IXLWorksheet.FirstColumnUsed() + { + return FirstColumnUsed(); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLColumn IXLWorksheet.FirstColumnUsed(Boolean includeFormats) + { + return FirstColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + IXLColumn IXLWorksheet.FirstColumnUsed(XLCellsUsedOptions options) + { + return FirstColumnUsed(options); + } + + IXLColumn IXLWorksheet.LastColumnUsed() + { + return LastColumnUsed(); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + IXLColumn IXLWorksheet.LastColumnUsed(Boolean includeFormats) + { + return LastColumnUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents); + } + + IXLColumn IXLWorksheet.LastColumnUsed(XLCellsUsedOptions options) + { + return LastColumnUsed(options); + } + + public IXLColumns Columns() + { + var retVal = new XLColumns(this, StyleValue); + var columnList = new List(); + + if (Internals.CellsCollection.Count > 0) + columnList.AddRange(Internals.CellsCollection.ColumnsUsed.Keys); + + if (Internals.ColumnsCollection.Count > 0) + columnList.AddRange(Internals.ColumnsCollection.Keys.Where(c => !columnList.Contains(c))); + + foreach (int c in columnList) + retVal.Add(Column(c)); + + return retVal; + } + + public IXLColumns Columns(String columns) + { + var retVal = new XLColumns(null, StyleValue); + var columnPairs = columns.Split(','); + foreach (string tPair in columnPairs.Select(pair => pair.Trim())) + { + String firstColumn; + String lastColumn; + if (tPair.Contains(':') || tPair.Contains('-')) + { + var columnRange = XLHelper.SplitRange(tPair); + firstColumn = columnRange[0]; + lastColumn = columnRange[1]; + } + else + { + firstColumn = tPair; + lastColumn = tPair; + } + + if (Int32.TryParse(firstColumn, out int tmp)) + { + foreach (IXLColumn col in Columns(Int32.Parse(firstColumn), Int32.Parse(lastColumn))) + retVal.Add((XLColumn)col); + } + else + { + foreach (IXLColumn col in Columns(firstColumn, lastColumn)) + retVal.Add((XLColumn)col); + } + } + return retVal; + } + + public IXLColumns Columns(String firstColumn, String lastColumn) + { + return Columns(XLHelper.GetColumnNumberFromLetter(firstColumn), + XLHelper.GetColumnNumberFromLetter(lastColumn)); + } + + public IXLColumns Columns(Int32 firstColumn, Int32 lastColumn) + { + var retVal = new XLColumns(null, StyleValue); + + for (int co = firstColumn; co <= lastColumn; co++) + retVal.Add(Column(co)); + return retVal; + } + + public IXLRows Rows() + { + var retVal = new XLRows(this, StyleValue); + var rowList = new List(); + + if (Internals.CellsCollection.Count > 0) + rowList.AddRange(Internals.CellsCollection.RowsUsed.Keys); + + if (Internals.RowsCollection.Count > 0) + rowList.AddRange(Internals.RowsCollection.Keys.Where(r => !rowList.Contains(r))); + + foreach (int r in rowList) + retVal.Add(Row(r)); + + return retVal; + } + + public IXLRows Rows(String rows) + { + var retVal = new XLRows(null, StyleValue); + var rowPairs = rows.Split(','); + foreach (string tPair in rowPairs.Select(pair => pair.Trim())) + { + String firstRow; + String lastRow; + if (tPair.Contains(':') || tPair.Contains('-')) + { + var rowRange = XLHelper.SplitRange(tPair); + firstRow = rowRange[0]; + lastRow = rowRange[1]; + } + else + { + firstRow = tPair; + lastRow = tPair; + } + + Rows(Int32.Parse(firstRow), Int32.Parse(lastRow)) + .ForEach(row => retVal.Add((XLRow)row)); + } + return retVal; + } + + public IXLRows Rows(Int32 firstRow, Int32 lastRow) + { + var retVal = new XLRows(null, StyleValue); + + for (int ro = firstRow; ro <= lastRow; ro++) + retVal.Add(Row(ro)); + return retVal; + } + + IXLRow IXLWorksheet.Row(Int32 row) + { + return Row(row); + } + + IXLColumn IXLWorksheet.Column(Int32 column) + { + return Column(column); + } + + IXLColumn IXLWorksheet.Column(String column) + { + return Column(column); + } + + IXLCell IXLWorksheet.Cell(int row, int column) + { + return Cell(row, column); + } + + IXLCell IXLWorksheet.Cell(string cellAddressInRange) + { + return Cell(cellAddressInRange); + } + + IXLCell IXLWorksheet.Cell(int row, string column) + { + return Cell(row, column); + } + + IXLCell IXLWorksheet.Cell(IXLAddress cellAddressInRange) + { + return Cell(cellAddressInRange); + } + + IXLRange IXLWorksheet.Range(IXLRangeAddress rangeAddress) + { + return Range(rangeAddress); + } + + IXLRange IXLWorksheet.Range(string rangeAddress) + { + return Range(rangeAddress); + } + + IXLRange IXLWorksheet.Range(IXLCell firstCell, IXLCell lastCell) + { + return Range(firstCell, lastCell); + } + + IXLRange IXLWorksheet.Range(string firstCellAddress, string lastCellAddress) + { + return Range(firstCellAddress, lastCellAddress); + } + + IXLRange IXLWorksheet.Range(IXLAddress firstCellAddress, IXLAddress lastCellAddress) + { + return Range(firstCellAddress, lastCellAddress); + } + + IXLRange IXLWorksheet.Range(int firstCellRow, int firstCellColumn, int lastCellRow, int lastCellColumn) + { + return Range(firstCellRow, firstCellColumn, lastCellRow, lastCellColumn); + } + + public IXLWorksheet CollapseRows() + { + Enumerable.Range(1, 8).ForEach(i => CollapseRows(i)); + return this; + } + + public IXLWorksheet CollapseColumns() + { + Enumerable.Range(1, 8).ForEach(i => CollapseColumns(i)); + return this; + } + + public IXLWorksheet ExpandRows() + { + Enumerable.Range(1, 8).ForEach(i => ExpandRows(i)); + return this; + } + + public IXLWorksheet ExpandColumns() + { + Enumerable.Range(1, 8).ForEach(i => ExpandColumns(i)); + return this; + } + + public IXLWorksheet CollapseRows(Int32 outlineLevel) + { + if (outlineLevel < 1 || outlineLevel > 8) + throw new ArgumentOutOfRangeException("outlineLevel", "Outline level must be between 1 and 8."); + + Internals.RowsCollection.Values.Where(r => r.OutlineLevel == outlineLevel).ForEach(r => r.Collapse()); + return this; + } + + public IXLWorksheet CollapseColumns(Int32 outlineLevel) + { + if (outlineLevel < 1 || outlineLevel > 8) + throw new ArgumentOutOfRangeException("outlineLevel", "Outline level must be between 1 and 8."); + + Internals.ColumnsCollection.Values.Where(c => c.OutlineLevel == outlineLevel).ForEach(c => c.Collapse()); + return this; + } + + public IXLWorksheet ExpandRows(Int32 outlineLevel) + { + if (outlineLevel < 1 || outlineLevel > 8) + throw new ArgumentOutOfRangeException("outlineLevel", "Outline level must be between 1 and 8."); + + Internals.RowsCollection.Values.Where(r => r.OutlineLevel == outlineLevel).ForEach(r => r.Expand()); + return this; + } + + public IXLWorksheet ExpandColumns(Int32 outlineLevel) + { + if (outlineLevel < 1 || outlineLevel > 8) + throw new ArgumentOutOfRangeException("outlineLevel", "Outline level must be between 1 and 8."); + + Internals.ColumnsCollection.Values.Where(c => c.OutlineLevel == outlineLevel).ForEach(c => c.Expand()); + return this; + } + + public void Delete() + { + IsDeleted = true; + (Workbook.NamedRanges as XLNamedRanges).OnWorksheetDeleted(Name); + Workbook.WorksheetsInternal.Delete(Name); + } + + public IXLNamedRanges NamedRanges { get; private set; } + + public IXLNamedRange NamedRange(String rangeName) + { + return NamedRanges.NamedRange(rangeName); + } + + IXLSheetView IXLWorksheet.SheetView { get => SheetView; } + + public XLSheetView SheetView { get; private set; } + + public IXLTables Tables { get; private set; } + + public IXLTable Table(Int32 index) + { + return Tables.Table(index); + } + + public IXLTable Table(String name) + { + return Tables.Table(name); + } + + public IXLWorksheet CopyTo(String newSheetName) + { + return CopyTo(Workbook, newSheetName, Workbook.WorksheetsInternal.Count + 1); + } + + public IXLWorksheet CopyTo(String newSheetName, Int32 position) + { + return CopyTo(Workbook, newSheetName, position); + } + + public IXLWorksheet CopyTo(XLWorkbook workbook, String newSheetName) + { + return CopyTo(workbook, newSheetName, workbook.WorksheetsInternal.Count + 1); + } + + public IXLWorksheet CopyTo(XLWorkbook workbook, String newSheetName, Int32 position) + { + if (this.IsDeleted) + throw new InvalidOperationException($"`{this.Name}` has been deleted and cannot be copied."); + + var targetSheet = (XLWorksheet)workbook.WorksheetsInternal.Add(newSheetName, position); + Internals.ColumnsCollection.ForEach(kp => kp.Value.CopyTo(targetSheet.Column(kp.Key))); + Internals.RowsCollection.ForEach(kp => kp.Value.CopyTo(targetSheet.Row(kp.Key))); + Internals.CellsCollection.GetCells().ForEach(c => targetSheet.Cell(c.Address).CopyFrom(c, XLCellCopyOptions.Values | XLCellCopyOptions.Styles)); + DataValidations.ForEach(dv => targetSheet.DataValidations.Add(new XLDataValidation(dv, this))); + targetSheet.Visibility = Visibility; + targetSheet.ColumnWidth = ColumnWidth; + targetSheet.ColumnWidthChanged = ColumnWidthChanged; + targetSheet.RowHeight = RowHeight; + targetSheet.RowHeightChanged = RowHeightChanged; + targetSheet.InnerStyle = InnerStyle; + targetSheet.PageSetup = new XLPageSetup((XLPageSetup)PageSetup, targetSheet); + (targetSheet.PageSetup.Header as XLHeaderFooter).Changed = true; + (targetSheet.PageSetup.Footer as XLHeaderFooter).Changed = true; + targetSheet.Outline = new XLOutline(Outline); + targetSheet.SheetView = new XLSheetView(targetSheet, SheetView); + targetSheet.SelectedRanges.RemoveAll(); + + Pictures.ForEach(picture => picture.CopyTo(targetSheet)); + NamedRanges.ForEach(nr => nr.CopyTo(targetSheet)); + Tables.Cast().ForEach(t => t.CopyTo(targetSheet, false)); + ConditionalFormats.ForEach(cf => cf.CopyTo(targetSheet)); + SparklineGroups.CopyTo(targetSheet); + MergedRanges.ForEach(mr => targetSheet.Range(((XLRangeAddress)mr.RangeAddress).WithoutWorksheet()).Merge()); + SelectedRanges.ForEach(sr => targetSheet.SelectedRanges.Add(targetSheet.Range(((XLRangeAddress)sr.RangeAddress).WithoutWorksheet()))); + + if (AutoFilter.IsEnabled) + { + var range = targetSheet.Range(((XLRangeAddress)AutoFilter.Range.RangeAddress).WithoutWorksheet()); + range.SetAutoFilter(); + } + + return targetSheet; + } + + public new IXLHyperlinks Hyperlinks { get; private set; } + + IXLDataValidations IXLWorksheet.DataValidations + { + get { return DataValidations; } + } + + private XLWorksheetVisibility _visibility; + + public XLWorksheetVisibility Visibility + { + get { return _visibility; } + set + { + if (value != XLWorksheetVisibility.Visible) + TabSelected = false; + + _visibility = value; + } + } + + public IXLWorksheet Hide() + { + Visibility = XLWorksheetVisibility.Hidden; + return this; + } + + public IXLWorksheet Unhide() + { + Visibility = XLWorksheetVisibility.Visible; + return this; + } + + IXLSheetProtection IXLProtectable.Protection + { + get => Protection; + set => Protection = value as XLSheetProtection; + } + + public IXLSheetProtection Protect() + { + return Protection.Protect(); + } + + public IXLSheetProtection Protect(String password, Algorithm algorithm = DefaultProtectionAlgorithm) + { + return Protection.Protect(password, algorithm, XLSheetProtectionElements.SelectEverything); + } + + public IXLSheetProtection Protect(String password, Algorithm algorithm, XLSheetProtectionElements allowedElements) + { + return Protection.Protect(password, algorithm, allowedElements); + } + + IXLElementProtection IXLProtectable.Protect() + { + return Protect(); + } + + IXLElementProtection IXLProtectable.Protect(String password, Algorithm algorithm) + { + return Protect(password, algorithm); + } + + public IXLSheetProtection Unprotect() + { + return Protection.Unprotect(); + } + + public IXLSheetProtection Unprotect(String password) + { + return Protection.Unprotect(password); + } + + IXLElementProtection IXLProtectable.Unprotect() + { + return Unprotect(); + } + + IXLElementProtection IXLProtectable.Unprotect(String password) + { + return Unprotect(password); + } + + public new IXLRange Sort() + { + return GetRangeForSort().Sort(); + } + + public new IXLRange Sort(String columnsToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, + Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return GetRangeForSort().Sort(columnsToSortBy, sortOrder, matchCase, ignoreBlanks); + } + + public new IXLRange Sort(Int32 columnToSortBy, XLSortOrder sortOrder = XLSortOrder.Ascending, + Boolean matchCase = false, Boolean ignoreBlanks = true) + { + return GetRangeForSort().Sort(columnToSortBy, sortOrder, matchCase, ignoreBlanks); + } + + public new IXLRange SortLeftToRight(XLSortOrder sortOrder = XLSortOrder.Ascending, Boolean matchCase = false, + Boolean ignoreBlanks = true) + { + return GetRangeForSort().SortLeftToRight(sortOrder, matchCase, ignoreBlanks); + } + + public Boolean ShowFormulas { get; set; } + + public Boolean ShowGridLines { get; set; } + + public Boolean ShowOutlineSymbols { get; set; } + + public Boolean ShowRowColHeaders { get; set; } + + public Boolean ShowRuler { get; set; } + + public Boolean ShowWhiteSpace { get; set; } + + public Boolean ShowZeros { get; set; } + + public IXLWorksheet SetShowFormulas() + { + ShowFormulas = true; + return this; + } + + public IXLWorksheet SetShowFormulas(Boolean value) + { + ShowFormulas = value; + return this; + } + + public IXLWorksheet SetShowGridLines() + { + ShowGridLines = true; + return this; + } + + public IXLWorksheet SetShowGridLines(Boolean value) + { + ShowGridLines = value; + return this; + } + + public IXLWorksheet SetShowOutlineSymbols() + { + ShowOutlineSymbols = true; + return this; + } + + public IXLWorksheet SetShowOutlineSymbols(Boolean value) + { + ShowOutlineSymbols = value; + return this; + } + + public IXLWorksheet SetShowRowColHeaders() + { + ShowRowColHeaders = true; + return this; + } + + public IXLWorksheet SetShowRowColHeaders(Boolean value) + { + ShowRowColHeaders = value; + return this; + } + + public IXLWorksheet SetShowRuler() + { + ShowRuler = true; + return this; + } + + public IXLWorksheet SetShowRuler(Boolean value) + { + ShowRuler = value; + return this; + } + + public IXLWorksheet SetShowWhiteSpace() + { + ShowWhiteSpace = true; + return this; + } + + public IXLWorksheet SetShowWhiteSpace(Boolean value) + { + ShowWhiteSpace = value; + return this; + } + + public IXLWorksheet SetShowZeros() + { + ShowZeros = true; + return this; + } + + public IXLWorksheet SetShowZeros(Boolean value) + { + ShowZeros = value; + return this; + } + + public XLColor TabColor { get; set; } + + public IXLWorksheet SetTabColor(XLColor color) + { + TabColor = color; + return this; + } + + public Boolean TabSelected { get; set; } + + public Boolean TabActive + { + get { return _tabActive; } + set + { + if (value && !_tabActive) + { + foreach (XLWorksheet ws in Worksheet.Workbook.WorksheetsInternal) + ws._tabActive = false; + } + _tabActive = value; + } + } + + public IXLWorksheet SetTabSelected() + { + TabSelected = true; + return this; + } + + public IXLWorksheet SetTabSelected(Boolean value) + { + TabSelected = value; + return this; + } + + public IXLWorksheet SetTabActive() + { + TabActive = true; + return this; + } + + public IXLWorksheet SetTabActive(Boolean value) + { + TabActive = value; + return this; + } + + IXLPivotTable IXLWorksheet.PivotTable(String name) + { + return PivotTable(name); + } + + public IXLPivotTables PivotTables { get; private set; } + + public Boolean RightToLeft { get; set; } + + public IXLWorksheet SetRightToLeft() + { + RightToLeft = true; + return this; + } + + public IXLWorksheet SetRightToLeft(Boolean value) + { + RightToLeft = value; + return this; + } + + public override IXLRanges Ranges(String ranges) + { + var retVal = new XLRanges(); + foreach (string rangeAddressStr in ranges.Split(',').Select(s => s.Trim())) + { + if (rangeAddressStr.StartsWith("#REF!")) + { + continue; + } + else if (XLHelper.IsValidRangeAddress(rangeAddressStr)) + { + retVal.Add(Range(new XLRangeAddress(Worksheet, rangeAddressStr))); + } + else if (NamedRanges.TryGetValue(rangeAddressStr, out IXLNamedRange worksheetNamedRange)) + { + worksheetNamedRange.Ranges.ForEach(retVal.Add); + } + else if (Workbook.NamedRanges.TryGetValue(rangeAddressStr, out IXLNamedRange workbookNamedRange) + && workbookNamedRange.Ranges.First().Worksheet == this) + { + workbookNamedRange.Ranges.ForEach(retVal.Add); + } + } + return retVal; + } + + IXLAutoFilter IXLWorksheet.AutoFilter + { + get { return AutoFilter; } + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLRows RowsUsed(Boolean includeFormats, Func predicate = null) + { + return RowsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + public IXLRows RowsUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents, Func predicate = null) + { + var rows = new XLRows(Worksheet, StyleValue); + var rowsUsed = new HashSet(); + Internals.RowsCollection.Keys.ForEach(r => rowsUsed.Add(r)); + Internals.CellsCollection.RowsUsed.Keys.ForEach(r => rowsUsed.Add(r)); + foreach (var rowNum in rowsUsed) + { + var row = Row(rowNum); + if (!row.IsEmpty(options) && (predicate == null || predicate(row))) + rows.Add(row); + } + return rows; + } + + public IXLRows RowsUsed(Func predicate = null) + { + return RowsUsed(XLCellsUsedOptions.AllContents, predicate); + } + + [Obsolete("Use the overload with XLCellsUsedOptions")] + public IXLColumns ColumnsUsed(Boolean includeFormats, Func predicate = null) + { + return ColumnsUsed(includeFormats + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents, + predicate); + } + + public IXLColumns ColumnsUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents, Func predicate = null) + { + var columns = new XLColumns(Worksheet, StyleValue); + var columnsUsed = new HashSet(); + Internals.ColumnsCollection.Keys.ForEach(r => columnsUsed.Add(r)); + Internals.CellsCollection.ColumnsUsed.Keys.ForEach(r => columnsUsed.Add(r)); + foreach (var columnNum in columnsUsed) + { + var column = Column(columnNum); + if (!column.IsEmpty(options) && (predicate == null || predicate(column))) + columns.Add(column); + } + return columns; + } + + public IXLColumns ColumnsUsed(Func predicate = null) + { + return ColumnsUsed(XLCellsUsedOptions.AllContents, predicate); + } + + internal void RegisterRangeIndex(IXLRangeIndex rangeIndex) + { + _rangeIndices.Add(rangeIndex); + } + + internal void Cleanup() + { + Internals.Dispose(); + Pictures.ForEach(p => p.Dispose()); + _rangeRepository.Clear(); + _rangeIndices.Clear(); + } + + #endregion IXLWorksheet Members + + #region Outlines + + public void IncrementColumnOutline(Int32 level) + { + if (level <= 0) return; + if (_columnOutlineCount.TryGetValue(level, out Int32 value)) + _columnOutlineCount[level] = value + 1; + else + _columnOutlineCount.Add(level, 1); + } + + public void DecrementColumnOutline(Int32 level) + { + if (level <= 0) return; + if (_columnOutlineCount.TryGetValue(level, out Int32 value)) + { + if (value > 0) + _columnOutlineCount[level] = value - 1; + } + else + _columnOutlineCount.Add(level, 0); + } + + public Int32 GetMaxColumnOutline() + { + var list = _columnOutlineCount.Where(kp => kp.Value > 0).ToList(); + return list.Count == 0 ? 0 : list.Max(kp => kp.Key); + } + + public void IncrementRowOutline(Int32 level) + { + if (level <= 0) return; + if (_rowOutlineCount.TryGetValue(level, out Int32 value)) + _rowOutlineCount[level] = value + 1; + else + _rowOutlineCount.Add(level, 0); + } + + public void DecrementRowOutline(Int32 level) + { + if (level <= 0) return; + if (_rowOutlineCount.TryGetValue(level, out Int32 value)) + { + if (value > 0) + _rowOutlineCount[level] = level - 1; + } + else + _rowOutlineCount.Add(level, 0); + } + + public Int32 GetMaxRowOutline() + { + return _rowOutlineCount.Count == 0 ? 0 : _rowOutlineCount.Where(kp => kp.Value > 0).Max(kp => kp.Key); + } + + #endregion Outlines + + public XLRow FirstRowUsed() + { + return FirstRowUsed(XLCellsUsedOptions.AllContents); + } + + public XLRow FirstRowUsed(XLCellsUsedOptions options) + { + var rngRow = AsRange().FirstRowUsed(options); + return rngRow != null ? Row(rngRow.RangeAddress.FirstAddress.RowNumber) : null; + } + + public XLRow LastRowUsed() + { + return LastRowUsed(XLCellsUsedOptions.AllContents); + } + + public XLRow LastRowUsed(XLCellsUsedOptions options) + { + var rngRow = AsRange().LastRowUsed(options); + return rngRow != null ? Row(rngRow.RangeAddress.LastAddress.RowNumber) : null; + } + + public XLColumn LastColumn() + { + return Column(XLHelper.MaxColumnNumber); + } + + public XLColumn FirstColumn() + { + return Column(1); + } + + public XLRow FirstRow() + { + return Row(1); + } + + public XLRow LastRow() + { + return Row(XLHelper.MaxRowNumber); + } + + public XLColumn FirstColumnUsed() + { + return FirstColumnUsed(XLCellsUsedOptions.AllContents); + } + + public XLColumn FirstColumnUsed(XLCellsUsedOptions options) + { + var rngColumn = AsRange().FirstColumnUsed(options); + return rngColumn != null ? Column(rngColumn.RangeAddress.FirstAddress.ColumnNumber) : null; + } + + public XLColumn LastColumnUsed() + { + return LastColumnUsed(XLCellsUsedOptions.AllContents); + } + + public XLColumn LastColumnUsed(XLCellsUsedOptions options) + { + var rngColumn = AsRange().LastColumnUsed(options); + return rngColumn != null ? Column(rngColumn.RangeAddress.LastAddress.ColumnNumber) : null; + } + + public XLRow Row(Int32 row) + { + return Row(row, true); + } + + public XLColumn Column(Int32 columnNumber) + { + if (columnNumber <= 0 || columnNumber > XLHelper.MaxColumnNumber) + throw new ArgumentOutOfRangeException(nameof(columnNumber), $"Column number must be between 1 and {XLHelper.MaxColumnNumber}"); + + if (Internals.ColumnsCollection.TryGetValue(columnNumber, out XLColumn column)) + return column; + else + { + // This is a new column so we're going to reference all + // cells in this column to preserve their formatting + Internals.RowsCollection.Keys.ForEach(r => Cell(r, columnNumber)); + + column = RangeFactory.CreateColumn(columnNumber); + Internals.ColumnsCollection.Add(columnNumber, column); + } + + return column; + } + + public IXLColumn Column(String column) + { + return Column(XLHelper.GetColumnNumberFromLetter(column)); + } + + public override XLRange AsRange() + { + return Range(1, 1, XLHelper.MaxRowNumber, XLHelper.MaxColumnNumber); + } + + internal override void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) + { + if (!range.IsEntireColumn()) + { + var model = new XLRangeAddress( + range.RangeAddress.FirstAddress, + new XLAddress(range.RangeAddress.LastAddress.RowNumber, XLHelper.MaxColumnNumber, false, false)); + var rangesToSplit = Worksheet.MergedRanges + .GetIntersectedRanges(model) + .Where(r => r.RangeAddress.FirstAddress.RowNumber < range.RangeAddress.FirstAddress.RowNumber || + r.RangeAddress.LastAddress.RowNumber > range.RangeAddress.LastAddress.RowNumber) + .ToList(); + foreach (var rangeToSplit in rangesToSplit) + { + Worksheet.MergedRanges.Remove(rangeToSplit); + } + } + + Workbook.Worksheets.ForEach(ws => MoveNamedRangesColumns(range, columnsShifted, ws.NamedRanges)); + MoveNamedRangesColumns(range, columnsShifted, Workbook.NamedRanges); + ShiftConditionalFormattingColumns(range, columnsShifted); + ShiftDataValidationColumns(range, columnsShifted); + ShiftPageBreaksColumns(range, columnsShifted); + RemoveInvalidSparklines(); + } + + private void ShiftPageBreaksColumns(XLRange range, int columnsShifted) + { + for (var i = 0; i < PageSetup.ColumnBreaks.Count; i++) + { + int br = PageSetup.ColumnBreaks[i]; + if (range.RangeAddress.FirstAddress.ColumnNumber <= br) + { + PageSetup.ColumnBreaks[i] = br + columnsShifted; + } + } + } + + private void ShiftConditionalFormattingColumns(XLRange range, int columnsShifted) + { + if (!ConditionalFormats.Any()) return; + Int32 firstCol = range.RangeAddress.FirstAddress.ColumnNumber; + if (firstCol == 1) return; + + int colNum = columnsShifted > 0 ? firstCol - 1 : firstCol; + var model = Column(colNum).AsRange(); + + foreach (var cf in ConditionalFormats.ToList()) + { + var cfRanges = cf.Ranges.ToList(); + cf.Ranges.RemoveAll(); + + foreach (var cfRange in cfRanges) + { + var cfAddress = cfRange.RangeAddress; + IXLRange newRange; + if (cfRange.Intersects(model)) + { + newRange = Range(cfAddress.FirstAddress.RowNumber, + cfAddress.FirstAddress.ColumnNumber, + cfAddress.LastAddress.RowNumber, + Math.Min(XLHelper.MaxColumnNumber, cfAddress.LastAddress.ColumnNumber + columnsShifted)); + } + else if (cfAddress.FirstAddress.ColumnNumber >= firstCol) + { + newRange = Range(cfAddress.FirstAddress.RowNumber, + Math.Max(cfAddress.FirstAddress.ColumnNumber + columnsShifted, firstCol), + cfAddress.LastAddress.RowNumber, + Math.Min(XLHelper.MaxColumnNumber, cfAddress.LastAddress.ColumnNumber + columnsShifted)); + } + else + newRange = cfRange; + + if (newRange.RangeAddress.IsValid && + newRange.RangeAddress.FirstAddress.ColumnNumber <= + newRange.RangeAddress.LastAddress.ColumnNumber) + cf.Ranges.Add(newRange); + } + + if (!cf.Ranges.Any()) + ConditionalFormats.Remove(f => f == cf); + } + } + + private void ShiftDataValidationColumns(XLRange range, int columnsShifted) + { + if (!DataValidations.Any()) return; + Int32 firstCol = range.RangeAddress.FirstAddress.ColumnNumber; + if (firstCol == 1) return; + + int colNum = columnsShifted > 0 ? firstCol - 1 : firstCol; + var model = Column(colNum).AsRange(); + + foreach (var dv in DataValidations.ToList()) + { + var dvRanges = dv.Ranges.ToList(); + dv.ClearRanges(); + + foreach (var dvRange in dvRanges) + { + var dvAddress = dvRange.RangeAddress; + IXLRange newRange; + if (dvRange.Intersects(model)) + { + newRange = Range(dvAddress.FirstAddress.RowNumber, + dvAddress.FirstAddress.ColumnNumber, + dvAddress.LastAddress.RowNumber, + Math.Min(XLHelper.MaxColumnNumber, dvAddress.LastAddress.ColumnNumber + columnsShifted)); + } + else if (dvAddress.FirstAddress.ColumnNumber >= firstCol) + { + newRange = Range(dvAddress.FirstAddress.RowNumber, + Math.Max(dvAddress.FirstAddress.ColumnNumber + columnsShifted, firstCol), + dvAddress.LastAddress.RowNumber, + Math.Min(XLHelper.MaxColumnNumber, dvAddress.LastAddress.ColumnNumber + columnsShifted)); + } + else + newRange = dvRange; + + if (newRange.RangeAddress.IsValid && + newRange.RangeAddress.FirstAddress.ColumnNumber <= + newRange.RangeAddress.LastAddress.ColumnNumber) + dv.AddRange(newRange); + } + + if (!dv.Ranges.Any()) + DataValidations.Delete(v => v == dv); + } + } + + internal override void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) + { + if (!range.IsEntireRow()) + { + var model = new XLRangeAddress( + range.RangeAddress.FirstAddress, + new XLAddress(XLHelper.MaxRowNumber, range.RangeAddress.LastAddress.ColumnNumber, false, false)); + var rangesToSplit = Worksheet.MergedRanges + .GetIntersectedRanges(model) + .Where(r => r.RangeAddress.FirstAddress.ColumnNumber < range.RangeAddress.FirstAddress.ColumnNumber || + r.RangeAddress.LastAddress.ColumnNumber > range.RangeAddress.LastAddress.ColumnNumber) + .ToList(); + foreach (var rangeToSplit in rangesToSplit) + { + Worksheet.MergedRanges.Remove(rangeToSplit); + } + } + + Workbook.Worksheets.ForEach(ws => MoveNamedRangesRows(range, rowsShifted, ws.NamedRanges)); + MoveNamedRangesRows(range, rowsShifted, Workbook.NamedRanges); + ShiftConditionalFormattingRows(range, rowsShifted); + ShiftDataValidationRows(range, rowsShifted); + RemoveInvalidSparklines(); + ShiftPageBreaksRows(range, rowsShifted); + } + + private void ShiftPageBreaksRows(XLRange range, int rowsShifted) + { + for (var i = 0; i < PageSetup.RowBreaks.Count; i++) + { + int br = PageSetup.RowBreaks[i]; + if (range.RangeAddress.FirstAddress.RowNumber <= br) + { + PageSetup.RowBreaks[i] = br + rowsShifted; + } + } + } + + private void ShiftConditionalFormattingRows(XLRange range, int rowsShifted) + { + if (!ConditionalFormats.Any()) return; + Int32 firstRow = range.RangeAddress.FirstAddress.RowNumber; + if (firstRow == 1) return; + + int rowNum = rowsShifted > 0 ? firstRow - 1 : firstRow; + var model = Row(rowNum).AsRange(); + + foreach (var cf in ConditionalFormats.ToList()) + { + var cfRanges = cf.Ranges.ToList(); + cf.Ranges.RemoveAll(); + + foreach (var cfRange in cfRanges) + { + var cfAddress = cfRange.RangeAddress; + IXLRange newRange; + if (cfRange.Intersects(model)) + { + newRange = Range(cfAddress.FirstAddress.RowNumber, + cfAddress.FirstAddress.ColumnNumber, + Math.Min(XLHelper.MaxRowNumber, cfAddress.LastAddress.RowNumber + rowsShifted), + cfAddress.LastAddress.ColumnNumber); + } + else if (cfAddress.FirstAddress.RowNumber >= firstRow) + { + newRange = Range(Math.Max(cfAddress.FirstAddress.RowNumber + rowsShifted, firstRow), + cfAddress.FirstAddress.ColumnNumber, + Math.Min(XLHelper.MaxRowNumber, cfAddress.LastAddress.RowNumber + rowsShifted), + cfAddress.LastAddress.ColumnNumber); + } + else + newRange = cfRange; + + if (newRange.RangeAddress.IsValid && + newRange.RangeAddress.FirstAddress.RowNumber <= newRange.RangeAddress.LastAddress.RowNumber) + cf.Ranges.Add(newRange); + } + + if (!cf.Ranges.Any()) + ConditionalFormats.Remove(f => f == cf); + } + } + + private void ShiftDataValidationRows(XLRange range, int rowsShifted) + { + if (!DataValidations.Any()) return; + Int32 firstRow = range.RangeAddress.FirstAddress.RowNumber; + if (firstRow == 1) return; + + int rowNum = rowsShifted > 0 ? firstRow - 1 : firstRow; + var model = Row(rowNum).AsRange(); + + foreach (var dv in DataValidations.ToList()) + { + var dvRanges = dv.Ranges.ToList(); + dv.ClearRanges(); + + foreach (var dvRange in dvRanges) + { + var dvAddress = dvRange.RangeAddress; + IXLRange newRange; + if (dvRange.Intersects(model)) + { + newRange = Range(dvAddress.FirstAddress.RowNumber, + dvAddress.FirstAddress.ColumnNumber, + Math.Min(XLHelper.MaxRowNumber, dvAddress.LastAddress.RowNumber + rowsShifted), + dvAddress.LastAddress.ColumnNumber); + } + else if (dvAddress.FirstAddress.RowNumber >= firstRow) + { + newRange = Range(Math.Max(dvAddress.FirstAddress.RowNumber + rowsShifted, firstRow), + dvAddress.FirstAddress.ColumnNumber, + Math.Min(XLHelper.MaxRowNumber, dvAddress.LastAddress.RowNumber + rowsShifted), + dvAddress.LastAddress.ColumnNumber); + } + else + newRange = dvRange; + + if (newRange.RangeAddress.IsValid && + newRange.RangeAddress.FirstAddress.RowNumber <= newRange.RangeAddress.LastAddress.RowNumber) + dv.AddRange(newRange); + } + + if (!dv.Ranges.Any()) + DataValidations.Delete(v => v == dv); + } + } + + private void RemoveInvalidSparklines() + { + var invalidSparklines = SparklineGroups.SelectMany(g => g) + .Where(sl => !((XLAddress)sl.Location.Address).IsValid) + .ToList(); + + foreach (var sparkline in invalidSparklines) + { + Worksheet.SparklineGroups.Remove(sparkline.Location); + } + } + + private void MoveNamedRangesRows(XLRange range, int rowsShifted, IXLNamedRanges namedRanges) + { + foreach (XLNamedRange nr in namedRanges) + { + var newRangeList = + nr.RangeList.Select(r => XLCell.ShiftFormulaRows(r, this, range, rowsShifted)).Where( + newReference => newReference.Length > 0).ToList(); + nr.RangeList = newRangeList; + } + } + + private void MoveNamedRangesColumns(XLRange range, int columnsShifted, IXLNamedRanges namedRanges) + { + foreach (XLNamedRange nr in namedRanges) + { + var newRangeList = + nr.RangeList.Select(r => XLCell.ShiftFormulaColumns(r, this, range, columnsShifted)).Where( + newReference => newReference.Length > 0).ToList(); + nr.RangeList = newRangeList; + } + } + + public void NotifyRangeShiftedRows(XLRange range, Int32 rowsShifted) + { + try + { + SuspendEvents(); + + var rangesToShift = _rangeRepository + .Where(r => r.RangeAddress.IsValid) + .OrderBy(r => r.RangeAddress.FirstAddress.RowNumber * -Math.Sign(rowsShifted)) + .ToList(); + + WorksheetRangeShiftedRows(range, rowsShifted); + + foreach (var storedRange in rangesToShift) + { + if (storedRange.IsEntireColumn()) + continue; + + if (ReferenceEquals(range, storedRange)) + continue; + + storedRange.WorksheetRangeShiftedRows(range, rowsShifted); + } + range.WorksheetRangeShiftedRows(range, rowsShifted); + } + finally + { + ResumeEvents(); + } + } + + public void NotifyRangeShiftedColumns(XLRange range, Int32 columnsShifted) + { + try + { + SuspendEvents(); + + var rangesToShift = _rangeRepository + .Where(r => r.RangeAddress.IsValid) + .OrderBy(r => r.RangeAddress.FirstAddress.ColumnNumber * -Math.Sign(columnsShifted)) + .ToList(); + + WorksheetRangeShiftedColumns(range, columnsShifted); + + foreach (var storedRange in rangesToShift) + { + if (storedRange.IsEntireRow()) + continue; + + if (ReferenceEquals(range, storedRange)) + continue; + + storedRange.WorksheetRangeShiftedColumns(range, columnsShifted); + } + range.WorksheetRangeShiftedColumns(range, columnsShifted); + } + finally + { + ResumeEvents(); + } + } + + public XLRow Row(Int32 rowNumber, Boolean pingCells) + { + if (rowNumber <= 0 || rowNumber > XLHelper.MaxRowNumber) + throw new ArgumentOutOfRangeException(nameof(rowNumber), $"Row number must be between 1 and {XLHelper.MaxRowNumber}"); + + if (Internals.RowsCollection.TryGetValue(rowNumber, out XLRow row)) + return row; + else + { + if (pingCells) + { + // This is a new row so we're going to reference all + // cells in columns of this row to preserve their formatting + + Internals.ColumnsCollection.Keys.ForEach(c => Cell(rowNumber, c)); + } + + row = RangeFactory.CreateRow(rowNumber); + Internals.RowsCollection.Add(rowNumber, row); + } + + return row; + } + + public IXLTable Table(XLRange range, Boolean addToTables, Boolean setAutofilter = true) + { + return Table(range, GetNewTableName("Table"), addToTables, setAutofilter); + } + + public IXLTable Table(XLRange range, String name, Boolean addToTables, Boolean setAutofilter = true) + { + CheckRangeNotOverlappingOtherEntities(range); + XLRangeAddress rangeAddress; + if (range.Rows().Count() == 1) + { + rangeAddress = new XLRangeAddress(range.FirstCell().Address, range.LastCell().CellBelow().Address); + range.InsertRowsBelow(1); + } + else + rangeAddress = range.RangeAddress; + + var rangeKey = new XLRangeKey(XLRangeType.Table, rangeAddress); + var table = (XLTable)_rangeRepository.GetOrCreate(ref rangeKey); + + if (table.Name != name) + table.Name = name; + + if (addToTables && !Tables.Contains(table)) + { + Tables.Add(table); + } + + if (setAutofilter && !table.ShowAutoFilter) + table.InitializeAutoFilter(); + + return table; + } + + private void CheckRangeNotOverlappingOtherEntities(XLRange range) + { + // Check that the range doesn't overlap with any existing tables + var firstOverlappingTable = Tables.FirstOrDefault(t => t.RangeUsed().Intersects(range)); + if (firstOverlappingTable != null) + throw new InvalidOperationException($"The range {range.RangeAddress.ToStringRelative(includeSheet: true)} is already part of table '{firstOverlappingTable.Name}'"); + + // Check that the range doesn't overlap with any filters + if (AutoFilter.IsEnabled && this.AutoFilter.Range.Intersects(range)) + throw new InvalidOperationException($"The range {range.RangeAddress.ToStringRelative(includeSheet: true)} overlaps with the worksheet's autofilter."); + } + + private string GetNewTableName(string baseName) + { + var existingTableNames = new HashSet( + this.Workbook.Worksheets + .SelectMany(ws => ws.Tables) + .Select(t => t.Name), + StringComparer.OrdinalIgnoreCase); + + var i = 1; + string tableName; + do + { + tableName = baseName + i.ToString(); + i++; + } while (existingTableNames.Contains(tableName)); + + return tableName; + } + + private IXLRange GetRangeForSort() + { + var range = RangeUsed(); + SortColumns.ForEach(e => range.SortColumns.Add(e.ElementNumber, e.SortOrder, e.IgnoreBlanks, e.MatchCase)); + SortRows.ForEach(e => range.SortRows.Add(e.ElementNumber, e.SortOrder, e.IgnoreBlanks, e.MatchCase)); + return range; + } + + public XLPivotTable PivotTable(String name) + { + return (XLPivotTable)PivotTables.PivotTable(name); + } + + public override IXLCells Cells() + { + return Cells(true, XLCellsUsedOptions.All); + } + + public override IXLCells Cells(Boolean usedCellsOnly) + { + if (usedCellsOnly) + return Cells(true, XLCellsUsedOptions.AllContents); + else + return Range((this as IXLRangeBase).FirstCellUsed(XLCellsUsedOptions.All), + (this as IXLRangeBase).LastCellUsed(XLCellsUsedOptions.All)) + .Cells(false, XLCellsUsedOptions.All); + } + + public override XLCell Cell(String cellAddressInRange) + { + var cell = base.Cell(cellAddressInRange); + if (cell != null) return cell; + + if (Workbook.NamedRanges.TryGetValue(cellAddressInRange, out IXLNamedRange workbookNamedRange)) + { + if (!workbookNamedRange.Ranges.Any()) + return null; + + return workbookNamedRange.Ranges.FirstOrDefault().FirstCell().CastTo(); + } + + return null; + } + + public override XLRange Range(String rangeAddressStr) + { + if (XLHelper.IsValidRangeAddress(rangeAddressStr)) + return Range(new XLRangeAddress(Worksheet, rangeAddressStr)); + + if (rangeAddressStr.Contains("[")) + return Table(rangeAddressStr.Substring(0, rangeAddressStr.IndexOf("["))) as XLRange; + + if (NamedRanges.TryGetValue(rangeAddressStr, out IXLNamedRange worksheetNamedRange)) + return worksheetNamedRange.Ranges.First().CastTo(); + + if (Workbook.NamedRanges.TryGetValue(rangeAddressStr, out IXLNamedRange workbookNamedRange)) + { + if (!workbookNamedRange.Ranges.Any()) + return null; + + return workbookNamedRange.Ranges.First().CastTo(); + } + + return null; + } + + public IXLRanges MergedRanges { get { return Internals.MergedRanges; } } + + public IXLConditionalFormats ConditionalFormats { get; private set; } + + public IXLSparklineGroups SparklineGroups { get; private set; } + + private Boolean _eventTracking; + + public void SuspendEvents() + { + _eventTracking = EventTrackingEnabled; + EventTrackingEnabled = false; + } + + public void ResumeEvents() + { + EventTrackingEnabled = _eventTracking; + } + + private IXLRanges _selectedRanges; + + public IXLRanges SelectedRanges + { + get + { + _selectedRanges?.RemoveAll(r => !r.RangeAddress.IsValid); + return _selectedRanges; + } + internal set + { + _selectedRanges = value; + } + } + + public IXLCell ActiveCell { get; set; } + + private XLCalcEngine _calcEngine; + + internal XLCalcEngine CalcEngine + { + get { return _calcEngine ?? (_calcEngine = new XLCalcEngine(this)); } + } + + public Object Evaluate(String expression) + { + return CalcEngine.Evaluate(expression); + } + + /// + /// Force recalculation of all cell formulas. + /// + public void RecalculateAllFormulas() + { + CellsUsed().Cast().ForEach(cell => cell.Evaluate(true)); + } + + public String Author { get; set; } + + public override string ToString() + { + return this.Name; + } + + public IXLPictures Pictures { get; private set; } + + public Boolean IsPasswordProtected => Protection.IsPasswordProtected; + + public bool IsProtected => Protection.IsProtected; + + public IXLPicture Picture(string pictureName) + { + return Pictures.Picture(pictureName); + } + + public IXLPicture AddPicture(Stream stream) + { + return Pictures.Add(stream); + } + + public IXLPicture AddPicture(Stream stream, string name) + { + return Pictures.Add(stream, name); + } + + internal IXLPicture AddPicture(Stream stream, string name, int Id) + { + return (Pictures as XLPictures).Add(stream, name, Id); + } + + public IXLPicture AddPicture(Stream stream, XLPictureFormat format) + { + return Pictures.Add(stream, format); + } + + public IXLPicture AddPicture(Stream stream, XLPictureFormat format, string name) + { + return Pictures.Add(stream, format, name); + } + + public IXLPicture AddPicture(Bitmap bitmap) + { + return Pictures.Add(bitmap); + } + + public IXLPicture AddPicture(Bitmap bitmap, string name) + { + return Pictures.Add(bitmap, name); + } + + public IXLPicture AddPicture(string imageFile) + { + return Pictures.Add(imageFile); + } + + public IXLPicture AddPicture(string imageFile, string name) + { + return Pictures.Add(imageFile, name); + } + + public override Boolean IsEntireRow() + { + return true; + } + + public override Boolean IsEntireColumn() + { + return true; + } + + internal void SetValue(T value, int ro, int co) where T : class + { + if (value == null) + this.Cell(ro, co).SetValue(String.Empty); + else if (value is IConvertible) + this.Cell(ro, co).SetValue((T)Convert.ChangeType(value, typeof(T))); + else + this.Cell(ro, co).SetValue(value); + } + + /// + /// Get a cell value not initializing it if it has not been initialized yet. + /// + /// Row number + /// Column number + /// Current value of the specified cell. Empty string for non-initialized cells. + internal object GetCellValue(int ro, int co) + { + if (Internals.CellsCollection.MaxRowUsed < ro || + Internals.CellsCollection.MaxColumnUsed < co || + !Internals.CellsCollection.Contains(ro, co)) + return string.Empty; + + var cell = Worksheet.Internals.CellsCollection.GetCell(ro, co); + if (cell.IsEvaluating) + return string.Empty; + + return cell.Value; + } + + public XLRange GetOrCreateRange(XLRangeParameters xlRangeParameters) + { + var rangeKey = new XLRangeKey(XLRangeType.Range, xlRangeParameters.RangeAddress); + var range = _rangeRepository.GetOrCreate(ref rangeKey); + if (xlRangeParameters.DefaultStyle != null && range.StyleValue == StyleValue) + range.InnerStyle = xlRangeParameters.DefaultStyle; + + return range as XLRange; + } + + /// + /// Get a range row from the shared repository or create a new one. + /// + /// Address of range row. + /// Style to apply. If null the worksheet's style is applied. + /// Range row with the specified address. + public XLRangeRow RangeRow(XLRangeAddress address, IXLStyle defaultStyle = null) + { + var rangeKey = new XLRangeKey(XLRangeType.RangeRow, address); + var rangeRow = (XLRangeRow)_rangeRepository.GetOrCreate(ref rangeKey); + + if (defaultStyle != null && rangeRow.StyleValue == StyleValue) + rangeRow.InnerStyle = defaultStyle; + + return rangeRow; + } + + /// + /// Get a range column from the shared repository or create a new one. + /// + /// Address of range column. + /// Style to apply. If null the worksheet's style is applied. + /// Range column with the specified address. + public XLRangeColumn RangeColumn(XLRangeAddress address, IXLStyle defaultStyle = null) + { + var rangeKey = new XLRangeKey(XLRangeType.RangeColumn, address); + var rangeColumn = (XLRangeColumn)_rangeRepository.GetOrCreate(ref rangeKey); + + if (defaultStyle != null && rangeColumn.StyleValue == StyleValue) + rangeColumn.InnerStyle = defaultStyle; + + return rangeColumn; + } + + protected override void OnRangeAddressChanged(XLRangeAddress oldAddress, XLRangeAddress newAddress) + { + } + + public void RellocateRange(XLRangeType rangeType, XLRangeAddress oldAddress, XLRangeAddress newAddress) + { + if (_rangeRepository == null) + return; + + var oldKey = new XLRangeKey(rangeType, oldAddress); + var newKey = new XLRangeKey(rangeType, newAddress); + var range = _rangeRepository.Replace(ref oldKey, ref newKey); + + foreach (var rangeIndex in _rangeIndices) + { + if (!rangeIndex.MatchesType(rangeType)) + continue; + + if (rangeIndex.Remove(oldAddress) && + newAddress.IsValid && + range != null) + { + rangeIndex.Add(range); + } + } + } + + internal void DeleteColumn(int columnNumber) + { + Internals.ColumnsCollection.Remove(columnNumber); + + var columnsToMove = new List(); + columnsToMove.AddRange( + Internals.ColumnsCollection.Where(c => c.Key > columnNumber).Select(c => c.Key)); + foreach (int column in columnsToMove.OrderBy(c => c)) + { + Internals.ColumnsCollection.Add(column - 1, Internals.ColumnsCollection[column]); + Internals.ColumnsCollection.Remove(column); + + Internals.ColumnsCollection[column - 1].SetColumnNumber(column - 1); + } + } + + internal void DeleteRow(int rowNumber) + { + Internals.RowsCollection.Remove(rowNumber); + + var rowsToMove = new List(); + rowsToMove.AddRange(Internals.RowsCollection.Where(c => c.Key > rowNumber).Select(c => c.Key)); + foreach (int row in rowsToMove.OrderBy(r => r)) + { + Internals.RowsCollection.Add(row - 1, Worksheet.Internals.RowsCollection[row]); + Internals.RowsCollection.Remove(row); + + Internals.RowsCollection[row - 1].SetRowNumber(row - 1); + } + } + + internal void DeleteRange(XLRangeAddress rangeAddress) + { + var rangeKey = new XLRangeKey(XLRangeType.Range, rangeAddress); + _rangeRepository.Remove(ref rangeKey); + } + } +} diff --git a/ClosedXML/Excel/XLWorksheetInternals.cs b/ClosedXML/Excel/XLWorksheetInternals.cs new file mode 100644 index 0000000..ea5a915 --- /dev/null +++ b/ClosedXML/Excel/XLWorksheetInternals.cs @@ -0,0 +1,51 @@ +using System; + +namespace ClosedXML.Excel +{ + internal class XLWorksheetInternals : IDisposable + { + public XLWorksheetInternals( + XLCellsCollection cellsCollection, + XLColumnsCollection columnsCollection, + XLRowsCollection rowsCollection, + XLRanges mergedRanges + ) + { + CellsCollection = cellsCollection; + ColumnsCollection = columnsCollection; + RowsCollection = rowsCollection; + MergedRanges = mergedRanges; + } + + public XLCellsCollection CellsCollection { get; private set; } + public XLColumnsCollection ColumnsCollection { get; private set; } + public XLRowsCollection RowsCollection { get; private set; } + public XLRanges MergedRanges { get; internal set; } + + // Used by Janitor.Fody + private void DisposeManaged() + { + CellsCollection.Clear(); + ColumnsCollection.Clear(); + RowsCollection.Clear(); + MergedRanges.RemoveAll(); + } + +#if _NET40_ + + public void Dispose() + { + // net40 doesn't support Janitor.Fody, so let's dispose manually + DisposeManaged(); + } + +#else + + public void Dispose() + { + // Leave this empty (for non net40 targets) so that Janitor.Fody can do its work + } + +#endif + } +} diff --git a/ClosedXML/Excel/XLWorksheets.cs b/ClosedXML/Excel/XLWorksheets.cs new file mode 100644 index 0000000..25cd79d --- /dev/null +++ b/ClosedXML/Excel/XLWorksheets.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLWorksheets : IXLWorksheets, IEnumerable + { + private readonly XLWorkbook _workbook; + private readonly Dictionary _worksheets = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal ICollection Deleted { get; private set; } + + #region Constructor + + public XLWorksheets(XLWorkbook workbook) + { + _workbook = workbook; + Deleted = new HashSet(); + } + + #endregion Constructor + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_worksheets.Values).GetEnumerator(); + } + + #endregion IEnumerable Members + + #region IXLWorksheets Members + + public int Count + { + [DebuggerStepThrough] + get { return _worksheets.Count; } + } + + public Boolean Contains(String sheetName) + { + return _worksheets.ContainsKey(sheetName); + } + + public bool TryGetWorksheet(string sheetName, out IXLWorksheet worksheet) + { + if (_worksheets.TryGetValue(sheetName.UnescapeSheetName(), out XLWorksheet w)) + { + worksheet = w; + return true; + } + worksheet = null; + return false; + } + + public IXLWorksheet Worksheet(String sheetName) + { + sheetName = sheetName.UnescapeSheetName(); + + if (_worksheets.TryGetValue(sheetName, out XLWorksheet w)) + return w; + + throw new ArgumentException("There isn't a worksheet named '" + sheetName + "'."); + } + + public IXLWorksheet Worksheet(Int32 position) + { + int wsCount = _worksheets.Values.Count(w => w.Position == position); + if (wsCount == 0) + throw new ArgumentException("There isn't a worksheet associated with that position."); + + if (wsCount > 1) + { + throw new ArgumentException( + "Can't retrieve a worksheet because there are multiple worksheets associated with that position."); + } + + return _worksheets.Values.Single(w => w.Position == position); + } + + public IXLWorksheet Add() + { + return Add(GetNextWorksheetName()); + } + + public IXLWorksheet Add(Int32 position) + { + return Add(GetNextWorksheetName(), position); + } + + public IXLWorksheet Add(String sheetName) + { + var sheet = new XLWorksheet(sheetName, _workbook); + Add(sheetName, sheet); + sheet._position = _worksheets.Count + _workbook.UnsupportedSheets.Count; + return sheet; + } + + public IXLWorksheet Add(String sheetName, Int32 position) + { + _worksheets.Values.Where(w => w._position >= position).ForEach(w => w._position += 1); + _workbook.UnsupportedSheets.Where(w => w.Position >= position).ForEach(w => w.Position += 1); + var sheet = new XLWorksheet(sheetName, _workbook); + Add(sheetName, sheet); + sheet._position = position; + return sheet; + } + + private void Add(String sheetName, XLWorksheet sheet) + { + if (_worksheets.ContainsKey(sheetName)) + throw new ArgumentException(String.Format("A worksheet with the same name ({0}) has already been added.", sheetName), nameof(sheetName)); + + _worksheets.Add(sheetName, sheet); + } + + public void Delete(String sheetName) + { + Delete(_worksheets[sheetName].Position); + } + + public void Delete(Int32 position) + { + int wsCount = _worksheets.Values.Count(w => w.Position == position); + if (wsCount == 0) + throw new ArgumentException("There isn't a worksheet associated with that index."); + + if (wsCount > 1) + throw new ArgumentException( + "Can't delete the worksheet because there are multiple worksheets associated with that index."); + + var ws = _worksheets.Values.Single(w => w.Position == position); + if (!String.IsNullOrWhiteSpace(ws.RelId) && !Deleted.Contains(ws.RelId)) + Deleted.Add(ws.RelId); + + _worksheets.RemoveAll(w => w.Position == position); + _worksheets.Values.Where(w => w.Position > position).ForEach(w => w._position -= 1); + _workbook.UnsupportedSheets.Where(w => w.Position > position).ForEach(w => w.Position -= 1); + _workbook.InvalidateFormulas(); + + ws.Cleanup(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _worksheets.Values.Cast().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLWorksheet Add(DataTable dataTable) + { + return Add(dataTable, dataTable.TableName); + } + + public IXLWorksheet Add(DataTable dataTable, String sheetName) + { + var ws = Add(sheetName); + ws.Cell(1, 1).InsertTable(dataTable, sheetName); + return ws; + } + + public void Add(DataSet dataSet) + { + foreach (DataTable t in dataSet.Tables) + Add(t); + } + + #endregion IXLWorksheets Members + + public void Rename(String oldSheetName, String newSheetName) + { + if (String.IsNullOrWhiteSpace(oldSheetName) || !_worksheets.TryGetValue(oldSheetName, out XLWorksheet ws)) return; + + if (!oldSheetName.Equals(newSheetName, StringComparison.OrdinalIgnoreCase) + && _worksheets.ContainsKey(newSheetName)) + throw new ArgumentException(String.Format("A worksheet with the same name ({0}) has already been added.", newSheetName), nameof(newSheetName)); + + _worksheets.Remove(oldSheetName); + Add(newSheetName, ws); + } + + #region Private members + + private String GetNextWorksheetName() + { + var worksheetNumber = this.Count + 1; + var sheetName = $"Sheet{worksheetNumber}"; + while (_worksheets.Values.Any(p => p.Name.Equals(sheetName, StringComparison.OrdinalIgnoreCase))) + { + worksheetNumber++; + sheetName = $"Sheet{worksheetNumber}"; + } + return sheetName; + } + + #endregion Private members + } +} diff --git a/ClosedXML/Extensions/AttributeExtensions.cs b/ClosedXML/Extensions/AttributeExtensions.cs new file mode 100644 index 0000000..008de85 --- /dev/null +++ b/ClosedXML/Extensions/AttributeExtensions.cs @@ -0,0 +1,37 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace ClosedXML +{ + public static class AttributeExtensions + { + public static TAttribute[] GetAttributes( + this MemberInfo member) + where TAttribute : Attribute + { + var attributes = member.GetCustomAttributes(typeof(TAttribute), true); + + return (TAttribute[])attributes; + } + + public static MethodInfo GetMethod(this T instance, Expression> methodSelector) + { + return ((MethodCallExpression)methodSelector.Body).Method; + } + + public static MethodInfo GetMethod(this T instance, Expression> methodSelector) + { + return ((MethodCallExpression)methodSelector.Body).Method; + } + + public static bool HasAttribute( + this MemberInfo member) + where TAttribute : Attribute + { + return GetAttributes(member).Any(); + } + } +} diff --git a/ClosedXML/Extensions/ColorExtensions.cs b/ClosedXML/Extensions/ColorExtensions.cs new file mode 100644 index 0000000..b832509 --- /dev/null +++ b/ClosedXML/Extensions/ColorExtensions.cs @@ -0,0 +1,37 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Drawing; + +namespace ClosedXML.Excel +{ + internal static class ColorExtensions + { + private static readonly char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + public static String ToHex(this Color color) + { + byte[] bytes = new byte[4]; + + bytes[0] = color.A; + + bytes[1] = color.R; + + bytes[2] = color.G; + + bytes[3] = color.B; + + char[] chars = new char[bytes.Length * 2]; + + for (int i = 0; i < bytes.Length; i++) + { + int b = bytes[i]; + + chars[i * 2] = hexDigits[b >> 4]; + + chars[i * 2 + 1] = hexDigits[b & 0xF]; + } + + return new string(chars); + } + } +} diff --git a/ClosedXML/Extensions/DateTimeExtensions.cs b/ClosedXML/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..af989eb --- /dev/null +++ b/ClosedXML/Extensions/DateTimeExtensions.cs @@ -0,0 +1,43 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal static class DateTimeExtensions + { + public static Double MaxOADate + { + get + { + return 2958465.99999999; + } + } + + public static bool IsWorkDay(this DateTime date, IEnumerable bankHolidays) + { + return date.DayOfWeek != DayOfWeek.Saturday + && date.DayOfWeek != DayOfWeek.Sunday + && !bankHolidays.Contains(date); + } + + public static DateTime NextWorkday(this DateTime date, IEnumerable bankHolidays) + { + var nextDate = date.AddDays(1); + while (!nextDate.IsWorkDay(bankHolidays)) + nextDate = nextDate.AddDays(1); + + return nextDate; + } + + public static DateTime PreviousWorkDay(this DateTime date, IEnumerable bankHolidays) + { + var previousDate = date.AddDays(-1); + while (!previousDate.IsWorkDay(bankHolidays)) + previousDate = previousDate.AddDays(-1); + + return previousDate; + } + } +} diff --git a/ClosedXML/Extensions/DecimalExtensions.cs b/ClosedXML/Extensions/DecimalExtensions.cs new file mode 100644 index 0000000..221950f --- /dev/null +++ b/ClosedXML/Extensions/DecimalExtensions.cs @@ -0,0 +1,13 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal static class DecimalExtensions + { + public static Decimal SaveRound(this Decimal value) + { + return Math.Round(value, 6); + } + } +} diff --git a/ClosedXML/Extensions/DictionaryExtensions.cs b/ClosedXML/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..559ace5 --- /dev/null +++ b/ClosedXML/Extensions/DictionaryExtensions.cs @@ -0,0 +1,20 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal static class DictionaryExtensions + { + public static void RemoveAll(this Dictionary dic, + Func predicate) + { + var keys = dic.Keys.Where(k => predicate(dic[k])).ToList(); + foreach (var key in keys) + { + dic.Remove(key); + } + } + } +} diff --git a/ClosedXML/Extensions/DoubleExtensions.cs b/ClosedXML/Extensions/DoubleExtensions.cs new file mode 100644 index 0000000..8a096f3 --- /dev/null +++ b/ClosedXML/Extensions/DoubleExtensions.cs @@ -0,0 +1,13 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal static class DoubleExtensions + { + public static Double SaveRound(this Double value) + { + return Math.Round(value, 6); + } + } +} diff --git a/ClosedXML/Extensions/DoubleValueExtensions.cs b/ClosedXML/Extensions/DoubleValueExtensions.cs new file mode 100644 index 0000000..e5f77b0 --- /dev/null +++ b/ClosedXML/Extensions/DoubleValueExtensions.cs @@ -0,0 +1,14 @@ +// Keep this file CodeMaid organised and cleaned +using DocumentFormat.OpenXml; +using System; + +namespace ClosedXML.Excel +{ + internal static class DoubleValueExtensions + { + public static DoubleValue SaveRound(this DoubleValue value) + { + return value.HasValue ? new DoubleValue(Math.Round(value, 6)) : value; + } + } +} diff --git a/ClosedXML/Extensions/EnumerableExtensions.cs b/ClosedXML/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..1c2084a --- /dev/null +++ b/ClosedXML/Extensions/EnumerableExtensions.cs @@ -0,0 +1,33 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; + +namespace ClosedXML.Excel +{ + internal static class EnumerableExtensions + { + public static void ForEach(this IEnumerable source, Action action) + { + foreach (T item in source) + action(item); + } + + public static Type GetItemType(this IEnumerable source) + { + return typeof(T); + } + + public static Boolean HasDuplicates(this IEnumerable source) + { + HashSet distinctItems = new HashSet(); + foreach (var item in source) + { + if (!distinctItems.Add(item)) + { + return true; + } + } + return false; + } + } +} diff --git a/ClosedXML/Extensions/FontBaseExtensions.cs b/ClosedXML/Extensions/FontBaseExtensions.cs new file mode 100644 index 0000000..595865f --- /dev/null +++ b/ClosedXML/Extensions/FontBaseExtensions.cs @@ -0,0 +1,67 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Utils; +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ClosedXML.Excel +{ + internal static class FontBaseExtensions + { + public static void CopyFont(this IXLFontBase font, IXLFontBase sourceFont) + { + font.Bold = sourceFont.Bold; + font.Italic = sourceFont.Italic; + font.Underline = sourceFont.Underline; + font.Strikethrough = sourceFont.Strikethrough; + font.VerticalAlignment = sourceFont.VerticalAlignment; + font.Shadow = sourceFont.Shadow; + font.FontSize = sourceFont.FontSize; + font.FontColor = sourceFont.FontColor; + font.FontName = sourceFont.FontName; + font.FontFamilyNumbering = sourceFont.FontFamilyNumbering; + font.FontCharSet = sourceFont.FontCharSet; + } + + public static Double GetHeight(this IXLFontBase fontBase, Dictionary fontCache) + { + var font = GetCachedFont(fontBase, fontCache); + var textHeight = GraphicsUtils.MeasureString("X", font).Height; + return (double)textHeight * 0.85; + } + + public static Double GetWidth(this IXLFontBase fontBase, String text, Dictionary fontCache) + { + if (String.IsNullOrWhiteSpace(text)) + return 0; + + var font = GetCachedFont(fontBase, fontCache); + var textWidth = GraphicsUtils.MeasureString(text, font).Width; + + double width = (textWidth / 7d * 256 - 128 / 7) / 256; + width = Math.Round(width + 0.2, 2); + + return width; + } + + private static Font GetCachedFont(IXLFontBase fontBase, Dictionary fontCache) + { + if (!fontCache.TryGetValue(fontBase, out Font font)) + { + font = new Font(fontBase.FontName, (float)fontBase.FontSize, GetFontStyle(fontBase)); + fontCache.Add(fontBase, font); + } + return font; + } + + private static FontStyle GetFontStyle(IXLFontBase font) + { + FontStyle fontStyle = FontStyle.Regular; + if (font.Bold) fontStyle |= FontStyle.Bold; + if (font.Italic) fontStyle |= FontStyle.Italic; + if (font.Strikethrough) fontStyle |= FontStyle.Strikeout; + if (font.Underline != XLFontUnderlineValues.None) fontStyle |= FontStyle.Underline; + return fontStyle; + } + } +} diff --git a/ClosedXML/Extensions/FormatExtensions.cs b/ClosedXML/Extensions/FormatExtensions.cs new file mode 100644 index 0000000..c8a3add --- /dev/null +++ b/ClosedXML/Extensions/FormatExtensions.cs @@ -0,0 +1,18 @@ +// Keep this file CodeMaid organised and cleaned +using ExcelNumberFormat; +using System.Globalization; + +namespace ClosedXML.Extensions +{ + internal static class FormatExtensions + { + public static string ToExcelFormat(this object o, string format) + { + var nf = new NumberFormat(format); + if (!nf.IsValid) + return format; + + return nf.Format(o, CultureInfo.InvariantCulture); + } + } +} diff --git a/ClosedXML/Extensions/GuidExtensions.cs b/ClosedXML/Extensions/GuidExtensions.cs new file mode 100644 index 0000000..fe2f3a5 --- /dev/null +++ b/ClosedXML/Extensions/GuidExtensions.cs @@ -0,0 +1,13 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Extensions +{ + internal static class GuidExtensions + { + internal static String WrapInBraces(this Guid guid) + { + return string.Concat('{', guid.ToString(), '}'); + } + } +} diff --git a/ClosedXML/Extensions/IntegerExtensions.cs b/ClosedXML/Extensions/IntegerExtensions.cs new file mode 100644 index 0000000..27cf90b --- /dev/null +++ b/ClosedXML/Extensions/IntegerExtensions.cs @@ -0,0 +1,11 @@ +// Keep this file CodeMaid organised and cleaned +namespace ClosedXML.Excel +{ + internal static class IntegerExtensions + { + public static bool Between(this int val, int from, int to) + { + return val >= from && val <= to; + } + } +} diff --git a/ClosedXML/Extensions/ListExtensions.cs b/ClosedXML/Extensions/ListExtensions.cs new file mode 100644 index 0000000..9c699a1 --- /dev/null +++ b/ClosedXML/Extensions/ListExtensions.cs @@ -0,0 +1,19 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal static class ListExtensions + { + public static void RemoveAll(this IList list, Func predicate) + { + var indices = list.Where(item => predicate(item)).Select((item, i) => i).OrderByDescending(i => i).ToList(); + foreach (var i in indices) + { + list.RemoveAt(i); + } + } + } +} diff --git a/ClosedXML/Extensions/ObjectExtensions.cs b/ClosedXML/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..6660683 --- /dev/null +++ b/ClosedXML/Extensions/ObjectExtensions.cs @@ -0,0 +1,134 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Globalization; + +namespace ClosedXML.Excel +{ + internal static class ObjectExtensions + { + public static T CastTo(this Object o) + { + return (T)Convert.ChangeType(o, typeof(T)); + } + + public static bool IsNumber(this object value) + { + return value is sbyte + || value is byte + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + } + + public static string ToInvariantString(this T value) where T : struct + { + switch (value) + { + case sbyte v: + return v.ToString(CultureInfo.InvariantCulture); + + case byte v: + return v.ToString(CultureInfo.InvariantCulture); + + case short v: + return v.ToString(CultureInfo.InvariantCulture); + + case ushort v: + return v.ToString(CultureInfo.InvariantCulture); + + case int v: + return v.ToString(CultureInfo.InvariantCulture); + + case uint v: + return v.ToString(CultureInfo.InvariantCulture); + + case long v: + return v.ToString(CultureInfo.InvariantCulture); + + case ulong v: + return v.ToString(CultureInfo.InvariantCulture); + + case float v: + // Specify precision explicitly for backward compatibility + return v.ToString("G7", CultureInfo.InvariantCulture); + + case double v: + // Specify precision explicitly for backward compatibility + return v.ToString("G15", CultureInfo.InvariantCulture); + + case decimal v: + return v.ToString(CultureInfo.InvariantCulture); + + case TimeSpan ts: + return ts.ToString("c", CultureInfo.InvariantCulture); + + case DateTime d: + return d.ToString(CultureInfo.InvariantCulture); + + default: + return value.ToString(); + } + } + + // This method may cause boxing of value types so it is better to replace its calls with + // the generic version, where applicable + public static string ObjectToInvariantString(this object value) + { + if (value == null) + return string.Empty; + + switch (value) + { + case sbyte v: + return v.ToString(CultureInfo.InvariantCulture); + + case byte v: + return v.ToString(CultureInfo.InvariantCulture); + + case short v: + return v.ToString(CultureInfo.InvariantCulture); + + case ushort v: + return v.ToString(CultureInfo.InvariantCulture); + + case int v: + return v.ToString(CultureInfo.InvariantCulture); + + case uint v: + return v.ToString(CultureInfo.InvariantCulture); + + case long v: + return v.ToString(CultureInfo.InvariantCulture); + + case ulong v: + return v.ToString(CultureInfo.InvariantCulture); + + case float v: + // Specify precision explicitly for backward compatibility + return v.ToString("G7", CultureInfo.InvariantCulture); + + case double v: + // Specify precision explicitly for backward compatibility + return v.ToString("G15", CultureInfo.InvariantCulture); + + case decimal v: + return v.ToString(CultureInfo.InvariantCulture); + + case TimeSpan ts: + return ts.ToString("c", CultureInfo.InvariantCulture); + + case DateTime d: + return d.ToString(CultureInfo.InvariantCulture); + + default: + return value.ToString(); + } + } + } +} diff --git a/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs b/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs new file mode 100644 index 0000000..3c5ed93 --- /dev/null +++ b/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs @@ -0,0 +1,15 @@ +// Keep this file CodeMaid organised and cleaned +using DocumentFormat.OpenXml.Packaging; +using System; +using System.Linq; + +namespace ClosedXML.Extensions +{ + internal static class OpenXmlPartContainerExtensions + { + public static Boolean HasPartWithId(this OpenXmlPartContainer container, String relId) + { + return container.Parts.Any(p => p.RelationshipId.Equals(relId)); + } + } +} diff --git a/ClosedXML/Extensions/StringExtensions.cs b/ClosedXML/Extensions/StringExtensions.cs new file mode 100644 index 0000000..527db32 --- /dev/null +++ b/ClosedXML/Extensions/StringExtensions.cs @@ -0,0 +1,87 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel +{ + internal static class StringExtensions + { + private static readonly Regex RegexNewLine = new Regex(@"((? (Char.IsPunctuation(c) && c != '.' && c != '_') || + Char.IsSeparator(c) || + Char.IsControl(c) || + Char.IsSymbol(c)); + if (needEscape) + return String.Concat('\'', sheetName.Replace("'", "''"), '\''); + else + return sheetName; + } + + internal static String FixNewLines(this String value) + { + return value.Contains("\n") ? RegexNewLine.Replace(value, Environment.NewLine) : value; + } + + internal static Boolean PreserveSpaces(this String value) + { + return value.StartsWith(" ") || value.EndsWith(" ") || value.Contains(Environment.NewLine); + } + + internal static String ToCamel(this String value) + { + if (value.Length == 0) + return value; + + if (value.Length == 1) + return value.ToLower(); + + return value.Substring(0, 1).ToLower() + value.Substring(1); + } + + internal static String ToProper(this String value) + { + if (value.Length == 0) + return value; + + if (value.Length == 1) + return value.ToUpper(); + + return value.Substring(0, 1).ToUpper() + value.Substring(1); + } + + internal static string UnescapeSheetName(this String sheetName) + { + return sheetName + .Trim('\'') + .Replace("''", "'"); + } + } +} diff --git a/ClosedXML/Extensions/TypeExtensions.cs b/ClosedXML/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..3b4433c --- /dev/null +++ b/ClosedXML/Extensions/TypeExtensions.cs @@ -0,0 +1,42 @@ +// Keep this file CodeMaid organised and cleaned +using System; + +namespace ClosedXML.Excel +{ + internal static class TypeExtensions + { + public static Type GetUnderlyingType(this Type type) + { + return Nullable.GetUnderlyingType(type) ?? type; + } + + public static bool IsNullableType(this Type type) + { + return Nullable.GetUnderlyingType(type) != null; + } + + public static bool IsNumber(this Type type) + { + return type == typeof(sbyte) + || type == typeof(byte) + || type == typeof(short) + || type == typeof(ushort) + || type == typeof(int) + || type == typeof(uint) + || type == typeof(long) + || type == typeof(ulong) + || type == typeof(float) + || type == typeof(double) + || type == typeof(decimal); + } + + public static bool IsSimpleType(this Type type) + { + return type.IsPrimitive + || type == typeof(String) + || type == typeof(DateTime) + || type == typeof(TimeSpan) + || type.IsNumber(); + } + } +} diff --git a/ClosedXML/Extensions/XDocumentExtensions.cs b/ClosedXML/Extensions/XDocumentExtensions.cs new file mode 100644 index 0000000..36a7a44 --- /dev/null +++ b/ClosedXML/Extensions/XDocumentExtensions.cs @@ -0,0 +1,25 @@ +// Keep this file CodeMaid organised and cleaned +using System.IO; +using System.Xml; +using System.Xml.Linq; + +namespace ClosedXML.Excel +{ + internal static class XDocumentExtensions + { + public static XDocument Load(Stream stream) + { + using (XmlReader reader = XmlReader.Create(stream)) + { + try + { + return XDocument.Load(reader); + } + catch (XmlException) + { + return null; + } + } + } + } +} diff --git a/ClosedXML/FodyWeavers.xml b/ClosedXML/FodyWeavers.xml new file mode 100644 index 0000000..958bbfc --- /dev/null +++ b/ClosedXML/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ClosedXML/FodyWeavers.xsd b/ClosedXML/FodyWeavers.xsd new file mode 100644 index 0000000..df82d5f --- /dev/null +++ b/ClosedXML/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/ClosedXML/PathHelper.cs b/ClosedXML/PathHelper.cs new file mode 100644 index 0000000..f87a9db --- /dev/null +++ b/ClosedXML/PathHelper.cs @@ -0,0 +1,243 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; + +namespace ClosedXML +{ + /// + /// Help methods for work with files + /// + internal static class PathHelper + { + private static readonly Regex ms_checkValidAbsolutePathRegEx = new Regex( + "^(([a-zA-Z]\\:)|\\\\)(\\\\|(\\\\[^\\n\\r\\t/:*?<>\\\"|]*)+)$", + RegexOptions.IgnoreCase + | RegexOptions.Singleline + | RegexOptions.CultureInvariant + | RegexOptions.Compiled + ); + /// + /// Can check only .\dfdfd\dfdf\dfdf or ..\..\gfhfgh\fghfh + /// + private static readonly Regex ms_checkValidRelativePathRegEx = new Regex( + "^(\\.\\.|\\.)(\\\\|(\\\\[^\\n\\r\\t/:*?<>\\\"|]*)+)$", + RegexOptions.IgnoreCase + | RegexOptions.Multiline + | RegexOptions.CultureInvariant + | RegexOptions.Compiled + ); + + private static readonly object ms_syncAbsolutePathObj = new object(); + + /// + /// Gets data and time string stamp for file name + /// + /// + public static string GetTimeStamp() + { + return GetTimeStamp(DateTime.Now); + } + /// + /// Gets data and time string stamp for file name + /// + /// + public static string GetTimeStamp(DateTime dateTime) + { + return dateTime.ToString("ddMMMyyyy_HHmmss", DateTimeFormatInfo.InvariantInfo); + } + /// + /// Safety delete file(with try block) + /// + /// file name + public static void SafetyDeleteFile(string fileName) + { + try + { + if (!string.IsNullOrEmpty(fileName) && File.Exists(fileName)) + { + File.Delete(fileName); + } + } + catch (Exception ex) + { + Debug.Fail("Error during file deleting. Error message:" + ex.Message); + } + } + /// + /// Replace all not implemented symbols to '_' + /// + /// input filename + /// + public static string NormalizeFileName(string fileName) + { + char[] invalidCharacters = Path.GetInvalidFileNameChars(); + #region Replace begin space + string result = fileName.TrimStart(' '); + if (result.Length < fileName.Length) + { + result = new string('_', fileName.Length - result.Length) + result; + } + #endregion + foreach (char curChar in invalidCharacters) + { + result = result.Replace(curChar, '_'); + } + return result; + } + + public static string NormalizePathName(string fileName) + { + char[] invalidCharacters = Path.GetInvalidPathChars(); + string result = fileName.TrimStart(' '); + foreach (char curChar in invalidCharacters) + { + result = result.Replace(curChar, '_'); + } + return result; + } + /// + /// ValidatePath file or diretory path + /// + /// + /// path type + /// + public static bool ValidatePath(string path, PathTypes type) + { + bool _result = false; + if ((type & PathTypes.Absolute) == PathTypes.Absolute) + { + _result |= ms_checkValidAbsolutePathRegEx.IsMatch(path); + } + if ((type & PathTypes.Relative) == PathTypes.Relative) + { + _result |= ms_checkValidRelativePathRegEx.IsMatch(path); + } + return _result; + } + + /// + /// ValidatePath file or diretory path + /// + /// + /// + public static bool ValidateFileName(string fileName) + { + return fileName.LastIndexOfAny(Path.GetInvalidFileNameChars()) < 0; + } + + public static string EvaluateRelativePath(string mainDirPath, string absoluteFilePath) + { + string[] firstPathParts = mainDirPath.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); + string[] secondPathParts = absoluteFilePath.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); + + int sameCounter = 0; + int partsCount = Math.Min(firstPathParts.Length, secondPathParts.Length); + for (int i = 0; i < partsCount; i++) + { + if (String.Compare(firstPathParts[i], secondPathParts[i], true) != 0) + { + break; + } + sameCounter++; + } + + if (sameCounter == 0) + { + return absoluteFilePath; + } + + string newPath = string.Empty; + for (int i = sameCounter; i < firstPathParts.Length; i++) + { + if (i > sameCounter) + { + newPath += Path.DirectorySeparatorChar; + } + newPath += ".."; + } + if (newPath.Length == 0) + { + newPath = "."; + } + for (int i = sameCounter; i < secondPathParts.Length; i++) + { + newPath += Path.DirectorySeparatorChar; + newPath += secondPathParts[i]; + } + return newPath; + } + + public static string EvaluateAbsolutePath(string rootPath, string relativePath) + { + lock (ms_syncAbsolutePathObj) + { + string _temp = Environment.CurrentDirectory; + Environment.CurrentDirectory = rootPath; + string _result = Path.GetFullPath(relativePath); + Environment.CurrentDirectory = _temp; + return _result; + } + } + + public static bool TryCreateFile(string filePath, out string message) + { + #region Check + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException("filePath"); + } + #endregion + try + { + if (File.Exists(filePath)) + { + message = null; + return true; + } + + using (FileStream _stream = new FileStream(filePath, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Read, + 1024, + FileOptions.DeleteOnClose)) + { + } + } + catch (Exception exc) + { + message = exc.Message; + return false; + } + + message = null; + return true; + } + + public static string CreateDirectory(string path) + { + try + { + var info = new DirectoryInfo(path); + if (!info.Exists) + { + info.Create(); + } + } + catch + { + } + return path; + } + + [Flags] + public enum PathTypes + { + None = 0, + Absolute = 1, + Relative = 2 + } + } +} \ No newline at end of file diff --git a/ClosedXML/Properties/AssemblyInfo.cs b/ClosedXML/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4c21750 --- /dev/null +++ b/ClosedXML/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +#if STRONGNAME +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ClosedXML_Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a1fb8ba59167fe734d64128ca73d32c45cb8a117246d09c95c8769db88fe332b0a3396bedd0ea48ee42b0e5796fec0798ca5cb628a9a6de80d35d6c67b936ca1670347b3d4f2b769c8ce2ddcf959dbac6bcd88e6c08751ea1fffa0522de3507193e7035305a8aa008d6c88cca1341b3120fa9c347ab3f97e2d772e2709277da5")] +#else +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ClosedXML_Tests")] +#endif diff --git a/ClosedXML/Utils/ColorStringParser.cs b/ClosedXML/Utils/ColorStringParser.cs new file mode 100644 index 0000000..60203a1 --- /dev/null +++ b/ClosedXML/Utils/ColorStringParser.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using System.Globalization; + +namespace ClosedXML.Utils +{ + internal static class ColorStringParser + { + public static Color ParseFromHtml(string htmlColor) + { + try + { + return ColorTranslator.FromHtml(htmlColor); + } + catch + { + // https://github.com/ClosedXML/ClosedXML/issues/675 + // When regional settings list separator is # , the standard ColorTranslator.FromHtml fails + return Color.FromArgb(int.Parse(htmlColor.Replace("#", ""), NumberStyles.AllowHexSpecifier)); + } + } + } +} diff --git a/ClosedXML/Utils/CryptographicAlgorithms.cs b/ClosedXML/Utils/CryptographicAlgorithms.cs new file mode 100644 index 0000000..472d5de --- /dev/null +++ b/ClosedXML/Utils/CryptographicAlgorithms.cs @@ -0,0 +1,138 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML.Utils +{ + internal static class CryptographicAlgorithms + { + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + public static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static String GenerateNewSalt(Algorithm algorithm) + { + if (RequiresSalt(algorithm)) + return GetSalt(); + else + return String.Empty; + } + + public static String GetPasswordHash(Algorithm algorithm, String password, String salt = "", UInt32 spinCount = 0) + { + if (password == null) + throw new ArgumentNullException(nameof(password)); + + if (salt == null) + throw new ArgumentNullException(nameof(salt)); + + if ("" == password) return ""; + + switch (algorithm) + { + case Algorithm.SimpleHash: + return GetDefaultPasswordHash(password); + + case Algorithm.SHA512: + return GetSha512PasswordHash(password, salt, spinCount); + + default: + return string.Empty; + } + } + + public static string GetSalt(int length = 32) + { + using (var random = new RNGCryptoServiceProvider()) + { + var salt = new byte[length]; + random.GetNonZeroBytes(salt); + return Convert.ToBase64String(salt); + } + } + + public static Boolean RequiresSalt(Algorithm algorithm) + { + switch (algorithm) + { + case Algorithm.SimpleHash: + return false; + + case Algorithm.SHA512: + return true; + + default: + return false; + } + } + + private static String GetDefaultPasswordHash(String password) + { + if (password == null) + throw new ArgumentNullException(nameof(password)); + + // http://kohei.us/2008/01/18/excel-sheet-protection-password-hash/ + // http://sc.openoffice.org/excelfileformat.pdf - 4.18.4 + // http://web.archive.org/web/20080906232341/http://blogs.infosupport.com/wouterv/archive/2006/11/21/Hashing-password-for-use-in-SpreadsheetML.aspx + byte[] passwordCharacters = Encoding.ASCII.GetBytes(password); + int hash = 0; + if (passwordCharacters.Length > 0) + { + int charIndex = passwordCharacters.Length; + + while (charIndex-- > 0) + { + hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); + hash ^= passwordCharacters[charIndex]; + } + // Main difference from spec, also hash with charcount + hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); + hash ^= passwordCharacters.Length; + hash ^= (0x8000 | ('N' << 8) | 'K'); + } + + return Convert.ToString(hash, 16).ToUpperInvariant(); + } + + private static String GetSha512PasswordHash(String password, String salt, UInt32 spinCount) + { + if (password == null) + throw new ArgumentNullException(nameof(password)); + + if (salt == null) + throw new ArgumentNullException(nameof(salt)); + + var saltBytes = Convert.FromBase64String(salt); + var passwordBytes = Encoding.Unicode.GetBytes(password); + var bytes = saltBytes.Concat(passwordBytes).ToArray(); + + byte[] hashedBytes; + using (var hash = new SHA512Managed()) + { + hashedBytes = hash.ComputeHash(bytes); + + bytes = new byte[hashedBytes.Length + sizeof(uint)]; + for (uint i = 0; i < spinCount; i++) + { + var le = BitConverter.GetBytes(i); + Array.Copy(hashedBytes, bytes, hashedBytes.Length); + Array.Copy(le, 0, bytes, hashedBytes.Length, le.Length); + hashedBytes = hash.ComputeHash(bytes); + } + } + + return Convert.ToBase64String(hashedBytes); + } + } +} diff --git a/ClosedXML/Utils/DescribedEnumParser.cs b/ClosedXML/Utils/DescribedEnumParser.cs new file mode 100644 index 0000000..838a768 --- /dev/null +++ b/ClosedXML/Utils/DescribedEnumParser.cs @@ -0,0 +1,53 @@ +// Keep this file CodeMaid organised and cleaned +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace ClosedXML.Utils +{ + internal static class DescribedEnumParser + { + private static Lazy> fromDescriptions = new Lazy>(() => + { + return ParseEnumDescriptions().ToDictionary(a => a.Item2, a => a.Item1); + }); + + private static Lazy> toDescriptions = new Lazy>(() => + { + return ParseEnumDescriptions().ToDictionary(a => a.Item1, a => a.Item2); + }); + + public static T FromDescription(String value) + { + return fromDescriptions.Value[value]; + } + + public static Boolean IsValidDescription(String value) + { + return fromDescriptions.Value.ContainsKey(value); + } + + public static String ToDescription(T value) + { + return toDescriptions.Value[value]; + } + + private static IEnumerable> ParseEnumDescriptions() + { + var type = typeof(T); + return type.GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(f => + { + var attributes = f.GetCustomAttributes(typeof(DescriptionAttribute), inherit: false).OfType(); + var description = attributes.FirstOrDefault()?.Description ?? f.Name; + return new Tuple + ( + (T)Enum.Parse(type, f.Name), + description + ); + }); + } + } +} diff --git a/ClosedXML/Utils/GraphicsUtils.cs b/ClosedXML/Utils/GraphicsUtils.cs new file mode 100644 index 0000000..f0f7644 --- /dev/null +++ b/ClosedXML/Utils/GraphicsUtils.cs @@ -0,0 +1,31 @@ +using System; +using System.Drawing; + +namespace ClosedXML.Utils +{ + internal static class GraphicsUtils + { + [ThreadStatic] + private static Graphics threadLocalGraphics; + + internal static Graphics Graphics + { + get + { + if (threadLocalGraphics == null) + { + var image = new Bitmap(1, 1); + threadLocalGraphics = Graphics.FromImage(image); + } + return threadLocalGraphics; + } + } + + private static StringFormat defaultStringFormat = StringFormat.GenericTypographic; + public static SizeF MeasureString(string s, Font font) + { + SizeF result = Graphics.MeasureString(s, font, Int32.MaxValue, defaultStringFormat); + return result; + } + } +} diff --git a/ClosedXML/Utils/OpenXmlColorTypeAdapters.cs b/ClosedXML/Utils/OpenXmlColorTypeAdapters.cs new file mode 100644 index 0000000..3d22df4 --- /dev/null +++ b/ClosedXML/Utils/OpenXmlColorTypeAdapters.cs @@ -0,0 +1,56 @@ +// Keep this file CodeMaid organised and cleaned +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Spreadsheet; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; + +namespace ClosedXML.Utils +{ + internal interface IColorTypeAdapter + { + public BooleanValue Auto { get; set; } + public UInt32Value Indexed { get; set; } + public HexBinaryValue Rgb { get; set; } + public UInt32Value Theme { get; set; } + public DoubleValue Tint { get; set; } + } + + internal class ColorTypeAdapter : IColorTypeAdapter + { + public ColorTypeAdapter(ColorType colorType) + { + this.ColorType = colorType; + } + + #region ColorType + + public ColorType ColorType { get; } + + #endregion ColorType + + public BooleanValue Auto { get => ColorType.Auto; set => ColorType.Auto = value; } + public UInt32Value Indexed { get => ColorType.Indexed; set => ColorType.Indexed = value; } + public HexBinaryValue Rgb { get => ColorType.Rgb; set => ColorType.Rgb = value; } + public UInt32Value Theme { get => ColorType.Theme; set => ColorType.Theme = value; } + public DoubleValue Tint { get => ColorType.Tint; set => ColorType.Tint = value; } + } + + internal class X14ColorTypeAdapter : IColorTypeAdapter + { + public X14ColorTypeAdapter(X14.ColorType colorType) + { + this.ColorType = colorType; + } + + #region ColorType + + public X14.ColorType ColorType { get; } + + #endregion ColorType + + public BooleanValue Auto { get => ColorType.Auto; set => ColorType.Auto = value; } + public UInt32Value Indexed { get => ColorType.Indexed; set => ColorType.Indexed = value; } + public HexBinaryValue Rgb { get => ColorType.Rgb; set => ColorType.Rgb = value; } + public UInt32Value Theme { get => ColorType.Theme; set => ColorType.Theme = value; } + public DoubleValue Tint { get => ColorType.Tint; set => ColorType.Tint = value; } + } +} diff --git a/ClosedXML/Utils/OpenXmlHelper.cs b/ClosedXML/Utils/OpenXmlHelper.cs new file mode 100644 index 0000000..35cd2be --- /dev/null +++ b/ClosedXML/Utils/OpenXmlHelper.cs @@ -0,0 +1,161 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Collections.Generic; +using Drawing = System.Drawing; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; + +namespace ClosedXML.Utils +{ + internal static class OpenXmlHelper + { + #region Public Methods + + /// + /// Convert color in ClosedXML representation to specified OpenXML type. + /// + /// The descendant of . + /// The existing instance of ColorType. + /// Color in ClosedXML format. + /// Flag specifiying that the color should be saved in + /// differential format (affects the transparent color processing). + /// The original color in OpenXML format. + public static T FromClosedXMLColor(this ColorType openXMLColor, XLColor xlColor, bool isDifferential = false) + where T : ColorType + { + var adapter = new ColorTypeAdapter(openXMLColor); + FillFromClosedXMLColor(adapter, xlColor, isDifferential); + return (T)adapter.ColorType; + } + + /// + /// Convert color in ClosedXML representation to specified OpenXML type. + /// + /// The descendant of . + /// The existing instance of ColorType. + /// Color in ClosedXML format. + /// Flag specifiying that the color should be saved in + /// differential format (affects the transparent color processing). + /// The original color in OpenXML format. + public static T FromClosedXMLColor(this X14.ColorType openXMLColor, XLColor xlColor, bool isDifferential = false) + where T : X14.ColorType + { + var adapter = new X14ColorTypeAdapter(openXMLColor); + FillFromClosedXMLColor(adapter, xlColor, isDifferential); + return (T)adapter.ColorType; + } + + public static BooleanValue GetBooleanValue(bool value, bool defaultValue) + { + return value == defaultValue ? null : new BooleanValue(value); + } + + public static bool GetBooleanValueAsBool(BooleanValue value, bool defaultValue) + { + return (value?.HasValue ?? false) ? value.Value : defaultValue; + } + + /// + /// Convert color in OpenXML representation to ClosedXML type. + /// + /// Color in OpenXML format. + /// The dictionary containing parsed colors to optimize performance. + /// The color in ClosedXML format. + public static XLColor ToClosedXMLColor(this ColorType openXMLColor, IDictionary colorCache = null) + { + return ConvertToClosedXMLColor(new ColorTypeAdapter(openXMLColor), colorCache); + } + + /// + /// Convert color in OpenXML representation to ClosedXML type. + /// + /// Color in OpenXML format. + /// The dictionary containing parsed colors to optimize performance. + /// The color in ClosedXML format. + public static XLColor ToClosedXMLColor(this X14.ColorType openXMLColor, IDictionary colorCache = null) + { + return ConvertToClosedXMLColor(new X14ColorTypeAdapter(openXMLColor), colorCache); + } + + #endregion Public Methods + + #region Private Methods + + /// + /// Here we perform the actual convertion from OpenXML color to ClosedXML color. + /// + /// OpenXML color. Must be either or . + /// Since these types do not implement a common interface we use dynamic. + /// The dictionary containing parsed colors to optimize performance. + /// The color in ClosedXML format. + private static XLColor ConvertToClosedXMLColor(IColorTypeAdapter openXMLColor, IDictionary colorCache ) + { + XLColor retVal = null; + if (openXMLColor != null) + { + if (openXMLColor.Rgb != null) + { + String htmlColor = "#" + openXMLColor.Rgb.Value; + if (colorCache == null || !colorCache.TryGetValue(htmlColor, out Drawing.Color thisColor)) + { + thisColor = ColorStringParser.ParseFromHtml(htmlColor); + colorCache?.Add(htmlColor, thisColor); + } + + retVal = XLColor.FromColor(thisColor); + } + else if (openXMLColor.Indexed != null && openXMLColor.Indexed <= 64) + retVal = XLColor.FromIndex((Int32)openXMLColor.Indexed.Value); + else if (openXMLColor.Theme != null) + { + retVal = openXMLColor.Tint != null + ? XLColor.FromTheme((XLThemeColor)openXMLColor.Theme.Value, openXMLColor.Tint.Value) + : XLColor.FromTheme((XLThemeColor)openXMLColor.Theme.Value); + } + } + return retVal ?? XLColor.NoColor; + } + + /// + /// Initialize properties of the existing instance of the color in OpenXML format basing on properties of the color + /// in ClosedXML format. + /// + /// OpenXML color. Must be either or . + /// Since these types do not implement a common interface we use dynamic. + /// Color in ClosedXML format. + /// Flag specifiying that the color should be saved in + /// differential format (affects the transparent color processing). + private static void FillFromClosedXMLColor(IColorTypeAdapter openXMLColor, XLColor xlColor, bool isDifferential) + { + if (openXMLColor == null) + throw new ArgumentNullException(nameof(openXMLColor)); + + if (xlColor == null) + throw new ArgumentNullException(nameof(xlColor)); + + switch (xlColor.ColorType) + { + case XLColorType.Color: + openXMLColor.Rgb = xlColor.Color.ToHex(); + break; + + case XLColorType.Indexed: + // 64 is 'transparent' and should be ignored for differential formats + if (!isDifferential || xlColor.Indexed != 64) + openXMLColor.Indexed = (UInt32)xlColor.Indexed; + break; + + case XLColorType.Theme: + openXMLColor.Theme = (UInt32)xlColor.ThemeColor; + + if (xlColor.ThemeTint != 0) + openXMLColor.Tint = xlColor.ThemeTint; + break; + } + } + + #endregion Private Methods + } +} diff --git a/ClosedXML/Utils/XmlEncoder.cs b/ClosedXML/Utils/XmlEncoder.cs new file mode 100644 index 0000000..866852f --- /dev/null +++ b/ClosedXML/Utils/XmlEncoder.cs @@ -0,0 +1,47 @@ +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; + +namespace ClosedXML.Utils +{ + public static class XmlEncoder + { + private static readonly Regex xHHHHRegex = new Regex("_(x[\\dA-Fa-f]{4})_", RegexOptions.Compiled); + private static readonly Regex Uppercase_X_HHHHRegex = new Regex("_(X[\\dA-Fa-f]{4})_", RegexOptions.Compiled); + + public static string EncodeString(string encodeStr) + { + if (encodeStr == null) return null; + + encodeStr = xHHHHRegex.Replace(encodeStr, "_x005F_$1_"); + + var sb = new StringBuilder(encodeStr.Length); + + foreach (var ch in encodeStr) + { + if (XmlConvert.IsXmlChar(ch)) + { + sb.Append(ch); + } + else + { + sb.Append(XmlConvert.EncodeName(ch.ToString())); + } + } + + return sb.ToString(); + } + + public static string DecodeString(string decodeStr) + { + if (string.IsNullOrEmpty(decodeStr)) return string.Empty; + + // Strings "escaped" with _X (capital X) should not be treated as escaped + // Example: _Xceed_Something + // https://github.com/ClosedXML/ClosedXML/issues/1154 + decodeStr = Uppercase_X_HHHHRegex.Replace(decodeStr, "_x005F_$1_"); + + return XmlConvert.DecodeName(decodeStr); + } + } +} diff --git a/ClosedXML/XLHelper.cs b/ClosedXML/XLHelper.cs new file mode 100644 index 0000000..aa55a6c --- /dev/null +++ b/ClosedXML/XLHelper.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel +{ + /// + /// Common methods + /// + public static class XLHelper + { + public const int MinRowNumber = 1; + public const int MinColumnNumber = 1; + public const int MaxRowNumber = 1048576; + public const int MaxColumnNumber = 16384; + public const String MaxColumnLetter = "XFD"; + public const Double Epsilon = 1e-10; + + public static String LastCell { get { return $"{MaxColumnLetter}{MaxRowNumber}"; } } + + private static readonly Lazy graphics = new Lazy(() => Graphics.FromImage(new Bitmap(200, 200))); + internal static Graphics Graphics { get => graphics.Value; } + internal static Double DpiX { get => Graphics.DpiX; } + + internal static readonly NumberStyles NumberStyle = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowExponent; + internal static readonly CultureInfo ParseCulture = CultureInfo.InvariantCulture; + + internal static readonly Regex RCSimpleRegex = new Regex( + @"^(r(((-\d)?\d*)|\[(-\d)?\d*\]))?(c(((-\d)?\d*)|\[(-\d)?\d*\]))?$" + , RegexOptions.IgnoreCase | RegexOptions.Compiled); + + internal static readonly Regex A1SimpleRegex = new Regex( + @"\A" + + @"(?" // Start Group to pick + + @"(?" // Start Sheet Name, optional + + @"(" + + @"\'([^\[\]\*/\\\?:\']+|\'\')\'" + // Sheet name with special characters, surrounding apostrophes are required + + @"|" + + @"\'?\w+\'?" // Sheet name with letters and numbers, surrounding apostrophes are optional + + @")" + + @"!)?" // End Sheet Name, optional + + @"(?" // Start range + + @"\$?[a-zA-Z]{1,3}\$?\d{1,7}" // A1 Address 1 + + @"(?:\$?[a-zA-Z]{1,3}\$?\d{1,7})?" // A1 Address 2, optional + + @"|" + + @"(?\$?\d{1,7}:\$?\d{1,7})" // 1:1 + + @"|" + + @"(?\$?[a-zA-Z]{1,3}:\$?[a-zA-Z]{1,3})" // A:A + + @")" // End Range + + @")" // End Group to pick + + @"\Z" + , RegexOptions.Compiled); + + internal static readonly Regex NamedRangeReferenceRegex = + new Regex(@"^('?(?[^'!]+)'?!(?.+))|((?[^\[]+)\[(?[^\]]+)\])$", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture + ); + + private static readonly string[] letters = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; + + private static readonly string[] allLetters; + private static readonly Dictionary letterIndexes; + + static XLHelper() + { + allLetters = new string[XLHelper.MaxColumnNumber]; + letterIndexes = new Dictionary(XLHelper.MaxColumnNumber, StringComparer.Create(ParseCulture, true)); + for (int i = 0; i < XLHelper.MaxColumnNumber; i++) + { + string letter; + if (i < 26) + letter = letters[i]; + else if (i < 26 * 27) + letter = letters[i / 26 - 1] + letters[i % 26]; + else + letter = letters[(i - 26) / 26 / 26 - 1] + letters[(i / 26 - 1) % 26] + letters[i % 26]; + allLetters[i] = letter; + letterIndexes.Add(letter, i + 1); + } + } + + /// + /// Gets the column number of a given column letter. + /// + /// The column letter to translate into a column number. + public static int GetColumnNumberFromLetter(string columnLetter) + { + if (string.IsNullOrEmpty(columnLetter)) throw new ArgumentNullException("columnLetter"); + + //Extra check because we allow users to pass row col positions in as strings + if (columnLetter[0] <= '9') + { + return Int32.Parse(columnLetter, XLHelper.NumberStyle, XLHelper.ParseCulture); + } + + if (letterIndexes.TryGetValue(columnLetter, out var retVal)) + return retVal; + + throw new ArgumentOutOfRangeException(columnLetter + " is not recognized as a column letter"); + } + + /// + /// Gets the column letter of a given column number. + /// + /// The column number to translate into a column letter. + /// if set to true the column letter will be restricted to the allowed range. + /// + public static string GetColumnLetterFromNumber(int columnNumber, bool trimToAllowed = false) + { + if (trimToAllowed) columnNumber = TrimColumnNumber(columnNumber); + + if (columnNumber <= 0 || columnNumber > allLetters.Length) + throw new ArgumentOutOfRangeException(nameof(columnNumber)); + + // Adjust for start on column 1 + return allLetters[columnNumber - 1]; + } + + internal static int TrimColumnNumber(int columnNumber) + { + return Math.Max(XLHelper.MinColumnNumber, Math.Min(XLHelper.MaxColumnNumber, columnNumber)); + } + + internal static int TrimRowNumber(int rowNumber) + { + return Math.Max(XLHelper.MinRowNumber, Math.Min(XLHelper.MaxRowNumber, rowNumber)); + } + + public static bool IsValidColumn(string column) + { + if (String.IsNullOrWhiteSpace(column)) + return false; + var length = column.Length; + if (length > 3) + return false; + + var theColumn = column.ToUpper(); + + var isValid = theColumn[0] >= 'A' && theColumn[0] <= 'Z'; + if (length == 1) + return isValid; + + if (length == 2) + return isValid && theColumn[1] >= 'A' && theColumn[1] <= 'Z'; + + if (theColumn[0] >= 'A' && theColumn[0] < 'X') + return theColumn[1] >= 'A' && theColumn[1] <= 'Z' + && theColumn[2] >= 'A' && theColumn[2] <= 'Z'; + + if (theColumn[0] != 'X') return false; + + if (theColumn[1] < 'F') + return theColumn[2] >= 'A' && theColumn[2] <= 'Z'; + + if (theColumn[1] != 'F') return false; + + return theColumn[2] >= 'A' && theColumn[2] <= 'D'; + } + + public static bool IsValidRow(string rowString) + { + if (Int32.TryParse(rowString, out int row)) + return row > 0 && row <= MaxRowNumber; + return false; + } + + public static bool IsValidA1Address(string address) + { + if (String.IsNullOrWhiteSpace(address)) + return false; + + address = address.Replace("$", ""); + var rowPos = 0; + var addressLength = address.Length; + while (rowPos < addressLength && (address[rowPos] > '9' || address[rowPos] < '0')) + rowPos++; + + return + rowPos < addressLength + && IsValidRow(address.Substring(rowPos)) + && IsValidColumn(address.Substring(0, rowPos)); + } + + public static bool IsValidRCAddress(string address) + { + if (String.IsNullOrWhiteSpace(address)) + return false; + + return RCSimpleRegex.IsMatch(address); + } + + public static Boolean IsValidRangeAddress(String rangeAddress) + { + return A1SimpleRegex.IsMatch(rangeAddress); + } + + public static Boolean IsValidRangeAddress(IXLRangeAddress rangeAddress) + { + return rangeAddress.IsValid + && rangeAddress.FirstAddress.RowNumber >= 1 && rangeAddress.LastAddress.RowNumber <= MaxRowNumber + && rangeAddress.FirstAddress.ColumnNumber >= 1 && rangeAddress.LastAddress.ColumnNumber <= MaxColumnNumber + && rangeAddress.FirstAddress.RowNumber <= rangeAddress.LastAddress.RowNumber + && rangeAddress.FirstAddress.ColumnNumber <= rangeAddress.LastAddress.ColumnNumber; + } + + public static int GetColumnNumberFromAddress(string cellAddressString) + { + var rowPos = 0; + while (cellAddressString[rowPos] > '9') + rowPos++; + + return GetColumnNumberFromLetter(cellAddressString.Substring(0, rowPos)); + } + + internal static string[] SplitRange(string range) + { + return range.Contains('-') ? range.Replace('-', ':').Split(':') : range.Split(':'); + } + + public static Int32 GetPtFromPx(Double px) + { + return Convert.ToInt32(px * 72.0 / DpiX); + } + + public static Double GetPxFromPt(Int32 pt) + { + return Convert.ToDouble(pt) * DpiX / 72.0; + } + + internal static IXLTableRows InsertRowsWithoutEvents(Func insertFunc, + XLTableRange tableRange, Int32 numberOfRows, + Boolean expandTable) + { + var ws = tableRange.Worksheet; + var tracking = ws.EventTrackingEnabled; + ws.EventTrackingEnabled = false; + + var rows = new XLTableRows(ws.Style); + var inserted = insertFunc(numberOfRows, false); + inserted.ForEach(r => rows.Add(new XLTableRow(tableRange, r as XLRangeRow))); + + if (expandTable) + tableRange.Table.ExpandTableRows(numberOfRows); + + ws.EventTrackingEnabled = tracking; + + return rows; + } + +#if false +// Not using this anymore, but keeping it around for in case we bring back .NET3.5 support. + public static bool IsNullOrWhiteSpace(string value) + { +#if _NET35_ + if (value == null) return true; + return value.All(c => char.IsWhiteSpace(c)); +#else + return String.IsNullOrWhiteSpace(value); +#endif + } +#endif + + private static readonly Regex A1RegexRelative = new Regex( + @"(?<=\W)(?\$?[a-zA-Z]{1,3}\$?\d{1,7})(?=\W)" // A1 + + @"|(?<=\W)(?\$?\d{1,7}:\$?\d{1,7})(?=\W)" // 1:1 + + @"|(?<=\W)(?\$?[a-zA-Z]{1,3}:\$?[a-zA-Z]{1,3})(?=\W)", RegexOptions.Compiled); // A:A + + private static string Evaluator(Match match, Int32 row, String column) + { + if (match.Groups["one"].Success) + { + var split = match.Groups["one"].Value.Split('$'); + if (split.Length == 1) return column + row; // A1 + if (split.Length == 3) return match.Groups["one"].Value; // $A$1 + var a = XLAddress.Create(match.Groups["one"].Value); + if (split[0].Length == 0) return "$" + a.ColumnLetter + row; // $A1 + return column + "$" + a.RowNumber; + } + + if (match.Groups["two"].Success) + return ReplaceGroup(match.Groups["two"].Value, row.ToString()); + + return ReplaceGroup(match.Groups["three"].Value, column); + } + + private static String ReplaceGroup(String value, String item) + { + var split = value.Split(':'); + String ret1 = split[0].StartsWith("$") ? split[0] : item; + String ret2 = split[1].StartsWith("$") ? split[1] : item; + return ret1 + ":" + ret2; + } + + internal static String ReplaceRelative(String value, Int32 row, String column) + { + var oldValue = ">" + value + "<"; + var newVal = A1RegexRelative.Replace(oldValue, m => Evaluator(m, row, column)); + return newVal.Substring(1, newVal.Length - 2); + } + + public static Boolean AreEqual(Double d1, Double d2) + { + return Math.Abs(d1 - d2) < Epsilon; + } + + public static DateTime GetDate(Object v) + { + // handle dates + if (v is DateTime) + { + return (DateTime)v; + } + + // handle doubles + if (v is double && ((double)v).IsValidOADateNumber()) + { + return DateTime.FromOADate((double)v); + } + + // handle everything else + return (DateTime)Convert.ChangeType(v, typeof(DateTime)); + } + + internal static bool IsValidOADateNumber(this double d) + { + return -657435 <= d && d < 2958466; + } + + /// + /// A backward compatible version of that returns a value + /// rounded to milliseconds. In .Net Core 3.0 the behavior has changed and timespan includes microseconds + /// as well. As a result, the value 1:12:30 saved on one machine could become 1:12:29.999999 on another. + /// + internal static TimeSpan GetTimeSpan(double totalDays) + { + var timeSpan = TimeSpan.FromDays(totalDays); + var roundedMilliseconds = Math.Round(timeSpan.TotalMilliseconds); + return TimeSpan.FromMilliseconds(roundedMilliseconds); + } + + public static Boolean ValidateName(String objectType, String newName, String oldName, IEnumerable existingNames, out String message) + { + message = ""; + if (String.IsNullOrWhiteSpace(newName)) + { + message = $"The {objectType} name '{newName}' is invalid"; + return false; + } + + // Table names are case insensitive + if (!oldName.Equals(newName, StringComparison.OrdinalIgnoreCase) + && existingNames.Contains(newName, StringComparer.OrdinalIgnoreCase)) + { + message = $"There is already a {objectType} named '{newName}'"; + return false; + } + + var allowedFirstCharacters = new[] { '_', '\\' }; + if (!allowedFirstCharacters.Contains(newName[0]) && !char.IsLetter(newName[0])) + { + message = $"The {objectType} name '{newName}' does not begin with a letter, an underscore or a backslash."; + return false; + } + + if (newName.Length > 255) + { + message = $"The {objectType} name is more than 255 characters"; + return false; + } + + if (new[] { 'C', 'R' }.Any(c => newName.ToUpper().Equals(c.ToString()))) + { + message = $"The {objectType} name '{newName}' is invalid"; + return false; + } + + return true; + } + } +} diff --git a/ClosedXML_Examples/AutoFilters/CustomAutoFilter.cs b/ClosedXML_Examples/AutoFilters/CustomAutoFilter.cs new file mode 100644 index 0000000..d173415 --- /dev/null +++ b/ClosedXML_Examples/AutoFilters/CustomAutoFilter.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class CustomAutoFilter : IXLExample + { + public void Create(string filePath) + { + var wb = new XLWorkbook(); + IXLWorksheet ws; + + #region Single Column Numbers + String singleColumnNumbers = "Single Column Numbers"; + ws = wb.Worksheets.Add(singleColumnNumbers); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).EqualTo(3).Or.GreaterThan(4); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + #endregion + + #region Single Column Strings + String singleColumnStrings = "Single Column Strings"; + ws = wb.Worksheets.Add(singleColumnStrings); + + // Add a bunch of strings to filter + ws.Cell("A1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).Between("B", "D"); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + #endregion + + #region Single Column Mixed + String singleColumnMixed = "Single Column Mixed"; + ws = wb.Worksheets.Add(singleColumnMixed); + + // Add a bunch of items to filter + ws.Cell("A1").SetValue("Mixed") + .CellBelow().SetValue("B") + .CellBelow().SetValue(3) + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).EqualTo(3).Or.EqualTo("C"); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + #endregion + + #region Multi Column + String multiColumn = "Multi Column"; + ws = wb.Worksheets.Add(multiColumn); + + ws.Cell("A1").SetValue("First") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + ws.Cell("B1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + ws.Cell("C1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(2).EqualTo(3).Or.GreaterThan(4); + ws.RangeUsed().SetAutoFilter().Column(3).Between("B", "D"); + + // Sort the filtered list + ws.AutoFilter.Sort(3); + #endregion + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms); + + var workbook = new XLWorkbook(ms); + + #region Single Column Numbers + workbook.Worksheet(singleColumnNumbers).AutoFilter.Sort(1, XLSortOrder.Descending); + #endregion + + #region Single Column Strings + workbook.Worksheet(singleColumnStrings).AutoFilter.Sort(1, XLSortOrder.Descending); + #endregion + + #region Single Column Mixed + workbook.Worksheet(singleColumnMixed).AutoFilter.Column(1).EqualOrGreaterThan("D"); + workbook.Worksheet(singleColumnMixed).AutoFilter.Sort(1, XLSortOrder.Descending); + #endregion + + #region Multi Column + workbook.Worksheet(multiColumn).AutoFilter.Column(3).EqualTo("E"); + workbook.Worksheet(multiColumn).AutoFilter.Sort(3, XLSortOrder.Descending); + #endregion + + workbook.SaveAs(filePath); + ms.Close(); + } + } + } +} diff --git a/ClosedXML_Examples/AutoFilters/DateTimeGroupAutoFilter.cs b/ClosedXML_Examples/AutoFilters/DateTimeGroupAutoFilter.cs new file mode 100644 index 0000000..68905d2 --- /dev/null +++ b/ClosedXML_Examples/AutoFilters/DateTimeGroupAutoFilter.cs @@ -0,0 +1,43 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples +{ + public class DateTimeGroupAutoFilter : IXLExample + { + public void Create(string filePath) + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws; + + #region Single Column Dates + + String singleColumnDates = "Single Column Dates"; + ws = wb.Worksheets.Add(singleColumnDates); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Dates") + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(2)) + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(3)) + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(3)) + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(5)) + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(1)) + .CellBelow().SetValue(new DateTime(2018, 1, 1).AddDays(4)); + + ws.Column(1).Style.NumberFormat.Format = "d MMMM yyyy"; + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).AddDateGroupFilter(new DateTime(2018, 1, 1).AddDays(3), XLDateTimeGrouping.Day); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + + #endregion Single Column Dates + + ws.Columns().AdjustToContents(); + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/AutoFilters/DynamicAutoFilter.cs b/ClosedXML_Examples/AutoFilters/DynamicAutoFilter.cs new file mode 100644 index 0000000..543e14b --- /dev/null +++ b/ClosedXML_Examples/AutoFilters/DynamicAutoFilter.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class DynamicAutoFilter : IXLExample + { + public void Create(string filePath) + { + var wb = new XLWorkbook(); + IXLWorksheet ws; + + #region Single Column Numbers + String singleColumnNumbers = "Single Column Numbers"; + ws = wb.Worksheets.Add(singleColumnNumbers); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).AboveAverage(); + + // Sort the filtered list + //ws.AutoFilter.Sort(1); + #endregion + + #region Multi Column + String multiColumn = "Multi Column"; + ws = wb.Worksheets.Add(multiColumn); + + ws.Cell("A1").SetValue("First") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + ws.Cell("B1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + ws.Cell("C1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(2).BelowAverage(); + + // Sort the filtered list + //ws.AutoFilter.Sort(3); + #endregion + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms); + + var workbook = new XLWorkbook(ms); + + #region Single Column Numbers + //workbook.Worksheet(singleColumnNumbers).AutoFilter.Sort(1, XLSortOrder.Descending); + #endregion + + #region Multi Column + //workbook.Worksheet(multiColumn).AutoFilter.Sort(3, XLSortOrder.Descending); + #endregion + + workbook.SaveAs(filePath); + ms.Close(); + } + } + } +} diff --git a/ClosedXML_Examples/AutoFilters/RegularAutoFilter.cs b/ClosedXML_Examples/AutoFilters/RegularAutoFilter.cs new file mode 100644 index 0000000..9619ae2 --- /dev/null +++ b/ClosedXML_Examples/AutoFilters/RegularAutoFilter.cs @@ -0,0 +1,205 @@ +using ClosedXML.Excel; +using System; +using System.IO; + +namespace ClosedXML_Examples +{ + public class RegularAutoFilter : IXLExample + { + public void Create(string filePath) + { + var wb = new XLWorkbook(); + IXLWorksheet ws; + + #region Single Column Numbers + + String singleColumnNumbers = "Single Column Numbers"; + ws = wb.Worksheets.Add(singleColumnNumbers); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4) + .CellBelow().SetValue(5); + + ws.Cell("B1").SetValue("Names") + .CellBelow().SetValue("John") + .CellBelow().SetValue("Jack") + .CellBelow().SetValue("Neil") + .CellBelow().SetValue("Alex") + .CellBelow().SetValue("Jason") + .CellBelow().SetValue("Patrick") + .CellBelow().SetValue("Jacques"); + + // Add filters + var autoFilter = ws.RangeUsed().SetAutoFilter(); + autoFilter.Column(1).AddFilter(3) + .AddFilter(1); + + autoFilter.Column(2).BeginsWith("J"); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + + #endregion Single Column Numbers + + #region Single Column Strings + + String singleColumnStrings = "Single Column Strings"; + ws = wb.Worksheets.Add(singleColumnStrings); + + // Add a bunch of strings to filter + ws.Cell("A1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).AddFilter("C") + .AddFilter("A"); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + + #endregion Single Column Strings + + #region Single Column Mixed + + String singleColumnMixed = "Single Column Mixed"; + ws = wb.Worksheets.Add(singleColumnMixed); + + // Add a bunch of items to filter + ws.Cell("A1").SetValue("Mixed") + .CellBelow().SetValue("B") + .CellBelow().SetValue(3) + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).AddFilter("C") + .AddFilter(1); + + // Sort the filtered list + ws.AutoFilter.Sort(1); + + #endregion Single Column Mixed + + #region Multi Column + + String multiColumn = "Multi Column"; + ws = wb.Worksheets.Add(multiColumn); + + ws.Cell("A1").SetValue("First") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + ws.Cell("B1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + ws.Cell("C1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(2).AddFilter(3) + .AddFilter(1); + + // Sort the filtered list + ws.AutoFilter.Sort(3); + + #endregion Multi Column + + #region Table + + String tableSheetName = "Table"; + ws = wb.Worksheets.Add(tableSheetName); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + var table = ws.RangeUsed().CreateTable(); + table.ShowTotalsRow = true; + table.Field(0).TotalsRowFunction = XLTotalsRowFunction.Sum; + table.AutoFilter.Column(1).AddFilter(3).AddFilter(4); + + table.AutoFilter.Sort(1); + + #endregion Table + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms); + + var workbook = new XLWorkbook(ms); + + #region Single Column Numbers + + workbook.Worksheet(singleColumnNumbers).AutoFilter.Column(1).AddFilter(5); + workbook.Worksheet(singleColumnNumbers).AutoFilter.Sort(1, XLSortOrder.Descending); + + #endregion Single Column Numbers + + #region Single Column Strings + + workbook.Worksheet(singleColumnStrings).AutoFilter.Column(1).AddFilter("E"); + workbook.Worksheet(singleColumnStrings).AutoFilter.Sort(1, XLSortOrder.Descending); + + #endregion Single Column Strings + + #region Single Column Mixed + + workbook.Worksheet(singleColumnMixed).AutoFilter.Column(1).AddFilter("E"); + workbook.Worksheet(singleColumnMixed).AutoFilter.Column(1).AddFilter(3); + workbook.Worksheet(singleColumnMixed).AutoFilter.Sort(1, XLSortOrder.Descending); + + #endregion Single Column Mixed + + #region Multi Column + + workbook.Worksheet(multiColumn).AutoFilter.Column(3).AddFilter("C"); + workbook.Worksheet(multiColumn).AutoFilter.Sort(3, XLSortOrder.Descending); + + #endregion Multi Column + + #region Table + + workbook.Worksheet(tableSheetName).Table(0).AutoFilter.Column(1).AddFilter(5); + workbook.Worksheet(tableSheetName).Table(0).AutoFilter.Sort(1, XLSortOrder.Descending); + + #endregion Table + + workbook.SaveAs(filePath); + ms.Close(); + } + } + } +} diff --git a/ClosedXML_Examples/AutoFilters/TopBottomAutoFilter.cs b/ClosedXML_Examples/AutoFilters/TopBottomAutoFilter.cs new file mode 100644 index 0000000..7d3b0b7 --- /dev/null +++ b/ClosedXML_Examples/AutoFilters/TopBottomAutoFilter.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class TopBottomAutoFilter : IXLExample + { + public void Create(string filePath) + { + var wb = new XLWorkbook(); + IXLWorksheet ws; + + #region Single Column Numbers + String singleColumnNumbers = "Single Column Numbers"; + ws = wb.Worksheets.Add(singleColumnNumbers); + + // Add a bunch of numbers to filter + ws.Cell("A1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(1).Top(2); + + // Sort the filtered list + //ws.AutoFilter.Sort(1); + #endregion + + #region Multi Column + String multiColumn = "Multi Column"; + ws = wb.Worksheets.Add(multiColumn); + + ws.Cell("A1").SetValue("First") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + ws.Cell("B1").SetValue("Numbers") + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(3) + .CellBelow().SetValue(5) + .CellBelow().SetValue(1) + .CellBelow().SetValue(4); + + ws.Cell("C1").SetValue("Strings") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C") + .CellBelow().SetValue("C") + .CellBelow().SetValue("E") + .CellBelow().SetValue("A") + .CellBelow().SetValue("D"); + + // Add filters + ws.RangeUsed().SetAutoFilter().Column(2).Bottom(50, XLTopBottomType.Percent); + + // Sort the filtered list + //ws.AutoFilter.Sort(3); + #endregion + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms); + + var workbook = new XLWorkbook(ms); + + #region Single Column Numbers + //workbook.Worksheet(singleColumnNumbers).AutoFilter.Sort(1, XLSortOrder.Descending); + #endregion + + #region Multi Column + //workbook.Worksheet(multiColumn).AutoFilter.Sort(3, XLSortOrder.Descending); + #endregion + + workbook.SaveAs(filePath); + ms.Close(); + } + } + } +} diff --git a/ClosedXML_Examples/BasicTable.cs b/ClosedXML_Examples/BasicTable.cs new file mode 100644 index 0000000..083e890 --- /dev/null +++ b/ClosedXML_Examples/BasicTable.cs @@ -0,0 +1,93 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples +{ + public class BasicTable : IXLExample + { + public void Create(string filePath) + { + // Creating a new workbook + var wb = new XLWorkbook(); + + //Adding a worksheet + var ws = wb.Worksheets.Add("Contacts"); + + //Adding text + //Title + ws.Cell("B2").Value = "Contacts"; + //First Names + ws.Cell("B3").Value = "FName"; + ws.Cell("B4").Value = "John"; + ws.Cell("B5").Value = "Hank"; + ws.Cell("B6").Value = "Dagny"; + //Last Names + ws.Cell("C3").Value = "LName"; + ws.Cell("C4").Value = "Galt"; + ws.Cell("C5").Value = "Rearden"; + ws.Cell("C6").Value = "Taggart"; + + //Adding more data types + //Is an outcast? + ws.Cell("D3").Value = "Outcast"; + ws.Cell("D4").Value = true; + ws.Cell("D5").Value = false; + ws.Cell("D6").Value = false; + //Date of Birth + ws.Cell("E3").Value = "DOB"; + ws.Cell("E4").Value = new DateTime(1919, 1, 21); + ws.Cell("E5").Value = new DateTime(1907, 3, 4); + ws.Cell("E6").Value = new DateTime(1921, 12, 15); + //Income + ws.Cell("F3").Value = "Income"; + ws.Cell("F4").Value = 2000; + ws.Cell("F5").Value = 40000; + ws.Cell("F6").Value = 10000; + + //Defining ranges + //From worksheet + var rngTable = ws.Range("B2:F6"); + //From another range + var rngDates = rngTable.Range("E4:E6"); + var rngNumbers = rngTable.Range("F4:F6"); + + //Formatting dates and numbers + //Using a OpenXML's predefined formats + rngDates.Style.NumberFormat.NumberFormatId = 15; + //Using a custom format + rngNumbers.Style.NumberFormat.Format = "$ #,##0"; + + //Formatting headers + var rngHeaders = rngTable.Range("B3:F3"); + rngHeaders.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + rngHeaders.Style.Font.Bold = true; + rngHeaders.Style.Fill.BackgroundColor = XLColor.Aqua; + + //Adding grid lines + rngTable.Style.Border.BottomBorder = XLBorderStyleValues.Thin; + + //Format title cell + rngTable.Cell(1, 1).Style.Font.Bold = true; + rngTable.Cell(1, 1).Style.Fill.BackgroundColor = XLColor.CornflowerBlue; + rngTable.Cell(1, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + + //Merge title cells + rngTable.Row(1).Merge(); // We could've also used: rngTable.Range("A1:E1").Merge() + + //Add thick borders + rngTable.Style.Border.OutsideBorder = XLBorderStyleValues.Thick; + + // You can also specify the border for each side with: + // rngTable.FirstColumn().Style.Border.LeftBorder = XLBorderStyleValues.Thick; + // rngTable.LastColumn().Style.Border.RightBorder = XLBorderStyleValues.Thick; + // rngTable.FirstRow().Style.Border.TopBorder = XLBorderStyleValues.Thick; + // rngTable.LastRow().Style.Border.BottomBorder = XLBorderStyleValues.Thick; + + // Adjust column widths to their content + ws.Columns(2, 6).AdjustToContents(); + + //Saving the workbook + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/ClosedXML.snk b/ClosedXML_Examples/ClosedXML.snk new file mode 100644 index 0000000..a17cff7 Binary files /dev/null and b/ClosedXML_Examples/ClosedXML.snk differ diff --git a/ClosedXML_Examples/ClosedXML_Examples.csproj b/ClosedXML_Examples/ClosedXML_Examples.csproj new file mode 100644 index 0000000..db5ad81 --- /dev/null +++ b/ClosedXML_Examples/ClosedXML_Examples.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.0;net40;net46 + 8.0 + Exe + 0.95.4 + $(NoWarn);NU1605 + Debug;Release;Release.Signed + + + + bin\Release.Signed\ + true + ClosedXML.snk + $(DefineConstants);STRONGNAME + + + + $(DefineConstants);_NETSTANDARD_;_NETSTANDARD2_0_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET40_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET46_ + + + + + + + + + + + + + + + + + + diff --git a/ClosedXML_Examples/Columns/ColumnCells.cs b/ClosedXML_Examples/Columns/ColumnCells.cs new file mode 100644 index 0000000..3c26137 --- /dev/null +++ b/ClosedXML_Examples/Columns/ColumnCells.cs @@ -0,0 +1,32 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples +{ + public class ColumnCells : IXLExample + { + + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Column Cells"); + + var columnFromWorksheet = ws.Column(1); + columnFromWorksheet.Cell(1).Style.Fill.BackgroundColor = XLColor.Red; + columnFromWorksheet.Cells("2").Style.Fill.BackgroundColor = XLColor.Blue; + columnFromWorksheet.Cells("3,5:6").Style.Fill.BackgroundColor = XLColor.Red; + columnFromWorksheet.Cells(8, 9).Style.Fill.BackgroundColor = XLColor.Blue; + + var columnFromRange = ws.Range("B1:B9").FirstColumn(); + + columnFromRange.Cell(1).Style.Fill.BackgroundColor = XLColor.Red; + columnFromRange.Cells("2").Style.Fill.BackgroundColor = XLColor.Blue; + columnFromRange.Cells("3,5:6").Style.Fill.BackgroundColor = XLColor.Red; + columnFromRange.Cells(8, 9).Style.Fill.BackgroundColor = XLColor.Blue; + + workbook.SaveAs(filePath); + } + + } +} diff --git a/ClosedXML_Examples/Columns/ColumnCollections.cs b/ClosedXML_Examples/Columns/ColumnCollections.cs new file mode 100644 index 0000000..33395ad --- /dev/null +++ b/ClosedXML_Examples/Columns/ColumnCollections.cs @@ -0,0 +1,108 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Columns +{ + public class ColumnCollection : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Columns of a Range"); + + // All columns in a range + ws.Range("A1:B2").Columns().Style.Fill.BackgroundColor = XLColor.DimGray; + + var bigRange = ws.Range("A4:V6"); + + // Contiguous columns by number + bigRange.Columns(1, 2).Style.Fill.BackgroundColor = XLColor.Red; + + // Contiguous columns by letter + bigRange.Columns("D", "E").Style.Fill.BackgroundColor = XLColor.Blue; + + // Contiguous columns by letter + bigRange.Columns("G:H").Style.Fill.BackgroundColor = XLColor.DeepPink; + + // Spread columns by number + bigRange.Columns("10:11,13:14").Style.Fill.BackgroundColor = XLColor.Orange; + + // Spread columns by letter + bigRange.Columns("P:Q,S:T").Style.Fill.BackgroundColor = XLColor.Turquoise; + + // Use a single number/letter + bigRange.Columns("V").Style.Fill.BackgroundColor = XLColor.Cyan; + + // Adjust the width + ws.Columns("A:V").Width = 3; + + var ws2 = workbook.Worksheets.Add("Columns of a worksheet"); + + // Contiguous columns by number + ws2.Columns(1, 2).Style.Fill.BackgroundColor = XLColor.Red; + + // Contiguous columns by letter + ws2.Columns("D", "E").Style.Fill.BackgroundColor = XLColor.Blue; + + // Contiguous columns by letter + ws2.Columns("G:H").Style.Fill.BackgroundColor = XLColor.DeepPink; + + // Spread columns by number + ws2.Columns("10:11,13:14").Style.Fill.BackgroundColor = XLColor.Orange; + + // Spread columns by letter + ws2.Columns("P:Q,S:T").Style.Fill.BackgroundColor = XLColor.Turquoise; + + // Use a single number/letter + ws2.Columns("V").Style.Fill.BackgroundColor = XLColor.Cyan; + + // Adjust the width + ws2.Columns("A:V").Width = 3; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Columns/ColumnSettings.cs b/ClosedXML_Examples/Columns/ColumnSettings.cs new file mode 100644 index 0000000..289d5ff --- /dev/null +++ b/ClosedXML_Examples/Columns/ColumnSettings.cs @@ -0,0 +1,80 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Columns +{ + public class ColumnSettings : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + public ColumnSettings() + { + + } + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Column Settings"); + + var col1 = ws.Column("B"); + col1.Style.Fill.BackgroundColor = XLColor.Red; + col1.Width = 20; + + var col2 = ws.Column(4); + col2.Style.Fill.BackgroundColor = XLColor.DarkOrange; + col2.Width = 5; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Columns/DeletingColumns.cs b/ClosedXML_Examples/Columns/DeletingColumns.cs new file mode 100644 index 0000000..c4f2e84 --- /dev/null +++ b/ClosedXML_Examples/Columns/DeletingColumns.cs @@ -0,0 +1,75 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples +{ + public class DeletingColumns : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Deleting Columns"); + + var rngTitles = ws.Range("B2:D2"); + ws.Row(1).InsertRowsBelow(2); + + var rng1 = ws.Range("B2:D2"); + var rng2 = ws.Range("F2:G2"); + var rng3 = ws.Range("A1:A3"); + var col1 = ws.Column(1); + + rng1.Style.Fill.BackgroundColor = XLColor.Orange; + rng2.Style.Fill.BackgroundColor = XLColor.Blue; + rng3.Style.Fill.BackgroundColor = XLColor.Red; + col1.Style.Fill.BackgroundColor = XLColor.Black; + + ws.Columns("A,C,E:H").Delete(); + ws.Cell("A2").Value = "OK"; + ws.Cell("B2").Value = "OK"; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Columns/InsertColumns.cs b/ClosedXML_Examples/Columns/InsertColumns.cs new file mode 100644 index 0000000..e908c01 --- /dev/null +++ b/ClosedXML_Examples/Columns/InsertColumns.cs @@ -0,0 +1,40 @@ +using ClosedXML.Excel; +using System; +using System.Linq; + +namespace ClosedXML_Examples.Columns +{ + public class InsertColumns : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Inserting Columns"); + + // Color the entire spreadsheet using columns + ws.Columns().Style.Fill.BackgroundColor = XLColor.LightCyan; + + // Put a value in a few cells + foreach (var r in Enumerable.Range(1, 5)) + foreach (var c in Enumerable.Range(1, 5)) + ws.Cell(r, c).Value = "X"; + + var blueColumn = ws.Column(2); + var redColumn = ws.Column(5); + + blueColumn.Style.Fill.BackgroundColor = XLColor.Blue; + blueColumn.InsertColumnsAfter(2); + + redColumn.Style.Fill.BackgroundColor = XLColor.Red; + redColumn.InsertColumnsBefore(2); + + ws.Rows(3, 4).Style.Fill.BackgroundColor = XLColor.Orange; + ws.Range("B1:D1").InsertColumnsAfter(2); + ws.Range("B2:D2").InsertColumnsBefore(2); + ws.Range("B3:D3").InsertColumnsAfter(2); + ws.Range("B4:D4").InsertColumnsBefore(2); + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Comments/AddingComments.cs b/ClosedXML_Examples/Comments/AddingComments.cs new file mode 100644 index 0000000..a095894 --- /dev/null +++ b/ClosedXML_Examples/Comments/AddingComments.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ClosedXML.Excel; +using System.IO; +using MoreLinq; + +namespace ClosedXML_Examples +{ + public class AddingComments : IXLExample + { + + public void Create(string filePath) + { + var wb = new XLWorkbook {Author = "Manuel"}; + AddMiscComments(wb); + AddVisibilityComments(wb); + AddPosition(wb); + AddSignatures(wb); + AddStyleAlignment(wb); + AddColorsAndLines(wb); + AddMagins(wb); + AddProperties(wb); + AddProtection(wb); + AddSize(wb); + AddWeb(wb); + + wb.SaveAs(filePath); + } + + private void AddWeb(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Web"); + ws.Cell("A1").Comment.Style.Web.AlternateText = "The alternate text in case you need it."; + } + + private void AddSize(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Size"); + + // Automatic size is a copy of the property comment.Style.Alignment.AutomaticSize + // I created the duplicate because it makes more sense for it to be in Size + // but Excel has it under the Alignment tab. + ws.Cell("A2").Comment.AddText("Things are very tight around here."); + ws.Cell("A2").Comment.Style.Size.SetAutomaticSize(); + + ws.Cell("A4").Comment.AddText("Different size"); + ws.Cell("A4").Comment.Style + .Size.SetHeight(30) // The height is set in the same units as row.Height + .Size.SetWidth(30); // The width is set in the same units as row.Width + + // Set all comments to visible + ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + + private void AddProtection(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Protection"); + + ws.Cell("A1").Comment.Style + .Protection.SetLocked(false) + .Protection.SetLockText(false); + } + + private void AddProperties(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Properties"); + + ws.Cell("A1").Comment.Style.Properties.Positioning = XLDrawingAnchor.Absolute; + ws.Cell("A2").Comment.Style.Properties.Positioning = XLDrawingAnchor.MoveAndSizeWithCells; + ws.Cell("A3").Comment.Style.Properties.Positioning = XLDrawingAnchor.MoveWithCells; + } + + private void AddMagins(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Margins"); + + ws.Cell("A2").Comment + .SetVisible() + .AddText("Lorem ipsum dolor sit amet, adipiscing elit. ").AddNewLine() + .AddText("Nunc elementum, sapien a ultrices, commodo nisl. ").AddNewLine() + .AddText("Consequat erat lectus a nisi. Aliquam facilisis."); + + ws.Cell("A2").Comment.Style + .Margins.SetAll(0.25) + .Size.SetAutomaticSize(); + } + + private void AddColorsAndLines(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Colors and Lines"); + + ws.Cell("A2").Comment + .AddText("Now ") + .AddText("THIS").SetBold().SetFontColor(XLColor.Red) + .AddText(" is colorful!"); + ws.Cell("A2").Comment.Style + .ColorsAndLines.SetFillColor(XLColor.RichCarmine) + .ColorsAndLines.SetFillTransparency(0.25) // 25% opaque + .ColorsAndLines.SetLineColor(XLColor.Blue) + .ColorsAndLines.SetLineTransparency(0.75) // 75% opaque + .ColorsAndLines.SetLineDash(XLDashStyle.LongDash) + .ColorsAndLines.SetLineStyle(XLLineStyle.ThickBetweenThin) + .ColorsAndLines.SetLineWeight(7.5); + + // Set all comments to visible + ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + + private void AddStyleAlignment(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Alignment"); + + // Automagically adjust the size of the comment to fit the contents + ws.Cell("A1").Comment.Style.Alignment.SetAutomaticSize(); + ws.Cell("A1").Comment.AddText("Things are pretty tight around here"); + + // Default values + ws.Cell("A3").Comment + .AddText("Default Alignments:").AddNewLine() + .AddText("Vertical = Top").AddNewLine() + .AddText("Horizontal = Left").AddNewLine() + .AddText("Orientation = Left to Right"); + + // Let's change the alignments + ws.Cell("A8").Comment + .AddText("Vertical = Bottom").AddNewLine() + .AddText("Horizontal = Right"); + ws.Cell("A8").Comment.Style + .Alignment.SetVertical(XLDrawingVerticalAlignment.Bottom) + .Alignment.SetHorizontal(XLDrawingHorizontalAlignment.Right); + + // And now the orientation... + ws.Cell("D3").Comment.AddText("Orientation = Bottom to Top"); + ws.Cell("D3").Comment.Style + .Alignment.SetOrientation(XLDrawingTextOrientation.BottomToTop) + .Alignment.SetAutomaticSize(); + + ws.Cell("E3").Comment.AddText("Orientation = Top to Bottom"); + ws.Cell("E3").Comment.Style + .Alignment.SetOrientation(XLDrawingTextOrientation.TopToBottom) + .Alignment.SetAutomaticSize(); + + ws.Cell("F3").Comment.AddText("Orientation = Vertical"); + ws.Cell("F3").Comment.Style + .Alignment.SetOrientation(XLDrawingTextOrientation.Vertical) + .Alignment.SetAutomaticSize(); + + + // Set all comments to visible + ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + + private static void AddMiscComments(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Comments"); + + ws.Cell("A1").SetValue("Hidden").Comment.AddText("Hidden"); + ws.Cell("A2").SetValue("Visible").Comment.AddText("Visible"); + ws.Cell("A3").SetValue("On Top").Comment.AddText("On Top"); + ws.Cell("A4").SetValue("Underneath").Comment.AddText("Underneath"); + ws.Cell("A4").Comment.Style.Alignment.SetVertical(XLDrawingVerticalAlignment.Bottom); + ws.Cell("A3").Comment.SetZOrder(ws.Cell("A4").Comment.ZOrder + 1); + + ws.Cell("D9").Comment.AddText("Vertical"); + ws.Cell("D9").Comment.Style.Alignment.Orientation = XLDrawingTextOrientation.Vertical; + ws.Cell("D9").Comment.Style.Size.SetAutomaticSize(); + + ws.Cell("E9").Comment.AddText("Top to Bottom"); + ws.Cell("E9").Comment.Style.Alignment.Orientation = XLDrawingTextOrientation.TopToBottom; + ws.Cell("E9").Comment.Style.Size.SetAutomaticSize(); + + ws.Cell("F9").Comment.AddText("Bottom to Top"); + ws.Cell("F9").Comment.Style.Alignment.Orientation = XLDrawingTextOrientation.BottomToTop; + ws.Cell("F9").Comment.Style.Size.SetAutomaticSize(); + + ws.Cell("E1").Comment.Position.SetColumn(5); + ws.Cell("E1").Comment.AddText("Start on Col E, on top border"); + ws.Cell("E1").Comment.Style.Size.SetWidth(10); + var cE3 = ws.Cell("E3").Comment; + cE3.AddText("Size and position"); + cE3.Position.SetColumn(5).SetRow(4).SetColumnOffset(7).SetRowOffset(10); + cE3.Style.Size.SetHeight(25).Size.SetWidth(10); + var cE7 = ws.Cell("E7").Comment; + cE7.Position.SetColumn(6).SetRow(7).SetColumnOffset(0).SetRowOffset(0); + cE7.Style.Size.SetHeight(ws.Row(7).Height).Size.SetWidth(ws.Column(6).Width); + + ws.Cell("G1").Comment.AddText("Automatic Size"); + ws.Cell("G1").Comment.Style.Alignment.SetAutomaticSize(); + var cG3 = ws.Cell("G3").Comment; + cG3.SetAuthor("MDeLeon"); + cG3.AddSignature(); + cG3.AddText("This is a test of the emergency broadcast system."); + cG3.AddNewLine(); + cG3.AddText("Do "); + cG3.AddText("NOT").SetFontColor(XLColor.RadicalRed).SetUnderline().SetBold(); + cG3.AddText(" forget it."); + cG3.Style + .Size.SetWidth(25) + .Size.SetHeight(100) + .Alignment.SetDirection(XLDrawingTextDirection.LeftToRight) + .Alignment.SetHorizontal(XLDrawingHorizontalAlignment.Distributed) + .Alignment.SetVertical(XLDrawingVerticalAlignment.Center) + .Alignment.SetOrientation(XLDrawingTextOrientation.LeftToRight) + .ColorsAndLines.SetFillColor(XLColor.Cyan) + .ColorsAndLines.SetFillTransparency(0.25) + .ColorsAndLines.SetLineColor(XLColor.DarkBlue) + .ColorsAndLines.SetLineTransparency(0.75) + .ColorsAndLines.SetLineDash(XLDashStyle.DashDot) + .ColorsAndLines.SetLineStyle(XLLineStyle.ThinThick) + .ColorsAndLines.SetLineWeight(5) + .Margins.SetAll(0.25) + .Properties.SetPositioning(XLDrawingAnchor.MoveAndSizeWithCells) + .Protection.SetLocked(false) + .Protection.SetLockText(false) + .Web.SetAlternateText("This won't be released to the web"); + + ws.Cell("A9").Comment.SetAuthor("MDeLeon").AddSignature().AddText("Something"); + ws.Cell("A9").Comment.SetBold().SetFontColor(XLColor.DarkBlue); + + ws.CellsUsed(XLCellsUsedOptions.All, c => !c.Address.ToStringRelative().Equals("A1") && c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + + private static void AddVisibilityComments(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Visibility"); + + // By default comments are hidden + ws.Cell("A1").SetValue("I have a hidden comment").Comment.AddText("Hidden"); + + // Set the comment as visible + ws.Cell("A2").Comment.SetVisible().AddText("Visible"); + + // The ZOrder on previous comments were 1 and 2 respectively + // here we're explicit about the ZOrder + ws.Cell("A3").Comment.SetZOrder(5).SetVisible().AddText("On Top"); + + // We want this comment to appear underneath the one for A3 + // so we set the ZOrder to something lower + ws.Cell("A4").Comment.SetZOrder(4).SetVisible().AddText("Underneath"); + ws.Cell("A4").Comment.Style.Alignment.SetVertical(XLDrawingVerticalAlignment.Bottom); + + // Alternatively you could set all comments to visible with the following line: + // ws.CellsUsed(true, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + + ws.Columns().AdjustToContents(); + } + + private void AddPosition(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Position"); + + ws.Columns().Width = 10; + + ws.Cell("A1").Comment.AddText("This is an unusual place for a comment..."); + ws.Cell("A1").Comment.Position + .SetColumn(3) // Starting from the third column + .SetColumnOffset(5) // The comment will start in the middle of the third column + .SetRow(5) // Starting from the fifth row + .SetRowOffset(7.5); // The comment will start in the middle of the fifth row + + // Set all comments to visible + ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + + private void AddSignatures(XLWorkbook wb) + { + var ws = wb.Worksheets.Add("Signatures"); + + // By default the signature will be with the logged user + // ws.Cell("A2").Comment.AddSignature().AddText("Hello World!"); + + // You can override this by specifying the comment's author: + ws.Cell("A2").Comment + .SetAuthor("MDeLeon") + .AddSignature() + .AddText("Hello World!"); + + + // Set all comments to visible + ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment).ForEach(c => c.Comment.SetVisible()); + } + } +} diff --git a/ClosedXML_Examples/Comments/EditingComments.cs b/ClosedXML_Examples/Comments/EditingComments.cs new file mode 100644 index 0000000..284f157 --- /dev/null +++ b/ClosedXML_Examples/Comments/EditingComments.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ClosedXML.Excel; +using System.IO; + +namespace ClosedXML_Examples +{ + class EditingComments : IXLExample + { + + public void Create(string filePath) { + + // Exercise(@"path/to/test/resources/comments"); + + } + + public void Exercise(string basePath) + { + + // INCOMPLETE + + var book = new XLWorkbook(Path.Combine(basePath, "EditingComments.xlsx")); + var sheet = book.Worksheet(1); + + // no change + // A1 + + // edit existing comment + sheet.Cell("B3").Comment.AddNewLine(); + sheet.Cell("B3").Comment.AddSignature(); + sheet.Cell("B3").Comment.AddText("more comment"); + + // delete + //sheet.Cell("C1").DeleteComment(); + + // clear contents + sheet.Cell("D3").Clear(XLClearOptions.Contents); + + // new basic + sheet.Cell("E1").Comment.AddText("non authored comment"); + + // new with author + sheet.Cell("F3").Comment.AddSignature(); + sheet.Cell("F3").Comment.AddText("comment from author"); + + // TODO: merge with cells + // TODO: resize with cells + // TODO: visible + + book.SaveAs(Path.Combine(basePath, "EditingComments_modified.xlsx")); + } + } +} diff --git a/ClosedXML_Examples/ConditionalFormatting/ConditionalFormatting.cs b/ClosedXML_Examples/ConditionalFormatting/ConditionalFormatting.cs new file mode 100644 index 0000000..f4af662 --- /dev/null +++ b/ClosedXML_Examples/ConditionalFormatting/ConditionalFormatting.cs @@ -0,0 +1,761 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples +{ + public class CFColorScaleLowMidHigh : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().ColorScale() + .LowestValue(XLColor.Red) + .Midpoint(XLCFContentType.Percent, "50", XLColor.Yellow) + .HighestValue(XLColor.Green); + + workbook.SaveAs(filePath); + } + } + + public class CFColorScaleLowHigh : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().ColorScale() + .Minimum(XLCFContentType.Number, "2", XLColor.Red) + .Maximum(XLCFContentType.Percentile, "90", XLColor.Green); + + workbook.SaveAs(filePath); + } + } + + public class CFColorScaleMinimumMaximum : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().ColorScale() + .LowestValue(XLColor.FromHtml("#FFFF7128")) + .HighestValue(XLColor.FromHtml("#FFFFEF9C")); + + workbook.SaveAs(filePath); + } + } + + public class CFStartsWith : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenStartsWith("Hell") + .Fill.SetBackgroundColor(XLColor.Red) + .Border.SetOutsideBorder(XLBorderStyleValues.Thick) + .Border.SetOutsideBorderColor(XLColor.Blue) + .Font.SetBold(); + + workbook.SaveAs(filePath); + } + } + + public class CFEndsWith : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenEndsWith("ll") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFIsBlank : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("") + .CellBelow().SetValue("") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenIsBlank() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotBlank : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("") + .CellBelow().SetValue("") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenNotBlank() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFIsError : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetFormulaA1("1/0") + .CellBelow().SetFormulaA1("1/0") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenIsError() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotError : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetFormulaA1("1/0") + .CellBelow().SetFormulaA1("1/0") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenNotError() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFContains : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenContains("Hell") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotContains : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenNotContains("Hell") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFEqualsString : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenEquals("Hell") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFEqualsNumber : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenEquals(2) + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotEqualsString : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue("Hello") + .CellBelow().SetValue("Hellos") + .CellBelow().SetValue("Hell") + .CellBelow().SetValue("Holl"); + + ws.RangeUsed().AddConditionalFormat().WhenNotEquals("Hell") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotEqualsNumber : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenNotEquals(2) + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFGreaterThan : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenGreaterThan("2") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFEqualOrGreaterThan : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenEqualOrGreaterThan("2") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFLessThan : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenLessThan("2") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFEqualOrLessThan : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenEqualOrLessThan("2") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFBetween : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenBetween("2", "3") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFNotBetween : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenNotBetween("2", "3") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFUnique : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenIsUnique() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFDuplicate : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenIsDuplicate() + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFIsTrue : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenIsTrue("TRUE") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFTop : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenIsTop(2) + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFBottom : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().WhenIsBottom(10, XLTopBottomType.Percent) + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFDataBar : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().DataBar(XLColor.Red, true) + .LowestValue() + .Maximum(XLCFContentType.Percent, "100"); + + workbook.SaveAs(filePath); + } + } + + public class CFDataBarNegative : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.Cell(1, 1).SetValue(-1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.Range(ws.Cell(1, 1), ws.Cell(4, 1)) + .AddConditionalFormat() + .DataBar(XLColor.Green, XLColor.Red, showBarOnly: false) + .LowestValue() + .HighestValue(); + + ws.Cell(1, 3).SetValue(-20) + .CellBelow().SetValue(40) + .CellBelow().SetValue(-60) + .CellBelow().SetValue(30); + + ws.Range(ws.Cell(1, 3), ws.Cell(4, 3)) + .AddConditionalFormat() + .DataBar(XLColor.Green, XLColor.Red, showBarOnly: true) + .Minimum(XLCFContentType.Number, -100) + .Maximum(XLCFContentType.Number, 100); + + workbook.SaveAs(filePath); + } + } + + public class CFIconSet : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().IconSet(XLIconSetStyle.ThreeTrafficLights2, true, true) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "0", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "2", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "3", XLCFContentType.Number); + + workbook.SaveAs(filePath); + } + } + + public class CFTwoConditions : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().IconSet(XLIconSetStyle.ThreeTrafficLights2, true, true) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "0", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "2", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "3", XLCFContentType.Number); + + ws.RangeUsed().AddConditionalFormat().WhenContains("1") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFInsertRows : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.Cell(2, 1).SetValue(1) + .CellRight().SetValue(1) + .CellRight().SetValue(2) + .CellRight().SetValue(3); + + var range = ws.RangeUsed(); + range.AddConditionalFormat().WhenEquals("1").Font.SetBold(); + range.InsertRowsAbove(1); + + workbook.SaveAs(filePath); + } + } + + public class CFTest : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(1) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3) + .CellBelow().SetValue(4); + + ws.RangeUsed().AddConditionalFormat().DataBar(XLColor.Red, XLColor.Green) + .LowestValue() + .HighestValue(); + + workbook.SaveAs(filePath); + } + } + + public class CFMultipleConditions : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + var range = ws.Range("A1:A10"); + range.AddConditionalFormat().WhenEquals("3") + .Fill.SetBackgroundColor(XLColor.Blue); + range.AddConditionalFormat().WhenEquals("2") + .Fill.SetBackgroundColor(XLColor.Green); + range.AddConditionalFormat().WhenEquals("1") + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + + public class CFStopIfTrue : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + ws.FirstCell().SetValue(6) + .CellBelow().SetValue(1) + .CellBelow().SetValue(2) + .CellBelow().SetValue(3); + + ws.RangeUsed().AddConditionalFormat().SetStopIfTrue().WhenGreaterThan(5); + + ws.RangeUsed().AddConditionalFormat().IconSet(XLIconSetStyle.ThreeTrafficLights2, true, true) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "0", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "2", XLCFContentType.Number) + .AddValue(XLCFIconSetOperator.EqualOrGreaterThan, "3", XLCFContentType.Number); + + workbook.SaveAs(filePath); + } + } + + public class CFDatesOccurring : IXLExample + { + public void Create(String filePath) + { + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet("Sheet1"); + + var range = ws.Range("A1:A10"); + range.AddConditionalFormat() + .WhenDateIs(XLTimePeriod.Tomorrow) + .Fill.SetBackgroundColor(XLColor.GrannySmithApple); + + range.AddConditionalFormat() + .WhenDateIs(XLTimePeriod.Yesterday) + .Fill.SetBackgroundColor(XLColor.Orange); + + range.AddConditionalFormat() + .WhenDateIs(XLTimePeriod.InTheLast7Days) + .Fill.SetBackgroundColor(XLColor.Blue); + + range.AddConditionalFormat() + .WhenDateIs(XLTimePeriod.ThisMonth) + .Fill.SetBackgroundColor(XLColor.Red); + + workbook.SaveAs(filePath); + } + } + } + + public class CFDataBars : IXLExample + { + public void Create(string filePath) + { + using var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet(); + + ws.Range("A2:F3").Value = 1; + ws.Range("A4:F4").Value = 2; + ws.Range("A5:F5").Value = 3; + ws.Range("A6:F6").Value = 4; + + ws.Cell("A1").Value = "Automatic"; + ws.Range("A2:A6").AddConditionalFormat().DataBar(XLColor.Amber); + + ws.Cell("B1").Value = "Lowest/Highest"; + ws.Range("B2:B6").AddConditionalFormat().DataBar(XLColor.BallBlue) + .LowestValue() + .HighestValue(); + + ws.Cell("C1").Value = "Value"; + ws.Range("C2:C6").AddConditionalFormat().DataBar(XLColor.Cadet) + .Minimum(XLCFContentType.Number, 0) + .Maximum(XLCFContentType.Number, 10); + + ws.Cell("D1").Value = "Percent"; + ws.Range("D2:D6").AddConditionalFormat().DataBar(XLColor.Desert) + .Minimum(XLCFContentType.Percent, 50) + .Maximum(XLCFContentType.Percent, 100); + + ws.Cell("E1").Value = "Formula"; + ws.Range("E2:E6").AddConditionalFormat().DataBar(XLColor.Ecru) + .Minimum(XLCFContentType.Formula, "-SUM($A$2:$E$2)") + .Maximum(XLCFContentType.Formula, "SUM($A$6:$E$6)"); + + ws.Cell("F1").Value = "Percentile"; + ws.Range("F2:F6").AddConditionalFormat().DataBar(XLColor.Fandango) + .Minimum(XLCFContentType.Percentile, 30) + .Maximum(XLCFContentType.Percentile, 70); + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Creating/CreateFiles.cs b/ClosedXML_Examples/Creating/CreateFiles.cs new file mode 100644 index 0000000..8f1978f --- /dev/null +++ b/ClosedXML_Examples/Creating/CreateFiles.cs @@ -0,0 +1,98 @@ +using ClosedXML_Examples.Columns; +using ClosedXML_Examples.Misc; +using ClosedXML_Examples.PageSetup; +using ClosedXML_Examples.Ranges; +using ClosedXML_Examples.Rows; +using ClosedXML_Examples.Styles; +using ClosedXML_Examples.Tables; +using System.IO; + +namespace ClosedXML_Examples +{ + public class CreateFiles + { + public static void CreateAllFiles() + { + var path = Program.BaseCreatedDirectory; + + new HelloWorld().Create(Path.Combine(path, "HelloWorld.xlsx")); + new BasicTable().Create(Path.Combine(path, "BasicTable.xlsx")); + + new StyleExamples().Create(); + new ChangingBasicTable().Create(Path.Combine(path, "BasicTable_Modified.xlsx")); + new ShiftingRanges().Create(Path.Combine(path, "ShiftingRanges.xlsx")); + new ColumnSettings().Create(Path.Combine(path, "ColumnSettings.xlsx")); + new RowSettings().Create(Path.Combine(path, "RowSettings.xlsx")); + new MergeCells().Create(Path.Combine(path, "MergedCells.xlsx")); + new InsertRows().Create(Path.Combine(path, "InsertRows.xlsx")); + new InsertColumns().Create(Path.Combine(path, "InsertColumns.xlsx")); + new ColumnCollection().Create(Path.Combine(path, "ColumnCollection.xlsx")); + new DataTypes().Create(Path.Combine(path, "DataTypes.xlsx")); + new DataTypesUnderDifferentCulture().Create(Path.Combine(path, "DataTypesUnderDifferentCulture.xlsx")); + new MultipleSheets().Create(Path.Combine(path, "MultipleSheets.xlsx")); + new RowCollection().Create(Path.Combine(path, "RowCollection.xlsx")); + new DefiningRanges().Create(Path.Combine(path, "DefiningRanges.xlsx")); + new ClearingRanges().Create(Path.Combine(path, "ClearingRanges.xlsx")); + new DeletingRanges().Create(Path.Combine(path, "DeletingRanges.xlsx")); + new Margins().Create(Path.Combine(path, "Margins.xlsx")); + new Page().Create(Path.Combine(path, "Page.xlsx")); + new HeaderFooters().Create(Path.Combine(path, "HeaderFooters.xlsx")); + new Sheets().Create(Path.Combine(path, "Sheets.xlsx")); + new SheetTab().Create(Path.Combine(path, "SheetTab.xlsx")); + new MultipleRanges().Create(Path.Combine(path, "MultipleRanges.xlsx")); + new StyleWorksheet().Create(Path.Combine(path, "StyleWorksheet.xlsx")); + new StyleRowsColumns().Create(Path.Combine(path, "StyleRowsColumns.xlsx")); + new InsertingDeletingRows().Create(Path.Combine(path, "InsertingDeletingRows.xlsx")); + new InsertingDeletingColumns().Create(Path.Combine(path, "InsertingDeletingColumns.xlsx")); + new DeletingColumns().Create(Path.Combine(path, "DeletingColumns.xlsx")); + new CellValues().Create(Path.Combine(path, "CellValues.xlsx")); + new LambdaExpressions().Create(Path.Combine(path, "LambdaExpressions.xlsx")); + new DefaultStyles().Create(Path.Combine(path, "DefaultStyles.xlsx")); + new TransposeRanges().Create(Path.Combine(path, "TransposeRanges.xlsx")); + new TransposeRangesPlus().Create(Path.Combine(path, "TransposeRangesPlus.xlsx")); + new MergeMoves().Create(Path.Combine(path, "MergedMoves.xlsx")); + new WorkbookProperties().Create(Path.Combine(path, "WorkbookProperties.xlsx")); + new AdjustToContents().Create(Path.Combine(path, "AdjustToContents.xlsx")); + new AdjustToContentsWithAutoFilter().Create(Path.Combine(path, "AdjustToContentsWithAutoFilter.xlsx")); + new HideUnhide().Create(Path.Combine(path, "HideUnhide.xlsx")); + new Outline().Create(Path.Combine(path, "Outline.xlsx")); + new Formulas().Create(Path.Combine(path, "Formulas.xlsx")); + new Collections().Create(Path.Combine(path, "Collections.xlsx")); + new NamedRanges().Create(Path.Combine(path, "NamedRanges.xlsx")); + new CopyingRanges().Create(Path.Combine(path, "CopyingRanges.xlsx")); + new BlankCells().Create(Path.Combine(path, "BlankCells.xlsx")); + new TwoPages().Create(Path.Combine(path, "TwoPages.xlsx")); + new UsingColors().Create(Path.Combine(path, "UsingColors.xlsx")); + + new ColumnCells().Create(Path.Combine(path, "ColumnCells.xlsx")); + new RowCells().Create(Path.Combine(path, "RowCells.xlsx")); + new FreezePanes().Create(Path.Combine(path, "FreezePanes.xlsx")); + new UsingTables().Create(Path.Combine(path, "UsingTables.xlsx")); + new ResizingTables().Create(Path.Combine(path, "ResizingTables.xlsx")); + new AddingRowToTables().Create(Path.Combine(path, "AddingRowToTables.xlsx")); + new RightToLeft().Create(Path.Combine(path, "RightToLeft.xlsx")); + new ShowCase().Create(Path.Combine(path, "ShowCase.xlsx")); + new CopyingWorksheets().Create(Path.Combine(path, "CopyingWorksheets.xlsx")); + new InsertingTables().Create(Path.Combine(path, "InsertingTables.xlsx")); + new InsertingData().Create(Path.Combine(path, "InsertingData.xlsx")); + new Hyperlinks().Create(Path.Combine(path, "Hyperlinks.xlsx")); + new DataValidation().Create(Path.Combine(path, "DataValidation.xlsx")); + new HideSheets().Create(Path.Combine(path, "HideSheets.xlsx")); + new SheetProtection().Create(Path.Combine(path, "SheetProtection.xlsx")); + new AutoFilter().Create(Path.Combine(path, "AutoFilter.xlsx")); + new Sorting().Create(Path.Combine(path, "Sorting.xlsx")); + new SortExample().Create(Path.Combine(path, "SortExample.xlsx")); + new AddingDataSet().Create(Path.Combine(path, "AddingDataSet.xlsx")); + new AddingDataTableAsWorksheet().Create(Path.Combine(path, "AddingDataTableAsWorksheet.xlsx")); + new TabColors().Create(Path.Combine(path, "TabColors.xlsx")); + new ShiftingFormulas().Create(Path.Combine(path, "ShiftingFormulas.xlsx")); + new CopyingRowsAndColumns().Create(Path.Combine(path, "CopyingRowsAndColumns.xlsx")); + new UsingRichText().Create(Path.Combine(path, "UsingRichText.xlsx")); + new UsingPhonetics().Create(Path.Combine(path, "UsingPhonetics.xlsx")); + new WalkingRanges().Create(Path.Combine(path, "CellMoves.xlsx")); + new AddingComments().Create(Path.Combine(path, "AddingComments.xlsx")); + new PivotTables().Create(Path.Combine(path, "PivotTables.xlsx")); + new SheetViews().Create(Path.Combine(path, "SheetViews.xlsx")); + } + } +} diff --git a/ClosedXML_Examples/Delete/DeleteFewWorksheets.cs b/ClosedXML_Examples/Delete/DeleteFewWorksheets.cs new file mode 100644 index 0000000..61aa28b --- /dev/null +++ b/ClosedXML_Examples/Delete/DeleteFewWorksheets.cs @@ -0,0 +1,41 @@ +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Delete +{ + public class DeleteFewWorksheets:IXLExample + { + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + //Note: Prepare + { + var workbook = new XLWorkbook(); + workbook.Worksheets.Add("1"); + workbook.Worksheets.Add("2"); + workbook.Worksheets.Add("3"); + workbook.Worksheets.Add("4"); + workbook.SaveAs(tempFile); + } + + //Note: Delate few worksheet + { + var workbook = new XLWorkbook(tempFile); + workbook.Worksheets.Delete("1"); + workbook.Worksheets.Delete("2"); + workbook.SaveAs(filePath); + } + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Delete/DeleteRows.cs b/ClosedXML_Examples/Delete/DeleteRows.cs new file mode 100644 index 0000000..bf58d62 --- /dev/null +++ b/ClosedXML_Examples/Delete/DeleteRows.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Delete +{ + public class DeleteRows : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + #region Create case + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Delete red rows"); + + // Put a value in a few cells + foreach (var r in Enumerable.Range(1, 5)) + foreach (var c in Enumerable.Range(1, 5)) + ws.Cell(r, c).Value = string.Format("R{0}C{1}", r, c); + + + var blueRow = ws.Rows(1, 2); + var redRow = ws.Row(5); + + blueRow.Style.Fill.BackgroundColor = XLColor.Blue; + + redRow.Style.Fill.BackgroundColor = XLColor.Red; + workbook.SaveAs(filePath); + } + #endregion + + #region Remove rows + { + var workbook = new XLWorkbook(filePath); + var ws = workbook.Worksheets.Worksheet("Delete red rows"); + + ws.Rows(1, 2).Delete(); + workbook.Save(); + } + #endregion + + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/ExampleHelper.cs b/ClosedXML_Examples/ExampleHelper.cs new file mode 100644 index 0000000..c0d9fe6 --- /dev/null +++ b/ClosedXML_Examples/ExampleHelper.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace ClosedXML_Examples +{ + public static class ExampleHelper + { + public static string GetTempFilePath() + { + return Path.GetTempFileName(); + } + + public static string GetTempFilePath(string filePath) + { + var extension = Path.GetExtension(filePath); + var tempFilePath = GetTempFilePath(); + return Path.ChangeExtension(tempFilePath, extension); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/HelloWorld.cs b/ClosedXML_Examples/HelloWorld.cs new file mode 100644 index 0000000..588a83f --- /dev/null +++ b/ClosedXML_Examples/HelloWorld.cs @@ -0,0 +1,16 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class HelloWorld + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Sample Sheet"); + worksheet.Cell("A1").Value = "Hello World!"; + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/IXLExample.cs b/ClosedXML_Examples/IXLExample.cs new file mode 100644 index 0000000..23e3a0e --- /dev/null +++ b/ClosedXML_Examples/IXLExample.cs @@ -0,0 +1,7 @@ +namespace ClosedXML_Examples +{ + public interface IXLExample + { + void Create(string filePath); + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/ImageHandling/ImageAnchors.cs b/ClosedXML_Examples/ImageHandling/ImageAnchors.cs new file mode 100644 index 0000000..2285c15 --- /dev/null +++ b/ClosedXML_Examples/ImageHandling/ImageAnchors.cs @@ -0,0 +1,84 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using System.IO; +using System.Reflection; + +namespace ClosedXML_Examples +{ + public class ImageAnchors : IXLExample + { + public void Create(string filePath) + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws; + + using (Stream fs = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Examples.Resources.ImageHandling.png")) + { + ws = wb.Worksheets.Add("Images1"); + + #region AbsoluteAnchor + + ws.AddPicture(fs, XLPictureFormat.Png, "Image10") + .MoveTo(220, 150); + + #endregion AbsoluteAnchor + + #region OneCellAnchor + + fs.Position = 0; + ws.AddPicture(fs, XLPictureFormat.Png, "Image11") + .MoveTo(ws.Cell(1, 1)); + + #endregion OneCellAnchor + + ws = wb.Worksheets.Add("Images2"); + + #region TwoCellAnchor + + fs.Position = 0; + ws.AddPicture(fs, XLPictureFormat.Png, "Image20") + .MoveTo(ws.Cell(6, 5), ws.Cell(9, 7)); + + #endregion TwoCellAnchor + } + + using (Stream fs = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + { + // Moving images around and scaling them + ws = wb.Worksheets.Add("Images3"); + + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(ws.Cell(2, 2), 20, 5, ws.Cell(5, 5), 30, 10) + .MoveTo(ws.Cell(2, 2), ws.Cell(5, 5)); + + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(ws.Cell(6, 2), 2, 2, ws.Cell(9, 5), 2, 2) + .MoveTo(ws.Cell(6, 2), 20, 5, ws.Cell(9, 5), 30, 10); + + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(ws.Cell(10, 2), 20, 5) + .Scale(0.2, true) + .MoveTo(ws.Cell(10, 1)); + } + + using (Stream fs = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + { + // Changing of placement + ws = wb.Worksheets.Add("Images4"); + + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(100, 100) + .WithPlacement(XLPicturePlacement.FreeFloating); + + // Add and delete picture immediately + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(100, 600) + .Delete(); + } + + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/ImageHandling/ImageFormats.cs b/ClosedXML_Examples/ImageHandling/ImageFormats.cs new file mode 100644 index 0000000..eaad0f8 --- /dev/null +++ b/ClosedXML_Examples/ImageHandling/ImageFormats.cs @@ -0,0 +1,40 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using System.IO; +using System.Reflection; + +namespace ClosedXML_Examples +{ + public class ImageFormats : IXLExample + { + public void Create(string filePath) + { + var wb = new XLWorkbook(); + IXLWorksheet ws; + + using (Stream fs = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Examples.Resources.ImageHandling.jpg")) + { + #region Jpeg + + ws = wb.Worksheets.Add("Jpg"); + ws.AddPicture(fs, XLPictureFormat.Jpeg, "JpegImage") + .MoveTo(ws.Cell(1, 1)); + + #endregion Jpeg + } + + using (Stream fs = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Examples.Resources.ImageHandling.png")) + { + #region Png + + ws = wb.Worksheets.Add("Png"); + ws.AddPicture(fs, XLPictureFormat.Png, "PngImage") + .MoveTo(ws.Cell(1, 1)); + + #endregion Png + + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Loading/ChangingBasicTable.cs b/ClosedXML_Examples/Loading/ChangingBasicTable.cs new file mode 100644 index 0000000..c64d0d3 --- /dev/null +++ b/ClosedXML_Examples/Loading/ChangingBasicTable.cs @@ -0,0 +1,48 @@ +using System.IO; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples +{ + public class ChangingBasicTable : IXLExample + { + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + var ws = workbook.Worksheet(1); + + // Change the background color of the headers + var rngHeaders = ws.Range("B3:F3"); + rngHeaders.Style.Fill.BackgroundColor = XLColor.LightSalmon; + + // Change the date formats + var rngDates = ws.Range("E4:E6"); + rngDates.Style.DateFormat.Format = "MM/dd/yyyy"; + + // Change the income values to text + var rngNumbers = ws.Range("F4:F6"); + foreach (var cell in rngNumbers.Cells()) + { + string formattedString = cell.GetFormattedString(); + cell.DataType = XLDataType.Text; + cell.Value = formattedString + " Dollars"; + } + + ws.Columns().AdjustToContents(); + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Loading/LoadFiles.cs b/ClosedXML_Examples/Loading/LoadFiles.cs new file mode 100644 index 0000000..772ad70 --- /dev/null +++ b/ClosedXML_Examples/Loading/LoadFiles.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class LoadFiles + { + public static void LoadAllFiles() + { + foreach (var file in Directory.GetFiles(Program.BaseCreatedDirectory)) + { + var fileInfo = new FileInfo(file); + var fileName = fileInfo.Name; + LoadAndSaveFile(Path.Combine(Program.BaseCreatedDirectory, fileName), Path.Combine(Program.BaseModifiedDirectory, fileName)); + } + } + + private static void LoadAndSaveFile(String input, String output) + { + var wb = new XLWorkbook(input); + wb.SaveAs(output); + wb.SaveAs(output); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Misc/AddingDataSet.cs b/ClosedXML_Examples/Misc/AddingDataSet.cs new file mode 100644 index 0000000..1ea165f --- /dev/null +++ b/ClosedXML_Examples/Misc/AddingDataSet.cs @@ -0,0 +1,51 @@ +using ClosedXML.Excel; +using System; +using System.Data; +using System.Linq; + +namespace ClosedXML_Examples.Misc +{ + public class AddingDataSet : IXLExample + { + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + var dataSet = GetDataSet(); + + // Add all DataTables in the DataSet as a worksheets + wb.Worksheets.Add(dataSet); + + foreach (var ws in wb.Worksheets) + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + private DataSet GetDataSet() + { + var ds = new DataSet(); + ds.Tables.Add(GetTable("Patients")); + ds.Tables.Add(GetTable("Employees")); + ds.Tables.Add(GetTable("Information")); + return ds; + } + + private DataTable GetTable(String tableName) + { + DataTable table = new DataTable(); + table.TableName = tableName; + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", "Janet", new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", new DateTime(2000, 1, 5)); + return table; + } + } +} diff --git a/ClosedXML_Examples/Misc/AddingDataTableAsWorksheet.cs b/ClosedXML_Examples/Misc/AddingDataTableAsWorksheet.cs new file mode 100644 index 0000000..78a9d0f --- /dev/null +++ b/ClosedXML_Examples/Misc/AddingDataTableAsWorksheet.cs @@ -0,0 +1,79 @@ +using System; +using System.Data; +using System.Linq; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Misc +{ + public class AddingDataTableAsWorksheet : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + var dataTable = GetTable("Information"); + + // Add a DataTable as a worksheet + wb.Worksheets.Add(dataTable); + wb.Worksheets.First().Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + // Private + private DataTable GetTable(String tableName) + { + DataTable table = new DataTable(); + table.TableName = tableName; + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", "Janet", new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", new DateTime(2000, 1, 5)); + return table; + } + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/AdjustToContents.cs b/ClosedXML_Examples/Misc/AdjustToContents.cs new file mode 100644 index 0000000..048dbd2 --- /dev/null +++ b/ClosedXML_Examples/Misc/AdjustToContents.cs @@ -0,0 +1,113 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class AdjustToContents : IXLExample + { + // Public + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Adjust To Contents"); + + // Set some values with different font sizes + ws.Cell(1, 1).Value = "Tall Row"; + ws.Cell(1, 1).Style.Font.FontSize = 30; + ws.Cell(2, 1).Value = "Very Wide Column"; + ws.Cell(2, 1).Style.Font.FontSize = 20; + + // Adjust column width + ws.Column(1).AdjustToContents(); + + // Adjust row heights + ws.Rows(1, 2).AdjustToContents(); + + // You can also adjust all rows/columns in one shot + // ws.Rows().AdjustToContents(); + // ws.Columns().AdjustToContents(); + + // We'll now select which cells should be used for calculating the + // column widths (same method applies for row heights) + + // Set the values + ws.Cell(4, 2).Value = "Width ignored because calling column.AdjustToContents(5, 7)"; + ws.Cell(5, 2).Value = "Short text"; + ws.Cell(6, 2).Value = "Width ignored because it's part of a merge"; + ws.Range(6, 2, 6, 4).Merge(); + ws.Cell(7, 2).Value = "Width should adjust to this cell"; + ws.Cell(8, 2).Value = "Width ignored because calling column.AdjustToContents(5, 7)"; + + // Adjust column widths only taking into account rows 5-7 + // (merged cells will be ignored) + ws.Column(2).AdjustToContents(5, 7); + + // You can also specify the starting row to start calculating the widths: + // e.g. ws.Column(3).AdjustToContents(9); + + var ws2 = wb.Worksheets.Add("Adjust Widths"); + ws2.Cell(1, 1).SetValue("Text to adjust - 255").Style.Alignment.TextRotation = 255; + for (Int32 co = 0; co < 90; co += 5) + { + ws2.Cell(1, (co / 5) + 2).SetValue("Text to adjust - " + co).Style.Alignment.TextRotation = co; + } + + ws2.Columns().AdjustToContents(); + + var ws4 = wb.Worksheets.Add("Adjust Widths 2"); + ws4.Cell(1, 1).SetValue("Text to adjust - 255").Style.Alignment.TextRotation = 255; + for (Int32 co = 0; co < 90; co += 5) + { + var c = ws4.Cell(1, (co / 5) + 2); + + c.RichText.AddText("Text to adjust - " + co).SetBold(); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("World!").SetBold().SetFontColor(XLColor.Blue).SetFontSize(25); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("Hello Cruel and unsusual world").SetBold().SetFontSize(20); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("Hello").SetBold(); + c.Style.Alignment.SetTextRotation(co); + } + ws4.Columns().AdjustToContents(); + + var ws3 = wb.Worksheets.Add("Adjust Heights"); + ws3.Cell(1, 1).SetValue("Text to adjust - 255").Style.Alignment.TextRotation = 255; + for (Int32 ro = 0; ro < 90; ro += 5) + { + ws3.Cell((ro / 5) + 2, 1).SetValue("Text to adjust - " + ro).Style.Alignment.TextRotation = ro; + } + + ws3.Rows().AdjustToContents(); + + var ws5 = wb.Worksheets.Add("Adjust Heights 2"); + ws5.Cell(1, 1).SetValue("Text to adjust - 255").Style.Alignment.TextRotation = 255; + for (Int32 ro = 0; ro < 90; ro += 5) + { + var c = ws5.Cell((ro / 5) + 2, 1); + c.RichText.AddText("Text to adjust - " + ro).SetBold(); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("World!").SetBold().SetFontColor(XLColor.Blue).SetFontSize(10); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("Hello Cruel and unsusual world").SetBold().SetFontSize(15); + c.RichText.AddText(Environment.NewLine); + c.RichText.AddText("Hello").SetBold(); + c.Style.Alignment.SetTextRotation(ro); + } + + ws5.Rows().AdjustToContents(); + + var ws6 = wb.Worksheets.Add("Absurdly wide column"); + ws6.Cell("A1").Value = "Some string"; + + // This column's width should be capped at 255 + ws6.Cell("B1").Value = @"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + + ws6.Columns().AdjustToContents(); + + wb.SaveAs(filePath, true); + } + } + } +} diff --git a/ClosedXML_Examples/Misc/AdjustToContentsWithAutoFilter.cs b/ClosedXML_Examples/Misc/AdjustToContentsWithAutoFilter.cs new file mode 100644 index 0000000..ed5fe6c --- /dev/null +++ b/ClosedXML_Examples/Misc/AdjustToContentsWithAutoFilter.cs @@ -0,0 +1,25 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class AdjustToContentsWithAutoFilter : IXLExample + { + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "AVeryLongColumnHeader"; + ws.Cell("A2").Value = "John"; + ws.Cell("A3").Value = "Hank"; + ws.Cell("A4").Value = "Dagny"; + + ws.RangeUsed().SetAutoFilter(); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/AutoFilter.cs b/ClosedXML_Examples/Misc/AutoFilter.cs new file mode 100644 index 0000000..bb119b2 --- /dev/null +++ b/ClosedXML_Examples/Misc/AutoFilter.cs @@ -0,0 +1,69 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class AutoFilter : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "Names"; + ws.Cell("A2").Value = "John"; + ws.Cell("A3").Value = "Hank"; + ws.Cell("A4").Value = "Dagny"; + + ws.RangeUsed().SetAutoFilter(); + + // Your can turn off the autofilter by: + // 1) worksheet.AutoFilter.Clear() + // 2) worksheet.SetAutoFilter(false) + // 3) Pick any range in the worksheet and call the above methods on the range + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/BlankCells.cs b/ClosedXML_Examples/Misc/BlankCells.cs new file mode 100644 index 0000000..a3220d6 --- /dev/null +++ b/ClosedXML_Examples/Misc/BlankCells.cs @@ -0,0 +1,59 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class BlankCells : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).Value = "X"; + ws.Cell(1, 1).Clear(); + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/CellValues.cs b/ClosedXML_Examples/Misc/CellValues.cs new file mode 100644 index 0000000..236da97 --- /dev/null +++ b/ClosedXML_Examples/Misc/CellValues.cs @@ -0,0 +1,148 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class CellValues : IXLExample + { + public void Create(String filePath) + { + using (var workbook = new XLWorkbook()) + { + var ws = workbook.Worksheets.Add("Cell Values"); + + // Set the titles + ws.Cell(2, 2).Value = "Initial Value"; + ws.Cell(2, 3).Value = "Casting"; + ws.Cell(2, 4).Value = "Using Get...()"; + ws.Cell(2, 5).Value = "Using GetValue()"; + ws.Cell(2, 6).Value = "GetString()"; + ws.Cell(2, 7).Value = "GetFormattedString()"; + + ////////////////////////////////////////////////////////////////// + // DateTime + + // Fill a cell with a date + var cellDateTime = ws.Cell(3, 2); + cellDateTime.Value = new DateTime(2010, 9, 2); + cellDateTime.Style.DateFormat.Format = "yyyy-MMM-dd"; + + // Extract the date in different ways + DateTime dateTime1 = (DateTime)cellDateTime.Value; + DateTime dateTime2 = cellDateTime.GetDateTime(); + DateTime dateTime3 = cellDateTime.GetValue(); + String dateTimeString = cellDateTime.GetString(); + String dateTimeFormattedString = cellDateTime.GetFormattedString(); + + // Set the values back to cells + // The apostrophe is to force ClosedXML to treat the date as a string + ws.Cell(3, 3).Value = dateTime1; + ws.Cell(3, 4).Value = dateTime2; + ws.Cell(3, 5).Value = dateTime3; + ws.Cell(3, 6).Value = "'" + dateTimeString; + ws.Cell(3, 7).Value = "'" + dateTimeFormattedString; + + ////////////////////////////////////////////////////////////////// + // Boolean + + // Fill a cell with a boolean + var cellBoolean = ws.Cell(4, 2); + cellBoolean.Value = true; + + // Extract the boolean in different ways + Boolean boolean1 = (Boolean)cellBoolean.Value; + Boolean boolean2 = cellBoolean.GetBoolean(); + Boolean boolean3 = cellBoolean.GetValue(); + String booleanString = cellBoolean.GetString(); + String booleanFormattedString = cellBoolean.GetFormattedString(); + + // Set the values back to cells + // The apostrophe is to force ClosedXML to treat the boolean as a string + ws.Cell(4, 3).Value = boolean1; + ws.Cell(4, 4).Value = boolean2; + ws.Cell(4, 5).Value = boolean3; + ws.Cell(4, 6).Value = "'" + booleanString; + ws.Cell(4, 7).Value = "'" + booleanFormattedString; + + ////////////////////////////////////////////////////////////////// + // Double + + // Fill a cell with a double + var cellDouble = ws.Cell(5, 2); + cellDouble.Value = 1234.567; + cellDouble.Style.NumberFormat.Format = "#,##0.00"; + + // Extract the double in different ways + Double double1 = (Double)cellDouble.Value; + Double double2 = cellDouble.GetDouble(); + Double double3 = cellDouble.GetValue(); + String doubleString = cellDouble.GetString(); + String doubleFormattedString = cellDouble.GetFormattedString(); + + // Set the values back to cells + // The apostrophe is to force ClosedXML to treat the double as a string + ws.Cell(5, 3).Value = double1; + ws.Cell(5, 4).Value = double2; + ws.Cell(5, 5).Value = double3; + ws.Cell(5, 6).Value = "'" + doubleString; + ws.Cell(5, 7).Value = "'" + doubleFormattedString; + + ////////////////////////////////////////////////////////////////// + // String + + // Fill a cell with a string + var cellString = ws.Cell(6, 2); + cellString.Value = "Test Case"; + + // Extract the string in different ways + String string1 = (String)cellString.Value; + String string2 = cellString.GetString(); + String string3 = cellString.GetValue(); + String stringString = cellString.GetString(); + String stringFormattedString = cellString.GetFormattedString(); + + // Set the values back to cells + ws.Cell(6, 3).Value = string1; + ws.Cell(6, 4).Value = string2; + ws.Cell(6, 5).Value = string3; + ws.Cell(6, 6).Value = stringString; + ws.Cell(6, 7).Value = stringFormattedString; + + ////////////////////////////////////////////////////////////////// + // TimeSpan + + // Fill a cell with a timeSpan + var cellTimeSpan = ws.Cell(7, 2); + cellTimeSpan.Value = new TimeSpan(1, 2, 31, 45); + + // Extract the timeSpan in different ways + TimeSpan timeSpan1 = (TimeSpan)cellTimeSpan.Value; + TimeSpan timeSpan2 = cellTimeSpan.GetTimeSpan(); + TimeSpan timeSpan3 = cellTimeSpan.GetValue(); + String timeSpanString = "'" + cellTimeSpan.GetString(); + String timeSpanFormattedString = "'" + cellTimeSpan.GetFormattedString(); + + // Set the values back to cells + ws.Cell(7, 3).Value = timeSpan1; + ws.Cell(7, 4).Value = timeSpan2; + ws.Cell(7, 5).Value = timeSpan3; + ws.Cell(7, 6).Value = timeSpanString; + ws.Cell(7, 7).Value = timeSpanFormattedString; + + ////////////////////////////////////////////////////////////////// + // Do some formatting + ws.Columns("B:G").Width = 20; + var rngTitle = ws.Range("B2:G2"); + rngTitle.Style.Font.Bold = true; + rngTitle.Style.Fill.BackgroundColor = XLColor.Cyan; + + ws.Columns().AdjustToContents(); + + ws = workbook.AddWorksheet("Test Whitespace"); + ws.FirstCell().Value = "' "; + + workbook.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Misc/Collections.cs b/ClosedXML_Examples/Misc/Collections.cs new file mode 100644 index 0000000..a9b9fe5 --- /dev/null +++ b/ClosedXML_Examples/Misc/Collections.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Misc +{ + public class Collections : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Collections"); + + // From a list of strings + var listOfStrings = new List(); + listOfStrings.Add("House"); + listOfStrings.Add("Car"); + ws.Cell(1, 1).Value = "Strings"; + ws.Cell(1, 1).AsRange().AddToNamed("Titles"); + ws.Cell(2, 1).Value = listOfStrings; + + // From a list of arrays + var listOfArr = new List(); + listOfArr.Add(new Int32[] { 1, 2, 3 }); + listOfArr.Add(new Int32[] { 1 }); + listOfArr.Add(new Int32[] { 1, 2, 3, 4, 5, 6 }); + ws.Cell(1, 3).Value = "Arrays"; + ws.Range(1, 3, 1, 8).Merge().AddToNamed("Titles"); + ws.Cell(2, 3).Value = listOfArr; + + // From a DataTable + var dataTable = GetTable(); + ws.Cell(6, 1).Value = "DataTable"; + ws.Range(6, 1, 6, 4).Merge().AddToNamed("Titles"); + ws.Cell(7, 1).Value = dataTable; + + // From a query + var list = new List(); + list.Add(new Person() { Name = "John", Age = 30, House = "On Elm St." }); + list.Add(new Person() { Name = "Mary", Age = 15, House = "On Main St." }); + list.Add(new Person() { Name = "Luis", Age = 21, House = "On 23rd St." }); + list.Add(new Person() { Name = "Henry", Age = 45, House = "On 5th Ave." }); + + var people = from p in list + where p.Age >= 21 + select new { p.Name, p.House, p.Age }; + + ws.Cell(6, 6).Value = "Query"; + ws.Range(6, 6, 6, 8).Merge().AddToNamed("Titles"); + ws.Cell(7, 6).Value = people; + + + // Prepare the style for the titles + var titlesStyle = wb.Style; + titlesStyle.Font.Bold = true; + titlesStyle.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + titlesStyle.Fill.BackgroundColor = XLColor.Cyan; + + // Format all titles in one shot + wb.NamedRanges.NamedRange("Titles").Ranges.Style = titlesStyle; + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + class Person + { + public String House { get; set; } + public String Name { get; set; } + public Int32 Age { get; set; } + } + + // Private + private DataTable GetTable() + { + + DataTable table = new DataTable(); + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", "Janet", new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", new DateTime(2000, 1, 5)); + return table; + } + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/CopyingRowsAndColumns.cs b/ClosedXML_Examples/Misc/CopyingRowsAndColumns.cs new file mode 100644 index 0000000..9f3a190 --- /dev/null +++ b/ClosedXML_Examples/Misc/CopyingRowsAndColumns.cs @@ -0,0 +1,114 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class CopyingRowsAndColumns : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + + var originalSheet = workbook.Worksheets.Add("original"); + + originalSheet.Cell("A2").SetValue("test value"); + originalSheet.Range("A2:E2").Merge(); + + originalSheet.Cell("F1").SetValue("test value").Style.Alignment.SetTopToBottom(); + originalSheet.Range("F1:F6").Merge(); + + var fromRow = workbook.Worksheets.Add("From a Row"); + fromRow.Cell(1, 1).SetValue("Row to Row:"); + originalSheet.Row(2).CopyTo(fromRow.Row(2)); + fromRow.Cell(3, 1).SetValue("Row to Range:"); + originalSheet.Row(2).CopyTo(fromRow.Row(4).AsRange()); + fromRow.Cell(5, 1).SetValue("Row to Cell:"); + originalSheet.Row(2).CopyTo(fromRow.Row(6).FirstCell()); + + var fromRange = workbook.Worksheets.Add("From a Range"); + fromRange.Cell(1, 1).SetValue("Range to Row:"); + originalSheet.Row(2).AsRange().CopyTo(fromRange.Row(2)); + fromRange.Cell(3, 1).SetValue("Range to Range:"); + originalSheet.Row(2).AsRange().CopyTo(fromRange.Row(4).AsRange()); + fromRange.Cell(5, 1).SetValue("Range to Cell:"); + originalSheet.Row(2).AsRange().CopyTo(fromRange.Row(6).FirstCell()); + + CopyRowAsRange(originalSheet, 2, fromRange, 8); + + var fromColumn = workbook.Worksheets.Add("From a Column to Column"); + fromColumn.Cell(1, 1).SetValue("Column to Column:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").CopyTo(fromColumn.Column(2)); + fromColumn.Cell(1, 3).SetValue("Column to Range:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").CopyTo(fromColumn.Column(4).AsRange()); + fromColumn.Cell(1, 5).SetValue("Column to Cell:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").CopyTo(fromColumn.Column(6).FirstCell()); + + var fromRangeToColumn = workbook.Worksheets.Add("From a Range to Column"); + fromRangeToColumn.Cell(1, 1).SetValue("Range to Column:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").AsRange().CopyTo(fromRangeToColumn.Column(2)); + fromRangeToColumn.Cell(1, 3).SetValue("Range to Range:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").AsRange().CopyTo(fromRangeToColumn.Column(4).AsRange()); + fromRangeToColumn.Cell(1, 5).SetValue("Range to Cell:").Style.Alignment.SetTopToBottom(); + originalSheet.Column("F").AsRange().CopyTo(fromRangeToColumn.Column(6).FirstCell()); + + + workbook.SaveAs(filePath); + } + + private static void CopyRowAsRange(IXLWorksheet originalSheet, int originalRowNumber, IXLWorksheet destSheet, int destRowNumber) + { + { + var destinationRow = destSheet.Row(destRowNumber); + destinationRow.Clear(); + + var originalRow = originalSheet.Row(originalRowNumber); + int columnNumber = originalRow.LastCellUsed(XLCellsUsedOptions.All).Address.ColumnNumber; + + var originalRange = originalSheet.Range(originalRowNumber, 1, originalRowNumber, columnNumber); + var destRange = destSheet.Range(destRowNumber, 1, destRowNumber, columnNumber); + originalRange.CopyTo(destRange); + } + } + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/CopyingWorksheets.cs b/ClosedXML_Examples/Misc/CopyingWorksheets.cs new file mode 100644 index 0000000..e06cd66 --- /dev/null +++ b/ClosedXML_Examples/Misc/CopyingWorksheets.cs @@ -0,0 +1,44 @@ +using ClosedXML.Excel; +using ClosedXML_Examples.Tables; +using System.IO; + +namespace ClosedXML_Examples.Misc +{ + public class CopyingWorksheets : IXLExample + { + public void Create(string filePath) + { + string tempFile1 = ExampleHelper.GetTempFilePath(filePath); + string tempFile2 = ExampleHelper.GetTempFilePath(filePath); + try + { + new UsingTables().Create(tempFile1); + var wb = new XLWorkbook(tempFile1); + + var wsSource = wb.Worksheet(1); + // Copy the worksheet to a new sheet in this workbook + wsSource.CopyTo("Copy"); + + // We're going to open another workbook to show that you can + // copy a sheet from one workbook to another: + new BasicTable().Create(tempFile2); + var wbSource = new XLWorkbook(tempFile2); + wbSource.Worksheet(1).CopyTo(wb, "Copy From Other"); + + // Save the workbook with the 2 copies + wb.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile1)) + { + File.Delete(tempFile1); + } + if (File.Exists(tempFile2)) + { + File.Delete(tempFile2); + } + } + } + } +} diff --git a/ClosedXML_Examples/Misc/DataTypes.cs b/ClosedXML_Examples/Misc/DataTypes.cs new file mode 100644 index 0000000..e642a7f --- /dev/null +++ b/ClosedXML_Examples/Misc/DataTypes.cs @@ -0,0 +1,227 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class DataTypes : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Data Types"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co).Value = "Plain Text:"; + ws.Cell(ro, co + 1).Value = "Hello World."; + + ws.Cell(++ro, co).Value = "Plain Date:"; + ws.Cell(ro, co + 1).Value = new DateTime(2010, 9, 2); + + ws.Cell(++ro, co).Value = "Plain DateTime:"; + ws.Cell(ro, co + 1).Value = new DateTime(2010, 9, 2, 13, 45, 22); + + ws.Cell(++ro, co).Value = "Plain Boolean:"; + ws.Cell(ro, co + 1).Value = true; + + ws.Cell(++ro, co).Value = "Plain Number:"; + ws.Cell(ro, co + 1).Value = 123.45; + + ws.Cell(++ro, co).Value = "TimeSpan:"; + ws.Cell(ro, co + 1).Value = new TimeSpan(33, 45, 22); + + ro++; + + ws.Cell(++ro, co).Value = "Decimal Number:"; + ws.Cell(ro, co + 1).Value = 123.45m; + + ws.Cell(++ro, co).Value = "Float Number:"; + ws.Cell(ro, co + 1).Value = 123.45f; + + ws.Cell(++ro, co).Value = "Double Number:"; + ws.Cell(ro, co + 1).Value = 123.45d; + + ws.Cell(++ro, co).Value = "Large Double Number:"; + ws.Cell(ro, co + 1).Value = 9.999E307d; + + ro++; + + ws.Cell(++ro, co).Value = "Explicit Text:"; + ws.Cell(ro, co + 1).Value = "'Hello World."; + + ws.Cell(++ro, co).Value = "Date as Text:"; + ws.Cell(ro, co + 1).Value = "'" + new DateTime(2010, 9, 2).ToString(); + + ws.Cell(++ro, co).Value = "DateTime as Text:"; + ws.Cell(ro, co + 1).Value = "'" + new DateTime(2010, 9, 2, 13, 45, 22).ToString(); + + ws.Cell(++ro, co).Value = "Boolean as Text:"; + ws.Cell(ro, co + 1).Value = "'" + true.ToString(); + + ws.Cell(++ro, co).Value = "Number as Text:"; + ws.Cell(ro, co + 1).Value = "'123.45"; + + ws.Cell(++ro, co).Value = "Number with @ format:"; + ws.Cell(ro, co + 1).Style.NumberFormat.Format = "@"; + ws.Cell(ro, co + 1).Value = 123.45; + + ws.Cell(++ro, co).Value = "Format number with @:"; + ws.Cell(ro, co + 1).Value = 123.45; + ws.Cell(ro, co + 1).Style.NumberFormat.Format = "@"; + + ws.Cell(++ro, co).Value = "TimeSpan as Text:"; + ws.Cell(ro, co + 1).Value = "'" + new TimeSpan(33, 45, 22).ToString(); + + ro++; + + ws.Cell(++ro, co).Value = "Changing Data Types:"; + + ro++; + + ws.Cell(++ro, co).Value = "Date to Text:"; + ws.Cell(ro, co + 1).Value = new DateTime(2010, 9, 2); + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "DateTime to Text:"; + ws.Cell(ro, co + 1).Value = new DateTime(2010, 9, 2, 13, 45, 22); + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "Boolean to Text:"; + ws.Cell(ro, co + 1).Value = true; + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "Number to Text:"; + ws.Cell(ro, co + 1).Value = 123.45; + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "TimeSpan to Text:"; + ws.Cell(ro, co + 1).Value = new TimeSpan(33, 45, 22); + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "Text to Date:"; + ws.Cell(ro, co + 1).Value = "'" + new DateTime(2010, 9, 2).ToString(); + ws.Cell(ro, co + 1).DataType = XLDataType.DateTime; + + ws.Cell(++ro, co).Value = "Text to DateTime:"; + ws.Cell(ro, co + 1).Value = "'" + new DateTime(2010, 9, 2, 13, 45, 22).ToString(); + ws.Cell(ro, co + 1).DataType = XLDataType.DateTime; + + ws.Cell(++ro, co).Value = "Text to Boolean:"; + ws.Cell(ro, co + 1).Value = "'" + true.ToString(); + ws.Cell(ro, co + 1).DataType = XLDataType.Boolean; + + ws.Cell(++ro, co).Value = "Text to Number:"; + ws.Cell(ro, co + 1).Value = "'123.45"; + ws.Cell(ro, co + 1).DataType = XLDataType.Number; + + ws.Cell(++ro, co).Value = "Percentage Text to Number:"; + ws.Cell(ro, co + 1).Value = "'55.12%"; + ws.Cell(ro, co + 1).Style.NumberFormat.SetNumberFormatId((int)XLPredefinedFormat.Number.PercentPrecision2); + ws.Cell(ro, co + 1).DataType = XLDataType.Number; + + ws.Cell(++ro, co).Value = "@ format to Number:"; + ws.Cell(ro, co + 1).Style.NumberFormat.Format = "@"; + ws.Cell(ro, co + 1).Value = 123.45; + ws.Cell(ro, co + 1).DataType = XLDataType.Number; + + ws.Cell(++ro, co).Value = "Text to TimeSpan:"; + ws.Cell(ro, co + 1).Value = "'" + new TimeSpan(33, 45, 22).ToString(); + ws.Cell(ro, co + 1).DataType = XLDataType.TimeSpan; + + ro++; + + ws.Cell(++ro, co).Value = "Formatted Date to Text:"; + ws.Cell(ro, co + 1).Value = new DateTime(2010, 9, 2); + ws.Cell(ro, co + 1).Style.DateFormat.Format = "yyyy-MM-dd"; + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ws.Cell(++ro, co).Value = "Formatted Number to Text:"; + ws.Cell(ro, co + 1).Value = 12345.6789; + ws.Cell(ro, co + 1).Style.NumberFormat.Format = "#,##0.00"; + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + + ro++; + + ws.Cell(++ro, co).Value = "Blank Text:"; + ws.Cell(ro, co + 1).Value = 12345.6789; + ws.Cell(ro, co + 1).Style.NumberFormat.Format = "#,##0.00"; + ws.Cell(ro, co + 1).DataType = XLDataType.Text; + ws.Cell(ro, co + 1).Value = ""; + + ro++; + + // Using inline strings (few users will ever need to use this feature) + // + // By default all strings are stored as shared so one block of text + // can be reference by multiple cells. + // You can override this by setting the .ShareString property to false + ws.Cell(++ro, co).Value = "Inline String:"; + var cell = ws.Cell(ro, co + 1); + cell.Value = "Not Shared"; + cell.ShareString = false; + + // To view all shared strings (all texts in the workbook actually), use the following: + // workbook.GetSharedStrings() + + ws.Cell(++ro, co) + .SetDataType(XLDataType.Text) + .SetDataType(XLDataType.Boolean) + .SetDataType(XLDataType.DateTime) + .SetDataType(XLDataType.Number) + .SetDataType(XLDataType.TimeSpan) + .SetDataType(XLDataType.Text) + .SetDataType(XLDataType.TimeSpan) + .SetDataType(XLDataType.Number) + .SetDataType(XLDataType.DateTime) + .SetDataType(XLDataType.Boolean) + .SetDataType(XLDataType.Text); + + ws.Columns(2, 3).AdjustToContents(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/DataTypesUnderDifferentCulture.cs b/ClosedXML_Examples/Misc/DataTypesUnderDifferentCulture.cs new file mode 100644 index 0000000..cbf1110 --- /dev/null +++ b/ClosedXML_Examples/Misc/DataTypesUnderDifferentCulture.cs @@ -0,0 +1,35 @@ +using System; +using ClosedXML.Excel; +using System.Threading; +using System.Globalization; +using System.IO; + +namespace ClosedXML_Examples.Misc +{ + public class DataTypesUnderDifferentCulture : IXLExample + { + public void Create(string filePath) + { + var backupCulture = Thread.CurrentThread.CurrentCulture; + + // Set thread culture to French, which should format numbers using decimal COMMA + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR"); + + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new DataTypes().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + workbook.SaveAs(filePath); + } + finally + { + Thread.CurrentThread.CurrentCulture = backupCulture; + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} diff --git a/ClosedXML_Examples/Misc/DataValidation.cs b/ClosedXML_Examples/Misc/DataValidation.cs new file mode 100644 index 0000000..28d2238 --- /dev/null +++ b/ClosedXML_Examples/Misc/DataValidation.cs @@ -0,0 +1,139 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class DataValidation : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Data Validation"); + + // Decimal between 1 and 5 + ws.Cell(1, 1).SetDataValidation().Decimal.Between(1, 5); + + // Whole number equals 2 + var dv1 = ws.Range("A2:A3").SetDataValidation(); + dv1.WholeNumber.EqualTo(2); + // Change the error message + dv1.ErrorStyle = XLErrorStyle.Warning; + dv1.ErrorTitle = "Number out of range"; + dv1.ErrorMessage = "This cell only allows the number 2."; + + // Date after the millenium + var dv2 = ws.Cell("A4").SetDataValidation(); + dv2.Date.EqualOrGreaterThan(new DateTime(2000, 1, 1)); + // Change the input message + dv2.InputTitle = "Can't party like it's 1999."; + dv2.InputMessage = "Please enter a date in this century."; + + // From a list + ws.Cell("C1").Value = "Yes"; + ws.Cell("C2").Value = "No"; + ws.Cell("A5").SetDataValidation().List(ws.Range("C1:C2")); + + ws.Range("C1:C2").AddToNamed("YesNo"); + ws.Cell("A6").SetDataValidation().List("=YesNo"); + + // Intersecting dataValidations + ws.Range("B1:B4").SetDataValidation().WholeNumber.EqualTo(1); + ws.Range("B3:B4").SetDataValidation().WholeNumber.EqualTo(2); + + + // Validate with multiple ranges + var ws2 = wb.Worksheets.Add("Validate Ranges"); + var rng1 = ws2.Ranges("A1:B2,B4:D7,F4:G5"); + rng1.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng1Validation = rng1.SetDataValidation(); + rng1Validation.Decimal.EqualTo(1); + rng1Validation.IgnoreBlanks = false; + + var rng2 = ws2.Range("A11:E14"); + rng2.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng2Validation = rng2.SetDataValidation(); + rng2Validation.Decimal.EqualTo(2); + rng2Validation.IgnoreBlanks = false; + + var rng3 = ws2.Range("B2:B12"); + //rng3.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng3Validation = rng3.SetDataValidation(); + rng3Validation.Decimal.EqualTo(3); + rng3Validation.IgnoreBlanks = true; + + var rng4 = ws2.Range("D5:D6"); + //rng4.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng4Validation = rng4.SetDataValidation(); + rng4Validation.Decimal.EqualTo(4); + rng4Validation.IgnoreBlanks = true; + + var rng5 = ws2.Range("C13:C14"); + //rng5.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng5Validation = rng5.SetDataValidation(); + rng5Validation.Decimal.EqualTo(5); + rng5Validation.IgnoreBlanks = true; + + var rng6 = ws2.Range("D11:D12"); + //rng6.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng6Validation = rng6.SetDataValidation(); + rng6Validation.Decimal.EqualTo(5); + rng6Validation.IgnoreBlanks = true; + + var rng7 = ws2.Range("G4:G5"); + //rng7.Style.Fill.SetBackgroundColor(XLColor.YellowGreen); + var rng7Validation = rng7.SetDataValidation(); + rng7Validation.Decimal.EqualTo(5); + rng7Validation.IgnoreBlanks = true; + + ws.CopyTo(ws.Name + " - Copy"); + ws2.CopyTo(ws2.Name + " - Copy"); + + wb.AddWorksheet("Copy From Range 1").FirstCell().Value = ws.RangeUsed(XLCellsUsedOptions.All); + wb.AddWorksheet("Copy From Range 2").FirstCell().Value = ws2.RangeUsed(XLCellsUsedOptions.All); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/Formulas.cs b/ClosedXML_Examples/Misc/Formulas.cs new file mode 100644 index 0000000..8066a81 --- /dev/null +++ b/ClosedXML_Examples/Misc/Formulas.cs @@ -0,0 +1,79 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class Formulas : IXLExample + { + public virtual void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Formulas"); + + ws.Cell(1, 1).Value = "Num1"; + ws.Cell(1, 2).Value = "Num2"; + ws.Cell(1, 3).Value = "Total"; + ws.Cell(1, 4).Value = "cell.FormulaA1"; + ws.Cell(1, 5).Value = "cell.FormulaR1C1"; + ws.Cell(1, 6).Value = "cell.Value"; + ws.Cell(1, 7).Value = "Are Equal?"; + + ws.Cell(2, 1).Value = 1; + ws.Cell(2, 2).Value = 2; + var cellWithFormulaA1 = ws.Cell(2, 3); + // Use A1 notation + cellWithFormulaA1.FormulaA1 = "=A2+$B$2"; // The equal sign (=) in a formula is optional + ws.Cell(2, 4).Value = cellWithFormulaA1.FormulaA1; + ws.Cell(2, 5).Value = cellWithFormulaA1.FormulaR1C1; + ws.Cell(2, 6).Value = cellWithFormulaA1.Value; + + ws.Cell(3, 1).Value = 1; + ws.Cell(3, 2).Value = 2; + var cellWithFormulaR1C1 = ws.Cell(3, 3); + // Use R1C1 notation + cellWithFormulaR1C1.FormulaR1C1 = "RC[-2]+R3C2"; // The equal sign (=) in a formula is optional + ws.Cell(3, 4).Value = cellWithFormulaR1C1.FormulaA1; + ws.Cell(3, 5).Value = cellWithFormulaR1C1.FormulaR1C1; + ws.Cell(3, 6).Value = cellWithFormulaR1C1.Value; + + ws.Cell(4, 1).Value = "A"; + ws.Cell(4, 2).Value = "B"; + var cellWithStringFormula = ws.Cell(4, 3); + + // Use R1C1 notation + cellWithStringFormula.FormulaR1C1 = "=\"Test\" & RC[-2] & \"R3C2\""; + ws.Cell(4, 4).Value = cellWithStringFormula.FormulaA1; + ws.Cell(4, 5).Value = cellWithStringFormula.FormulaR1C1; + ws.Cell(4, 6).Value = cellWithStringFormula.Value; + + // Setting the formula of a range + var rngData = ws.Range(2, 1, 4, 7); + rngData.LastColumn().FormulaR1C1 = "=IF(RC[-4]=RC[-1],\"Yes\", \"No\")"; + + // Using an array formula: + // Just put the formula between curly braces + ws.Cell("A6").Value = "Array Formula: "; + ws.Cell("B6").FormulaA1 = "{A2+A3}"; + ws.Range("C6:D6").FormulaA1 = "{TRANSPOSE(A2:A3)}"; + + ws.Range(1, 1, 1, 7).Style.Fill.BackgroundColor = XLColor.Cyan; + ws.Range(1, 1, 1, 7).Style.Font.Bold = true; + ws.Columns().AdjustToContents(); + + // You can also change the reference notation: + wb.ReferenceStyle = XLReferenceStyle.R1C1; + + // And the workbook calculation mode: + wb.CalculateMode = XLCalculateMode.Auto; + + ws.Range("A10").AddToNamed("A10_R1C1_A10_R1C1"); + ws.Cell("A10").Value = 0; + ws.Cell("A11").FormulaA1 = "A2 + A10_R1C1_A10_R1C1"; + ws.Cell("A12").FormulaR1C1 = "R2C1 + A10_R1C1_A10_R1C1"; + ws.Cell("A13").FormulaR1C1 = "=SUM(R[-5]:R[-4])"; + ws.Cell("A14").FormulaA1 = "=SUM(8:9)"; + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/FormulasWithEvaluation.cs b/ClosedXML_Examples/Misc/FormulasWithEvaluation.cs new file mode 100644 index 0000000..dca890e --- /dev/null +++ b/ClosedXML_Examples/Misc/FormulasWithEvaluation.cs @@ -0,0 +1,16 @@ +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Misc +{ + public class FormulasWithEvaluation : Formulas + { + public override void Create(string filePath) + { + base.Create(filePath); + using (var wb = new XLWorkbook(filePath)) + { + wb.Save(true, true); + } + } + } +} diff --git a/ClosedXML_Examples/Misc/FreezePanes.cs b/ClosedXML_Examples/Misc/FreezePanes.cs new file mode 100644 index 0000000..d831af3 --- /dev/null +++ b/ClosedXML_Examples/Misc/FreezePanes.cs @@ -0,0 +1,36 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class FreezePanes : IXLExample + { + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + // Freeze rows and columns in one shot + var ws1 = wb.AddWorksheet("Freeze1"); + ws1.Cell(5, 5).SetActive(); + ws1.SheetView.Freeze(3, 3); + + // You can also be more specific on what you want to freeze + // For example: + var ws2 = wb.AddWorksheet("FreezeRows"); + ws2.Cell(5, 5).SetActive(); + ws2.SheetView.FreezeRows(3); + + var ws3 = wb.AddWorksheet("FreezeColumns"); + ws3.Cell(5, 5).SetActive(); + ws3.SheetView.FreezeColumns(3); + + var wsSplit = wb.AddWorksheet("Split View"); + wsSplit.Cell(2, 2).SetActive(); + wsSplit.SheetView.SplitRow = 3; + wsSplit.SheetView.SplitColumn = 3; + + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Misc/HideSheets.cs b/ClosedXML_Examples/Misc/HideSheets.cs new file mode 100644 index 0000000..e4f769f --- /dev/null +++ b/ClosedXML_Examples/Misc/HideSheets.cs @@ -0,0 +1,63 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class HideSheets : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + wb.Worksheets.Add("First Hidden").Hide(); + wb.Worksheets.Add("Visible"); + wb.Worksheets.Add("Unhidden").Hide().Unhide(); + wb.Worksheets.Add("VeryHidden").Visibility = XLWorksheetVisibility.VeryHidden; + wb.Worksheets.Add("Last Hidden").Hide(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/HideUnhide.cs b/ClosedXML_Examples/Misc/HideUnhide.cs new file mode 100644 index 0000000..98c3df3 --- /dev/null +++ b/ClosedXML_Examples/Misc/HideUnhide.cs @@ -0,0 +1,64 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class HideUnhide : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Hide Rows Columns"); + + ws.Columns(1, 3).Hide(); + ws.Rows(1, 3).Hide(); + + ws.Column(2).Unhide(); + ws.Row(2).Unhide(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/Hyperlinks.cs b/ClosedXML_Examples/Misc/Hyperlinks.cs new file mode 100644 index 0000000..4ce869f --- /dev/null +++ b/ClosedXML_Examples/Misc/Hyperlinks.cs @@ -0,0 +1,128 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class Hyperlinks : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Hyperlinks"); + wb.Worksheets.Add("Second Sheet"); + + Int32 ro = 0; + + // You can create a link with pretty much anything you can put on a + // browser: http, ftp, mailto, gopher, news, nntp, etc. + + ws.Cell(++ro, 1).Value = "Link to a web page, no tooltip - Yahoo!"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink(@"http://www.yahoo.com"); + + ws.Cell(++ro, 1).Value = "Link to a web page, with a tooltip - Yahoo!"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink(@"http://www.yahoo.com", "Click to go to Yahoo!"); + + ws.Cell(++ro, 1).Value = "Link to a file - same folder"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink("Test.xlsx"); + + ws.Cell(++ro, 1).Value = "Link to a file - Absolute"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink(@"D:\Test.xlsx"); + + ws.Cell(++ro, 1).Value = "Link to a file - relative address"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink(@"../Test.xlsx"); + + ws.Cell(++ro, 1).Value = "Link to an address in this worksheet"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink("B1"); + + ws.Cell(++ro, 1).Value = "Link to an address in another worksheet"; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink("'Second Sheet'!A1"); + + // You can also set the properties of a hyperlink directly: + + ws.Cell(++ro, 1).Value = "Link to a range in this worksheet"; + ws.Cell(ro, 1).Hyperlink.InternalAddress = "B1:C2"; + ws.Cell(ro, 1).Hyperlink.Tooltip = "SquareBox"; + + ws.Cell(++ro, 1).Value = "Link to an email message"; + ws.Cell(ro, 1).Hyperlink.ExternalAddress = new Uri(@"mailto:SantaClaus@NorthPole.com?subject=Presents"); + + // Deleting a hyperlink + ws.Cell(++ro, 1).Value = "This is no longer a link"; + ws.Cell(ro, 1).Hyperlink.InternalAddress = "A1"; + ws.Cell(ro, 1).Hyperlink.Delete(); + + // Setting a hyperlink preserves previous formatting: + ws.Cell(++ro, 1).Value = "Odd looking link"; + ws.Cell(ro, 1).Style.Font.FontColor = XLColor.Red; + ws.Cell(ro, 1).Style.Font.Underline = XLFontUnderlineValues.Double; + ws.Cell(ro, 1).Hyperlink = new XLHyperlink(ws.Range("B1:C2")); + + // Hyperlink via formula + ws.Cell( ++ro, 1 ).SetValue( "Send Email" ) + .SetFormulaA1( "=HYPERLINK(\"mailto:test@test.com\", \"Send Email\")" ) + .Hyperlink = new XLHyperlink( "mailto:test@test.com", "'Send Email'" ); + + // List all hyperlinks in a worksheet: + var hyperlinksInWorksheet = ws.Hyperlinks; + + // List all hyperlinks in a range: + var hyperlinksInRange = ws.Range("A1:A3").Hyperlinks; + + // Clearing a cell with a hyperlink + ws.Cell(++ro, 1).Value = "ERROR!"; + ws.Cell(ro, 1).Hyperlink.InternalAddress = "A1"; + ws.Cell(ro, 1).Clear(); + + // Deleting a cell with a hyperlink + ws.Cell(++ro, 1).Value = "ERROR!"; + ws.Cell(ro, 1).Hyperlink.InternalAddress = "A1"; + ws.Cell(ro, 1).Clear(); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/InsertingData.cs b/ClosedXML_Examples/Misc/InsertingData.cs new file mode 100644 index 0000000..626a9ec --- /dev/null +++ b/ClosedXML_Examples/Misc/InsertingData.cs @@ -0,0 +1,110 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace ClosedXML_Examples.Misc +{ + public class InsertingData : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Inserting Data"); + + // From a list of strings + var listOfStrings = new List(); + listOfStrings.Add("House"); + listOfStrings.Add("001"); + ws.Cell(1, 1).Value = "From Strings"; + ws.Cell(1, 1).AsRange().AddToNamed("Titles"); + ws.Cell(2, 1).InsertData(listOfStrings); + + // From a list of arrays + var listOfArr = new List(); + listOfArr.Add(new Int32[] { 1, 2, 3 }); + listOfArr.Add(new Int32[] { 1 }); + listOfArr.Add(new Int32[] { 1, 2, 3, 4, 5, 6 }); + ws.Cell(1, 3).Value = "From Arrays"; + ws.Range(1, 3, 1, 8).Merge().AddToNamed("Titles"); + ws.Cell(2, 3).InsertData(listOfArr); + + // From a DataTable + var dataTable = GetTable(); + ws.Cell(6, 1).Value = "From DataTable"; + ws.Range(6, 1, 6, 4).Merge().AddToNamed("Titles"); + ws.Cell(7, 1).InsertData(dataTable); + + // From a query + var list = new List(); + list.Add(new Person() { Name = "John", Age = 30, House = "On Elm St." }); + list.Add(new Person() { Name = "Mary", Age = 15, House = "On Main St." }); + list.Add(new Person() { Name = "Luis", Age = 21, House = "On 23rd St." }); + list.Add(new Person() { Name = "Henry", Age = 45, House = "On 5th Ave." }); + + var people = from p in list + where p.Age >= 21 + select new { p.Name, p.House, p.Age }; + + ws.Cell(6, 6).Value = "From Query"; + ws.Range(6, 6, 6, 8).Merge().AddToNamed("Titles"); + ws.Cell(7, 6).InsertData(people); + + ws.Cell(11, 6).Value = "From List"; + ws.Range(11, 6, 11, 9).Merge().AddToNamed("Titles"); + ws.Cell(12, 6).InsertData(list); + + ws.Cell("A13").Value = "Transposed"; + ws.Range(13, 1, 13, 3).Merge().AddToNamed("Titles"); + ws.Cell("A14").InsertData(people.AsEnumerable(), true); + + // Prepare the style for the titles + var titlesStyle = wb.Style; + titlesStyle.Font.Bold = true; + titlesStyle.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + titlesStyle.Fill.BackgroundColor = XLColor.Cyan; + + // Format all titles in one shot + wb.NamedRanges.NamedRange("Titles").Ranges.Style = titlesStyle; + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + } + + private class Person + { + public String House { get; set; } + public String Name { get; set; } + public Int32 Age { get; set; } + public static String ClassType { get { return nameof(Person); } } + } + + // Private + private DataTable GetTable() + { + DataTable table = new DataTable(); + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", "Janet", new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", new DateTime(2000, 1, 5)); + return table; + } + + // Override + + #endregion Methods + } +} diff --git a/ClosedXML_Examples/Misc/LambdaExpressions.cs b/ClosedXML_Examples/Misc/LambdaExpressions.cs new file mode 100644 index 0000000..cbffea1 --- /dev/null +++ b/ClosedXML_Examples/Misc/LambdaExpressions.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Linq; +using ClosedXML.Excel; +using MoreLinq; + +namespace ClosedXML_Examples +{ + public class LambdaExpressions : IXLExample + { + public void Create(string filePath) + { + + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + var ws = workbook.Worksheet(1); + + // Define a range with the data + var firstDataCell = ws.Cell("B4"); + var lastDataCell = ws.LastCellUsed(); + var rngData = ws.Range(firstDataCell.Address, lastDataCell.Address); + + // Delete all rows where Outcast = false (the 3rd column) + rngData.Rows() // From all rows + .Where(r => !r.Cell(3).GetBoolean()) // where the 3rd cell of each row is false + .ForEach(r => r.Delete()); // delete the row and shift the cells up (the default for rows in a range) + + //// Put a light gray background to all text cells + //rngData.Cells() // From all cells + // .Where(c => c.DataType == XLCellValues.Text) // where the data type is Text + // .ForEach(c => c.Style.Fill.BackgroundColor = XLColor.LightGray); // Fill with a light gray + + var cells = rngData.Cells(); + var filtered = cells.Where(c => c.DataType == XLDataType.Text); + var list = filtered.ToList(); + foreach (var c in list) + { + c.Style.Fill.BackgroundColor = XLColor.LightGray; + } + + // Put a thick border to the bottom of the table (we may have deleted the bottom cells with the border) + rngData.LastRow().Style.Border.BottomBorder = XLBorderStyleValues.Thick; + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} diff --git a/ClosedXML_Examples/Misc/MergeCells.cs b/ClosedXML_Examples/Misc/MergeCells.cs new file mode 100644 index 0000000..dfe542b --- /dev/null +++ b/ClosedXML_Examples/Misc/MergeCells.cs @@ -0,0 +1,88 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class MergeCells : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Merge Cells"); + + // Merge a row + ws.Cell("B2").Value = "Merged Row(1) of Range (B2:D3)"; + ws.Range("B2:D3").Row(1).Merge(); + + // Merge a column + ws.Cell("F2").Value = "Merged Column(1) of Range (F2:G8)"; + ws.Cell("F2").Style.Alignment.WrapText = true; + ws.Range("F2:G8").Column(1).Merge(); + + // Merge a range + ws.Cell("B4").Value = "Merged Range (B4:D6)"; + ws.Cell("B4").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + ws.Cell("B4").Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + ws.Range("B4:D6").Merge(); + + // Unmerging a range... + ws.Cell("B8").Value = "Unmerged"; + ws.Range("B8:D8").Merge(); + ws.Range("B8:D8").Unmerge(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/MergeMoves.cs b/ClosedXML_Examples/Misc/MergeMoves.cs new file mode 100644 index 0000000..a5be4ce --- /dev/null +++ b/ClosedXML_Examples/Misc/MergeMoves.cs @@ -0,0 +1,53 @@ +using System.IO; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class MergeMoves : IXLExample + { + + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new MergeCells().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + + var ws = workbook.Worksheet(1); + + ws.Range("B1:F1").InsertRowsBelow(1); + ws.Range("A3:A9").InsertColumnsAfter(1); + ws.Row(1).Delete(); + ws.Column(1).Delete(); + + ws.Range("E8:E9").InsertColumnsAfter(1); + ws.Range("F2:F8").Merge(); + ws.Range("E3:E4").InsertColumnsAfter(1); + ws.Range("F2:F8").Merge(); + ws.Range("E1:E2").InsertColumnsAfter(1); + ws.Range("G2:G8").Merge(); + ws.Range("E1:E2").Delete(XLShiftDeletedCells.ShiftCellsLeft); + + ws.Range("D3:E3").InsertRowsBelow(1); + ws.Range("A1:B1").InsertRowsBelow(1); + ws.Range("B3:D3").Merge(); + ws.Range("A1:B1").Delete(XLShiftDeletedCells.ShiftCellsUp); + + ws.Range("B8:D8").Merge(); + ws.Range("D8:D9").Clear(); + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + } +} diff --git a/ClosedXML_Examples/Misc/MultipleSheets.cs b/ClosedXML_Examples/Misc/MultipleSheets.cs new file mode 100644 index 0000000..b0217c3 --- /dev/null +++ b/ClosedXML_Examples/Misc/MultipleSheets.cs @@ -0,0 +1,30 @@ +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class MultipleSheets : IXLExample + { + + public void Create(string filePath) + { + var wb = new XLWorkbook(); + foreach (var wsNum in Enumerable.Range(1, 5)) + { + wb.Worksheets.Add("Original Pos. is " + wsNum.ToString()); + } + + // Move first worksheet to the last position + wb.Worksheet(1).Position = wb.Worksheets.Count() + 1; + + // Delete worksheet on position 4 (in this case it's where original position = 5) + wb.Worksheet(4).Delete(); + + // Swap sheets in positions 1 and 2 + wb.Worksheet(2).Position = 1; + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/Outline.cs b/ClosedXML_Examples/Misc/Outline.cs new file mode 100644 index 0000000..f920a5e --- /dev/null +++ b/ClosedXML_Examples/Misc/Outline.cs @@ -0,0 +1,86 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class Outline : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Outline"); + + ws.Outline.SummaryHLocation = XLOutlineSummaryHLocation.Right; + ws.Columns(2, 6).Group(); // Create an outline (level 1) for columns 2-6 + ws.Columns(2, 4).Group(); // Create an outline (level 2) for columns 2-4 + ws.Column(2).Ungroup(true); // Remove column 2 from all outlines + + ws.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Bottom; + ws.Rows(1, 5).Group(); // Create an outline (level 1) for rows 1-5 + ws.Rows(1, 4).Group(); // Create an outline (level 2) for rows 1-4 + ws.Rows(1, 4).Collapse(); // Collapse rows 1-4 + ws.Rows(1, 2).Group(); // Create an outline (level 3) for rows 1-2 + ws.Rows(1, 2).Ungroup(); // Ungroup rows 1-2 from their last outline + + // You can also Collapse/Expand specific outline levels + // + // ws.CollapseRows(Int32 outlineLevel) + // ws.CollapseColumns(Int32 outlineLevel) + // + // ws.ExpandRows(Int32 outlineLevel) + // ws.ExpandColumns(Int32 outlineLevel) + + // And you can also Collapse/Expand ALL outline levels in one shot + // + // ws.CollapseRows() + // ws.CollapseColumns() + // + // ws.ExpandRows() + // ws.ExpandColumns() + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/RightToLeft.cs b/ClosedXML_Examples/Misc/RightToLeft.cs new file mode 100644 index 0000000..5137fec --- /dev/null +++ b/ClosedXML_Examples/Misc/RightToLeft.cs @@ -0,0 +1,21 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class RightToLeft : IXLExample + { + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + var ws = wb.Worksheets.Add("RightToLeftSheet"); + ws.Cell("A1").Value = "A1"; + ws.Cell("B1").Value = "B1"; + ws.Cell("C1").Value = "C1"; + ws.RightToLeft = true; + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/SheetProtection.cs b/ClosedXML_Examples/Misc/SheetProtection.cs new file mode 100644 index 0000000..c15435a --- /dev/null +++ b/ClosedXML_Examples/Misc/SheetProtection.cs @@ -0,0 +1,58 @@ +using ClosedXML.Excel; +using System; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML_Examples.Misc +{ + public class SheetProtection : IXLExample + { + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Protected No-Password"); + + ws.Protect().AllowElement + ( + // On this sheet we will only allow: + XLSheetProtectionElements.FormatCells + | XLSheetProtectionElements.InsertColumns + | XLSheetProtectionElements.DeleteColumns + | XLSheetProtectionElements.DeleteRows + | XLSheetProtectionElements.EditScenarios + ); + + ws.Cell("A1").SetValue("Locked, No Hidden (Default):").Style.Font.SetBold().Fill.SetBackgroundColor(XLColor.Cyan); + ws.Cell("B1").Style + .Border.SetOutsideBorder(XLBorderStyleValues.Medium); + + ws.Cell("A2").SetValue("Locked, Hidden:").Style.Font.SetBold().Fill.SetBackgroundColor(XLColor.Cyan); + ws.Cell("B2").Style + .Protection.SetHidden() + .Border.SetOutsideBorder(XLBorderStyleValues.Medium); + + ws.Cell("A3").SetValue("Not Locked, Hidden:").Style.Font.SetBold().Fill.SetBackgroundColor(XLColor.Cyan); + ws.Cell("B3").Style + .Protection.SetLocked(false) + .Protection.SetHidden() + .Border.SetOutsideBorder(XLBorderStyleValues.Medium); + + ws.Cell("A4").SetValue("Not Locked, Not Hidden:").Style.Font.SetBold().Fill.SetBackgroundColor(XLColor.Cyan); + ws.Cell("B4").Style + .Protection.SetLocked(false) + .Border.SetOutsideBorder(XLBorderStyleValues.Medium); + + ws.Columns().AdjustToContents(); + + // Protect a sheet with a password + var protectedSheet = wb.Worksheets.Add("Protected Password = 123"); + var protection = protectedSheet.Protect("123", Algorithm.SimpleHash); + protection.AllowElement + ( + XLSheetProtectionElements.InsertRows + | XLSheetProtectionElements.InsertColumns + ); + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/SheetViews.cs b/ClosedXML_Examples/Misc/SheetViews.cs new file mode 100644 index 0000000..598f722 --- /dev/null +++ b/ClosedXML_Examples/Misc/SheetViews.cs @@ -0,0 +1,44 @@ +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Misc +{ + public class SheetViews : IXLExample + { + public void Create(string filePath) + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws; + + ws = wb.AddWorksheet("ZoomScale"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScale = 50; + + ws = wb.AddWorksheet("ZoomScaleNormal"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScaleNormal = 70; + + ws = wb.AddWorksheet("ZoomScalePageLayoutView"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScalePageLayoutView = 85; + + ws = wb.AddWorksheet("ZoomScaleSheetLayoutView"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScaleSheetLayoutView = 120; + + ws = wb.AddWorksheet("ZoomScaleTooSmall"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScale = 5; + + ws = wb.AddWorksheet("ZoomScaleTooBig"); + ws.FirstCell().SetValue(ws.Name); + ws.SheetView.ZoomScale = 500; + + ws = wb.AddWorksheet("TopLeftCell"); + ws.SheetView.TopLeftCellAddress = ws.Cell("AZ2000").Address; + + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Misc/ShiftingFormulas.cs b/ClosedXML_Examples/Misc/ShiftingFormulas.cs new file mode 100644 index 0000000..63c636f --- /dev/null +++ b/ClosedXML_Examples/Misc/ShiftingFormulas.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class ShiftingFormulas : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Shifting Formulas"); + ws.Cell("B2").Value = 5; + ws.Cell("B3").Value = 6; + ws.Cell("C2").Value = 1; + ws.Cell("C3").Value = 2; + ws.Cell("A4").Value = "Sum:"; + ws.Range("B4:C4").FormulaR1C1 = "Sum(R[-2]C:R[-1]C)"; + ws.Range("B4:C4").AddToNamed("WorkbookB4C4"); + ws.Range("B4:C4").AddToNamed("WorksheetB4C4", XLScope.Worksheet); + ws.Cell("E2").Value = "Avg:"; + + ws.Cell("F2").FormulaA1 = "Average(B2:C3)"; + ws.Ranges("A4,E2").Style + .Font.SetBold() + .Fill.SetBackgroundColor(XLColor.CyanProcess); + + var ws2 = wb.Worksheets.Add("WS2"); + ws2.Cell(1, 1).FormulaA1 = "='Shifting Formulas'!B2"; + ws2.Cell(1, 2).Value = ws2.Cell(1, 1).Value; + ws2.Cell(2, 1).FormulaA1 = "Average('Shifting Formulas'!$B$2:$C$3)"; + ws2.Cell(3, 1).FormulaA1 = "Average('Shifting Formulas'!$B$2:$C3)"; + ws2.Cell(4, 1).FormulaA1 = "Average('Shifting Formulas'!$B$2:C3)"; + ws2.Cell(5, 1).FormulaA1 = "Average('Shifting Formulas'!$B2:C3)"; + ws2.Cell(6, 1).FormulaA1 = "Average('Shifting Formulas'!B2:C3)"; + ws2.Cell(7, 1).FormulaA1 = "Average('Shifting Formulas'!B2:C$3)"; + ws2.Cell(8, 1).FormulaA1 = "Average('Shifting Formulas'!B2:$C$3)"; + ws2.Cell(9, 1).FormulaA1 = "Average('Shifting Formulas'!B$2:$C$3)"; + + var dataGrid = ws.Range("B2:D3"); + ws.Row(1).InsertRowsAbove(1); + var newRow = dataGrid.LastRow().InsertRowsAbove(1).First(); + newRow.Value = 1; + dataGrid.LastColumn().FormulaR1C1 = String.Format("SUM(RC[-{0}]:RC[-1])", dataGrid.ColumnCount() - 1); + ws.Cell(1, 1).InsertCellsBelow(1); + ws.Column(1).InsertColumnsBefore(1); + ws.Row(4).Delete(); + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/ShowCase.cs b/ClosedXML_Examples/Misc/ShowCase.cs new file mode 100644 index 0000000..77337f3 --- /dev/null +++ b/ClosedXML_Examples/Misc/ShowCase.cs @@ -0,0 +1,104 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples +{ + public class ShowCase : IXLExample + { + public void Create(String filePath) + { + // Creating a new workbook + var wb = new XLWorkbook(); + + //Adding a worksheet + var ws = wb.Worksheets.Add("Contacts"); + + //Adding text + //Title + ws.Cell("B2").Value = "Contacts"; + //First Names + ws.Cell("B3").Value = "FName"; + ws.Cell("B4").Value = "John"; + ws.Cell("B5").Value = "Hank"; + ws.Cell("B6").SetValue("Dagny"); // Another way to set the value + //Last Names + ws.Cell("C3").Value = "LName"; + ws.Cell("C4").Value = "Galt"; + ws.Cell("C5").Value = "Rearden"; + ws.Cell("C6").SetValue("Taggart"); // Another way to set the value + + //Adding more data types + //Is an outcast? + ws.Cell("D3").Value = "Outcast"; + ws.Cell("D4").Value = true; + ws.Cell("D5").Value = false; + ws.Cell("D6").SetValue(false); // Another way to set the value + //Date of Birth + ws.Cell("E3").Value = "DOB"; + ws.Cell("E4").Value = new DateTime(1919, 1, 21); + ws.Cell("E5").Value = new DateTime(1907, 3, 4); + ws.Cell("E6").SetValue(new DateTime(1921, 12, 15)); // Another way to set the value + //Income + ws.Cell("F3").Value = "Income"; + ws.Cell("F4").Value = 2000; + ws.Cell("F5").Value = 40000; + ws.Cell("F6").SetValue(10000); // Another way to set the value + + //Defining ranges + //From worksheet + var rngTable = ws.Range("B2:F6"); + //From another range + var rngDates = rngTable.Range("E4:E6"); + var rngNumbers = rngTable.Range("F4:F6"); + + //Formatting dates and numbers + //Using a OpenXML's predefined formats + rngDates.Style.NumberFormat.NumberFormatId = 15; + //Using a custom format + rngNumbers.Style.NumberFormat.Format = "$ #,##0"; + + //Format title cell in one shot + rngTable.Cell(1, 1).Style + .Font.SetBold() + .Fill.SetBackgroundColor(XLColor.CornflowerBlue) + .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center); + + //Merge title cells + rngTable.FirstRow().Merge(); // We could've also used: rngTable.Range("A1:E1").Merge() or rngTable.Row(1).Merge() + + //Formatting headers + var rngHeaders = rngTable.Range("B3:F3"); + rngHeaders.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + rngHeaders.Style.Font.Bold = true; + rngHeaders.Style.Font.FontColor = XLColor.DarkBlue; + rngHeaders.Style.Fill.BackgroundColor = XLColor.Aqua; + + // Create an Excel table with the data portion + var rngData = ws.Range("B3:F6"); + var excelTable = rngData.CreateTable(); + + // Add the totals row + excelTable.ShowTotalsRow = true; + // Put the average on the field "Income" + // Notice how we're calling the cell by the column name + excelTable.Field("Income").TotalsRowFunction = XLTotalsRowFunction.Average; + // Put a label on the totals cell of the field "DOB" + excelTable.Field("DOB").TotalsRowLabel = "Average:"; + + //Add thick borders to the contents of our spreadsheet + ws.RangeUsed().Style.Border.OutsideBorder = XLBorderStyleValues.Thick; + + // You can also specify the border for each side with: + // contents.FirstColumn().Style.Border.LeftBorder = XLBorderStyleValues.Thick; + // contents.LastColumn().Style.Border.RightBorder = XLBorderStyleValues.Thick; + // contents.FirstRow().Style.Border.TopBorder = XLBorderStyleValues.Thick; + // contents.LastRow().Style.Border.BottomBorder = XLBorderStyleValues.Thick; + + // Adjust column widths to their content + ws.Columns(2, 6).AdjustToContents(); + + //Saving the workbook + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Misc/TabColors.cs b/ClosedXML_Examples/Misc/TabColors.cs new file mode 100644 index 0000000..15222b9 --- /dev/null +++ b/ClosedXML_Examples/Misc/TabColors.cs @@ -0,0 +1,67 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class TabColors : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + var wsRed = wb.Worksheets.Add("Red").SetTabColor(XLColor.Red); + + var wsAccent3 = wb.Worksheets.Add("Accent3").SetTabColor(XLColor.FromTheme(XLThemeColor.Accent3)); + + var wsIndexed = wb.Worksheets.Add("Indexed"); + wsIndexed.TabColor = XLColor.FromIndex(24); + + var wsArgb = wb.Worksheets.Add("Argb"); + wsArgb.TabColor = XLColor.FromArgb(23, 23, 23); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/WorkbookProperties.cs b/ClosedXML_Examples/Misc/WorkbookProperties.cs new file mode 100644 index 0000000..03a7426 --- /dev/null +++ b/ClosedXML_Examples/Misc/WorkbookProperties.cs @@ -0,0 +1,75 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class WorkbookProperties : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Workbook Properties"); + + wb.Properties.Author = "theAuthor"; + wb.Properties.Title = "theTitle"; + wb.Properties.Subject = "theSubject"; + wb.Properties.Category = "theCategory"; + wb.Properties.Keywords = "theKeywords"; + wb.Properties.Comments = "theComments"; + wb.Properties.Status = "theStatus"; + wb.Properties.LastModifiedBy = "theLastModifiedBy"; + wb.Properties.Company = "theCompany"; + wb.Properties.Manager = "theManager"; + + // Creating/Using custom properties + wb.CustomProperties.Add("theText", "XXX"); + wb.CustomProperties.Add("theDate", new DateTime(2011, 1, 1, 17, 0, 0, DateTimeKind.Utc)); // Use UTC to make sure test can be run in any time zone + wb.CustomProperties.Add("theNumber", 123.456); + wb.CustomProperties.Add("theBoolean", true); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Misc/WorkbookProtection.cs b/ClosedXML_Examples/Misc/WorkbookProtection.cs new file mode 100644 index 0000000..0bd0122 --- /dev/null +++ b/ClosedXML_Examples/Misc/WorkbookProtection.cs @@ -0,0 +1,23 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Misc +{ + public class WorkbookProtection : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Workbook Protection"); + wb.Protect(true, false, "Abc@123"); + wb.SaveAs(filePath); + } + } + + #endregion + } +} diff --git a/ClosedXML_Examples/ModifyFiles.cs b/ClosedXML_Examples/ModifyFiles.cs new file mode 100644 index 0000000..69790b0 --- /dev/null +++ b/ClosedXML_Examples/ModifyFiles.cs @@ -0,0 +1,14 @@ +using ClosedXML_Examples.Delete; +using System.IO; + +namespace ClosedXML_Examples +{ + public class ModifyFiles + { + public static void Run() + { + var path = Program.BaseModifiedDirectory; + new DeleteRows().Create(Path.Combine(path, "DeleteRows.xlsx")); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/PageSetup/HeaderFooters.cs b/ClosedXML_Examples/PageSetup/HeaderFooters.cs new file mode 100644 index 0000000..ed8fee0 --- /dev/null +++ b/ClosedXML_Examples/PageSetup/HeaderFooters.cs @@ -0,0 +1,65 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class HeaderFooters : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Headers and Footers"); + + // Simple left header to be placed on all pages + ws.PageSetup.Header.Left.AddText("Created with ClosedXML"); + + // Using various font decorations for the right header on the first page only + // Here we show different methods for setting font decorations. + + // Set single font decorations immediately + ws.PageSetup.Header.Right.AddText("The ", XLHFOccurrence.FirstPage).SetBold(); + ws.PageSetup.Header.Right.AddText("First ", XLHFOccurrence.FirstPage).SetFontColor(XLColor.Red); + + // Use the IXLRichText returned by the AddText(...) method to later on modify the font + var richText = ws.PageSetup.Header.Right.AddText("Colorful ", XLHFOccurrence.FirstPage); + richText.FontColor = XLColor.Blue; + richText.Underline = XLFontUnderlineValues.Double; + + // Set multiple font decorations chained + ws.PageSetup.Header.Right.AddText("Page", XLHFOccurrence.FirstPage) + .SetBold() + .SetItalic() + .SetFontName("Broadway"); + + + // Using predefined header/footer text: + + // Let's put the full path to the file on the right footer of every odd page: + ws.PageSetup.Footer.Right.AddText(XLHFPredefinedText.FullPath, XLHFOccurrence.OddPages); + + // Let's put the current page number and total pages on the center of every footer: + ws.PageSetup.Footer.Center.AddText(XLHFPredefinedText.PageNumber, XLHFOccurrence.AllPages); + ws.PageSetup.Footer.Center.AddText(" / ", XLHFOccurrence.AllPages); + ws.PageSetup.Footer.Center.AddText(XLHFPredefinedText.NumberOfPages, XLHFOccurrence.AllPages); + + // Don't align headers and footers with the margins + ws.PageSetup.AlignHFWithMargins = false; + + // Don't scale headers and footers with the document + ws.PageSetup.ScaleHFWithDocument = false; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PageSetup/Margins.cs b/ClosedXML_Examples/PageSetup/Margins.cs new file mode 100644 index 0000000..ec2ad79 --- /dev/null +++ b/ClosedXML_Examples/PageSetup/Margins.cs @@ -0,0 +1,36 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class Margins : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Margins"); + ws.PageSetup.Margins.Top = 1; + ws.PageSetup.Margins.Bottom = 1.25; + ws.PageSetup.Margins.Left = 0.5; + ws.PageSetup.Margins.Right = 0.75; + ws.PageSetup.Margins.Footer = 0.15; + ws.PageSetup.Margins.Header = 0.30; + + ws.PageSetup.CenterHorizontally = true; + ws.PageSetup.CenterVertically = true; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PageSetup/Page.cs b/ClosedXML_Examples/PageSetup/Page.cs new file mode 100644 index 0000000..7c8078b --- /dev/null +++ b/ClosedXML_Examples/PageSetup/Page.cs @@ -0,0 +1,43 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class Page : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws1 = workbook.Worksheets.Add("Page Setup - Page1"); + ws1.PageSetup.PageOrientation = XLPageOrientation.Landscape; + ws1.PageSetup.AdjustTo(80); + ws1.PageSetup.PaperSize = XLPaperSize.LegalPaper; + ws1.PageSetup.VerticalDpi = 600; + ws1.PageSetup.HorizontalDpi = 600; + + var ws2 = workbook.Worksheets.Add("Page Setup - Page2"); + ws2.PageSetup.PageOrientation = XLPageOrientation.Portrait; + ws2.PageSetup.FitToPages(2, 2); // Alternatively you can use + // ws2.PageSetup.PagesTall = # + // and/or ws2.PageSetup.PagesWide = # + + ws2.PageSetup.PaperSize = XLPaperSize.LetterPaper; + ws2.PageSetup.VerticalDpi = 600; + ws2.PageSetup.HorizontalDpi = 600; + ws2.PageSetup.FirstPageNumber = 5; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PageSetup/SheetTab.cs b/ClosedXML_Examples/PageSetup/SheetTab.cs new file mode 100644 index 0000000..8bb70d3 --- /dev/null +++ b/ClosedXML_Examples/PageSetup/SheetTab.cs @@ -0,0 +1,58 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class SheetTab : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Sheet Tab"); + + // Adding print areas + ws.PageSetup.PrintAreas.Add("A1:B2"); + ws.PageSetup.PrintAreas.Add("D3:D5"); + + // Adding rows to repeat at top + ws.PageSetup.SetRowsToRepeatAtTop(1,2); + + // Adding columns to repeat at left + ws.PageSetup.SetColumnsToRepeatAtLeft(1, 2); + + // Show gridlines + ws.PageSetup.ShowGridlines = true; + + // Print in black and white + ws.PageSetup.BlackAndWhite = true; + + // Print in draft quality + ws.PageSetup.DraftQuality = true; + + // Show row and column headings + ws.PageSetup.ShowRowAndColumnHeadings = true; + + // Set the page print order to over, then down + ws.PageSetup.PageOrder = XLPageOrderValues.OverThenDown; + + // Place comments at the end of the sheet + ws.PageSetup.ShowComments = XLShowCommentsValues.AtEnd; + + // Print errors as #N/A + ws.PageSetup.PrintErrorValue = XLPrintErrorValues.NA; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PageSetup/Sheets.cs b/ClosedXML_Examples/PageSetup/Sheets.cs new file mode 100644 index 0000000..0ee94cc --- /dev/null +++ b/ClosedXML_Examples/PageSetup/Sheets.cs @@ -0,0 +1,34 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class Sheets : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws1 = workbook.Worksheets.Add("Separate PrintAreas"); + ws1.PageSetup.PrintAreas.Add("A1:B2"); + ws1.PageSetup.PrintAreas.Add("D3:D5"); + + var ws2 = workbook.Worksheets.Add("Page Breaks"); + ws2.PageSetup.PrintAreas.Add("A1:D5"); + ws2.PageSetup.AddHorizontalPageBreak(2); + ws2.PageSetup.AddVerticalPageBreak(2); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PageSetup/TwoPages.cs b/ClosedXML_Examples/PageSetup/TwoPages.cs new file mode 100644 index 0000000..fe06049 --- /dev/null +++ b/ClosedXML_Examples/PageSetup/TwoPages.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.PageSetup +{ + public class TwoPages : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + foreach (var ro in Enumerable.Range(1, 100)) + { + foreach (var co in Enumerable.Range(1, 10)) + { + ws.Cell(ro, co).Value = ws.Cell(ro, co).Address.ToString(); + } + } + ws.PageSetup.PagesWide = 1; + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/PivotTables/PivotTables.cs b/ClosedXML_Examples/PivotTables/PivotTables.cs new file mode 100644 index 0000000..cfc8a42 --- /dev/null +++ b/ClosedXML_Examples/PivotTables/PivotTables.cs @@ -0,0 +1,234 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; + +namespace ClosedXML_Examples +{ + public class PivotTables : IXLExample + { + private class Pastry + { + public Pastry(string name, int? code, int numberOfOrders, double quality, string month, DateTime? bakeDate) + { + Name = name; + Code = code; + NumberOfOrders = numberOfOrders; + Quality = quality; + Month = month; + BakeDate = bakeDate; + } + + public string Name { get; set; } + public int? Code { get; } + public int NumberOfOrders { get; set; } + public double Quality { get; set; } + public string Month { get; set; } + public DateTime? BakeDate { get; set; } + } + + public void Create(String filePath) + { + var pastries = new List + { + new Pastry("Croissant", 101, 150, 60.2, "Apr", new DateTime(2016, 04, 21)), + new Pastry("Croissant", 101, 250, 50.42, "May", new DateTime(2016, 05, 03)), + new Pastry("Croissant", 101, 134, 22.12, "Jun", new DateTime(2016, 06, 24)), + new Pastry("Doughnut", 102, 250, 89.99, "Apr", new DateTime(2017, 04, 23)), + new Pastry("Doughnut", 102, 225, 70, "May", new DateTime(2016, 05, 24)), + new Pastry("Doughnut", 102, 210, 75.33, "Jun", new DateTime(2016, 06, 02)), + new Pastry("Bearclaw", 103, 134, 10.24, "Apr", new DateTime(2016, 04, 27)), + new Pastry("Bearclaw", 103, 184, 33.33, "May", new DateTime(2016, 05, 20)), + new Pastry("Bearclaw", 103, 124, 25, "Jun", new DateTime(2017, 06, 05)), + new Pastry("Danish", 104, 394, -20.24, "Apr", new DateTime(2017, 04, 24)), + new Pastry("Danish", 104, 190, 60, "May", new DateTime(2017, 05, 08)), + new Pastry("Danish", 104, 221, 24.76, "Jun", new DateTime(2016, 06, 21)), + + // Deliberately add different casings of same string to ensure pivot table doesn't duplicate it. + new Pastry("Scone", 105, 135, 0, "Apr", new DateTime(2017, 04, 22)), + new Pastry("SconE", 105, 122, 5.19, "May", new DateTime(2017, 05, 03)), + new Pastry("SCONE", 105, 243, 44.2, "Jun", new DateTime(2017, 06, 14)), + + // For ContainsBlank and integer rows/columns test + new Pastry("Scone", null, 255, 18.4, null, null), + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("PastrySalesData"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = ws.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + ws.Columns().AdjustToContents(); + + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + #region Pivots + + for (int i = 1; i <= 3; i++) + { + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt" + i); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("pvt", ptSheet.Cell(1, 1), table.AsRange()); + + // The rows in our pivot table will be the names of the pastries + if (i == 2) pt.RowLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + pt.RowLabels.Add("Name"); + + // The columns will be the months + pt.ColumnLabels.Add("Month"); + if (i == 3) pt.ColumnLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + + // The values in our table will come from the "NumberOfOrders" field + // The default calculation setting is a total of each row/column + pt.Values.Add("NumberOfOrders", "NumberOfOrdersPercentageOfBearclaw") + .ShowAsPercentageFrom("Name").And("Bearclaw") + .NumberFormat.Format = "0%"; + + if (i > 1) + { + pt.Values.Add("Quality", "Sum of Quality") + .NumberFormat.SetFormat("#,##0.00"); + } + if (i > 2) + { + pt.Values.Add("NumberOfOrders", "Sum of NumberOfOrders"); + } + + ptSheet.Columns().AdjustToContents(); + } + + #endregion Pivots + + #region Different kind of pivot + + ptSheet = wb.Worksheets.Add("pvtNoColumnLabels"); + pt = ptSheet.PivotTables.Add("pvtNoColumnLabels", ptSheet.Cell(1, 1), table.AsRange()); + + pt.RowLabels.Add("Name"); + pt.RowLabels.Add("Month"); + + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + pt.Values.Add("Quality").SetSummaryFormula(XLPivotSummary.Sum); + + pt.SetRowHeaderCaption("Pastry name"); + + #endregion Different kind of pivot + + #region Pivot table with collapsed fields + + ptSheet = wb.Worksheets.Add("pvtCollapsedFields"); + pt = ptSheet.PivotTables.Add("pvtCollapsedFields", ptSheet.Cell(1, 1), table.AsRange()); + + pt.RowLabels.Add("Name").SetCollapsed(); + pt.RowLabels.Add("Month").SetCollapsed(); + + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + pt.Values.Add("Quality").SetSummaryFormula(XLPivotSummary.Sum); + + #endregion Pivot table with collapsed fields + + #region Pivot table with a field both as a value and as a row/column/filter label + + ptSheet = wb.Worksheets.Add("pvtFieldAsValueAndLabel"); + pt = ptSheet.PivotTables.Add("pvtFieldAsValueAndLabel", ptSheet.Cell(1, 1), table.AsRange()); + + pt.RowLabels.Add("Name"); + pt.RowLabels.Add("Month"); + + pt.Values.Add("Name").SetSummaryFormula(XLPivotSummary.Count);//.NumberFormat.Format = "#0.00"; + + #endregion Pivot table with a field both as a value and as a row/column/filter label + + #region Pivot table with subtotals disabled + + ptSheet = wb.Worksheets.Add("pvtHideSubTotals"); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("pvtHidesubTotals", ptSheet.Cell(1, 1), table.AsRange()); + + // The rows in our pivot table will be the names of the pastries + pt.RowLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + + // The columns will be the months + pt.ColumnLabels.Add("Month"); + pt.ColumnLabels.Add("Name"); + + // The values in our table will come from the "NumberOfOrders" field + // The default calculation setting is a total of each row/column + pt.Values.Add("NumberOfOrders", "NumberOfOrdersPercentageOfBearclaw") + .ShowAsPercentageFrom("Name").And("Bearclaw") + .NumberFormat.Format = "0%"; + + pt.Values.Add("Quality", "Sum of Quality") + .NumberFormat.SetFormat("#,##0.00"); + + pt.Subtotals = XLPivotSubtotals.DoNotShow; + + pt.SetColumnHeaderCaption("Measures"); + + ptSheet.Columns().AdjustToContents(); + + #endregion Pivot table with subtotals disabled + + #region Pivot Table with filter + + ptSheet = wb.Worksheets.Add("pvtFilter"); + + pt = table.CreatePivotTable(ptSheet.FirstCell(), "pvtFilter"); + + pt.RowLabels.Add("Month"); + + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + pt.ReportFilters.Add("Name") + .AddSelectedValue("Scone") + .AddSelectedValue("Doughnut"); + + pt.ReportFilters.Add("Quality") + .AddSelectedValue(5.19); + + pt.ReportFilters.Add("BakeDate") + .AddSelectedValue(new DateTime(2017, 05, 03)); + + #endregion Pivot Table with filter + + #region Pivot table sorting + + ptSheet = wb.Worksheets.Add("pvtSort"); + pt = ptSheet.PivotTables.Add("pvtSort", ptSheet.Cell(1, 1), table.AsRange()); + + pt.RowLabels.Add("Name").SetSort(XLPivotSortType.Ascending); + pt.RowLabels.Add("Month").SetSort(XLPivotSortType.Descending); + + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + pt.Values.Add("Quality").SetSummaryFormula(XLPivotSummary.Sum); + + pt.SetRowHeaderCaption("Pastry name"); + + #endregion Different kind of pivot + + #region Pivot Table with integer rows + + ptSheet = wb.Worksheets.Add("pvtInteger"); + + pt = ptSheet.PivotTables.Add("pvtInteger", ptSheet.Cell(1, 1), table); + + pt.RowLabels.Add("Name"); + pt.RowLabels.Add("Code"); + pt.RowLabels.Add("BakeDate"); + + pt.ColumnLabels.Add("Month"); + + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + pt.Values.Add("Quality").SetSummaryFormula(XLPivotSummary.Sum); + + #endregion Pivot Table with filter + + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Program.cs b/ClosedXML_Examples/Program.cs new file mode 100644 index 0000000..afefad3 --- /dev/null +++ b/ClosedXML_Examples/Program.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; + +namespace ClosedXML_Examples +{ + public class Program + { + public static string BaseCreatedDirectory + { + get + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Created"); + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + return path; + } + } + + public static string BaseModifiedDirectory + { + get + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modified"); + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + return path; + } + } + + private static void Main(string[] args) + { + CreateFiles.CreateAllFiles(); + LoadFiles.LoadAllFiles(); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Ranges/AddingRowToTables.cs b/ClosedXML_Examples/Ranges/AddingRowToTables.cs new file mode 100644 index 0000000..b0a7b65 --- /dev/null +++ b/ClosedXML_Examples/Ranges/AddingRowToTables.cs @@ -0,0 +1,56 @@ +using ClosedXML.Excel; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Examples.Ranges +{ + public class AddingRowToTables : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var wb = new XLWorkbook(tempFile); + var ws = wb.Worksheets.First(); + + var firstCell = ws.FirstCellUsed(); + var lastCell = ws.LastCellUsed(); + var range = ws.Range(firstCell.Address, lastCell.Address); + range.FirstRow().Delete(); // Deleting the "Contacts" header (we don't need it for our purposes) + + // We want to use a theme for table, not the hard coded format of the BasicTable + range.Clear(XLClearOptions.AllFormats); + // Put back the date and number formats + range.Column(4).Style.NumberFormat.NumberFormatId = 15; + range.Column(5).Style.NumberFormat.Format = "$ #,##0"; + + var table = range.CreateTable(); // You can also use range.AsTable() if you want to + + ws.Cell("Q6000").Value = "dummy value"; + + var row = table.DataRange.InsertRowsBelow(1).First(); + + wb.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + // Private + + // Override + + #endregion Methods + } +} diff --git a/ClosedXML_Examples/Ranges/ClearingRanges.cs b/ClosedXML_Examples/Ranges/ClearingRanges.cs new file mode 100644 index 0000000..f33df7c --- /dev/null +++ b/ClosedXML_Examples/Ranges/ClearingRanges.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class ClearingRanges : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Clearing Ranges"); + foreach (var ro in Enumerable.Range(1, 10)) + { + foreach (var co in Enumerable.Range(1, 10)) + { + var cell = ws.Cell(ro, co); + cell.Value = cell.Address.ToString(); + cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + cell.Style.Fill.BackgroundColor = XLColor.Turquoise; + cell.Style.Font.Bold = true; + } + } + + // Clearing a range + ws.Range("B1:C2").Clear(); + + // Clearing a row in a range + ws.Range("B4:C5").Row(1).Clear(); + + // Clearing a column in a range + ws.Range("E1:F4").Column(2).Clear(); + + // Clear an entire row + ws.Row(7).Clear(); + + // Clear an entire column + ws.Column("H").Clear(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Ranges/CopyingRanges.cs b/ClosedXML_Examples/Ranges/CopyingRanges.cs new file mode 100644 index 0000000..7718f7a --- /dev/null +++ b/ClosedXML_Examples/Ranges/CopyingRanges.cs @@ -0,0 +1,37 @@ +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class CopyingRanges : IXLExample + { + public void Create(string filePath) + { + var tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + var ws = workbook.Worksheet(1); + + // Define a range with the data + var firstTableCell = ws.FirstCellUsed(); + var lastTableCell = ws.LastCellUsed(); + var rngData = ws.Range(firstTableCell.Address, lastTableCell.Address); + + // Copy the table to another worksheet + var wsCopy = workbook.Worksheets.Add("Contacts Copy"); + wsCopy.Cell(1, 1).Value = rngData; + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Ranges/CurrentRowColumn.cs b/ClosedXML_Examples/Ranges/CurrentRowColumn.cs new file mode 100644 index 0000000..98d662c --- /dev/null +++ b/ClosedXML_Examples/Ranges/CurrentRowColumn.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class CurrentRowColumn : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Current Row Column"); + + var cell = ws.Cell(5, 2); + cell.Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(1, 1) + .SetValue("Red's Row:") + .CellRight().SetValue(cell.WorksheetRow().RowNumber()) + .CellBelow().SetValue(cell.WorksheetColumn().ColumnLetter()) + .CellLeft().SetValue("Red's Column:"); + + var row = ws.Range("A6:C6").FirstRow(); + row.Style.Fill.SetBackgroundColor(XLColor.Blue); + + var column = ws.Range("B7:B9").FirstColumn(); + column.Style.Fill.SetBackgroundColor(XLColor.Green); + + ws.Cell(1, 4) + .SetValue("Blue's Row:") + .CellRight().SetValue(row.WorksheetRow().RowNumber()) + .CellBelow().SetValue(column.WorksheetColumn().ColumnLetter()) + .CellLeft().SetValue("Green's Column:"); + + ws.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right; + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Ranges/DefiningRanges.cs b/ClosedXML_Examples/Ranges/DefiningRanges.cs new file mode 100644 index 0000000..125ba44 --- /dev/null +++ b/ClosedXML_Examples/Ranges/DefiningRanges.cs @@ -0,0 +1,49 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class DefiningRanges : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Defining a Range"); + + // With a string + var range1 = ws.Range("A1:B1"); + range1.Cell(1, 1).Value = "ws.Range(\"A1:B1\").Merge()"; + range1.Merge(); + + // With two XLAddresses + var range2 = ws.Range(ws.Cell(2, 1).Address, ws.Cell(2, 2).Address); + range2.Cell(1, 1).Value = "ws.Range(ws.Cell(2, 1).Address, ws.Cell(2, 2).Address).Merge()"; + range2.Merge(); + + // With two strings + var range4 = ws.Range("A3", "B3"); + range4.Cell(1, 1).Value = "ws.Range(\"A3\", \"B3\").Merge()"; + range4.Merge(); + + // With 4 points + var range5 = ws.Range(4, 1, 4, 2); + range5.Cell(1, 1).Value = "ws.Range(4, 1, 4, 2).Merge()"; + range5.Merge(); + + ws.Column("A").AdjustToContents(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Ranges/DeletingRanges.cs b/ClosedXML_Examples/Ranges/DeletingRanges.cs new file mode 100644 index 0000000..cd916c8 --- /dev/null +++ b/ClosedXML_Examples/Ranges/DeletingRanges.cs @@ -0,0 +1,69 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class DeletingRanges : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Deleting Ranges"); + + // Deleting Columns + // Setup test values + ws.Columns("1-3, 5, 7").Style.Fill.BackgroundColor = XLColor.Gray; + ws.Columns("4, 6").Style.Fill.BackgroundColor = XLColor.GreenPigment; + ws.Row(1).Cells("1-3, 5, 7").Value = "FAIL"; + + ws.Column(7).Delete(); + ws.Column(1).Delete(); + ws.Columns(1,2).Delete(); + ws.Column(2).Delete(); + + // Deleting Rows + ws.Rows("1,5,7").Style.Fill.BackgroundColor = XLColor.GreenPigment; + ws.Rows("2-4,6, 8").Style.Fill.BackgroundColor = XLColor.Gray; + ws.Column(1).Cells("2-4,6, 8").Value = "FAIL"; + + ws.Row(8).Delete(); + ws.Row(2).Delete(); + ws.Rows(2, 3).Delete(); + ws.Rows(3, 4).Delete(); + + // Deleting Ranges (Shifting Left) + var rng1 = ws.Range(2, 2, 8, 8); + rng1.Columns("1-3, 5, 7").Style.Fill.BackgroundColor = XLColor.Gray; + rng1.Columns("4, 6").Style.Fill.BackgroundColor = XLColor.Orange; + rng1.Row(1).Cells("1-3, 5, 7").Value = "FAIL"; + + rng1.Column(7).Delete(); + rng1.Column(1).Delete(); + rng1.Range(1, 1, rng1.RowCount(), 2).Delete(XLShiftDeletedCells.ShiftCellsLeft); + rng1.Column(2).Delete(); + + // Deleting Ranges (Shifting Up) + rng1.Rows("4, 6").Style.Fill.BackgroundColor = XLColor.Orange; + rng1.Rows("1-3, 5, 7").Style.Fill.BackgroundColor = XLColor.Gray; + rng1.Column(1).Cells("1-3, 5, 7").Value = "FAIL"; + + rng1.Row(7).Delete(); + rng1.Row(1).Delete(); + rng1.Range(1, 1, 2, rng1.ColumnCount()).Delete(XLShiftDeletedCells.ShiftCellsUp); + rng1.Row(2).Delete(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Ranges/InsertingDeletingColumns.cs b/ClosedXML_Examples/Ranges/InsertingDeletingColumns.cs new file mode 100644 index 0000000..f2aecb5 --- /dev/null +++ b/ClosedXML_Examples/Ranges/InsertingDeletingColumns.cs @@ -0,0 +1,55 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class InsertingDeletingColumns : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Inserting and Deleting Columns"); + + // Range starts with 2 columns + var rng = ws.Range("B2:C3"); // Range starts on B2 + + // Insert a column before the range + ws.Column(1).InsertColumnsAfter(1); // Range starts on C2 + + // Insert a column in between the range + ws.Column(3).InsertColumnsAfter(1); // Range now has 3 columns + + // Insert a column (from a range) before the range + ws.Range("A2:A3").InsertColumnsAfter(1); // Range starts on D2 + + // Insert a column (from a range) in between the range + ws.Range("D2:D3").InsertColumnsAfter(1); // Range now has 4 columns + + // Inserting columns from a range not covering all columns + // does not affect our defined range + ws.Range("A1:A2").InsertColumnsAfter(1); + ws.Range("E3:E4").InsertColumnsAfter(1); + + // Delete a column before the range + ws.Column(1).Delete(); // Range starts on C2 + + // Delete a column (from a range) before the range + ws.Range("A2:A3").Delete(XLShiftDeletedCells.ShiftCellsLeft); // Range starts on B2 + + // Delete a column in between the range + ws.Column(3).Delete(); // Range now has 3 columns + + // Delete a column (from a range) in between the range + ws.Range("C2:C3").Delete(XLShiftDeletedCells.ShiftCellsLeft); // Range now has 2 columns + + // Deleting columns from a range not covering all columns + // does not affect our defined range + ws.Range("A1:A2").Delete(XLShiftDeletedCells.ShiftCellsLeft); + ws.Range("D3:D4").Delete(XLShiftDeletedCells.ShiftCellsLeft); + + rng.Style.Fill.BackgroundColor = XLColor.Orange; + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Ranges/InsertingDeletingRows.cs b/ClosedXML_Examples/Ranges/InsertingDeletingRows.cs new file mode 100644 index 0000000..9d4617c --- /dev/null +++ b/ClosedXML_Examples/Ranges/InsertingDeletingRows.cs @@ -0,0 +1,55 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class InsertingDeletingRows : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Inserting and Deleting Rows"); + + // Range starts with 2 rows + var rng = ws.Range("B2:C3"); + + // Insert a row above the range + ws.Row(1).InsertRowsBelow(1); // Range starts on B3 + + // Insert a row in between the range + ws.Row(3).InsertRowsBelow(1); // Range now has 3 rows + + // Insert a row (from a range) above the range + ws.Range("B1:C1").InsertRowsBelow(1); // Range starts on B4 + + // Insert a row (from a range) in between the range + ws.Range("B4:C4").InsertRowsBelow(1); // Range now has 4 rows + + // Inserting rows from a range not covering all columns + // does not affect our defined range + ws.Range("A1:B1").InsertRowsBelow(1); + ws.Range("C4:D4").InsertRowsBelow(1); + + // Delete a row above the range + ws.Row(1).Delete(); // Range starts on B3 + + // Delete a row (from a range) above the range + ws.Range("B1:C1").Delete(XLShiftDeletedCells.ShiftCellsUp); // Range starts on B2 + + // Delete a row in between the range + ws.Row(3).Delete(); // Range now has 3 rows + + // Delete a row (from a range) in between the range + ws.Range("B3:C3").Delete(XLShiftDeletedCells.ShiftCellsUp); // Range now has 2 rows + + // Deleting rows from a range not covering all columns + // does not affect our defined range + ws.Range("A1:B1").Delete(XLShiftDeletedCells.ShiftCellsUp); + ws.Range("C4:D4").Delete(XLShiftDeletedCells.ShiftCellsUp); + + rng.Style.Fill.BackgroundColor = XLColor.Orange; + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Ranges/MultipleRanges.cs b/ClosedXML_Examples/Ranges/MultipleRanges.cs new file mode 100644 index 0000000..0652f98 --- /dev/null +++ b/ClosedXML_Examples/Ranges/MultipleRanges.cs @@ -0,0 +1,22 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class MultipleRanges : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Multiple Ranges"); + + // using multiple string range definitions + ws.Ranges("A1:B2,C3:D4,E5:F6").Style.Fill.BackgroundColor = XLColor.Red; + + // using a single string separated by commas + ws.Ranges("A5:B6,E1:F2").Style.Fill.BackgroundColor = XLColor.Orange; + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Ranges/NamedRanges.cs b/ClosedXML_Examples/Ranges/NamedRanges.cs new file mode 100644 index 0000000..948a56c --- /dev/null +++ b/ClosedXML_Examples/Ranges/NamedRanges.cs @@ -0,0 +1,85 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class NamedRanges : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var wsPresentation = wb.Worksheets.Add("Presentation"); + var wsData = wb.Worksheets.Add("Data"); + + // Fill up some data + wsData.Cell(1, 1).Value = "Name"; + wsData.Cell(1, 2).Value = "Age"; + wsData.Cell(2, 1).Value = "Tom"; + wsData.Cell(2, 2).Value = 30; + wsData.Cell(3, 1).Value = "Dick"; + wsData.Cell(3, 2).Value = 25; + wsData.Cell(4, 1).Value = "Harry"; + wsData.Cell(4, 2).Value = 29; + + // Create a named range with the data: + wsData.Range("A2:B4").AddToNamed("PeopleData"); // Default named range scope is Workbook + + // Create a hidden named range + wb.NamedRanges.Add("Headers", wsData.Range("A1:B1")).Visible = false; + + // Create a hidden named range n worksheet scope + wsData.NamedRanges.Add("HeadersAndData", wsData.Range("A1:B4")).Visible = false; + + // Let's use the named range in a formula: + wsPresentation.Cell(1, 1).Value = "People Count:"; + wsPresentation.Cell(1, 2).FormulaA1 = "COUNT(PeopleData)"; + + // Create a named range with worksheet scope: + wsPresentation.Range("B1").AddToNamed("PeopleCount", XLScope.Worksheet); + + // Let's use the named range: + wsPresentation.Cell(2, 1).Value = "Total:"; + wsPresentation.Cell(2, 2).FormulaA1 = "PeopleCount"; + + // Copy the data in a named range: + wsPresentation.Cell(4, 1).Value = "People Data:"; + wsPresentation.Cell(5, 1).Value = wb.Range("PeopleData"); + + ///////////////////////////////////////////////////////////////////////// + // For the Excel geeks out there who actually know about + // named ranges with relative addresses, you can + // create such a thing with the following methods: + + // The following creates a relative named range pointing to the same row + // and one column to the right. For example if the current cell is B4 + // relativeRange1 will point to C4. + wsPresentation.NamedRanges.Add("relativeRange1", "Presentation!B1"); + + // The following creates a ralative named range pointing to the same row + // and one column to the left. For example if the current cell is D2 + // relativeRange2 will point to C2. + wb.NamedRanges.Add("relativeRange2", "Presentation!XFD1"); + + // Explanation: The address of a relative range always starts at A1 + // and moves from then on. To get the desired relative range just + // add or subtract the required rows and/or columns from A1. + // Column -1 = XFD, Column -2 = XFC, etc. + // Row -1 = 1048576, Row -2 = 1048575, etc. + ///////////////////////////////////////////////////////////////////////// + + wsData.Columns().AdjustToContents(); + wsPresentation.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + #endregion Methods + } +} diff --git a/ClosedXML_Examples/Ranges/SelectingRanges.cs b/ClosedXML_Examples/Ranges/SelectingRanges.cs new file mode 100644 index 0000000..f5897df --- /dev/null +++ b/ClosedXML_Examples/Ranges/SelectingRanges.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class SelectingRanges : IXLExample + { + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var wsActiveCell = wb.AddWorksheet("Set Active Cell"); + wsActiveCell.Cell("B2").SetActive(); + + var wsSelectRowsColumns = wb.AddWorksheet("Select Rows and Columns"); + wsSelectRowsColumns.Rows("2, 4-5").Select(); + wsSelectRowsColumns.Columns("2, 4-5").Select(); + + var wsSelectMisc = wb.AddWorksheet("Select Misc"); + wsSelectMisc.Cell("B2").Select(); + wsSelectMisc.Range("D2:E2").Select(); + wsSelectMisc.Ranges("C3, D4:E5").Select(); + + wb.SaveAs(filePath); + } + + } +} diff --git a/ClosedXML_Examples/Ranges/ShiftingRanges.cs b/ClosedXML_Examples/Ranges/ShiftingRanges.cs new file mode 100644 index 0000000..6bba32d --- /dev/null +++ b/ClosedXML_Examples/Ranges/ShiftingRanges.cs @@ -0,0 +1,40 @@ +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class ShiftingRanges : IXLExample + { + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + var ws = workbook.Worksheet(1); + + // Get a range object + var rngHeaders = ws.Range("B3:F3"); + + // Insert some rows/columns before the range + ws.Row(1).InsertRowsAbove(2); + ws.Column(1).InsertColumnsBefore(2); + + // Change the background color of the headers + rngHeaders.Style.Fill.BackgroundColor = XLColor.LightSalmon; + + ws.Columns().AdjustToContents(); + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} diff --git a/ClosedXML_Examples/Ranges/SortExample.cs b/ClosedXML_Examples/Ranges/SortExample.cs new file mode 100644 index 0000000..084808f --- /dev/null +++ b/ClosedXML_Examples/Ranges/SortExample.cs @@ -0,0 +1,233 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Misc +{ + public class SortExample : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + + #region Sort a table + var wsTable = wb.Worksheets.Add("Table"); + AddTestTable(wsTable); + var header = wsTable.Row(1).InsertRowsAbove(1).First(); + for(Int32 co = 1; co <= wsTable.LastColumnUsed().ColumnNumber(); co++) + { + header.Cell(co).Value = "Column" + co.ToString(); + } + var rangeTable = wsTable.RangeUsed(); + var table = rangeTable.CopyTo(wsTable.Column(wsTable.LastColumnUsed().ColumnNumber() + 3)).CreateTable(); + + table.Sort("Column2, Column3 Desc, Column1 ASC"); + + wsTable.Row(1).InsertRowsAbove(2); + wsTable.Cell(1, 1) + .SetValue(".Sort(\"Column2, Column3 Desc, Column1 ASC\") = Sort table Top to Bottom, Col 2 Asc, Col 3 Desc, Col 1 Asc, Ignore Blanks, Ignore Case") + .Style.Font.SetBold(); + #endregion + + #region Sort a simple range left to right + var wsLeftToRight = wb.Worksheets.Add("Sort Left to Right"); + AddTestTable(wsLeftToRight); + wsLeftToRight.RangeUsed().Transpose(XLTransposeOptions.MoveCells); + var rangeLeftToRight = wsLeftToRight.RangeUsed(); + var copyLeftToRight = rangeLeftToRight.CopyTo(wsLeftToRight.Row(wsLeftToRight.LastRowUsed().RowNumber() + 3)); + + copyLeftToRight.SortLeftToRight(); + + wsLeftToRight.Row(1).InsertRowsAbove(2); + wsLeftToRight.Cell(1, 1) + .SetValue(".SortLeftToRight() = Sort Range Left to Right, Ascendingly, Ignore Blanks, Ignore Case") + .Style.Font.SetBold(); + #endregion + + #region Sort a range + var wsComplex2 = wb.Worksheets.Add("Complex 2"); + AddTestTable(wsComplex2); + var rangeComplex2 = wsComplex2.RangeUsed(); + var copyComplex2 = rangeComplex2.CopyTo(wsComplex2.Column(wsComplex2.LastColumnUsed().ColumnNumber() + 3)); + + copyComplex2.SortColumns.Add(1, XLSortOrder.Ascending, false, true); + copyComplex2.SortColumns.Add(3, XLSortOrder.Descending); + copyComplex2.Sort(); + + wsComplex2.Row(1).InsertRowsAbove(4); + wsComplex2.Cell(1, 1) + .SetValue(".SortColumns.Add(1, XLSortOrder.Ascending, false, true) = Sort Col 1 Asc, Match Blanks, Match Case").Style.Font.SetBold(); + wsComplex2.Cell(2, 1) + .SetValue(".SortColumns.Add(3, XLSortOrder.Descending) = Sort Col 3 Desc, Ignore Blanks, Ignore Case").Style.Font.SetBold(); + wsComplex2.Cell(3, 1) + .SetValue(".Sort() = Sort range using the parameters defined in SortColumns").Style.Font.SetBold(); + #endregion + + #region Sort a range + var wsComplex1 = wb.Worksheets.Add("Complex 1"); + AddTestTable(wsComplex1); + var rangeComplex1 = wsComplex1.RangeUsed(); + var copyComplex1 = rangeComplex1.CopyTo(wsComplex1.Column(wsComplex1.LastColumnUsed().ColumnNumber() + 3)); + + copyComplex1.Sort("2, 1 DESC", XLSortOrder.Ascending, true); + + wsComplex1.Row(1).InsertRowsAbove(2); + wsComplex1.Cell(1, 1) + .SetValue(".Sort(\"2, 1 DESC\", XLSortOrder.Ascending, true) = Sort Range Top to Bottom, Col 2 Asc, Col 1 Desc, Ignore Blanks, Match Case").Style.Font.SetBold(); + #endregion + + #region Sort a simple column + var wsSimpleColumn = wb.Worksheets.Add("Simple Column"); + AddTestColumn(wsSimpleColumn); + var rangeSimpleColumn = wsSimpleColumn.RangeUsed(); + var copySimpleColumn = rangeSimpleColumn.CopyTo(wsSimpleColumn.Column(wsSimpleColumn.LastColumnUsed().ColumnNumber() + 3)); + + copySimpleColumn.FirstColumn().Sort(XLSortOrder.Descending, true); + + wsSimpleColumn.Row(1).InsertRowsAbove(2); + wsSimpleColumn.Cell(1, 1) + .SetValue(".Sort(XLSortOrder.Descending, true) = Sort Range Top to Bottom, Descendingly, Ignore Blanks, Match Case").Style.Font.SetBold(); + #endregion + + #region Sort a simple range + var wsSimple = wb.Worksheets.Add("Simple"); + AddTestTable(wsSimple); + var rangeSimple = wsSimple.RangeUsed(); + var copySimple = rangeSimple.CopyTo(wsSimple.Column(wsSimple.LastColumnUsed().ColumnNumber() + 3)); + + copySimple.Sort(); + + wsSimple.Row(1).InsertRowsAbove(2); + wsSimple.Cell(1, 1).SetValue(".Sort() = Sort Range Top to Bottom, Ascendingly, Ignore Blanks, Ignore Case").Style.Font.SetBold(); + #endregion + + wb.SaveAs(filePath); + } + + private void AddTestColumnMixed(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new DateTime(2011, 1, 30)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(1.15).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new TimeSpan(1, 1, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(9).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new TimeSpan(9, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new DateTime(2011, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + private void AddTestColumnNumbers(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(1.30).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(1.15).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(1230).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(9).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(4.30).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(4.15).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + private void AddTestColumnTimeSpans(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new TimeSpan(0, 12, 35, 21)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(new TimeSpan(45, 1, 15)).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new TimeSpan(1, 1, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(new TimeSpan(0, 12, 15)).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new TimeSpan(1, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new TimeSpan(1, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + private void AddTestColumnDates(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new DateTime(2011, 1, 30)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(new DateTime(2011, 1, 15)).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new DateTime(2011, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(new DateTime(2011, 12, 15)).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new DateTime(2011, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new DateTime(2011, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + private void AddTestColumn(IXLWorksheet ws) + { + ws.Cell("A1").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue("b").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue("c").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + private void AddTestTable(IXLWorksheet ws) + { + ws.Cell("A1").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + + ws.Cell("B1").SetValue("").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("B2").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("B3").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("B4").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("B5").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("B6").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("B7").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("B8").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + + ws.Cell("C1").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("C2").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("C3").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("C4").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("C5").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("C6").SetValue("b").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("C7").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("C8").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Ranges/Sorting.cs b/ClosedXML_Examples/Ranges/Sorting.cs new file mode 100644 index 0000000..b61c19c --- /dev/null +++ b/ClosedXML_Examples/Ranges/Sorting.cs @@ -0,0 +1,257 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Misc +{ + public class Sorting : IXLExample + { + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + + #region Sort Table + + var wsTable = wb.Worksheets.Add("Table"); + AddTestTable(wsTable); + + wsTable.Row(1).InsertRowsAbove(1); + Int32 lastCo = wsTable.LastColumnUsed().ColumnNumber(); + for (Int32 co = 1; co <= lastCo; co++) + wsTable.Cell(1, co).Value = "Column" + co.ToString(); + + var table = wsTable.RangeUsed().AsTable(); + table.Sort("Column2 Desc, 1, 3 Asc"); + + // Sort table another way + wsTable = wb.Worksheets.Add("Table2"); + AddTestTable(wsTable); + + wsTable.Row(1).InsertRowsAbove(1); + lastCo = wsTable.LastColumnUsed().ColumnNumber(); + for (Int32 co = 1; co <= lastCo; co++) + wsTable.Cell(1, co).Value = "Column" + co.ToString(); + + table = wsTable.RangeUsed().AsTable(); + table.Sort("Column2", XLSortOrder.Descending, false, true); + + + #endregion Sort Table + + #region Sort Rows + + var wsRows = wb.Worksheets.Add("Rows"); + AddTestTable(wsRows); + wsRows.Row(1).Sort(); + wsRows.RangeUsed().Row(2).Sort(); + wsRows.Rows(3, wsRows.LastRowUsed().RowNumber()).Delete(); + + #endregion Sort Rows + + #region Sort Columns + + var wsColumns = wb.Worksheets.Add("Columns"); + AddTestTable(wsColumns); + wsColumns.LastColumnUsed().Delete(); + wsColumns.Column(1).Sort(); + wsColumns.RangeUsed().Column(2).Sort(); + + #endregion Sort Columns + + #region Sort Mixed + + var wsMixed = wb.Worksheets.Add("Mixed"); + AddTestColumnMixed(wsMixed); + wsMixed.Sort(); + + #endregion Sort Mixed + + #region Sort Numbers + + var wsNumbers = wb.Worksheets.Add("Numbers"); + AddTestColumnNumbers(wsNumbers); + wsNumbers.Sort(); + + #endregion Sort Numbers + + #region Sort TimeSpans + + var wsTimeSpans = wb.Worksheets.Add("TimeSpans"); + AddTestColumnTimeSpans(wsTimeSpans); + wsTimeSpans.Sort(); + + #endregion Sort TimeSpans + + #region Sort Dates + + var wsDates = wb.Worksheets.Add("Dates"); + AddTestColumnDates(wsDates); + wsDates.Sort(); + + #endregion Sort Dates + + #region Do Not Ignore Blanks + + var wsIncludeBlanks = wb.Worksheets.Add("Include Blanks"); + AddTestTable(wsIncludeBlanks); + var rangeIncludeBlanks = wsIncludeBlanks; + rangeIncludeBlanks.SortColumns.Add(1, XLSortOrder.Ascending, false, true); + rangeIncludeBlanks.SortColumns.Add(2, XLSortOrder.Descending, false, true); + rangeIncludeBlanks.Sort(); + + var wsIncludeBlanksColumn = wb.Worksheets.Add("Include Blanks Column"); + AddTestColumn(wsIncludeBlanksColumn); + var rangeIncludeBlanksColumn = wsIncludeBlanksColumn; + rangeIncludeBlanksColumn.SortColumns.Add(1, XLSortOrder.Ascending, false, true); + rangeIncludeBlanksColumn.Sort(); + + var wsIncludeBlanksColumnDesc = wb.Worksheets.Add("Include Blanks Column Desc"); + AddTestColumn(wsIncludeBlanksColumnDesc); + var rangeIncludeBlanksColumnDesc = wsIncludeBlanksColumnDesc; + rangeIncludeBlanksColumnDesc.SortColumns.Add(1, XLSortOrder.Descending, false, true); + rangeIncludeBlanksColumnDesc.Sort(); + + #endregion Do Not Ignore Blanks + + #region Case Sensitive + + var wsCaseSensitive = wb.Worksheets.Add("Case Sensitive"); + AddTestTable(wsCaseSensitive); + var rangeCaseSensitive = wsCaseSensitive; + rangeCaseSensitive.SortColumns.Add(1, XLSortOrder.Ascending, true, true); + rangeCaseSensitive.SortColumns.Add(2, XLSortOrder.Descending, true, true); + rangeCaseSensitive.Sort(); + + var wsCaseSensitiveColumn = wb.Worksheets.Add("Case Sensitive Column"); + AddTestColumn(wsCaseSensitiveColumn); + var rangeCaseSensitiveColumn = wsCaseSensitiveColumn; + rangeCaseSensitiveColumn.SortColumns.Add(1, XLSortOrder.Ascending, true, true); + rangeCaseSensitiveColumn.Sort(); + + var wsCaseSensitiveColumnDesc = wb.Worksheets.Add("Case Sensitive Column Desc"); + AddTestColumn(wsCaseSensitiveColumnDesc); + var rangeCaseSensitiveColumnDesc = wsCaseSensitiveColumnDesc; + rangeCaseSensitiveColumnDesc.SortColumns.Add(1, XLSortOrder.Descending, true, true); + rangeCaseSensitiveColumnDesc.Sort(); + + #endregion Case Sensitive + + #region Simple Sorts + + var wsSimple = wb.Worksheets.Add("Simple"); + AddTestTable(wsSimple); + wsSimple.Sort(); + + var wsSimpleDesc = wb.Worksheets.Add("Simple Desc"); + AddTestTable(wsSimpleDesc); + wsSimpleDesc.Sort("", XLSortOrder.Descending); + + var wsSimpleColumns = wb.Worksheets.Add("Simple Columns"); + AddTestTable(wsSimpleColumns); + wsSimpleColumns.Sort("2, A DESC, 3"); + + var wsSimpleColumn = wb.Worksheets.Add("Simple Column"); + AddTestColumn(wsSimpleColumn); + wsSimpleColumn.Sort(); + + var wsSimpleColumnDesc = wb.Worksheets.Add("Simple Column Desc"); + AddTestColumn(wsSimpleColumnDesc); + wsSimpleColumnDesc.Sort(1, XLSortOrder.Descending); + + #endregion Simple Sorts + + wb.SaveAs(filePath); + } + } + + private void AddTestColumnMixed(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new DateTime(2011, 1, 30)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(1.15).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new TimeSpan(1, 1, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(9).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new TimeSpan(9, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new DateTime(2011, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + + private void AddTestColumnNumbers(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(1.30).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(1.15).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(1230).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(9).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(4.30).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(4.15).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + + private void AddTestColumnTimeSpans(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new TimeSpan(0, 12, 35, 21)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(new TimeSpan(45, 1, 15)).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new TimeSpan(1, 1, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(new TimeSpan(0, 12, 15)).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new TimeSpan(1, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new TimeSpan(1, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + + private void AddTestColumnDates(IXLWorksheet ws) + { + ws.Cell("A1").SetValue(new DateTime(2011, 1, 30)).Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue(new DateTime(2011, 1, 15)).Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue(new DateTime(2011, 12, 30)).Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue(new DateTime(2011, 12, 15)).Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue(new DateTime(2011, 4, 30)).Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue(new DateTime(2011, 4, 15)).Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + + private void AddTestColumn(IXLWorksheet ws) + { + ws.Cell("A1").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue("b").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue("c").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + + private void AddTestTable(IXLWorksheet ws) + { + ws.Cell("A1").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("A2").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("A3").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("A4").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("A5").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("A6").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("A7").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("A8").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + + ws.Cell("B1").SetValue("").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("B2").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("B3").SetValue("B").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("B4").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("B5").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("B6").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("B7").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("B8").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + + ws.Cell("C1").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.LightGreen); + ws.Cell("C2").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DarkTurquoise); + ws.Cell("C3").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.BurlyWood); + ws.Cell("C4").SetValue("a").Style.Fill.SetBackgroundColor(XLColor.DarkGray); + ws.Cell("C5").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.DarkSalmon); + ws.Cell("C6").SetValue("b").Style.Fill.SetBackgroundColor(XLColor.DodgerBlue); + ws.Cell("C7").SetValue("A").Style.Fill.SetBackgroundColor(XLColor.IndianRed); + ws.Cell("C8").SetValue("").Style.Fill.SetBackgroundColor(XLColor.DeepPink); + } + } +} diff --git a/ClosedXML_Examples/Ranges/TransposeRanges.cs b/ClosedXML_Examples/Ranges/TransposeRanges.cs new file mode 100644 index 0000000..bc74b0c --- /dev/null +++ b/ClosedXML_Examples/Ranges/TransposeRanges.cs @@ -0,0 +1,37 @@ +using System.IO; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples +{ + public class TransposeRanges:IXLExample + { + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + + var ws = workbook.Worksheet(1); + + var rngTable = ws.Range("B2:F6"); + + rngTable.Transpose(XLTransposeOptions.MoveCells); + + ws.Columns().AdjustToContents(); + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Ranges/TransposeRangesPlus.cs b/ClosedXML_Examples/Ranges/TransposeRangesPlus.cs new file mode 100644 index 0000000..f96ba38 --- /dev/null +++ b/ClosedXML_Examples/Ranges/TransposeRangesPlus.cs @@ -0,0 +1,46 @@ +using System.IO; +using ClosedXML.Excel; + +namespace ClosedXML_Examples +{ + public class TransposeRangesPlus : IXLExample + { + public void Create(string filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + var workbook = new XLWorkbook(tempFile); + + var ws = workbook.Worksheet(1); + + var rngTable = ws.Range("B2:F6"); + + rngTable.Row(rngTable.RowCount() - 1).Delete(XLShiftDeletedCells.ShiftCellsUp); + + // Place some markers + var cellNextRow = ws.Cell(rngTable.RangeAddress.LastAddress.RowNumber + 1, rngTable.RangeAddress.LastAddress.ColumnNumber); + cellNextRow.Value = "ColumnRight Row"; + var cellNextColumn = ws.Cell(rngTable.RangeAddress.LastAddress.RowNumber, rngTable.RangeAddress.LastAddress.ColumnNumber + 1); + cellNextColumn.Value = "ColumnRight Column"; + + rngTable.Transpose(XLTransposeOptions.MoveCells); + rngTable.Transpose(XLTransposeOptions.MoveCells); + rngTable.Transpose(XLTransposeOptions.ReplaceCells); + rngTable.Transpose(XLTransposeOptions.ReplaceCells); + + ws.Columns().AdjustToContents(); + + workbook.SaveAs(filePath); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Ranges/WalkingRanges.cs b/ClosedXML_Examples/Ranges/WalkingRanges.cs new file mode 100644 index 0000000..a6b5f75 --- /dev/null +++ b/ClosedXML_Examples/Ranges/WalkingRanges.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Ranges +{ + public class WalkingRanges : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Walking Cells"); + + var cell = ws.Cell(5, 5).SetValue("(5,5)"); + + cell.CellAbove().SetValue("(4,5)").Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + cell.CellAbove(2).SetValue("(3,5)").Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + cell.CellBelow().SetValue("(6,5)").Style.Fill.SetBackgroundColor(XLColor.Salmon); + cell.CellBelow(2).SetValue("(7,5)").Style.Fill.SetBackgroundColor(XLColor.Salmon); + + cell.CellLeft().SetValue("(5,4)").Style.Fill.SetBackgroundColor(XLColor.LightBlue); + cell.CellLeft(2).SetValue("(5,3)").Style.Fill.SetBackgroundColor(XLColor.LightBlue); + cell.CellRight().SetValue("(5,6)").Style.Fill.SetBackgroundColor(XLColor.BlueBell); + cell.CellRight(2).SetValue("(5,7)").Style.Fill.SetBackgroundColor(XLColor.BlueBell); + + var wsWalkRows = wb.Worksheets.Add("Walking rows"); + + var row = wsWalkRows.Row(3); + row.RowAbove().Style.Fill.SetBackgroundColor(XLColor.Salmon); + row.RowAbove(2).Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + row.RowBelow().Style.Fill.SetBackgroundColor(XLColor.Blue); + row.RowBelow(2).Style.Fill.SetBackgroundColor(XLColor.BlueBell); + + var rangeRow = wsWalkRows.Range("B8:D12").Row(3); + rangeRow.RowAbove().Style.Fill.SetBackgroundColor(XLColor.Salmon); + rangeRow.RowAbove(2).Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + rangeRow.RowBelow().Style.Fill.SetBackgroundColor(XLColor.Blue); + rangeRow.RowBelow(2).Style.Fill.SetBackgroundColor(XLColor.BlueBell); + + var wsWalkColumns = wb.Worksheets.Add("Walking columns"); + + var column = wsWalkColumns.Column(3); + column.ColumnLeft().Style.Fill.SetBackgroundColor(XLColor.Salmon); + column.ColumnLeft(2).Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + column.ColumnRight().Style.Fill.SetBackgroundColor(XLColor.Blue); + column.ColumnRight(2).Style.Fill.SetBackgroundColor(XLColor.BlueBell); + + var rangeColumn = wsWalkColumns.Range("H2:L4").Column(3); + rangeColumn.ColumnLeft().Style.Fill.SetBackgroundColor(XLColor.Salmon); + rangeColumn.ColumnLeft(2).Style.Fill.SetBackgroundColor(XLColor.LightSalmon); + rangeColumn.ColumnRight().Style.Fill.SetBackgroundColor(XLColor.Blue); + rangeColumn.ColumnRight(2).Style.Fill.SetBackgroundColor(XLColor.BlueBell); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Resources/ImageHandling.jpg b/ClosedXML_Examples/Resources/ImageHandling.jpg new file mode 100644 index 0000000..6ddf89a Binary files /dev/null and b/ClosedXML_Examples/Resources/ImageHandling.jpg differ diff --git a/ClosedXML_Examples/Resources/ImageHandling.png b/ClosedXML_Examples/Resources/ImageHandling.png new file mode 100644 index 0000000..cf23e54 Binary files /dev/null and b/ClosedXML_Examples/Resources/ImageHandling.png differ diff --git a/ClosedXML_Examples/Resources/SampleImage.jpg b/ClosedXML_Examples/Resources/SampleImage.jpg new file mode 100644 index 0000000..e7a7f75 Binary files /dev/null and b/ClosedXML_Examples/Resources/SampleImage.jpg differ diff --git a/ClosedXML_Examples/Rows/InsertRows.cs b/ClosedXML_Examples/Rows/InsertRows.cs new file mode 100644 index 0000000..bb519b3 --- /dev/null +++ b/ClosedXML_Examples/Rows/InsertRows.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Rows +{ + public class InsertRows : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Inserting Rows"); + + // Color the entire spreadsheet using rows + ws.Rows().Style.Fill.BackgroundColor = XLColor.LightCyan; + + // Put a value in a few cells + foreach (var r in Enumerable.Range(1, 5)) + foreach (var c in Enumerable.Range(1, 5)) + ws.Cell(r, c).Value = "X"; + + var blueRow = ws.Row(2); + var redRow = ws.Row(5); + + blueRow.Style.Fill.BackgroundColor = XLColor.Blue; + blueRow.InsertRowsBelow(2); + + + redRow.Style.Fill.BackgroundColor = XLColor.Red; + redRow.InsertRowsAbove(2); + + ws.Columns(3, 4).Style.Fill.BackgroundColor = XLColor.Orange; + ws.Range("A2:A4").InsertRowsBelow(2); + ws.Range("B2:B4").InsertRowsAbove(2); + ws.Range("C2:C4").InsertRowsBelow(2); + ws.Range("D2:D4").InsertRowsAbove(2); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Rows/RowCells.cs b/ClosedXML_Examples/Rows/RowCells.cs new file mode 100644 index 0000000..2add1eb --- /dev/null +++ b/ClosedXML_Examples/Rows/RowCells.cs @@ -0,0 +1,73 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples +{ + public class RowCells : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Row Cells"); + + var rowFromWorksheet = ws.Row(1); + rowFromWorksheet.Cell(1).Style.Fill.BackgroundColor = XLColor.Red; + rowFromWorksheet.Cells("2").Style.Fill.BackgroundColor = XLColor.Blue; + rowFromWorksheet.Cells("3,5:6").Style.Fill.BackgroundColor = XLColor.Red; + rowFromWorksheet.Cells(8, 9).Style.Fill.BackgroundColor = XLColor.Blue; + + var rowFromRange = ws.Range("A2:I2").FirstRow(); + + rowFromRange.Cell(1).Style.Fill.BackgroundColor = XLColor.Red; + rowFromRange.Cells("2").Style.Fill.BackgroundColor = XLColor.Blue; + rowFromRange.Cells("3,5:6").Style.Fill.BackgroundColor = XLColor.Red; + rowFromRange.Cells(8, 9).Style.Fill.BackgroundColor = XLColor.Blue; + + ws.Columns().Width = 7; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Rows/RowCollection.cs b/ClosedXML_Examples/Rows/RowCollection.cs new file mode 100644 index 0000000..31e63c1 --- /dev/null +++ b/ClosedXML_Examples/Rows/RowCollection.cs @@ -0,0 +1,96 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Rows +{ + public class RowCollection : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Rows of a Range"); + + // All rows in a range + ws.Range("A1:B2").Rows().Style.Fill.BackgroundColor = XLColor.DimGray; + + var bigRange = ws.Range("B4:C17"); + + // Contiguous rows by number + bigRange.Rows(1, 2).Style.Fill.BackgroundColor = XLColor.Red; + + // Contiguous rows by number + bigRange.Rows("4:5").Style.Fill.BackgroundColor = XLColor.Blue; + + // Spread rows by number + bigRange.Rows("7:8,10:11").Style.Fill.BackgroundColor = XLColor.Orange; + + // Using a single number + bigRange.Rows("13").Style.Fill.BackgroundColor = XLColor.Cyan; + + // Adjust the height + ws.Rows().Height = 15; + + var ws2 = workbook.Worksheets.Add("Rows of a Worksheet"); + + // Contiguous rows by number + ws2.Rows(1, 2).Style.Fill.BackgroundColor = XLColor.Red; + + // Contiguous rows by number + ws2.Rows("4:5").Style.Fill.BackgroundColor = XLColor.Blue; + + // Spread rows by number + ws2.Rows("7:8,10:11").Style.Fill.BackgroundColor = XLColor.Orange; + + // Using a single number + ws2.Rows("13").Style.Fill.BackgroundColor = XLColor.Cyan; + + // Adjust the height + ws2.Rows("1:13").Height = 15; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Rows/RowSettings.cs b/ClosedXML_Examples/Rows/RowSettings.cs new file mode 100644 index 0000000..ea402ff --- /dev/null +++ b/ClosedXML_Examples/Rows/RowSettings.cs @@ -0,0 +1,80 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Rows +{ + public class RowSettings : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + public RowSettings() + { + + } + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Row Settings"); + + var row1 = ws.Row(2); + row1.Style.Fill.BackgroundColor = XLColor.Red; + row1.Height = 30; + + var row2 = ws.Row(4); + row2.Style.Fill.BackgroundColor = XLColor.DarkOrange; + row2.Height = 3; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Sparklines/SampleSparklines.cs b/ClosedXML_Examples/Sparklines/SampleSparklines.cs new file mode 100644 index 0000000..c3de1ae --- /dev/null +++ b/ClosedXML_Examples/Sparklines/SampleSparklines.cs @@ -0,0 +1,98 @@ +using ClosedXML.Excel; +using MoreLinq; +using System; +using System.Linq; + +namespace ClosedXML_Examples.Sparklines +{ + public class SampleSparklines : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws1 = workbook.AddWorksheet("Linear"); + + FillSampleData(ws1); + + ws1.Range("A2:A4").Merge().SetValue("Linear, Colorful 1, All markers, SameForAll scale"); + ws1.SparklineGroups.Add("B2:B4", "C2:P4") + .SetStyle(XLSparklineTheme.Colorful1) + .SetShowMarkers(XLSparklineMarkers.All).VerticalAxis + .SetMaxAxisType(XLSparklineAxisMinMax.SameForAll) + .SetMinAxisType(XLSparklineAxisMinMax.SameForAll); + + ws1.Range("A5:A7").Merge().SetValue("Linear, Colorful 2, First+Last+High+Low, Automatic scale"); + ws1.SparklineGroups.Add("B5:B7", "C5:P7") + .SetStyle(XLSparklineTheme.Colorful2) + .SetShowMarkers(XLSparklineMarkers.FirstPoint | XLSparklineMarkers.LastPoint | XLSparklineMarkers.HighPoint | XLSparklineMarkers.LowPoint); + + ws1.Range("A8:A10").Merge().SetValue("Linear, Colorful 3, Markers+Negative, Custom scale"); + ws1.SparklineGroups.Add("B8:B10", "C8:P10") + .SetStyle(XLSparklineTheme.Colorful3) + .SetShowMarkers(XLSparklineMarkers.Markers | XLSparklineMarkers.NegativePoints) + .VerticalAxis + .SetManualMax(100) + .SetManualMin(-80); + + ws1.Range("A11:A13").Merge().SetValue("Linear, Colorful 1, Date range"); + ws1.SparklineGroups.Add("B11:B13", "C11:P13") + .SetStyle(XLSparklineTheme.Colorful1) + .SetDateRange(ws1.Range("C1:P1")); + + ws1.Range("A14:A16").Merge().SetValue("Linear, Colorful 4, Line weight=2, Right to left"); + ws1.SparklineGroups.Add("B14:B16", "C14:P16") + .SetStyle(XLSparklineTheme.Colorful4) + .SetLineWeight(2) + .HorizontalAxis + .SetVisible(true) + .SetColor(XLColor.Red) + .SetRightToLeft(true); + + ws1.Range("A17:A19").Merge().SetValue("Linear, Colorful 3, Different ranges"); + ws1.SparklineGroups.Add("B17", "C17:P17") + .Add("B18", "C18:K18").Single().SparklineGroup + .Add("B19", "C19:E19").Single().SparklineGroup + .SetStyle(XLSparklineTheme.Colorful3) + .SetShowMarkers(XLSparklineMarkers.FirstPoint | XLSparklineMarkers.LastPoint); + + + var ws2 = ws1.CopyTo("Column"); + ws2.SparklineGroups.ForEach(g => + g.SetType(XLSparklineType.Column)); + + ws2.Cell("A2").Value = "Column, Colorful 1, All markers, SameForAll scale"; + ws2.Cell("A5").Value = "Column, Colorful 2, First+Last+High+Low, Automatic scale"; + ws2.Cell("A8").Value = "Column, Colorful 3, Markers+Negative, Custom scale"; + ws2.Cell("A11").Value = "Column, Colorful 1, Date range"; + ws2.Cell("A14").Value = "Column, Colorful 4, Line weight=2, Right to left"; + ws2.Cell("A17").Value = "Column, Colorful 3, Different ranges"; + + + var ws3 = ws1.CopyTo("Stacked"); + ws3.SparklineGroups.ForEach(g => + g.SetType(XLSparklineType.Stacked)); + + ws3.Cell("A2").Value = "Stacked, Colorful 1, All markers, SameForAll scale"; + ws3.Cell("A5").Value = "Stacked, Colorful 2, First+Last+High+Low, Automatic scale"; + ws3.Cell("A8").Value = "Stacked, Colorful 3, Markers+Negative, Custom scale"; + ws3.Cell("A11").Value = "Stacked, Colorful 1, Date range"; + ws3.Cell("A14").Value = "Stacked, Colorful 4, Line weight=2, Right to left"; + ws3.Cell("A17").Value = "Stacked, Colorful 3, Different ranges"; + + workbook.SaveAs(filePath); + } + + private void FillSampleData(IXLWorksheet ws) + { + ws.Column(1).Style.Alignment.SetWrapText(true); + ws.Column(1).Width = 30; + ws.Column(2).Width = 30; + + ws.Range("C1:P1").Cells() + .ForEach(c => c.Value = new DateTime(2016, 1, 1).AddDays(c.Address.ColumnNumber * 7)); + + ws.Range("C2:P19").Cells() + .ForEach(c => c.Value = Math.Round(c.Address.RowNumber * Math.Sin(c.Address.ColumnNumber) * 10, 0)); + } + } +} diff --git a/ClosedXML_Examples/StyleExamples.cs b/ClosedXML_Examples/StyleExamples.cs new file mode 100644 index 0000000..869c9ab --- /dev/null +++ b/ClosedXML_Examples/StyleExamples.cs @@ -0,0 +1,74 @@ +using ClosedXML_Examples.Styles; +using System.IO; + +namespace ClosedXML_Examples +{ + public class StyleExamples + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + public StyleExamples() + { + + } + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create() + { + var path = Program.BaseCreatedDirectory; + new StyleFont().Create(Path.Combine(path, "styleFont.xlsx")); + new StyleFill().Create(Path.Combine(path, "styleFill.xlsx")); + new StyleBorder().Create(Path.Combine(path, "styleBorder.xlsx")); + new StyleAlignment().Create(Path.Combine(path, "styleAlignment.xlsx")); + new StyleNumberFormat().Create(Path.Combine(path, "styleNumberFormat.xlsx")); + new StyleIncludeQuotePrefix().Create(Path.Combine(path, "styleIncludeQuotePrefix.xlsx")); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Styles/DefaultStyles.cs b/ClosedXML_Examples/Styles/DefaultStyles.cs new file mode 100644 index 0000000..f940844 --- /dev/null +++ b/ClosedXML_Examples/Styles/DefaultStyles.cs @@ -0,0 +1,29 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class DefaultStyles : IXLExample + { + public void Create(String filePath) + { + // Create our workbook + var workbook = new XLWorkbook(); + + // This worksheet will have the default style, row height, column width, and page setup + var ws1 = workbook.Worksheets.Add("Default Style"); + + // Change the default row height for all new worksheets in this workbook + workbook.RowHeight = 30; + + var ws2 = workbook.Worksheets.Add("Tall Rows"); + + // Create a worksheet and change the default row height + var ws3 = workbook.Worksheets.Add("Short Rows"); + ws3.RowHeight = 7.5; + + workbook.SaveAs(filePath); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Styles/PurpleWorksheet.cs b/ClosedXML_Examples/Styles/PurpleWorksheet.cs new file mode 100644 index 0000000..a42856d --- /dev/null +++ b/ClosedXML_Examples/Styles/PurpleWorksheet.cs @@ -0,0 +1,19 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class PurpleWorksheet : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Purple Worksheet"); + + ws.Style.Fill.BackgroundColor = XLColor.Purple; + + workbook.SaveAs(filePath); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Styles/StyleAlignment.cs b/ClosedXML_Examples/Styles/StyleAlignment.cs new file mode 100644 index 0000000..ca6fcb9 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleAlignment.cs @@ -0,0 +1,105 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class StyleAlignment : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + public StyleAlignment() + { + + } + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Alignment"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co).Value = "Horizontal = Right"; + ws.Cell(ro, co).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Right; + + ws.Cell(++ro, co).Value = "Indent = 2"; + ws.Cell(ro, co).Style.Alignment.Indent = 2; + + ws.Cell(++ro, co).Value = "JustifyLastLine = true"; + ws.Cell(ro, co).Style.Alignment.JustifyLastLine = true; + + ws.Cell(++ro, co).Value = "ReadingOrder = ContextDependent"; + ws.Cell(ro, co).Style.Alignment.ReadingOrder = XLAlignmentReadingOrderValues.ContextDependent; + + ws.Cell(++ro, co).Value = "RelativeIndent = 2"; + ws.Cell(ro, co).Style.Alignment.RelativeIndent = 2; + + ws.Cell(++ro, co).Value = "ShrinkToFit = true"; + ws.Cell(ro, co).Style.Alignment.ShrinkToFit = true; + + ws.Cell(++ro, co).Value = "TextRotation = 45"; + ws.Cell(ro, co).Style.Alignment.TextRotation = 45; + + ws.Cell(++ro, co).Value = "TopToBottom = true"; + ws.Cell(ro, co).Style.Alignment.TopToBottom = true; + + ws.Cell(++ro, co).Value = "Vertical = Center"; + ws.Cell(ro, co).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; + + ws.Cell(++ro, co).Value = "WrapText = true"; + ws.Cell(ro, co).Style.Alignment.WrapText = true; + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Styles/StyleBorder.cs b/ClosedXML_Examples/Styles/StyleBorder.cs new file mode 100644 index 0000000..bffc64d --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleBorder.cs @@ -0,0 +1,52 @@ +using System; + +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Styles +{ + public class StyleBorder : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Border"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co).Value = "BottomBorder = Thick; BottomBorderColor = Red"; + ws.Cell(ro, co).Style.Border.BottomBorder = XLBorderStyleValues.Thick; + ws.Cell(ro, co).Style.Border.BottomBorderColor = XLColor.Red; + + ws.Cell(++ro, co).Value = "LeftBorder = Thick; LeftBorderColor = Blue"; + ws.Cell(ro, co).Style.Border.LeftBorder = XLBorderStyleValues.Thick; + ws.Cell(ro, co).Style.Border.LeftBorderColor = XLColor.Blue; + + ws.Cell(++ro, co).Value = "TopBorder = Thick; TopBorderColor = Yellow"; + ws.Cell(ro, co).Style.Border.TopBorder = XLBorderStyleValues.Thick; + ws.Cell(ro, co).Style.Border.TopBorderColor = XLColor.Yellow; + + ws.Cell(++ro, co).Value = "RightBorder = Thick; RightBorderColor = Black"; + ws.Cell(ro, co).Style.Border.RightBorder = XLBorderStyleValues.Thick; + ws.Cell(ro, co).Style.Border.RightBorderColor = XLColor.Black; + + ws.Cell(++ro, co).Value = "DiagonalBorder = Thin; DiagonalBorderColor = Red; DiagonalUp = true"; + ws.Cell(ro, co).Style.Border.DiagonalBorder = XLBorderStyleValues.Thin; + ws.Cell(ro, co).Style.Border.DiagonalBorderColor = XLColor.Red; + ws.Cell(ro, co).Style.Border.DiagonalUp = true; + + ws.Cell(++ro, co).Value = "DiagonalBorder = Thin; DiagonalBorderColor = Red; DiagonalDown = true"; + ws.Cell(ro, co).Style.Border.DiagonalBorder = XLBorderStyleValues.Thin; + ws.Cell(ro, co).Style.Border.DiagonalBorderColor = XLColor.Red; + ws.Cell(ro, co).Style.Border.DiagonalDown = true; + + ws.Cell(++ro, co).Value = "DiagonalBorder = Thin; DiagonalBorderColor = Red; DiagonalUp = true; DiagonalDown = true"; + ws.Cell(ro, co).Style.Border.DiagonalBorder = XLBorderStyleValues.Thin; + ws.Cell(ro, co).Style.Border.DiagonalBorderColor = XLColor.Red; + ws.Cell(ro, co).Style.Border.DiagonalUp = true; + ws.Cell(ro, co).Style.Border.DiagonalDown = true; + + workbook.SaveAs(filePath); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Styles/StyleFill.cs b/ClosedXML_Examples/Styles/StyleFill.cs new file mode 100644 index 0000000..8a85791 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleFill.cs @@ -0,0 +1,27 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Styles +{ + public class StyleFill : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Fill"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co + 1).Value = "BackgroundColor = Red"; + ws.Cell(ro, co).Style.Fill.BackgroundColor = XLColor.Red; + + ws.Cell(++ro, co + 1).Value = "PatternType = DarkTrellis; PatternColor = Orange; BackgroundColor = Blue"; + ws.Cell(ro, co).Style.Fill.PatternType = XLFillPatternValues.DarkTrellis; + ws.Cell(ro, co).Style.Fill.PatternColor = XLColor.Orange; + ws.Cell(ro, co).Style.Fill.BackgroundColor = XLColor.Blue; + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Styles/StyleFont.cs b/ClosedXML_Examples/Styles/StyleFont.cs new file mode 100644 index 0000000..6fed30f --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleFont.cs @@ -0,0 +1,53 @@ +using ClosedXML.Excel; +using System; + +namespace ClosedXML_Examples.Styles +{ + public class StyleFont : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Font"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co).Value = "Bold"; + ws.Cell(ro, co).Style.Font.Bold = true; + + ws.Cell(++ro, co).Value = "FontColor - Red"; + ws.Cell(ro, co).Style.Font.FontColor = XLColor.Red; + + ws.Cell(++ro, co).Value = "FontFamilyNumbering - Script"; + ws.Cell(ro, co).Style.Font.FontFamilyNumbering = XLFontFamilyNumberingValues.Script; + + ws.Cell(++ro, co).Value = "FontCharSet - العربية التنضيد"; + ws.Cell(ro, co).Style + .Font.SetFontName("Arabic Typesetting") + .Font.SetFontCharSet(XLFontCharSet.Arabic); + + ws.Cell(++ro, co).Value = "FontName - Stencil"; + ws.Cell(ro, co).Style.Font.FontName = "Stencil"; + + ws.Cell(++ro, co).Value = "FontSize - 15"; + ws.Cell(ro, co).Style.Font.FontSize = 15; + + ws.Cell(++ro, co).Value = "Italic - true"; + ws.Cell(ro, co).Style.Font.Italic = true; + + ws.Cell(++ro, co).Value = "Strikethrough - true"; + ws.Cell(ro, co).Style.Font.Strikethrough = true; + + ws.Cell(++ro, co).Value = "Underline - Double"; + ws.Cell(ro, co).Style.Font.Underline = XLFontUnderlineValues.Double; + + ws.Cell(++ro, co).Value = "VerticalAlignment - Superscript"; + ws.Cell(ro, co).Style.Font.VerticalAlignment = XLFontVerticalTextAlignmentValues.Superscript; + + ws.Column(co).AdjustToContents(); + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Styles/StyleIncludeQuotePrefix.cs b/ClosedXML_Examples/Styles/StyleIncludeQuotePrefix.cs new file mode 100644 index 0000000..47129c6 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleIncludeQuotePrefix.cs @@ -0,0 +1,39 @@ +using ClosedXML.Excel; +using System; +using System.Linq; + +namespace ClosedXML_Examples.Styles +{ + public class StyleIncludeQuotePrefix : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style IncludeQuotePrefix"); + + var data = Enumerable.Range(1, 20) + .Select(i => + new + { + IntegerIndex = i, + StringIndex = i.ToString(), + PaddedString1000 = (i * 1000).ToString().PadLeft(8, '0'), + PrependedString1000 = "Str" + (i * 1000).ToString().PadLeft(8, '0') + }); + + ws.FirstCell().InsertData(data); + + // Columns B to D will be of type text + // but column B will not have the leading quotation mark + ws.Column("B").Style.IncludeQuotePrefix = false; + + // Columns C and D will have the leading quotation mark + ws.Column("C").Style.IncludeQuotePrefix = true; + ws.Column("D").Style.SetIncludeQuotePrefix(); + + ws.Columns().AdjustToContents(); + + workbook.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Styles/StyleNumberFormat.cs b/ClosedXML_Examples/Styles/StyleNumberFormat.cs new file mode 100644 index 0000000..0f6e7e6 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleNumberFormat.cs @@ -0,0 +1,85 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Styles +{ + public class StyleNumberFormat : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + public StyleNumberFormat() + { + + } + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style NumberFormat"); + + var co = 2; + var ro = 1; + + ws.Cell(++ro, co).Value = "123456.789"; + ws.Cell(ro, co).Style.NumberFormat.Format = "$ #,##0.00"; + + ws.Cell(++ro, co).Value = "12.345"; + ws.Cell(ro, co).Style.NumberFormat.Format = "0000"; + + ws.Cell(++ro, co).Value = "12.345"; + ws.Cell(ro, co).Style.NumberFormat.NumberFormatId = 3; + + ws.Column(co).AdjustToContents(); + + workbook.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Styles/StyleRowsColumns.cs b/ClosedXML_Examples/Styles/StyleRowsColumns.cs new file mode 100644 index 0000000..a12a0b3 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleRowsColumns.cs @@ -0,0 +1,34 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class StyleRowsColumns : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Rows and Columns"); + + //Set the entire worksheet's cells to be bold and with a light cyan background + ws.Style.Font.Bold = true; + ws.Style.Fill.BackgroundColor = XLColor.LightCyan; + + // Set the width of all columns in the worksheet + ws.Columns().Width = 5; + + // Set the height of all rows in the worksheet + ws.Rows().Height = 20; + + // Let's play with the rows and columns + ws.Rows(2, 3).Style.Fill.BackgroundColor = XLColor.Blue; + ws.Columns(3, 4).Style.Fill.BackgroundColor = XLColor.Orange; + ws.Rows(5, 5).Style.Fill.BackgroundColor = XLColor.Pink; + ws.Row(6).Style.Fill.BackgroundColor = XLColor.Brown; + ws.Column("E").Style.Fill.BackgroundColor = XLColor.Gray; + + workbook.SaveAs(filePath); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Styles/StyleWorksheet.cs b/ClosedXML_Examples/Styles/StyleWorksheet.cs new file mode 100644 index 0000000..d17e4e3 --- /dev/null +++ b/ClosedXML_Examples/Styles/StyleWorksheet.cs @@ -0,0 +1,42 @@ +using System; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class StyleWorksheet : IXLExample + { + public void Create(String filePath) + { + var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("Style Worksheet"); + + ws.Style.Font.Bold = true; + ws.Style.Font.FontColor = XLColor.Red; + ws.Style.Fill.BackgroundColor = XLColor.Cyan; + + // The following cells will be bold and red + // because we've specified those attributes to the entire worksheet + ws.Cell(1, 1).Value = "Test"; + ws.Cell(1, 2).Value = "Case"; + + // Here we'll change the style of a single cell + ws.Cell(2, 1).Value = "Default"; + ws.Cell(2, 1).Style = XLWorkbook.DefaultStyle; + + // Let's play with some rows + ws.Row(4).Style = XLWorkbook.DefaultStyle; + ws.Row(4).Height = 20; + ws.Rows(5, 6).Style = XLWorkbook.DefaultStyle; + ws.Rows(5, 6).Height = 20; + + // Let's play with some columns + ws.Column(4).Style = XLWorkbook.DefaultStyle; + ws.Column(4).Width = 5; + ws.Columns(5, 6).Style = XLWorkbook.DefaultStyle; + ws.Columns(5, 6).Width = 5; + + workbook.SaveAs(filePath); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Examples/Styles/UsingColors.cs b/ClosedXML_Examples/Styles/UsingColors.cs new file mode 100644 index 0000000..54fea8f --- /dev/null +++ b/ClosedXML_Examples/Styles/UsingColors.cs @@ -0,0 +1,84 @@ +using System; +using System.Drawing; +using ClosedXML.Excel; + + +namespace ClosedXML_Examples.Styles +{ + public class UsingColors : IXLExample + { + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Using Colors"); + + Int32 ro = 0; + + // From Known color + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell(ro, 2).Value = "XLColor.Red"; + + // From Color not so known + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.Byzantine; + ws.Cell(ro, 2).Value = "XLColor.Byzantine"; + + ro++; + + // FromArgb(Int32 argb) using Hex notation + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromArgb(0xFF00FF); + ws.Cell(ro, 2).Value = "XLColor.FromArgb(0xFF00FF)"; + + // FromArgb(Int32 argb) using an integer (you need to convert the hex value to an int) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromArgb(16711935); + ws.Cell(ro, 2).Value = "XLColor.FromArgb(16711935)"; + + // FromArgb(Int32 r, Int32 g, Int32 b) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromArgb(255, 0, 255); + ws.Cell(ro, 2).Value = "XLColor.FromArgb(255, 0, 255)"; + + // FromArgb(Int32 a, Int32 r, Int32 g, Int32 b) + // Note: Excel ignores the alpha value + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromArgb(0, 255, 0, 255); + ws.Cell(ro, 2).Value = "XLColor.FromArgb(0, 255, 0, 255)"; + + ro++; + + // FromColor(Color color) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromColor(Color.Red); + ws.Cell(ro, 2).Value = "XLColor.FromColor(Color.Red)"; + + ro++; + + // FromHtml(String htmlColor) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#FF996515"); + ws.Cell(ro, 2).Value = "XLColor.FromHtml(\"#FF996515\")"; + + ro++; + + // FromIndex(Int32 indexedColor) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromIndex(25); + ws.Cell(ro, 2).Value = "XLColor.FromIndex(25)"; + + ro++; + + // FromName(String colorName) + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromName("PowderBlue"); + ws.Cell(ro, 2).Value = "XLColor.FromName(\"PowderBlue\")"; + + ro++; + + // From Theme color + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromTheme(XLThemeColor.Accent1); + ws.Cell(ro, 2).Value = "XLColor.FromTheme(XLThemeColor.Accent1)"; + + // From Theme color with tint + ws.Cell(++ro, 1).Style.Fill.BackgroundColor = XLColor.FromTheme(XLThemeColor.Accent1, 0.5); + ws.Cell(ro, 2).Value = "XLColor.FromTheme(XLThemeColor.Accent1, 0.5)"; + + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + } +} diff --git a/ClosedXML_Examples/Styles/UsingPhonetics.cs b/ClosedXML_Examples/Styles/UsingPhonetics.cs new file mode 100644 index 0000000..f6c6483 --- /dev/null +++ b/ClosedXML_Examples/Styles/UsingPhonetics.cs @@ -0,0 +1,41 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Styles +{ + public class UsingPhonetics : IXLExample + { + + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Using Phonetics"); + + var cell = ws.Cell(1, 1); + + // Phonetics are implemented as part of the Rich Text functionality. For more information see [Using Rich Text] + // First we add the text. + cell.RichText.AddText("みんなさんはお元気ですか。").SetFontSize(16); + + // And then we add the phonetics + cell.RichText.Phonetics.SetFontSize(8); + cell.RichText.Phonetics.Add("げん", 7, 8); + cell.RichText.Phonetics.Add("き", 8, 9); + + //TODO: I'm looking for someone who understands Japanese to confirm the validity of the above code. + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Styles/UsingRichText.cs b/ClosedXML_Examples/Styles/UsingRichText.cs new file mode 100644 index 0000000..7709a98 --- /dev/null +++ b/ClosedXML_Examples/Styles/UsingRichText.cs @@ -0,0 +1,121 @@ +using System; +using ClosedXML.Excel; + +namespace ClosedXML_Examples.Styles +{ + public class UsingRichText : IXLExample + { + #region Variables + + // Public + + // Private + + + #endregion + + #region Properties + + // Public + + // Private + + // Override + + + #endregion + + #region Constructors + + // Public + + + + // Private + + + #endregion + + #region Events + + // Public + + // Private + + // Override + + + #endregion + + #region Methods + + // Public + public void Create(String filePath) + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Rich Text"); + + // Let's start with a plain text and then decorate it... + var cell1 = ws.Cell(1, 1).SetValue("The show must go on..."); + + // We want everything in blue except the word show + // (which we want in red and with Courier Font) + cell1.Style.Font.FontColor = XLColor.Blue; // Set the color for the entire cell + cell1.RichText.Substring(4, 4) + .SetFontColor(XLColor.Red) + .SetFontName("Courier"); // Set the color and font for the word "show" + + // On the next example we'll start with an empty cell and add the rich text + var cell = ws.Cell(3, 1); + + // Add the text parts + cell.RichText.AddText("Hello").SetFontColor(XLColor.Red); + cell.RichText.AddText(" BIG ").SetFontColor(XLColor.Blue).SetBold(); + cell.RichText.AddText("World").SetFontColor(XLColor.Red); + + // Here we're showing that even though we added three pieces of text + // you can treat then like a single one. + cell.RichText.Substring(4, 7).SetUnderline(); + + // Right now cell.RichText has the following 5 strings: + // + // "Hell" -> Red + // "o" -> Red, Underlined + // " BIG " -> Blue, Underlined, Bold + // "W" -> Red, Underlined + // "orld" -> Red + + // Of course you can loop through each piece of text and check its properties + foreach (var richText in cell.RichText) + { + if(richText.Bold) + ws.Cell(3, 2).Value = String.Format("\"{0}\" is Bold.", richText.Text); + } + + + // Now we'll build a cell with rich text, and some other styles + cell = ws.Cell(5, 1); + + // Add the text parts + cell.RichText.AddText("Some").SetFontColor(XLColor.Green); + cell.RichText.AddText(" rich text ").SetFontColor(XLColor.Blue).SetBold(); + cell.RichText.AddText("with a gray background").SetItalic(); + + cell.Style.Fill.SetBackgroundColor(XLColor.Gray); + + ws.Cell(5, 2).Value = cell.RichText; // Should copy only rich text, but not background + + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + + // Private + + // Override + + + #endregion + } +} diff --git a/ClosedXML_Examples/Tables/InsertingTables.cs b/ClosedXML_Examples/Tables/InsertingTables.cs new file mode 100644 index 0000000..176eed1 --- /dev/null +++ b/ClosedXML_Examples/Tables/InsertingTables.cs @@ -0,0 +1,111 @@ +using ClosedXML.Attributes; +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace ClosedXML_Examples.Tables +{ + public class InsertingTables : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Inserting Tables"); + + // From a list of strings + var listOfStrings = new List(); + listOfStrings.Add("House"); + listOfStrings.Add("Car"); + ws.Cell(1, 1).Value = "From Strings"; + ws.Cell(1, 1).AsRange().AddToNamed("Titles"); + ws.Cell(2, 1).InsertTable(listOfStrings); + + // From a list of arrays + var listOfArr = new List(); + listOfArr.Add(new Int32[] { 1, 2, 3 }); + listOfArr.Add(new Int32[] { 1 }); + listOfArr.Add(new Int32[] { 1, 2, 3, 4, 5, 6 }); + ws.Cell(1, 3).Value = "From Arrays"; + ws.Range(1, 3, 1, 8).Merge().AddToNamed("Titles"); + ws.Cell(2, 3).InsertTable(listOfArr); + + // From a DataTable + var dataTable = GetTable(); + ws.Cell(7, 1).Value = "From DataTable"; + ws.Range(7, 1, 7, 4).Merge().AddToNamed("Titles"); + ws.Cell(8, 1).InsertTable(dataTable); + + // From a query + var list = new List(); + list.Add(new Person() { Name = "John", Age = 30, House = "On Elm St." }); + list.Add(new Person() { Name = "Mary", Age = 15, House = "On Main St." }); + list.Add(new Person() { Name = "Luis", Age = 21, House = "On 23rd St." }); + list.Add(new Person() { Name = "Henry", Age = 45, House = "On 5th Ave." }); + + var people = from p in list + where p.Age >= 21 + select p; + + ws.Cell(7, 6).Value = "From Query"; + ws.Range(7, 6, 7, 9).Merge().AddToNamed("Titles"); + ws.Cell(8, 6).InsertTable(people); + + ws.Cell(15, 6).Value = "From List"; + ws.Range(15, 6, 15, 9).Merge().AddToNamed("Titles"); + ws.Cell(16, 6).InsertTable(people); + + // Prepare the style for the titles + var titlesStyle = wb.Style; + titlesStyle.Font.Bold = true; + titlesStyle.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + titlesStyle.Fill.BackgroundColor = XLColor.Cyan; + + // Format all titles in one shot + wb.NamedRanges.NamedRange("Titles").Ranges.Style = titlesStyle; + + ws.Columns().AdjustToContents(); + + wb.SaveAs(filePath); + } + } + + private class Person + { + [XLColumn(Header = "House Street")] + public String House { get; set; } + + public String Name { get; set; } + public Int32 Age { get; set; } + + [XLColumn(Header = "Class Type")] + public static String ClassType { get { return nameof(Person); } } + } + + // Private + private DataTable GetTable() + { + DataTable table = new DataTable(); + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", "Janet", new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", new DateTime(2000, 1, 5)); + return table; + } + + // Override + + #endregion Methods + } +} diff --git a/ClosedXML_Examples/Tables/ResizingTables.cs b/ClosedXML_Examples/Tables/ResizingTables.cs new file mode 100644 index 0000000..0ef7713 --- /dev/null +++ b/ClosedXML_Examples/Tables/ResizingTables.cs @@ -0,0 +1,48 @@ +using ClosedXML.Excel; +using MoreLinq; +using System; +using System.Linq; + +// TODO: Add example to Wiki + +namespace ClosedXML_Examples.Tables +{ + public class ResizingTables : IXLExample + { + public void Create(string filePath) + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + + var data1 = Enumerable.Range(1, 10) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('a', i), + Integer = 64 + i + }); + + var table1 = ws1.Cell("B2").InsertTable(data1, true) + .SetShowHeaderRow() + .SetShowTotalsRow(); + + table1.Fields.First().TotalsRowLabel = "Sum of Integer"; + table1.Fields.Last().TotalsRowFunction = XLTotalsRowFunction.Sum; + + var ws2 = ws1.CopyTo("Sheet2"); + var table2 = ws2.Tables.First(); + table2.Resize(table2.FirstCell(), table2.LastCell().CellLeft().CellAbove(3)); + + var ws3 = ws2.CopyTo("Sheet3"); + var table3 = ws3.Tables.First(); + table3.Resize(table3.FirstCell().CellLeft(), table3.LastCell().CellRight().CellBelow(1)); + + wb.Worksheets.ForEach(ws => ws.Columns().AdjustToContents()); + wb.SaveAs(filePath); + } + } + } +} diff --git a/ClosedXML_Examples/Tables/UsingTables.cs b/ClosedXML_Examples/Tables/UsingTables.cs new file mode 100644 index 0000000..3dd012d --- /dev/null +++ b/ClosedXML_Examples/Tables/UsingTables.cs @@ -0,0 +1,116 @@ +using ClosedXML.Excel; +using System; +using System.IO; + +namespace ClosedXML_Examples.Tables +{ + public class UsingTables : IXLExample + { + #region Methods + + // Public + public void Create(String filePath) + { + string tempFile = ExampleHelper.GetTempFilePath(filePath); + try + { + new BasicTable().Create(tempFile); + using (var wb = new XLWorkbook(tempFile)) + { + var ws = wb.Worksheet(1); + ws.Name = "Contacts Table"; + var firstCell = ws.FirstCellUsed(); + var lastCell = ws.LastCellUsed(); + var range = ws.Range(firstCell.Address, lastCell.CellRight().Address); + range.FirstRow().Delete(); // Deleting the "Contacts" header (we don't need it for our purposes) + + // We want to use a theme for table, not the hard coded format of the BasicTable + range.Clear(XLClearOptions.AllFormats); + // Put back the date and number formats + range.Column(4).Style.NumberFormat.NumberFormatId = 15; + range.Column(5).Style.NumberFormat.Format = "$ #,##0"; + + // Add a field + range.Column(6).FirstCell().SetValue("Age"); + var c = range.Column(6).FirstCell().CellBelow(); + c.Style.NumberFormat.SetFormat("0.00"); + c.FormulaA1 = "=(DATE(2017, 10, 3) - E3) / 365"; + + c.CopyTo(c.CellBelow()) + .CopyTo(c.CellBelow().CellBelow()); + + var table = range.CreateTable(); // You can also use range.AsTable() if you want to + // manipulate the range as a table but don't want + // to create the table in the worksheet. + + // Let's activate the Totals row and add the sum of Income + table.ShowTotalsRow = true; + table.Field("Income").TotalsRowFunction = XLTotalsRowFunction.Sum; + // Just for fun let's add the text "Sum Of Income" to the totals row + table.Field(0).TotalsRowLabel = "Sum Of Income"; + + table.Field("Age").TotalsRowFunction = XLTotalsRowFunction.Average; + + // Copy all the headers + Int32 columnWithHeaders = lastCell.Address.ColumnNumber + 3; + Int32 currentRow = table.RangeAddress.FirstAddress.RowNumber; + ws.Cell(currentRow, columnWithHeaders).Value = "Table Headers"; + foreach (var cell in table.HeadersRow().Cells()) + { + currentRow++; + ws.Cell(currentRow, columnWithHeaders).Value = cell.Value; + } + + // Format the headers as a table with a different style and no autofilters + var htFirstCell = ws.Cell(table.RangeAddress.FirstAddress.RowNumber, columnWithHeaders); + var htLastCell = ws.Cell(currentRow, columnWithHeaders); + var headersTable = ws.Range(htFirstCell, htLastCell).CreateTable("Headers"); + headersTable.Theme = XLTableTheme.TableStyleLight10; + headersTable.ShowAutoFilter = false; + + // Add a custom formula to the headersTable + headersTable.ShowTotalsRow = true; + headersTable.Field(0).TotalsRowFormulaA1 = "CONCATENATE(\"Count: \", CountA(Headers[Table Headers]))"; + + // Copy the names + Int32 columnWithNames = columnWithHeaders + 2; + currentRow = table.RangeAddress.FirstAddress.RowNumber; // reset the currentRow + ws.Cell(currentRow, columnWithNames).Value = "Names"; + foreach (var row in table.DataRange.Rows()) + { + currentRow++; + var fName = row.Field("FName").GetString(); // Notice how we're calling the cell by field name + var lName = row.Field("LName").GetString(); // Notice how we're calling the cell by field name + var name = String.Format("{0} {1}", fName, lName); + ws.Cell(currentRow, columnWithNames).Value = name; + } + + // Format the names as a table with a different style and no autofilters + var ntFirstCell = ws.Cell(table.RangeAddress.FirstAddress.RowNumber, columnWithNames); + var ntLastCell = ws.Cell(currentRow, columnWithNames); + var namesTable = ws.Range(ntFirstCell, ntLastCell).CreateTable(); + namesTable.Theme = XLTableTheme.TableStyleLight12; + namesTable.ShowAutoFilter = false; + + ws.Columns().AdjustToContents(); + ws.Columns("A,H,J").Width = 3; + + wb.SaveAs(filePath); + } + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + // Private + + // Override + + #endregion Methods + } +} diff --git a/ClosedXML_Examples/app.config b/ClosedXML_Examples/app.config new file mode 100644 index 0000000..86fcc8b --- /dev/null +++ b/ClosedXML_Examples/app.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/ClosedXML_Sandbox/ClosedXML_Sandbox.csproj b/ClosedXML_Sandbox/ClosedXML_Sandbox.csproj new file mode 100644 index 0000000..c27e354 --- /dev/null +++ b/ClosedXML_Sandbox/ClosedXML_Sandbox.csproj @@ -0,0 +1,46 @@ + + + + Exe + netcoreapp2.0;net40;net46 + 8.0 + 0.95.4 + $(NoWarn);NU1605 + Debug;Release;Release.Signed + + + + $(DefineConstants);_NETSTANDARD_;_NETSTANDARD2_0_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET40_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET46_ + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/ClosedXML_Sandbox/OneRow.cs b/ClosedXML_Sandbox/OneRow.cs new file mode 100644 index 0000000..2627417 --- /dev/null +++ b/ClosedXML_Sandbox/OneRow.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.ComponentModel.DataAnnotations; + +namespace ClosedXML_Sandbox +{ + public class OneRow + { + [Display(Name = "Col01")] + public int Col01 { get; set; } + + [Display(Name = "Col02")] + public string Col02 { get; set; } + + [Display(Name = "Col03")] + public DateTime? Col03 { get; set; } + + [Display(Name = "Col04")] + public TimeSpan? Col04 { get; set; } + + [Display(Name = "Col05")] + public TimeSpan? Col05 { get; set; } + + [Display(Name = "Col06")] + public TimeSpan? Col06 { get; set; } + + [Display(Name = "Col07")] + public TimeSpan? Col07 { get; set; } + + [Display(Name = "Col08")] + public TimeSpan? Col08 { get; set; } + + [Display(Name = "Col09")] + public string Col09 { get; set; } + + [Display(Name = "Col10")] + public decimal Col10 { get; set; } + + [Display(Name = "Col11")] + public string Col11 { get; set; } + + [Display(Name = "Col12")] + public bool Col12 { get; set; } + + [Display(Name = "Col13")] + public bool Col13 { get; set; } + + [Display(Name = "Col14")] + public string Col14 { get; set; } + + [Display(Name = "Col15")] + public bool Col15 { get; set; } + + [Display(Name = "Col16")] + public string Col16 { get; set; } + + [Display(Name = "Col17")] + public string Col17 { get; set; } + + [Display(Name = "Col18")] + public DateTime? Col18 { get; set; } + + [Display(Name = "Col19")] + public string Col19 { get; set; } + + [Display(Name = "Col20")] + public string Col20 { get; set; } + + [Display(Name = "Col21")] + public string Col21 { get; set; } + + [Display(Name = "Col22")] + public string Col22 { get; set; } + + [Display(Name = "Col23")] + public string Col23 { get; set; } + + [Display(Name = "Col24")] + public bool Col24 { get; set; } + + [Display(Name = "Col25")] + public int? Col25 { get; set; } + + [Display(Name = "Col26")] + public string Col26 { get; set; } + } +} \ No newline at end of file diff --git a/ClosedXML_Sandbox/PerformanceRunner.cs b/ClosedXML_Sandbox/PerformanceRunner.cs new file mode 100644 index 0000000..b535a1d --- /dev/null +++ b/ClosedXML_Sandbox/PerformanceRunner.cs @@ -0,0 +1,170 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace ClosedXML_Sandbox +{ + internal class PerformanceRunner + { + public static void TimeAction(Action action) + { + var stopwatch = Stopwatch.StartNew(); + action(); + Console.WriteLine("Action done in " + stopwatch.Elapsed); + } + + private const int rowCount = 5000; + + public static void RunInsertTable() + { + var rows = new List(); + + for (int i = 0; i < rowCount; i++) + { + var row = GenerateRow(); + rows.Add(row); + } + + var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Sheet 1"); + worksheet.Cell(1, 1).InsertTable(rows); + + CreateMergedCell(worksheet); + + worksheet.Columns().AdjustToContents(); + + EmulateSave(workbook); + } + + public static void OpenTestFile() + { + using (var wb = new XLWorkbook("test.xlsx")) + { + wb.RecalculateAllFormulas(); + var ws = wb.Worksheets.First(); + var cell = ws.FirstCellUsed(); + Console.WriteLine(cell.Value); + } + } + + private static void CreateMergedCell(IXLWorksheet worksheet) + { + worksheet.Cell(rowCount + 2, 1).Value = "Merged cell"; + var range = worksheet.Range(rowCount + 2, 1, rowCount + 2, 2); + range.Row(1).Merge(); + } + + private static void EmulateSave(XLWorkbook workbook) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + workbook.SaveAs(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + Console.WriteLine("Total bytes = " + memoryStream.ToArray().Length); + } + } + + private static Random rnd = new Random(); + + private static T GenerateRow() where T : new() + { + var row = new T(); + + var rowProps = row.GetType().GetProperties(); + + var strings = rowProps.Where(p => p.PropertyType == typeof(string)); + var decimals = rowProps.Where(p => p.PropertyType == typeof(decimal)); + var ints = rowProps.Where(p => p.PropertyType == typeof(int) || p.PropertyType == typeof(int?)); + var dates = rowProps.Where(p => p.PropertyType == typeof(DateTime?)); + var timeSpans = rowProps.Where(p => p.PropertyType == typeof(TimeSpan?)); + var booleans = rowProps.Where(p => p.PropertyType == typeof(bool)); + + // Format strings + var tmpString = new StringBuilder(); + var tmpStringLength = rnd.Next(5, 50); + for (int x = 0; x <= tmpStringLength; x++) + { + tmpString.Append((char)(rnd.Next(48, 120))); + } + foreach (var str in strings) + { + str.SetValue(row, tmpString.ToString(), null); + } + + // Format decimals + var tmpDec = (decimal)(rnd.Next(-10000, 100000) / (Math.Pow(10.0, rnd.Next(1, 4)))); + + foreach (var dec in decimals) + { + dec.SetValue(row, tmpDec, null); + } + + // Format ints + var tmpInt = rnd.Next(-1000, 10000); + + foreach (var intValue in ints) + { + intValue.SetValue(row, tmpInt, null); + } + + // Format dates + var tmpDate = new DateTime(2012, 1, 1, 1, 1, 1); + tmpDate = tmpDate.AddSeconds(rnd.Next(-10000, 100000)); + foreach (var dt in dates) + { + dt.SetValue(row, tmpDate, null); + } + + // Format timespans + var tmpTimespan = new TimeSpan(rnd.Next(1, 24), rnd.Next(1, 60), rnd.Next(1, 60)); + + foreach (var ts in timeSpans) + { + ts.SetValue(row, tmpTimespan, null); + } + + // Format booleans + var tmpBool = (rnd.Next(0, 2) > 0); + foreach (var bl in booleans) + { + bl.SetValue(row, tmpBool, null); + } + + return row; + } + + public static void PerformHeavyCalculation() + { + int rows = 200; + int columns = 200; + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var lastColumnLetter = sheet.Column(columns).ColumnLetter(); + for (int i = 1; i <= rows; i++) + { + for (int j = 1; j <= columns; j++) + { + if (i == 1) + { + sheet.Cell(i, j).FormulaA1 = string.Format("=ROUND({0}*SIN({0}),2)", j); + } + else + { + sheet.Cell(i, j).FormulaA1 = string.Format("=SUM({0}$1:{0}{1})/SUM($A{1}:${2}{1})", + sheet.Column(j).ColumnLetter(), i - 1, lastColumnLetter); // i.e. for K8 there will be =SUM(K$1:K7)/SUM($A7:$GR7) + } + } + } + + var cells = sheet.CellsUsed(); + var sum1 = cells.Sum(cell => (double)cell.Value); + Console.WriteLine("Total sum: {0:N2}", sum1); + } + } + } +} diff --git a/ClosedXML_Sandbox/Program.cs b/ClosedXML_Sandbox/Program.cs new file mode 100644 index 0000000..704ab40 --- /dev/null +++ b/ClosedXML_Sandbox/Program.cs @@ -0,0 +1,29 @@ +using System; + +namespace ClosedXML_Sandbox +{ + internal static class Program + { + private static void Main(string[] args) + { + Console.WriteLine("Running {0}", nameof(PerformanceRunner.OpenTestFile)); + PerformanceRunner.TimeAction(PerformanceRunner.OpenTestFile); + Console.WriteLine(); + + // Disable this block by default - I don't use it often +#if false + + Console.WriteLine("Running {0}", nameof(PerformanceRunner.RunInsertTable)); + PerformanceRunner.TimeAction(PerformanceRunner.RunInsertTable); + Console.WriteLine(); + + Console.WriteLine("Running {0}", nameof(PerformanceRunner.PerformHeavyCalculation)); + PerformanceRunner.TimeAction(PerformanceRunner.PerformHeavyCalculation); + Console.WriteLine(); +#endif + + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + } + } +} diff --git a/ClosedXML_Sandbox/ReflectionExtensions.cs b/ClosedXML_Sandbox/ReflectionExtensions.cs new file mode 100644 index 0000000..7229a8c --- /dev/null +++ b/ClosedXML_Sandbox/ReflectionExtensions.cs @@ -0,0 +1,17 @@ +#if _NET40_ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace ClosedXML_Sandbox +{ + internal static class ReflectionExtensions + { + public static void SetValue(this PropertyInfo info, object obj, object value) + { + info.SetValue(obj, value, null); + } + } +} +#endif diff --git a/ClosedXML_Sandbox/app.config b/ClosedXML_Sandbox/app.config new file mode 100644 index 0000000..86fcc8b --- /dev/null +++ b/ClosedXML_Sandbox/app.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/ClosedXML_Sandbox/test.xlsx b/ClosedXML_Sandbox/test.xlsx new file mode 100644 index 0000000..296c174 Binary files /dev/null and b/ClosedXML_Sandbox/test.xlsx differ diff --git a/ClosedXML_Tests/ClosedXML.snk b/ClosedXML_Tests/ClosedXML.snk new file mode 100644 index 0000000..a17cff7 Binary files /dev/null and b/ClosedXML_Tests/ClosedXML.snk differ diff --git a/ClosedXML_Tests/ClosedXML_Tests.csproj b/ClosedXML_Tests/ClosedXML_Tests.csproj new file mode 100644 index 0000000..8dbf1d4 --- /dev/null +++ b/ClosedXML_Tests/ClosedXML_Tests.csproj @@ -0,0 +1,67 @@ + + + + netcoreapp3.1;net40;net46 + 8.0 + 0.95.4 + false + $(NoWarn);NU1605 + Debug;Release;Release.Signed + + + + $(AppVeyor) + + + + bin\Release.Signed\ + true + true + ClosedXML.snk + $(DefineConstants);RELEASE;STRONGNAME + + + + $(DefineConstants);_NETSTANDARD_;_NETSTANDARD2_0_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET40_ + + + + $(DefineConstants);_NETFRAMEWORK_;_NET46_ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ClosedXML_Tests/Examples/AutoFilterTests.cs b/ClosedXML_Tests/Examples/AutoFilterTests.cs new file mode 100644 index 0000000..5aaf1b9 --- /dev/null +++ b/ClosedXML_Tests/Examples/AutoFilterTests.cs @@ -0,0 +1,39 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class AutoFilterTests + { + [Test] + public void CustomAutoFilter() + { + TestHelper.RunTestExample(@"AutoFilter\CustomAutoFilter.xlsx"); + } + + [Test] + public void DynamicAutoFilter() + { + TestHelper.RunTestExample(@"AutoFilter\DynamicAutoFilter.xlsx"); + } + + [Test] + public void RegularAutoFilter() + { + TestHelper.RunTestExample(@"AutoFilter\RegularAutoFilter.xlsx"); + } + + [Test] + public void TopBottomAutoFilter() + { + TestHelper.RunTestExample(@"AutoFilter\TopBottomAutoFilter.xlsx"); + } + + [Test] + public void DateTimeGroupAutoFilter() + { + TestHelper.RunTestExample(@"AutoFilter\DateTimeGroupAutoFilter.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/ColumnsTests.cs b/ClosedXML_Tests/Examples/ColumnsTests.cs new file mode 100644 index 0000000..b5007bf --- /dev/null +++ b/ClosedXML_Tests/Examples/ColumnsTests.cs @@ -0,0 +1,40 @@ +using ClosedXML_Examples; +using ClosedXML_Examples.Columns; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class ColumnsTests + { + [Test] + public void ColumnCells() + { + TestHelper.RunTestExample(@"Columns\ColumnCells.xlsx"); + } + + [Test] + public void ColumnCollections() + { + TestHelper.RunTestExample(@"Columns\ColumnCollection.xlsx"); + } + + [Test] + public void ColumnSettings() + { + TestHelper.RunTestExample(@"Columns\ColumnSettings.xlsx"); + } + + [Test] + public void DeletingColumns() + { + TestHelper.RunTestExample(@"Columns\DeletingColumns.xlsx"); + } + + //[Test] // Not working yet + public void InsertColumns() + { + TestHelper.RunTestExample(@"Columns\InsertColumns.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/CommentsTests.cs b/ClosedXML_Tests/Examples/CommentsTests.cs new file mode 100644 index 0000000..11d719c --- /dev/null +++ b/ClosedXML_Tests/Examples/CommentsTests.cs @@ -0,0 +1,15 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class CommentsTests + { + [Test] + public void AddingComments() + { + TestHelper.RunTestExample(@"Comments\AddingComments.xlsx"); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Examples/ConditionalFormattingTests.cs b/ClosedXML_Tests/Examples/ConditionalFormattingTests.cs new file mode 100644 index 0000000..2de6d03 --- /dev/null +++ b/ClosedXML_Tests/Examples/ConditionalFormattingTests.cs @@ -0,0 +1,141 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class ConditionalFormattingTests + { + [Test] + public void CFColorScaleLowHigh() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFColorScaleLowHigh.xlsx"); + } + + [Test] + public void CFColorScaleLowMidHigh() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFColorScaleLowMidHigh.xlsx"); + } + + [Test] + public void CFColorScaleMinimumMaximum() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFColorScaleMinimumMaximum.xlsx"); + } + + [Test] + public void CFContains() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFContains.xlsx"); + } + + [Test] + public void CFDataBar() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFDataBar.xlsx"); + } + + [Test] + public void CFDataBarNegative() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFDataBarNegative.xlsx"); + } + + [Test] + public void CFEndsWith() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFEndsWith.xlsx"); + } + + [Test] + public void CFEqualsNumber() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFEqualsNumber.xlsx"); + } + + [Test] + public void CFEqualsString() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFEqualsString.xlsx"); + } + + [Test] + public void CFIconSet() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFIconSet.xlsx"); + } + + [Test] + public void CFIsBlank() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFIsBlank.xlsx"); + } + + [Test] + public void CFIsError() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFIsError.xlsx"); + } + + [Test] + public void CFNotBlank() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFNotBlank.xlsx"); + } + + [Test] + public void CFNotContains() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFNotContains.xlsx"); + } + + [Test] + public void CFNotEqualsNumber() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFNotEqualsNumber.xlsx"); + } + + [Test] + public void CFNotEqualsString() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFNotEqualsString.xlsx"); + } + + [Test] + public void CFNotError() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFNotError.xlsx"); + } + + [Test] + public void CFStartsWith() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFStartsWith.xlsx"); + } + + [Test] + public void CFMultipleConditions() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFMultipleConditions.xlsx"); + } + + [Test] + public void CFStopIfTrue() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFStopIfTrue.xlsx"); + } + + [Test] + public void CFDatesOccurring() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFDatesOccurring.xlsx"); + } + + [Test] + public void CFDataBars() + { + TestHelper.RunTestExample(@"ConditionalFormatting\CFDataBars.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/DeleteTests.cs b/ClosedXML_Tests/Examples/DeleteTests.cs new file mode 100644 index 0000000..bd498be --- /dev/null +++ b/ClosedXML_Tests/Examples/DeleteTests.cs @@ -0,0 +1,21 @@ +using ClosedXML_Examples.Delete; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class DeleteTests + { + [Test] + public void DeleteFewWorksheets() + { + TestHelper.RunTestExample(@"Delete\DeleteFewWorksheets.xlsx"); + } + + [Test] + public void RemoveRows() + { + TestHelper.RunTestExample(@"Delete\RemoveRows.xlsx"); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Examples/ImageHandlingTests.cs b/ClosedXML_Tests/Examples/ImageHandlingTests.cs new file mode 100644 index 0000000..cbbbd94 --- /dev/null +++ b/ClosedXML_Tests/Examples/ImageHandlingTests.cs @@ -0,0 +1,21 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class ImageHandlingTests + { + [Test] + public void ImageAnchors() + { + TestHelper.RunTestExample(@"ImageHandling\ImageAnchors.xlsx"); + } + + [Test] + public void ImageFormats() + { + TestHelper.RunTestExample(@"ImageHandling\ImageFormats.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/LoadingTests.cs b/ClosedXML_Tests/Examples/LoadingTests.cs new file mode 100644 index 0000000..d6af919 --- /dev/null +++ b/ClosedXML_Tests/Examples/LoadingTests.cs @@ -0,0 +1,15 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class LoadingTests + { + [Test] + public void ChangingBasicTable() + { + TestHelper.RunTestExample(@"Loading\ChangingBasicTable.xlsx"); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Examples/MiscTests.cs b/ClosedXML_Tests/Examples/MiscTests.cs new file mode 100644 index 0000000..d74a9fc --- /dev/null +++ b/ClosedXML_Tests/Examples/MiscTests.cs @@ -0,0 +1,209 @@ +using ClosedXML_Examples; +using ClosedXML_Examples.Misc; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class MiscTests + { + [Test] + public void AddingDataSet() + { + TestHelper.RunTestExample(@"Misc\AddingDataSet.xlsx"); + } + + [Test] + public void AddingDataTableAsWorksheet() + { + TestHelper.RunTestExample(@"Misc\AddingDataTableAsWorksheet.xlsx"); + } + + [Test] + public void AdjustToContents() + { + TestHelper.RunTestExample(@"Misc\AdjustToContents.xlsx"); + } + + [Test] + public void AdjustToContentsWithAutoFilter() + { + TestHelper.RunTestExample(@"Misc\AdjustToContentsWithAutoFilter.xlsx"); + } + + [Test] + public void AutoFilter() + { + TestHelper.RunTestExample(@"Misc\AutoFilter.xlsx"); + } + + [Test] + public void BasicTable() + { + TestHelper.RunTestExample(@"Misc\BasicTable.xlsx"); + } + + [Test] + public void BlankCells() + { + TestHelper.RunTestExample(@"Misc\BlankCells.xlsx"); + } + + [Test] + public void CellValues() + { + TestHelper.RunTestExample(@"Misc\CellValues.xlsx"); + } + + [Test] + public void Collections() + { + TestHelper.RunTestExample(@"Misc\Collections.xlsx"); + } + + [Test] + public void CopyingRowsAndColumns() + { + TestHelper.RunTestExample(@"Misc\CopyingRowsAndColumns.xlsx"); + } + + [Test] + public void CopyingWorksheets() + { + TestHelper.RunTestExample(@"Misc\CopyingWorksheets.xlsx"); + } + + [Test] + public void DataTypes() + { + TestHelper.RunTestExample(@"Misc\DataTypes.xlsx"); + } + + [Test] + public void DataTypesUnderDifferentCulture() + { + TestHelper.RunTestExample(@"Misc\DataTypesUnderDifferentCulture.xlsx"); + } + + [Test] + public void DataValidation() + { + TestHelper.RunTestExample(@"Misc\DataValidation.xlsx"); + } + + [Test] + public void Formulas() + { + TestHelper.RunTestExample(@"Misc\Formulas.xlsx"); + } + + [Test] + public void FormulasWithEvaluation() + { + TestHelper.RunTestExample(@"Misc\FormulasWithEvaluation.xlsx", true); + } + + [Test] + public void FreezePanes() + { + TestHelper.RunTestExample(@"Misc\FreezePanes.xlsx"); + } + + [Test] + public void HideSheets() + { + TestHelper.RunTestExample(@"Misc\HideSheets.xlsx"); + } + + [Test] + public void HideUnhide() + { + TestHelper.RunTestExample(@"Misc\HideUnhide.xlsx"); + } + + [Test] + public void Hyperlinks() + { + TestHelper.RunTestExample(@"Misc\Hyperlinks.xlsx"); + } + + [Test] + public void InsertingData() + { + TestHelper.RunTestExample(@"Misc\InsertingData.xlsx"); + } + + + [Test] + public void LambdaExpressions() + { + TestHelper.RunTestExample(@"Misc\LambdaExpressions.xlsx"); + } + + [Test] + public void MergeCells() + { + TestHelper.RunTestExample(@"Misc\MergeCells.xlsx"); + } + + [Test] + public void MergeMoves() + { + TestHelper.RunTestExample(@"Misc\MergeMoves.xlsx"); + } + + [Test] + public void Outline() + { + TestHelper.RunTestExample(@"Misc\Outline.xlsx"); + } + + [Test] + public void RightToLeft() + { + TestHelper.RunTestExample(@"Misc\RightToLeft.xlsx"); + } + + [Test] + public void SheetProtection() + { + TestHelper.RunTestExample(@"Misc\SheetProtection.xlsx"); + } + + [Test] + public void SheetViews() + { + TestHelper.RunTestExample(@"Misc\SheetViews.xlsx"); + } + + [Test] + public void ShiftingFormulas() + { + TestHelper.RunTestExample(@"Misc\ShiftingFormulas.xlsx"); + } + + [Test] + public void ShowCase() + { + TestHelper.RunTestExample(@"Misc\ShowCase.xlsx"); + } + + [Test] + public void TabColors() + { + TestHelper.RunTestExample(@"Misc\TabColors.xlsx"); + } + + [Test] + public void WorkbookProperties() + { + TestHelper.RunTestExample(@"Misc\WorkbookProperties.xlsx"); + } + + [Test] + public void WorkbookProtection() + { + TestHelper.RunTestExample(@"Misc\WorkbookProtection.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/PageSetupTests.cs b/ClosedXML_Tests/Examples/PageSetupTests.cs new file mode 100644 index 0000000..63930b0 --- /dev/null +++ b/ClosedXML_Tests/Examples/PageSetupTests.cs @@ -0,0 +1,45 @@ +using ClosedXML_Examples.PageSetup; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class PageSetupTests + { + [Test] + public void HeaderFooters() + { + TestHelper.RunTestExample(@"PageSetup\HeaderFooters.xlsx"); + } + + [Test] + public void Margins() + { + TestHelper.RunTestExample(@"PageSetup\Margins.xlsx"); + } + + [Test] + public void Page() + { + TestHelper.RunTestExample(@"PageSetup\Page.xlsx"); + } + + [Test] + public void SheetTab() + { + TestHelper.RunTestExample(@"PageSetup\SheetTab.xlsx"); + } + + [Test] + public void Sheets() + { + TestHelper.RunTestExample(@"PageSetup\Sheets.xlsx"); + } + + [Test] + public void TwoPages() + { + TestHelper.RunTestExample(@"PageSetup\TwoPages.xlsx"); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Examples/PivotTableTests.cs b/ClosedXML_Tests/Examples/PivotTableTests.cs new file mode 100644 index 0000000..5a1d061 --- /dev/null +++ b/ClosedXML_Tests/Examples/PivotTableTests.cs @@ -0,0 +1,15 @@ +using ClosedXML_Examples; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class PivotTableTests + { + [Test] + public void PivotTables() + { + TestHelper.RunTestExample(@"PivotTables\PivotTables.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/RangesTests.cs b/ClosedXML_Tests/Examples/RangesTests.cs new file mode 100644 index 0000000..59cd62b --- /dev/null +++ b/ClosedXML_Tests/Examples/RangesTests.cs @@ -0,0 +1,113 @@ +using ClosedXML_Examples; +using ClosedXML_Examples.Misc; +using ClosedXML_Examples.Ranges; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class RangesTests + { + [Test] + public void ClearingRanges() + { + TestHelper.RunTestExample(@"Ranges\ClearingRanges.xlsx"); + } + + [Test] + public void CopyingRanges() + { + TestHelper.RunTestExample(@"Ranges\CopyingRanges.xlsx"); + } + + [Test] + public void CurrentRowColumn() + { + TestHelper.RunTestExample(@"Ranges\CurrentRowColumn.xlsx"); + } + + [Test] + public void DefiningRanges() + { + TestHelper.RunTestExample(@"Ranges\DefiningRanges.xlsx"); + } + + [Test] + public void DeletingRanges() + { + TestHelper.RunTestExample(@"Ranges\DeletingRanges.xlsx"); + } + + [Test] + public void InsertingDeletingColumns() + { + TestHelper.RunTestExample(@"Ranges\InsertingDeletingColumns.xlsx"); + } + + [Test] + public void InsertingDeletingRows() + { + TestHelper.RunTestExample(@"Ranges\InsertingDeletingRows.xlsx"); + } + + [Test] + public void MultipleRanges() + { + TestHelper.RunTestExample(@"Ranges\MultipleRanges.xlsx"); + } + + [Test] + public void NamedRanges() + { + TestHelper.RunTestExample(@"Ranges\NamedRanges.xlsx"); + } + + [Test] + public void SelectingRanges() + { + TestHelper.RunTestExample(@"Ranges\SelectingRanges.xlsx"); + } + + [Test] + public void ShiftingRanges() + { + TestHelper.RunTestExample(@"Ranges\ShiftingRanges.xlsx"); + } + + [Test] + public void SortExample() + { + TestHelper.RunTestExample(@"Ranges\SortExample.xlsx"); + } + + [Test] + public void Sorting() + { + TestHelper.RunTestExample(@"Ranges\Sorting.xlsx"); + } + + [Test] + public void TransposeRanges() + { + TestHelper.RunTestExample(@"Ranges\TransposeRanges.xlsx"); + } + + [Test] + public void TransposeRangesPlus() + { + TestHelper.RunTestExample(@"Ranges\TransposeRangesPlus.xlsx"); + } + + [Test] + public void AddingRowToTables() + { + TestHelper.RunTestExample(@"Ranges\AddingRowToTables.xlsx"); + } + + [Test] + public void WalkingRanges() + { + TestHelper.RunTestExample(@"Ranges\WalkingRanges.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/RowsTests.cs b/ClosedXML_Tests/Examples/RowsTests.cs new file mode 100644 index 0000000..446b812 --- /dev/null +++ b/ClosedXML_Tests/Examples/RowsTests.cs @@ -0,0 +1,34 @@ +using ClosedXML_Examples; +using ClosedXML_Examples.Rows; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class RowsTests + { + [Test] + public void RowCells() + { + TestHelper.RunTestExample(@"Rows\RowCells.xlsx"); + } + + [Test] + public void RowCollection() + { + TestHelper.RunTestExample(@"Rows\RowCollection.xlsx"); + } + + [Test] + public void RowSettings() + { + TestHelper.RunTestExample(@"Rows\RowSettings.xlsx"); + } + + //[Test] // Not working yet + public void InsertRows() + { + TestHelper.RunTestExample(@"Rows\InsertRows.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/StylesTests.cs b/ClosedXML_Tests/Examples/StylesTests.cs new file mode 100644 index 0000000..ac6b17e --- /dev/null +++ b/ClosedXML_Tests/Examples/StylesTests.cs @@ -0,0 +1,87 @@ +using ClosedXML_Examples.Styles; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class StylesTests + { + [Test] + public void DefaultStyles() + { + TestHelper.RunTestExample(@"Styles\DefaultStyles.xlsx"); + } + + [Test] + public void PurpleWorksheet() + { + TestHelper.RunTestExample(@"Styles\PurpleWorksheet.xlsx"); + } + + [Test] + public void StyleAlignment() + { + TestHelper.RunTestExample(@"Styles\StyleAlignment.xlsx"); + } + + [Test] + public void StyleBorder() + { + TestHelper.RunTestExample(@"Styles\StyleBorder.xlsx"); + } + + [Test] + public void StyleFill() + { + TestHelper.RunTestExample(@"Styles\StyleFill.xlsx"); + } + + [Test] + public void StyleFont() + { + TestHelper.RunTestExample(@"Styles\StyleFont.xlsx"); + } + + [Test] + public void StyleNumberFormat() + { + TestHelper.RunTestExample(@"Styles\StyleNumberFormat.xlsx"); + } + + [Test] + public void StyleIncludeQuotePrefix() + { + TestHelper.RunTestExample(@"Styles\StyleIncludeQuotePrefix.xlsx"); + } + + [Test] + public void StyleRowsColumns() + { + TestHelper.RunTestExample(@"Styles\StyleRowsColumns.xlsx"); + } + + [Test] + public void StyleWorksheet() + { + TestHelper.RunTestExample(@"Styles\StyleWorksheet.xlsx"); + } + + [Test] + public void UsingColors() + { + TestHelper.RunTestExample(@"Styles\UsingColors.xlsx"); + } + + [Test] + public void UsingPhonetics() + { + TestHelper.RunTestExample(@"Styles\UsingPhonetics.xlsx"); + } + + [Test] + public void UsingRichText() + { + TestHelper.RunTestExample(@"Styles\UsingRichText.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Examples/TablesTests.cs b/ClosedXML_Tests/Examples/TablesTests.cs new file mode 100644 index 0000000..934bf65 --- /dev/null +++ b/ClosedXML_Tests/Examples/TablesTests.cs @@ -0,0 +1,27 @@ +using ClosedXML_Examples.Tables; +using NUnit.Framework; + +namespace ClosedXML_Tests.Examples +{ + [TestFixture] + public class TablesTests + { + [Test] + public void InsertingTables() + { + TestHelper.RunTestExample(@"Tables\InsertingTables.xlsx"); + } + + [Test] + public void ResizingTables() + { + TestHelper.RunTestExample(@"Tables\ResizingTables.xlsx"); + } + + [Test] + public void UsingTables() + { + TestHelper.RunTestExample(@"Tables\UsingTables.xlsx"); + } + } +} diff --git a/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs b/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs new file mode 100644 index 0000000..52c8122 --- /dev/null +++ b/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs @@ -0,0 +1,277 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class AutoFilterTests + { + [Test] + public void AutoFilterExpandsWithTable() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("1") + .CellBelow().SetValue("2"); + + IXLTable table = ws.RangeUsed().CreateTable(); + + var listOfArr = new List(); + listOfArr.Add(3); + listOfArr.Add(4); + listOfArr.Add(5); + listOfArr.Add(6); + + table.DataRange.InsertRowsBelow(listOfArr.Count - table.DataRange.RowCount()); + table.DataRange.FirstCell().InsertData(listOfArr); + + Assert.AreEqual("A1:A5", table.AutoFilter.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual(5, table.AutoFilter.VisibleRows.Count()); + } + } + + [Test] + public void AutoFilterSortWhenNotInFirstRow() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Cell(3, 3).SetValue("Names") + .CellBelow().SetValue("Manuel") + .CellBelow().SetValue("Carlos") + .CellBelow().SetValue("Dominic"); + ws.RangeUsed().SetAutoFilter().Sort(); + Assert.AreEqual("Carlos", ws.Cell(4, 3).GetString()); + } + } + + [Test] + public void CanClearAutoFilter() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "Names"; + ws.Cell("A2").Value = "John"; + ws.Cell("A3").Value = "Hank"; + ws.Cell("A4").Value = "Dagny"; + + ws.AutoFilter.Clear(); // We should be able to clear a filter even if it hasn't been set. + Assert.That(!ws.AutoFilter.IsEnabled); + + ws.RangeUsed().SetAutoFilter(); + Assert.That(ws.AutoFilter.IsEnabled); + + ws.AutoFilter.Clear(); + Assert.That(!ws.AutoFilter.IsEnabled); + } + + [Test] + public void CanClearAutoFilter2() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "Names"; + ws.Cell("A2").Value = "John"; + ws.Cell("A3").Value = "Hank"; + ws.Cell("A4").Value = "Dagny"; + + ws.SetAutoFilter(false); + Assert.That(!ws.AutoFilter.IsEnabled); + + ws.RangeUsed().SetAutoFilter(); + Assert.That(ws.AutoFilter.IsEnabled); + + ws.RangeUsed().SetAutoFilter(false); + Assert.That(!ws.AutoFilter.IsEnabled); + } + } + + [Test] + public void CanCopyAutoFilterToNewSheetOnNewWorkbook() + { + using (var ms1 = new MemoryStream()) + using (var ms2 = new MemoryStream()) + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws = wb1.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "Names"; + ws.Cell("A2").Value = "John"; + ws.Cell("A3").Value = "Hank"; + ws.Cell("A4").Value = "Dagny"; + + ws.RangeUsed().SetAutoFilter(); + + wb1.SaveAs(ms1); + + ws.CopyTo(wb2, ws.Name); + wb2.SaveAs(ms2); + } + + using (var wb2 = new XLWorkbook(ms2)) + { + Assert.IsTrue(wb2.Worksheets.First().AutoFilter.IsEnabled); + } + } + } + + [Test] + public void CannotAddAutoFilterOverExistingTable() + { + using var wb = new XLWorkbook(); + + var data = Enumerable.Range(1, 10).Select(i => new + { + Index = i, + String = $"String {i}" + }); + + var ws = wb.AddWorksheet(); + ws.FirstCell().InsertTable(data); + + Assert.Throws(() => ws.RangeUsed().SetAutoFilter()); + } + + [Test] + [TestCase("A1:A4")] + [TestCase("A1:B4")] + [TestCase("A1:C4")] + public void AutoFilterRangeRemainsValidOnInsertColumn(string rangeAddress) + { + //Arrange + using (var ms1 = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("AutoFilter"); + ws.Cell("A1").Value = "Ids"; + ws.Cell("B1").Value = "Names"; + ws.Cell("B2").Value = "John"; + ws.Cell("B3").Value = "Hank"; + ws.Cell("B4").Value = "Dagny"; + ws.Cell("C1").Value = "Phones"; + + ws.Range("B1:B4").SetAutoFilter(true); + + //Act + var range = ws.Range(rangeAddress); + range.InsertColumnsBefore(1); + + //Assert + Assert.IsTrue(ws.AutoFilter.Range.RangeAddress.IsValid); + } + } + } + + [Test] + public void AutoFilterVisibleRows() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Cell(3, 3).SetValue("Names") + .CellBelow().SetValue("Manuel") + .CellBelow().SetValue("Carlos") + .CellBelow().SetValue("Dominic"); + + var autoFilter = ws.RangeUsed() + .SetAutoFilter(); + + autoFilter.Column(1).AddFilter("Carlos"); + + Assert.AreEqual("Carlos", ws.Cell(5, 3).GetString()); + Assert.AreEqual(2, autoFilter.VisibleRows.Count()); + Assert.AreEqual(3, autoFilter.VisibleRows.First().WorksheetRow().RowNumber()); + Assert.AreEqual(5, autoFilter.VisibleRows.Last().WorksheetRow().RowNumber()); + } + } + + [Test] + public void ReapplyAutoFilter() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Cell(3, 3).SetValue("Names") + .CellBelow().SetValue("Manuel") + .CellBelow().SetValue("Carlos") + .CellBelow().SetValue("Dominic") + .CellBelow().SetValue("Jose"); + + var autoFilter = ws.RangeUsed() + .SetAutoFilter(); + + autoFilter.Column(1).AddFilter("Carlos"); + + Assert.AreEqual(3, autoFilter.HiddenRows.Count()); + + // Unhide the rows so that the table is out of sync with the filter + autoFilter.HiddenRows.ForEach(r => r.WorksheetRow().Unhide()); + Assert.False(autoFilter.HiddenRows.Any()); + + autoFilter.Reapply(); + Assert.AreEqual(3, autoFilter.HiddenRows.Count()); + } + } + + [Test] + public void CanLoadAutoFilterWithThousandsSeparator() + { + var backupCulture = Thread.CurrentThread.CurrentCulture; + + try + { + // Set thread culture to French, which should format numbers using a space as thousands separator + var culture = CultureInfo.CreateSpecificCulture("fr-FR"); + // but use a period instead of a comma as for decimal separator + culture.NumberFormat.CurrencyDecimalSeparator = "."; + + Thread.CurrentThread.CurrentCulture = culture; + + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\AutoFilter\AutoFilterWithThousandsSeparator.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(10000, (ws.AutoFilter as XLAutoFilter).Filters.First().Value.First().Value); + Assert.AreEqual(2, ws.AutoFilter.VisibleRows.Count()); + + ws.AutoFilter.Reapply(); + Assert.AreEqual(2, ws.AutoFilter.VisibleRows.Count()); + } + + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\AutoFilter\AutoFilterWithThousandsSeparator.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("10 000.00", (ws.AutoFilter as XLAutoFilter).Filters.First().Value.First().Value); + + var v = ws.AutoFilter.VisibleRows.Select(r => r.FirstCell().Value).ToList(); + Assert.AreEqual(2, ws.AutoFilter.VisibleRows.Count()); + + ws.AutoFilter.Reapply(); + Assert.AreEqual(1, ws.AutoFilter.VisibleRows.Count()); + } + } + finally + { + Thread.CurrentThread.CurrentCulture = backupCulture; + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Caching/SampleRepositoryTests.cs b/ClosedXML_Tests/Excel/Caching/SampleRepositoryTests.cs new file mode 100644 index 0000000..a139601 --- /dev/null +++ b/ClosedXML_Tests/Excel/Caching/SampleRepositoryTests.cs @@ -0,0 +1,237 @@ +using System; +using ClosedXML.Excel; +using ClosedXML.Excel.Caching; +using NUnit.Framework; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ClosedXML_Tests.Excel.Caching +{ + [TestFixture] + public class BaseRepositoryTests + { + [Test] + public void DifferentEntitiesWithSameKeyStoredOnce() + { + // Arrange + int key = 12345; + var entity1 = new SampleEntity(key); + var entity2 = new SampleEntity(key); + var sampleRepository = this.CreateSampleRepository(); + + // Act + var storedEntity1 = sampleRepository.Store(ref key, entity1); + var storedEntity2 = sampleRepository.Store(ref key, entity2); + + // Assert + Assert.AreSame(entity1, storedEntity1); + Assert.AreSame(entity1, storedEntity2); + Assert.AreNotSame(entity2, storedEntity2); + } + + + [Test] + public void NonUsedReferencesAreGCed() + { +#if !DEBUG + // Arrange + int key = 12345; + var sampleRepository = this.CreateSampleRepository(); + + // Act + var storedEntityRef1 = new WeakReference(sampleRepository.Store(ref key, new SampleEntity(key))); + + int count = 0; + do + { + Thread.Sleep(50); + GC.Collect(); + count++; + } while (storedEntityRef1.IsAlive && count < 10); + + // Assert + if (count == 10) + Assert.Fail("storedEntityRef1 was not GCed"); + + Assert.IsFalse(sampleRepository.Any()); +#else + Assert.Ignore("Can't run in DEBUG"); +#endif + } + + + [Test] + public void NonUsedReferencesAreGCed2() + { +#if !DEBUG + // Arrange + int countUnique = 30; + int repeatCount = 1000; + SampleEntity[] entities = new SampleEntity[countUnique * repeatCount]; + for (int i = 0; i < countUnique; i++) + { + for (int j = 0; j < repeatCount; j++) + { + entities[i * repeatCount + j] = new SampleEntity(i); + } + } + + var sampleRepository = this.CreateSampleRepository(); + + // Act + Parallel.ForEach(entities, new ParallelOptions { MaxDegreeOfParallelism = 8 }, + e => + { + var key = e.Key; + sampleRepository.Store(ref key, e); + }); + + Thread.Sleep(50); + GC.Collect(); + var storedEntries = sampleRepository.ToList(); + + // Assert + Assert.AreEqual(0, storedEntries.Count); +#else + Assert.Ignore("Can't run in DEBUG"); +#endif + } + + [Test] + public void ConcurrentAddingCausesNoDuplication() + { + // Arrange + int countUnique = 30; + int repeatCount = 1000; + SampleEntity[] entities = new SampleEntity[countUnique * repeatCount]; + for (int i = 0; i < countUnique; i++) + { + for (int j = 0; j < repeatCount; j++) + { + entities[i * repeatCount + j] = new SampleEntity(i); + } + } + + var sampleRepository = this.CreateSampleRepository(); + + // Act + Parallel.ForEach(entities, new ParallelOptions { MaxDegreeOfParallelism = 8 }, + e => + { + var key = e.Key; + sampleRepository.Store(ref key, e); + }); + var storedEntries = sampleRepository.ToList(); + + // Assert + Assert.AreEqual(countUnique, storedEntries.Count); + Assert.NotNull(entities); // To protect them from GC + } + + [Test] + public void ReplaceKeyInRepository() + { + // Arrange + int key1 = 12345; + int key2 = 54321; + var entity = new SampleEntity(key1); + var sampleRepository = this.CreateSampleRepository(); + var storedEntity1 = sampleRepository.Store(ref key1, entity); + + // Act + sampleRepository.Replace(ref key1, ref key2); + bool containsOld = sampleRepository.ContainsKey(ref key1, out var _); + bool containsNew = sampleRepository.ContainsKey(ref key2, out var _); + var storedEntity2 = sampleRepository.GetOrCreate(ref key2); + + // Assert + Assert.IsFalse(containsOld); + Assert.IsTrue(containsNew); + Assert.AreSame(entity, storedEntity1); + Assert.AreSame(entity, storedEntity2); + } + + [Test] + public void ConcurrentReplaceKeyInRepository() + { + var sampleRepository = new EditableRepository(); + int[] keys = Enumerable.Range(0, 1000).ToArray(); + keys.ForEach(key => sampleRepository.GetOrCreate(ref key)); + + Parallel.ForEach(keys, key => + { + var modifiedKey = key + 2000; + var val1 = sampleRepository.Replace(ref key, ref modifiedKey); + val1.Key = key + 2000; + var val2 = sampleRepository.GetOrCreate(ref modifiedKey); + Assert.AreSame(val1, val2); + }); + } + + [Test] + public void ReplaceNonExistingKeyInRepository() + { + int key1 = 100; + int key2 = 200; + int key3 = 300; + var entity = new SampleEntity(key1); + var sampleRepository = this.CreateSampleRepository(); + sampleRepository.Store(ref key1, entity); + + sampleRepository.Replace(ref key2, ref key3); + var all = sampleRepository.ToList(); + + Assert.AreEqual(1, all.Count); + Assert.AreSame(entity, all.First()); + } + + private SampleRepository CreateSampleRepository() + { + return new SampleRepository(); + } + + /// + /// Class under testing + /// + internal class SampleRepository : XLRepositoryBase + { + public SampleRepository() : base(key => new SampleEntity(key)) + { + } + } + + public class SampleEntity + { + public int Key { get; private set; } + + public SampleEntity(int key) + { + Key = key; + } + } + + + /// + /// Class under testing + /// + internal class EditableRepository : XLRepositoryBase + { + public EditableRepository() : base(key => new EditableEntity(key)) + { + } + } + + public class EditableEntity + { + public int Key { get; set; } + + public EditableEntity(int key) + { + Key = key; + } + } + } + + +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/CalcEngineCellEnumerationTests.cs b/ClosedXML_Tests/Excel/CalcEngine/CalcEngineCellEnumerationTests.cs new file mode 100644 index 0000000..fdc97c7 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/CalcEngineCellEnumerationTests.cs @@ -0,0 +1,23 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + public class CalcEngineCellEnumerationTests + { + [Test] + public void CanEnumerateCellsOverEmptySheet() + { + using (var wb = new XLWorkbook()) + { + var sheet1 = wb.AddWorksheet("Sheet1"); + var sheet2 = wb.AddWorksheet("Sheet2"); + + var cell = sheet1.FirstCell(); + cell.FormulaA1 = "=SUMIFS(Sheet2!B:B, Sheet2!C:C, 1)"; + + Assert.AreEqual(0, cell.Value); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/CalcEngineExceptionTests.cs b/ClosedXML_Tests/Excel/CalcEngine/CalcEngineExceptionTests.cs new file mode 100644 index 0000000..26e9f01 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/CalcEngineExceptionTests.cs @@ -0,0 +1,56 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Globalization; +using System.Threading; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class CalcEngineExceptionTests + { + [OneTimeSetUp] + public void SetCultureInfo() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + } + + [Test] + public void InvalidCharNumber() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("CHAR(-2)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("CHAR(270)")); + } + + [Test] + public void DivisionByZero() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("0/0")); + Assert.Throws(() => new XLWorkbook().AddWorksheet().Evaluate("0/0")); + } + + [Test] + public void InvalidFunction() + { + Exception ex; + ex = Assert.Throws(() => XLWorkbook.EvaluateExpr("XXX(A1:A2)")); + Assert.That(ex.Message, Is.EqualTo("The identifier `XXX` was not recognised.")); + + var ws = new XLWorkbook().AddWorksheet(); + ex = Assert.Throws(() => ws.Evaluate("XXX(A1:A2)")); + Assert.That(ex.Message, Is.EqualTo("The identifier `XXX` was not recognised.")); + } + + [Test] + public void NestedNameNotRecognizedException() + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + ws.Cell("A1").SetFormulaA1("=XXX"); + ws.Cell("A2").SetFormulaA1(@"=IFERROR(A1, ""Success"")"); + + Assert.AreEqual("Success", ws.Cell("A2").Value); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/DateAndTimeTests.cs b/ClosedXML_Tests/Excel/CalcEngine/DateAndTimeTests.cs new file mode 100644 index 0000000..b26f519 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/DateAndTimeTests.cs @@ -0,0 +1,564 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Globalization; +using System.Threading; + +namespace ClosedXML_Tests.Excel.DataValidations +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class DateAndTimeTests + { + [SetUp] + public void SetCultureInfo() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + } + + [Test] + public void Date() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr("Date(2008, 1, 1)"); + Assert.AreEqual(39448, actual); + + actual = XLWorkbook.EvaluateExpr("Date(2008, 15, 1)"); + Assert.AreEqual(39873, actual); + + actual = XLWorkbook.EvaluateExpr("Date(2008, -50, 1)"); + Assert.AreEqual(37895, actual); + + actual = XLWorkbook.EvaluateExpr("Date(2008, 5, 63)"); + Assert.AreEqual(39631, actual); + + actual = XLWorkbook.EvaluateExpr("Date(2008, 13, 63)"); + Assert.AreEqual(39876, actual); + + actual = XLWorkbook.EvaluateExpr("Date(2008, 15, -120)"); + Assert.AreEqual(39752, actual); + } + + [TestCase("1/1/2006", "12/12/2010", "Y", ExpectedResult = 4)] + [TestCase("1/1/2006", "12/12/2010", "M", ExpectedResult = 59)] + [TestCase("1/1/2006", "12/12/2010", "D", ExpectedResult = 1806)] + [TestCase("1/1/2006", "12/12/2010", "MD", ExpectedResult = 11)] + [TestCase("1/1/2006", "12/12/2010", "YM", ExpectedResult = 11)] + [TestCase("1/1/2006", "12/12/2010", "YD", ExpectedResult = 345)] + [TestCase(38718, 40524, "Y", ExpectedResult = 4)] + [TestCase(38718, 40524, "M", ExpectedResult = 59)] + [TestCase(38718, 40524, "D", ExpectedResult = 1806)] + [TestCase(38718, 40524, "MD", ExpectedResult = 11)] + [TestCase(38718, 40524, "YM", ExpectedResult = 11)] + [TestCase(38718, 40524, "YD", ExpectedResult = 345)] + [TestCase("2001-12-31", "2002-4-15", "YM", ExpectedResult = 3)] + [TestCase("2001-12-10", "2002-4-15", "YM", ExpectedResult = 4)] + [TestCase("2001-12-15", "2002-4-15", "YM", ExpectedResult = 4)] + [TestCase("2001-12-31", "2002-4-15", "YD", ExpectedResult = 105)] + [TestCase("2001-12-31", "2003-4-15", "YD", ExpectedResult = 105)] + [TestCase("2002-01-31", "2002-4-15", "YD", ExpectedResult = 74)] + [TestCase("2001-12-02", "2001-12-15", "Y", ExpectedResult = 0)] + [TestCase("2001-12-02", "2002-12-02", "Y", ExpectedResult = 1)] + [TestCase("2006-01-15", "2006-03-14", "M", ExpectedResult = 1)] + [TestCase("2020-11-22", "2020-11-23 9:00", "D", ExpectedResult = 1)] + public double Datedif(object startDate, object endDate, string unit) + { + if (startDate is string s1) startDate = $"\"{s1}\""; + if (endDate is string s2) endDate = $"\"{s2}\""; + return (double)XLWorkbook.EvaluateExpr($"DATEDIF({startDate}, {endDate}, \"{unit}\")"); + } + + [TestCase("\"1/1/2010\"", "\"12/12/2006\"", "Y")] + [TestCase(40524, 38718, "Y")] + [TestCase("\"1/1/2006\"", "\"12/12/2010\"", "N")] + [TestCase(38718, 40524, "N")] + public void DatedifExceptions(object startDate, object endDate, string unit) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"DATEDIF({startDate}, {endDate}, \"{unit}\")")); + } + + [Test] + public void Datevalue() + { + Object actual = XLWorkbook.EvaluateExpr("DateValue(\"8/22/2008\")"); + Assert.AreEqual(39682, actual); + } + + [Test] + public void Day() + { + Object actual = XLWorkbook.EvaluateExpr("Day(\"8/22/2008\")"); + Assert.AreEqual(22, actual); + } + + [Test] + public void Days() + { + Object actual = XLWorkbook.EvaluateExpr("DAYS(DATE(2016,10,1),DATE(1992,2,29))"); + Assert.AreEqual(8981, actual); + + actual = XLWorkbook.EvaluateExpr("DAYS(\"2016-10-1\",\"1992-2-29\")"); + Assert.AreEqual(8981, actual); + } + + [Test] + public void DayWithDifferentCulture() + { + CultureInfo ci = new CultureInfo(CultureInfo.InvariantCulture.LCID); + ci.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy"; + Thread.CurrentThread.CurrentCulture = ci; + Object actual = XLWorkbook.EvaluateExpr("Day(\"1/6/2008\")"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Days360_Default() + { + Object actual = XLWorkbook.EvaluateExpr("Days360(\"1/30/2008\", \"2/1/2008\")"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Days360_Europe1() + { + Object actual = XLWorkbook.EvaluateExpr("DAYS360(\"1/1/2008\", \"3/31/2008\",TRUE)"); + Assert.AreEqual(89, actual); + } + + [Test] + public void Days360_Europe2() + { + Object actual = XLWorkbook.EvaluateExpr("DAYS360(\"3/31/2008\", \"1/1/2008\",TRUE)"); + Assert.AreEqual(-89, actual); + } + + [Test] + public void Days360_US1() + { + Object actual = XLWorkbook.EvaluateExpr("DAYS360(\"1/1/2008\", \"3/31/2008\",FALSE)"); + Assert.AreEqual(90, actual); + } + + [Test] + public void Days360_US2() + { + Object actual = XLWorkbook.EvaluateExpr("DAYS360(\"3/31/2008\", \"1/1/2008\",FALSE)"); + Assert.AreEqual(-89, actual); + } + + [Test] + public void EDate_Negative1() + { + Object actual = XLWorkbook.EvaluateExpr("EDate(\"3/1/2008\", -1)"); + Assert.AreEqual(new DateTime(2008, 2, 1), actual); + } + + [Test] + public void EDate_Negative2() + { + Object actual = XLWorkbook.EvaluateExpr("EDate(\"3/31/2008\", -1)"); + Assert.AreEqual(new DateTime(2008, 2, 29), actual); + } + + [Test] + public void EDate_Positive1() + { + Object actual = XLWorkbook.EvaluateExpr("EDate(\"3/1/2008\", 1)"); + Assert.AreEqual(new DateTime(2008, 4, 1), actual); + } + + [Test] + public void EDate_Positive2() + { + Object actual = XLWorkbook.EvaluateExpr("EDate(\"3/31/2008\", 1)"); + Assert.AreEqual(new DateTime(2008, 4, 30), actual); + } + + [Test] + public void EOMonth_Negative() + { + Object actual = XLWorkbook.EvaluateExpr("EOMonth(\"3/1/2008\", -1)"); + Assert.AreEqual(new DateTime(2008, 2, 29), actual); + } + + [Test] + public void EOMonth_Positive() + { + Object actual = XLWorkbook.EvaluateExpr("EOMonth(\"3/31/2008\", 1)"); + Assert.AreEqual(new DateTime(2008, 4, 30), actual); + } + + [Test] + public void Hour() + { + Object actual = XLWorkbook.EvaluateExpr("Hour(\"8/22/2008 3:30:45 PM\")"); + Assert.AreEqual(15, actual); + } + + [Test] + public void Minute() + { + Object actual = XLWorkbook.EvaluateExpr("Minute(\"8/22/2008 3:30:45 AM\")"); + Assert.AreEqual(30, actual); + } + + [Test] + public void Month() + { + Object actual = XLWorkbook.EvaluateExpr("Month(\"8/22/2008\")"); + Assert.AreEqual(8, actual); + } + + [Test] + public void IsoWeekNum() + { + Object actual = XLWorkbook.EvaluateExpr("ISOWEEKNUM(DATEVALUE(\"2012-3-9\"))"); + Assert.AreEqual(10, actual); + + actual = XLWorkbook.EvaluateExpr("ISOWEEKNUM(DATE(2012,12,31))"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Networkdays_MultipleHolidaysGiven() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Date") + .CellBelow().SetValue(new DateTime(2008, 10, 1)) + .CellBelow().SetValue(new DateTime(2009, 3, 1)) + .CellBelow().SetValue(new DateTime(2008, 11, 26)) + .CellBelow().SetValue(new DateTime(2008, 12, 4)) + .CellBelow().SetValue(new DateTime(2009, 1, 21)); + Object actual = ws.Evaluate("Networkdays(A2,A3,A4:A6)"); + Assert.AreEqual(105, actual); + } + + [Test] + public void Networkdays_NoHolidaysGiven() + { + Object actual = XLWorkbook.EvaluateExpr("Networkdays(\"10/01/2008\", \"3/01/2009\")"); + Assert.AreEqual(108, actual); + } + + [Test] + public void Networkdays_NegativeResult() + { + Object actual = XLWorkbook.EvaluateExpr("Networkdays(\"3/01/2009\", \"10/01/2008\")"); + Assert.AreEqual(-108, actual); + + actual = XLWorkbook.EvaluateExpr("Networkdays(\"2016-01-01\", \"2015-12-23\")"); + Assert.AreEqual(-8, actual); + } + + [Test] + public void Networkdays_OneHolidaysGiven() + { + Object actual = XLWorkbook.EvaluateExpr("Networkdays(\"10/01/2008\", \"3/01/2009\", \"11/26/2008\")"); + Assert.AreEqual(107, actual); + } + + [Test] + public void Second() + { + Object actual = XLWorkbook.EvaluateExpr("Second(\"8/22/2008 3:30:45 AM\")"); + Assert.AreEqual(45, actual); + } + + [Test] + public void Time() + { + Object actual = XLWorkbook.EvaluateExpr("Time(1,2,3)"); + Assert.AreEqual(new TimeSpan(1, 2, 3), actual); + } + + [Test] + public void TimeValue1() + { + Object actual = XLWorkbook.EvaluateExpr("TimeValue(\"2:24 AM\")"); + Assert.IsTrue(XLHelper.AreEqual(0.1, (double)actual)); + } + + [Test] + public void TimeValue2() + { + Object actual = XLWorkbook.EvaluateExpr("TimeValue(\"22-Aug-2008 6:35 AM\")"); + Assert.IsTrue(XLHelper.AreEqual(0.27430555555555558, (double)actual)); + } + + [Test] + public void Today() + { + Object actual = XLWorkbook.EvaluateExpr("Today()"); + Assert.AreEqual(DateTime.Now.Date, actual); + } + + [Test] + public void Weekday_1() + { + Object actual = XLWorkbook.EvaluateExpr("Weekday(\"2/14/2008\", 1)"); + Assert.AreEqual(5, actual); + } + + [Test] + public void Weekday_2() + { + Object actual = XLWorkbook.EvaluateExpr("Weekday(\"2/14/2008\", 2)"); + Assert.AreEqual(4, actual); + } + + [Test] + public void Weekday_3() + { + Object actual = XLWorkbook.EvaluateExpr("Weekday(\"2/14/2008\", 3)"); + Assert.AreEqual(3, actual); + } + + [Test] + public void Weekday_Omitted() + { + Object actual = XLWorkbook.EvaluateExpr("Weekday(\"2/14/2008\")"); + Assert.AreEqual(5, actual); + } + + [Test] + public void Weeknum_1() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2000\", 1)")); + } + + [Test] + public void Weeknum_10() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2004\", 2)")); + } + + [Test] + public void Weeknum_11() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2005\", 1)")); + } + + [Test] + public void Weeknum_12() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2005\", 2)")); + } + + [Test] + public void Weeknum_13() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2006\", 1)")); + } + + [Test] + public void Weeknum_14() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2006\", 2)")); + } + + [Test] + public void Weeknum_15() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2007\", 1)")); + } + + [Test] + public void Weeknum_16() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2007\", 2)")); + } + + [Test] + public void Weeknum_17() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2008\", 1)")); + } + + [Test] + public void Weeknum_18() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2008\", 2)")); + } + + [Test] + public void Weeknum_19() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2009\", 1)")); + } + + [Test] + public void Weeknum_2() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2000\", 2)")); + } + + [Test] + public void Weeknum_20() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2009\", 2)")); + } + + [Test] + public void Weeknum_3() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2001\", 1)")); + } + + [Test] + public void Weeknum_4() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2001\", 2)")); + } + + [Test] + public void Weeknum_5() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2002\", 1)")); + } + + [Test] + public void Weeknum_6() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2002\", 2)")); + } + + [Test] + public void Weeknum_7() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2003\", 1)")); + } + + [Test] + public void Weeknum_8() + { + Assert.AreEqual(10, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2003\", 2)")); + } + + [Test] + public void Weeknum_9() + { + Assert.AreEqual(11, XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2004\", 1)")); + } + + [Test] + public void Weeknum_Default() + { + Object actual = XLWorkbook.EvaluateExpr("Weeknum(\"3/9/2008\")"); + Assert.AreEqual(11, actual); + } + + [Test] + public void Workdays_MultipleHolidaysGiven() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Date") + .CellBelow().SetValue(new DateTime(2008, 10, 1)) + .CellBelow().SetValue(151) + .CellBelow().SetValue(new DateTime(2008, 11, 26)) + .CellBelow().SetValue(new DateTime(2008, 12, 4)) + .CellBelow().SetValue(new DateTime(2009, 1, 21)); + Object actual = ws.Evaluate("Workday(A2,A3,A4:A6)"); + Assert.AreEqual(new DateTime(2009, 5, 5), actual); + } + + [Test] + public void Workdays_NoHolidaysGiven() + { + Object actual = XLWorkbook.EvaluateExpr("Workday(\"10/01/2008\", 151)"); + Assert.AreEqual(new DateTime(2009, 4, 30), actual); + + actual = XLWorkbook.EvaluateExpr("Workday(\"2016-01-01\", -10)"); + Assert.AreEqual(new DateTime(2015, 12, 18), actual); + } + + [Test] + public void Workdays_OneHolidaysGiven() + { + Object actual = XLWorkbook.EvaluateExpr("Workday(\"10/01/2008\", 152, \"11/26/2008\")"); + Assert.AreEqual(new DateTime(2009, 5, 4), actual); + } + + [Test] + public void Year() + { + Object actual = XLWorkbook.EvaluateExpr("Year(\"8/22/2008\")"); + Assert.AreEqual(2008, actual); + } + + [Test] + public void Yearfrac_1_base0() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2008\",0)"); + Assert.IsTrue(XLHelper.AreEqual(0.25, (double)actual)); + } + + [Test] + public void Yearfrac_1_base1() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2008\",1)"); + Assert.IsTrue(XLHelper.AreEqual(0.24590163934426229, (double)actual)); + } + + [Test] + public void Yearfrac_1_base2() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2008\",2)"); + Assert.IsTrue(XLHelper.AreEqual(0.25, (double)actual)); + } + + [Test] + public void Yearfrac_1_base3() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2008\",3)"); + Assert.IsTrue(XLHelper.AreEqual(0.24657534246575341, (double)actual)); + } + + [Test] + public void Yearfrac_1_base4() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2008\",4)"); + Assert.IsTrue(XLHelper.AreEqual(0.24722222222222223, (double)actual)); + } + + [Test] + public void Yearfrac_2_base0() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2013\",0)"); + Assert.IsTrue(XLHelper.AreEqual(5.25, (double)actual)); + } + + [Test] + public void Yearfrac_2_base1() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2013\",1)"); + Assert.IsTrue(XLHelper.AreEqual(5.24452554744526, (double)actual)); + } + + [Test] + public void Yearfrac_2_base2() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2013\",2)"); + Assert.IsTrue(XLHelper.AreEqual(5.32222222222222, (double)actual)); + } + + [Test] + public void Yearfrac_2_base3() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2013\",3)"); + Assert.IsTrue(XLHelper.AreEqual(5.24931506849315, (double)actual)); + } + + [Test] + public void Yearfrac_2_base4() + { + Object actual = XLWorkbook.EvaluateExpr("Yearfrac(\"1/1/2008\", \"3/31/2013\",4)"); + Assert.IsTrue(XLHelper.AreEqual(5.24722222222222, (double)actual)); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/FormulaCachingTests.cs b/ClosedXML_Tests/Excel/CalcEngine/FormulaCachingTests.cs new file mode 100644 index 0000000..221914c --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/FormulaCachingTests.cs @@ -0,0 +1,391 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Linq; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class FormulaCachingTests + { + [Test] + public void NewWorkbookDoesNotNeedRecalculation() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var cell = sheet.Cell(1, 1); + + Assert.AreEqual(0, wb.RecalculationCounter); + Assert.IsFalse(cell.NeedsRecalculation); + } + } + + [Test] + public void EditCellCausesCounterIncreasing() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var cell = sheet.Cell(1, 1); + cell.Value = "1234567"; + + Assert.Greater(wb.RecalculationCounter, 0); + } + } + + [Test] + public void StaticCellDoesNotNeedRecalculation() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var cell = sheet.Cell(1, 1); + cell.Value = "1234567"; + + Assert.IsFalse(cell.NeedsRecalculation); + } + } + + [Test] + public void EditCellInvalidatesDependentCells() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var cell = sheet.Cell(1, 1); + var dependentCell = sheet.Cell(2, 1); + dependentCell.FormulaA1 = "=A1"; + var _ = dependentCell.Value; + + cell.Value = "1234567"; + + Assert.IsTrue(dependentCell.NeedsRecalculation); + } + } + + [Test] + public void EditFormulaA1InvalidatesDependentCells() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=SUM(A1:A3)"; + a1.Value = 15; + + var res1 = a4.Value; + a2.FormulaA1 = "=A1*20"; + var res2 = a4.Value; + + Assert.AreEqual(15 + 150 + 1500, res1); + Assert.AreEqual(15 + 300 + 3000, res2); + } + } + + [Test] + public void EditFormulaR1C1InvalidatesDependentCells() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=SUM(A1:A3)"; + a1.Value = 15; + + var res1 = a4.Value; + a2.FormulaR1C1 = "=R[-1]C*2"; + var res2 = a4.Value; + + Assert.AreEqual(15 + 150 + 1500, res1); + Assert.AreEqual(15 + 30 + 300, res2); + } + } + + [Test] + public void InsertRowInvalidatesValues() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a4 = sheet.Cell("A4"); + a4.FormulaA1 = "=COUNTBLANK(A1:A3)"; + + var res1 = a4.Value; + sheet.Row(2).InsertRowsAbove(2); + var res2 = a4.Value; + + Assert.AreEqual(3, res1); + Assert.AreEqual(5, res2); + } + } + + [Test] + public void DeleteRowInvalidatesValues() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a4 = sheet.Cell("A4"); + a4.FormulaA1 = "=COUNTBLANK(A1:A3)"; + + var res1 = a4.Value; + sheet.Row(2).Delete(); + var res2 = a4.Value; + + Assert.AreEqual(3, res1); + Assert.AreEqual(2, res2); + } + } + + [Test] + public void ChainedCalculationPreservesIntermediateValues() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=SUM(A1:A3)"; + + a1.Value = 15; + var res = a4.Value; + + Assert.AreEqual(15 + 150 + 1500, res); + Assert.IsFalse(a4.NeedsRecalculation); + Assert.IsFalse(a3.NeedsRecalculation); + Assert.IsFalse(a2.NeedsRecalculation); + Assert.AreEqual(150, a2.CachedValue); + Assert.AreEqual(1500, a3.CachedValue); + Assert.AreEqual(15 + 150 + 1500, a4.CachedValue); + } + } + + [Test] + public void EditingAffectsDependentCells() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=SUM(A1:A3)"; + a1.Value = 15; + + var res1 = a4.Value; + a1.Value = 20; + var res2 = a4.Value; + + Assert.AreEqual(15 + 150 + 1500, res1); + Assert.AreEqual(20 + 200 + 2000, res2); + } + } + + [Test] + [TestCase("C4", new string[] { "C5" })] + [TestCase("D4", new string[] { })] + [TestCase("A1", new string[] { "A2", "A3", "A4", "C1", "C2", "C3", "C5" })] + [TestCase("B2", new string[] { "B3", "B4", "C2", "C3", "C5" })] + [TestCase("C2", new string[] { "C5" })] + public void EditingDoesNotAffectNonDependingCells(string changedCell, string[] affectedCells) + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + sheet.Cell("A2").FormulaA1 = "A1+1"; + sheet.Cell("A3").FormulaA1 = "SUM(A1:A2)"; + sheet.Cell("A4").FormulaA1 = "SUM(A1:A3)"; + sheet.Cell("B2").FormulaA1 = "B1+1"; + sheet.Cell("B3").FormulaA1 = "SUM(B1:B2)"; + sheet.Cell("B4").FormulaA1 = "SUM(B1:B3)"; + sheet.Cell("C1").FormulaA1 = "SUM(A1:B1)"; + sheet.Cell("C2").FormulaA1 = "SUM(A2:B2)"; + sheet.Cell("C3").FormulaA1 = "SUM(A3:B3)"; + sheet.Cell("C5").FormulaA1 = "SUM($A$1:$C$4)"; + sheet.RecalculateAllFormulas(); + var allCells = sheet.CellsUsed(); + + sheet.Cell(changedCell).Value = 100; + var modifiedCells = allCells.Where(cell => cell.NeedsRecalculation); + + Assert.AreEqual(affectedCells.Length, modifiedCells.Count()); + foreach (var cellAddress in affectedCells) + { + Assert.IsTrue(modifiedCells.Any(cell => cell.Address.ToString() == cellAddress), + string.Format("Cell {0} is expected to need recalculation, but it does not", cellAddress)); + } + } + } + + [Test] + public void CircularReferenceFailsCalculating() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=A3*10"; + a1.FormulaA1 = "A2+A3+A4"; + + var getValueA1 = new TestDelegate(() => { var v = a1.Value; }); + var getValueA2 = new TestDelegate(() => { var v = a2.Value; }); + var getValueA3 = new TestDelegate(() => { var v = a3.Value; }); + var getValueA4 = new TestDelegate(() => { var v = a4.Value; }); + + Assert.Throws(typeof(InvalidOperationException), getValueA1); + Assert.Throws(typeof(InvalidOperationException), getValueA2); + Assert.Throws(typeof(InvalidOperationException), getValueA3); + Assert.Throws(typeof(InvalidOperationException), getValueA4); + } + } + + [Test] + public void CircularReferenceRecalculationNeededDoesNotFail() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var a1 = sheet.Cell("A1"); + var a2 = sheet.Cell("A2"); + var a3 = sheet.Cell("A3"); + var a4 = sheet.Cell("A4"); + + a2.FormulaA1 = "=A1*10"; + a3.FormulaA1 = "=A2*10"; + a4.FormulaA1 = "=A3*10"; + var _ = a4.Value; + a1.FormulaA1 = "=SUM(A2:A4)"; + + var recalcNeededA1 = a1.NeedsRecalculation; + var recalcNeededA2 = a2.NeedsRecalculation; + var recalcNeededA3 = a3.NeedsRecalculation; + var recalcNeededA4 = a4.NeedsRecalculation; + + Assert.IsTrue(recalcNeededA1); + Assert.IsTrue(recalcNeededA2); + Assert.IsTrue(recalcNeededA3); + Assert.IsTrue(recalcNeededA4); + } + } + + [Test] + public void DeleteWorksheetInvalidatesValues() + { + using (var wb = new XLWorkbook()) + { + var sheet1 = wb.Worksheets.Add("Sheet1"); + var sheet2 = wb.Worksheets.Add("Sheet2"); + var sheet1_a1 = sheet1.Cell("A1"); + var sheet2_a1 = sheet2.Cell("A1"); + sheet1_a1.FormulaA1 = "Sheet2!A1"; + sheet2_a1.Value = "TestValue"; + + var val1 = sheet1_a1.Value; + sheet2.Delete(); + var getValue = new TestDelegate(() => { var val2 = sheet1_a1.Value; }); + + Assert.AreEqual("TestValue", val1.ToString()); + Assert.Throws(typeof(ArgumentOutOfRangeException), getValue); + } + } + + [Test] + public void TestValueCellsCachedValue() + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + var cell = sheet.Cell(1, 1); + + var date = new DateTime(2018, 4, 19); ; + cell.Value = date; + + Assert.AreEqual(XLDataType.DateTime, cell.DataType); + Assert.AreEqual(date, cell.CachedValue); + + cell.DataType = XLDataType.Number; + + Assert.AreEqual(XLDataType.Number, cell.DataType); + Assert.AreEqual(date.ToOADate(), cell.CachedValue); + } + } + + [Test] + public void CachedValueToExternalWorkbook() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\ExternalLinks\WorkbookWithExternalLink.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + var cell = ws.Cell("B2"); + Assert.IsFalse(cell.NeedsRecalculation); + Assert.IsTrue(cell.HasFormula); + + // This will fail when we start supporting external links + Assert.IsTrue(cell.FormulaA1.StartsWith("[1]")); + + Assert.AreEqual("hello world", cell.CachedValue); + Assert.AreEqual("hello world", cell.Value); + + Assert.AreEqual(11, ws.Evaluate("LEN(B2)")); + + Assert.Throws(() => wb.RecalculateAllFormulas()); + } + } + + [Test] + public void ChangingDataTypeChangesCachedValue() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Test"); + ws.Cell(1, 1).Value = new DateTime(2019, 1, 1, 14, 0, 0); + ws.Cell(1, 2).Value = new DateTime(2019, 1, 1, 17, 45, 0); + var cell = ws.Cell(1, 3); + cell.FormulaA1 = "=B1-A1"; + cell.Style.DateFormat.Format = "hh:mm"; + + Assert.IsNull(cell.CachedValue); + + double value = (double)cell.Value; + Assert.AreEqual(value, cell.CachedValue); + + cell.DataType = XLDataType.DateTime; + Assert.AreEqual(DateTime.FromOADate(value), cell.CachedValue); + Assert.AreEqual("03:45", cell.GetFormattedString()); + + cell.DataType = XLDataType.Number; + Assert.AreEqual(value, (double)cell.CachedValue, 1e-10); + Assert.AreEqual("0.15625", cell.GetFormattedString()); + + cell.DataType = XLDataType.TimeSpan; + Assert.AreEqual(TimeSpan.FromDays(value), (TimeSpan)cell.CachedValue); + Assert.AreEqual("03:45:00", cell.GetFormattedString()); // I think the seconds in this string is due to a shortcoming in the ExcelNumberFormat library + } + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/FunctionsTests.cs b/ClosedXML_Tests/Excel/CalcEngine/FunctionsTests.cs new file mode 100644 index 0000000..3773224 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/FunctionsTests.cs @@ -0,0 +1,754 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class FunctionsTests + { + [SetUp] + public void Init() + { + // Make sure tests run on a deterministic culture + System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + } + + [Test] + public void Asc() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Asc(""Text"")"); + Assert.AreEqual("Text", actual); + } + + [Test] + public void Clean() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(String.Format(@"Clean(""A{0}B"")", Environment.NewLine)); + Assert.AreEqual("AB", actual); + } + + [Test] + public void Combin() + { + object actual1 = XLWorkbook.EvaluateExpr("Combin(200, 2)"); + Assert.AreEqual(19900.0, actual1); + + object actual2 = XLWorkbook.EvaluateExpr("Combin(20.1, 2.9)"); + Assert.AreEqual(190.0, actual2); + } + + [Test] + public void Degrees() + { + object actual1 = XLWorkbook.EvaluateExpr("Degrees(180)"); + Assert.IsTrue(Math.PI - (double)actual1 < XLHelper.Epsilon); + } + + [Test] + public void Dollar() + { + object actual = XLWorkbook.EvaluateExpr("Dollar(12345.123)"); + Assert.AreEqual(TestHelper.CurrencySymbol + "12,345.12", actual); + + actual = XLWorkbook.EvaluateExpr("Dollar(12345.123, 1)"); + Assert.AreEqual(TestHelper.CurrencySymbol + "12,345.1", actual); + } + + [Test] + public void Even() + { + object actual = XLWorkbook.EvaluateExpr("Even(3)"); + Assert.AreEqual(4, actual); + + actual = XLWorkbook.EvaluateExpr("Even(2)"); + Assert.AreEqual(2, actual); + + actual = XLWorkbook.EvaluateExpr("Even(-1)"); + Assert.AreEqual(-2, actual); + + actual = XLWorkbook.EvaluateExpr("Even(-2)"); + Assert.AreEqual(-2, actual); + + actual = XLWorkbook.EvaluateExpr("Even(0)"); + Assert.AreEqual(0, actual); + + actual = XLWorkbook.EvaluateExpr("Even(1.5)"); + Assert.AreEqual(2, actual); + + actual = XLWorkbook.EvaluateExpr("Even(2.01)"); + Assert.AreEqual(4, actual); + } + + [Test] + public void Exact() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr("Exact(\"A\", \"A\")"); + Assert.AreEqual(true, actual); + + actual = XLWorkbook.EvaluateExpr("Exact(\"A\", \"a\")"); + Assert.AreEqual(false, actual); + } + + [Test] + public void Fact() + { + object actual = XLWorkbook.EvaluateExpr("Fact(5.9)"); + Assert.AreEqual(120.0, actual); + } + + [Test] + public void FactDouble() + { + object actual1 = XLWorkbook.EvaluateExpr("FactDouble(6)"); + Assert.AreEqual(48.0, actual1); + object actual2 = XLWorkbook.EvaluateExpr("FactDouble(7)"); + Assert.AreEqual(105.0, actual2); + } + + [Test] + public void Fixed() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr("Fixed(12345.123)"); + Assert.AreEqual("12,345.12", actual); + + actual = XLWorkbook.EvaluateExpr("Fixed(12345.123, 1)"); + Assert.AreEqual("12,345.1", actual); + + actual = XLWorkbook.EvaluateExpr("Fixed(12345.123, 1, TRUE)"); + Assert.AreEqual("12345.1", actual); + } + + [Test] + public void Formula_from_another_sheet() + { + var wb = new XLWorkbook(); + IXLWorksheet ws1 = wb.AddWorksheet("ws1"); + ws1.FirstCell().SetValue(1).CellRight().SetFormulaA1("A1 + 1"); + IXLWorksheet ws2 = wb.AddWorksheet("ws2"); + ws2.FirstCell().SetFormulaA1("ws1!B1 + 1"); + object v = ws2.FirstCell().Value; + Assert.AreEqual(3.0, v); + } + + [Test] + public void Gcd() + { + object actual = XLWorkbook.EvaluateExpr("Gcd(24, 36)"); + Assert.AreEqual(12, actual); + + object actual1 = XLWorkbook.EvaluateExpr("Gcd(5, 0)"); + Assert.AreEqual(5, actual1); + + object actual2 = XLWorkbook.EvaluateExpr("Gcd(0, 5)"); + Assert.AreEqual(5, actual2); + + object actual3 = XLWorkbook.EvaluateExpr("Gcd(240, 360, 30)"); + Assert.AreEqual(30, actual3); + } + + [Test] + public void Lcm() + { + object actual = XLWorkbook.EvaluateExpr("Lcm(24, 36)"); + Assert.AreEqual(72, actual); + + object actual1 = XLWorkbook.EvaluateExpr("Lcm(5, 0)"); + Assert.AreEqual(0, actual1); + + object actual2 = XLWorkbook.EvaluateExpr("Lcm(0, 5)"); + Assert.AreEqual(0, actual2); + + object actual3 = XLWorkbook.EvaluateExpr("Lcm(240, 360, 30)"); + Assert.AreEqual(720, actual3); + } + + [Test] + public void MDetem() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Cell("A1").SetValue(2).CellRight().SetValue(4); + ws.Cell("A2").SetValue(3).CellRight().SetValue(5); + + Object actual; + + ws.Cell("A5").FormulaA1 = "MDeterm(A1:B2)"; + actual = ws.Cell("A5").Value; + + Assert.IsTrue(XLHelper.AreEqual(-2.0, (double)actual)); + + ws.Cell("A6").FormulaA1 = "Sum(A5)"; + actual = ws.Cell("A6").Value; + + Assert.IsTrue(XLHelper.AreEqual(-2.0, (double)actual)); + + ws.Cell("A7").FormulaA1 = "Sum(MDeterm(A1:B2))"; + actual = ws.Cell("A7").Value; + + Assert.IsTrue(XLHelper.AreEqual(-2.0, (double)actual)); + } + + [Test] + public void MInverse() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Cell("A1").SetValue(1).CellRight().SetValue(2).CellRight().SetValue(1); + ws.Cell("A2").SetValue(3).CellRight().SetValue(4).CellRight().SetValue(-1); + ws.Cell("A3").SetValue(0).CellRight().SetValue(2).CellRight().SetValue(0); + + Object actual; + + ws.Cell("A5").FormulaA1 = "MInverse(A1:C3)"; + actual = ws.Cell("A5").Value; + + Assert.IsTrue(XLHelper.AreEqual(0.25, (double)actual)); + + ws.Cell("A6").FormulaA1 = "Sum(A5)"; + actual = ws.Cell("A6").Value; + + Assert.IsTrue(XLHelper.AreEqual(0.25, (double)actual)); + + ws.Cell("A7").FormulaA1 = "Sum(MInverse(A1:C3))"; + actual = ws.Cell("A7").Value; + + Assert.IsTrue(XLHelper.AreEqual(0.5, (double)actual)); + } + + [Test] + public void MMult() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Cell("A1").SetValue(2).CellRight().SetValue(4); + ws.Cell("A2").SetValue(3).CellRight().SetValue(5); + ws.Cell("A3").SetValue(2).CellRight().SetValue(4); + ws.Cell("A4").SetValue(3).CellRight().SetValue(5); + + Object actual; + + ws.Cell("A5").FormulaA1 = "MMult(A1:B2, A3:B4)"; + actual = ws.Cell("A5").Value; + + Assert.AreEqual(16.0, actual); + + ws.Cell("A6").FormulaA1 = "Sum(A5)"; + actual = ws.Cell("A6").Value; + + Assert.AreEqual(16.0, actual); + + ws.Cell("A7").FormulaA1 = "Sum(MMult(A1:B2, A3:B4))"; + actual = ws.Cell("A7").Value; + + Assert.AreEqual(102.0, actual); + } + + [Test] + public void Mod() + { + object actual = XLWorkbook.EvaluateExpr("Mod(3, 2)"); + Assert.AreEqual(1, actual); + + object actual1 = XLWorkbook.EvaluateExpr("Mod(-3, 2)"); + Assert.AreEqual(1, actual1); + + object actual2 = XLWorkbook.EvaluateExpr("Mod(3, -2)"); + Assert.AreEqual(-1, actual2); + + object actual3 = XLWorkbook.EvaluateExpr("Mod(-3, -2)"); + Assert.AreEqual(-1, actual3); + } + + [Test] + public void Multinomial() + { + object actual = XLWorkbook.EvaluateExpr("Multinomial(2,3,4)"); + Assert.AreEqual(1260.0, actual); + } + + [Test] + public void Odd() + { + object actual = XLWorkbook.EvaluateExpr("Odd(1.5)"); + Assert.AreEqual(3, actual); + + object actual1 = XLWorkbook.EvaluateExpr("Odd(3)"); + Assert.AreEqual(3, actual1); + + object actual2 = XLWorkbook.EvaluateExpr("Odd(2)"); + Assert.AreEqual(3, actual2); + + object actual3 = XLWorkbook.EvaluateExpr("Odd(-1)"); + Assert.AreEqual(-1, actual3); + + object actual4 = XLWorkbook.EvaluateExpr("Odd(-2)"); + Assert.AreEqual(-3, actual4); + + actual = XLWorkbook.EvaluateExpr("Odd(0)"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Product() + { + object actual = XLWorkbook.EvaluateExpr("Product(2,3,4)"); + Assert.AreEqual(24.0, actual); + } + + [Test] + public void Quotient() + { + object actual = XLWorkbook.EvaluateExpr("Quotient(5,2)"); + Assert.AreEqual(2, actual); + + actual = XLWorkbook.EvaluateExpr("Quotient(4.5,3.1)"); + Assert.AreEqual(1, actual); + + actual = XLWorkbook.EvaluateExpr("Quotient(-10,3)"); + Assert.AreEqual(-3, actual); + } + + [Test] + public void Radians() + { + object actual = XLWorkbook.EvaluateExpr("Radians(270)"); + Assert.IsTrue(Math.Abs(4.71238898038469 - (double)actual) < XLHelper.Epsilon); + } + + [Test] + public void Roman() + { + object actual = XLWorkbook.EvaluateExpr("Roman(3046, 1)"); + Assert.AreEqual("MMMXLVI", actual); + + actual = XLWorkbook.EvaluateExpr("Roman(270)"); + Assert.AreEqual("CCLXX", actual); + + actual = XLWorkbook.EvaluateExpr("Roman(3999, true)"); + Assert.AreEqual("MMMCMXCIX", actual); + } + + [Test] + public void Round() + { + object actual = XLWorkbook.EvaluateExpr("Round(2.15, 1)"); + Assert.AreEqual(2.2, actual); + + actual = XLWorkbook.EvaluateExpr("Round(2.149, 1)"); + Assert.AreEqual(2.1, actual); + + actual = XLWorkbook.EvaluateExpr("Round(-1.475, 2)"); + Assert.AreEqual(-1.48, actual); + + actual = XLWorkbook.EvaluateExpr("Round(21.5, -1)"); + Assert.AreEqual(20.0, actual); + + actual = XLWorkbook.EvaluateExpr("Round(626.3, -3)"); + Assert.AreEqual(1000.0, actual); + + actual = XLWorkbook.EvaluateExpr("Round(1.98, -1)"); + Assert.AreEqual(0.0, actual); + + actual = XLWorkbook.EvaluateExpr("Round(-50.55, -2)"); + Assert.AreEqual(-100.0, actual); + + actual = XLWorkbook.EvaluateExpr("ROUND(59 * 0.535, 2)"); // (59 * 0.535) = 31.565 + Assert.AreEqual(31.57, actual); + + actual = XLWorkbook.EvaluateExpr("ROUND(59 * -0.535, 2)"); // (59 * -0.535) = -31.565 + Assert.AreEqual(-31.57, actual); + } + + [Test] + public void RoundDown() + { + object actual = XLWorkbook.EvaluateExpr("RoundDown(3.2, 0)"); + Assert.AreEqual(3.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundDown(76.9, 0)"); + Assert.AreEqual(76.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundDown(3.14159, 3)"); + Assert.AreEqual(3.141, actual); + + actual = XLWorkbook.EvaluateExpr("RoundDown(-3.14159, 1)"); + Assert.AreEqual(-3.1, actual); + + actual = XLWorkbook.EvaluateExpr("RoundDown(31415.92654, -2)"); + Assert.AreEqual(31400.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundDown(0, 3)"); + Assert.AreEqual(0.0, actual); + } + + [Test] + public void RoundUp() + { + object actual = XLWorkbook.EvaluateExpr("RoundUp(3.2, 0)"); + Assert.AreEqual(4.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundUp(76.9, 0)"); + Assert.AreEqual(77.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundUp(3.14159, 3)"); + Assert.AreEqual(3.142, actual); + + actual = XLWorkbook.EvaluateExpr("RoundUp(-3.14159, 1)"); + Assert.AreEqual(-3.2, actual); + + actual = XLWorkbook.EvaluateExpr("RoundUp(31415.92654, -2)"); + Assert.AreEqual(31500.0, actual); + + actual = XLWorkbook.EvaluateExpr("RoundUp(0, 3)"); + Assert.AreEqual(0.0, actual); + } + + [Test] + public void SeriesSum() + { + object actual = XLWorkbook.EvaluateExpr("SERIESSUM(2,3,4,5)"); + Assert.AreEqual(40.0, actual); + + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A2").FormulaA1 = "PI()/4"; + ws.Cell("A3").Value = 1; + ws.Cell("A4").FormulaA1 = "-1/FACT(2)"; + ws.Cell("A5").FormulaA1 = "1/FACT(4)"; + ws.Cell("A6").FormulaA1 = "-1/FACT(6)"; + + actual = ws.Evaluate("SERIESSUM(A2,0,2,A3:A6)"); + Assert.IsTrue(Math.Abs(0.70710321482284566 - (double)actual) < XLHelper.Epsilon); + } + + [Test] + public void SqrtPi() + { + object actual = XLWorkbook.EvaluateExpr("SqrtPi(1)"); + Assert.IsTrue(Math.Abs(1.7724538509055159 - (double)actual) < XLHelper.Epsilon); + + actual = XLWorkbook.EvaluateExpr("SqrtPi(2)"); + Assert.IsTrue(Math.Abs(2.5066282746310002 - (double)actual) < XLHelper.Epsilon); + } + + [Test] + public void SubtotalAverage() + { + object actual = XLWorkbook.EvaluateExpr("Subtotal(1,2,3)"); + Assert.AreEqual(2.5, actual); + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(1,""A"",3, 2)"); + Assert.AreEqual(2.5, actual); + } + + [Test] + public void SubtotalCount() + { + object actual = XLWorkbook.EvaluateExpr("Subtotal(2,2,3)"); + Assert.AreEqual(2, actual); + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(2,""A"",3)"); + Assert.AreEqual(1, actual); + } + + [Test] + public void SubtotalCountA() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr("Subtotal(3,2,3)"); + Assert.AreEqual(2.0, actual); + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(3,"""",3)"); + Assert.AreEqual(1.0, actual); + } + + [Test] + public void SubtotalMax() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(4,2,3,""A"")"); + Assert.AreEqual(3.0, actual); + } + + [Test] + public void SubtotalMin() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(5,2,3,""A"")"); + Assert.AreEqual(2.0, actual); + } + + [Test] + public void SubtotalProduct() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(6,2,3,""A"")"); + Assert.AreEqual(6.0, actual); + } + + [Test] + public void SubtotalStDev() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(7,2,3,""A"")"); + Assert.IsTrue(Math.Abs(0.70710678118654757 - (double)actual) < XLHelper.Epsilon); + } + + [Test] + public void SubtotalStDevP() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(8,2,3,""A"")"); + Assert.AreEqual(0.5, actual); + } + + [Test] + public void SubtotalSum() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(9,2,3,""A"")"); + Assert.AreEqual(5.0, actual); + } + + [Test] + public void SubtotalVar() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(10,2,3,""A"")"); + Assert.IsTrue(Math.Abs(0.5 - (double)actual) < XLHelper.Epsilon); + } + + [Test] + public void SubtotalVarP() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"Subtotal(11,2,3,""A"")"); + Assert.AreEqual(0.25, actual); + } + + [Test] + public void SubtotalCalc() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + ws.NamedRanges.Add("subtotalrange", "A37:A38"); + + ws.Cell("A1").Value = 2; + ws.Cell("A2").Value = 4; + ws.Cell("A3").FormulaA1 = "SUBTOTAL(9, A1:A2)"; // simple add subtotal + ws.Cell("A4").Value = 8; + ws.Cell("A5").Value = 16; + ws.Cell("A6").FormulaA1 = "SUBTOTAL(9, A4:A5)"; // simple add subtotal + ws.Cell("A7").Value = 32; + ws.Cell("A8").Value = 64; + ws.Cell("A9").FormulaA1 = "SUM(A7:A8)"; // func but not subtotal + ws.Cell("A10").Value = 128; + ws.Cell("A11").Value = 256; + ws.Cell("A12").FormulaA1 = "SUBTOTAL(1, A10:A11)"; // simple avg subtotal + ws.Cell("A13").Value = 512; + ws.Cell("A14").FormulaA1 = "SUBTOTAL(9, A1:A13)"; // subtotals in range + ws.Cell("A15").Value = 1024; + ws.Cell("A16").Value = 2048; + ws.Cell("A17").FormulaA1 = "42 + SUBTOTAL(9, A15:A16)"; // simple add subtotal in formula + ws.Cell("A18").Value = 4096; + ws.Cell("A19").FormulaA1 = "SUBTOTAL(9, A15:A18)"; // subtotals in range + ws.Cell("A20").Value = 8192; + ws.Cell("A21").Value = 16384; + ws.Cell("A22").FormulaA1 = @"32768 * SEARCH(""SUBTOTAL(9, A1:A2)"", A28)"; // subtotal literal in formula + ws.Cell("A23").FormulaA1 = "SUBTOTAL(9, A20:A22)"; // subtotal literal in formula in range + ws.Cell("A24").Value = 65536; + ws.Cell("A25").FormulaA1 = "A23"; // link to subtotal + ws.Cell("A26").FormulaA1 = "PRODUCT(SUBTOTAL(9, A24:A25), 2)"; // subtotal as parameter in func + ws.Cell("A27").Value = 131072; + ws.Cell("A28").Value = "SUBTOTAL(9, A1:A2)"; // subtotal literal + ws.Cell("A29").FormulaA1 = "SUBTOTAL(9, A27:A28)"; // subtotal literal in range + ws.Cell("A30").FormulaA1 = "SUBTOTAL(9, A31:A32)"; // simple add subtotal backward + ws.Cell("A31").Value = 262144; + ws.Cell("A32").Value = 524288; + ws.Cell("A33").FormulaA1 = "SUBTOTAL(9, A20:A32)"; // subtotals in range + ws.Cell("A34").FormulaA1 = @"SUBTOTAL(VALUE(""9""), A1:A33, A35:A41)"; // func as parameter in subtotal and many ranges + ws.Cell("A35").Value = 1048576; + ws.Cell("A36").FormulaA1 = "SUBTOTAL(9, A31:A32, A35)"; // many ranges + ws.Cell("A37").Value = 2097152; + ws.Cell("A38").Value = 4194304; + ws.Cell("A39").FormulaA1 = "SUBTOTAL(3*3, subtotalrange)"; // formula as parameter in subtotal and named range + ws.Cell("A40").Value = 8388608; + ws.Cell("A41").FormulaA1 = "PRODUCT(SUBTOTAL(A4+1, A35:A40), 2)"; // formula with link as parameter in subtotal + ws.Cell("A42").FormulaA1 = "PRODUCT(SUBTOTAL(A4+1, A35:A40), 2) + SUBTOTAL(A4+1, A35:A40)"; // two subtotals in one formula + + Assert.AreEqual(6, ws.Cell("A3").Value); + Assert.AreEqual(24, ws.Cell("A6").Value); + Assert.AreEqual(192, ws.Cell("A12").Value); + Assert.AreEqual(1118, ws.Cell("A14").Value); + Assert.AreEqual(3114, ws.Cell("A17").Value); + Assert.AreEqual(7168, ws.Cell("A19").Value); + Assert.AreEqual(57344, ws.Cell("A23").Value); + Assert.AreEqual(245760, ws.Cell("A26").Value); + Assert.AreEqual(131072, ws.Cell("A29").Value); + Assert.AreEqual(786432, ws.Cell("A30").Value); + Assert.AreEqual(1097728, ws.Cell("A33").Value); + Assert.AreEqual(16834654, ws.Cell("A34").Value); + Assert.AreEqual(1835008, ws.Cell("A36").Value); + Assert.AreEqual(6291456, ws.Cell("A39").Value); + Assert.AreEqual(31457280, ws.Cell("A41").Value); + Assert.AreEqual(47185920, ws.Cell("A42").Value); + } + + [Test] + public void Sum() + { + IXLCell cell = new XLWorkbook().AddWorksheet("Sheet1").FirstCell(); + IXLCell fCell = cell.SetValue(1).CellBelow().SetValue(2).CellBelow(); + fCell.FormulaA1 = "sum(A1:A2)"; + + Assert.AreEqual(3.0, fCell.Value); + } + + [Test] + public void SumDateTimeAndNumber() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = new DateTime(2018, 1, 1); + Assert.AreEqual(43102, ws.Evaluate("SUM(A1:A2)")); + + ws.Cell("A1").Value = 2; + ws.Cell("A2").FormulaA1 = "DATE(2018,1,1)"; + Assert.AreEqual(43103, ws.Evaluate("SUM(A1:A2)")); + } + } + + [Test] + public void SumSq() + { + Object actual; + + actual = XLWorkbook.EvaluateExpr(@"SumSq(3,4)"); + Assert.AreEqual(25.0, actual); + } + + [Test] + public void TextConcat() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 1; + ws.Cell("B1").Value = 1; + ws.Cell("B2").Value = 1; + + ws.Cell("C1").FormulaA1 = "\"The total value is: \" & SUM(A1:B2)"; + + object r = ws.Cell("C1").Value; + Assert.AreEqual("The total value is: 4", r.ToString()); + } + + [Test] + public void Trim() + { + Assert.AreEqual("Test", XLWorkbook.EvaluateExpr("Trim(\"Test \")")); + + //Should not trim non breaking space + //See http://office.microsoft.com/en-us/excel-help/trim-function-HP010062581.aspx + Assert.AreEqual("Test\u00A0", XLWorkbook.EvaluateExpr("Trim(\"Test\u00A0 \")")); + } + + [Test] + public void TestEmptyTallyOperations() + { + //In these test no values have been set + XLWorkbook wb = new XLWorkbook(); + wb.Worksheets.Add("TallyTests"); + var cell = wb.Worksheet(1).Cell(1, 1).SetFormulaA1("=MAX(D1,D2)"); + Assert.AreEqual(0, cell.Value); + cell = wb.Worksheet(1).Cell(2, 1).SetFormulaA1("=MIN(D1,D2)"); + Assert.AreEqual(0, cell.Value); + cell = wb.Worksheet(1).Cell(3, 1).SetFormulaA1("=SUM(D1,D2)"); + Assert.AreEqual(0, cell.Value); + Assert.That(() => wb.Worksheet(1).Cell(3, 1).SetFormulaA1("=AVERAGE(D1,D2)").Value, Throws.TypeOf()); + } + + [Test] + public void TestOmittedParameters() + { + using (var wb = new XLWorkbook()) + { + object value; + value = wb.Evaluate("=IF(TRUE,1)"); + Assert.AreEqual(1, value); + + value = wb.Evaluate("=IF(TRUE,1,)"); + Assert.AreEqual(1, value); + + value = wb.Evaluate("=IF(FALSE,1,)"); + Assert.AreEqual(false, value); + + value = wb.Evaluate("=IF(FALSE,,2)"); + Assert.AreEqual(2, value); + } + } + + [Test] + public void TestDefaultExcelFunctionNamespace() + { + Assert.DoesNotThrow(() => XLWorkbook.EvaluateExpr("TODAY()")); + Assert.DoesNotThrow(() => XLWorkbook.EvaluateExpr("_xlfn.TODAY()")); + Assert.IsTrue((bool)XLWorkbook.EvaluateExpr("_xlfn.TODAY() = TODAY()")); + } + + [TestCase("=1234%", 12.34)] + [TestCase("=1234%%", 0.1234)] + [TestCase("=100+200%", 102.0)] + [TestCase("=100%+200", 201.0)] + [TestCase("=(100+200)%", 3.0)] + [TestCase("=200%^5", 32.0)] + [TestCase("=200%^400%", 16.0)] + [TestCase("=SUM(100,200,300)%", 6.0)] + public void PercentOperator(string formula, double expectedResult) + { + var res = (double)XLWorkbook.EvaluateExpr(formula); + + Assert.AreEqual(expectedResult, res, XLHelper.Epsilon); + } + + [TestCase("=--1", 1)] + [TestCase("=++1", 1)] + [TestCase("=-+-+-1", -1)] + [TestCase("=2^---2", 0.25)] + public void MultipleUnaryOperators(string formula, double expectedResult) + { + var res = (double)XLWorkbook.EvaluateExpr(formula); + + Assert.AreEqual(expectedResult, res, XLHelper.Epsilon); + } + + [TestCase("RIGHT(\"2020\", 2) + 1", 21)] + [TestCase("LEFT(\"20.2020\", 6) + 1", 21.202)] + [TestCase("2 + (\"3\" & \"4\")", 36)] + [TestCase("2 + \"3\" & \"4\"", "54")] + [TestCase("\"7\" & \"4\"", "74")] + public void TestStringSubExpression(string formula, object expectedResult) + { + var actual = XLWorkbook.EvaluateExpr(formula); + + Assert.AreEqual(expectedResult, actual); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs b/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs new file mode 100644 index 0000000..07b0027 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs @@ -0,0 +1,436 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Globalization; +using System.Threading; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class InformationTests + { + [SetUp] + public void SetCultureInfo() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + } + + #region IsBlank Tests + + [Test] + public void IsBlank_MultipleAllEmpty_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(true, actual); + } + } + + [Test] + public void IsBlank_MultipleAllFill_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "1"; + ws.Cell("A2").Value = "1"; + ws.Cell("A3").Value = "1"; + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsBlank_MultipleMixedFill_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "1"; + ws.Cell("A3").Value = "1"; + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsBlank_Single_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = " "; + var actual = ws.Evaluate("=IsBlank(A1)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsBlank_Single_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + var actual = ws.Evaluate("=IsBlank(A1)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsBlank Tests + + #region IsEven Tests + + [Test] + public void IsEven_Single_False() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 1.2; + ws.Cell("A3").Value = 3; + + var actual = ws.Evaluate("=IsEven(A1)"); + Assert.AreEqual(false, actual); + + actual = ws.Evaluate("=IsEven(A2)"); + Assert.AreEqual(false, actual); + + actual = ws.Evaluate("=IsEven(A3)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsEven_Single_True() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 4; + ws.Cell("A2").Value = 0.2; + ws.Cell("A3").Value = 12.2; + + var actual = ws.Evaluate("=IsEven(A1)"); + Assert.AreEqual(true, actual); + + actual = ws.Evaluate("=IsEven(A2)"); + Assert.AreEqual(true, actual); + + actual = ws.Evaluate("=IsEven(A3)"); + Assert.AreEqual(true, actual); + } + } + + #endregion IsEven Tests + + #region IsLogical Tests + + [Test] + public void IsLogical_Simpe_False() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 123; + + var actual = ws.Evaluate("=IsLogical(A1)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsLogical_Simple_True() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = true; + + var actual = ws.Evaluate("=IsLogical(A1)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsLogical Tests + + [Test] + public void IsNA() + { + object actual; + actual = XLWorkbook.EvaluateExpr("ISNA(#N/A)"); + Assert.AreEqual(true, actual); + + actual = XLWorkbook.EvaluateExpr("ISNA(#REF!)"); + Assert.AreEqual(false, actual); + } + + #region IsNotText Tests + + [Test] + public void IsNotText_Simple_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; + var actual = ws.Evaluate("=IsNonText(A1)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsNotText_Simple_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Comma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = true; //Bool Value + ws.Cell("A6").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsNonText(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A3)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A4)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A5)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A6)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsNotText Tests + + #region IsNumber Tests + + [Test] + public void IsNumber_Simple_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; //String Value + ws.Cell("A2").Value = true; //Bool Value + + var actual = ws.Evaluate("=IsNumber(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsNumber(A2)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsNumber_Simple_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Coma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsNumber(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A3)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A4)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A5)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsNumber Tests + + #region IsOdd Test + + [Test] + public void IsOdd_Simple_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 4; + ws.Cell("A2").Value = 0.2; + ws.Cell("A3").Value = 12.2; + + var actual = ws.Evaluate("=IsOdd(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsOdd(A2)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsOdd(A3)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsOdd_Simple_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 1.2; + ws.Cell("A3").Value = 3; + + var actual = ws.Evaluate("=IsOdd(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsOdd(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsOdd(A3)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsOdd Test + + [Test] + public void IsRef() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; + + ws.Cell("B1").FormulaA1 = "ISREF(A1)"; + ws.Cell("B2").FormulaA1 = "ISREF(5)"; + ws.Cell("B3").FormulaA1 = "ISREF(YEAR(TODAY()))"; + + bool actual; + actual = ws.Cell("B1").GetValue(); + Assert.AreEqual(true, actual); + + actual = ws.Cell("B2").GetValue(); + Assert.AreEqual(false, actual); + + actual = ws.Cell("B3").GetValue(); + Assert.AreEqual(false, actual); + } + } + + #region IsText Tests + + [Test] + public void IsText_Simple_false() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Comma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = true; //Bool Value + ws.Cell("A6").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsText(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A2)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A3)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A4)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A5)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A6)"); + Assert.AreEqual(false, actual); + } + } + + [Test] + public void IsText_Simple_true() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + + ws.Cell("A1").Value = "asd"; + + var actual = ws.Evaluate("=IsText(A1)"); + Assert.AreEqual(true, actual); + } + } + #endregion IsText Tests + + #region N Tests + + [Test] + public void N_Date_SerialNumber() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + var testedDate = DateTime.Now; + ws.Cell("A1").Value = testedDate; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(testedDate.ToOADate(), actual); + } + } + + [Test] + public void N_False_Zero() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = false; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(0, actual); + } + } + + [Test] + public void N_Number_Number() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + var testedValue = 123; + ws.Cell("A1").Value = testedValue; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(testedValue, actual); + } + } + + [Test] + public void N_String_Zero() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(0, actual); + } + } + + [Test] + public void N_True_One() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").Value = true; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(1, actual); + } + } + #endregion N Tests + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/LogicalTests.cs b/ClosedXML_Tests/Excel/CalcEngine/LogicalTests.cs new file mode 100644 index 0000000..403a6c1 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/LogicalTests.cs @@ -0,0 +1,63 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class LogicalTests + { + [Test] + public void If_2_Params_true() + { + Object actual = XLWorkbook.EvaluateExpr(@"if(1 = 1, ""T"")"); + Assert.AreEqual("T", actual); + } + + [Test] + public void If_2_Params_false() + { + Object actual = XLWorkbook.EvaluateExpr(@"if(1 = 2, ""T"")"); + Assert.AreEqual(false, actual); + } + + [Test] + public void If_3_Params_true() + { + Object actual = XLWorkbook.EvaluateExpr(@"if(1 = 1, ""T"", ""F"")"); + Assert.AreEqual("T", actual); + } + + [Test] + public void If_3_Params_false() + { + Object actual = XLWorkbook.EvaluateExpr(@"if(1 = 2, ""T"", ""F"")"); + Assert.AreEqual("F", actual); + } + + [Test] + public void If_Comparing_Against_Empty_String() + { + Object actual; + actual = XLWorkbook.EvaluateExpr(@"if(date(2016, 1, 1) = """", ""A"",""B"")"); + Assert.AreEqual("B", actual); + + actual = XLWorkbook.EvaluateExpr(@"if("""" = date(2016, 1, 1), ""A"",""B"")"); + Assert.AreEqual("B", actual); + + actual = XLWorkbook.EvaluateExpr(@"if("""" = 123, ""A"",""B"")"); + Assert.AreEqual("B", actual); + + actual = XLWorkbook.EvaluateExpr(@"if("""" = """", ""A"",""B"")"); + Assert.AreEqual("A", actual); + } + + [Test] + public void If_Case_Insensitivity() + { + Object actual; + actual = XLWorkbook.EvaluateExpr(@"IF(""text""=""TEXT"", 1, 2)"); + Assert.AreEqual(1, actual); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/LookupTests.cs b/ClosedXML_Tests/Excel/CalcEngine/LookupTests.cs new file mode 100644 index 0000000..2aae1f8 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/LookupTests.cs @@ -0,0 +1,250 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Linq; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class LookupTests + { + private XLWorkbook workbook; + + #region Setup and teardown + + [OneTimeTearDown] + public void Dispose() + { + workbook.Dispose(); + } + + [SetUp] + public void Init() + { + // Make sure tests run on a deterministic culture + System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + workbook = SetupWorkbook(); + } + + private XLWorkbook SetupWorkbook() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Data"); + var data = new object[] + { + new {Id=1, OrderDate = DateTime.Parse("2015-01-06"), Region = "East", Rep = "Jones", Item = "Pencil", Units = 95, UnitCost = 1.99, Total = 189.05 }, + new {Id=2, OrderDate = DateTime.Parse("2015-01-23"), Region = "Central", Rep = "Kivell", Item = "Binder", Units = 50, UnitCost = 19.99, Total = 999.5}, + new {Id=3, OrderDate = DateTime.Parse("2015-02-09"), Region = "Central", Rep = "Jardine", Item = "Pencil", Units = 36, UnitCost = 4.99, Total = 179.64}, + new {Id=4, OrderDate = DateTime.Parse("2015-02-26"), Region = "Central", Rep = "Gill", Item = "Pen", Units = 27, UnitCost = 19.99, Total = 539.73}, + new {Id=5, OrderDate = DateTime.Parse("2015-03-15"), Region = "West", Rep = "Sorvino", Item = "Pencil", Units = 56, UnitCost = 2.99, Total = 167.44}, + new {Id=6, OrderDate = DateTime.Parse("2015-04-01"), Region = "East", Rep = "Jones", Item = "Binder", Units = 60, UnitCost = 4.99, Total = 299.4}, + new {Id=7, OrderDate = DateTime.Parse("2015-04-18"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 75, UnitCost = 1.99, Total = 149.25}, + new {Id=8, OrderDate = DateTime.Parse("2015-05-05"), Region = "Central", Rep = "Jardine", Item = "Pencil", Units = 90, UnitCost = 4.99, Total = 449.1}, + new {Id=9, OrderDate = DateTime.Parse("2015-05-22"), Region = "West", Rep = "Thompson", Item = "Pencil", Units = 32, UnitCost = 1.99, Total = 63.68}, + new {Id=10, OrderDate = DateTime.Parse("2015-06-08"), Region = "East", Rep = "Jones", Item = "Binder", Units = 60, UnitCost = 8.99, Total = 539.4}, + new {Id=11, OrderDate = DateTime.Parse("2015-06-25"), Region = "Central", Rep = "Morgan", Item = "Pencil", Units = 90, UnitCost = 4.99, Total = 449.1}, + new {Id=12, OrderDate = DateTime.Parse("2015-07-12"), Region = "East", Rep = "Howard", Item = "Binder", Units = 29, UnitCost = 1.99, Total = 57.71}, + new {Id=13, OrderDate = DateTime.Parse("2015-07-29"), Region = "East", Rep = "Parent", Item = "Binder", Units = 81, UnitCost = 19.99, Total = 1619.19}, + new {Id=14, OrderDate = DateTime.Parse("2015-08-15"), Region = "East", Rep = "Jones", Item = "Pencil", Units = 35, UnitCost = 4.99, Total = 174.65}, + new {Id=15, OrderDate = DateTime.Parse("2015-09-01"), Region = "Central", Rep = "Smith", Item = "Desk", Units = 2, UnitCost = 125, Total = 250}, + new {Id=16, OrderDate = DateTime.Parse("2015-09-18"), Region = "East", Rep = "Jones", Item = "Pen Set", Units = 16, UnitCost = 15.99, Total = 255.84}, + new {Id=17, OrderDate = DateTime.Parse("2015-10-05"), Region = "Central", Rep = "Morgan", Item = "Binder", Units = 28, UnitCost = 8.99, Total = 251.72}, + new {Id=18, OrderDate = DateTime.Parse("2015-10-22"), Region = "East", Rep = "Jones", Item = "Pen", Units = 64, UnitCost = 8.99, Total = 575.36}, + new {Id=19, OrderDate = DateTime.Parse("2015-11-08"), Region = "East", Rep = "Parent", Item = "Pen", Units = 15, UnitCost = 19.99, Total = 299.85}, + new {Id=20, OrderDate = DateTime.Parse("2015-11-25"), Region = "Central", Rep = "Kivell", Item = "Pen Set", Units = 96, UnitCost = 4.99, Total = 479.04}, + new {Id=21, OrderDate = DateTime.Parse("2015-12-12"), Region = "Central", Rep = "Smith", Item = "Pencil", Units = 67, UnitCost = 1.29, Total = 86.43}, + new {Id=22, OrderDate = DateTime.Parse("2015-12-29"), Region = "East", Rep = "Parent", Item = "Pen Set", Units = 74, UnitCost = 15.99, Total = 1183.26}, + new {Id=23, OrderDate = DateTime.Parse("2016-01-15"), Region = "Central", Rep = "Gill", Item = "Binder", Units = 46, UnitCost = 8.99, Total = 413.54}, + new {Id=24, OrderDate = DateTime.Parse("2016-02-01"), Region = "Central", Rep = "Smith", Item = "Binder", Units = 87, UnitCost = 15, Total = 1305}, + new {Id=25, OrderDate = DateTime.Parse("2016-02-18"), Region = "East", Rep = "Jones", Item = "Binder", Units = 4, UnitCost = 4.99, Total = 19.96}, + new {Id=26, OrderDate = DateTime.Parse("2016-03-07"), Region = "West", Rep = "Sorvino", Item = "Binder", Units = 7, UnitCost = 19.99, Total = 139.93}, + new {Id=27, OrderDate = DateTime.Parse("2016-03-24"), Region = "Central", Rep = "Jardine", Item = "Pen Set", Units = 50, UnitCost = 4.99, Total = 249.5}, + new {Id=28, OrderDate = DateTime.Parse("2016-04-10"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 66, UnitCost = 1.99, Total = 131.34}, + new {Id=29, OrderDate = DateTime.Parse("2016-04-27"), Region = "East", Rep = "Howard", Item = "Pen", Units = 96, UnitCost = 4.99, Total = 479.04}, + new {Id=30, OrderDate = DateTime.Parse("2016-05-14"), Region = "Central", Rep = "Gill", Item = "Pencil", Units = 53, UnitCost = 1.29, Total = 68.37}, + new {Id=31, OrderDate = DateTime.Parse("2016-05-31"), Region = "Central", Rep = "Gill", Item = "Binder", Units = 80, UnitCost = 8.99, Total = 719.2}, + new {Id=32, OrderDate = DateTime.Parse("2016-06-17"), Region = "Central", Rep = "Kivell", Item = "Desk", Units = 5, UnitCost = 125, Total = 625}, + new {Id=33, OrderDate = DateTime.Parse("2016-07-04"), Region = "East", Rep = "Jones", Item = "Pen Set", Units = 62, UnitCost = 4.99, Total = 309.38}, + new {Id=34, OrderDate = DateTime.Parse("2016-07-21"), Region = "Central", Rep = "Morgan", Item = "Pen Set", Units = 55, UnitCost = 12.49, Total = 686.95}, + new {Id=35, OrderDate = DateTime.Parse("2016-08-07"), Region = "Central", Rep = "Kivell", Item = "Pen Set", Units = 42, UnitCost = 23.95, Total = 1005.9}, + new {Id=36, OrderDate = DateTime.Parse("2016-08-24"), Region = "West", Rep = "Sorvino", Item = "Desk", Units = 3, UnitCost = 275, Total = 825}, + new {Id=37, OrderDate = DateTime.Parse("2016-09-10"), Region = "Central", Rep = "Gill", Item = "Pencil", Units = 7, UnitCost = 1.29, Total = 9.03}, + new {Id=38, OrderDate = DateTime.Parse("2016-09-27"), Region = "West", Rep = "Sorvino", Item = "Pen", Units = 76, UnitCost = 1.99, Total = 151.24}, + new {Id=39, OrderDate = DateTime.Parse("2016-10-14"), Region = "West", Rep = "Thompson", Item = "Binder", Units = 57, UnitCost = 19.99, Total = 1139.43}, + new {Id=40, OrderDate = DateTime.Parse("2016-10-31"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 14, UnitCost = 1.29, Total = 18.06}, + new {Id=41, OrderDate = DateTime.Parse("2016-11-17"), Region = "Central", Rep = "Jardine", Item = "Binder", Units = 11, UnitCost = 4.99, Total = 54.89}, + new {Id=42, OrderDate = DateTime.Parse("2016-12-04"), Region = "Central", Rep = "Jardine", Item = "Binder", Units = 94, UnitCost = 19.99, Total = 1879.06}, + new {Id=43, OrderDate = DateTime.Parse("2016-12-21"), Region = "Central", Rep = "Andrews", Item = "Binder", Units = 28, UnitCost = 4.99, Total = 139.72} + }; + ws.FirstCell() + .CellBelow() + .CellRight() + .InsertTable(data); + + return wb; + } + + #endregion Setup and teardown + + [Test] + public void Hlookup() + { + // Range lookup false + var value = workbook.Evaluate(@"=HLOOKUP(""Total"",Data!$B$2:$I$71,4,FALSE)"); + Assert.AreEqual(179.64, value); + } + + [Test] + public void Hyperlink() + { + XLHyperlink hl; + hl = XLWorkbook.EvaluateExpr("HYPERLINK(\"http://github.com/ClosedXML/ClosedXML\")") as XLHyperlink; + Assert.IsNotNull(hl); + Assert.AreEqual("http://github.com/ClosedXML/ClosedXML", hl.ExternalAddress.ToString()); + Assert.AreEqual(string.Empty, hl.Tooltip); + + hl = XLWorkbook.EvaluateExpr("HYPERLINK(\"mailto:jsmith@github.com\", \"jsmith@github.com\")") as XLHyperlink; + Assert.IsNotNull(hl); + Assert.AreEqual("mailto:jsmith@github.com", hl.ExternalAddress.ToString()); + Assert.AreEqual("jsmith@github.com", hl.Tooltip); + } + + [Test] + public void Index() + { + var ws = workbook.Worksheets.First(); + Assert.AreEqual("Kivell", ws.Evaluate(@"=INDEX(B2:J12, 3, 4)")); + + // We don't support optional parameter fully here yet. + // Supposedly, if you omit e.g. the row number, then ROW() of the calling cell should be assumed + // Assert.AreEqual("Gill", ws.Evaluate(@"=INDEX(B2:J12, , 4)")); + + Assert.AreEqual("Rep", ws.Evaluate(@"=INDEX(B2:I2, 4)")); + + Assert.AreEqual(3, ws.Evaluate(@"=INDEX(B2:B20, 4)")); + Assert.AreEqual(3, ws.Evaluate(@"=INDEX(B2:B20, 4, 1)")); + Assert.AreEqual(3, ws.Evaluate(@"=INDEX(B2:B20, 4, )")); + + Assert.AreEqual("Rep", ws.Evaluate(@"=INDEX(B2:J2, 1, 4)")); + Assert.AreEqual("Rep", ws.Evaluate(@"=INDEX(B2:J2, , 4)")); + } + + [Test] + public void Index_Exceptions() + { + var ws = workbook.Worksheets.First(); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:I10, 20, 1)")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:I10, 1, 10)")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:I2, 10)")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:I2, 4, 1)")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:I2, 4, )")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:B10, 20)")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:B10, 20, )")); + Assert.Throws(() => ws.Evaluate(@"INDEX(B2:B10, , 4)")); + } + + [Test] + public void Match() + { + var ws = workbook.Worksheets.First(); + + Object value; + value = ws.Evaluate(@"=MATCH(""Rep"", B2:I2, 0)"); + Assert.AreEqual(4, value); + + value = ws.Evaluate(@"=MATCH(""Rep"", A2:Z2, 0)"); + Assert.AreEqual(5, value); + + value = ws.Evaluate(@"=MATCH(""REP"", B2:I2, 0)"); + Assert.AreEqual(4, value); + + value = ws.Evaluate(@"=MATCH(95, B3:I3, 0)"); + Assert.AreEqual(6, value); + + value = ws.Evaluate(@"=MATCH(DATE(2015,1,6), B3:I3, 0)"); + Assert.AreEqual(2, value); + + value = ws.Evaluate(@"=MATCH(1.99, 3:3, 0)"); + Assert.AreEqual(8, value); + + value = ws.Evaluate(@"=MATCH(43, B:B, 0)"); + Assert.AreEqual(45, value); + + value = ws.Evaluate(@"=MATCH(""cENtraL"", D3:D45, 0)"); + Assert.AreEqual(2, value); + + value = ws.Evaluate(@"=MATCH(4.99, H:H, 0)"); + Assert.AreEqual(5, value); + + value = ws.Evaluate(@"=MATCH(""Rapture"", B2:I2, 1)"); + Assert.AreEqual(2, value); + + value = ws.Evaluate(@"=MATCH(22.5, B3:B45, 1)"); + Assert.AreEqual(22, value); + + value = ws.Evaluate(@"=MATCH(""Rep"", B2:I2)"); + Assert.AreEqual(4, value); + + value = ws.Evaluate(@"=MATCH(""Rep"", B2:I2, 1)"); + Assert.AreEqual(4, value); + + value = ws.Evaluate(@"=MATCH(40, G3:G6, -1)"); + Assert.AreEqual(2, value); + } + + [Test] + public void Match_Exceptions() + { + var ws = workbook.Worksheets.First(); + Assert.Throws(() => ws.Evaluate(@"=MATCH(""Rep"", B2:I5)")); + Assert.Throws(() => ws.Evaluate(@"=MATCH(""Dummy"", B2:I2, 0)")); + Assert.Throws(() => ws.Evaluate(@"=MATCH(4.5,B3:B45,-1)")); + } + + [Test] + public void Vlookup() + { + // Range lookup false + var value = workbook.Evaluate("=VLOOKUP(3,Data!$B$2:$I$71,3,FALSE)"); + Assert.AreEqual("Central", value); + + value = workbook.Evaluate("=VLOOKUP(DATE(2015,5,22),Data!C:I,7,FALSE)"); + Assert.AreEqual(63.68, value); + + value = workbook.Evaluate(@"=VLOOKUP(""Central"",Data!D:E,2,FALSE)"); + Assert.AreEqual("Kivell", value); + + // Case insensitive lookup + value = workbook.Evaluate(@"=VLOOKUP(""central"",Data!D:E,2,FALSE)"); + Assert.AreEqual("Kivell", value); + + // Range lookup true + value = workbook.Evaluate("=VLOOKUP(3,Data!$B$2:$I$71,8,TRUE)"); + Assert.AreEqual(179.64, value); + + value = workbook.Evaluate("=VLOOKUP(3,Data!$B$2:$I$71,8)"); + Assert.AreEqual(179.64, value); + + value = workbook.Evaluate("=VLOOKUP(3,Data!$B$2:$I$71,8,)"); + Assert.AreEqual(179.64, value); + + value = workbook.Evaluate("=VLOOKUP(14.5,Data!$B$2:$I$71,8,TRUE)"); + Assert.AreEqual(174.65, value); + + value = workbook.Evaluate("=VLOOKUP(50,Data!$B$2:$I$71,8,TRUE)"); + Assert.AreEqual(139.72, value); + } + + [Test] + public void Vlookup_Exceptions() + { + Assert.Throws(() => workbook.Evaluate(@"=VLOOKUP("""",Data!$B$2:$I$71,3,FALSE)")); + Assert.Throws(() => workbook.Evaluate(@"=VLOOKUP(50,Data!$B$2:$I$71,3,FALSE)")); + Assert.Throws(() => workbook.Evaluate(@"=VLOOKUP(-1,Data!$B$2:$I$71,2,TRUE)")); + + Assert.Throws(() => workbook.Evaluate(@"=VLOOKUP(20,Data!$B$2:$I$71,9,FALSE)")); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs b/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs new file mode 100644 index 0000000..e264d23 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs @@ -0,0 +1,1471 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Globalization; +using System.Linq; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class MathTrigTests + { + private readonly double tolerance = 1e-10; + + [Theory] + public void Abs_ReturnsItselfOnPositiveNumbers([Range(0, 10, 0.1)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ABS({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(input, actual, tolerance * 10); + } + + [Theory] + public void Abs_ReturnsTheCorrectValueOnNegativeInput([Range(-10, -0.1, 0.1)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ABS({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(-input, actual, tolerance * 10); + } + + [TestCase(-1, 3.141592654)] + [TestCase(-0.9, 2.690565842)] + [TestCase(-0.8, 2.498091545)] + [TestCase(-0.7, 2.346193823)] + [TestCase(-0.6, 2.214297436)] + [TestCase(-0.5, 2.094395102)] + [TestCase(-0.4, 1.982313173)] + [TestCase(-0.3, 1.875488981)] + [TestCase(-0.2, 1.772154248)] + [TestCase(-0.1, 1.670963748)] + [TestCase(0, 1.570796327)] + [TestCase(0.1, 1.470628906)] + [TestCase(0.2, 1.369438406)] + [TestCase(0.3, 1.266103673)] + [TestCase(0.4, 1.159279481)] + [TestCase(0.5, 1.047197551)] + [TestCase(0.6, 0.927295218)] + [TestCase(0.7, 0.79539883)] + [TestCase(0.8, 0.643501109)] + [TestCase(0.9, 0.451026812)] + [TestCase(1, 0)] + public void Acos_ReturnsCorrectValue(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ACOS({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [Theory] + public void Acos_ThrowsNumberExceptionOutsideRange([Range(1.1, 3, 0.1)] double input) + { + // checking input and it's additive inverse as both are outside range. + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ACOS({0})", input.ToString(CultureInfo.InvariantCulture)))); + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ACOS({0})", (-input).ToString(CultureInfo.InvariantCulture)))); + } + + [Theory] + public void Acosh_NumbersBelow1ThrowNumberException([Range(-1, 0.9, 0.1)] double input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ACOSH({0})", input.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(1.2, 0.622362504)] + [TestCase(1.5, 0.96242365)] + [TestCase(1.8, 1.192910731)] + [TestCase(2.1, 1.372859144)] + [TestCase(2.4, 1.522079367)] + [TestCase(2.7, 1.650193455)] + [TestCase(3, 1.762747174)] + [TestCase(3.3, 1.863279351)] + [TestCase(3.6, 1.954207529)] + [TestCase(3.9, 2.037266466)] + [TestCase(4.2, 2.113748231)] + [TestCase(4.5, 2.184643792)] + [TestCase(4.8, 2.250731414)] + [TestCase(5.1, 2.312634419)] + [TestCase(5.4, 2.370860342)] + [TestCase(5.7, 2.425828318)] + [TestCase(6, 2.47788873)] + [TestCase(1, 0)] + public void Acosh_ReturnsCorrectValue(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ACOSH({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [TestCase(-10, 3.041924001)] + [TestCase(-9, 3.030935432)] + [TestCase(-8, 3.017237659)] + [TestCase(-7, 2.999695599)] + [TestCase(-6, 2.976443976)] + [TestCase(-5, 2.944197094)] + [TestCase(-4, 2.89661399)] + [TestCase(-3, 2.819842099)] + [TestCase(-2, 2.677945045)] + [TestCase(-1, 2.35619449)] + [TestCase(0, 1.570796327)] + [TestCase(1, 0.785398163)] + [TestCase(2, 0.463647609)] + [TestCase(3, 0.321750554)] + [TestCase(4, 0.244978663)] + [TestCase(5, 0.19739556)] + [TestCase(6, 0.165148677)] + [TestCase(7, 0.141897055)] + [TestCase(8, 0.124354995)] + [TestCase(9, 0.110657221)] + [TestCase(10, 0.099668652)] + public void Acot_ReturnsCorrectValue(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ACOT({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [Theory] + public void Acoth_ForPlusMinusXSmallerThan1_ThrowsNumberException([Range(-0.9, 0.9, 0.1)] double input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ACOTH({0})", input.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(-10, -0.100335348)] + [TestCase(-9, -0.111571776)] + [TestCase(-8, -0.125657214)] + [TestCase(-7, -0.143841036)] + [TestCase(-6, -0.168236118)] + [TestCase(-5, -0.202732554)] + [TestCase(-4, -0.255412812)] + [TestCase(-3, -0.34657359)] + [TestCase(-2, -0.549306144)] + [TestCase(2, 0.549306144)] + [TestCase(3, 0.34657359)] + [TestCase(4, 0.255412812)] + [TestCase(5, 0.202732554)] + [TestCase(6, 0.168236118)] + [TestCase(7, 0.143841036)] + [TestCase(8, 0.125657214)] + [TestCase(9, 0.111571776)] + [TestCase(10, 0.100335348)] + public void Acoth_ReturnsCorrectValue(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ACOTH({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [TestCase("LVII", 57)] + [TestCase("mcmxii", 1912)] + [TestCase("", 0)] + [TestCase("-IV", -4)] + [TestCase(" XIV", 14)] + [TestCase("MCMLXXXIII ", 1983)] + public void Arabic_ReturnsCorrectNumber(string roman, int arabic) + { + var actual = (int)XLWorkbook.EvaluateExpr(string.Format($"ARABIC(\"{roman}\")")); + Assert.AreEqual(arabic, actual); + } + + [Test] + public void Arabic_ThrowsNumberExceptionOnMinus() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("ARABIC(\"-\")")); + } + + [TestCase("- I")] + [TestCase("roman")] + public void Arabic_ThrowsValueExceptionOnInvalidNumber(string invalidRoman) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"ARABIC(\"{invalidRoman}\")")); + } + + [TestCase(-1, -1.570796327)] + [TestCase(-0.9, -1.119769515)] + [TestCase(-0.8, -0.927295218)] + [TestCase(-0.7, -0.775397497)] + [TestCase(-0.6, -0.643501109)] + [TestCase(-0.5, -0.523598776)] + [TestCase(-0.4, -0.411516846)] + [TestCase(-0.3, -0.304692654)] + [TestCase(-0.2, -0.201357921)] + [TestCase(-0.1, -0.100167421)] + [TestCase(0, 0)] + [TestCase(0.1, 0.100167421)] + [TestCase(0.2, 0.201357921)] + [TestCase(0.3, 0.304692654)] + [TestCase(0.4, 0.411516846)] + [TestCase(0.5, 0.523598776)] + [TestCase(0.6, 0.643501109)] + [TestCase(0.7, 0.775397497)] + [TestCase(0.8, 0.927295218)] + [TestCase(0.9, 1.119769515)] + [TestCase(1, 1.570796327)] + public void Asin_ReturnsCorrectResult(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ASIN({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [Theory] + public void Asin_ThrowsNumberExceptionWhenAbsOfInputGreaterThan1([Range(-3, -1.1, 0.1)] double input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ASIN({0})", input.ToString(CultureInfo.InvariantCulture)))); + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ASIN({0})", (-input).ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(0, 0)] + [TestCase(0.1, 0.0998340788992076)] + [TestCase(0.2, 0.198690110349241)] + [TestCase(0.3, 0.295673047563422)] + [TestCase(0.4, 0.390035319770715)] + [TestCase(0.5, 0.481211825059603)] + [TestCase(0.6, 0.568824898732248)] + [TestCase(0.7, 0.652666566082356)] + [TestCase(0.8, 0.732668256045411)] + [TestCase(0.9, 0.808866935652783)] + [TestCase(1, 0.881373587019543)] + [TestCase(2, 1.44363547517881)] + [TestCase(3, 1.81844645923207)] + [TestCase(4, 2.0947125472611)] + [TestCase(5, 2.31243834127275)] + public void Asinh_ReturnsCorrectResult(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ASINH({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance); + var minusActual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ASINH({0})", (-input).ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(-expectedResult, minusActual, tolerance); + } + + [TestCase(0, 0)] + [TestCase(0.1, 0.099668652491162)] + [TestCase(0.2, 0.197395559849881)] + [TestCase(0.3, 0.291456794477867)] + [TestCase(0.4, 0.380506377112365)] + [TestCase(0.5, 0.463647609000806)] + [TestCase(0.6, 0.540419500270584)] + [TestCase(0.7, 0.610725964389209)] + [TestCase(0.8, 0.674740942223553)] + [TestCase(0.9, 0.732815101786507)] + [TestCase(1, 0.785398163397448)] + [TestCase(2, 1.10714871779409)] + [TestCase(3, 1.24904577239825)] + [TestCase(4, 1.32581766366803)] + [TestCase(5, 1.37340076694502)] + public void Atan_ReturnsCorrectResult(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance); + var minusActual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN({0})", (-input).ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(-expectedResult, minusActual, tolerance); + } + + [Test] + public void Atan2_Returns0OnSecond0AndFirstGreater0([Range(0.1, 5, 0.4)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2({0}, 0)", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(0, actual, tolerance); + } + + [TestCase(1, 2, 1.10714871779409)] + [TestCase(1, 3, 1.24904577239825)] + [TestCase(2, 3, 0.98279372324733)] + [TestCase(1, 4, 1.32581766366803)] + [TestCase(3, 4, 0.92729521800161)] + [TestCase(1, 5, 1.37340076694502)] + [TestCase(2, 5, 1.19028994968253)] + [TestCase(3, 5, 1.03037682652431)] + [TestCase(4, 5, 0.89605538457134)] + [TestCase(1, 6, 1.40564764938027)] + [TestCase(5, 6, 0.87605805059819)] + [TestCase(1, 7, 1.42889927219073)] + [TestCase(2, 7, 1.29249666778979)] + [TestCase(3, 7, 1.16590454050981)] + [TestCase(4, 7, 1.05165021254837)] + [TestCase(5, 7, 0.95054684081208)] + [TestCase(6, 7, 0.86217005466723)] + public void Atan2_ReturnsCorrectResults_EqualOnAllMultiplesOfFraction(double x, double y, double expectedResult) + { + for (int i = 1; i < 5; i++) + { + var actual = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"ATAN2({0}, {1})", + (x * i).ToString(CultureInfo.InvariantCulture), + (y * i).ToString(CultureInfo.InvariantCulture))); + + Assert.AreEqual(expectedResult, actual, tolerance); + } + } + + [Test] + public void Atan2_ReturnsHalfPiOn0AsFirstInputWhenSecondGreater0([Range(0.1, 5, 0.4)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2(0, {0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(0.5 * Math.PI, actual, tolerance); + } + + [Test] + public void Atan2_ReturnsMinus3QuartersOfPiWhenFirstSmaller0AndSecondItsNegative([Range(-5, -0.1, 0.3)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2({0}, {0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(-0.75 * Math.PI, actual, tolerance); + } + + [Test] + public void Atan2_ReturnsMinusHalfPiOn0AsFirstInputWhenSecondSmaller0([Range(-5, -0.1, 0.4)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2(0, {0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(-0.5 * Math.PI, actual, tolerance); + } + + [Test] + public void Atan2_ReturnsPiOn0AsSecondInputWhenFirstSmaller0([Range(-5, -0.1, 0.4)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2({0}, 0)", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(Math.PI, actual, tolerance); + } + + [Test] + public void Atan2_ReturnsQuarterOfPiWhenInputsAreEqualAndGreater0([Range(0.1, 5, 0.3)] double input) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"ATAN2({0}, {0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(0.25 * Math.PI, actual, tolerance); + } + + [Test] + public void Atan2_ThrowsDiv0ExceptionOn0And0() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"ATAN2(0, 0)")); + } + + [TestCase(-0.99, -2.64665241236225)] + [TestCase(-0.9, -1.47221948958322)] + [TestCase(-0.8, -1.09861228866811)] + [TestCase(-0.6, -0.693147180559945)] + [TestCase(-0.4, -0.423648930193602)] + [TestCase(-0.2, -0.202732554054082)] + [TestCase(0, 0)] + [TestCase(0.2, 0.202732554054082)] + [TestCase(0.4, 0.423648930193602)] + [TestCase(0.6, 0.693147180559945)] + [TestCase(0.8, 1.09861228866811)] + [TestCase(-0.9, -1.47221948958322)] + [TestCase(-0.990, -2.64665241236225)] + [TestCase(-0.999, -3.8002011672502)] + public void Atanh_ReturnsCorrectResults(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"ATANH({0})", + input.ToString(CultureInfo.InvariantCulture))); + + Assert.AreEqual(expectedResult, actual, tolerance * 10); + } + + [Theory] + public void Atanh_ThrowsNumberExceptionWhenAbsOfInput1OrGreater([Range(1, 5, 0.2)] double input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ATANH({0})", input.ToString(CultureInfo.InvariantCulture)))); + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"ATANH({0})", (-input).ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(0, 36, "0")] + [TestCase(1, 36, "1")] + [TestCase(2, 36, "2")] + [TestCase(3, 36, "3")] + [TestCase(4, 36, "4")] + [TestCase(5, 36, "5")] + [TestCase(6, 36, "6")] + [TestCase(7, 36, "7")] + [TestCase(8, 36, "8")] + [TestCase(9, 36, "9")] + [TestCase(10, 36, "A")] + [TestCase(11, 36, "B")] + [TestCase(12, 36, "C")] + [TestCase(13, 36, "D")] + [TestCase(14, 36, "E")] + [TestCase(15, 36, "F")] + [TestCase(16, 36, "G")] + [TestCase(17, 36, "H")] + [TestCase(18, 36, "I")] + [TestCase(19, 36, "J")] + [TestCase(20, 36, "K")] + [TestCase(21, 36, "L")] + [TestCase(22, 36, "M")] + [TestCase(23, 36, "N")] + [TestCase(24, 36, "O")] + [TestCase(25, 36, "P")] + [TestCase(26, 36, "Q")] + [TestCase(27, 36, "R")] + [TestCase(28, 36, "S")] + [TestCase(29, 36, "T")] + [TestCase(30, 36, "U")] + [TestCase(31, 36, "V")] + [TestCase(32, 36, "W")] + [TestCase(33, 36, "X")] + [TestCase(34, 36, "Y")] + [TestCase(35, 36, "Z")] + [TestCase(36, 36, "10")] + [TestCase(255, 29, "8N")] + [TestCase(255, 2, "11111111")] + public void Base_ReturnsCorrectResultOnInput(int input, int theBase, string expectedResult) + { + var actual = (string)XLWorkbook.EvaluateExpr(string.Format(@"BASE({0}, {1})", input, theBase)); + Assert.AreEqual(expectedResult, actual); + } + + [TestCase(255, 2, 3, "11111111")] + [TestCase(255, 2, 8, "11111111")] + [TestCase(255, 2, 10, "0011111111")] + [TestCase(10, 3, 4, "0101")] + public void Base_ReturnsCorrectResultOnInputWithMinimalLength(int input, int theBase, int minLength, string expectedResult) + { + var actual = (string)XLWorkbook.EvaluateExpr(string.Format(@"BASE({0}, {1}, {2})", input, theBase, minLength)); + Assert.AreEqual(expectedResult, actual); + } + + [TestCase(@"""x""", "2", "2")] + [TestCase("0", @"""x""", "2")] + [TestCase("0", "2", @"""x""")] + public void Base_ThrowsCellValueExceptionOnAnyInputNotANumber(string input, string theBase, string minLength) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"BASE({input}, {theBase}, {minLength})")); + } + + [Theory] + public void Base_ThrowsNumberExceptionOnBaseSmallerThan2([Range(-2, 1)] int theBase) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"BASE(0, {0})", theBase.ToString(CultureInfo.InvariantCulture)))); + } + + [Theory] + public void Base_ThrowsNumberExceptionOnInputSmallerThan0([Range(-5, -1)] int input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"BASE({0}, 2)", input.ToString(CultureInfo.InvariantCulture)))); + } + + [Theory] + public void Base_ThrowsNumberExceptionOnRadixGreaterThan36([Range(37, 40)] int radix) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"BASE(1, {0})", radix.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(24.3, 5, 25)] + [TestCase(6.7, 1, 7)] + [TestCase(-8.1, 2, -8)] + [TestCase(5.5, 2.1, 6.3)] + [TestCase(5.5, 0, 0)] + [TestCase(-5.5, 2.1, -4.2)] + [TestCase(-5.5, -2.1, -6.3)] + [TestCase(-5.5, 0, 0)] + public void Ceiling(double input, double significance, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr($"CEILING({input.ToInvariantString()}, {significance.ToInvariantString()})"); + Assert.AreEqual(expectedResult, actual, tolerance); + } + + [TestCase(6.7, -1)] + public void Ceiling_ThrowsNumberExceptionOnInvalidInput(double input, double significance) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"CEILING({input.ToInvariantString()}, {significance.ToInvariantString()})")); + } + + [TestCase(24.3, 5, null, 25)] + [TestCase(6.7, null, null, 7)] + [TestCase(-8.1, 2, null, -8)] + [TestCase(5.5, 2.1, 0, 6.3)] + [TestCase(5.5, -2.1, 0, 6.3)] + [TestCase(5.5, 0, 0, 0)] + [TestCase(5.5, 2.1, -1, 6.3)] + [TestCase(5.5, -2.1, -1, 6.3)] + [TestCase(5.5, 0, -1, 0)] + [TestCase(5.5, 2.1, 10, 6.3)] + [TestCase(5.5, -2.1, 10, 6.3)] + [TestCase(5.5, 0, 10, 0)] + [TestCase(-5.5, 2.1, 0, -4.2)] + [TestCase(-5.5, -2.1, 0, -4.2)] + [TestCase(-5.5, 0, 0, 0)] + [TestCase(-5.5, 2.1, -1, -6.3)] + [TestCase(-5.5, -2.1, -1, -6.3)] + [TestCase(-5.5, 0, -1, 0)] + [TestCase(-5.5, 2.1, 10, -6.3)] + [TestCase(-5.5, -2.1, 10, -6.3)] + [TestCase(-5.5, 0, 10, 0)] + public void CeilingMath(double input, double? step, int? mode, double expectedResult) + { + string parameters = input.ToString(CultureInfo.InvariantCulture); + if (step != null) + { + parameters = parameters + ", " + step?.ToString(CultureInfo.InvariantCulture); + if (mode != null) + parameters = parameters + ", " + mode?.ToString(CultureInfo.InvariantCulture); + } + + var actual = (double)XLWorkbook.EvaluateExpr($"CEILING.MATH({parameters})"); + Assert.AreEqual(expectedResult, actual, tolerance); + } + + [Theory] + public void Combin_Returns1ForKis0OrKEqualsN([Range(0, 10)] int n) + { + var actual = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, 0)", n)); + Assert.AreEqual(1, actual); + + var actual2 = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, {0})", n)); + Assert.AreEqual(1, actual2); + } + + [TestCase(4, 2, 6)] + [TestCase(5, 2, 10)] + [TestCase(6, 2, 15)] + [TestCase(6, 3, 20)] + [TestCase(7, 2, 21)] + [TestCase(7, 3, 35)] + public void Combin_ReturnsCorrectResults(int n, int k, int expectedResult) + { + var actual = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, {1})", n, k)); + Assert.AreEqual(expectedResult, actual); + + var actual2 = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, {1})", n, n - k)); + Assert.AreEqual(expectedResult, actual2); + } + + [Theory] + public void Combin_ReturnsNforKis1OrKisNminus1([Range(1, 10)] int n) + { + var actual = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, 1)", n)); + Assert.AreEqual(n, actual); + + var actual2 = XLWorkbook.EvaluateExpr(string.Format(@"COMBIN({0}, {1})", n, n - 1)); + Assert.AreEqual(n, actual2); + } + + [Theory] + public void Combin_ThrowsNumberExceptionForAnyArgumentSmaller0([Range(-4, -1)] int smaller0) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr( + string.Format( + @"COMBIN({0}, {1})", + smaller0.ToString(CultureInfo.InvariantCulture), + (-smaller0).ToString(CultureInfo.InvariantCulture)))); + + Assert.Throws(() => XLWorkbook.EvaluateExpr( + string.Format( + @"COMBIN({0}, {1})", + (-smaller0).ToString(CultureInfo.InvariantCulture), + smaller0.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase("\"no number\"")] + [TestCase("\"\"")] + public void Combin_ThrowsNumericExceptionForAnyArgumentNotNumeric(string input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr( + string.Format( + @"COMBIN({0}, 1)", + input?.ToString(CultureInfo.InvariantCulture)))); + + Assert.Throws(() => XLWorkbook.EvaluateExpr( + string.Format( + @"COMBIN(1, {0})", + input?.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(4, 3, 20)] + [TestCase(10, 3, 220)] + [TestCase(0, 0, 1)] + public void Combina_CalculatesCorrectValues(int number, int chosen, int expectedResult) + { + var actualResult = XLWorkbook.EvaluateExpr($"COMBINA({number}, {chosen})"); + Assert.AreEqual(expectedResult, (long)actualResult); + } + + [Theory] + public void Combina_Returns1WhenChosenIs0([Range(0, 10)] int number) + { + Combina_CalculatesCorrectValues(number, 0, 1); + } + + [TestCase(-1, 2)] + [TestCase(-3, -2)] + [TestCase(2, -2)] + public void Combina_ThrowsNumExceptionOnInvalidValues(int number, int chosen) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr( + string.Format( + @"COMBINA({0}, {1})", + number.ToString(CultureInfo.InvariantCulture), + chosen.ToString(CultureInfo.InvariantCulture)))); + } + + [TestCase(4.23, 3, 20)] + [TestCase(10.4, 3.14, 220)] + [TestCase(0, 0.4, 1)] + public void Combina_TruncatesNumbersCorrectly(double number, double chosen, int expectedResult) + { + var actualResult = XLWorkbook.EvaluateExpr(string.Format( + @"COMBINA({0}, {1})", + number.ToString(CultureInfo.InvariantCulture), + chosen.ToString(CultureInfo.InvariantCulture))); + + Assert.AreEqual(expectedResult, (long)actualResult); + } + + [TestCase(0, 1)] + [TestCase(0.4, 0.921060994002885)] + [TestCase(0.8, 0.696706709347165)] + [TestCase(1.2, 0.362357754476674)] + [TestCase(1.6, -0.0291995223012888)] + [TestCase(2, -0.416146836547142)] + [TestCase(2.4, -0.737393715541245)] + [TestCase(2.8, -0.942222340668658)] + [TestCase(3.2, -0.998294775794753)] + [TestCase(3.6, -0.896758416334147)] + [TestCase(4, -0.653643620863612)] + [TestCase(4.4, -0.307332869978419)] + [TestCase(4.8, 0.0874989834394464)] + [TestCase(5.2, 0.468516671300377)] + [TestCase(5.6, 0.77556587851025)] + [TestCase(6, 0.960170286650366)] + [TestCase(6.4, 0.993184918758193)] + [TestCase(6.8, 0.869397490349825)] + [TestCase(7.2, 0.608351314532255)] + [TestCase(7.6, 0.251259842582256)] + [TestCase(8, -0.145500033808614)] + [TestCase(8.4, -0.519288654116686)] + public void Cos_ReturnsCorrectResult(double input, double expectedResult) + { + var actualResult = (double)XLWorkbook.EvaluateExpr(string.Format("COS({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actualResult, tolerance); + } + + [TestCase(0, 1)] + [TestCase(0.4, 1.08107237183845)] + [TestCase(0.8, 1.33743494630484)] + [TestCase(1.2, 1.81065556732437)] + [TestCase(1.6, 2.57746447119489)] + [TestCase(2, 3.76219569108363)] + [TestCase(2.4, 5.55694716696551)] + [TestCase(2.8, 8.25272841686113)] + [TestCase(3.2, 12.2866462005439)] + [TestCase(3.6, 18.3127790830626)] + [TestCase(4, 27.3082328360165)] + [TestCase(4.4, 40.7315730024356)] + [TestCase(4.8, 60.7593236328919)] + [TestCase(5.2, 90.638879219786)] + [TestCase(5.6, 135.215052644935)] + [TestCase(6, 201.715636122456)] + [TestCase(6.4, 300.923349714678)] + [TestCase(6.8, 448.924202712783)] + [TestCase(7.2, 669.715755490113)] + [TestCase(7.6, 999.098197777775)] + [TestCase(8, 1490.47916125218)] + [TestCase(8.4, 2223.53348628359)] + public void Cosh_ReturnsCorrectResult(double input, double expectedResult) + { + var actualResult = (double)XLWorkbook.EvaluateExpr(string.Format("COSH({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actualResult, tolerance); + var actualResult2 = (double)XLWorkbook.EvaluateExpr(string.Format("COSH({0})", (-input).ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actualResult2, tolerance); + } + + [TestCase(1, 0.642092616)] + [TestCase(2, -0.457657554)] + [TestCase(3, -7.015252551)] + [TestCase(4, 0.863691154)] + [TestCase(5, -0.295812916)] + [TestCase(6, -3.436353004)] + [TestCase(7, 1.147515422)] + [TestCase(8, -0.147065064)] + [TestCase(9, -2.210845411)] + [TestCase(10, 1.542351045)] + [TestCase(11, -0.004425741)] + [TestCase(Math.PI * 0.5, 0)] + [TestCase(45, 0.617369624)] + [TestCase(-2, 0.457657554)] + [TestCase(-3, 7.015252551)] + public void Cot(double input, double expected) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"COT({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expected, actual, tolerance * 10.0); + } + + [Test] + public void Cot_Input0() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("COT(0)")); + } + + [Test] + public void Cot_On0_ThrowsDivisionByZeroException() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"COTH(0)")); + } + + [TestCase(-10, -1.000000004)] + [TestCase(-9, -1.00000003)] + [TestCase(-8, -1.000000225)] + [TestCase(-7, -1.000001663)] + [TestCase(-6, -1.000012289)] + [TestCase(-5, -1.000090804)] + [TestCase(-4, -1.00067115)] + [TestCase(-3, -1.004969823)] + [TestCase(-2, -1.037314721)] + [TestCase(-1, -1.313035285)] + [TestCase(1, 1.313035285)] + [TestCase(2, 1.037314721)] + [TestCase(3, 1.004969823)] + [TestCase(4, 1.00067115)] + [TestCase(5, 1.000090804)] + [TestCase(6, 1.000012289)] + [TestCase(7, 1.000001663)] + [TestCase(8, 1.000000225)] + [TestCase(9, 1.00000003)] + [TestCase(10, 1.000000004)] + public void Coth_Examples(double input, double expected) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"COTH({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expected, actual, tolerance * 10.0); + } + + [Test] + public void Csc_On0_ThrowsDivisionByZeroException() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"CSC(0)")); + } + + [TestCase(-10, 1.838163961)] + [TestCase(-9, -2.426486644)] + [TestCase(-8, -1.010756218)] + [TestCase(-7, -1.522101063)] + [TestCase(-6, 3.578899547)] + [TestCase(-5, 1.042835213)] + [TestCase(-4, 1.321348709)] + [TestCase(-3, -7.086167396)] + [TestCase(-2, -1.09975017)] + [TestCase(-1, -1.188395106)] + [TestCase(1, 1.188395106)] + [TestCase(2, 1.09975017)] + [TestCase(3, 7.086167396)] + [TestCase(4, -1.321348709)] + [TestCase(5, -1.042835213)] + [TestCase(6, -3.578899547)] + [TestCase(7, 1.522101063)] + [TestCase(8, 1.010756218)] + [TestCase(9, 2.426486644)] + [TestCase(10, -1.838163961)] + public void Csc_ReturnsCorrectValues(double input, double expected) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"CSC({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expected, actual, tolerance * 10); + } + + [TestCase(1, 0.850918128)] + [TestCase(2, 0.275720565)] + [TestCase(3, 0.09982157)] + [TestCase(4, 0.03664357)] + [TestCase(5, 0.013476506)] + [TestCase(6, 0.004957535)] + [TestCase(7, 0.001823765)] + [TestCase(8, 0.000670925)] + [TestCase(9, 0.00024682)] + [TestCase(10, 0.000090799859712122200000)] + [TestCase(11, 0.0000334034)] + public void CSch_CalculatesCorrectValues(double input, double expectedOutput) + { + Assert.AreEqual(expectedOutput, (double)XLWorkbook.EvaluateExpr($@"CSCH({input})"), 0.000000001); + } + + [Test] + public void Csch_ReturnsDivisionByZeroErrorOnInput0() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("CSCH(0)")); + } + + [TestCase("FF", 16, 255)] + [TestCase("111", 2, 7)] + [TestCase("zap", 36, 45745)] + public void Decimal(string inputString, int radix, int expectedResult) + { + var actualResult = XLWorkbook.EvaluateExpr($"DECIMAL(\"{inputString}\", {radix})"); + Assert.AreEqual(expectedResult, actualResult); + } + + [Theory] + public void Decimal_ReturnsErrorForRadiansGreater36([Range(37, 255)] int radix) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"DECIMAL(\"0\", {radix})")); + } + + [Theory] + public void Decimal_ReturnsErrorForRadiansSmaller2([Range(-5, 1)] int radix) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"DECIMAL(\"0\", {radix})")); + } + + [Test] + public void Decimal_ZeroIsZeroInAnyRadix([Range(2, 36)] int radix) + { + Assert.AreEqual(0, XLWorkbook.EvaluateExpr($"DECIMAL(\"0\", {radix})")); + } + + [TestCase(0, 0)] + [TestCase(Math.PI, 180)] + [TestCase(Math.PI * 2, 360)] + [TestCase(1, 57.2957795130823)] + [TestCase(2, 114.591559026165)] + [TestCase(3, 171.887338539247)] + [TestCase(4, 229.183118052329)] + [TestCase(5, 286.478897565412)] + [TestCase(6, 343.774677078494)] + [TestCase(7, 401.070456591576)] + [TestCase(8, 458.366236104659)] + [TestCase(9, 515.662015617741)] + [TestCase(10, 572.957795130823)] + [TestCase(Math.PI * 0.5, 90)] + [TestCase(Math.PI * 1.5, 270)] + [TestCase(Math.PI * 0.25, 45)] + [TestCase(-1, -57.2957795130823)] + public void Degrees_ReturnsCorrectResult(double input, double expected) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"DEGREES({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expected, actual, tolerance); + } + + [TestCase(1.5, 2)] + [TestCase(3, 4)] + [TestCase(2, 2)] + [TestCase(-1, -2)] + [TestCase(0, 0)] + [TestCase(Math.PI, 4)] + public void Even_ReturnsCorrectResults(double input, int expectedResult) + { + var actual = (int)XLWorkbook.EvaluateExpr(string.Format(@"EVEN({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual); + } + + [TestCase(0, 1)] + [TestCase(1, Math.E)] + [TestCase(2, 7.38905609893065)] + [TestCase(3, 20.0855369231877)] + [TestCase(4, 54.5981500331442)] + [TestCase(5, 148.413159102577)] + [TestCase(6, 403.428793492735)] + [TestCase(7, 1096.63315842846)] + [TestCase(8, 2980.95798704173)] + [TestCase(9, 8103.08392757538)] + [TestCase(10, 22026.4657948067)] + [TestCase(11, 59874.1417151978)] + [TestCase(12, 162754.791419004)] + public void Exp_ReturnsCorrectResults(double input, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"EXP({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual, tolerance); + } + + [TestCase(0, 1L)] + [TestCase(1, 1L)] + [TestCase(2, 2L)] + [TestCase(3, 6L)] + [TestCase(4, 24L)] + [TestCase(5, 120L)] + [TestCase(6, 720L)] + [TestCase(7, 5040L)] + [TestCase(8, 40320L)] + [TestCase(9, 362880L)] + [TestCase(10, 3628800L)] + [TestCase(11, 39916800L)] + [TestCase(12, 479001600L)] + [TestCase(13, 6227020800L)] + [TestCase(14, 87178291200L)] + [TestCase(15, 1307674368000L)] + [TestCase(16, 20922789888000L)] + [TestCase(0.1, 1L)] + [TestCase(2.3, 2L)] + [TestCase(2.8, 2L)] + public void Fact_ReturnsCorrectResult(double input, long expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"FACT({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual); + } + + [Theory] + public void Fact_ThrowsNumberExceptionForNegativeInput([Range(-10, -1)] int input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"FACT({0})", input.ToString(CultureInfo.InvariantCulture)))); + } + + [Test] + public void Fact_ThrowsValueExceptionForNonNumericInput() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"FACT(""x"")"))); + } + + [TestCase(0, 1L)] + [TestCase(1, 1L)] + [TestCase(2, 2L)] + [TestCase(3, 3L)] + [TestCase(4, 8L)] + [TestCase(5, 15L)] + [TestCase(6, 48L)] + [TestCase(7, 105L)] + [TestCase(8, 384L)] + [TestCase(9, 945L)] + [TestCase(10, 3840L)] + [TestCase(11, 10395L)] + [TestCase(12, 46080L)] + [TestCase(13, 135135L)] + [TestCase(14, 645120)] + [TestCase(15, 2027025)] + [TestCase(16, 10321920)] + [TestCase(-1, 1L)] + [TestCase(0, 1)] + [TestCase(0.1, 1L)] + [TestCase(1.4, 1L)] + [TestCase(2.3, 2L)] + [TestCase(2.8, 2L)] + public void FactDouble_ReturnsCorrectResult(double input, long expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"FACTDOUBLE({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedResult, actual); + } + + [Theory] + public void FactDouble_ThrowsNumberExceptionForInputSmallerThanMinus1([Range(-10, -2)] int input) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"FACTDOUBLE({0})", input.ToString(CultureInfo.InvariantCulture)))); + } + + [Test] + public void FactDouble_ThrowsValueExceptionForNonNumericInput() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(string.Format(@"FACTDOUBLE(""x"")"))); + } + + [TestCase(24.3, 5, 20)] + [TestCase(6.7, 1, 6)] + [TestCase(-8.1, 2, -10)] + [TestCase(5.5, 2.1, 4.2)] + [TestCase(-5.5, 2.1, -6.3)] + [TestCase(-5.5, -2.1, -4.2)] + public void Floor(double input, double significance, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr($"FLOOR({input.ToInvariantString()}, {significance.ToInvariantString()})"); + Assert.AreEqual(expectedResult, actual, tolerance); + } + + [TestCase(6.7, 0)] + [TestCase(-6.7, 0)] + public void Floor_ThrowsDivisionByZeroOnZeroSignificance(double input, double significance) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"FLOOR({input.ToInvariantString()}, {significance.ToInvariantString()})")); + } + + [TestCase(6.7, -1)] + public void Floor_ThrowsNumberExceptionOnInvalidInput(double input, double significance) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"FLOOR({input.ToInvariantString()}, {significance.ToInvariantString()})")); + } + + [Test] + // Functions have to support a period first before we can implement this + [TestCase(24.3, 5, null, 20)] + [TestCase(6.7, null, null, 6)] + [TestCase(-8.1, 2, null, -10)] + [TestCase(5.5, 2.1, 0, 4.2)] + [TestCase(5.5, -2.1, 0, 4.2)] + [TestCase(5.5, 0, 0, 0)] + [TestCase(5.5, 2.1, -1, 4.2)] + [TestCase(5.5, -2.1, -1, 4.2)] + [TestCase(5.5, 0, -2, 0)] + [TestCase(5.5, 2.1, 10, 4.2)] + [TestCase(5.5, -2.1, 10, 4.2)] + [TestCase(5.5, 0, 10, 0)] + [TestCase(-5.5, 2.1, 0, -6.3)] + [TestCase(-5.5, -2.1, 0, -6.3)] + [TestCase(-5.5, 0, 0, 0)] + [TestCase(-5.5, 2.1, -1, -4.2)] + [TestCase(-5.5, -2.1, -1, -4.2)] + [TestCase(-5.5, 0, -1, 0)] + [TestCase(-5.5, 2.1, 10, -4.2)] + [TestCase(-5.5, -2.1, 10, -4.2)] + [TestCase(-5.5, 0, 0, 0)] + public void FloorMath(double input, double? step, int? mode, double expectedResult) + { + string parameters = input.ToString(CultureInfo.InvariantCulture); + if (step != null) + { + parameters = parameters + ", " + step?.ToString(CultureInfo.InvariantCulture); + if (mode != null) + parameters = parameters + ", " + mode?.ToString(CultureInfo.InvariantCulture); + } + + var actual = (double)XLWorkbook.EvaluateExpr(string.Format(@"FLOOR.MATH({0})", parameters)); + Assert.AreEqual(expectedResult, actual, tolerance); + } + + [TestCase(8.9, 8)] + [TestCase(-8.9, -9)] + public void Int(double input, double expected) + { + var actual = XLWorkbook.EvaluateExpr(string.Format(@"INT({0})", input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expected, actual); + } + + [Test] + public void Mod() + { + double actual; + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(1.5, 1)"); + Assert.AreEqual(0.5, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(3, 2)"); + Assert.AreEqual(1, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(-3, 2)"); + Assert.AreEqual(1, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(3, -2)"); + Assert.AreEqual(-1, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(-3, -2)"); + Assert.AreEqual(-1, actual, tolerance); + + ////// + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(-4.3, -0.5)"); + Assert.AreEqual(-0.3, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(6.9, -0.2)"); + Assert.AreEqual(-0.1, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(0.7, 0.6)"); + Assert.AreEqual(0.1, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"MOD(6.2, 1.1)"); + Assert.AreEqual(0.7, actual, tolerance); + } + + [TestCase(10, 3, ExpectedResult = 9.0)] + [TestCase(10.5, 3, ExpectedResult = 12.0)] + [TestCase(10.4, 3, ExpectedResult = 9.0)] + [TestCase(-10, -3, ExpectedResult = -9.0)] + [TestCase(1.3, 0.2, ExpectedResult = 1.4)] + [TestCase(5677.912288, 10, ExpectedResult = 5680.0)] + [TestCase(5674.912288, 10, ExpectedResult = 5670.0)] + [TestCase(0.5, 1, ExpectedResult = 1.0)] + [TestCase(0.49999, 1, ExpectedResult = 0.0)] + [TestCase(0.5, 1, ExpectedResult = 1.0)] + [TestCase(0.49999, 1, ExpectedResult = 0.0)] + [TestCase(0.5, 1, ExpectedResult = 1.0)] + [TestCase(0.49999, 1, ExpectedResult = 0.0)] + [TestCase(-13.4, -3, ExpectedResult = -12.0)] + [TestCase(-13.5, -3, ExpectedResult = -15.0)] + [TestCase(0.9, 0.2, ExpectedResult = 1.0)] + [TestCase(0.89999, 0.2, ExpectedResult = 0.8)] + [TestCase(15.5, 3, ExpectedResult = 15.0)] + [TestCase(1.4, 0.5, ExpectedResult = 1.5)] + [DefaultFloatingPointTolerance(1e-12)] + public double MRound(double number, double multiple) + { + return (double)XLWorkbook.EvaluateExpr($"MROUND({number}, {multiple})"); + } + + [TestCase(123456.123, -10)] + [TestCase(-123456.123, 5)] + public void MRoundExceptions(double number, double multiple) + { + Assert.Throws(() => XLWorkbook.EvaluateExpr($"MROUND({number}, {multiple})")); + } + + [TestCase(0, 1)] + [TestCase(0.3, 1.0467516)] + [TestCase(0.6, 1.21162831)] + [TestCase(0.9, 1.60872581)] + [TestCase(1.2, 2.759703601)] + [TestCase(1.5, 14.1368329)] + [TestCase(1.8, -4.401367872)] + [TestCase(2.1, -1.980801656)] + [TestCase(2.4, -1.356127641)] + [TestCase(2.7, -1.10610642)] + [TestCase(3.0, -1.010108666)] + [TestCase(3.3, -1.012678974)] + [TestCase(3.6, -1.115127532)] + [TestCase(3.9, -1.377538917)] + [TestCase(4.2, -2.039730601)] + [TestCase(4.5, -4.743927548)] + [TestCase(4.8, 11.42870421)] + [TestCase(5.1, 2.645658426)] + [TestCase(5.4, 1.575565187)] + [TestCase(5.7, 1.198016873)] + [TestCase(6.0, 1.041481927)] + [TestCase(6.3, 1.000141384)] + [TestCase(6.6, 1.052373922)] + [TestCase(6.9, 1.225903187)] + [TestCase(7.2, 1.643787029)] + [TestCase(7.5, 2.884876262)] + [TestCase(7.8, 18.53381902)] + [TestCase(8.1, -4.106031636)] + [TestCase(8.4, -1.925711244)] + [TestCase(8.7, -1.335743646)] + [TestCase(9.0, -1.097537906)] + [TestCase(9.3, -1.007835594)] + [TestCase(9.6, -1.015550252)] + [TestCase(9.9, -1.124617578)] + [TestCase(10.2, -1.400039323)] + [TestCase(10.5, -2.102886109)] + [TestCase(10.8, -5.145888341)] + [TestCase(11.1, 9.593612018)] + [TestCase(11.4, 2.541355049)] + [TestCase(45, 1.90359)] + [TestCase(30, 6.48292)] + public void Sec_ReturnsCorrectNumber(double input, double expectedOutput) + { + double result = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"SEC({0})", + input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedOutput, result, 0.00001); + + // as the secant is symmetric for positive and negative numbers, let's assert twice: + double resultForNegative = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"SEC({0})", + (-input).ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedOutput, resultForNegative, 0.00001); + } + + [Test] + public void Sec_ThrowsCellValueExceptionOnNonNumericValue() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"SEC(""number"")")); + } + + [TestCase(-9, 0.00024682)] + [TestCase(-8, 0.000670925)] + [TestCase(-7, 0.001823762)] + [TestCase(-6, 0.004957474)] + [TestCase(-5, 0.013475282)] + [TestCase(-4, 0.036618993)] + [TestCase(-3, 0.099327927)] + [TestCase(-2, 0.265802229)] + [TestCase(-1, 0.648054274)] + [TestCase(0, 1)] + public void Sech_ReturnsCorrectNumber(double input, double expectedOutput) + { + double result = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"SECH({0})", + input.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedOutput, result, 0.00001); + + // as the secant is symmetric for positive and negative numbers, let's assert twice: + double resultForNegative = (double)XLWorkbook.EvaluateExpr( + string.Format( + @"SECH({0})", + (-input).ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(expectedOutput, resultForNegative, 0.00001); + } + + /// + /// refers to Example 1 from the Excel documentation, + /// + /// + /// + /// + [TestCase(63000, "SUMIF(A1:A4,\">160000\", B1:B4)")] + [TestCase(900000, "SUMIF(A1:A4,\">160000\")")] + [TestCase(21000, "SUMIF(A1:A4, 300000, B1:B4)")] + [TestCase(28000, "SUMIF(A1:A4, \">\" &C1, B1:B4)")] + public void SumIf_ReturnsCorrectValues_ReferenceExample1FromMicrosoft(int expectedOutcome, string formula) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell(1, 1).Value = 100000; + ws.Cell(1, 2).Value = 7000; + ws.Cell(2, 1).Value = 200000; + ws.Cell(2, 2).Value = 14000; + ws.Cell(3, 1).Value = 300000; + ws.Cell(3, 2).Value = 21000; + ws.Cell(4, 1).Value = 400000; + ws.Cell(4, 2).Value = 28000; + + ws.Cell(1, 3).Value = 300000; + + Assert.AreEqual(expectedOutcome, (double)ws.Evaluate(formula)); + } + } + + /// + /// refers to Example 2 from the Excel documentation, + /// + /// + /// + /// + [TestCase(2000, "SUMIF(A2:A7,\"Fruits\", C2:C7)")] + [TestCase(12000, "SUMIF(A2:A7,\"Vegetables\", C2:C7)")] + [TestCase(4300, "SUMIF(B2:B7, \"*es\", C2:C7)")] + [TestCase(400, "SUMIF(A2:A7, \"\", C2:C7)")] + public void SumIf_ReturnsCorrectValues_ReferenceExample2FromMicrosoft(int expectedOutcome, string formula) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell(2, 1).Value = "Vegetables"; + ws.Cell(3, 1).Value = "Vegetables"; + ws.Cell(4, 1).Value = "Fruits"; + ws.Cell(5, 1).Value = ""; + ws.Cell(6, 1).Value = "Vegetables"; + ws.Cell(7, 1).Value = "Fruits"; + + ws.Cell(2, 2).Value = "Tomatoes"; + ws.Cell(3, 2).Value = "Celery"; + ws.Cell(4, 2).Value = "Oranges"; + ws.Cell(5, 2).Value = "Butter"; + ws.Cell(6, 2).Value = "Carrots"; + ws.Cell(7, 2).Value = "Apples"; + + ws.Cell(2, 3).Value = 2300; + ws.Cell(3, 3).Value = 5500; + ws.Cell(4, 3).Value = 800; + ws.Cell(5, 3).Value = 400; + ws.Cell(6, 3).Value = 4200; + ws.Cell(7, 3).Value = 1200; + + ws.Cell(1, 3).Value = 300000; + + Assert.AreEqual(expectedOutcome, (double)ws.Evaluate(formula)); + } + } + + [Test] + public void SumIf_ReturnsCorrectValues_WhenCalledOnFullColumn() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Data"); + var data = new object[] + { + new { Id = "A", Value = 2}, + new { Id = "B", Value = 3}, + new { Id = "C", Value = 2}, + new { Id = "A", Value = 1}, + new { Id = "B", Value = 4} + }; + ws.Cell("A1").InsertTable(data); + var formula = "=SUMIF(A:A,\"=A\",B:B)"; + var value = ws.Evaluate(formula); + Assert.AreEqual(3, value); + } + } + + [Test] + public void SumIf_ReturnsCorrectValues_WhenFormulaBelongToSameRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Data"); + var data = new object[] + { + new { Id = "A", Value = 2}, + new { Id = "B", Value = 3}, + new { Id = "C", Value = 2}, + new { Id = "A", Value = 1}, + new { Id = "B", Value = 4}, + }; + ws.Cell("A1").InsertTable(data); + ws.Cell("A7").SetValue("Sum A"); + // SUMIF formula + var formula = "=SUMIF(A:A,\"=A\",B:B)"; + ws.Cell("B7").SetFormulaA1(formula); + var value = ws.Cell("B7").Value; + Assert.AreEqual(3, value); + } + } + + /// + /// refers to Example 2 to SumIf from the Excel documentation. + /// As SumIfs should behave the same if called with three parameters, we can take that example here again. + /// + /// + /// + /// + [TestCase(2000, "SUMIFS(C2:C7, A2:A7, \"Fruits\")")] + [TestCase(12000, "SUMIFS(C2:C7, A2:A7, \"Vegetables\")")] + [TestCase(4300, "SUMIFS(C2:C7, B2:B7, \"*es\")")] + [TestCase(400, "SUMIFS(C2:C7, A2:A7, \"\")")] + public void SumIfs_ReturnsCorrectValues_ReferenceExample2FromMicrosoft(int expectedResult, string formula) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell(2, 1).Value = "Vegetables"; + ws.Cell(3, 1).Value = "Vegetables"; + ws.Cell(4, 1).Value = "Fruits"; + ws.Cell(5, 1).Value = ""; + ws.Cell(6, 1).Value = "Vegetables"; + ws.Cell(7, 1).Value = "Fruits"; + + ws.Cell(2, 2).Value = "Tomatoes"; + ws.Cell(3, 2).Value = "Celery"; + ws.Cell(4, 2).Value = "Oranges"; + ws.Cell(5, 2).Value = "Butter"; + ws.Cell(6, 2).Value = "Carrots"; + ws.Cell(7, 2).Value = "Apples"; + + ws.Cell(2, 3).Value = 2300; + ws.Cell(3, 3).Value = 5500; + ws.Cell(4, 3).Value = 800; + ws.Cell(5, 3).Value = 400; + ws.Cell(6, 3).Value = 4200; + ws.Cell(7, 3).Value = 1200; + + ws.Cell(1, 3).Value = 300000; + + var actualResult = ws.Evaluate(formula).CastTo(); + Assert.AreEqual(expectedResult, actualResult); + } + } + + /// + /// refers to Example 1 to SumIf from the Excel documentation. + /// As SumIfs should behave the same if called with three parameters, but in a different order + /// + /// + /// + /// + [TestCase(63000, "SUMIFS(B1:B4, A1:A4, \">160000\")")] + [TestCase(21000, "SUMIFS(B1:B4, A1:A4, 300000)")] + [TestCase(28000, "SUMIFS(B1:B4, A1:A4, \">\" &C1)")] + public void SumIfs_ReturnsCorrectValues_ReferenceExampleForSumIf1FromMicrosoft(int expectedOutcome, string formula) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell(1, 1).Value = 100000; + ws.Cell(1, 2).Value = 7000; + ws.Cell(2, 1).Value = 200000; + ws.Cell(2, 2).Value = 14000; + ws.Cell(3, 1).Value = 300000; + ws.Cell(3, 2).Value = 21000; + ws.Cell(4, 1).Value = 400000; + ws.Cell(4, 2).Value = 28000; + + ws.Cell(1, 3).Value = 300000; + + Assert.AreEqual(expectedOutcome, (double)ws.Evaluate(formula)); + } + } + + /// + /// refers to example data and formula to SumIfs in the Excel documentation, + /// + /// + [TestCase(20, "=SUMIFS(A2:A9, B2:B9, \"=A*\", C2:C9, \"Tom\")")] + [TestCase(30, "=SUMIFS(A2:A9, B2:B9, \"<>Bananas\", C2:C9, \"Tom\")")] + public void SumIfs_ReturnsCorrectValues_ReferenceExampleFromMicrosoft( + int expectedResult, + string formula) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + var ws = wb.AddWorksheet("Sheet1"); + + var row = 2; + + ws.Cell(row, 1).Value = 5; + ws.Cell(row, 2).Value = "Apples"; + ws.Cell(row, 3).Value = "Tom"; + row++; + + ws.Cell(row, 1).Value = 4; + ws.Cell(row, 2).Value = "Apples"; + ws.Cell(row, 3).Value = "Sarah"; + row++; + + ws.Cell(row, 1).Value = 15; + ws.Cell(row, 2).Value = "Artichokes"; + ws.Cell(row, 3).Value = "Tom"; + row++; + + ws.Cell(row, 1).Value = 3; + ws.Cell(row, 2).Value = "Artichokes"; + ws.Cell(row, 3).Value = "Sarah"; + row++; + + ws.Cell(row, 1).Value = 22; + ws.Cell(row, 2).Value = "Bananas"; + ws.Cell(row, 3).Value = "Tom"; + row++; + + ws.Cell(row, 1).Value = 12; + ws.Cell(row, 2).Value = "Bananas"; + ws.Cell(row, 3).Value = "Sarah"; + row++; + + ws.Cell(row, 1).Value = 10; + ws.Cell(row, 2).Value = "Carrots"; + ws.Cell(row, 3).Value = "Tom"; + row++; + + ws.Cell(row, 1).Value = 33; + ws.Cell(row, 2).Value = "Carrots"; + ws.Cell(row, 3).Value = "Sarah"; + + var actualResult = ws.Evaluate(formula).CastTo(); + + Assert.AreEqual(expectedResult, actualResult, tolerance); + } + } + + [Test] + public void SumProduct() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = Enumerable.Range(1, 10); + ws.FirstCell().CellRight().Value = Enumerable.Range(1, 10).Reverse(); + + Assert.AreEqual(2, ws.Evaluate("SUMPRODUCT(A2)")); + Assert.AreEqual(55, ws.Evaluate("SUMPRODUCT(A1:A10)")); + Assert.AreEqual(220, ws.Evaluate("SUMPRODUCT(A1:A10, B1:B10)")); + + Assert.Throws(() => ws.Evaluate("SUMPRODUCT(A1:A10, B1:B5)")); + + // Blank cells and cells with text should be treated as zeros + ws.Range("A1:A5").Clear(); + Assert.AreEqual(110, ws.Evaluate("SUMPRODUCT(A1:A10, B1:B10)")); + + ws.Range("A1:A5").SetValue("asdf"); + Assert.AreEqual(110, ws.Evaluate("SUMPRODUCT(A1:A10, B1:B10)")); + } + } + + [Test] + public void Trunc() + { + var input = 27.64799257; + var expectedResult = 27; + var actual = (double)XLWorkbook.EvaluateExpr($"TRUNC({input.ToString(CultureInfo.InvariantCulture)})"); + Assert.AreEqual(expectedResult, actual); + } + + [TestCase(27.64799257, -1, 20)] + [TestCase(27.64799257, 0, 27)] + [TestCase(27.64799257, 1, 27.6)] + [TestCase(27.64799257, 4, 27.6479)] + public void Trunc_Specify_Digits(double input, int digits, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr($"TRUNC({input.ToString(CultureInfo.InvariantCulture)}, {digits})"); + Assert.AreEqual(expectedResult, actual); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/PrecedentCellsTests.cs b/ClosedXML_Tests/Excel/CalcEngine/PrecedentCellsTests.cs new file mode 100644 index 0000000..197033d --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/PrecedentCellsTests.cs @@ -0,0 +1,86 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class PrecedentCellsTests + { + [Test] + public void GetPrecedentRangesPreventsDuplication() + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook wb = new XLWorkbook()) + { + var sheet1 = wb.AddWorksheet("Sheet1") as XLWorksheet; + var sheet2 = wb.AddWorksheet("Sheet2"); + var formula = "=MAX(A2:E2)/COUNTBLANK(A2:E2)*MAX(B1:C3)+SUM(Sheet2!B1:C3)+SUM($A$2:$E$2)+A2+B$2+$C$2"; + + var ranges = sheet1.CalcEngine.GetPrecedentRanges(formula); + + Assert.AreEqual(6, ranges.Count()); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet1" && r.RangeAddress.ToString() == "A2:E2")); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet1" && r.RangeAddress.ToString() == "B1:C3")); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet2" && r.RangeAddress.ToString() == "B1:C3")); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet1" && r.RangeAddress.ToString() == "A2:A2")); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet1" && r.RangeAddress.ToString() == "B$2:B$2")); + Assert.IsTrue(ranges.Any(r => r.RangeAddress.Worksheet.Name == "Sheet1" && r.RangeAddress.ToString() == "$C$2:$C$2")); + } + } + } + + [Test] + public void GetPrecedentRangesDealsWithNamedRanges() + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook wb = new XLWorkbook()) + { + var sheet1 = wb.AddWorksheet("Sheet1") as XLWorksheet; + sheet1.NamedRanges.Add("NAMED_RANGE", sheet1.Range("A2:B3")); + var formula = "=SUM(NAMED_RANGE)"; + + var ranges = sheet1.CalcEngine.GetPrecedentRanges(formula); + + Assert.AreEqual(1, ranges.Count()); + Assert.AreEqual("$A$2:$B$3", ranges.First().RangeAddress.ToString()); + } + } + } + + [Test] + public void GetPrecedentCells() + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook wb = new XLWorkbook()) + { + var sheet1 = wb.AddWorksheet("Sheet1") as XLWorksheet; + var sheet2 = wb.AddWorksheet("Sheet2"); + var formula = "=MAX(A2:E2)/COUNTBLANK(A2:E2)*MAX(B1:C3)+SUM(Sheet2!B1:C3)+SUM($A$2:$E$2)+A2+B$2+$C$2"; + var expectedAtSheet1 = new string[] + { "A2", "B2", "C2", "D2", "E2", "B1", "C1", "B3", "C3" }; + var expectedAtSheet2 = new string[] + { "B1", "C1", "B2", "C2", "B3", "C3" }; + + var cells = sheet1.CalcEngine.GetPrecedentCells(formula); + + Assert.AreEqual(15, cells.Count()); + foreach (var address in expectedAtSheet1) + { + Assert.IsTrue(cells.Any(cell => cell.Address.Worksheet.Name == sheet1.Name && cell.Address.ToString() == address), + string.Format("Address {0}!{1} is not presented", sheet1.Name, address)); + } + foreach (var address in expectedAtSheet2) + { + Assert.IsTrue(cells.Any(cell => cell.Address.Worksheet.Name == sheet2.Name && cell.Address.ToString() == address), + string.Format("Address {0}!{1} is not presented", sheet2.Name, address)); + } + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs b/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs new file mode 100644 index 0000000..e4b7cdb --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs @@ -0,0 +1,457 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Linq; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class StatisticalTests + { + private double tolerance = 1e-6; + private XLWorkbook workbook; + + [Test] + public void Average() + { + double value; + value = workbook.Evaluate("AVERAGE(-27.5,93.93,64.51,-70.56)").CastTo(); + Assert.AreEqual(15.095, value, tolerance); + + var ws = workbook.Worksheets.First(); + value = ws.Evaluate("AVERAGE(G3:G45)").CastTo(); + Assert.AreEqual(49.3255814, value, tolerance); + + Assert.That(() => ws.Evaluate("AVERAGE(D3:D45)"), Throws.TypeOf()); + } + + [Test] + public void Count() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=COUNT(D3:D45)").CastTo(); + Assert.AreEqual(0, value); + + value = ws.Evaluate(@"=COUNT(G3:G45)").CastTo(); + Assert.AreEqual(43, value); + + value = ws.Evaluate(@"=COUNT(G:G)").CastTo(); + Assert.AreEqual(43, value); + + value = workbook.Evaluate(@"=COUNT(Data!G:G)").CastTo(); + Assert.AreEqual(43, value); + } + + [Test] + public void CountA() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=COUNTA(D3:D45)").CastTo(); + Assert.AreEqual(43, value); + + value = ws.Evaluate(@"=COUNTA(G3:G45)").CastTo(); + Assert.AreEqual(43, value); + + value = ws.Evaluate(@"=COUNTA(G:G)").CastTo(); + Assert.AreEqual(44, value); + + value = workbook.Evaluate(@"=COUNTA(Data!G:G)").CastTo(); + Assert.AreEqual(44, value); + } + + [Test] + public void CountBlank() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=COUNTBLANK(B:B)").CastTo(); + Assert.AreEqual(1048532, value); + + value = ws.Evaluate(@"=COUNTBLANK(D43:D49)").CastTo(); + Assert.AreEqual(4, value); + + value = ws.Evaluate(@"=COUNTBLANK(E3:E45)").CastTo(); + Assert.AreEqual(0, value); + + value = ws.Evaluate(@"=COUNTBLANK(A1)").CastTo(); + Assert.AreEqual(1, value); + + Assert.Throws(() => workbook.Evaluate(@"=COUNTBLANK(E3:E45)")); + Assert.Throws(() => ws.Evaluate(@"=COUNTBLANK()")); + Assert.Throws(() => ws.Evaluate(@"=COUNTBLANK(A3:A45,E3:E45)")); + } + + [Test] + public void CountIf() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=COUNTIF(D3:D45,""Central"")").CastTo(); + Assert.AreEqual(24, value); + + value = ws.Evaluate(@"=COUNTIF(D:D,""Central"")").CastTo(); + Assert.AreEqual(24, value); + + value = workbook.Evaluate(@"=COUNTIF(Data!D:D,""Central"")").CastTo(); + Assert.AreEqual(24, value); + } + + [TestCase(@"=COUNTIF(Data!E:E, ""J*"")", 13)] + [TestCase(@"=COUNTIF(Data!E:E, ""*i*"")", 21)] + [TestCase(@"=COUNTIF(Data!E:E, ""*in*"")", 9)] + [TestCase(@"=COUNTIF(Data!E:E, ""*i*l"")", 9)] + [TestCase(@"=COUNTIF(Data!E:E, ""*i?e*"")", 9)] + [TestCase(@"=COUNTIF(Data!E:E, ""*o??s*"")", 10)] + [TestCase(@"=COUNTIF(Data!X1:X1000, """")", 1000)] + [TestCase(@"=COUNTIF(Data!E1:E44, """")", 1)] + public void CountIf_ConditionWithWildcards(string formula, int expectedResult) + { + var ws = workbook.Worksheets.First(); + + int value = ws.Evaluate(formula).CastTo(); + Assert.AreEqual(expectedResult, value); + } + + [TestCase(@"=COUNTIF(A1:A10, 1)", 1)] + [TestCase(@"=COUNTIF(A1:A10, 2.0)", 1)] + [TestCase(@"=COUNTIF(A1:A10, ""3"")", 2)] + [TestCase(@"=COUNTIF(A1:A10, 3)", 2)] + [TestCase(@"=COUNTIF(A1:A10, 43831)", 1)] + [TestCase(@"=COUNTIF(A1:A10, DATE(2020, 1, 1))", 1)] + [TestCase(@"=COUNTIF(A1:A10, TRUE)", 1)] + public void CountIf_MixedData(string formula, int expected) + { + // We follow to Excel's convention. + // Excel treats 1 and TRUE as unequal, but 3 and "3" as equal + // LibreOffice Calc handles some SUMIF and COUNTIF differently, e.g. it treats 1 and TRUE as equal, but 3 and "3" differently + var ws = workbook.Worksheet("MixedData"); + Assert.AreEqual(expected, ws.Evaluate(formula)); + } + + [TestCase("x", @"=COUNTIF(A1:A1, ""?"")", 1)] + [TestCase("x", @"=COUNTIF(A1:A1, ""~?"")", 0)] + [TestCase("?", @"=COUNTIF(A1:A1, ""~?"")", 1)] + [TestCase("~?", @"=COUNTIF(A1:A1, ""~?"")", 0)] + [TestCase("~?", @"=COUNTIF(A1:A1, ""~~~?"")", 1)] + [TestCase("?", @"=COUNTIF(A1:A1, ""~~?"")", 0)] + [TestCase("~?", @"=COUNTIF(A1:A1, ""~~?"")", 1)] + [TestCase("~x", @"=COUNTIF(A1:A1, ""~~?"")", 1)] + [TestCase("*", @"=COUNTIF(A1:A1, ""~*"")", 1)] + [TestCase("~*", @"=COUNTIF(A1:A1, ""~*"")", 0)] + [TestCase("~*", @"=COUNTIF(A1:A1, ""~~~*"")", 1)] + [TestCase("*", @"=COUNTIF(A1:A1, ""~~*"")", 0)] + [TestCase("~*", @"=COUNTIF(A1:A1, ""~~*"")", 1)] + [TestCase("~x", @"=COUNTIF(A1:A1, ""~~*"")", 1)] + [TestCase("~xyz", @"=COUNTIF(A1:A1, ""~~*"")", 1)] + public void CountIf_MoreWildcards(string cellContent, string formula, int expectedResult) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.Cell(1, 1).Value = cellContent; + + Assert.AreEqual(expectedResult, (double)ws.Evaluate(formula)); + } + } + + [TestCase("=COUNTIFS(B1:D1, \"=Yes\")", 1)] + [TestCase("=COUNTIFS(B1:B4, \"=Yes\", C1:C4, \"=Yes\")", 2)] + [TestCase("= COUNTIFS(B4:D4, \"=Yes\", B2:D2, \"=Yes\")", 1)] + public void CountIfs_ReferenceExample1FromExcelDocumentations( + string formula, + int expectedOutcome) + { + using (var wb = new XLWorkbook()) + { + wb.ReferenceStyle = XLReferenceStyle.A1; + + var ws = wb.AddWorksheet("Sheet1"); + + ws.Cell(1, 1).Value = "Davidoski"; + ws.Cell(1, 2).Value = "Yes"; + ws.Cell(1, 3).Value = "No"; + ws.Cell(1, 4).Value = "No"; + + ws.Cell(2, 1).Value = "Burke"; + ws.Cell(2, 2).Value = "Yes"; + ws.Cell(2, 3).Value = "Yes"; + ws.Cell(2, 4).Value = "No"; + + ws.Cell(3, 1).Value = "Sundaram"; + ws.Cell(3, 2).Value = "Yes"; + ws.Cell(3, 3).Value = "Yes"; + ws.Cell(3, 4).Value = "Yes"; + + ws.Cell(4, 1).Value = "Levitan"; + ws.Cell(4, 2).Value = "No"; + ws.Cell(4, 3).Value = "Yes"; + ws.Cell(4, 4).Value = "Yes"; + + Assert.AreEqual(expectedOutcome, ws.Evaluate(formula)); + } + } + + [Test] + public void CountIfs_SingleCondition() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=COUNTIFS(D3:D45,""Central"")").CastTo(); + Assert.AreEqual(24, value); + + value = ws.Evaluate(@"=COUNTIFS(D:D,""Central"")").CastTo(); + Assert.AreEqual(24, value); + + value = workbook.Evaluate(@"=COUNTIFS(Data!D:D,""Central"")").CastTo(); + Assert.AreEqual(24, value); + } + + [TestCase(@"=COUNTIFS(Data!E:E, ""J*"")", 13)] + [TestCase(@"=COUNTIFS(Data!E:E, ""*i*"")", 21)] + [TestCase(@"=COUNTIFS(Data!E:E, ""*in*"")", 9)] + [TestCase(@"=COUNTIFS(Data!E:E, ""*i*l"")", 9)] + [TestCase(@"=COUNTIFS(Data!E:E, ""*i?e*"")", 9)] + [TestCase(@"=COUNTIFS(Data!E:E, ""*o??s*"")", 10)] + [TestCase(@"=COUNTIFS(Data!X1:X1000, """")", 1000)] + [TestCase(@"=COUNTIFS(Data!E1:E44, """")", 1)] + public void CountIfs_SingleConditionWithWildcards(string formula, int expectedResult) + { + var ws = workbook.Worksheets.First(); + + int value = ws.Evaluate(formula).CastTo(); + Assert.AreEqual(expectedResult, value); + } + + [OneTimeTearDown] + public void Dispose() + { + workbook.Dispose(); + } + + [SetUp] + public void Init() + { + // Make sure tests run on a deterministic culture + System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + workbook = SetupWorkbook(); + } + + [Test] + public void Max() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=MAX(D3:D45)").CastTo(); + Assert.AreEqual(0, value); + + value = ws.Evaluate(@"=MAX(G3:G45)").CastTo(); + Assert.AreEqual(96, value); + + value = ws.Evaluate(@"=MAX(G:G)").CastTo(); + Assert.AreEqual(96, value); + + value = workbook.Evaluate(@"=MAX(Data!G:G)").CastTo(); + Assert.AreEqual(96, value); + } + + [Test] + public void Min() + { + var ws = workbook.Worksheets.First(); + int value; + value = ws.Evaluate(@"=MIN(D3:D45)").CastTo(); + Assert.AreEqual(0, value); + + value = ws.Evaluate(@"=MIN(G3:G45)").CastTo(); + Assert.AreEqual(2, value); + + value = ws.Evaluate(@"=MIN(G:G)").CastTo(); + Assert.AreEqual(2, value); + + value = workbook.Evaluate(@"=MIN(Data!G:G)").CastTo(); + Assert.AreEqual(2, value); + } + + [Test] + public void StDev() + { + var ws = workbook.Worksheets.First(); + double value; + Assert.That(() => ws.Evaluate(@"=STDEV(D3:D45)"), Throws.TypeOf()); + + value = ws.Evaluate(@"=STDEV(H3:H45)").CastTo(); + Assert.AreEqual(47.34511769, value, tolerance); + + value = ws.Evaluate(@"=STDEV(H:H)").CastTo(); + Assert.AreEqual(47.34511769, value, tolerance); + + value = workbook.Evaluate(@"=STDEV(Data!H:H)").CastTo(); + Assert.AreEqual(47.34511769, value, tolerance); + } + + [Test] + public void StDevP() + { + var ws = workbook.Worksheets.First(); + double value; + Assert.That(() => ws.Evaluate(@"=STDEVP(D3:D45)"), Throws.InvalidOperationException); + + value = ws.Evaluate(@"=STDEVP(H3:H45)").CastTo(); + Assert.AreEqual(46.79135458, value, tolerance); + + value = ws.Evaluate(@"=STDEVP(H:H)").CastTo(); + Assert.AreEqual(46.79135458, value, tolerance); + + value = workbook.Evaluate(@"=STDEVP(Data!H:H)").CastTo(); + Assert.AreEqual(46.79135458, value, tolerance); + } + + [TestCase(@"=SUMIF(A1:A10, 1, A1:A10)", 1)] + [TestCase(@"=SUMIF(A1:A10, 2.0, A1:A10)", 2)] + [TestCase(@"=SUMIF(A1:A10, 3, A1:A10)", 3)] + [TestCase(@"=SUMIF(A1:A10, ""3"", A1:A10)", 3)] + [TestCase(@"=SUMIF(A1:A10, 43831, A1:A10)", 43831)] + [TestCase(@"=SUMIF(A1:A10, DATE(2020, 1, 1), A1:A10)", 43831)] + [TestCase(@"=SUMIF(A1:A10, TRUE, A1:A10)", 0)] + public void SumIf_MixedData(string formula, double expected) + { + // We follow to Excel's convention. + // Excel treats 1 and TRUE as unequal, but 3 and "3" as equal + // LibreOffice Calc handles some SUMIF and COUNTIF differently, e.g. it treats 1 and TRUE as equal, but 3 and "3" differently + var ws = workbook.Worksheet("MixedData"); + Assert.AreEqual(expected, ws.Evaluate(formula)); + } + + [Test] + [TestCase("COUNT(G:I,G:G,H:I)", 258d, Description = "COUNT overlapping columns")] + [TestCase("COUNT(6:8,6:6,7:8)", 30d, Description = "COUNT overlapping rows")] + [TestCase("COUNTBLANK(H:J)", 3145640d, Description = "COUNTBLANK columns")] + [TestCase("COUNTBLANK(7:9)", 49128d, Description = "COUNTBLANK rows")] + [TestCase("COUNT(1:1048576)", 216d, Description = "COUNT worksheet")] + [TestCase("COUNTBLANK(1:1048576)", 17179868831d, Description = "COUNTBLANK worksheet")] + [TestCase("SUM(H:J)", 20501.15d, Description = "SUM columns")] + [TestCase("SUM(4:5)", 85366.12d, Description = "SUM rows")] + [TestCase("SUMIF(G:G,50,H:H)", 24.98d, Description = "SUMIF columns")] + [TestCase("SUMIF(G23:G52,\"\",H3:H32)", 53.24d, Description = "SUMIF ranges")] + [TestCase("SUMIFS(H:H,G:G,50,I:I,\">900\")", 19.99d, Description = "SUMIFS columns")] + public void TallySkipsEmptyCells(string formulaA1, double expectedResult) + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + //Let's pre-initialize cells we need so they didn't affect the result + ws.Range("A1:J45").Style.Fill.BackgroundColor = XLColor.Amber; + ws.Cell("ZZ1000").Value = 1; + int initialCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + var actualResult = (double)ws.Evaluate(formulaA1); + int cellsCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + Assert.AreEqual(expectedResult, actualResult, tolerance); + Assert.AreEqual(initialCount, cellsCount); + } + } + + [Test] + public void Var() + { + var ws = workbook.Worksheets.First(); + double value; + Assert.That(() => ws.Evaluate(@"=VAR(D3:D45)"), Throws.InvalidOperationException); + + value = ws.Evaluate(@"=VAR(H3:H45)").CastTo(); + Assert.AreEqual(2241.560169, value, tolerance); + + value = ws.Evaluate(@"=VAR(H:H)").CastTo(); + Assert.AreEqual(2241.560169, value, tolerance); + + value = workbook.Evaluate(@"=VAR(Data!H:H)").CastTo(); + Assert.AreEqual(2241.560169, value, tolerance); + } + + [Test] + public void VarP() + { + var ws = workbook.Worksheets.First(); + double value; + Assert.That(() => ws.Evaluate(@"=VARP(D3:D45)"), Throws.InvalidOperationException); + + value = ws.Evaluate(@"=VARP(H3:H45)").CastTo(); + Assert.AreEqual(2189.430863, value, tolerance); + + value = ws.Evaluate(@"=VARP(H:H)").CastTo(); + Assert.AreEqual(2189.430863, value, tolerance); + + value = workbook.Evaluate(@"=VARP(Data!H:H)").CastTo(); + Assert.AreEqual(2189.430863, value, tolerance); + } + + private XLWorkbook SetupWorkbook() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Data"); + var data = new object[] + { + new {Id=1, OrderDate = DateTime.Parse("2015-01-06"), Region = "East", Rep = "Jones", Item = "Pencil", Units = 95, UnitCost = 1.99, Total = 189.05 }, + new {Id=2, OrderDate = DateTime.Parse("2015-01-23"), Region = "Central", Rep = "Kivell", Item = "Binder", Units = 50, UnitCost = 19.99, Total = 999.5}, + new {Id=3, OrderDate = DateTime.Parse("2015-02-09"), Region = "Central", Rep = "Jardine", Item = "Pencil", Units = 36, UnitCost = 4.99, Total = 179.64}, + new {Id=4, OrderDate = DateTime.Parse("2015-02-26"), Region = "Central", Rep = "Gill", Item = "Pen", Units = 27, UnitCost = 19.99, Total = 539.73}, + new {Id=5, OrderDate = DateTime.Parse("2015-03-15"), Region = "West", Rep = "Sorvino", Item = "Pencil", Units = 56, UnitCost = 2.99, Total = 167.44}, + new {Id=6, OrderDate = DateTime.Parse("2015-04-01"), Region = "East", Rep = "Jones", Item = "Binder", Units = 60, UnitCost = 4.99, Total = 299.4}, + new {Id=7, OrderDate = DateTime.Parse("2015-04-18"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 75, UnitCost = 1.99, Total = 149.25}, + new {Id=8, OrderDate = DateTime.Parse("2015-05-05"), Region = "Central", Rep = "Jardine", Item = "Pencil", Units = 90, UnitCost = 4.99, Total = 449.1}, + new {Id=9, OrderDate = DateTime.Parse("2015-05-22"), Region = "West", Rep = "Thompson", Item = "Pencil", Units = 32, UnitCost = 1.99, Total = 63.68}, + new {Id=10, OrderDate = DateTime.Parse("2015-06-08"), Region = "East", Rep = "Jones", Item = "Binder", Units = 60, UnitCost = 8.99, Total = 539.4}, + new {Id=11, OrderDate = DateTime.Parse("2015-06-25"), Region = "Central", Rep = "Morgan", Item = "Pencil", Units = 90, UnitCost = 4.99, Total = 449.1}, + new {Id=12, OrderDate = DateTime.Parse("2015-07-12"), Region = "East", Rep = "Howard", Item = "Binder", Units = 29, UnitCost = 1.99, Total = 57.71}, + new {Id=13, OrderDate = DateTime.Parse("2015-07-29"), Region = "East", Rep = "Parent", Item = "Binder", Units = 81, UnitCost = 19.99, Total = 1619.19}, + new {Id=14, OrderDate = DateTime.Parse("2015-08-15"), Region = "East", Rep = "Jones", Item = "Pencil", Units = 35, UnitCost = 4.99, Total = 174.65}, + new {Id=15, OrderDate = DateTime.Parse("2015-09-01"), Region = "Central", Rep = "Smith", Item = "Desk", Units = 2, UnitCost = 125, Total = 250}, + new {Id=16, OrderDate = DateTime.Parse("2015-09-18"), Region = "East", Rep = "Jones", Item = "Pen Set", Units = 16, UnitCost = 15.99, Total = 255.84}, + new {Id=17, OrderDate = DateTime.Parse("2015-10-05"), Region = "Central", Rep = "Morgan", Item = "Binder", Units = 28, UnitCost = 8.99, Total = 251.72}, + new {Id=18, OrderDate = DateTime.Parse("2015-10-22"), Region = "East", Rep = "Jones", Item = "Pen", Units = 64, UnitCost = 8.99, Total = 575.36}, + new {Id=19, OrderDate = DateTime.Parse("2015-11-08"), Region = "East", Rep = "Parent", Item = "Pen", Units = 15, UnitCost = 19.99, Total = 299.85}, + new {Id=20, OrderDate = DateTime.Parse("2015-11-25"), Region = "Central", Rep = "Kivell", Item = "Pen Set", Units = 96, UnitCost = 4.99, Total = 479.04}, + new {Id=21, OrderDate = DateTime.Parse("2015-12-12"), Region = "Central", Rep = "Smith", Item = "Pencil", Units = 67, UnitCost = 1.29, Total = 86.43}, + new {Id=22, OrderDate = DateTime.Parse("2015-12-29"), Region = "East", Rep = "Parent", Item = "Pen Set", Units = 74, UnitCost = 15.99, Total = 1183.26}, + new {Id=23, OrderDate = DateTime.Parse("2016-01-15"), Region = "Central", Rep = "Gill", Item = "Binder", Units = 46, UnitCost = 8.99, Total = 413.54}, + new {Id=24, OrderDate = DateTime.Parse("2016-02-01"), Region = "Central", Rep = "Smith", Item = "Binder", Units = 87, UnitCost = 15, Total = 1305}, + new {Id=25, OrderDate = DateTime.Parse("2016-02-18"), Region = "East", Rep = "Jones", Item = "Binder", Units = 4, UnitCost = 4.99, Total = 19.96}, + new {Id=26, OrderDate = DateTime.Parse("2016-03-07"), Region = "West", Rep = "Sorvino", Item = "Binder", Units = 7, UnitCost = 19.99, Total = 139.93}, + new {Id=27, OrderDate = DateTime.Parse("2016-03-24"), Region = "Central", Rep = "Jardine", Item = "Pen Set", Units = 50, UnitCost = 4.99, Total = 249.5}, + new {Id=28, OrderDate = DateTime.Parse("2016-04-10"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 66, UnitCost = 1.99, Total = 131.34}, + new {Id=29, OrderDate = DateTime.Parse("2016-04-27"), Region = "East", Rep = "Howard", Item = "Pen", Units = 96, UnitCost = 4.99, Total = 479.04}, + new {Id=30, OrderDate = DateTime.Parse("2016-05-14"), Region = "Central", Rep = "Gill", Item = "Pencil", Units = 53, UnitCost = 1.29, Total = 68.37}, + new {Id=31, OrderDate = DateTime.Parse("2016-05-31"), Region = "Central", Rep = "Gill", Item = "Binder", Units = 80, UnitCost = 8.99, Total = 719.2}, + new {Id=32, OrderDate = DateTime.Parse("2016-06-17"), Region = "Central", Rep = "Kivell", Item = "Desk", Units = 5, UnitCost = 125, Total = 625}, + new {Id=33, OrderDate = DateTime.Parse("2016-07-04"), Region = "East", Rep = "Jones", Item = "Pen Set", Units = 62, UnitCost = 4.99, Total = 309.38}, + new {Id=34, OrderDate = DateTime.Parse("2016-07-21"), Region = "Central", Rep = "Morgan", Item = "Pen Set", Units = 55, UnitCost = 12.49, Total = 686.95}, + new {Id=35, OrderDate = DateTime.Parse("2016-08-07"), Region = "Central", Rep = "Kivell", Item = "Pen Set", Units = 42, UnitCost = 23.95, Total = 1005.9}, + new {Id=36, OrderDate = DateTime.Parse("2016-08-24"), Region = "West", Rep = "Sorvino", Item = "Desk", Units = 3, UnitCost = 275, Total = 825}, + new {Id=37, OrderDate = DateTime.Parse("2016-09-10"), Region = "Central", Rep = "Gill", Item = "Pencil", Units = 7, UnitCost = 1.29, Total = 9.03}, + new {Id=38, OrderDate = DateTime.Parse("2016-09-27"), Region = "West", Rep = "Sorvino", Item = "Pen", Units = 76, UnitCost = 1.99, Total = 151.24}, + new {Id=39, OrderDate = DateTime.Parse("2016-10-14"), Region = "West", Rep = "Thompson", Item = "Binder", Units = 57, UnitCost = 19.99, Total = 1139.43}, + new {Id=40, OrderDate = DateTime.Parse("2016-10-31"), Region = "Central", Rep = "Andrews", Item = "Pencil", Units = 14, UnitCost = 1.29, Total = 18.06}, + new {Id=41, OrderDate = DateTime.Parse("2016-11-17"), Region = "Central", Rep = "Jardine", Item = "Binder", Units = 11, UnitCost = 4.99, Total = 54.89}, + new {Id=42, OrderDate = DateTime.Parse("2016-12-04"), Region = "Central", Rep = "Jardine", Item = "Binder", Units = 94, UnitCost = 19.99, Total = 1879.06}, + new {Id=43, OrderDate = DateTime.Parse("2016-12-21"), Region = "Central", Rep = "Andrews", Item = "Binder", Units = 28, UnitCost = 4.99, Total = 139.72} + }; + + ws1.FirstCell() + .CellBelow() + .CellRight() + .InsertTable(data, "Table1"); + + var ws2 = wb.AddWorksheet("MixedData"); + ws2.FirstCell().InsertData(new object[] { 1, 2.0, "3", 3, new DateTime(2020, 1, 1), true, new TimeSpan(10, 5, 30, 10) }); + + return wb; + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs b/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs new file mode 100644 index 0000000..74046b0 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs @@ -0,0 +1,696 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.CalcEngine; +using ClosedXML.Excel.CalcEngine.Exceptions; +using NUnit.Framework; +using System; +using System.Globalization; +using System.Threading; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + [TestFixture] + public class TextTests + { + [SetUp] + public void Init() + { + // Make sure tests run on a deterministic culture + Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); + } + + [Test] + public void Char_Empty_Input_String() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"Char("""")")); + } + + [Test] + public void Char_Input_Too_Large() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr(@"Char(9797)")); + } + + [Test] + public void Char_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Char(97)"); + Assert.AreEqual("a", actual); + } + + [Test] + public void Clean_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Clean("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Clean_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Clean(CHAR(9)&""Monthly report""&CHAR(10))"); + Assert.AreEqual("Monthly report", actual); + + actual = XLWorkbook.EvaluateExpr(@"Clean("" "")"); + Assert.AreEqual(" ", actual); + } + + [Test] + public void Code_Empty_Input_String() + { + // Todo: more specific exception - ValueException? + Assert.That(() => XLWorkbook.EvaluateExpr(@"Code("""")"), Throws.TypeOf()); + } + + [Test] + public void Code_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Code(""A"")"); + Assert.AreEqual(65, actual); + + actual = XLWorkbook.EvaluateExpr(@"Code(""BCD"")"); + Assert.AreEqual(66, actual); + } + + [Test] + public void Concat_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Concat(""ABC"", ""123"")"); + Assert.AreEqual("ABC123", actual); + + actual = XLWorkbook.EvaluateExpr(@"Concat("""", ""123"")"); + Assert.AreEqual("123", actual); + + var ws = new XLWorkbook().AddWorksheet(); + + ws.FirstCell().SetValue(20) + .CellBelow().SetValue("AB") + .CellBelow().SetFormulaA1("=DATE(2019,1,1)") + .CellBelow().SetFormulaA1("=CONCAT(A1:A3)"); + + actual = ws.Cell("A4").Value; + Assert.AreEqual("20AB43466", actual); + } + + [Test] + public void Concatenate_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Concatenate(""ABC"", ""123"")"); + Assert.AreEqual("ABC123", actual); + + actual = XLWorkbook.EvaluateExpr(@"Concatenate("""", ""123"")"); + Assert.AreEqual("123", actual); + + // TODO: Fix CONCATENATE when calling cell references are available + // In Excel, it seems that if the parameter is a range, + // CONCATENATE will return the cell in the range that is the same row number as the calling cell, + // i.e. the cell with the CONCATENATE function. + // Therefore we need reference info about the calling cell to solve this. + // If we can solve ROW(), then we can solve this too. + // For the example below, the calling cell doesn't share any + + var ws = new XLWorkbook().AddWorksheet(); + + ws.FirstCell().SetValue(20) + .CellBelow().SetValue("AB") + .CellBelow().SetFormulaA1("=DATE(2019,1,1)"); + + // Calling cell is 1st row, so formula should return A1 + //ws.Cell("B1").SetFormulaA1("=CONCATENATE(A1:A3)"); + //Assert.AreEqual("20", ws.Cell("B1").Value); + + // Calling cell is 2nd row, so formula should return A2 + //ws.Cell("B2").SetFormulaA1("=CONCATENATE(A1:A3)"); + //Assert.AreEqual("AB", ws.Cell("B2").Value); + + // Calling cell is 3rd row, so formula should return A3's textual representation + //ws.Cell("B3").SetFormulaA1("=CONCATENATE(A1:A3)"); + //Assert.AreEqual("43466", ws.Cell("B3").Value); + + // Calling cell doesn't share row with any cell in parameter range. Throw CellValueException + //ws.Cell("A4").SetFormulaA1("=CONCATENATE(A1:A3)"); + //Assert.Throws(() => ws.Cell("A4").GetString()); + } + + [Test] + public void Concatenate_with_references() + { + var ws = new XLWorkbook().AddWorksheet(); + + ws.Cell("A1").Value = "Hello"; + ws.Cell("B1").Value = "World"; + + Assert.AreEqual("Hello World", ws.Evaluate(@"=CONCATENATE(A1, "" "", B1)")); + Assert.Throws(() => ws.Evaluate(@"=CONCATENATE(A1:A2, "" "", B1:B2)")); + } + + [Test] + public void Dollar_Empty_Input_String() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Dollar("", 3)"), Throws.TypeOf()); + } + + [Test] + public void Dollar_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Dollar(123.54)"); + Assert.AreEqual("$123.54", actual); + + actual = XLWorkbook.EvaluateExpr(@"Dollar(123.54, 3)"); + Assert.AreEqual("$123.540", actual); + } + + [Test] + public void Exact_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Exact("""", """")"); + Assert.AreEqual(true, actual); + } + + [Test] + public void Exact_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Exact(""asdf"", ""asdf"")"); + Assert.AreEqual(true, actual); + + actual = XLWorkbook.EvaluateExpr(@"Exact(""asdf"", ""ASDF"")"); + Assert.AreEqual(false, actual); + + actual = XLWorkbook.EvaluateExpr(@"Exact(123, 123)"); + Assert.AreEqual(true, actual); + + actual = XLWorkbook.EvaluateExpr(@"Exact(321, 123)"); + Assert.AreEqual(false, actual); + } + + [Test] + public void Find_Start_Position_Too_Large() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Find(""abc"", ""abcdef"", 10)"), Throws.TypeOf()); + } + + [Test] + public void Find_String_In_Another_Empty_String() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Find(""abc"", """")"), Throws.TypeOf()); + } + + [Test] + public void Find_String_Not_Found() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Find(""123"", ""asdf"")"), Throws.TypeOf()); + } + + [Test] + public void Find_Case_Sensitive_String_Not_Found() + { + // Find is case-sensitive + Assert.That(() => XLWorkbook.EvaluateExpr(@"Find(""excel"", ""Microsoft Excel 2010"")"), Throws.TypeOf()); + } + + [Test] + public void Find_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Find(""Tuesday"", ""Today is Tuesday"")"); + Assert.AreEqual(10, actual); + + actual = XLWorkbook.EvaluateExpr(@"Find("""", """")"); + Assert.AreEqual(1, actual); + + actual = XLWorkbook.EvaluateExpr(@"Find("""", ""asdf"")"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Fixed_Input_Is_String() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Fixed(""asdf"")"), Throws.TypeOf()); + } + + [Test] + public void Fixed_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Fixed(17300.67, 4)"); + Assert.AreEqual("17,300.6700", actual); + + actual = XLWorkbook.EvaluateExpr(@"Fixed(17300.67, 2, TRUE)"); + Assert.AreEqual("17300.67", actual); + + actual = XLWorkbook.EvaluateExpr(@"Fixed(17300.67)"); + Assert.AreEqual("17,300.67", actual); + } + + [Test] + public void Left_Bigger_Than_Length() + { + Object actual = XLWorkbook.EvaluateExpr(@"Left(""ABC"", 5)"); + Assert.AreEqual("ABC", actual); + } + + [Test] + public void Left_Default() + { + Object actual = XLWorkbook.EvaluateExpr(@"Left(""ABC"")"); + Assert.AreEqual("A", actual); + } + + [Test] + public void Left_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Left("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Left_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Left(""ABC"", 2)"); + Assert.AreEqual("AB", actual); + } + + [Test] + public void Len_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Len("""")"); + Assert.AreEqual(0, actual); + } + + [Test] + public void Len_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Len(""word"")"); + Assert.AreEqual(4, actual); + } + + [Test] + public void Lower_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Lower("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Lower_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Lower(""AbCdEfG"")"); + Assert.AreEqual("abcdefg", actual); + } + + [Test] + public void Mid_Bigger_Than_Length() + { + Object actual = XLWorkbook.EvaluateExpr(@"Mid(""ABC"", 1, 5)"); + Assert.AreEqual("ABC", actual); + } + + [Test] + public void Mid_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Mid("""", 1, 1)"); + Assert.AreEqual("", actual); + } + + [Test] + public void Mid_Start_After() + { + Object actual = XLWorkbook.EvaluateExpr(@"Mid(""ABC"", 5, 5)"); + Assert.AreEqual("", actual); + } + + [Test] + public void Mid_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Mid(""ABC"", 2, 2)"); + Assert.AreEqual("BC", actual); + } + + [TestCase("NUMBERVALUE(\"\")", 0d)] + [TestCase("NUMBERVALUE(\"1,234.56\", \".\", \",\")", 1234.56d)] + [TestCase("NUMBERVALUE(\"1.234,56\", \",\", \".\")", 1234.56d)] + [TestCase("NUMBERVALUE(\"+ 1\")", 1d)] + [TestCase("NUMBERVALUE(\"+1\")", 1d)] + [TestCase("NUMBERVALUE(\"+1.23\")", 1.23)] + [TestCase("NUMBERVALUE(\"- 1.23\")", -1.23)] + [TestCase("NUMBERVALUE(\" - 0 1 2 . 3 4 \")", -12.34)] + [TestCase("NUMBERVALUE(\" - 0 \t1\t2\r .\n3 4 \")", -12.34)] + [TestCase("NUMBERVALUE(\".1\")", 0.1)] + [TestCase("NUMBERVALUE(\"-.1\")", -0.1)] + [TestCase("NUMBERVALUE(\"1.234567890E+307\")", 1.234567890E+307)] + [TestCase("NUMBERVALUE(\"1.234567890E-307\")", 1.234567890E-307d)] + [TestCase("NUMBERVALUE(\"1.234567890E-309\")", 0d)] + [TestCase("NUMBERVALUE(\"-1.234567890E-307\")", -1.234567890E-307d)] + [TestCase("NUMBERVALUE(\".99999999999999\")", 0.99999999999999)] + [TestCase("NUMBERVALUE(\"1,23,4\")", 1234)] + [TestCase("NUMBERVALUE(\"1,234,56\")", 123456)] + public void NumberValue_Correct(string expression, double expectedResult) + { + var actual = (double)XLWorkbook.EvaluateExpr(expression); + Assert.AreEqual(expectedResult, actual, XLHelper.Epsilon); + } + + [TestCase("NUMBERVALUE(\"123.45\", \".\", \".\")")] // Group separator same as decimal separator + [TestCase("NUMBERVALUE(\"1.234.5\")")] // Two decimal separators + [TestCase("NUMBERVALUE(\"1.234,5\")")] // Decimal separator before group separator + [TestCase("NUMBERVALUE(\"12;34\")")] // Illegal character + [TestCase("NUMBERVALUE(\"--1\")")] // Two minuses + [TestCase("NUMBERVALUE(\"1.234567890E+308\")")] // Too large + [TestCase("NUMBERVALUE(\"-1.234567890E+308\")")] // Too large (negative) + [TestCase("NUMBERVALUE(\"1.234567890E-310\")")] // Too tiny + [TestCase("NUMBERVALUE(\"-1.234567890E-310\")")] // Too tiny (negative) + public void NumberValue_Invalid(string expression) + { + TestDelegate action = () => XLWorkbook.EvaluateExpr(expression); + Assert.Throws(action); + } + + [Test] + public void Proper_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Proper("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Proper_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Proper(""my name is francois botha"")"); + Assert.AreEqual("My Name Is Francois Botha", actual); + } + + [Test] + public void Replace_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Replace("""", 1, 1, ""newtext"")"); + Assert.AreEqual("newtext", actual); + } + + [Test] + public void Replace_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Replace(""Here is some obsolete text to replace."", 14, 13, ""new text"")"); + Assert.AreEqual("Here is some new text to replace.", actual); + } + + [Test] + public void Rept_Empty_Input_Strings() + { + Object actual = XLWorkbook.EvaluateExpr(@"Rept("""", 3)"); + Assert.AreEqual("", actual); + } + + [Test] + public void Rept_Start_Is_Negative() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Rept(""Francois"", -1)"), Throws.TypeOf()); + } + + [Test] + public void Rept_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Rept(""Francois Botha,"", 3)"); + Assert.AreEqual("Francois Botha,Francois Botha,Francois Botha,", actual); + + actual = XLWorkbook.EvaluateExpr(@"Rept(""123"", 5/2)"); + Assert.AreEqual("123123", actual); + + actual = XLWorkbook.EvaluateExpr(@"Rept(""Francois"", 0)"); + Assert.AreEqual("", actual); + } + + [Test] + public void Right_Bigger_Than_Length() + { + Object actual = XLWorkbook.EvaluateExpr(@"Right(""ABC"", 5)"); + Assert.AreEqual("ABC", actual); + } + + [Test] + public void Right_Default() + { + Object actual = XLWorkbook.EvaluateExpr(@"Right(""ABC"")"); + Assert.AreEqual("C", actual); + } + + [Test] + public void Right_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Right("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Right_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Right(""ABC"", 2)"); + Assert.AreEqual("BC", actual); + } + + [Test] + public void Search_No_Parameters_With_Values() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search("""", """")"), Throws.TypeOf()); + } + + [Test] + public void Search_Empty_Search_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Search("""", ""asdf"")"); + Assert.AreEqual(1, actual); + } + + [Test] + public void Search_Start_Position_Too_Large() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search(""abc"", ""abcdef"", 10)"), Throws.TypeOf()); + } + + [Test] + public void Search_Empty_Input_String() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search(""abc"", """")"), Throws.TypeOf()); + } + + [Test] + public void Search_String_Not_Found() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search(""123"", ""asdf"")"), Throws.TypeOf()); + } + + [Test] + public void Search_Wildcard_String_Not_Found() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search(""soft?2010"", ""Microsoft Excel 2010"")"), Throws.TypeOf()); + } + + [Test] + public void Search_Start_Position_Too_Large2() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Search(""text"", ""This is some text"", 15)"), Throws.TypeOf()); + } + + // http://www.excel-easy.com/examples/find-vs-search.html + [Test] + public void Search_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Search(""Tuesday"", ""Today is Tuesday"")"); + Assert.AreEqual(10, actual); + + // Find is case-INsensitive + actual = XLWorkbook.EvaluateExpr(@"Search(""excel"", ""Microsoft Excel 2010"")"); + Assert.AreEqual(11, actual); + + actual = XLWorkbook.EvaluateExpr(@"Search(""soft*2010"", ""Microsoft Excel 2010"")"); + Assert.AreEqual(6, actual); + + actual = XLWorkbook.EvaluateExpr(@"Search(""Excel 20??"", ""Microsoft Excel 2010"")"); + Assert.AreEqual(11, actual); + + actual = XLWorkbook.EvaluateExpr(@"Search(""text"", ""This is some text"", 14)"); + Assert.AreEqual(14, actual); + } + + [Test] + public void Substitute_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Substitute(""This is a Tuesday."", ""Tuesday"", ""Monday"")"); + Assert.AreEqual("This is a Monday.", actual); + + actual = XLWorkbook.EvaluateExpr(@"Substitute(""This is a Tuesday. Next week also has a Tuesday."", ""Tuesday"", ""Monday"", 1)"); + Assert.AreEqual("This is a Monday. Next week also has a Tuesday.", actual); + + actual = XLWorkbook.EvaluateExpr(@"Substitute(""This is a Tuesday. Next week also has a Tuesday."", ""Tuesday"", ""Monday"", 2)"); + Assert.AreEqual("This is a Tuesday. Next week also has a Monday.", actual); + + actual = XLWorkbook.EvaluateExpr(@"Substitute("""", """", ""Monday"")"); + Assert.AreEqual("", actual); + + actual = XLWorkbook.EvaluateExpr(@"Substitute(""This is a Tuesday. Next week also has a Tuesday."", """", ""Monday"")"); + Assert.AreEqual("This is a Tuesday. Next week also has a Tuesday.", actual); + + actual = XLWorkbook.EvaluateExpr(@"Substitute(""This is a Tuesday. Next week also has a Tuesday."", ""Tuesday"", """")"); + Assert.AreEqual("This is a . Next week also has a .", actual); + } + + [Test] + public void T_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"T("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void T_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"T(""asdf"")"); + Assert.AreEqual("asdf", actual); + + actual = XLWorkbook.EvaluateExpr(@"T(Today())"); + Assert.AreEqual("", actual); + + actual = XLWorkbook.EvaluateExpr(@"T(TRUE)"); + Assert.AreEqual("", actual); + } + + [Test] + public void Text_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Text(1913415.93, """")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Text_Value() + { + Object actual; + actual = XLWorkbook.EvaluateExpr(@"Text(Date(2010, 1, 1), ""yyyy-MM-dd"")"); + Assert.AreEqual("2010-01-01", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(1469.07, ""0,000,000.00"")"); + Assert.AreEqual("0,001,469.07", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(1913415.93, ""#,000.00"")"); + Assert.AreEqual("1,913,415.93", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(2800, ""$0.00"")"); + Assert.AreEqual("$2800.00", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(0.4, ""0%"")"); + Assert.AreEqual("40%", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(Date(2010, 1, 1), ""MMMM yyyy"")"); + Assert.AreEqual("January 2010", actual); + + actual = XLWorkbook.EvaluateExpr(@"Text(Date(2010, 1, 1), ""M/d/y"")"); + Assert.AreEqual("1/1/10", actual); + } + + [Test] + public void Text_String_Input() + { + Object actual = XLWorkbook.EvaluateExpr(@"TEXT(""211x"", ""#00"")"); + Assert.AreEqual("211x", actual); + } + + [TestCase("=TEXTJOIN(\",\",TRUE,A1:B2)", "A,B,D")] + [TestCase("=TEXTJOIN(\",\",FALSE,A1:B2)", "A,,B,D")] + [TestCase("=TEXTJOIN(\",\",FALSE,A1,A2,B1,B2)", "A,B,,D")] + [TestCase("=TEXTJOIN(\",\",FALSE,1)", "1")] + [TestCase("=TEXTJOIN(\",\", TRUE, A:A, B:B)", "A,B,D")] + [TestCase("=TEXTJOIN(\",\", TRUE, D1:E2)", "")] + [TestCase("=TEXTJOIN(\",\", FALSE, D1:E2)", ",,,")] + [TestCase("=TEXTJOIN(\",\", FALSE, D1:D32768)", ",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,")] + [TestCase("=TEXTJOIN(0, FALSE, A1:B2)", "A00B0D")] + [TestCase("=TEXTJOIN(false, FALSE, A1:B2)", "AFALSEFALSEBFALSED")] + [TestCase("=TEXTJOIN(\",\", 0, A1:B2)", "A,,B,D")] + [TestCase("=TEXTJOIN(\",\", 100, A1:B2)", "A,B,D")] + [TestCase("=TEXTJOIN(B2, FALSE, A1:B2)", "ADDBDD")] + [TestCase("=TEXTJOIN(\",\", FALSE, 12345.67, DATE(2018, 10, 30))", "12345.67,43403")] + [TestCase("=TEXTJOIN(\",\", \"0\", A1:B2)", "A,,B,D")] // Excel does not accept text argument, LibreOffice does + public void TextJoin(string formula, string expectedOutput) + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "A"; + ws.Cell("A2").Value = "B"; + ws.Cell("B1").Value = ""; + ws.Cell("B2").Value = "D"; + + ws.Cell("C1").FormulaA1 = formula; + var a = ws.Cell("C1").Value; + + Assert.AreEqual(expectedOutput, a.ToString()); + } + + [TestCase("=TEXTJOIN(\",\", FALSE, D1:D32769)", "The value is too long")] + [TestCase("=TEXTJOIN(\",\", \"Invalid\", A1:B2)", "The second argument is invalid")] + public void TextJoinWithInvalidArgumentsThrows(string formula, string explain) + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + + ws.Cell("C1").FormulaA1 = formula; + + TestDelegate action = () => + { + var a = ws.Cell("C1").Value; + }; + + Assert.Throws(action, explain); + } + + [TestCase(2020, 11, 1, 9, 23, 11, "m/d/yyyy h:mm:ss", "11/1/2020 9:23:11")] + [TestCase(2023, 7, 14, 2, 12, 3, "m/d/yyyy h:mm:ss", "7/14/2023 2:12:03")] + [TestCase(2025, 10, 14, 2, 48, 55, "m/d/yyyy h:mm:ss", "10/14/2025 2:48:55")] + [TestCase(2023, 2, 19, 22, 1, 38, "m/d/yyyy h:mm:ss", "2/19/2023 22:01:38")] + [TestCase(2025, 12, 19, 19, 43, 58, "m/d/yyyy h:mm:ss", "12/19/2025 19:43:58")] + [TestCase(2034, 11, 16, 1, 48, 9, "m/d/yyyy h:mm:ss", "11/16/2034 1:48:09")] + [TestCase(2018, 12, 10, 11, 22, 42, "m/d/yyyy h:mm:ss", "12/10/2018 11:22:42")] + public void Text_DateFormats(int year, int months, int days, int hour, int minutes, int seconds, string format, string expected) + { + Assert.AreEqual(expected, XLWorkbook.EvaluateExpr($@"TEXT(DATE({year}, {months}, {days}) + TIME({hour}, {minutes}, {seconds}), ""{format}"")")); + } + + [Test] + public void Trim_EmptyInput_Striong() + { + Object actual = XLWorkbook.EvaluateExpr(@"Trim("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Trim_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Trim("" some text with padding "")"); + Assert.AreEqual("some text with padding", actual); + } + + [Test] + public void Upper_Empty_Input_String() + { + Object actual = XLWorkbook.EvaluateExpr(@"Upper("""")"); + Assert.AreEqual("", actual); + } + + [Test] + public void Upper_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Upper(""AbCdEfG"")"); + Assert.AreEqual("ABCDEFG", actual); + } + + [Test] + public void Value_Input_String_Is_Not_A_Number() + { + Assert.That(() => XLWorkbook.EvaluateExpr(@"Value(""asdf"")"), Throws.TypeOf()); + } + + [Test] + public void Value_Value() + { + Object actual = XLWorkbook.EvaluateExpr(@"Value(""123.54"")"); + Assert.AreEqual(123.54, actual); + + actual = XLWorkbook.EvaluateExpr(@"Value(654.32)"); + Assert.AreEqual(654.32, actual); + } + } +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/XLMathTests.cs b/ClosedXML_Tests/Excel/CalcEngine/XLMathTests.cs new file mode 100644 index 0000000..5ce2ce5 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/XLMathTests.cs @@ -0,0 +1,26 @@ +using ClosedXML.Excel.CalcEngine.Functions; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class XLMathTests + { + [Test] + public void IsEven() + { + Assert.IsTrue(XLMath.IsEven(2)); + Assert.IsFalse(XLMath.IsEven(3)); + } + + [Test] + public void IsOdd() + { + Assert.IsTrue(XLMath.IsOdd(3)); + Assert.IsFalse(XLMath.IsOdd(2)); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Excel/Cells/DataTypeTests.cs b/ClosedXML_Tests/Excel/Cells/DataTypeTests.cs new file mode 100644 index 0000000..2808008 --- /dev/null +++ b/ClosedXML_Tests/Excel/Cells/DataTypeTests.cs @@ -0,0 +1,22 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; + +namespace ClosedXML_Tests.Excel.Cells +{ + [TestFixture] + public class DataTypeTests + { + [Test] + public void ConvertNonNumericTextToNumberThrowsException() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + var c = ws.Cell("A1"); + c.Value = "ABC123"; + Assert.Throws(() => c.DataType = XLDataType.Number); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Cells/XLCellTests.cs b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs new file mode 100644 index 0000000..44c3b39 --- /dev/null +++ b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs @@ -0,0 +1,1180 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLCellTests + { + [Test] + public void CellsUsed() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Cell(1, 1); + ws.Cell(2, 2); + int count = ws.Range("A1:B2").CellsUsed().Count(); + Assert.AreEqual(0, count); + } + + [Test] + public void CellsUsedIncludeStyles1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Row(3).Style.Fill.BackgroundColor = XLColor.Red; + ws.Column(3).Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell(2, 2).Value = "ASDF"; + var range = ws.RangeUsed(XLCellsUsedOptions.All).RangeAddress.ToString(); + Assert.AreEqual("B2:C3", range); + } + + [Test] + public void CellsUsedIncludeStyles2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Row(2).Style.Fill.BackgroundColor = XLColor.Red; + ws.Column(2).Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell(3, 3).Value = "ASDF"; + var range = ws.RangeUsed(XLCellsUsedOptions.All).RangeAddress.ToString(); + Assert.AreEqual("B2:C3", range); + } + + [Test] + public void CellsUsedIncludeStyles3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var range = ws.RangeUsed(XLCellsUsedOptions.All); + Assert.AreEqual(null, range); + } + + [Test] + public void CellUsedIncludesSparklines() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:E4").Value = 1; + ws.SparklineGroups.Add("B2", "C3:E3"); + ws.SparklineGroups.Add("F5", "C4:E4"); + + var range = ws.RangeUsed(true).RangeAddress.ToString(); + Assert.AreEqual("B2:F5", range); + } + + [Test] + public void Double_Infinity_is_a_string() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1"); + var doubleList = new List { 1.0 / 0.0 }; + + cell.Value = 5; + cell.Value = doubleList; + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.PositiveInfinitySymbol, cell.Value); + + cell.Value = 5; + cell.SetValue(doubleList); + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.PositiveInfinitySymbol, cell.Value); + } + + [Test] + public void Double_NaN_is_a_string() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1"); + var doubleList = new List { 0.0 / 0.0 }; + + cell.Value = 5; + cell.Value = doubleList; + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.NaNSymbol, cell.Value); + + cell.Value = 5; + cell.SetValue(doubleList); + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.NaNSymbol, cell.Value); + } + + [Test] + public void GetValue_Nullable() + { + var cell = new XLWorkbook().AddWorksheet().FirstCell(); + + Assert.IsNull(cell.Clear().GetValue()); + Assert.AreEqual(1.5, cell.SetValue(1.5).GetValue()); + Assert.AreEqual(2, cell.SetValue(1.5).GetValue()); + Assert.AreEqual(2.5, cell.SetValue("2.5").GetValue()); + Assert.Throws(() => cell.SetValue("text").GetValue()); + } + + [Test] + public void InsertData1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRange range = ws.Cell(2, 2).InsertData(new[] { "a", "b", "c" }); + Assert.AreEqual("Sheet1!B2:B4", range.ToString()); + } + + [Test] + public void InsertData2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRange range = ws.Cell(2, 2).InsertData(new[] { "a", "b", "c" }, false); + Assert.AreEqual("Sheet1!B2:B4", range.ToString()); + } + + [Test] + public void InsertData3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRange range = ws.Cell(2, 2).InsertData(new[] { "a", "b", "c" }, true); + Assert.AreEqual("Sheet1!B2:D2", range.ToString()); + } + + [Test] + public void InsertData_with_Guids() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.FirstCell().InsertData(Enumerable.Range(1, 20).Select(i => new { Guid = Guid.NewGuid() })); + + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(Guid.NewGuid().ToString().Length, ws.FirstCell().GetString().Length); + } + + [Test] + public void InsertData_with_Nulls() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + + var table = new DataTable(); + table.TableName = "Patients"; + table.Columns.Add("Dosage", typeof(int)); + table.Columns.Add("Drug", typeof(string)); + table.Columns.Add("Patient", typeof(string)); + table.Columns.Add("Date", typeof(DateTime)); + + table.Rows.Add(25, "Indocin", "David", new DateTime(2000, 1, 1)); + table.Rows.Add(50, "Enebrel", "Sam", new DateTime(2000, 1, 2)); + table.Rows.Add(10, "Hydralazine", "Christoff", new DateTime(2000, 1, 3)); + table.Rows.Add(21, "Combivent", DBNull.Value, new DateTime(2000, 1, 4)); + table.Rows.Add(100, "Dilantin", "Melanie", DBNull.Value); + + ws.FirstCell().InsertData(table); + + Assert.AreEqual(25, ws.Cell("A1").Value); + Assert.AreEqual("", ws.Cell("C4").Value); + Assert.AreEqual("", ws.Cell("D5").Value); + } + + [Test] + public void InsertData_with_Nulls_IEnumerable() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + + var dateTimeList = new List() + { + new DateTime(2000, 1, 1), + new DateTime(2000, 1, 2), + new DateTime(2000, 1, 3), + new DateTime(2000, 1, 4), + null + }; + + ws.FirstCell().InsertData(dateTimeList); + + Assert.AreEqual(new DateTime(2000, 1, 1), ws.Cell("A1").GetDateTime()); + Assert.AreEqual(String.Empty, ws.Cell("A5").Value); + } + + [Test] + public void IsEmpty1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + bool actual = cell.IsEmpty(); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + bool actual = cell.IsEmpty(XLCellsUsedOptions.All); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + bool actual = cell.IsEmpty(); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty4() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + bool actual = cell.IsEmpty(XLCellsUsedOptions.AllContents); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty5() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + bool actual = cell.IsEmpty(XLCellsUsedOptions.All); + bool expected = false; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty6() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Value = "X"; + bool actual = cell.IsEmpty(); + bool expected = false; + Assert.AreEqual(expected, actual); + } + + [Test] + public void NaN_is_not_a_number() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1"); + cell.Value = "NaN"; + + Assert.AreNotEqual(XLDataType.Number, cell.DataType); + } + + [Test] + public void Nan_is_not_a_number() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1"); + cell.Value = "Nan"; + + Assert.AreNotEqual(XLDataType.Number, cell.DataType); + } + + [Test] + public void TryGetValue_Boolean_Bad() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue("ABC"); + bool success = cell.TryGetValue(out bool outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_Boolean_False() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue(false); + bool success = cell.TryGetValue(out bool outValue); + Assert.IsTrue(success); + Assert.IsFalse(outValue); + } + + [Test] + public void TryGetValue_Boolean_Good() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue("True"); + bool success = cell.TryGetValue(out bool outValue); + Assert.IsTrue(success); + Assert.IsTrue(outValue); + } + + [Test] + public void TryGetValue_Boolean_True() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue(true); + bool success = cell.TryGetValue(out bool outValue); + Assert.IsTrue(success); + Assert.IsTrue(outValue); + } + + [Test] + public void TryGetValue_DateTime_Good() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var date = "2018-01-01"; + bool success = ws.Cell("A1").SetValue(date).TryGetValue(out DateTime outValue); + Assert.IsTrue(success); + Assert.AreEqual(new DateTime(2018, 1, 1), outValue); + } + + [Test] + public void TryGetValue_DateTime_Good2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + bool success = ws.Cell("A1").SetFormulaA1("=TODAY() + 10").TryGetValue(out DateTime outValue); + Assert.IsTrue(success); + Assert.AreEqual(DateTime.Today.AddDays(10), outValue); + } + + [Test] + public void TryGetValue_DateTime_BadButFormulaGood() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + bool success = ws.Cell("A1").SetFormulaA1("=\"44\"&\"020\"").TryGetValue(out DateTime outValue); + Assert.IsFalse(success); + + ws.Cell("B1").SetFormulaA1("=A1+1"); + + success = ws.Cell("B1").TryGetValue(out outValue); + Assert.IsTrue(success); + Assert.AreEqual(new DateTime(2020, 07, 09), outValue); + } + + [Test] + public void TryGetValue_DateTime_BadString() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var date = "ABC"; + bool success = ws.Cell("A1").SetValue(date).TryGetValue(out DateTime outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_DateTime_BadString2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var date = 5545454; + ws.FirstCell().SetValue(date).DataType = XLDataType.DateTime; + bool success = ws.FirstCell().TryGetValue(out DateTime outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_Enum_Good() + { + var ws = new XLWorkbook().AddWorksheet(); + Assert.IsTrue(ws.FirstCell().SetValue(NumberStyles.AllowCurrencySymbol).TryGetValue(out NumberStyles value)); + Assert.AreEqual(NumberStyles.AllowCurrencySymbol, value); + + // Nullable alternative + Assert.IsTrue(ws.FirstCell().SetValue(NumberStyles.AllowCurrencySymbol).TryGetValue(out NumberStyles? value2)); + Assert.AreEqual(NumberStyles.AllowCurrencySymbol, value2); + } + + [Test] + public void TryGetValue_Enum_BadString() + { + var ws = new XLWorkbook().AddWorksheet(); + Assert.IsFalse(ws.FirstCell().SetValue("ABC").TryGetValue(out NumberStyles value)); + Assert.IsFalse(ws.FirstCell().SetValue("ABC").TryGetValue(out NumberStyles? value2)); + } + + [Test] + public void TryGetValue_RichText_Bad() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue("Anything"); + bool success = cell.TryGetValue(out IXLRichText outValue); + Assert.IsTrue(success); + Assert.AreEqual(cell.RichText, outValue); + Assert.AreEqual("Anything", outValue.ToString()); + } + + [Test] + public void TryGetValue_RichText_Good() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1"); + cell.RichText.AddText("Anything"); + bool success = cell.TryGetValue(out IXLRichText outValue); + Assert.IsTrue(success); + Assert.AreEqual(cell.RichText, outValue); + } + + [Test] + public void TryGetValue_TimeSpan_BadString() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + string timeSpan = "ABC"; + bool success = ws.Cell("A1").SetValue(timeSpan).TryGetValue(out TimeSpan outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_TimeSpan_Good() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var timeSpan = new TimeSpan(1, 1, 1); + bool success = ws.Cell("A1").SetValue(timeSpan).TryGetValue(out TimeSpan outValue); + Assert.IsTrue(success); + Assert.AreEqual(timeSpan, outValue); + } + + [Test] + public void TryGetValue_TimeSpan_Good_Large() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var timeSpan = TimeSpan.FromMilliseconds((double)int.MaxValue + 1); + bool success = ws.Cell("A1").SetValue(timeSpan).TryGetValue(out TimeSpan outValue); + Assert.IsTrue(success); + Assert.AreEqual(timeSpan, outValue); + } + + [Test] + public void TryGetValue_TimeSpan_GoodString() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var timeSpan = new TimeSpan(1, 1, 1); + bool success = ws.Cell("A1").SetValue(timeSpan.ToString()).TryGetValue(out TimeSpan outValue); + Assert.IsTrue(success); + Assert.AreEqual(timeSpan, outValue); + } + + [Test] + public void TryGetValue_sbyte_Bad() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue(255); + bool success = cell.TryGetValue(out sbyte outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_sbyte_Bad2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue("255"); + bool success = cell.TryGetValue(out sbyte outValue); + Assert.IsFalse(success); + } + + [Test] + public void TryGetValue_sbyte_Good() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue(5); + bool success = cell.TryGetValue(out sbyte outValue); + Assert.IsTrue(success); + Assert.AreEqual(5, outValue); + } + + [Test] + public void TryGetValue_sbyte_Good2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell("A1").SetValue("5"); + bool success = cell.TryGetValue(out sbyte outValue); + Assert.IsTrue(success); + Assert.AreEqual(5, outValue); + } + + [Test] + public void TryGetValue_decimal_Good() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var cell = ws.Cell("A1").SetValue("5"); + bool success = cell.TryGetValue(out decimal outValue); + Assert.IsTrue(success); + Assert.AreEqual(5, outValue); + } + + [Test] + public void TryGetValue_decimal_Good2() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var cell = ws.Cell("A1").SetValue("1.60000001869776E-06"); + bool success = cell.TryGetValue(out decimal outValue); + Assert.IsTrue(success); + Assert.AreEqual(1.60000001869776E-06, outValue); + } + + [Test] + public void TryGetValue_Hyperlink() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet1"); + var ws2 = wb.Worksheets.Add("Sheet2"); + + var targetCell = ws2.Cell("A1"); + + var linkCell1 = ws1.Cell("A1"); + linkCell1.Value = "Link to IXLCell"; + linkCell1.Hyperlink = new XLHyperlink(targetCell); + + var success = linkCell1.TryGetValue(out XLHyperlink hyperlink); + Assert.IsTrue(success); + Assert.AreEqual("Sheet2!A1", hyperlink.InternalAddress); + } + } + + [Test] + public void TryGetValue_Unicode_String() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + + Boolean success; + String outValue; + + success = ws.Cell("A1") + .SetValue("Site_x0020_Column_x0020_Test") + .TryGetValue(out outValue); + Assert.IsTrue(success); + Assert.AreEqual("Site Column Test", outValue); + + success = ws.Cell("A1") + .SetValue("Site_x005F_x0020_Column_x005F_x0020_Test") + .TryGetValue(out outValue); + + Assert.IsTrue(success); + Assert.AreEqual("Site_x005F_x0020_Column_x005F_x0020_Test", outValue); + } + + [Test] + public void TryGetValue_Nullable() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + + ws.Cell("A1").Clear(); + ws.Cell("A2").SetValue(1.5); + ws.Cell("A3").SetValue("2.5"); + ws.Cell("A4").SetValue("text"); + + foreach (var cell in ws.Range("A1:A3").Cells()) + { + Assert.IsTrue(cell.TryGetValue(out double? value)); + } + + Assert.IsFalse(ws.Cell("A4").TryGetValue(out double? _)); + } + + [Test] + public void SetCellValueToGuid() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var guid = Guid.NewGuid(); + ws.FirstCell().Value = guid; + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(guid.ToString(), ws.FirstCell().Value); + Assert.AreEqual(guid.ToString(), ws.FirstCell().GetString()); + + guid = Guid.NewGuid(); + ws.FirstCell().SetValue(guid); + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(guid.ToString(), ws.FirstCell().Value); + Assert.AreEqual(guid.ToString(), ws.FirstCell().GetString()); + } + + [Test] + public void SetCellValueToEnum() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var dataType = XLDataType.Number; + ws.FirstCell().Value = dataType; + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(dataType.ToString(), ws.FirstCell().Value); + Assert.AreEqual(dataType.ToString(), ws.FirstCell().GetString()); + + dataType = XLDataType.TimeSpan; + ws.FirstCell().SetValue(dataType); + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(dataType.ToString(), ws.FirstCell().Value); + Assert.AreEqual(dataType.ToString(), ws.FirstCell().GetString()); + } + + [Test] + public void SetCellValueToRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + + ws.Cell("A1").SetValue(2) + .CellRight().SetValue(3) + .CellRight().SetValue(5) + .CellRight().SetValue(7); + + var range = ws.Range("1:1"); + + ws.Cell("B2").Value = range; + + Assert.AreEqual(2, ws.Cell("B2").Value); + Assert.AreEqual(3, ws.Cell("C2").Value); + Assert.AreEqual(5, ws.Cell("D2").Value); + Assert.AreEqual(7, ws.Cell("E2").Value); + } + + [Test] + public void ValueSetToEmptyString() + { + string expected = String.Empty; + + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Value = new DateTime(2000, 1, 2); + cell.Value = String.Empty; + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + + cell.Value = new DateTime(2000, 1, 2); + cell.SetValue(string.Empty); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + } + + [Test] + public void ValueSetToNull() + { + string expected = String.Empty; + + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Value = new DateTime(2000, 1, 2); + cell.Value = null; + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + + cell.Value = new DateTime(2000, 1, 2); + cell.SetValue(null as string); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + } + + [Test] + public void ValueSetDateWithShortUserDateFormat() + { + // For this test to make sense, user's local date format should be dd/MM/yy (note without the 2 century digits) + // What happened previously was that the century digits got lost in .ToString() conversion and wrong century was sometimes returned. + var ci = new CultureInfo(CultureInfo.InvariantCulture.LCID); + ci.DateTimeFormat.ShortDatePattern = "dd/MM/yy"; + Thread.CurrentThread.CurrentCulture = ci; + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + var expected = DateTime.Today.AddYears(20); + cell.Value = expected; + var actual = (DateTime)cell.Value; + Assert.AreEqual(expected, actual); + } + + [Test] + public void SetStringCellValues() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.FirstCell(); + + object expected; + + var date = new DateTime(2018, 4, 18); + expected = date.ToString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.DateTime, cell.DataType); + Assert.AreEqual(date, cell.Value); + + var b = true; + expected = b.ToString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.Boolean, cell.DataType); + Assert.AreEqual(b, cell.Value); + + var ts = new TimeSpan(8, 12, 4); + expected = ts.ToString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.TimeSpan, cell.DataType); + Assert.AreEqual(ts, cell.Value); + } + } + + [Test] + public void SetStringValueTooLong() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = new DateTime(2018, 5, 15); + + ws.FirstCell().SetValue(new String('A', 32767)); + + Assert.Throws(() => ws.FirstCell().Value = new String('A', 32768)); + Assert.Throws(() => ws.FirstCell().SetValue(new String('A', 32768))); + } + } + + [Test] + public void SetDateOutOfRange() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-ZA"); + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = 5; + + var date = XLCell.BaseDate.AddDays(-1); + ws.FirstCell().Value = date; + + // Should default to string representation using current culture's date format + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(date.ToString(), ws.FirstCell().Value); + + Assert.Throws(() => ws.FirstCell().SetValue(XLCell.BaseDate.AddDays(-1))); + } + } + + [Test] + public void SetCellValueWipesFormulas() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().FormulaA1 = "=TODAY()"; + ws.FirstCell().Value = "hello world"; + Assert.IsFalse(ws.FirstCell().HasFormula); + + ws.FirstCell().FormulaA1 = "=TODAY()"; + ws.FirstCell().SetValue("hello world"); + Assert.IsFalse(ws.FirstCell().HasFormula); + } + } + + [Test] + public void CellValueLineWrapping() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = "hello world"; + Assert.IsFalse(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().Value = "hello\r\nworld"; + Assert.IsTrue(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().Style.Alignment.WrapText = false; + + ws.FirstCell().SetValue("hello world"); + Assert.IsFalse(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().SetValue("hello\r\nworld"); + Assert.IsTrue(ws.FirstCell().Style.Alignment.WrapText); + } + } + + [Test] + public void TestInvalidXmlCharacters() + { + byte[] data; + + using (var stream = new MemoryStream()) + { + var wb = new XLWorkbook(); + wb.AddWorksheet("Sheet1").FirstCell().SetValue("\u0018"); + wb.SaveAs(stream); + data = stream.ToArray(); + } + + using (var stream = new MemoryStream(data)) + { + var wb = new XLWorkbook(stream); + Assert.AreEqual("\u0018", wb.Worksheets.First().FirstCell().Value); + } + } + + [Test] + public void CanClearCellValueBySettingNullValue() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.FirstCell(); + + cell.Value = "Test"; + Assert.AreEqual("Test", cell.Value); + Assert.AreEqual(XLDataType.Text, cell.DataType); + + string s = null; + cell.SetValue(s); + Assert.AreEqual(string.Empty, cell.Value); + + cell.Value = "Test"; + cell.Value = null; + Assert.AreEqual(string.Empty, cell.Value); + } + } + + [Test] + public void CanClearDateTimeCellValue() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var c = ws.FirstCell(); + c.SetValue(new DateTime(2017, 10, 08)); + Assert.AreEqual(XLDataType.DateTime, c.DataType); + Assert.AreEqual(new DateTime(2017, 10, 08), c.Value); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var c = ws.FirstCell(); + Assert.AreEqual(XLDataType.DateTime, c.DataType); + Assert.AreEqual(new DateTime(2017, 10, 08), c.Value); + + c.Clear(); + wb.Save(); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var c = ws.FirstCell(); + Assert.AreEqual(XLDataType.Text, c.DataType); + Assert.True(c.IsEmpty()); + } + } + } + + [Test] + public void ClearCellRemovesSparkline() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.SparklineGroups.Add("B1:B3", "C1:E3"); + + ws.Cell("B1").Clear(XLClearOptions.All); + ws.Cell("B2").Clear(XLClearOptions.Sparklines); + + Assert.AreEqual(1, ws.SparklineGroups.Single().Count()); + Assert.IsFalse(ws.Cell("B1").HasSparkline); + Assert.IsFalse(ws.Cell("B2").HasSparkline); + Assert.IsTrue(ws.Cell("B3").HasSparkline); + } + + [Test] + public void CurrentRegion() + { + // Partially based on sample in https://github.com/ClosedXML/ClosedXML/issues/120 + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.Cell("B1").SetValue("x") + .CellBelow().SetValue("x") + .CellBelow().SetValue("x"); + + ws.Cell("C1").SetValue("x") + .CellBelow().SetValue("x") + .CellBelow().SetValue("x"); + + //Deliberately D2 + ws.Cell("D2").SetValue("x") + .CellBelow().SetValue("x"); + + ws.Cell("G1").SetValue("x") + .CellBelow() // skip a cell + .CellBelow().SetValue("x") + .CellBelow().SetValue("x"); + + // Deliberately H2 + ws.Cell("H2").SetValue("x") + .CellBelow().SetValue("x") + .CellBelow().SetValue("x"); + + // A diagonal + ws.Cell("E8").SetValue("x") + .CellBelow().CellRight().SetValue("x") + .CellBelow().CellRight().SetValue("x") + .CellBelow().CellRight().SetValue("x") + .CellBelow().CellRight().SetValue("x"); + + Assert.AreEqual("A10:A10", ws.Cell("A10").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("B5:B5", ws.Cell("B5").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("P1:P1", ws.Cell("P1").CurrentRegion.RangeAddress.ToString()); + + Assert.AreEqual("B1:D3", ws.Cell("D3").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("B1:D4", ws.Cell("D4").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("B1:E4", ws.Cell("E4").CurrentRegion.RangeAddress.ToString()); + + foreach (var c in ws.Range("B1:D3").Cells()) + { + Assert.AreEqual("B1:D3", c.CurrentRegion.RangeAddress.ToString()); + } + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.AreEqual("A1:D3", c.CurrentRegion.RangeAddress.ToString()); + } + + Assert.AreEqual("A1:D4", ws.Cell("A4").CurrentRegion.RangeAddress.ToString()); + + foreach (var c in ws.Range("E1:E3").Cells()) + { + Assert.AreEqual("B1:E3", c.CurrentRegion.RangeAddress.ToString()); + } + + Assert.AreEqual("B1:E4", ws.Cell("E4").CurrentRegion.RangeAddress.ToString()); + + //// SECOND REGION + foreach (var c in ws.Range("F1:F4").Cells()) + { + Assert.AreEqual("F1:H4", c.CurrentRegion.RangeAddress.ToString()); + } + + Assert.AreEqual("F1:H5", ws.Cell("F5").CurrentRegion.RangeAddress.ToString()); + + //// DIAGONAL + Assert.AreEqual("E8:I12", ws.Cell("E8").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:I12", ws.Cell("F9").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:I12", ws.Cell("G10").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:I12", ws.Cell("H11").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:I12", ws.Cell("I12").CurrentRegion.RangeAddress.ToString()); + + Assert.AreEqual("E8:I12", ws.Cell("G9").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:I12", ws.Cell("F10").CurrentRegion.RangeAddress.ToString()); + + Assert.AreEqual("D7:I12", ws.Cell("D7").CurrentRegion.RangeAddress.ToString()); + Assert.AreEqual("E8:J13", ws.Cell("J13").CurrentRegion.RangeAddress.ToString()); + } + } + + // https://github.com/ClosedXML/ClosedXML/issues/630 + [Test] + public void ConsiderEmptyValueAsNumericInSumFormula() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.Cell("A1").SetValue("Empty"); + ws.Cell("A2").SetValue("Numeric"); + ws.Cell("A3").SetValue("Copy of numeric"); + + ws.Cell("B2").SetFormulaA1("=B1"); + ws.Cell("B3").SetFormulaA1("=B2"); + + ws.Cell("C2").SetFormulaA1("=SUM(C1)"); + ws.Cell("C3").SetFormulaA1("=C2"); + + object b1 = ws.Cell("B1").Value; + object b2 = ws.Cell("B2").Value; + object b3 = ws.Cell("B3").Value; + + Assert.AreEqual("", b1); + Assert.AreEqual(0, b2); + Assert.AreEqual(0, b3); + + object c1 = ws.Cell("C1").Value; + object c2 = ws.Cell("C2").Value; + object c3 = ws.Cell("C3").Value; + + Assert.AreEqual("", c1); + Assert.AreEqual(0, c2); + Assert.AreEqual(0, c3); + } + } + + [Test] + public void SetFormulaA1AffectsR1C1() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.Cell(1, 1); + cell.FormulaR1C1 = "R[1]C"; + + cell.FormulaA1 = "B2"; + + Assert.AreEqual("R[1]C[1]", cell.FormulaR1C1); + } + } + + [Test] + public void SetFormulaR1C1AffectsA1() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.Cell(1, 1); + cell.FormulaA1 = "A2"; + + cell.FormulaR1C1 = "R[1]C[1]"; + + Assert.AreEqual("B2", cell.FormulaA1); + } + } + + [Test] + public void FormulaWithCircularReferenceFails() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var A1 = ws.Cell("A1"); + var A2 = ws.Cell("A2"); + A1.FormulaA1 = "A2 + 1"; + A2.FormulaA1 = "A1 + 1"; + + Assert.Throws(() => + { + var _ = A1.Value; + }); + Assert.Throws(() => + { + var _ = A2.Value; + }); + } + } + + [Test] + public void InvalidFormulaShiftProducesREF() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").Value = 1; + ws.Cell("B1").Value = 2; + ws.Cell("B2").FormulaA1 = "=A1+B1"; + + Assert.AreEqual(3, ws.Cell("B2").Value); + + ws.Range("A2").Value = ws.Range("B2"); + var fA2 = ws.Cell("A2").FormulaA1; + + wb.SaveAs(ms); + + Assert.AreEqual("#REF!+A1", fA2); + } + + using (var wb2 = new XLWorkbook(ms)) + { + var fA2 = wb2.Worksheets.First().Cell("A2").FormulaA1; + Assert.AreEqual("#REF!+A1", fA2); + } + } + } + + [Test] + public void FormulaWithCircularReferenceFails2() + { + var cell = new XLWorkbook().Worksheets.Add("Sheet1").FirstCell(); + cell.FormulaA1 = "A1"; + Assert.Throws(() => + { + var _ = cell.Value; + }); + } + + [Test] + public void TryGetValueFormulaEvaluation() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var A1 = ws.Cell("A1"); + var A2 = ws.Cell("A2"); + var A3 = ws.Cell("A3"); + A1.FormulaA1 = "A2 + 1"; + A2.FormulaA1 = "A1 + 1"; + + Assert.IsFalse(A1.TryGetValue(out String _)); + Assert.IsFalse(A2.TryGetValue(out String _)); + Assert.IsTrue(A3.TryGetValue(out String _)); + } + } + + [Test] + public void SetValue_IEnumerable() + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + object[] values = { "Text", 45, DateTime.Today, true, "More text" }; + + ws.FirstCell().SetValue(values); + + Assert.AreEqual("Text", ws.FirstCell().GetString()); + Assert.AreEqual(45, ws.Cell("A2").GetDouble()); + Assert.AreEqual(DateTime.Today, ws.Cell("A3").GetDateTime()); + Assert.AreEqual(true, ws.Cell("A4").GetBoolean()); + Assert.AreEqual("More text", ws.Cell("A5").GetString()); + Assert.IsTrue(ws.Cell("A6").IsEmpty()); + } + + [Test] + public void ToStringNoFormatString() + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + var c = ws.FirstCell().CellBelow(2).CellRight(3); + + Assert.AreEqual("D3", c.ToString()); + } + + [Test] + [TestCase("D3", "A")] + [TestCase("YEAR(DATE(2018, 1, 1))", "F")] + [TestCase("YEAR(DATE(2018, 1, 1))", "f")] + [TestCase("0000.00", "NF")] + [TestCase("0000.00", "nf")] + [TestCase("FFFF0000", "fg")] + [TestCase("Color Theme: Accent5, Tint: 0", "BG")] + [TestCase("2018.00", "v")] + public void ToStringFormatString(string expected, string format) + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + var c = ws.FirstCell().CellBelow(2).CellRight(3); + + var formula = "YEAR(DATE(2018, 1, 1))"; + c.FormulaA1 = formula; + + var numberFormat = "0000.00"; + c.Style.NumberFormat.Format = numberFormat; + + c.Style.Font.FontColor = XLColor.Red; + c.Style.Fill.BackgroundColor = XLColor.FromTheme(XLThemeColor.Accent5); + + Assert.AreEqual(expected, c.ToString(format)); + + Assert.Throws(() => c.ToString("dummy")); + } + + [Test] + public void ToStringInvalidFormat() + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + var c = ws.FirstCell(); + + Assert.Throws(() => c.ToString("dummy")); + } + } +} diff --git a/ClosedXML_Tests/Excel/Clearing/ClearingTests.cs b/ClosedXML_Tests/Excel/Clearing/ClearingTests.cs new file mode 100644 index 0000000..be4a3ba --- /dev/null +++ b/ClosedXML_Tests/Excel/Clearing/ClearingTests.cs @@ -0,0 +1,309 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class ClearingTests + { + private static XLColor backgroundColor = XLColor.LightBlue; + private static XLColor foregroundColor = XLColor.DarkBrown; + + private IXLWorkbook SetupWorkbook() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + var c = ws.FirstCell() + .SetValue("Hello world!"); + + c.Comment.AddText("Some comment"); + + c.Style.Fill.BackgroundColor = backgroundColor; + c.Style.Font.FontColor = foregroundColor; + c.SetDataValidation().Custom("B1"); + + //// + + c = ws.FirstCell() + .CellBelow() + .SetFormulaA1("=LEFT(A1,5)"); + + c.Comment.AddText("Another comment"); + + c.Style.Fill.BackgroundColor = backgroundColor; + c.Style.Font.FontColor = foregroundColor; + + //// + + c = ws.FirstCell() + .CellBelow(2) + .SetValue(new DateTime(2018, 1, 15)); + + c.Comment.AddText("A date"); + + c.Style.Fill.BackgroundColor = backgroundColor; + c.Style.Font.FontColor = foregroundColor; + + ws.Column(1) + .AddConditionalFormat().WhenStartsWith("Hell") + .Fill.SetBackgroundColor(XLColor.Red) + .Border.SetOutsideBorder(XLBorderStyleValues.Thick) + .Border.SetOutsideBorderColor(XLColor.Blue) + .Font.SetBold(); + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + + Assert.AreEqual(false, ws.Cell("A1").HasFormula); + Assert.AreEqual(true, ws.Cell("A2").HasFormula); + Assert.AreEqual(false, ws.Cell("A1").HasFormula); + + foreach (var cell in ws.Range("A1:A3").Cells()) + { + Assert.AreEqual(backgroundColor, cell.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, cell.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsTrue(cell.HasComment); + } + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + + return wb; + } + + [Test] + public void WorksheetClearAll() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.All); + + foreach (var c in ws.Range("A1:A10").Cells()) + { + Assert.IsTrue(c.IsEmpty()); + Assert.AreEqual(XLDataType.Text, c.DataType); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Font.FontColor, c.Style.Font.FontColor); + Assert.IsFalse(ws.ConditionalFormats.Any()); + Assert.IsFalse(c.HasComment); + Assert.AreEqual(String.Empty, c.DataValidation.Value); + } + } + } + + [Test] + public void WorksheetClearContents() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.Contents); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsTrue(c.IsEmpty(XLCellsUsedOptions.Contents)); + Assert.AreEqual(backgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, c.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsTrue(c.HasComment); + } + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + } + } + + [Test] + public void WorksheetClearDataType() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.DataType); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsFalse(c.IsEmpty()); + Assert.AreEqual(XLDataType.Text, c.DataType); + Assert.AreEqual(backgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, c.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsTrue(c.HasComment); + } + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + } + } + + [Test] + public void WorksheetClearNormalFormats() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.NormalFormats); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsFalse(c.IsEmpty()); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Font.FontColor, c.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsTrue(c.HasComment); + } + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + } + } + + [Test] + public void WorksheetClearConditionalFormats() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.ConditionalFormats); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsFalse(c.IsEmpty()); + Assert.AreEqual(backgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, c.Style.Font.FontColor); + Assert.IsFalse(ws.ConditionalFormats.Any()); + Assert.IsTrue(c.HasComment); + } + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + } + } + + [Test] + public void WorksheetClearComments() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.Comments); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsFalse(c.IsEmpty()); + Assert.AreEqual(backgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, c.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsFalse(c.HasComment); + } + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + + Assert.AreEqual("B1", ws.Cell("A1").DataValidation.Value); + } + } + + [Test] + public void WorksheetClearDataValidation() + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + + ws.Clear(XLClearOptions.DataValidation); + + foreach (var c in ws.Range("A1:A3").Cells()) + { + Assert.IsFalse(c.IsEmpty()); + Assert.AreEqual(backgroundColor, c.Style.Fill.BackgroundColor); + Assert.AreEqual(foregroundColor, c.Style.Font.FontColor); + Assert.IsTrue(ws.ConditionalFormats.Any()); + Assert.IsTrue(c.HasComment); + } + + Assert.AreEqual(XLDataType.Text, ws.Cell("A1").DataType); + Assert.AreEqual(XLDataType.Text, ws.Cell("A2").DataType); + Assert.AreEqual(XLDataType.DateTime, ws.Cell("A3").DataType); + + Assert.AreEqual(string.Empty, ws.Cell("A1").DataValidation.Value); + } + } + + [Test] + public void DeleteClearedCellValue() + { + using (var ms = new MemoryStream()) + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("Hello world!", ws.Cell("A1").GetString()); + Assert.AreEqual(new DateTime(2018, 1, 15), ws.Cell("A3").GetDateTime()); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + ws.Clear(XLClearOptions.Contents); + Assert.AreEqual("", ws.Cell("A1").GetString()); + Assert.Throws(() => ws.Cell("A3").GetDateTime()); + + wb.Save(); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("", ws.Cell("A1").GetString()); + Assert.Throws(() => ws.Cell("A3").GetDateTime()); + } + } + } + + [TestCase(XLClearOptions.All, 2)] + [TestCase(XLClearOptions.AllContents, 4)] + [TestCase(XLClearOptions.AllFormats, 4)] + [TestCase(XLClearOptions.Contents, 4)] + [TestCase(XLClearOptions.MergedRanges, 2)] + public void CanClearMergedRanges(XLClearOptions options, int expectedCount) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Test"); + + ws.Range("A1:C3").Merge(); + ws.Range("A4:B6").Merge(); + ws.Range("D1:F3").Merge(); + ws.Range("E4:F6").Merge(); + + ws.Range("C1:D6").Clear(options); + + Assert.AreEqual(expectedCount, ws.MergedRanges.Count); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Columns/ColumnTests.cs b/ClosedXML_Tests/Excel/Columns/ColumnTests.cs new file mode 100644 index 0000000..5e07723 --- /dev/null +++ b/ClosedXML_Tests/Excel/Columns/ColumnTests.cs @@ -0,0 +1,249 @@ +using System; +using System.IO; +using System.Linq; +using ClosedXML.Excel; +using NUnit.Framework; +using ClosedXML_Tests.Utils; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class ColumnTests + { + [Test] + public void ColumnUsed() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(2, 1).SetValue("Test"); + ws.Cell(3, 1).SetValue("Test"); + + IXLRangeColumn fromColumn = ws.Column(1).ColumnUsed(); + Assert.AreEqual("A2:A3", fromColumn.RangeAddress.ToStringRelative()); + + IXLRangeColumn fromRange = ws.Range("A1:A5").FirstColumn().ColumnUsed(); + Assert.AreEqual("A2:A3", fromRange.RangeAddress.ToStringRelative()); + } + + [Test] + public void CopyColumn() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Test").Style.Font.SetBold(); + ws.FirstColumn().CopyTo(ws.Column(2)); + + Assert.IsTrue(ws.Cell("B1").Style.Font.Bold); + } + + [Test] + public void InsertingColumnsBefore1() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Columns("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Column(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLColumn column1 = ws.Column(1); + IXLColumn column2 = ws.Column(2); + IXLColumn column3 = ws.Column(3); + + IXLColumn columnIns = ws.Column(1).InsertColumnsBefore(1).First(); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Column(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Column(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Column(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Column(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Column(3).Cell(2).GetString()); + + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, columnIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, columnIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, columnIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, column2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, column2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, column2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", column2.Cell(2).GetString()); + } + + [Test] + public void InsertingColumnsBefore2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Columns("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Column(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLColumn column1 = ws.Column(1); + IXLColumn column2 = ws.Column(2); + IXLColumn column3 = ws.Column(3); + + IXLColumn columnIns = ws.Column(2).InsertColumnsBefore(1).First(); + + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Column(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Column(3).Cell(2).GetString()); + + + Assert.AreEqual(XLColor.Red, columnIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, columnIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, columnIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, column2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, column2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, column2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", column2.Cell(2).GetString()); + } + + [Test] + public void InsertingColumnsBefore3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Columns("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Column(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLColumn column1 = ws.Column(1); + IXLColumn column2 = ws.Column(2); + IXLColumn column3 = ws.Column(3); + + IXLColumn columnIns = ws.Column(3).InsertColumnsBefore(1).First(); + + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Column(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Column(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Column(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Column(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Column(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Column(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Column(2).Cell(2).GetString()); + + + Assert.AreEqual(XLColor.Yellow, columnIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, columnIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, columnIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, column2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, column2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, column2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, column3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, column3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", column2.Cell(2).GetString()); + } + + [Test] + public void NoColumnsUsed() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + Int32 count = 0; + + foreach (IXLColumn row in ws.ColumnsUsed()) + count++; + + foreach (IXLRangeColumn row in ws.Range("A1:C3").ColumnsUsed()) + count++; + + Assert.AreEqual(0, count); + } + + [Test] + public void UngroupFromAll() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Columns(1, 2).Group(); + ws.Columns(1, 2).Ungroup(true); + } + + [Test] + public void LastColumnUsed() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "A1"; + ws.Cell("B1").Value = "B1"; + ws.Cell("A2").Value = "A2"; + var lastCoUsed = ws.LastColumnUsed().ColumnNumber(); + Assert.AreEqual(2, lastCoUsed); + } + + [Test] + public void NegativeColumnNumberIsInvalid() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1") as XLWorksheet; + + var column = new XLColumn(ws, -1); + + Assert.IsFalse(column.RangeAddress.IsValid); + } + } +} diff --git a/ClosedXML_Tests/Excel/Comments/CommentsTests.cs b/ClosedXML_Tests/Excel/Comments/CommentsTests.cs new file mode 100644 index 0000000..9d41745 --- /dev/null +++ b/ClosedXML_Tests/Excel/Comments/CommentsTests.cs @@ -0,0 +1,194 @@ +using ClosedXML.Excel; +using DocumentFormat.OpenXml.Packaging; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Comments +{ + public class CommentsTests + { + [Test] + public void CanGetColorFromIndex81() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\CommentsWithIndexedColor81.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + var c = ws.FirstCellUsed(); + + var xlColor = c.Comment.Style.ColorsAndLines.LineColor; + Assert.AreEqual(XLColorType.Indexed, xlColor.ColorType); + Assert.AreEqual(81, xlColor.Indexed); + + var color = xlColor.Color.ToHex(); + Assert.AreEqual("FF000000", color); + } + } + + [Test] + public void AddingCommentDoesNotAffectCollections() + { + var ws = new XLWorkbook().AddWorksheet() as XLWorksheet; + ws.Cell("A1").SetValue(10); + ws.Cell("A4").SetValue(10); + ws.Cell("A5").SetValue(10); + + ws.Rows("1,4").Height = 20; + + Assert.AreEqual(2, ws.Internals.RowsCollection.Count); + Assert.AreEqual(3, ws.Internals.CellsCollection.RowsCollection.SelectMany(r => r.Value.Values).Count()); + + ws.Cell("A4").Comment.AddText("Comment"); + Assert.AreEqual(2, ws.Internals.RowsCollection.Count); + Assert.AreEqual(3, ws.Internals.CellsCollection.RowsCollection.SelectMany(r => r.Value.Values).Count()); + + ws.Row(1).Delete(); + Assert.AreEqual(1, ws.Internals.RowsCollection.Count); + Assert.AreEqual(2, ws.Internals.CellsCollection.RowsCollection.SelectMany(r => r.Value.Values).Count()); + } + + [Test] + public void CopyCommentStyle() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + string strExcelComment = "1) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "1) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "2) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "3) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "4) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "5) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "6) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "7) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "8) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + strExcelComment = strExcelComment + "9) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABC ABC ABC ABC ABC" + Environment.NewLine; + + var cell = ws.Cell(2, 2).SetValue("Comment 1"); + + cell.Comment + .SetVisible(false) + .AddText(strExcelComment); + + cell.Comment + .Style + .Alignment + .SetAutomaticSize(); + + cell.Comment + .Style + .ColorsAndLines + .SetFillColor(XLColor.Red); + + ws.Row(1).InsertRowsAbove(1); + + Action validate = c => + { + Assert.IsTrue(c.Comment.Style.Alignment.AutomaticSize); + Assert.AreEqual(XLColor.Red, c.Comment.Style.ColorsAndLines.FillColor); + }; + + validate(ws.Cell("B3")); + + ws.Column(1).InsertColumnsBefore(2); + + validate(ws.Cell("D3")); + + ws.Column(1).Delete(); + + validate(ws.Cell("C3")); + + ws.Row(1).Delete(); + + validate(ws.Cell("C2")); + } + } + + [Test] + public void EnsureUnaffectedCommentAndVmlPartIdsAndUris() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\CommentAndButton.xlsx"))) + using (var ms = new MemoryStream()) + { + string commentPartId; + string commentPartUri; + + string vmlPartId; + string vmlPartUri; + + using (var ssd = SpreadsheetDocument.Open(stream, isEditable: false)) + { + var wbp = ssd.GetPartsOfType().Single(); + var wsp = wbp.GetPartsOfType().Last(); + + var wscp = wsp.GetPartsOfType().Single(); + commentPartId = wsp.GetIdOfPart(wscp); + commentPartUri = wscp.Uri.ToString(); + + var vmlp = wsp.GetPartsOfType().Single(); + vmlPartId = wsp.GetIdOfPart(vmlp); + vmlPartUri = vmlp.Uri.ToString(); + } + + stream.Position = 0; + stream.CopyTo(ms); + ms.Position = 0; + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.IsTrue(ws.FirstCell().HasComment); + + wb.SaveAs(ms); + } + + ms.Position = 0; + + using (var ssd = SpreadsheetDocument.Open(ms, isEditable: false)) + { + var wbp = ssd.GetPartsOfType().Single(); + var wsp = wbp.GetPartsOfType().Last(); + + var wscp = wsp.GetPartsOfType().Single(); + Assert.AreEqual(commentPartUri, wscp.Uri.ToString()); + Assert.AreEqual(commentPartId, wsp.GetIdOfPart(wscp)); + + var vmlp = wsp.GetPartsOfType().Single(); + Assert.AreEqual(vmlPartUri, vmlp.Uri.ToString()); + Assert.AreEqual(vmlPartId, wsp.GetIdOfPart(vmlp)); + } + } + } + + [Test] + public void SavingDoesNotCauseTwoRootElements() // See #1157 + { + using (var ms = new MemoryStream()) + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\CommentAndButton.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + wb.SaveAs(ms); + } + + Assert.DoesNotThrow(() => new XLWorkbook(ms)); + } + } + + [Test] + public void CanLoadCommentVisibility() + { + using (var inputStream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Drawings\Comments\inputfile.xlsx"))) + using (var workbook = new XLWorkbook(inputStream)) + { + var ws = workbook.Worksheets.First(); + + Assert.True(ws.Cell("A1").Comment.Visible); + Assert.False(ws.Cell("A4").Comment.Visible); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatCopyTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatCopyTests.cs new file mode 100644 index 0000000..3ac5cbd --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatCopyTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatCopyTests + { + [Test] + public void StylesAreCreatedDuringCopy() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet"); + var format = ws.Range("A1:A1").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + + var wb2 = new XLWorkbook(); + var ws2 = wb2.Worksheets.Add("Sheet2"); + ws2.FirstCell().CopyFrom(ws.FirstCell()); + Assert.That(ws2.ConditionalFormats.First().Style.Fill.BackgroundColor, Is.EqualTo(XLColor.Blue)); //Added blue style + } + + [Test] + public void CopyConditionalFormatSingleWorksheet() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet"); + var format = ws.Range("A1:A1").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + + ws.Cell("A1").CopyTo("B2"); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual(2, ws.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual("A1:A1", ws.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + Assert.AreEqual("B2:B2", ws.ConditionalFormats.First().Ranges.Last().RangeAddress.ToString()); + } + + [Test] + public void CopyConditionalFormatSameRange() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet"); + var format = ws.Range("A1:C3").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + + ws.Cell("A1").CopyTo("B2"); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual(1, ws.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual("A1:C3", ws.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + } + + [Test] + public void CopyConditionalFormatsDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var format = ws1.Range("A1:A1").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + var ws2 = wb.Worksheets.Add("Sheet2"); + var otherCell = ws2.Cell("B2"); + + ws1.Cell("A1").CopyTo(otherCell); + + Assert.AreEqual(1, ws1.ConditionalFormats.Count()); + Assert.AreEqual(1, ws2.ConditionalFormats.Count()); + Assert.AreEqual(1, ws1.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual(1, ws2.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual("Sheet1", ws1.ConditionalFormats.First().Ranges.First().Worksheet.Name); + Assert.AreEqual("Sheet2", ws2.ConditionalFormats.First().Ranges.First().Worksheet.Name); + Assert.AreEqual("A1:A1", ws1.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + Assert.AreEqual("B2:B2", ws2.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + } + + [Test] + public void FullCopyConditionalFormatSameWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var format = (XLConditionalFormat)ws1.Range("A1:A1").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + + TestDelegate action = () => format.CopyTo(ws1); + + Assert.Throws(typeof(InvalidOperationException), action); + + } + [Test] + public void FullCopyConditionalFormatDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var format = (XLConditionalFormat)ws1.Range("A1:C3").AddConditionalFormat(); + format.WhenEquals("=" + format.Ranges.First().FirstCell().CellRight(4).Address.ToStringRelative()).Fill + .SetBackgroundColor(XLColor.Blue); + var ws2 = wb.Worksheets.Add("Sheet2"); + + format.CopyTo(ws2); + + Assert.AreEqual(1, ws1.ConditionalFormats.Count()); + Assert.AreEqual(1, ws2.ConditionalFormats.Count()); + Assert.AreEqual(1, ws1.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual(1, ws2.ConditionalFormats.First().Ranges.Count); + Assert.AreEqual("Sheet1!A1:C3", ws1.ConditionalFormats.First().Ranges.First().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet2!A1:C3", ws2.ConditionalFormats.First().Ranges.First().RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + } +} diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatShiftTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatShiftTests.cs new file mode 100644 index 0000000..43684cf --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatShiftTests.cs @@ -0,0 +1,127 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatShiftTests + { + [Test] + public void CFShiftedOnColumnInsert() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("CFShift"); + ws.Range("A1:A1").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AirForceBlue); + ws.Range("A2:B2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AliceBlue); + ws.Range("A3:C3").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Alizarin); + ws.Range("B4:B6").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Almond); + ws.Range("C7:D7").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Amaranth); + ws.Cells("A1:D7").Value = 1; + + ws.Column(2).InsertColumnsAfter(2); + var cf = ws.ConditionalFormats.ToArray(); + + Assert.AreEqual(5, cf.Length); + Assert.AreEqual("A1:A1", cf[0].Range.RangeAddress.ToString()); + Assert.AreEqual("A2:D2", cf[1].Range.RangeAddress.ToString()); + Assert.AreEqual("A3:E3", cf[2].Range.RangeAddress.ToString()); + Assert.AreEqual("B4:D6", cf[3].Range.RangeAddress.ToString()); + Assert.AreEqual("E7:F7", cf[4].Range.RangeAddress.ToString()); + } + } + + [Test] + public void CFShiftedOnRowInsert() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("CFShift"); + ws.Range("A1:A1").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AirForceBlue); + ws.Range("B1:B2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AliceBlue); + ws.Range("C1:C3").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Alizarin); + ws.Range("D2:F2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Almond); + ws.Range("G4:G5").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Amaranth); + ws.Cells("A1:G5").Value = 1; + + ws.Row(2).InsertRowsBelow(2); + var cf = ws.ConditionalFormats.ToArray(); + + Assert.AreEqual(5, cf.Length); + Assert.AreEqual("A1:A1", cf[0].Range.RangeAddress.ToString()); + Assert.AreEqual("B1:B4", cf[1].Range.RangeAddress.ToString()); + Assert.AreEqual("C1:C5", cf[2].Range.RangeAddress.ToString()); + Assert.AreEqual("D2:F4", cf[3].Range.RangeAddress.ToString()); + Assert.AreEqual("G6:G7", cf[4].Range.RangeAddress.ToString()); + } + } + + [Test] + public void CFShiftedOnColumnDelete() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("CFShift"); + ws.Range("A1:A1").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AirForceBlue); + ws.Range("A2:B2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AliceBlue); + ws.Range("A3:C3").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Alizarin); + ws.Range("B4:B6").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Almond); + ws.Range("C7:D7").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Amaranth); + ws.Cells("A1:D7").Value = 1; + + ws.Column(2).Delete(); + var cf = ws.ConditionalFormats.ToArray(); + + Assert.AreEqual(4, cf.Length); + Assert.AreEqual("A1:A1", cf[0].Range.RangeAddress.ToString()); + Assert.AreEqual("A2:A2", cf[1].Range.RangeAddress.ToString()); + Assert.AreEqual("A3:B3", cf[2].Range.RangeAddress.ToString()); + Assert.AreEqual("B7:C7", cf[3].Range.RangeAddress.ToString()); + } + } + + [Test] + public void CFShiftedOnRowDelete() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("CFShift"); + ws.Range("A1:A1").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AirForceBlue); + ws.Range("B1:B2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.AliceBlue); + ws.Range("C1:C3").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Alizarin); + ws.Range("D2:F2").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Almond); + ws.Range("G4:G5").AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Amaranth); + ws.Cells("A1:G5").Value = 1; + + ws.Row(2).Delete(); + var cf = ws.ConditionalFormats.ToArray(); + + Assert.AreEqual(4, cf.Length); + Assert.AreEqual("A1:A1", cf[0].Range.RangeAddress.ToString()); + Assert.AreEqual("B1:B1", cf[1].Range.RangeAddress.ToString()); + Assert.AreEqual("C1:C2", cf[2].Range.RangeAddress.ToString()); + Assert.AreEqual("G3:G4", cf[3].Range.RangeAddress.ToString()); + } + } + + [Test] + public void CFShiftedTruncateRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("CFShift"); + ws.AsRange().AddConditionalFormat().WhenGreaterThan(0).Fill.SetBackgroundColor(XLColor.Red); + var cf = ws.ConditionalFormats.Single(); + + ws.Row(2).InsertRowsAbove(1); + Assert.IsTrue(cf.Range.RangeAddress.IsValid); + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", cf.Range.RangeAddress.ToString()); + + ws.Column(2).InsertColumnsAfter(1); + Assert.IsTrue(cf.Range.RangeAddress.IsValid); + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", cf.Range.RangeAddress.ToString()); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs new file mode 100644 index 0000000..252a4d8 --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs @@ -0,0 +1,118 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatTests + { + [Test] + public void MaintainConditionalFormattingOrder() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\StyleReferenceFiles\ConditionalFormattingOrder\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"Other\StyleReferenceFiles\ConditionalFormattingOrder\ConditionalFormattingOrder.xlsx"); + } + } + + [TestCase(true, 7)] + [TestCase(false, 8)] + public void SaveOptionAffectsConsolidationConditionalFormatRanges(bool consolidateConditionalFormatRanges, int expectedCount) + { + var options = new SaveOptions + { + ConsolidateConditionalFormatRanges = consolidateConditionalFormatRanges + }; + + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet"); + + ws.Range("D2:D3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue(); + ws.Range("B2:B3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue(); + ws.Range("E2:E6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue); + ws.Range("F2:F6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue); + ws.Range("G2:G7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue); + ws.Range("H2:H7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue); + ws.Range("I2:I6").AddConditionalFormat().WhenContains("test"); + ws.Range("J2:J6").AddConditionalFormat().WhenContains("test"); + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, options); + var wb_saved = new XLWorkbook(ms); + Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").ConditionalFormats.Count()); + } + } + + [TestCase(true, 1)] + [TestCase(false, 2)] + public void SaveOptionAffectsConsolidationDataValidationRanges(bool consolidateDataValidationRanges, int expectedCount) + { + var options = new SaveOptions + { + ConsolidateDataValidationRanges = consolidateDataValidationRanges + }; + + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet"); + ws.Range("C2:C5").SetDataValidation().Decimal.Between(1, 5); + ws.Range("D2:D5").SetDataValidation().Decimal.Between(1, 5); + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, options); + var wb_saved = new XLWorkbook(ms); + Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").DataValidations.Count()); + } + } + + [TestCase("en-US")] + [TestCase("fr-FR")] + [TestCase("ru-RU")] + public void SaveConditionalFormat_CultureIndependent(string culture) + { + using (var ms = new MemoryStream()) + { + var expectedValue = 1.5; + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var i = 1; + ws.Cell(i++, 1).AddConditionalFormat().WhenEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenNotEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenNotBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + + var conditionalFormatValues = ws.ConditionalFormats + .SelectMany(cf => cf.Values.Values) + .Select(v => v.Value) + .Distinct(); + + Assert.AreEqual(1, conditionalFormatValues.Count()); + Assert.AreEqual("1.5", conditionalFormatValues.Single()); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatsConsolidateTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatsConsolidateTests.cs new file mode 100644 index 0000000..65a8e22 --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatsConsolidateTests.cs @@ -0,0 +1,206 @@ +using System.IO; +using System.Linq; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatsConsolidateTests + { + [Test] + public void ConsecutivelyRowsConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B2:C2").AddConditionalFormat()); + SetFormat1(ws.Range("B4:C4").AddConditionalFormat()); + SetFormat1(ws.Range("B3:C3").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B2:C4", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F2", format.Values.Values.First().Value); + } + + [Test] + public void ConsecutivelyColumnsConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("D2:D3").AddConditionalFormat()); + SetFormat1(ws.Range("B2:B3").AddConditionalFormat()); + SetFormat1(ws.Range("C2:C3").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B2:D3", format.Ranges.First().RangeAddress.ToStringRelative()); + Assert.AreEqual("F2", format.Values.Values.First().Value); + } + + [Test] + public void Contains1ConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B11:D12").AddConditionalFormat()); + SetFormat1(ws.Range("C12:D12").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B11:D12", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F11", format.Values.Values.First().Value); + } + + [Test] + public void Contains2ConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B14:C14").AddConditionalFormat()); + SetFormat1(ws.Range("B14:B14").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B14:C14", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F14", format.Values.Values.First().Value); + } + + [Test] + public void SuperimposedConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B16:D18").AddConditionalFormat()); + SetFormat1(ws.Range("B18:D19").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B16:D19", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F16", format.Values.Values.First().Value); + } + + [Test] + public void DifferentFormatNoConsolidateTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B11:D12").AddConditionalFormat()); + SetFormat2(ws.Range("C12:D12").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(2, ws.ConditionalFormats.Count()); + } + + [Test] + public void ConsolidatePreservesPriorities() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("A1:A5").AddConditionalFormat()); + SetFormat2(ws.Range("A1:A5").AddConditionalFormat()); + SetFormat2(ws.Range("A6:A10").AddConditionalFormat()); + SetFormat1(ws.Range("A6:A10").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(3, ws.ConditionalFormats.Count()); + Assert.AreEqual((ws.ConditionalFormats.First().Style as XLStyle).Value, (ws.ConditionalFormats.Last().Style as XLStyle).Value); + Assert.AreNotEqual((ws.ConditionalFormats.First().Style as XLStyle).Value, (ws.ConditionalFormats.ElementAt(1).Style as XLStyle).Value); + } + + + [Test] + public void ConsolidatePreservesPriorities2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("A1:A1").AddConditionalFormat()); + SetFormat2(ws.Range("A2:A3").AddConditionalFormat()); + SetFormat1(ws.Range("A2:A6").AddConditionalFormat()); + SetFormat1(ws.Range("A7:A8").AddConditionalFormat()); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(3, ws.ConditionalFormats.Count()); + Assert.AreEqual((ws.ConditionalFormats.First().Style as XLStyle).Value, (ws.ConditionalFormats.Last().Style as XLStyle).Value); + Assert.AreNotEqual((ws.ConditionalFormats.First().Style as XLStyle).Value, (ws.ConditionalFormats.ElementAt(1).Style as XLStyle).Value); + Assert.IsTrue(ws.ConditionalFormats.All(cf => cf.Ranges.Count == 1), "Number of ranges in consolidated conditional formats is expected to be 1"); + Assert.AreEqual("A1:A1", ws.ConditionalFormats.ElementAt(0).Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A2:A3", ws.ConditionalFormats.ElementAt(1).Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A2:A8", ws.ConditionalFormats.ElementAt(2).Ranges.Single().RangeAddress.ToString()); + } + + + [Test] + public void ConsolidateShiftsFormulaRelativelyToTopMostCell() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + var ranges = ws.Ranges("B3:B8,C3:C4,A3:A4,C5:C8,A5:A8").Cast(); + var cf = new XLConditionalFormat(ranges); + cf.Values.Add(new XLFormula("=A3=$D3")); + cf.Style.Fill.SetBackgroundColor(XLColor.Red); + ws.ConditionalFormats.Add(cf); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual((ws.ConditionalFormats.Single().Style as XLStyle).Value, (cf.Style as XLStyle).Value); + Assert.AreEqual("A3:C8", ws.ConditionalFormats.Single().Ranges.Single().RangeAddress.ToString()); + Assert.IsTrue(ws.ConditionalFormats.Single().Values.Single().Value.IsFormula); + Assert.AreEqual("A3=$D3", ws.ConditionalFormats.Single().Values.Single().Value.Value); + } + + [Test] + public void ColorScaleComparing() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet"); + + var ranges = ws.Ranges("B3:B8,C3:C4,A3:A4,C5:C8,A5:A8").Cast(); + var cf1 = new XLConditionalFormat(ranges); + cf1.ColorScale() + .LowestValue(XLColor.Red) + .HighestValue(XLColor.Green); + + var cf2 = new XLConditionalFormat(ranges); + cf2.ColorScale() + .LowestValue(XLColor.Red) + .HighestValue(XLColor.Green); + Assert.True(XLConditionalFormat.NoRangeComparer.Equals(cf1, cf2)); + } + } + + private static void SetFormat1(IXLConditionalFormat format) + { + format.WhenEquals("="+format.Range.FirstCell().CellRight(4).Address.ToStringRelative()).Fill.SetBackgroundColor(XLColor.Blue); + } + + private static void SetFormat2(IXLConditionalFormat format) + { + format.WhenEquals(5).Fill.SetBackgroundColor(XLColor.AliceBlue); + } + } +} diff --git a/ClosedXML_Tests/Excel/Coordinates/XLAddressTests.cs b/ClosedXML_Tests/Excel/Coordinates/XLAddressTests.cs new file mode 100644 index 0000000..77dc0ed --- /dev/null +++ b/ClosedXML_Tests/Excel/Coordinates/XLAddressTests.cs @@ -0,0 +1,191 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLAddressTests + { + [Test] + public void ToStringTest() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLAddress address = ws.Cell(1, 1).Address; + + Assert.AreEqual("A1", address.ToString()); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("Sheet1!A1", address.ToString(XLReferenceStyle.Default, true)); + + Assert.AreEqual("A1", address.ToStringRelative()); + Assert.AreEqual("Sheet1!A1", address.ToStringRelative(true)); + + Assert.AreEqual("$A$1", address.ToStringFixed()); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("Sheet1!$A$1", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet1!R1C1", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("Sheet1!$A$1", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void ToStringTestWithSpace() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + IXLAddress address = ws.Cell(1, 1).Address; + + Assert.AreEqual("A1", address.ToString()); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!A1", address.ToString(XLReferenceStyle.Default, true)); + + Assert.AreEqual("A1", address.ToStringRelative()); + Assert.AreEqual("'Sheet 1'!A1", address.ToStringRelative(true)); + + Assert.AreEqual("$A$1", address.ToStringFixed()); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!$A$1", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!R1C1", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("'Sheet 1'!$A$1", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void InvalidAddressToStringTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToString()); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToString(XLReferenceStyle.Default, true)); + } + + [Test] + public void InvalidAddressToStringFixedTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToStringFixed()); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void InvalidAddressToStringRelativeTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToStringRelative()); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringRelative(true)); + } + + [Test] + public void AddressOnDeletedWorksheetToStringTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("A1", address.ToString()); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("A1", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!A1", address.ToString(XLReferenceStyle.Default, true)); + } + + [Test] + public void AddressOnDeletedWorksheetToStringFixedTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("$A$1", address.ToStringFixed()); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("$A$1", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!$A$1", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!R1C1", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("#REF!$A$1", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void AddressOnDeletedWorksheetToStringRelativeTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("A1", address.ToStringRelative()); + Assert.AreEqual("#REF!A1", address.ToStringRelative(true)); + } + + [Test] + public void InvalidAddressOnDeletedWorksheetToStringTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!", address.ToString()); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.Default, true)); + } + + [Test] + public void InvalidAddressOnDeletedWorksheetToStringFixedTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!", address.ToStringFixed()); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void InvalidAddressOnDeletedWorksheetToStringRelativeTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!", address.ToStringRelative()); + Assert.AreEqual("#REF!#REF!", address.ToStringRelative(true)); + } + + #region Private Methods + + private IXLAddress ProduceInvalidAddress() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + var range = ws.Range("A1:B2"); + + ws.Rows(1, 5).Delete(); + return range.RangeAddress.FirstAddress; + } + + private IXLAddress ProduceAddressOnDeletedWorksheet() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + var address = ws.Cell("A1").Address; + + ws.Delete(); + return address; + } + + private IXLAddress ProduceInvalidAddressOnDeletedWorksheet() + { + var address = ProduceInvalidAddress(); + address.Worksheet.Delete(); + return address; + } + + #endregion Private Methods + } +} diff --git a/ClosedXML_Tests/Excel/DataValidations/DataValidationShiftTests.cs b/ClosedXML_Tests/Excel/DataValidations/DataValidationShiftTests.cs new file mode 100644 index 0000000..03f452f --- /dev/null +++ b/ClosedXML_Tests/Excel/DataValidations/DataValidationShiftTests.cs @@ -0,0 +1,127 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests.Excel.DataValidations +{ + [TestFixture] + public class DataValidationShiftTests + { + [Test] + public void DataValidationShiftedOnColumnInsert() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("DataValidationShift"); + ws.Range("A1:A1").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("A2:B2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("A3:C3").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("B4:B6").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("C7:D7").SetDataValidation().WholeNumber.Between(0, 1); + ws.Cells("A1:D7").Value = 1; + + ws.Column(2).InsertColumnsAfter(2); + var dv = ws.DataValidations.ToArray(); + + Assert.AreEqual(5, dv.Length); + Assert.AreEqual("A1:A1", dv[0].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A2:D2", dv[1].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A3:E3", dv[2].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("B4:D6", dv[3].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("E7:F7", dv[4].Ranges.Single().RangeAddress.ToString()); + } + } + + [Test] + public void DataValidationShiftedOnRowInsert() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("DataValidationShift"); + ws.Range("A1:A1").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("B1:B2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("C1:C3").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("D2:F2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("G4:G5").SetDataValidation().WholeNumber.Between(0, 1); + ws.Cells("A1:G5").Value = 1; + + ws.Row(2).InsertRowsBelow(2); + var dv = ws.DataValidations.ToArray(); + + Assert.AreEqual(5, dv.Length); + Assert.AreEqual("A1:A1", dv[0].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("B1:B4", dv[1].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("C1:C5", dv[2].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("D2:F4", dv[3].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("G6:G7", dv[4].Ranges.Single().RangeAddress.ToString()); + } + } + + [Test] + public void DataValidationShiftedOnColumnDelete() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("DataValidationShift"); + ws.Range("A1:A1").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("A2:B2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("A3:C3").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("B4:B6").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("C7:D7").SetDataValidation().WholeNumber.Between(0, 1); + ws.Cells("A1:D7").Value = 1; + + ws.Column(2).Delete(); + var dv = ws.DataValidations.ToArray(); + + Assert.AreEqual(4, dv.Length); + Assert.AreEqual("A1:A1", dv[0].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A2:A2", dv[1].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("A3:B3", dv[2].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("B7:C7", dv[3].Ranges.Single().RangeAddress.ToString()); + } + } + + [Test] + public void DataValidationShiftedOnRowDelete() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("DataValidationShift"); + ws.Range("A1:A1").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("B1:B2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("C1:C3").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("D2:F2").SetDataValidation().WholeNumber.Between(0, 1); + ws.Range("G4:G5").SetDataValidation().WholeNumber.Between(0, 1); + ws.Cells("A1:G5").Value = 1; + + ws.Row(2).Delete(); + var dv = ws.DataValidations.ToArray(); + + Assert.AreEqual(4, dv.Length); + Assert.AreEqual("A1:A1", dv[0].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("B1:B1", dv[1].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("C1:C2", dv[2].Ranges.Single().RangeAddress.ToString()); + Assert.AreEqual("G3:G4", dv[3].Ranges.Single().RangeAddress.ToString()); + } + } + + [Test] + public void DataValidationShiftedTruncateRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("DataValidationShift"); + ws.AsRange().SetDataValidation().WholeNumber.Between(0, 1); + var dv = ws.DataValidations.Single(); + + ws.Row(2).InsertRowsAbove(1); + Assert.IsTrue(dv.Ranges.Single().RangeAddress.IsValid); + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", dv.Ranges.Single().RangeAddress.ToString()); + + ws.Column(2).InsertColumnsAfter(1); + Assert.IsTrue(dv.Ranges.Single().RangeAddress.IsValid); + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", dv.Ranges.Single().RangeAddress.ToString()); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs b/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs new file mode 100644 index 0000000..4c82a8d --- /dev/null +++ b/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs @@ -0,0 +1,483 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML_Tests.Excel.DataValidations +{ + [TestFixture] + public class DataValidationTests + { + [Test] + public void Validation_1() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Data Validation Issue"); + IXLCell cell = ws.Cell("E1"); + cell.SetValue("Value 1"); + cell = cell.CellBelow(); + cell.SetValue("Value 2"); + cell = cell.CellBelow(); + cell.SetValue("Value 3"); + cell = cell.CellBelow(); + cell.SetValue("Value 4"); + + ws.Cell("A1").SetValue("Cell below has Validation Only."); + cell = ws.Cell("A2"); + cell.DataValidation.List(ws.Range("$E$1:$E$4")); + + ws.Cell("B1").SetValue("Cell below has Validation with a title."); + cell = ws.Cell("B2"); + cell.DataValidation.List(ws.Range("$E$1:$E$4")); + cell.DataValidation.InputTitle = "Title for B2"; + + Assert.AreEqual(XLAllowedValues.List, cell.DataValidation.AllowedValues); + Assert.AreEqual("'Data Validation Issue'!$E$1:$E$4", cell.DataValidation.Value); + Assert.AreEqual("Title for B2", cell.DataValidation.InputTitle); + + ws.Cell("C1").SetValue("Cell below has Validation with a message."); + cell = ws.Cell("C2"); + cell.DataValidation.List(ws.Range("$E$1:$E$4")); + cell.DataValidation.InputMessage = "Message for C2"; + + Assert.AreEqual(XLAllowedValues.List, cell.DataValidation.AllowedValues); + Assert.AreEqual("'Data Validation Issue'!$E$1:$E$4", cell.DataValidation.Value); + Assert.AreEqual("Message for C2", cell.DataValidation.InputMessage); + + ws.Cell("D1").SetValue("Cell below has Validation with title and message."); + cell = ws.Cell("D2"); + cell.DataValidation.List(ws.Range("$E$1:$E$4")); + cell.DataValidation.InputTitle = "Title for D2"; + cell.DataValidation.InputMessage = "Message for D2"; + + Assert.AreEqual(XLAllowedValues.List, cell.DataValidation.AllowedValues); + Assert.AreEqual("'Data Validation Issue'!$E$1:$E$4", cell.DataValidation.Value); + Assert.AreEqual("Title for D2", cell.DataValidation.InputTitle); + Assert.AreEqual("Message for D2", cell.DataValidation.InputMessage); + } + + [Test] + public void Validation_2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").SetValue("A"); + ws.Cell("B1").SetDataValidation().Custom("Sheet1!A1"); + + IXLWorksheet ws2 = wb.AddWorksheet("Sheet2"); + ws2.Cell("A1").SetValue("B"); + ws.Cell("B1").CopyTo(ws2.Cell("B1")); + + Assert.AreEqual("Sheet1!A1", ws2.Cell("B1").DataValidation.Value); + } + + [Test, Ignore("Wait for proper formula shifting (#686)")] + public void Validation_3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").SetValue("A"); + ws.Cell("B1").SetDataValidation().Custom("A1"); + ws.FirstRow().InsertRowsAbove(1); + + Assert.AreEqual("A2", ws.Cell("B2").DataValidation.Value); + } + + [Test] + public void Validation_4() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").SetValue("A"); + ws.Cell("B1").SetDataValidation().Custom("A1"); + ws.Cell("B1").CopyTo(ws.Cell("B2")); + Assert.AreEqual("A2", ws.Cell("B2").DataValidation.Value); + } + + [Test, Ignore("Wait for proper formula shifting (#686)")] + public void Validation_5() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").SetValue("A"); + ws.Cell("B1").SetDataValidation().Custom("A1"); + ws.FirstColumn().InsertColumnsBefore(1); + + Assert.AreEqual("B1", ws.Cell("C1").DataValidation.Value); + } + + [Test] + public void Validation_6() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").SetValue("A"); + ws.Cell("B1").SetDataValidation().Custom("A1"); + ws.Cell("B1").CopyTo(ws.Cell("C1")); + Assert.AreEqual("B1", ws.Cell("C1").DataValidation.Value); + } + + [Test] + public void Validation_persists_on_Cell_DataValidation() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("People"); + + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("A") + .CellBelow().SetValue("B"); + + IXLTable table = ws.RangeUsed().CreateTable(); + + IXLDataValidation dv = table.DataRange.SetDataValidation(); + dv.ErrorTitle = "Error"; + + Assert.AreEqual("Error", table.DataRange.FirstCell().DataValidation.ErrorTitle); + } + + [Test] + public void Validation_persists_on_Worksheet_DataValidations() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("People"); + + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("A"); + + IXLTable table = ws.RangeUsed().CreateTable(); + + IXLDataValidation dv = table.DataRange.SetDataValidation(); + dv.ErrorTitle = "Error"; + + Assert.AreEqual("Error", ws.DataValidations.Single().ErrorTitle); + } + + [Test] + [TestCase("A1:C3", 5, false, "A1:C3")] + [TestCase("A1:C3", 2, false, "A1:C4")] + [TestCase("A1:C3", 1, false, "A2:C4")] + [TestCase("A1:C3", 5, true, "A1:C3")] + [TestCase("A1:C3", 2, true, "A1:C4")] + [TestCase("A1:C3", 1, true, "A2:C4")] + public void DataValidationShiftedOnRowInsert(string initialAddress, int rowNum, bool setValue, string expectedAddress) + { + //Arrange + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("DataValidation"); + var validation = ws.Range(initialAddress).SetDataValidation(); + validation.WholeNumber.Between(0, 100); + if (setValue) + ws.Range(initialAddress).Value = 50; + + //Act + ws.Row(rowNum).InsertRowsAbove(1); + + //Assert + Assert.AreEqual(1, ws.DataValidations.Count()); + Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count()); + Assert.AreEqual(expectedAddress, ws.DataValidations.First().Ranges.First().RangeAddress.ToString()); + } + + [Test] + [TestCase("A1:C3", 5, false, "A1:C3")] + [TestCase("A1:C3", 2, false, "A1:D3")] + [TestCase("A1:C3", 1, false, "B1:D3")] + [TestCase("A1:C3", 5, true, "A1:C3")] + [TestCase("A1:C3", 2, true, "A1:D3")] + [TestCase("A1:C3", 1, true, "B1:D3")] + public void DataValidationShiftedOnColumnInsert(string initialAddress, int columnNum, bool setValue, string expectedAddress) + { + //Arrange + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("DataValidation"); + var validation = ws.Range(initialAddress).SetDataValidation(); + validation.WholeNumber.Between(0, 100); + if (setValue) + ws.Range(initialAddress).Value = 50; + + //Act + ws.Column(columnNum).InsertColumnsBefore(1); + + //Assert + Assert.AreEqual(1, ws.DataValidations.Count()); + Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count()); + Assert.AreEqual(expectedAddress, ws.DataValidations.First().Ranges.First().RangeAddress.ToString()); + } + + [Test] + public void DataValidationClearSplitsRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("DataValidation"); + var validation = ws.Range("A1:C3").SetDataValidation(); + validation.WholeNumber.Between(0, 100); + + //Act + ws.Cell("B2").Clear(XLClearOptions.DataValidation); + + //Assert + Assert.IsFalse(ws.Cell("B2").HasDataValidation); + Assert.IsTrue(ws.Range("A1:C3").Cells().Where(c => c.Address.ToString() != "B2").All(c => c.HasDataValidation)); + } + } + + [Test] + public void NewDataValidationSplitsRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("DataValidation"); + var validation = ws.Range("A1:C3").SetDataValidation(); + validation.WholeNumber.Between(10, 100); + + //Act + ws.Cell("B2").NewDataValidation.WholeNumber.Between(-100, -0); + + //Assert + Assert.AreEqual("-100", ws.Cell("B2").DataValidation.MinValue); + Assert.IsTrue(ws.Range("A1:C3").Cells().Where(c => c.Address.ToString() != "B2").All(c => c.HasDataValidation)); + Assert.IsTrue(ws.Range("A1:C3").Cells().Where(c => c.Address.ToString() != "B2") + .All(c => c.DataValidation.MinValue == "10")); + } + } + + [Test] + public void ListLengthOverflow() + { + var values = string.Join(",", Enumerable.Range(1, 20) + .Select(i => Guid.NewGuid().ToString("N"))); + + Assert.True(values.Length > 255); + + using (var wb = new XLWorkbook()) + { + var dv = wb.AddWorksheet("Sheet 1").Cell(1, 1).DataValidation; + + Assert.Throws(() => dv.List(values)); + Assert.Throws(() => + { + dv.TextLength.Between(0, 5); + dv.MinValue = values; + }); + + Assert.Throws(() => + { + dv.TextLength.Between(0, 5); + dv.MaxValue = values; + }); + } + } + + [Test] + public void CannotCreateDataValidationWithoutRange() + { + Assert.Throws(() => new XLDataValidation(null)); + } + + [Test] + public void DataValidationHasWorksheetAndRangesWhenCreated() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range = ws.Range("A1:A3"); + + var dv = new XLDataValidation(range); + + Assert.AreSame(ws, dv.Worksheet); + Assert.AreSame(range, dv.Ranges.Single()); + } + } + + [Test] + public void CanAddRangeFromSameWorksheet() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var ranges3 = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + + dv.AddRange(range2); + dv.AddRanges(ranges3); + + Assert.IsTrue(dv.Ranges.Any(r => r == range1)); + Assert.IsTrue(dv.Ranges.Any(r => r == range2)); + Assert.IsTrue(dv.Ranges.Any(r => r == ranges3.First())); + Assert.IsTrue(dv.Ranges.Any(r => r == ranges3.Last())); + } + } + + [Test] + public void CanAddRangeFromAnotherWorksheet() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + var ws2 = wb.AddWorksheet(); + var range1 = ws1.Range("A1:A3"); + var range2 = ws2.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + dv.AddRange(range2); + + Assert.IsTrue(dv.Ranges.Any(r => r != range2 && r.RangeAddress.ToString() == range2.RangeAddress.ToString())); + } + } + + [Test] + public void CanClearRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var ranges3 = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + dv.AddRanges(ranges3); + + dv.ClearRanges(); + + Assert.IsEmpty(dv.Ranges); + } + } + + [Test] + public void CanRemoveExistingRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + + dv.RemoveRange(range1); + + Assert.AreSame(range2, dv.Ranges.Single()); + } + } + + [Test] + public void RemovingExistingRangeDoesNoFail() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + + var dv = new XLDataValidation(range1); + + dv.RemoveRange(range2); + dv.RemoveRange(null); + + Assert.AreSame(range1, dv.Ranges.Single()); + } + } + + [Test] + public void AddRangeFiresEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + IXLRange addedRange = null; + + dv.RangeAdded += (s, e) => addedRange = e.Range; + + dv.AddRange(range2); + + Assert.AreSame(range2, addedRange); + } + } + + [Test] + public void AddRangesFiresMultipleEvents() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var ranges = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + + var addedRanges = new List(); + + dv.RangeAdded += (s, e) => addedRanges.Add(e.Range); + + dv.AddRanges(ranges); + + Assert.AreEqual(2, addedRanges.Count); + } + } + + [Test] + public void RemoveRangeFiresEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + IXLRange removedRange = null; + dv.RangeRemoved += (s, e) => removedRange = e.Range; + + dv.RemoveRange(range2); + + Assert.AreSame(range2, removedRange); + } + } + + [Test] + public void RemoveNonExistingRangeDoesNotFireEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + dv.RangeRemoved += (s, e) => Assert.Fail("Expected not to fire event"); + + dv.RemoveRange(range2); + } + } + + [Test] + public void ClearRangesFiresMultipleEvents() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + + var removedRanges = new List(); + + dv.RangeRemoved += (s, e) => removedRanges.Add(e.Range); + + dv.ClearRanges(); + + Assert.AreEqual(2, removedRanges.Count); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs b/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs new file mode 100644 index 0000000..70757b0 --- /dev/null +++ b/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.DataValidations +{ + public class XLDataValidationsTests + { + [Test] + public void CannotCreateWithoutWorksheet() + { + Assert.Throws(() => new XLDataValidations(null)); + } + + [Test] + public void AddedRangesAreTransferredToTargetSheet() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + var ws2 = wb.AddWorksheet(); + + var dv1 = ws1.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + + var dv2 = ws2.DataValidations.Add(dv1); + + Assert.AreEqual(1, ws1.DataValidations.Count()); + Assert.AreEqual(1, ws2.DataValidations.Count()); + + Assert.AreNotSame(dv1, dv2); + + Assert.AreSame(ws1, dv1.Ranges.Single().Worksheet); + Assert.AreSame(ws2, dv2.Ranges.Single().Worksheet); + } + } + + [TestCase("A1:A1", true)] + [TestCase("A1:A3", true)] + [TestCase("A1:A4", false)] + [TestCase("C2:C2", true)] + [TestCase("C1:C3", true)] + [TestCase("A1:C3", false)] + public void CanFindDataValidationForRange(string searchAddress, bool expectedResult) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv = ws.Range("A1:A3").SetDataValidation(); + dv.MinValue = "100"; + dv.AddRange(ws.Range("C1:C3")); + + var address = new XLRangeAddress(ws as XLWorksheet, searchAddress); + + var actualResult = ws.DataValidations.TryGet(address, out var foundDv); + Assert.AreEqual(expectedResult, actualResult); + if (expectedResult) + Assert.AreSame(dv, foundDv); + else + Assert.IsNull(foundDv); + } + } + + + [TestCase("A1:A1", 1)] + [TestCase("A1:A3", 1)] + [TestCase("B1:B4", 0)] + [TestCase("A1:C3", 1)] + [TestCase("C2:C3", 1)] + [TestCase("C2:G6", 2)] + [TestCase("E2:E3", 0)] + public void CanGetAllDataValidationsForRange(string searchAddress, int expectedCount) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + dv1.AddRange(ws.Range("C1:C3")); + + var dv2 = ws.Range("E4:G6").SetDataValidation(); + dv2.MinValue = "200"; + + var address = new XLRangeAddress(ws as XLWorksheet, searchAddress); + + var actualResult = ws.DataValidations.GetAllInRange(address); + + Assert.AreEqual(expectedCount, actualResult.Count()); + } + } + + [Test] + public void AddDataValidationSplitsExistingRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Ranges("B2:G7,C11:C13").SetDataValidation(); + dv1.MinValue = "100"; + + var dv2 = ws.Range("E4:G6").SetDataValidation(); + dv2.MinValue = "100"; + + Assert.AreEqual(4, dv1.Ranges.Count()); + Assert.AreEqual("B2:G3,B4:D6,B7:G7,C11:C13", + string.Join(",", dv1.Ranges.Select(r => r.RangeAddress.ToString()))); + } + } + + [Test] + public void RemovedRangeExcludedFromIndex() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv = ws.Range("A1:A3").SetDataValidation(); + dv.MinValue = "100"; + var range = ws.Range("C1:C3"); + dv.AddRange(range); + + dv.RemoveRange(range); + + var actualResult = ws.DataValidations.TryGet(range.RangeAddress, out var foundDv); + Assert.IsFalse(actualResult); + Assert.IsNull(foundDv); + } + } + + [Test] + public void ConsolidatedDataValidationsAreUnsubscribed() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + var dv2 = ws.Range("B1:B3").SetDataValidation(); + dv2.MinValue = "100"; + + (ws.DataValidations as XLDataValidations).Consolidate(); + dv1.AddRange(ws.Range("C1:C3")); + dv2.AddRange(ws.Range("D1:D3")); + + var consolidatedDv = ws.DataValidations.Single(); + Assert.AreSame(dv1, consolidatedDv); + Assert.True(ws.Cell("C1").HasDataValidation); + Assert.False(ws.Cell("D1").HasDataValidation); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Globalization/GlobalizationTests.cs b/ClosedXML_Tests/Excel/Globalization/GlobalizationTests.cs new file mode 100644 index 0000000..5cbf5a5 --- /dev/null +++ b/ClosedXML_Tests/Excel/Globalization/GlobalizationTests.cs @@ -0,0 +1,46 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.IO; +using System.Threading; + +namespace ClosedXML_Tests.Excel.Globalization +{ + [TestFixture] + public class GlobalizationTests + { + [Test] + [TestCase("A1*10", 1230d)] + [TestCase("A1/10", 12.3)] + [TestCase("A1&\" cells\"", "123 cells")] + [TestCase("A1&\"000\"", "123000")] + [TestCase("ISNUMBER(A1)", true)] + [TestCase("ISBLANK(A1)", false)] + [TestCase("DATE(2018,1,28)", 43128d)] + public void LoadFormulaCachedValue(string formula, object expectedValue) + { + Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("ru-RU"); + + using (var ms = new MemoryStream()) + { + using (XLWorkbook book1 = new XLWorkbook()) + { + var sheet = book1.AddWorksheet("sheet1"); + sheet.Cell("A1").Value = 123; + sheet.Cell("A2").FormulaA1 = formula; + var options = new SaveOptions { EvaluateFormulasBeforeSaving = true }; + + book1.SaveAs(ms, options); + } + ms.Position = 0; + + using (XLWorkbook book2 = new XLWorkbook(ms)) + { + var ws = book2.Worksheet(1); + var storedValueA2 = ws.Cell("A2").CachedValue; + Assert.IsFalse(ws.Cell("A2").NeedsRecalculation); + Assert.AreEqual(expectedValue, storedValueA2); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs b/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs new file mode 100644 index 0000000..cf56d83 --- /dev/null +++ b/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs @@ -0,0 +1,471 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using NUnit.Framework; +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class PictureTests + { + [Test] + public void CanAddPictureFromBitmap() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)).GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + using (var bitmap = Bitmap.FromStream(resourceStream) as Bitmap) + { + var picture = ws.AddPicture(bitmap, "MyPicture") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50) + .WithSize(200, 200); + + Assert.AreEqual(XLPictureFormat.Jpeg, picture.Format); + Assert.AreEqual(200, picture.Width); + Assert.AreEqual(200, picture.Height); + } + } + } + + [Test] + public void CanAddPictureFromStream() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)).GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + { + var picture = ws.AddPicture(resourceStream, "MyPicture") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50) + .WithSize(200, 200); + + Assert.AreEqual(XLPictureFormat.Jpeg, picture.Format); + Assert.AreEqual(200, picture.Width); + Assert.AreEqual(200, picture.Height); + } + } + } + + [Test] + public void CanAddPictureFromFile() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var path = Path.ChangeExtension(Path.GetTempFileName(), "jpg"); + + try + { + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)).GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + using (var fileStream = File.Create(path)) + { + resourceStream.Seek(0, SeekOrigin.Begin); + resourceStream.CopyTo(fileStream); + fileStream.Close(); + } + + var picture = ws.AddPicture(path) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50); + + Assert.AreEqual(XLPictureFormat.Jpeg, picture.Format); + Assert.AreEqual(400, picture.Width); + Assert.AreEqual(400, picture.Height); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } + } + + [Test] + public void CanAddPictureConcurrentlyFromFile() + { + var path = Path.ChangeExtension(Path.GetTempFileName(), "jpg"); + + try + { + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)).GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + using (var fileStream = File.Create(path)) + { + resourceStream.Seek(0, SeekOrigin.Begin); + resourceStream.CopyTo(fileStream); + fileStream.Close(); + } + + Parallel.Invoke(() => verifyAddImageFromFile(path), () => verifyAddImageFromFile(path)); + } + finally + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + + private void verifyAddImageFromFile(string filePath) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var picture = ws.AddPicture(filePath) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50); + + Assert.AreEqual(XLPictureFormat.Jpeg, picture.Format); + Assert.AreEqual(400, picture.Width); + Assert.AreEqual(50, picture.Top); + } + } + + + [Test] + public void CanScaleImage() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + var pic = ws.AddPicture(resourceStream, "MyPicture") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50); + + Assert.AreEqual(252, pic.OriginalWidth); + Assert.AreEqual(152, pic.OriginalHeight); + Assert.AreEqual(252, pic.Width); + Assert.AreEqual(152, pic.Height); + + pic.ScaleHeight(0.7); + pic.ScaleWidth(1.2); + + Assert.AreEqual(252, pic.OriginalWidth); + Assert.AreEqual(152, pic.OriginalHeight); + Assert.AreEqual(302, pic.Width); + Assert.AreEqual(106, pic.Height); + + pic.ScaleHeight(0.7); + pic.ScaleWidth(1.2); + + Assert.AreEqual(252, pic.OriginalWidth); + Assert.AreEqual(152, pic.OriginalHeight); + Assert.AreEqual(362, pic.Width); + Assert.AreEqual(74, pic.Height); + + pic.ScaleHeight(0.8, true); + pic.ScaleWidth(1.1, true); + + Assert.AreEqual(252, pic.OriginalWidth); + Assert.AreEqual(152, pic.OriginalHeight); + Assert.AreEqual(277, pic.Width); + Assert.AreEqual(122, pic.Height); + } + } + } + + [Test] + public void TestDefaultPictureNames() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png).Name = "Picture 4"; + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + } + + Assert.AreEqual("Picture 1", ws.Pictures.Skip(0).First().Name); + Assert.AreEqual("Picture 2", ws.Pictures.Skip(1).First().Name); + Assert.AreEqual("Picture 4", ws.Pictures.Skip(2).First().Name); + Assert.AreEqual("Picture 5", ws.Pictures.Skip(3).First().Name); + } + } + + [Test] + public void TestDefaultIds() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png).Name = "Picture 4"; + stream.Position = 0; + + ws.AddPicture(stream, XLPictureFormat.Png); + stream.Position = 0; + } + + Assert.AreEqual(1, ws.Pictures.Skip(0).First().Id); + Assert.AreEqual(2, ws.Pictures.Skip(1).First().Id); + Assert.AreEqual(3, ws.Pictures.Skip(2).First().Id); + Assert.AreEqual(4, ws.Pictures.Skip(3).First().Id); + } + } + + [Test] + public void XLMarkerTests() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + XLMarker firstMarker = new XLMarker(ws.Cell(1, 10), new Point(100, 0)); + + Assert.AreEqual(10, firstMarker.ColumnNumber); + Assert.AreEqual(1, firstMarker.RowNumber); + Assert.AreEqual(100, firstMarker.Offset.X); + Assert.AreEqual(0, firstMarker.Offset.Y); + } + + [Test] + public void XLPictureTests() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + var pic = ws.AddPicture(stream, XLPictureFormat.Png, "Image1") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(220, 155); + + Assert.AreEqual(XLPicturePlacement.FreeFloating, pic.Placement); + Assert.AreEqual("Image1", pic.Name); + Assert.AreEqual(XLPictureFormat.Png, pic.Format); + Assert.AreEqual(252, pic.OriginalWidth); + Assert.AreEqual(152, pic.OriginalHeight); + Assert.AreEqual(252, pic.Width); + Assert.AreEqual(152, pic.Height); + Assert.AreEqual(220, pic.Left); + Assert.AreEqual(155, pic.Top); + } + } + } + + [Test] + public void CanLoadFileWithImagesAndCopyImagesToNewSheet() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(2, ws.Pictures.Count); + + var copy = ws.CopyTo("NewSheet"); + Assert.AreEqual(2, copy.Pictures.Count); + } + } + + [Test] + public void CanDeletePictures() + { + using (var ms = new MemoryStream()) + { + int originalCount; + + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + originalCount = ws.Pictures.Count; + ws.Pictures.Delete(ws.Pictures.First()); + + var pictureName = ws.Pictures.First().Name; + ws.Pictures.Delete(pictureName); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(originalCount - 2, ws.Pictures.Count); + } + } + } + + [Test] + public void PictureRenameTests() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("Images3"); + var picture = ws.Pictures.First(); + Assert.AreEqual("Picture 1", picture.Name); + + picture.Name = "picture 1"; + picture.Name = "pICture 1"; + picture.Name = "Picture 1"; + + picture = ws.Pictures.Last(); + picture.Name = "new name"; + + Assert.Throws(() => picture.Name = "Picture 1"); + Assert.Throws(() => picture.Name = "picTURE 1"); + } + } + + [Test] + public void HandleDuplicatePictureIdsAcrossWorksheets() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + var ws2 = wb.AddWorksheet("Sheet2"); + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + (ws1 as XLWorksheet).AddPicture(stream, "Picture 1", 2); + (ws1 as XLWorksheet).AddPicture(stream, "Picture 2", 3); + + //Internal method - used for loading files + var pic = (ws2 as XLWorksheet).AddPicture(stream, "Picture 1", 2) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(220, 155) as XLPicture; + + var id = pic.Id; + + pic.Id = id; + Assert.AreEqual(id, pic.Id); + + pic.Id = 3; + Assert.AreEqual(3, pic.Id); + + pic.Id = id; + + var pic2 = (ws2 as XLWorksheet).AddPicture(stream, "Picture 2", 3) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(440, 300) as XLPicture; + } + } + } + + [Test] + public void CopyImageSameWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + + IXLPicture original; + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + original = (ws1 as XLWorksheet).AddPicture(stream, "Picture 1", 2) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(220, 155) as XLPicture; + } + + var copy = original.Duplicate() + .MoveTo(300, 200) as XLPicture; + + Assert.AreEqual(2, ws1.Pictures.Count()); + Assert.AreEqual(ws1, copy.Worksheet); + Assert.AreEqual(original.Format, copy.Format); + Assert.AreEqual(original.Height, copy.Height); + Assert.AreEqual(original.Placement, copy.Placement); + Assert.AreEqual(original.TopLeftCell.ToString(), copy.TopLeftCell.ToString()); + Assert.AreEqual(original.Width, copy.Width); + Assert.AreEqual(original.ImageStream.ToArray(), copy.ImageStream.ToArray(), "Image streams differ"); + + Assert.AreEqual(200, copy.Top); + Assert.AreEqual(300, copy.Left); + Assert.AreNotEqual(original.Id, copy.Id); + Assert.AreNotEqual(original.Name, copy.Name); + } + + [Test] + public void CopyImageDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + IXLPicture original; + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + original = (ws1 as XLWorksheet).AddPicture(stream, "Picture 1", 2) + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(220, 155) as XLPicture; + } + var ws2 = wb.Worksheets.Add("Sheet2"); + + var copy = original.CopyTo(ws2); + + Assert.AreEqual(1, ws1.Pictures.Count()); + Assert.AreEqual(1, ws2.Pictures.Count()); + + Assert.AreEqual(ws2, copy.Worksheet); + + Assert.AreEqual(original.Format, copy.Format); + Assert.AreEqual(original.Height, copy.Height); + Assert.AreEqual(original.Left, copy.Left); + Assert.AreEqual(original.Name, copy.Name); + Assert.AreEqual(original.Placement, copy.Placement); + Assert.AreEqual(original.Top, copy.Top); + Assert.AreEqual(original.TopLeftCell.ToString(), copy.TopLeftCell.ToString()); + Assert.AreEqual(original.Width, copy.Width); + Assert.AreEqual(original.ImageStream.ToArray(), copy.ImageStream.ToArray(), "Image streams differ"); + + Assert.AreNotEqual(original.Id, copy.Id); + } + + [Test] + public void PictureShiftsWhenInsertingRows() + { + using (var wb = new XLWorkbook()) + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ClosedXML_Tests.Resource.Images.ImageHandling.png")) + { + var ws = wb.Worksheets.Add("ImageShift"); + var picture = ws.AddPicture(stream, XLPictureFormat.Png, "PngImage") + .MoveTo(ws.Cell(5, 2)) + .WithPlacement(XLPicturePlacement.Move); + + ws.Row(2).InsertRowsBelow(20); + + Assert.AreEqual(25, picture.TopLeftCell.Address.RowNumber); + } + } + + [Test] + public void PictureNotFound() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + Assert.Throws(() => ws.Picture("dummy")); + Assert.Throws(() => ws.Pictures.Delete("dummy")); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Loading/LoadingTests.cs b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs new file mode 100644 index 0000000..21e4e58 --- /dev/null +++ b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs @@ -0,0 +1,546 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using ClosedXML_Tests.Utils; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + // Tests in this fixture test only the successful loading of existing Excel files, + // i.e. we test that ClosedXML doesn't choke on a given input file + // These tests DO NOT test that ClosedXML successfully recognises all the Excel parts or that it can successfully save those parts again. + [TestFixture] + public class LoadingTests + { + private static IEnumerable TryToLoad => + TestHelper.ListResourceFiles(s => + s.Contains(".TryToLoad.") && + !s.Contains(".LO.")); + + [TestCaseSource(nameof(TryToLoad))] + public void CanSuccessfullyLoadFiles(string file) + { + TestHelper.LoadFile(file); + } + + [TestCaseSource(nameof(LOFiles))] + public void CanSuccessfullyLoadLOFiles(string file) + { + TestHelper.LoadFile(file); + } + + private static IEnumerable LOFiles + { + get + { + // TODO: unpark all files + var parkedForLater = new[] + { + "TryToLoad.LO.xlsx.formats.xlsx", + "TryToLoad.LO.xlsx.pivot_table.shared-group-field.xlsx", + "TryToLoad.LO.xlsx.pivot_table.shared-nested-dategroup.xlsx", + "TryToLoad.LO.xlsx.pivottable_bool_field_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_date_field_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_double_field_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_duplicated_member_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_rowcolpage_field_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_string_field_filter.xlsx", + "TryToLoad.LO.xlsx.pivottable_tabular_mode.xlsx", + "TryToLoad.LO.xlsx.pivot_table_first_header_row.xlsx", + "TryToLoad.LO.xlsx.tdf100709.xlsx", + "TryToLoad.LO.xlsx.tdf89139_pivot_table.xlsx", + "TryToLoad.LO.xlsx.universal-content-strict.xlsx", + "TryToLoad.LO.xlsx.universal-content.xlsx", + "TryToLoad.LO.xlsx.xf_default_values.xlsx", + "TryToLoad.LO.xlsm.pass.CVE-2016-0122-1.xlsm", + "TryToLoad.LO.xlsm.tdf111974.xlsm", + "TryToLoad.LO.xlsm.vba-user-function.xlsm", + }; + + return TestHelper.ListResourceFiles(s => s.Contains(".LO.") && !parkedForLater.Any(i => s.Contains(i))); + } + } + + [Test] + public void CanLoadAndManipulateFileWithEmptyTable() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\EmptyTable.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + var table = ws.Tables.First(); + table.DataRange.InsertRowsBelow(5); + } + } + + [Test] + public void CanLoadDate1904SystemCorrectly() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\Date1904System.xlsx"))) + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + var c = ws.Cell("A2"); + Assert.AreEqual(XLDataType.DateTime, c.DataType); + Assert.AreEqual(new DateTime(2017, 10, 27, 21, 0, 0), c.GetDateTime()); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var c = ws.Cell("A2"); + Assert.AreEqual(XLDataType.DateTime, c.DataType); + Assert.AreEqual(new DateTime(2017, 10, 27, 21, 0, 0), c.GetDateTime()); + wb.SaveAs(ms); + } + } + } + + [Test] + public void CanLoadAndSaveFileWithMismatchingSheetIdAndRelId() + { + // This file's workbook.xml contains: + // + // and the mismatch between the sheetId and r:id can create problems. + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\FileWithMismatchSheetIdAndRelId.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + } + } + } + + [Test] + public void CanLoadBasicPivotTable() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\LoadPivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("PivotTable1"); + var pt = ws.PivotTable("PivotTable1"); + Assert.AreEqual("PivotTable1", pt.Name); + + Assert.AreEqual(1, pt.RowLabels.Count()); + Assert.AreEqual("Name", pt.RowLabels.Single().SourceName); + + Assert.AreEqual(1, pt.ColumnLabels.Count()); + Assert.AreEqual("Month", pt.ColumnLabels.Single().SourceName); + + var pv = pt.Values.Single(); + Assert.AreEqual("Sum of NumberOfOrders", pv.CustomName); + Assert.AreEqual("NumberOfOrders", pv.SourceName); + } + } + + [Test] + public void CanLoadOrderedPivotTable() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\LoadPivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("OrderedPivotTable"); + var pt = ws.PivotTable("OrderedPivotTable"); + + Assert.AreEqual(XLPivotSortType.Ascending, pt.RowLabels.Single().SortType); + Assert.AreEqual(XLPivotSortType.Descending, pt.ColumnLabels.Single().SortType); + } + } + + [Test] + public void CanLoadPivotTableSubtotals() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\LoadPivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("PivotTableSubtotals"); + var pt = ws.PivotTable("PivotTableSubtotals"); + + var subtotals = pt.RowLabels.Get("Group").Subtotals.ToArray(); + Assert.AreEqual(3, subtotals.Length); + Assert.AreEqual(XLSubtotalFunction.Average, subtotals[0]); + Assert.AreEqual(XLSubtotalFunction.Count, subtotals[1]); + Assert.AreEqual(XLSubtotalFunction.Sum, subtotals[2]); + } + } + + /// + /// For non-English locales, the default style ("Normal" in English) can be + /// another piece of text (e.g. ??????? in Russian). + /// This test ensures that the default style is correctly detected and + /// no style conflicts occur on save. + /// + [Test] + public void CanSaveFileWithDefaultStyleNameNotInEnglish() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\FileWithDefaultStyleNameNotInEnglish.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + } + } + } + + /// + /// As per https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.cellvalues(v=office.15).aspx + /// the 'Date' DataType is available only in files saved with Microsoft Office + /// In other files, the data type will be saved as numeric + /// ClosedXML then deduces the data type by inspecting the number format string + /// + [Test] + public void CanLoadLibreOfficeFileWithDates() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\LibreOfficeFileWithDates.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + foreach (var cell in ws.CellsUsed()) + { + Assert.AreEqual(XLDataType.DateTime, cell.DataType); + } + } + } + + [Test] + public void CanLoadFileWithImagesWithCorrectAnchorTypes() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(2, ws.Pictures.Count); + Assert.AreEqual(XLPicturePlacement.FreeFloating, ws.Pictures.First().Placement); + Assert.AreEqual(XLPicturePlacement.Move, ws.Pictures.Skip(1).First().Placement); + + var ws2 = wb.Worksheets.Skip(1).First(); + Assert.AreEqual(1, ws2.Pictures.Count); + Assert.AreEqual(XLPicturePlacement.MoveAndSize, ws2.Pictures.First().Placement); + } + } + + [Test] + public void CanLoadFileWithImagesWithCorrectImageType() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageFormats.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(1, ws.Pictures.Count); + Assert.AreEqual(XLPictureFormat.Jpeg, ws.Pictures.First().Format); + + var ws2 = wb.Worksheets.Skip(1).First(); + Assert.AreEqual(1, ws2.Pictures.Count); + Assert.AreEqual(XLPictureFormat.Png, ws2.Pictures.First().Format); + } + } + + [Test] + public void CanLoadAndDeduceAnchorsFromExcelGeneratedFile() + { + // This file was produced by Excel. It contains 3 images, but the latter 2 were copied from the first. + // There is actually only 1 embedded image if you inspect the file's internals. + // Additionally, Excel saves all image anchors as TwoCellAnchor, but uses the EditAs attribute to distinguish the types + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\ExcelProducedWorkbookWithImages.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual(3, ws.Pictures.Count); + + Assert.AreEqual(XLPicturePlacement.MoveAndSize, ws.Picture("Picture 1").Placement); + Assert.AreEqual(XLPicturePlacement.Move, ws.Picture("Picture 2").Placement); + Assert.AreEqual(XLPicturePlacement.FreeFloating, ws.Picture("Picture 3").Placement); + + using (var ms = new MemoryStream()) + wb.SaveAs(ms, true); + } + } + + [Test] + public void CanLoadFromTemplate() + { + using (var tf1 = new TemporaryFile()) + using (var tf2 = new TemporaryFile()) + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\AllShapes.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + // Save as temporary file + wb.SaveAs(tf1.Path); + } + + var workbook = XLWorkbook.OpenFromTemplate(tf1.Path); + Assert.True(workbook.Worksheets.Any()); + Assert.Throws(() => workbook.Save()); + + workbook.SaveAs(tf2.Path); + } + } + + /// + /// Excel escapes symbol ' in worksheet title so we have to process this correctly. + /// + [Test] + public void CanOpenWorksheetWithEscapedApostrophe() + { + string title = ""; + TestDelegate openWorkbook = () => + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\EscapedApostrophe.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + title = ws.Name; + } + }; + + Assert.DoesNotThrow(openWorkbook); + Assert.AreEqual("L'E", title); + } + + [Test] + public void CanRoundTripSheetProtectionForObjects() + { + using (var book = new XLWorkbook()) + { + var sheet = book.AddWorksheet("TestSheet"); + sheet.Protect() + .AllowElement(XLSheetProtectionElements.EditObjects | XLSheetProtectionElements.EditScenarios); + + Assert.AreEqual(XLSheetProtectionElements.SelectEverything | XLSheetProtectionElements.EditObjects | XLSheetProtectionElements.EditScenarios, sheet.Protection.AllowedElements); + + using (var xlStream = new MemoryStream()) + { + book.SaveAs(xlStream); + + using (var persistedBook = new XLWorkbook(xlStream)) + { + var persistedSheet = persistedBook.Worksheets.Worksheet(1); + + Assert.AreEqual(sheet.Protection.AllowedElements, persistedSheet.Protection.AllowedElements); + } + } + } + } + + [Test] + [TestCase("A1*10", 1230)] + [TestCase("A1/10", 12.3)] + [TestCase("A1&\" cells\"", "123 cells")] + [TestCase("A1&\"000\"", "123000")] + [TestCase("ISNUMBER(A1)", true)] + [TestCase("ISBLANK(A1)", false)] + [TestCase("DATE(2018,1,28)", 43128)] + public void LoadFormulaCachedValue(string formula, object expectedCachedValue) + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook book1 = new XLWorkbook()) + { + var sheet = book1.AddWorksheet("sheet1"); + sheet.Cell("A1").Value = 123; + sheet.Cell("A2").FormulaA1 = formula; + var options = new SaveOptions { EvaluateFormulasBeforeSaving = true }; + + book1.SaveAs(ms, options); + } + ms.Position = 0; + + using (XLWorkbook book2 = new XLWorkbook(ms)) + { + var ws = book2.Worksheet(1); + Assert.IsFalse(ws.Cell("A2").NeedsRecalculation); + Assert.AreEqual(expectedCachedValue, ws.Cell("A2").CachedValue); + } + } + } + + [Test] + public void LoadingOptions() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\ExternalLinks\WorkbookWithExternalLink.xlsx"))) + { + Assert.DoesNotThrow(() => new XLWorkbook(stream, new LoadOptions { RecalculateAllFormulas = false })); + Assert.Throws(() => new XLWorkbook(stream, new LoadOptions { RecalculateAllFormulas = true })); + + Assert.AreEqual(XLEventTracking.Disabled, new XLWorkbook(stream, new LoadOptions { EventTracking = XLEventTracking.Disabled }).EventTracking); + Assert.AreEqual(XLEventTracking.Enabled, new XLWorkbook(stream, new LoadOptions { EventTracking = XLEventTracking.Enabled }).EventTracking); + } + } + + [Test] + public void CanLoadWorksheetStyle() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\BaseColumnWidth.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + Assert.AreEqual(8, ws.Style.Font.FontSize); + Assert.AreEqual("Arial", ws.Style.Font.FontName); + Assert.AreEqual(8, ws.Cell("A1").Style.Font.FontSize); + Assert.AreEqual("Arial", ws.Cell("A1").Style.Font.FontName); + } + } + + [Test] + public void CanCorrectLoadWorkbookCellWithStringDataType() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\CellWithStringDataType.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var cellToCheck = wb.Worksheet(1).Cell("B2"); + Assert.AreEqual(XLDataType.Text, cellToCheck.DataType); + Assert.AreEqual("String with String Data type", cellToCheck.Value); + } + } + + [Test] + public void CanLoadFileWithInvalidSelectedRanges() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\SelectedRanges\InvalidSelectedRange.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + Assert.AreEqual(2, ws.SelectedRanges.Count); + Assert.AreEqual("B2:B2", ws.SelectedRanges.First().RangeAddress.ToString()); + Assert.AreEqual("B2:C2", ws.SelectedRanges.Last().RangeAddress.ToString()); + } + } + + [Test] + public void CanLoadCellsWithoutReferencesCorrectly() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\LO\xlsx\row-index-1-based.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + Assert.AreEqual("Page 1", ws.Name); + + var expected = new Dictionary() + { + ["A1"] = "Action Plan.Name", + ["B1"] = "Action Plan.Description", + ["A2"] = "Jerry", + ["B2"] = "This is a longer Text.\nSecond line.\nThird line.", + ["A3"] = "", + ["B3"] = "" + }; + + foreach (var pair in expected) + Assert.AreEqual(pair.Value, ws.Cell(pair.Key).GetString(), pair.Key); + } + } + + [Test] + public void CorrectlyLoadMergedCellsBorder() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\StyleReferenceFiles\MergedCellsBorder\inputfile.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + var c = ws.Cell("B2"); + Assert.AreEqual(XLColorType.Theme, c.Style.Border.TopBorderColor.ColorType); + Assert.AreEqual(XLThemeColor.Accent1, c.Style.Border.TopBorderColor.ThemeColor); + Assert.AreEqual(0.39994506668294322d, c.Style.Border.TopBorderColor.ThemeTint, XLHelper.Epsilon); + } + } + + [Test] + public void CorrectlyLoadDefaultRowAndColumnStyles() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\StyleReferenceFiles\RowAndColumnStyles\inputfile.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + Assert.AreEqual(8, ws.Row(1).Style.Font.FontSize); + Assert.AreEqual(8, ws.Row(2).Style.Font.FontSize); + Assert.AreEqual(8, ws.Column("A").Style.Font.FontSize); + } + } + + [Test] + public void EmptyNumberFormatIdTreatedAsGeneral() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\EmptyNumberFormatId.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet(1); + + Assert.AreEqual(XLPredefinedFormat.General, ws.Cell("A2").Style.NumberFormat.NumberFormatId); + } + } + + [Test] + public void CanLoadProperties() + { + const string author = "TestAuthor"; + const string title = "TestTitle"; + const string subject = "TestSubject"; + const string category = "TestCategory"; + const string keywords = "TestKeywords"; + const string comments = "TestComments"; + const string status = "TestStatus"; + var created = new DateTime(2019, 10, 19, 20, 42, 30); + var modified = new DateTime(2020, 11, 20, 09, 51, 20); + const string lastModifiedBy = "TestLastModifiedBy"; + const string company = "TestCompany"; + const string manager = "TestManager"; + + using (var stream = new MemoryStream()) + { + using (var wb = new XLWorkbook ()) + { + var sheet = wb.AddWorksheet("sheet1"); + + wb.Properties.Author = author; + wb.Properties.Title = title; + wb.Properties.Subject = subject; + wb.Properties.Category = category; + wb.Properties.Keywords = keywords; + wb.Properties.Comments = comments; + wb.Properties.Status = status; + wb.Properties.Created = created; + wb.Properties.Modified = modified; + wb.Properties.LastModifiedBy = lastModifiedBy; + wb.Properties.Company = company; + wb.Properties.Manager = manager; + + wb.SaveAs (stream, true); + } + + stream.Position = 0; + + using (var wb = new XLWorkbook(stream)) + { + Assert.AreEqual (author, wb.Properties.Author); + Assert.AreEqual (title, wb.Properties.Title); + Assert.AreEqual (subject, wb.Properties.Subject); + Assert.AreEqual (category, wb.Properties.Category); + Assert.AreEqual (keywords, wb.Properties.Keywords); + Assert.AreEqual (comments, wb.Properties.Comments); + Assert.AreEqual (status, wb.Properties.Status); + Assert.AreEqual (created, wb.Properties.Created); + Assert.AreEqual (modified, wb.Properties.Modified); + Assert.AreEqual (lastModifiedBy, wb.Properties.LastModifiedBy); + Assert.AreEqual (company, wb.Properties.Company); + Assert.AreEqual (manager, wb.Properties.Manager); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/CopyContentsTests.cs b/ClosedXML_Tests/Excel/Misc/CopyContentsTests.cs new file mode 100644 index 0000000..ed49a87 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/CopyContentsTests.cs @@ -0,0 +1,134 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Misc +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class CopyContentsTests + { + private static void CopyRowAsRange(IXLWorksheet originalSheet, int originalRowNumber, IXLWorksheet destSheet, + int destRowNumber) + { + { + IXLRow destinationRow = destSheet.Row(destRowNumber); + destinationRow.Clear(); + + IXLRow originalRow = originalSheet.Row(originalRowNumber); + int columnNumber = originalRow.LastCellUsed(XLCellsUsedOptions.All).Address.ColumnNumber; + + IXLRange originalRange = originalSheet.Range(originalRowNumber, 1, originalRowNumber, columnNumber); + IXLRange destRange = destSheet.Range(destRowNumber, 1, destRowNumber, columnNumber); + originalRange.CopyTo(destRange); + } + } + + [Test] + public void CopyConditionalFormatsCount() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().AddConditionalFormat().WhenContains("1").Fill.SetBackgroundColor(XLColor.Blue); + ws.Cell("A2").Value = ws.FirstCell(); + Assert.AreEqual(2, ws.ConditionalFormats.Count()); + } + + [Test] + public void CopyConditionalFormatsFixedNum() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "1"; + ws.Cell("B1").Value = "1"; + ws.Cell("A1").AddConditionalFormat().WhenEquals(1).Fill.SetBackgroundColor(XLColor.Blue); + ws.Cell("A2").Value = ws.Cell("A1"); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "1" && !v.Value.IsFormula))); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "1" && !v.Value.IsFormula))); + } + + [Test] + public void CopyConditionalFormatsFixedString() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "A"; + ws.Cell("B1").Value = "B"; + ws.Cell("A1").AddConditionalFormat().WhenEquals("A").Fill.SetBackgroundColor(XLColor.Blue); + ws.Cell("A2").Value = ws.Cell("A1"); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "A" && !v.Value.IsFormula))); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "A" && !v.Value.IsFormula))); + } + + [Test] + public void CopyConditionalFormatsFixedStringNum() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "1"; + ws.Cell("B1").Value = "1"; + ws.Cell("A1").AddConditionalFormat().WhenEquals("1").Fill.SetBackgroundColor(XLColor.Blue); + ws.Cell("A2").Value = ws.Cell("A1"); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "1" && !v.Value.IsFormula))); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "1" && !v.Value.IsFormula))); + } + + [Test] + public void CopyConditionalFormatsRelative() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "1"; + ws.Cell("B1").Value = "1"; + ws.Cell("A1").AddConditionalFormat().WhenEquals("=B1").Fill.SetBackgroundColor(XLColor.Blue); + ws.Cell("A2").Value = ws.Cell("A1"); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "B1" && v.Value.IsFormula))); + Assert.IsTrue(ws.ConditionalFormats.Any(cf => cf.Values.Any(v => v.Value.Value == "B2" && v.Value.IsFormula))); + } + + [Test] + public void TestRowCopyContents() + { + var workbook = new XLWorkbook(); + IXLWorksheet originalSheet = workbook.Worksheets.Add("original"); + IXLWorksheet copyRowSheet = workbook.Worksheets.Add("copy row"); + IXLWorksheet copyRowAsRangeSheet = workbook.Worksheets.Add("copy row as range"); + IXLWorksheet copyRangeSheet = workbook.Worksheets.Add("copy range"); + + originalSheet.Cell("A2").SetValue("test value"); + originalSheet.Range("A2:E2").Merge(); + + { + IXLRange originalRange = originalSheet.Range("A2:E2"); + IXLRange destinationRange = copyRangeSheet.Range("A2:E2"); + + originalRange.CopyTo(destinationRange); + } + CopyRowAsRange(originalSheet, 2, copyRowAsRangeSheet, 3); + { + IXLRow originalRow = originalSheet.Row(2); + IXLRow destinationRow = copyRowSheet.Row(2); + copyRowSheet.Cell("G2").Value = "must be removed after copy"; + originalRow.CopyTo(destinationRow); + } + TestHelper.SaveWorkbook(workbook, "Misc", "CopyRowContents.xlsx"); + } + + [Test] + public void UpdateCellsWorksheetTest() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Cell(1, 1).Value = "hello, world."; + + var ws2 = ws1.CopyTo("Sheet2"); + + Assert.AreEqual("Sheet1", ws1.FirstCell().Address.Worksheet.Name); + Assert.AreEqual("Sheet2", ws2.FirstCell().Address.Worksheet.Name); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/ExtensionsTests.cs b/ClosedXML_Tests/Excel/Misc/ExtensionsTests.cs new file mode 100644 index 0000000..52ae72a --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/ExtensionsTests.cs @@ -0,0 +1,87 @@ +using System; +using ClosedXML.Excel; +using NUnit.Framework; +using DocumentFormat.OpenXml; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class ExtensionsTests + { + [Test] + public void FixNewLines() + { + Assert.AreEqual("\n".FixNewLines(), Environment.NewLine); + Assert.AreEqual("\r\n".FixNewLines(), Environment.NewLine); + Assert.AreEqual("\rS\n".FixNewLines(), "\rS" + Environment.NewLine); + Assert.AreEqual("\r\n\n".FixNewLines(), Environment.NewLine + Environment.NewLine); + } + + [Test] + public void DoubleSaveRound() + { + Double value = 1234.1234567; + Assert.AreEqual(value.SaveRound(), Math.Round(value, 6)); + } + + [Test] + public void DoubleValueSaveRound() + { + Double value = 1234.1234567; + Assert.AreEqual(new DoubleValue(value).SaveRound().Value, Math.Round(value, 6)); + } + + [TestCase("NoEscaping", ExpectedResult = "NoEscaping")] + [TestCase("1", ExpectedResult = "'1'")] + [TestCase("AB-CD", ExpectedResult = "'AB-CD'")] + [TestCase(" AB", ExpectedResult = "' AB'")] + [TestCase("Test sheet", ExpectedResult = "'Test sheet'")] + [TestCase("O'Kelly", ExpectedResult = "'O''Kelly'")] + [TestCase("A2+3", ExpectedResult = "'A2+3'")] + [TestCase("A\"B", ExpectedResult = "'A\"B'")] + [TestCase("A!B", ExpectedResult = "'A!B'")] + [TestCase("A~B", ExpectedResult = "'A~B'")] + [TestCase("A^B", ExpectedResult = "'A^B'")] + [TestCase("A&B", ExpectedResult = "'A&B'")] + [TestCase("A>B", ExpectedResult = "'A>B'")] + [TestCase("A ws.Cell("A1").Value, Throws.InstanceOf()); + + ws.Cell("B1").FormulaA1 = "=SUM(Sheet1!A1:UnknownSheet!A50)"; + Assert.That(() => ws.Cell("B1").Value, Throws.InstanceOf()); + } + } + + [Test] + public void DateAgainstStringComparison() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = new DateTime(2016, 1, 1); + ws.Cell("A1").DataType = XLDataType.DateTime; + + ws.Cell("A2").FormulaA1 = @"=IF(A1 = """", ""A"", ""B"")"; + var actual = ws.Cell("A2").Value; + Assert.AreEqual(actual, "B"); + + ws.Cell("A3").FormulaA1 = @"=IF("""" = A1, ""A"", ""B"")"; + actual = ws.Cell("A3").Value; + Assert.AreEqual(actual, "B"); + } + } + + [Test] + public void FormulaThatReferencesEntireRow() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().Value = 1; + ws.FirstCell().CellRight().Value = 2; + ws.FirstCell().CellRight(5).Value = 3; + + ws.FirstCell().CellBelow().FormulaA1 = "=SUM(1:1)"; + + var actual = ws.FirstCell().CellBelow().Value; + Assert.AreEqual(6, actual); + } + } + + [Test] + public void FormulaThatReferencesEntireColumn() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().Value = 1; + ws.FirstCell().CellBelow().Value = 2; + ws.FirstCell().CellBelow(5).Value = 3; + + ws.FirstCell().CellRight().FormulaA1 = "=SUM(A:A)"; + + var actual = ws.FirstCell().CellRight().Value; + Assert.AreEqual(6, actual); + } + } + + [Test] + public void FormulaThatStartsWithEqualsAndPlus() + { + object actual; + actual = XLWorkbook.EvaluateExpr("=MID(\"This is a test\", 6, 2)"); + Assert.AreEqual("is", actual); + + actual = XLWorkbook.EvaluateExpr("=+MID(\"This is a test\", 6, 2)"); + Assert.AreEqual("is", actual); + + actual = XLWorkbook.EvaluateExpr("=+++++MID(\"This is a test\", 6, 2)"); + Assert.AreEqual("is", actual); + + actual = XLWorkbook.EvaluateExpr("+MID(\"This is a test\", 6, 2)"); + Assert.AreEqual("is", actual); + } + + [Test] + public void FormulasWithErrors() + { + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#REF!)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#VALUE!)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#DIV/0!)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#NAME?)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#N/A)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#NULL!)")); + Assert.Throws(() => XLWorkbook.EvaluateExpr("YEAR(#NUM!)")); + } + + [Test] + public void UnicodeLetterParsing() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet C C"); + var ws2 = wb.AddWorksheet("C"); + var ws3 = wb.AddWorksheet("Sheet3"); + + ws1.FirstCell().SetValue(100); + ws2.FirstCell().SetValue(50); + + ws3.FirstCell().FormulaA1 = "='Sheet C C'!A1"; + ws3.FirstCell().CellBelow().FormulaA1 = "C!A1"; + + Assert.AreEqual(100, ws3.FirstCell().Value); + Assert.AreEqual(50, ws3.FirstCell().CellBelow().Value); + } + } + + [Test] + public void ShiftFormula() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + ws.Cell("B1").FormulaA1 = "ATAN2(C1,C2)"; + ws.Cell("B2").FormulaA1 = "DEC2HEX(C2)"; + ws.Range("B3:B5").FormulaA1 = "{DAYS360(C3:C5, D3:D5)}"; + + ws.Column(1).Delete(); + + Assert.AreEqual("ATAN2(B1,B2)", ws.Cell("A1").FormulaA1); + Assert.AreEqual("DEC2HEX(B2)", ws.Cell("A2").FormulaA1); + Assert.AreEqual("{DAYS360(B3:B5, C3:C5)}", ws.Cell("A3").FormulaA1); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/HyperlinkTests.cs b/ClosedXML_Tests/Excel/Misc/HyperlinkTests.cs new file mode 100644 index 0000000..95b2431 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/HyperlinkTests.cs @@ -0,0 +1,32 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.Misc +{ + [TestFixture] + public class HyperlinkTests + { + [Test] + public void TestHyperlinks() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet1"); + var ws2 = wb.Worksheets.Add("Sheet2"); + + var targetCell = ws2.Cell("A1"); + var targetRange = ws2.Range("A1", "B1"); + + var linkCell1 = ws1.Cell("A1"); + linkCell1.Value = "Link to IXLCell"; + linkCell1.Hyperlink = new XLHyperlink(targetCell); + Assert.AreEqual("Sheet2!A1", linkCell1.Hyperlink.InternalAddress); + + var linkRange1 = ws1.Cell("A2"); + linkRange1.Value = "Link to IXLRangeBase"; + linkRange1.Hyperlink = new XLHyperlink(targetRange); + Assert.AreEqual("Sheet2!A1:B1", linkRange1.Hyperlink.InternalAddress); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/SearchTests.cs b/ClosedXML_Tests/Excel/Misc/SearchTests.cs new file mode 100644 index 0000000..21e299d --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/SearchTests.cs @@ -0,0 +1,78 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Globalization; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Misc +{ + [TestFixture] + public class SearchTests + { + [Test] + public void TestSearch() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\CellValues.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + + IXLCells foundCells; + + foundCells = ws.Search("Initial Value"); + Assert.AreEqual(1, foundCells.Count()); + Assert.AreEqual("B2", foundCells.Single().Address.ToString()); + Assert.AreEqual("Initial Value", foundCells.Single().GetString()); + + foundCells = ws.Search("Using"); + Assert.AreEqual(2, foundCells.Count()); + Assert.AreEqual("D2", foundCells.First().Address.ToString()); + Assert.AreEqual("Using Get...()", foundCells.First().GetString()); + Assert.AreEqual(2, foundCells.Count()); + Assert.AreEqual("E2", foundCells.Last().Address.ToString()); + Assert.AreEqual("Using GetValue()", foundCells.Last().GetString()); + + foundCells = ws.Search("1234"); + Assert.AreEqual(4, foundCells.Count()); + Assert.AreEqual("C5,D5,E5,F5", string.Join(",", foundCells.Select(c => c.Address.ToString()).ToArray())); + + foundCells = ws.Search("Sep"); + Assert.AreEqual(2, foundCells.Count()); + Assert.AreEqual("B3,G3", string.Join(",", foundCells.Select(c => c.Address.ToString()).ToArray())); + + foundCells = ws.Search("1234", CompareOptions.Ordinal, true); + Assert.AreEqual(5, foundCells.Count()); + Assert.AreEqual("B5,C5,D5,E5,F5", string.Join(",", foundCells.Select(c => c.Address.ToString()).ToArray())); + + foundCells = ws.Search("test case", CompareOptions.Ordinal); + Assert.AreEqual(0, foundCells.Count()); + + foundCells = ws.Search("test case", CompareOptions.OrdinalIgnoreCase); + Assert.AreEqual(6, foundCells.Count()); + } + } + + [Test] + public void TestSearch2() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\Formulas.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + + IXLCells foundCells; + + foundCells = ws.Search("3", CompareOptions.Ordinal); + Assert.AreEqual(10, foundCells.Count()); + Assert.AreEqual("C2", foundCells.First().Address.ToString()); + + foundCells = ws.Search("A2", CompareOptions.Ordinal, true); + Assert.AreEqual(6, foundCells.Count()); + Assert.AreEqual("C2,D2,B6,C6,D6,A11", string.Join(",", foundCells.Select(c => c.Address.ToString()).ToArray())); + + foundCells = ws.Search("RC", CompareOptions.Ordinal, true); + Assert.AreEqual(3, foundCells.Count()); + Assert.AreEqual("E2,E3,E4", string.Join(",", foundCells.Select(c => c.Address.ToString()).ToArray())); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/StylesTests.cs b/ClosedXML_Tests/Excel/Misc/StylesTests.cs new file mode 100644 index 0000000..96740f4 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/StylesTests.cs @@ -0,0 +1,114 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.Misc +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class StylesTests + { + private static void SetupBorders(IXLRange range) + { + range.FirstRow().Cell(1).Style.Border.TopBorder = XLBorderStyleValues.None; + range.FirstRow().Cell(2).Style.Border.TopBorder = XLBorderStyleValues.Thick; + range.FirstRow().Cell(3).Style.Border.TopBorder = XLBorderStyleValues.Double; + + range.LastRow().Cell(1).Style.Border.BottomBorder = XLBorderStyleValues.None; + range.LastRow().Cell(2).Style.Border.BottomBorder = XLBorderStyleValues.Thick; + range.LastRow().Cell(3).Style.Border.BottomBorder = XLBorderStyleValues.Double; + + range.FirstColumn().Cell(1).Style.Border.LeftBorder = XLBorderStyleValues.None; + range.FirstColumn().Cell(2).Style.Border.LeftBorder = XLBorderStyleValues.Thick; + range.FirstColumn().Cell(3).Style.Border.LeftBorder = XLBorderStyleValues.Double; + + range.LastColumn().Cell(1).Style.Border.RightBorder = XLBorderStyleValues.None; + range.LastColumn().Cell(2).Style.Border.RightBorder = XLBorderStyleValues.Thick; + range.LastColumn().Cell(3).Style.Border.RightBorder = XLBorderStyleValues.Double; + } + + [Test] + public void InsideBorderTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + IXLRange range = ws.Range("B2:D4"); + + SetupBorders(range); + + range.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + range.Style.Border.InsideBorderColor = XLColor.Red; + + IXLCell center = range.Cell(2, 2); + + Assert.AreEqual(XLColor.Red, center.Style.Border.TopBorderColor); + Assert.AreEqual(XLColor.Red, center.Style.Border.BottomBorderColor); + Assert.AreEqual(XLColor.Red, center.Style.Border.LeftBorderColor); + Assert.AreEqual(XLColor.Red, center.Style.Border.RightBorderColor); + + Assert.AreEqual(XLBorderStyleValues.None, range.FirstRow().Cell(1).Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, range.FirstRow().Cell(2).Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.Double, range.FirstRow().Cell(3).Style.Border.TopBorder); + + Assert.AreEqual(XLBorderStyleValues.None, range.LastRow().Cell(1).Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, range.LastRow().Cell(2).Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.Double, range.LastRow().Cell(3).Style.Border.BottomBorder); + + Assert.AreEqual(XLBorderStyleValues.None, range.FirstColumn().Cell(1).Style.Border.LeftBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, range.FirstColumn().Cell(2).Style.Border.LeftBorder); + Assert.AreEqual(XLBorderStyleValues.Double, range.FirstColumn().Cell(3).Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, range.LastColumn().Cell(1).Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, range.LastColumn().Cell(2).Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.Double, range.LastColumn().Cell(3).Style.Border.RightBorder); + } + + [Test] + public void ResolveThemeColors() + { + using (var wb = new XLWorkbook()) + { + string color; + color = wb.Theme.ResolveThemeColor(XLThemeColor.Accent1).Color.ToHex(); + Assert.AreEqual("FF4F81BD", color); + + color = wb.Theme.ResolveThemeColor(XLThemeColor.Background1).Color.ToHex(); + Assert.AreEqual("FFFFFFFF", color); + } + } + + [Theory] + public void CanResolveAllThemeColors(XLThemeColor themeColor) + { + var theme = new XLWorkbook().Theme; + var color = theme.ResolveThemeColor(themeColor); + Assert.IsNotNull(color); + } + + [Test] + public void SetStyleViaRowReference() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Style + .Font.SetFontSize(8) + .Font.SetFontColor(XLColor.Green) + .Font.SetBold(true); + + var row = ws.Row(1); + ws.Cell(1, 1).Value = "Test"; + row.Cell(2).Value = "Test"; + row.Cells(3, 3).Value = "Test"; + + foreach (var cell in ws.CellsUsed()) + { + Assert.AreEqual(8, ws.Cell("A1").Style.Font.FontSize); + Assert.AreEqual(XLColor.Green, ws.Cell("B1").Style.Font.FontColor); + Assert.AreEqual(true, ws.Cell("C1").Style.Font.Bold); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/XLWorkbookTests.cs b/ClosedXML_Tests/Excel/Misc/XLWorkbookTests.cs new file mode 100644 index 0000000..5352f70 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/XLWorkbookTests.cs @@ -0,0 +1,463 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class XLWorkbookTests + { + [Test] + public void Cell1() + { + var wb = new XLWorkbook(); + IXLCell cell = wb.Cell("ABC"); + Assert.IsNull(cell); + } + + [Test] + public void Cell2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result", XLScope.Worksheet); + IXLCell cell = wb.Cell("Sheet1!Result"); + Assert.IsNotNull(cell); + Assert.AreEqual(1, cell.GetValue()); + } + + [Test] + public void Cell3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result"); + IXLCell cell = wb.Cell("Sheet1!Result"); + Assert.IsNotNull(cell); + Assert.AreEqual(1, cell.GetValue()); + } + + [Test] + public void Cells1() + { + var wb = new XLWorkbook(); + IXLCells cells = wb.Cells("ABC"); + Assert.IsNotNull(cells); + Assert.AreEqual(0, cells.Count()); + } + + [Test] + public void Cells2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result", XLScope.Worksheet); + IXLCells cells = wb.Cells("Sheet1!Result, ABC"); + Assert.IsNotNull(cells); + Assert.AreEqual(1, cells.Count()); + Assert.AreEqual(1, cells.First().GetValue()); + } + + [Test] + public void Cells3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result"); + IXLCells cells = wb.Cells("Sheet1!Result, ABC"); + Assert.IsNotNull(cells); + Assert.AreEqual(1, cells.Count()); + Assert.AreEqual(1, cells.First().GetValue()); + } + + [Test] + public void GetCellFromFullAddress() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + IXLWorksheet ws2 = wb.AddWorksheet("O'Sheet 2"); + var c1 = ws.Cell("C123"); + var c2 = ws2.Cell("B7"); + + var c1_full = wb.Cell("Sheet1!C123"); + var c2_full = wb.Cell("'O'Sheet 2'!B7"); + + Assert.AreSame(c1, c1_full); + Assert.AreSame(c2, c2_full); + Assert.NotNull(c1_full); + Assert.NotNull(c2_full); + } + + [TestCase("Sheet1")] + [TestCase("Sheet1!")] + [TestCase("Sheet2!")] + [TestCase("Sheet2!C1")] + [TestCase("Sheet1!ZZZ1")] + [TestCase("Sheet1!A")] + public void GetCellFromNonExistingFullAddress(string address) + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + + var c = wb.Cell(address); + + Assert.IsNull(c); + } + + [Test] + public void GetRangeFromFullAddress() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + var r1 = ws.Range("C123:D125"); + + var r2 = wb.Range("Sheet1!C123:D125"); + + Assert.AreSame(r1, r2); + Assert.NotNull(r2); + } + + [TestCase("Sheet2!C1:D2")] + [TestCase("Sheet1!A")] + public void GetRangeFromNonExistingFullAddress(string rangeAddress) + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + + var r = wb.Range(rangeAddress); + + Assert.IsNull(r); + } + + [Test] + public void GetRangesFromFullAddress() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + var r1 = ws.Ranges("A1:B2,C1:E3"); + + var r2 = wb.Ranges("Sheet1!A1:B2,Sheet1!C1:E3"); + + Assert.AreEqual(2, r2.Count); + Assert.AreSame(r1.First(), r2.First()); + Assert.AreSame(r1.Last(), r2.Last()); + } + + [TestCase("Sheet2!C1:D2,Sheet2!F1:G4")] + [TestCase("Sheet1!A,Sheet1!B")] + public void GetRangesFromNonExistingFullAddress(string rangesAddress) + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + + var r = wb.Ranges(rangesAddress); + + Assert.NotNull(r); + Assert.False(r.Any()); + } + + [Test] + public void NamedRange1() + { + var wb = new XLWorkbook(); + IXLNamedRange range = wb.NamedRange("ABC"); + Assert.IsNull(range); + } + + [Test] + public void NamedRange2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result", XLScope.Worksheet); + IXLNamedRange range = wb.NamedRange("Sheet1!Result"); + Assert.IsNotNull(range); + Assert.AreEqual(1, range.Ranges.Count); + Assert.AreEqual(1, range.Ranges.Cells().Count()); + Assert.AreEqual(1, range.Ranges.First().FirstCell().GetValue()); + } + + [Test] + public void NamedRange3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + IXLNamedRange range = wb.NamedRange("Sheet1!Result"); + Assert.IsNull(range); + } + + [Test] + public void NamedRange4() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result"); + IXLNamedRange range = wb.NamedRange("Sheet1!Result"); + Assert.IsNotNull(range); + Assert.AreEqual(1, range.Ranges.Count); + Assert.AreEqual(1, range.Ranges.Cells().Count()); + Assert.AreEqual(1, range.Ranges.First().FirstCell().GetValue()); + } + + [Test] + public void Range1() + { + var wb = new XLWorkbook(); + IXLRange range = wb.Range("ABC"); + Assert.IsNull(range); + } + + [Test] + public void Range2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result", XLScope.Worksheet); + IXLRange range = wb.Range("Sheet1!Result"); + Assert.IsNotNull(range); + Assert.AreEqual(1, range.Cells().Count()); + Assert.AreEqual(1, range.FirstCell().GetValue()); + } + + [Test] + public void Range3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result"); + IXLRange range = wb.Range("Sheet1!Result"); + Assert.IsNotNull(range); + Assert.AreEqual(1, range.Cells().Count()); + Assert.AreEqual(1, range.FirstCell().GetValue()); + } + + [Test] + public void Ranges1() + { + var wb = new XLWorkbook(); + IXLRanges ranges = wb.Ranges("ABC"); + Assert.IsNotNull(ranges); + Assert.AreEqual(0, ranges.Count()); + } + + [Test] + public void Ranges2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result", XLScope.Worksheet); + IXLRanges ranges = wb.Ranges("Sheet1!Result, ABC"); + Assert.IsNotNull(ranges); + Assert.AreEqual(1, ranges.Cells().Count()); + Assert.AreEqual(1, ranges.First().FirstCell().GetValue()); + } + + [Test] + public void Ranges3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue(1).AddToNamed("Result"); + IXLRanges ranges = wb.Ranges("Sheet1!Result, ABC"); + Assert.IsNotNull(ranges); + Assert.AreEqual(1, ranges.Cells().Count()); + Assert.AreEqual(1, ranges.First().FirstCell().GetValue()); + } + + [Test] + public void WbNamedCell() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("Test").AddToNamed("TestCell"); + Assert.AreEqual("Test", wb.Cell("TestCell").GetString()); + Assert.AreEqual("Test", ws.Cell("TestCell").GetString()); + } + + [Test] + public void WbNamedCells() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("Test").AddToNamed("TestCell"); + ws.Cell(2, 1).SetValue("B").AddToNamed("Test2"); + IXLCells wbCells = wb.Cells("TestCell, Test2"); + Assert.AreEqual("Test", wbCells.First().GetString()); + Assert.AreEqual("B", wbCells.Last().GetString()); + + IXLCells wsCells = ws.Cells("TestCell, Test2"); + Assert.AreEqual("Test", wsCells.First().GetString()); + Assert.AreEqual("B", wsCells.Last().GetString()); + } + + [Test] + public void WbNamedRange() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("A"); + ws.Cell(2, 1).SetValue("B"); + IXLRange original = ws.Range("A1:A2"); + original.AddToNamed("TestRange"); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), wb.Range("TestRange").RangeAddress.ToString()); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), ws.Range("TestRange").RangeAddress.ToString()); + } + + [Test] + public void WbNamedRanges() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("A"); + ws.Cell(2, 1).SetValue("B"); + ws.Cell(3, 1).SetValue("C").AddToNamed("Test2"); + IXLRange original = ws.Range("A1:A2"); + original.AddToNamed("TestRange"); + IXLRanges wbRanges = wb.Ranges("TestRange, Test2"); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), wbRanges.First().RangeAddress.ToString()); + Assert.AreEqual("$A$3:$A$3", wbRanges.Last().RangeAddress.ToStringFixed()); + + IXLRanges wsRanges = wb.Ranges("TestRange, Test2"); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), wsRanges.First().RangeAddress.ToString()); + Assert.AreEqual("$A$3:$A$3", wsRanges.Last().RangeAddress.ToStringFixed()); + } + + [Test] + public void WbNamedRangesOneString() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + wb.NamedRanges.Add("TestRange", "Sheet1!$A$1,Sheet1!$A$3"); + + IXLRanges wbRanges = ws.Ranges("TestRange"); + Assert.AreEqual("$A$1:$A$1", wbRanges.First().RangeAddress.ToStringFixed()); + Assert.AreEqual("$A$3:$A$3", wbRanges.Last().RangeAddress.ToStringFixed()); + + IXLRanges wsRanges = ws.Ranges("TestRange"); + Assert.AreEqual("$A$1:$A$1", wsRanges.First().RangeAddress.ToStringFixed()); + Assert.AreEqual("$A$3:$A$3", wsRanges.Last().RangeAddress.ToStringFixed()); + } + + [Test] + public void WbProtect1() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + wb.Protect(); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsFalse(wb.IsPasswordProtected); + } + } + + [Test] + public void WbProtect2() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + wb.Protect(true, false); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsFalse(wb.IsPasswordProtected); + } + } + + [Test] + public void WbProtect3() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + wb.Protect("Abc@123"); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsTrue(wb.IsPasswordProtected); + Assert.Throws(() => wb.Protect()); + Assert.Throws(() => wb.Unprotect()); + Assert.Throws(() => wb.Unprotect("Cde@345")); + } + } + + [Test] + public void WbProtect4() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + wb.Protect(); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsFalse(wb.IsPasswordProtected); + wb.Unprotect(); + wb.Protect("Abc@123"); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsTrue(wb.IsPasswordProtected); + } + } + + [Test] + public void WbProtect5() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + wb.Protect(true, false, "Abc@123"); + Assert.IsTrue(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsTrue(wb.IsPasswordProtected); + wb.Unprotect("Abc@123"); + Assert.IsFalse(wb.LockStructure); + Assert.IsFalse(wb.LockWindows); + Assert.IsFalse(wb.IsPasswordProtected); + } + } + + [Test] + public void FileSharingProperties() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet("Sheet1").Cell("A1").Value = "Hello world!"; + wb.FileSharing.ReadOnlyRecommended = true; + wb.FileSharing.UserName = Environment.UserName; + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsTrue(wb.FileSharing.ReadOnlyRecommended); + Assert.AreEqual(Environment.UserName, wb.FileSharing.UserName); + } + } + } + +#if _NET40_ + [Ignore(".NET 40 does not support Janitor.Fody")] +#endif + + [Test] + public void AccessDisposedWorkbookThrowsException() + { + IXLWorkbook wb; + using (wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + ws.FirstCell().SetValue("Hello world"); + } + + Assert.Throws(() => Console.WriteLine(wb.Worksheets.First().FirstCell().Value)); + } + } +} diff --git a/ClosedXML_Tests/Excel/Misc/XlHelperTests.cs b/ClosedXML_Tests/Excel/Misc/XlHelperTests.cs new file mode 100644 index 0000000..83e1dd0 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/XlHelperTests.cs @@ -0,0 +1,200 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class XlHelperTests + { + private static void CheckColumnNumber(int column) + { + Assert.AreEqual(column, XLHelper.GetColumnNumberFromLetter(XLHelper.GetColumnLetterFromNumber(column))); + } + + [Test] + public void InvalidA1Addresses() + { + Assert.IsFalse(XLHelper.IsValidA1Address("")); + Assert.IsFalse(XLHelper.IsValidA1Address("A")); + Assert.IsFalse(XLHelper.IsValidA1Address("a")); + Assert.IsFalse(XLHelper.IsValidA1Address("1")); + Assert.IsFalse(XLHelper.IsValidA1Address("-1")); + Assert.IsFalse(XLHelper.IsValidA1Address("AAAA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("XFG1")); + + Assert.IsFalse(XLHelper.IsValidA1Address("@A1")); + Assert.IsFalse(XLHelper.IsValidA1Address("@AA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("@AAA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("[A1")); + Assert.IsFalse(XLHelper.IsValidA1Address("[AA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("[AAA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("{A1")); + Assert.IsFalse(XLHelper.IsValidA1Address("{AA1")); + Assert.IsFalse(XLHelper.IsValidA1Address("{AAA1")); + + Assert.IsFalse(XLHelper.IsValidA1Address("A1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("AA1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("AAA1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("A1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("AA1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("AAA1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("A1{")); + Assert.IsFalse(XLHelper.IsValidA1Address("AA1{")); + Assert.IsFalse(XLHelper.IsValidA1Address("AAA1{")); + + Assert.IsFalse(XLHelper.IsValidA1Address("@A1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("@AA1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("@AAA1@")); + Assert.IsFalse(XLHelper.IsValidA1Address("[A1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("[AA1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("[AAA1[")); + Assert.IsFalse(XLHelper.IsValidA1Address("{A1{")); + Assert.IsFalse(XLHelper.IsValidA1Address("{AA1{")); + Assert.IsFalse(XLHelper.IsValidA1Address("{AAA1{")); + } + + [Test] + public void PlusAA1_Is_Not_an_address() + { + Assert.IsFalse(XLHelper.IsValidA1Address("+AA1")); + } + + [Test] + public void TestConvertColumnLetterToNumberAnd() + { + CheckColumnNumber(1); + CheckColumnNumber(27); + CheckColumnNumber(28); + CheckColumnNumber(52); + CheckColumnNumber(53); + CheckColumnNumber(1000); + CheckColumnNumber(1353); + } + + [Test] + public void ValidA1Addresses() + { + Assert.IsTrue(XLHelper.IsValidA1Address("A1")); + Assert.IsTrue(XLHelper.IsValidA1Address("A" + XLHelper.MaxRowNumber)); + Assert.IsTrue(XLHelper.IsValidA1Address("Z1")); + Assert.IsTrue(XLHelper.IsValidA1Address("Z" + XLHelper.MaxRowNumber)); + + Assert.IsTrue(XLHelper.IsValidA1Address("AA1")); + Assert.IsTrue(XLHelper.IsValidA1Address("AA" + XLHelper.MaxRowNumber)); + Assert.IsTrue(XLHelper.IsValidA1Address("ZZ1")); + Assert.IsTrue(XLHelper.IsValidA1Address("ZZ" + XLHelper.MaxRowNumber)); + + Assert.IsTrue(XLHelper.IsValidA1Address("AAA1")); + Assert.IsTrue(XLHelper.IsValidA1Address("AAA" + XLHelper.MaxRowNumber)); + Assert.IsTrue(XLHelper.IsValidA1Address(XLHelper.MaxColumnLetter + "1")); + Assert.IsTrue(XLHelper.IsValidA1Address(XLHelper.MaxColumnLetter + XLHelper.MaxRowNumber)); + } + + [Test] + public void TestColumnLetterLookup() + { + var columnLetters = new List(); + for (int c = 1; c <= XLHelper.MaxColumnNumber; c++) + { + var columnLetter = NaiveGetColumnLetterFromNumber(c); + columnLetters.Add(columnLetter); + + Assert.AreEqual(columnLetter, XLHelper.GetColumnLetterFromNumber(c)); + } + + foreach (var cl in columnLetters) + { + var columnNumber = NaiveGetColumnNumberFromLetter(cl); + Assert.AreEqual(columnNumber, XLHelper.GetColumnNumberFromLetter(cl)); + } + } + + [TestCase("R")] + [TestCase("C")] + [TestCase("RC")] + [TestCase("R111C222")] + [TestCase("R[]C")] + [TestCase("RC[]")] + [TestCase("R[]C[]")] + [TestCase("R[111]C222")] + [TestCase("R111C[222]")] + [TestCase("R[111]C[222]")] + [TestCase("R[-111]C[-222]")] + public void ValidRCAddresses(string address) + { + Assert.IsTrue(XLHelper.IsValidRCAddress(address)); + } + + [TestCase("RD")] + [TestCase("CC")] + [TestCase("R[-]C222")] + [TestCase("R[]C[-]")] + [TestCase("_R111C222")] + public void InvalidRCAddresses(string address) + { + Assert.IsFalse(XLHelper.IsValidRCAddress(address)); + } + + #region Old XLHelper methods + + private static readonly string[] letters = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; + + /// + /// These used to be the methods in XLHelper, but were later changed + /// We now use them as a check against the new methods + /// Gets the column number of a given column letter. + /// + /// The column letter to translate into a column number. + private static int NaiveGetColumnNumberFromLetter(string columnLetter) + { + if (string.IsNullOrEmpty(columnLetter)) throw new ArgumentNullException("columnLetter"); + + int retVal; + columnLetter = columnLetter.ToUpper(); + + //Extra check because we allow users to pass row col positions in as strings + if (columnLetter[0] <= '9') + { + retVal = Int32.Parse(columnLetter, XLHelper.NumberStyle, XLHelper.ParseCulture); + return retVal; + } + + int sum = 0; + + for (int i = 0; i < columnLetter.Length; i++) + { + sum *= 26; + sum += (columnLetter[i] - 'A' + 1); + } + + return sum; + } + + /// + /// Gets the column letter of a given column number. + /// + /// The column number to translate into a column letter. + /// if set to true the column letter will be restricted to the allowed range. + /// + private static string NaiveGetColumnLetterFromNumber(int columnNumber, bool trimToAllowed = false) + { + if (trimToAllowed) columnNumber = XLHelper.TrimColumnNumber(columnNumber); + + columnNumber--; // Adjust for start on column 1 + if (columnNumber <= 25) + { + return letters[columnNumber]; + } + var firstPart = (columnNumber) / 26; + var remainder = ((columnNumber) % 26) + 1; + return NaiveGetColumnLetterFromNumber(firstPart) + NaiveGetColumnLetterFromNumber(remainder); + } + + #endregion Old XLHelper methods + } +} diff --git a/ClosedXML_Tests/Excel/Misc/XmlEncoderTests.cs b/ClosedXML_Tests/Excel/Misc/XmlEncoderTests.cs new file mode 100644 index 0000000..8076915 --- /dev/null +++ b/ClosedXML_Tests/Excel/Misc/XmlEncoderTests.cs @@ -0,0 +1,23 @@ +using ClosedXML.Utils; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel +{ + [TestFixture] + public class XmlEncoderTest + { + [Test] + public void TestControlChars() + { + Assert.AreEqual("_x0001_ _x0002_ _x0003_ _x0004_", XmlEncoder.EncodeString("\u0001 \u0002 \u0003 \u0004")); + Assert.AreEqual("_x0005_ _x0006_ _x0007_ _x0008_", XmlEncoder.EncodeString("\u0005 \u0006 \u0007 \u0008")); + + Assert.AreEqual("\u0001 \u0002 \u0003 \u0004", XmlEncoder.DecodeString("_x0001_ _x0002_ _x0003_ _x0004_")); + Assert.AreEqual("\u0005 \u0006 \u0007 \u0008", XmlEncoder.DecodeString("_x0005_ _x0006_ _x0007_ _x0008_")); + Assert.AreEqual("\uAABB \uAABB", XmlEncoder.DecodeString("_xaaBB_ _xAAbb_")); + + // https://github.com/ClosedXML/ClosedXML/issues/1154 + Assert.AreEqual("_Xceed_Something", XmlEncoder.DecodeString("_Xceed_Something")); + } + } +} diff --git a/ClosedXML_Tests/Excel/NamedRanges/NamedRangesTests.cs b/ClosedXML_Tests/Excel/NamedRanges/NamedRangesTests.cs new file mode 100644 index 0000000..517779e --- /dev/null +++ b/ClosedXML_Tests/Excel/NamedRanges/NamedRangesTests.cs @@ -0,0 +1,626 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class NamedRangesTests + { + [Test] + public void MovingRanges() + { + var wb = new XLWorkbook(); + + IXLWorksheet sheet1 = wb.Worksheets.Add("Sheet1"); + IXLWorksheet sheet2 = wb.Worksheets.Add("Sheet2"); + + wb.NamedRanges.Add("wbNamedRange", + "Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G"); + sheet1.NamedRanges.Add("sheet1NamedRange", + "Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G"); + sheet2.NamedRanges.Add("sheet2NamedRange", "Sheet1!A1,Sheet2!A1"); + + sheet1.Row(1).InsertRowsAbove(2); + sheet1.Row(1).Delete(); + sheet1.Column(1).InsertColumnsBefore(2); + sheet1.Column(1).Delete(); + + Assert.AreEqual("Sheet1!$C$3,Sheet1!$C$4:$D$4,Sheet2!$D$3:$D$4,Sheet1!$7:$8,Sheet1!$G:$H", + wb.NamedRanges.First().RefersTo); + Assert.AreEqual("Sheet1!$C$3,Sheet1!$C$4:$D$4,Sheet2!$D$3:$D$4,Sheet1!$7:$8,Sheet1!$G:$H", + sheet1.NamedRanges.First().RefersTo); + Assert.AreEqual("Sheet1!B2,Sheet2!A1", sheet2.NamedRanges.First().RefersTo); + + wb.NamedRanges.ForEach(nr => Assert.AreEqual(XLNamedRangeScope.Workbook, nr.Scope)); + sheet1.NamedRanges.ForEach(nr => Assert.AreEqual(XLNamedRangeScope.Worksheet, nr.Scope)); + sheet2.NamedRanges.ForEach(nr => Assert.AreEqual(XLNamedRangeScope.Worksheet, nr.Scope)); + } + + [Test] + public void WbContainsWsNamedRange() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().AddToNamed("Name", XLScope.Worksheet); + + Assert.IsTrue(wb.NamedRanges.Contains("Sheet1!Name")); + Assert.IsFalse(wb.NamedRanges.Contains("Sheet1!NameX")); + + Assert.IsNotNull(wb.NamedRange("Sheet1!Name")); + Assert.IsNull(wb.NamedRange("Sheet1!NameX")); + + Boolean result1 = wb.NamedRanges.TryGetValue("Sheet1!Name", out IXLNamedRange range1); + Assert.IsTrue(result1); + Assert.IsNotNull(range1); + Assert.AreEqual(XLNamedRangeScope.Worksheet, range1.Scope); + + Boolean result2 = wb.NamedRanges.TryGetValue("Sheet1!NameX", out IXLNamedRange range2); + Assert.IsFalse(result2); + Assert.IsNull(range2); + } + + [Test] + public void WorkbookContainsNamedRange() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().AddToNamed("Name"); + + Assert.IsTrue(wb.NamedRanges.Contains("Name")); + Assert.IsFalse(wb.NamedRanges.Contains("NameX")); + + Assert.IsNotNull(wb.NamedRange("Name")); + Assert.IsNull(wb.NamedRange("NameX")); + + Boolean result1 = wb.NamedRanges.TryGetValue("Name", out IXLNamedRange range1); + Assert.IsTrue(result1); + Assert.IsNotNull(range1); + + Boolean result2 = wb.NamedRanges.TryGetValue("NameX", out IXLNamedRange range2); + Assert.IsFalse(result2); + Assert.IsNull(range2); + } + + [Test] + public void WorksheetContainsNamedRange() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.FirstCell().AddToNamed("Name", XLScope.Worksheet); + + Assert.IsTrue(ws.NamedRanges.Contains("Name")); + Assert.IsFalse(ws.NamedRanges.Contains("NameX")); + + Assert.IsNotNull(ws.NamedRange("Name")); + Assert.IsNull(ws.NamedRange("NameX")); + + Boolean result1 = ws.NamedRanges.TryGetValue("Name", out IXLNamedRange range1); + Assert.IsTrue(result1); + Assert.IsNotNull(range1); + + Boolean result2 = ws.NamedRanges.TryGetValue("NameX", out IXLNamedRange range2); + Assert.IsFalse(result2); + Assert.IsNull(range2); + } + + [Test] + public void DeleteColumnUsedInNamedRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Column1"); + ws.FirstCell().CellRight().SetValue("Column2").Style.Font.SetBold(); + ws.FirstCell().CellRight(2).SetValue("Column3"); + ws.NamedRanges.Add("MyRange", "A1:C1"); + + ws.Column(1).Delete(); + + Assert.IsTrue(ws.Cell("A1").Style.Font.Bold); + Assert.AreEqual("Column3", ws.Cell("B1").GetValue()); + Assert.IsEmpty(ws.Cell("C1").GetValue()); + } + } + + [Test] + public void TestInvalidNamedRangeOnWorkbookScope() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Column1"); + ws.FirstCell().CellRight().SetValue("Column2").Style.Font.SetBold(); + ws.FirstCell().CellRight(2).SetValue("Column3"); + + Assert.Throws(() => wb.NamedRanges.Add("MyRange", "A1:C1")); + } + } + + [Test] + public void NamedRangesWhenCopyingWorksheets() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + ws1.FirstCell().Value = Enumerable.Range(1, 10); + wb.NamedRanges.Add("wbNamedRange", ws1.Range("A1:A10")); + ws1.NamedRanges.Add("wsNamedRange", ws1.Range("A3")); + + var ws2 = wb.AddWorksheet("Sheet2"); + ws2.FirstCell().Value = Enumerable.Range(101, 10); + ws1.NamedRanges.Add("wsNamedRangeAcrossSheets", ws2.Range("A4")); + + ws1.Cell("C1").FormulaA1 = "=wbNamedRange"; + ws1.Cell("C2").FormulaA1 = "=wsNamedRange"; + ws1.Cell("C3").FormulaA1 = "=wsNamedRangeAcrossSheets"; + + Assert.AreEqual(1, ws1.Cell("C1").Value); + Assert.AreEqual(3, ws1.Cell("C2").Value); + Assert.AreEqual(104, ws1.Cell("C3").Value); + + var wsCopy = ws1.CopyTo("Copy"); + Assert.AreEqual(1, wsCopy.Cell("C1").Value); + Assert.AreEqual(3, wsCopy.Cell("C2").Value); + Assert.AreEqual(104, wsCopy.Cell("C3").Value); + + Assert.AreEqual("Sheet1!A1:A10", + wb.NamedRange("wbNamedRange").Ranges.First().RangeAddress.ToStringRelative(true)); + Assert.AreEqual("Copy!A3:A3", + wsCopy.NamedRange("wsNamedRange").Ranges.First().RangeAddress.ToStringRelative(true)); + Assert.AreEqual("Sheet2!A4:A4", + wsCopy.NamedRange("wsNamedRangeAcrossSheets").Ranges.First().RangeAddress.ToStringRelative(true)); + } + } + + [Test] + public void NamedRangeMayReferToExpression() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + wb.NamedRanges.Add("TEST", "=0.1"); + wb.NamedRanges.Add("TEST2", "=TEST*2"); + + ws1.Cell(1, 1).FormulaA1 = "TEST"; + ws1.Cell(2, 1).FormulaA1 = "TEST*10"; + ws1.Cell(3, 1).FormulaA1 = "TEST2"; + ws1.Cell(4, 1).FormulaA1 = "TEST2*3"; + + Assert.AreEqual(0.1, (double)ws1.Cell(1, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(1.0, (double)ws1.Cell(2, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.2, (double)ws1.Cell(3, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.6, (double)ws1.Cell(4, 1).Value, XLHelper.Epsilon); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws1 = wb.Worksheets.First(); + + Assert.AreEqual(0.1, (double)ws1.Cell(1, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(1.0, (double)ws1.Cell(2, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.2, (double)ws1.Cell(3, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.6, (double)ws1.Cell(4, 1).Value, XLHelper.Epsilon); + } + } + } + + [Test] + public void CanEvaluateNamedMultiRange() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + ws1.Range("A1:C1").Value = 1; + ws1.Range("A3:C3").Value = 3; + wb.NamedRanges.Add("TEST", ws1.Ranges("A1:C1,A3:C3")); + + ws1.Cell(2, 1).FormulaA1 = "=SUM(TEST)"; + + Assert.AreEqual(12.0, (double)ws1.Cell(2, 1).Value, XLHelper.Epsilon); + } + } + + [Test] + public void CanGetNamedFromAnother() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Cell("A1").SetValue(1).AddToNamed("value1"); + + Assert.AreEqual(1, wb.Cell("value1").GetValue()); + Assert.AreEqual(1, wb.Range("value1").FirstCell().GetValue()); + + Assert.AreEqual(1, ws1.Cell("value1").GetValue()); + Assert.AreEqual(1, ws1.Range("value1").FirstCell().GetValue()); + + var ws2 = wb.Worksheets.Add("Sheet2"); + + ws2.Cell("A1").SetFormulaA1("=value1").AddToNamed("value2"); + + Assert.AreEqual(1, wb.Cell("value2").GetValue()); + Assert.AreEqual(1, wb.Range("value2").FirstCell().GetValue()); + + Assert.AreEqual(1, ws2.Cell("value1").GetValue()); + Assert.AreEqual(1, ws2.Range("value1").FirstCell().GetValue()); + + Assert.AreEqual(1, ws2.Cell("value2").GetValue()); + Assert.AreEqual(1, ws2.Range("value2").FirstCell().GetValue()); + } + + [Test] + public void CanRenameNamedRange() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + var nr1 = wb.NamedRanges.Add("TEST", "=0.1"); + + Assert.IsTrue(wb.NamedRanges.TryGetValue("TEST", out IXLNamedRange _)); + Assert.IsFalse(wb.NamedRanges.TryGetValue("TEST1", out IXLNamedRange _)); + + nr1.Name = "TEST1"; + + Assert.IsFalse(wb.NamedRanges.TryGetValue("TEST", out IXLNamedRange _)); + Assert.IsTrue(wb.NamedRanges.TryGetValue("TEST1", out IXLNamedRange _)); + + var nr2 = wb.NamedRanges.Add("TEST2", "=TEST1*2"); + + ws1.Cell(1, 1).FormulaA1 = "TEST1"; + ws1.Cell(2, 1).FormulaA1 = "TEST1*10"; + ws1.Cell(3, 1).FormulaA1 = "TEST2"; + ws1.Cell(4, 1).FormulaA1 = "TEST2*3"; + + Assert.AreEqual(0.1, (double)ws1.Cell(1, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(1.0, (double)ws1.Cell(2, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.2, (double)ws1.Cell(3, 1).Value, XLHelper.Epsilon); + Assert.AreEqual(0.6, (double)ws1.Cell(4, 1).Value, XLHelper.Epsilon); + } + } + + [Test] + public void CopyNamedRangeSameWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Range("B2:E6").AddToNamed("Named range", XLScope.Worksheet); + var nr = ws1.NamedRange("Named range"); + + TestDelegate action = () => nr.CopyTo(ws1); + + Assert.Throws(typeof(InvalidOperationException), action); + } + + [Test] + public void CopyNamedRangeDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var ws2 = wb.Worksheets.Add("Sheet2"); + var ranges = new XLRanges(); + ranges.Add(ws1.Range("B2:E6")); + ranges.Add(ws2.Range("D1:E2")); + var original = ws1.NamedRanges.Add("Named range", ranges); + + var copy = original.CopyTo(ws2); + + Assert.AreEqual(1, ws1.NamedRanges.Count()); + Assert.AreEqual(1, ws2.NamedRanges.Count()); + Assert.AreEqual(2, original.Ranges.Count); + Assert.AreEqual(2, copy.Ranges.Count); + Assert.AreEqual(original.Name, copy.Name); + Assert.AreEqual(original.Scope, copy.Scope); + Assert.AreEqual("Sheet1!B2:E6", original.Ranges.First().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet2!D1:E2", original.Ranges.Last().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet2!D1:E2", copy.Ranges.First().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet2!B2:E6", copy.Ranges.Last().RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + + [Test] + public void NamedRangesBecomeInvalidOnWorksheetDeleting() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet 1"); + var ws2 = wb.Worksheets.Add("Sheet 2"); + var ws3 = wb.Worksheets.Add("Sheet'3"); + + ws1.Range("A1:D1").AddToNamed("Named range 1", XLScope.Worksheet); + ws1.Range("A2:D2").AddToNamed("Named range 2", XLScope.Workbook); + ws2.Range("A3:D3").AddToNamed("Named range 3", XLScope.Worksheet); + ws2.Range("A4:D4").AddToNamed("Named range 4", XLScope.Workbook); + wb.NamedRanges.Add("Named range 5", new XLRanges + { + ws1.Range("A5:D5"), + ws3.Range("A5:D5") + }); + + ws2.Delete(); + ws3.Delete(); + + Assert.AreEqual(1, ws1.NamedRanges.Count()); + Assert.AreEqual("Named range 1", ws1.NamedRanges.First().Name); + Assert.AreEqual(XLNamedRangeScope.Worksheet, ws1.NamedRanges.First().Scope); + Assert.AreEqual("'Sheet 1'!$A$1:$D$1", ws1.NamedRanges.First().RefersTo); + Assert.AreEqual("'Sheet 1'!A1:D1", ws1.NamedRanges.First().Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + + Assert.AreEqual(3, wb.NamedRanges.Count()); + + Assert.AreEqual("Named range 2", wb.NamedRanges.ElementAt(0).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(0).Scope); + Assert.AreEqual("'Sheet 1'!$A$2:$D$2", wb.NamedRanges.ElementAt(0).RefersTo); + Assert.AreEqual("'Sheet 1'!A2:D2", wb.NamedRanges.ElementAt(0).Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + + Assert.AreEqual("Named range 4", wb.NamedRanges.ElementAt(1).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(1).Scope); + Assert.AreEqual("#REF!$A$4:$D$4", wb.NamedRanges.ElementAt(1).RefersTo); + Assert.IsFalse(wb.NamedRanges.ElementAt(1).Ranges.Any()); + + Assert.AreEqual("Named range 5", wb.NamedRanges.ElementAt(2).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(2).Scope); + Assert.AreEqual("'Sheet 1'!$A$5:$D$5,#REF!$A$5:$D$5", wb.NamedRanges.ElementAt(2).RefersTo); + Assert.AreEqual(1, wb.NamedRanges.ElementAt(2).Ranges.Count); + Assert.AreEqual("'Sheet 1'!A5:D5", wb.NamedRanges.ElementAt(2).Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + } + + [Test] + public void SavedNamedRangesBecomeInvalidOnWorksheetDeleting() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet 1"); + var ws2 = wb.Worksheets.Add("Sheet2"); + var ws3 = wb.Worksheets.Add("Sheet'3"); + + ws1.Range("A1:D1").AddToNamed("Named range 1", XLScope.Worksheet); + ws1.Range("A2:D2").AddToNamed("Named range 2", XLScope.Workbook); + ws2.Range("A3:D3").AddToNamed("Named range 3", XLScope.Worksheet); + ws2.Range("A4:D4").AddToNamed("Named range 4", XLScope.Workbook); + wb.NamedRanges.Add("Named range 5", new XLRanges + { + ws1.Range("A5:D5"), + ws3.Range("A5:D5") + }); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + wb.Worksheet("Sheet2").Delete(); + wb.Worksheet("Sheet'3").Delete(); + wb.Save(); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws1 = wb.Worksheet("Sheet 1"); + Assert.AreEqual(1, ws1.NamedRanges.Count()); + Assert.AreEqual("Named range 1", ws1.NamedRanges.First().Name); + Assert.AreEqual(XLNamedRangeScope.Worksheet, ws1.NamedRanges.First().Scope); + Assert.AreEqual("'Sheet 1'!$A$1:$D$1", ws1.NamedRanges.First().RefersTo); + Assert.AreEqual("'Sheet 1'!A1:D1", + ws1.NamedRanges.First().Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + + Assert.AreEqual(3, wb.NamedRanges.Count()); + + Assert.AreEqual("Named range 2", wb.NamedRanges.ElementAt(0).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(0).Scope); + Assert.AreEqual("'Sheet 1'!$A$2:$D$2", wb.NamedRanges.ElementAt(0).RefersTo); + Assert.AreEqual("'Sheet 1'!A2:D2", + wb.NamedRanges.ElementAt(0).Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + + Assert.AreEqual("Named range 4", wb.NamedRanges.ElementAt(1).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(1).Scope); + Assert.AreEqual("#REF!", wb.NamedRanges.ElementAt(1).RefersTo); + Assert.IsFalse(wb.NamedRanges.ElementAt(1).Ranges.Any()); + + Assert.AreEqual("Named range 5", wb.NamedRanges.ElementAt(2).Name); + Assert.AreEqual(XLNamedRangeScope.Workbook, wb.NamedRanges.ElementAt(2).Scope); + Assert.AreEqual("'Sheet 1'!$A$5:$D$5,#REF!", wb.NamedRanges.ElementAt(2).RefersTo); + Assert.AreEqual(1, wb.NamedRanges.ElementAt(2).Ranges.Count); + Assert.AreEqual("'Sheet 1'!A5:D5", + wb.NamedRanges.ElementAt(2).Ranges.Single().RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + } + } + + [Test] + public void NamedRangeReferringToMultipleRangesCanBeSavedAndLoaded() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet 1"); + + wb.NamedRanges.Add("Multirange named range", new XLRanges + { + ws.Range("A5:D5"), + ws.Range("A15:D15") + }); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual(1, wb.NamedRanges.Count()); + var nr = wb.NamedRanges.Single() as XLNamedRange; + Assert.AreEqual("'Sheet 1'!$A$5:$D$5,'Sheet 1'!$A$15:$D$15", nr.RefersTo); + Assert.AreEqual(2, nr.Ranges.Count); + Assert.AreEqual("'Sheet 1'!A5:D5", nr.Ranges.First().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!A15:D15", nr.Ranges.Last().RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual(2, nr.RangeList.Count); + Assert.AreEqual("'Sheet 1'!$A$5:$D$5", nr.RangeList.First()); + Assert.AreEqual("'Sheet 1'!$A$15:$D$15", nr.RangeList.Last()); + } + } + } + + [Test, Ignore("Muted until shifting is fixed (see #880)")] + public void NamedRangeBecomesInvalidOnRangeDeleting() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet 1"); + ws.Range("A1:B2").AddToNamed("Simple", XLScope.Workbook); + wb.NamedRanges.Add("Compound", new XLRanges + { + ws.Range("C1:D2"), + ws.Range("A10:D15") + }); + + ws.Rows(1,5).Delete(); + + Assert.AreEqual(2, wb.NamedRanges.Count()); + Assert.AreEqual(0, wb.NamedRanges.ValidNamedRanges().Count()); + Assert.AreEqual("'Sheet 1'!#REF!", wb.NamedRanges.ElementAt(0).RefersTo); + Assert.AreEqual("'Sheet 1'!#REF!,'Sheet 1'!A5:D10", wb.NamedRanges.ElementAt(0).RefersTo); + } + } + + [Test, Ignore("Muted until shifting is fixed (see #880)")] + public void NamedRangeBecomesInvalidOnRangeAndWorksheetDeleting() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet 1"); + var ws2 = wb.Worksheets.Add("Sheet 2"); + ws1.Range("A1:B2").AddToNamed("Simple", XLScope.Workbook); + wb.NamedRanges.Add("Compound", new XLRanges + { + ws1.Range("C1:D2"), + ws2.Range("A10:D15") + }); + + ws1.Rows(1, 5).Delete(); + ws1.Delete(); + + Assert.AreEqual(2, wb.NamedRanges.Count()); + Assert.AreEqual(0, wb.NamedRanges.ValidNamedRanges().Count()); + Assert.AreEqual("#REF!#REF!", wb.NamedRanges.ElementAt(0).RefersTo); + Assert.AreEqual("#REF!#REF!,'Sheet 2'!A10:D15", wb.NamedRanges.ElementAt(0).RefersTo); + } + } + + [Test] + public void CanSaveAndLoadNamedRanges() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var sheet1 = wb.Worksheets.Add("Sheet1"); + var sheet2 = wb.Worksheets.Add("Sheet2"); + + wb.NamedRanges.Add("wbNamedRange", + "Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G"); + sheet1.NamedRanges.Add("sheet1NamedRange", + "Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G"); + sheet2.NamedRanges.Add("sheet2NamedRange", "Sheet1!A1,Sheet2!A1"); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var sheet1 = wb.Worksheet("Sheet1"); + var sheet2 = wb.Worksheet("Sheet2"); + + Assert.AreEqual(1, wb.NamedRanges.Count()); + Assert.AreEqual("wbNamedRange", wb.NamedRanges.Single().Name); + Assert.AreEqual("Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G", wb.NamedRanges.Single().RefersTo); + Assert.AreEqual(5, wb.NamedRanges.Single().Ranges.Count); + + Assert.AreEqual(1, sheet1.NamedRanges.Count()); + Assert.AreEqual("sheet1NamedRange", sheet1.NamedRanges.Single().Name); + Assert.AreEqual("Sheet1!$B$2,Sheet1!$B$3:$C$3,Sheet2!$D$3:$D$4,Sheet1!$6:$7,Sheet1!$F:$G", sheet1.NamedRanges.Single().RefersTo); + Assert.AreEqual(5, sheet1.NamedRanges.Single().Ranges.Count); + + Assert.AreEqual(1, sheet2.NamedRanges.Count()); + Assert.AreEqual("sheet2NamedRange", sheet2.NamedRanges.Single().Name); + Assert.AreEqual("Sheet1!A1,Sheet2!A1", sheet2.NamedRanges.Single().RefersTo); + Assert.AreEqual(2, sheet2.NamedRanges.Single().Ranges.Count); + } + } + } + + [Test] + public void NamedRangesFromDeletedSheetAreSavedWithoutAddress() + { + // Range address referring to the deleted sheet look like #REF!A1:B2. + // But workbooks with such references in named ranges Excel considers as broken files. + // It requires #REF! + + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add("Sheet 1"); + var ws2 = wb.Worksheets.Add("Sheet 2"); + ws2.Range("A4:D4").AddToNamed("Test named range", XLScope.Workbook); + ws2.Delete(); + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual("#REF!", wb.NamedRanges.Single().RefersTo); + } + } + } + + [Test] + public void CanGetValidNamedRanges() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.Worksheets.Add("Sheet 1"); + var ws2 = wb.Worksheets.Add("Sheet 2"); + var ws3 = wb.Worksheets.Add("Sheet'3"); + + ws1.Range("A1:D1").AddToNamed("Named range 1", XLScope.Worksheet); + ws1.Range("A2:D2").AddToNamed("Named range 2", XLScope.Workbook); + ws2.Range("A3:D3").AddToNamed("Named range 3", XLScope.Worksheet); + ws2.Range("A4:D4").AddToNamed("Named range 4", XLScope.Workbook); + wb.NamedRanges.Add("Named range 5", new XLRanges + { + ws1.Range("A5:D5"), + ws3.Range("A5:D5") + }); + + ws2.Delete(); + ws3.Delete(); + + var globalValidRanges = wb.NamedRanges.ValidNamedRanges(); + var globalInvalidRanges = wb.NamedRanges.InvalidNamedRanges(); + var localValidRanges = ws1.NamedRanges.ValidNamedRanges(); + var localInvalidRanges = ws1.NamedRanges.InvalidNamedRanges(); + + Assert.AreEqual(1, globalValidRanges.Count()); + Assert.AreEqual("Named range 2", globalValidRanges.First().Name); + + Assert.AreEqual(2, globalInvalidRanges.Count()); + Assert.AreEqual("Named range 4", globalInvalidRanges.First().Name); + Assert.AreEqual("Named range 5", globalInvalidRanges.Last().Name); + + Assert.AreEqual(1, localValidRanges.Count()); + Assert.AreEqual("Named range 1", localValidRanges.First().Name); + + Assert.AreEqual(0, localInvalidRanges.Count()); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/PageSetup/HeaderFooterTests.cs b/ClosedXML_Tests/Excel/PageSetup/HeaderFooterTests.cs new file mode 100644 index 0000000..002df23 --- /dev/null +++ b/ClosedXML_Tests/Excel/PageSetup/HeaderFooterTests.cs @@ -0,0 +1,51 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + [TestFixture] + public class HeaderFooterTests + { + [Test] + public void CanChangeWorksheetHeader() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + + ws.PageSetup.Header.Center.AddText("Initial page header", XLHFOccurrence.EvenPages); + + var ms = new MemoryStream(); + wb.SaveAs(ms, true); + + wb = new XLWorkbook(ms); + ws = wb.Worksheets.First(); + + ws.PageSetup.Header.Center.Clear(); + ws.PageSetup.Header.Center.AddText("Changed header", XLHFOccurrence.EvenPages); + + wb.SaveAs(ms, true); + + wb = new XLWorkbook(ms); + ws = wb.Worksheets.First(); + + var newHeader = ws.PageSetup.Header.Center.GetText(XLHFOccurrence.EvenPages); + Assert.AreEqual("Changed header", newHeader); + } + + [TestCase("")] + [TestCase("&L&C&\"Arial\"&9 19-10-2017 \n&9&\"Arial\" &P &N &R")] // https://github.com/ClosedXML/ClosedXML/issues/563 + public void CanSetHeaderFooter(string s) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + { + var header = ws.PageSetup.Header as XLHeaderFooter; + header.SetInnerText(XLHFOccurrence.AllPages, s); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/PageSetup/PageBreaksTests.cs b/ClosedXML_Tests/Excel/PageSetup/PageBreaksTests.cs new file mode 100644 index 0000000..ff1673c --- /dev/null +++ b/ClosedXML_Tests/Excel/PageSetup/PageBreaksTests.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel +{ + [TestFixture] + public class PageBreaksTests + { + [Test] + public void RowBreaksShouldBeSorted() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddHorizontalPageBreak(10); + sheet.PageSetup.AddHorizontalPageBreak(12); + sheet.PageSetup.AddHorizontalPageBreak(5); + Assert.That(sheet.PageSetup.RowBreaks, Is.EqualTo(new[] { 5, 10, 12 })); + } + + [Test] + public void ColumnBreaksShouldBeSorted() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddVerticalPageBreak(10); + sheet.PageSetup.AddVerticalPageBreak(12); + sheet.PageSetup.AddVerticalPageBreak(5); + Assert.That(sheet.PageSetup.ColumnBreaks, Is.EqualTo(new[] { 5, 10, 12 })); + } + + [Test] + public void RowBreaksShiftWhenInsertedRowAbove() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddHorizontalPageBreak(10); + sheet.Row(5).InsertRowsAbove(1); + Assert.AreEqual(11, sheet.PageSetup.RowBreaks[0]); + } + + [Test] + public void RowBreaksNotShiftWhenInsertedRowBelow() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddHorizontalPageBreak(10); + sheet.Row(15).InsertRowsAbove(1); + Assert.AreEqual(10, sheet.PageSetup.RowBreaks[0]); + } + + [Test] + public void ColumnBreaksShiftWhenInsertedColumnBefore() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddVerticalPageBreak(10); + sheet.Column(5).InsertColumnsBefore(1); + Assert.AreEqual(11, sheet.PageSetup.ColumnBreaks[0]); + } + + [Test] + public void ColumnBreaksNotShiftWhenInsertedColumnAfter() + { + var wb = new XLWorkbook(); + IXLWorksheet sheet = wb.AddWorksheet("Sheet1"); + + sheet.PageSetup.AddVerticalPageBreak(10); + sheet.Column(15).InsertColumnsBefore(1); + Assert.AreEqual(10, sheet.PageSetup.ColumnBreaks[0]); + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Excel/PivotTables/XLPivotTableTests.cs b/ClosedXML_Tests/Excel/PivotTables/XLPivotTableTests.cs new file mode 100644 index 0000000..1d72252 --- /dev/null +++ b/ClosedXML_Tests/Excel/PivotTables/XLPivotTableTests.cs @@ -0,0 +1,765 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLPivotTableTests + { + [Test] + public void PivotTables() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\PivotTables\PivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("PastrySalesData"); + var table = ws.Table("PastrySalesData"); + var ptSheet = wb.Worksheets.Add("BlankPivotTable"); + ptSheet.PivotTables.Add("pvt", ptSheet.Cell(1, 1), table); + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + } + } + } + + [Test] + public void TestPivotTableVersioningAttributes() + { + // Pivot table definitions in input file has created and refreshed versioning attributes = 3 + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\VersioningAttributes\inputfile.xlsx"))) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + + var data = wb.Worksheet("Data"); + + var pt = data.RangeUsed().CreatePivotTable(wb.AddWorksheet("pvt2").FirstCell(), "pvt2"); + + pt.ColumnLabels.Add("Sex"); + pt.RowLabels.Add("FullName"); + pt.Values.Add("Id", "Count of Id").SetSummaryFormula(XLPivotSummary.Count); + + return wb; + // Pivot table definitions in output file has created and refreshed versioning attributes = 5 + }, @"Other\PivotTableReferenceFiles\VersioningAttributes\outputfile.xlsx"); + } + } + + [Test] + public void PivotTableOptionsSaveTest() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\PivotTables\PivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("PastrySalesData"); + var table = ws.Table("PastrySalesData"); + var ptSheet = wb.Worksheets.Add("BlankPivotTable"); + var pt = ptSheet.PivotTables.Add("pvtOptionsTest", ptSheet.Cell(1, 1), table); + + pt.ColumnHeaderCaption = "clmn header"; + pt.RowHeaderCaption = "row header"; + + pt.AutofitColumns = true; + pt.PreserveCellFormatting = false; + pt.ShowGrandTotalsColumns = true; + pt.ShowGrandTotalsRows = true; + pt.UseCustomListsForSorting = false; + pt.ShowExpandCollapseButtons = false; + pt.ShowContextualTooltips = false; + pt.DisplayCaptionsAndDropdowns = false; + pt.RepeatRowLabels = true; + pt.SaveSourceData = false; + pt.EnableShowDetails = false; + pt.ShowColumnHeaders = false; + pt.ShowRowHeaders = false; + + pt.MergeAndCenterWithLabels = true; // MergeItem + pt.RowLabelIndent = 12; // Indent + pt.FilterAreaOrder = XLFilterAreaOrder.OverThenDown; // PageOverThenDown + pt.FilterFieldsPageWrap = 14; // PageWrap + pt.ErrorValueReplacement = "error test"; // ErrorCaption + pt.EmptyCellReplacement = "empty test"; // MissingCaption + + pt.FilteredItemsInSubtotals = true; // Subtotal filtered page items + pt.AllowMultipleFilters = false; // MultipleFieldFilters + + pt.ShowPropertiesInTooltips = false; + pt.ClassicPivotTableLayout = true; + pt.ShowEmptyItemsOnRows = true; + pt.ShowEmptyItemsOnColumns = true; + pt.DisplayItemLabels = false; + pt.SortFieldsAtoZ = true; + + pt.PrintExpandCollapsedButtons = true; + pt.PrintTitles = true; + + // TODO pt.RefreshDataOnOpen = false; + pt.ItemsToRetainPerField = XLItemsToRetain.Max; + pt.EnableCellEditing = true; + pt.ShowValuesRow = true; + pt.ShowRowStripes = true; + pt.ShowColumnStripes = true; + pt.Theme = XLPivotTableTheme.PivotStyleDark13; + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + + ms.Position = 0; + + using (var wbassert = new XLWorkbook(ms)) + { + var wsassert = wbassert.Worksheet("BlankPivotTable"); + var ptassert = wsassert.PivotTable("pvtOptionsTest"); + Assert.AreNotEqual(null, ptassert, "name save failure"); + Assert.AreEqual("clmn header", ptassert.ColumnHeaderCaption, "ColumnHeaderCaption save failure"); + Assert.AreEqual("row header", ptassert.RowHeaderCaption, "RowHeaderCaption save failure"); + Assert.AreEqual(true, ptassert.MergeAndCenterWithLabels, "MergeAndCenterWithLabels save failure"); + Assert.AreEqual(12, ptassert.RowLabelIndent, "RowLabelIndent save failure"); + Assert.AreEqual(XLFilterAreaOrder.OverThenDown, ptassert.FilterAreaOrder, "FilterAreaOrder save failure"); + Assert.AreEqual(14, ptassert.FilterFieldsPageWrap, "FilterFieldsPageWrap save failure"); + Assert.AreEqual("error test", ptassert.ErrorValueReplacement, "ErrorValueReplacement save failure"); + Assert.AreEqual("empty test", ptassert.EmptyCellReplacement, "EmptyCellReplacement save failure"); + Assert.AreEqual(true, ptassert.AutofitColumns, "AutofitColumns save failure"); + Assert.AreEqual(false, ptassert.PreserveCellFormatting, "PreserveCellFormatting save failure"); + Assert.AreEqual(true, ptassert.ShowGrandTotalsRows, "ShowGrandTotalsRows save failure"); + Assert.AreEqual(true, ptassert.ShowGrandTotalsColumns, "ShowGrandTotalsColumns save failure"); + Assert.AreEqual(true, ptassert.FilteredItemsInSubtotals, "FilteredItemsInSubtotals save failure"); + Assert.AreEqual(false, ptassert.AllowMultipleFilters, "AllowMultipleFilters save failure"); + Assert.AreEqual(false, ptassert.UseCustomListsForSorting, "UseCustomListsForSorting save failure"); + Assert.AreEqual(false, ptassert.ShowExpandCollapseButtons, "ShowExpandCollapseButtons save failure"); + Assert.AreEqual(false, ptassert.ShowContextualTooltips, "ShowContextualTooltips save failure"); + Assert.AreEqual(false, ptassert.ShowPropertiesInTooltips, "ShowPropertiesInTooltips save failure"); + Assert.AreEqual(false, ptassert.DisplayCaptionsAndDropdowns, "DisplayCaptionsAndDropdowns save failure"); + Assert.AreEqual(true, ptassert.ClassicPivotTableLayout, "ClassicPivotTableLayout save failure"); + Assert.AreEqual(true, ptassert.ShowEmptyItemsOnRows, "ShowEmptyItemsOnRows save failure"); + Assert.AreEqual(true, ptassert.ShowEmptyItemsOnColumns, "ShowEmptyItemsOnColumns save failure"); + Assert.AreEqual(false, ptassert.DisplayItemLabels, "DisplayItemLabels save failure"); + Assert.AreEqual(true, ptassert.SortFieldsAtoZ, "SortFieldsAtoZ save failure"); + Assert.AreEqual(true, ptassert.PrintExpandCollapsedButtons, "PrintExpandCollapsedButtons save failure"); + Assert.AreEqual(true, ptassert.RepeatRowLabels, "RepeatRowLabels save failure"); + Assert.AreEqual(true, ptassert.PrintTitles, "PrintTitles save failure"); + Assert.AreEqual(false, ptassert.SaveSourceData, "SaveSourceData save failure"); + Assert.AreEqual(false, ptassert.EnableShowDetails, "EnableShowDetails save failure"); + // TODO Assert.AreEqual(false, ptassert.RefreshDataOnOpen, "RefreshDataOnOpen save failure"); + Assert.AreEqual(XLItemsToRetain.Max, ptassert.ItemsToRetainPerField, "ItemsToRetainPerField save failure"); + Assert.AreEqual(true, ptassert.EnableCellEditing, "EnableCellEditing save failure"); + Assert.AreEqual(XLPivotTableTheme.PivotStyleDark13, ptassert.Theme, "Theme save failure"); + Assert.AreEqual(true, ptassert.ShowValuesRow, "ShowValuesRow save failure"); + Assert.AreEqual(false, ptassert.ShowRowHeaders, "ShowRowHeaders save failure"); + Assert.AreEqual(false, ptassert.ShowColumnHeaders, "ShowColumnHeaders save failure"); + Assert.AreEqual(true, ptassert.ShowRowStripes, "ShowRowStripes save failure"); + Assert.AreEqual(true, ptassert.ShowColumnStripes, "ShowColumnStripes save failure"); + } + } + } + } + + [TestCase(true)] + [TestCase(false)] + public void PivotFieldOptionsSaveTest(bool withDefaults) + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\PivotTables\PivotTables.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("PastrySalesData"); + var table = ws.Table("PastrySalesData"); + + var ptSheet = wb.Worksheets.Add("pvtFieldOptionsTest"); + var pt = ptSheet.PivotTables.Add("pvtFieldOptionsTest", ptSheet.Cell(1, 1), table); + + var field = pt.RowLabels.Add("Name") + .SetSubtotalCaption("Test caption") + .SetCustomName("Test name"); + SetFieldOptions(field, withDefaults); + + pt.ColumnLabels.Add("Month"); + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + + ms.Position = 0; + + using (var wbassert = new XLWorkbook(ms)) + { + var wsassert = wbassert.Worksheet("pvtFieldOptionsTest"); + var ptassert = wsassert.PivotTable("pvtFieldOptionsTest"); + var pfassert = ptassert.RowLabels.Get("Name"); + Assert.AreNotEqual(null, pfassert, "name save failure"); + Assert.AreEqual("Test caption", pfassert.SubtotalCaption, "SubtotalCaption save failure"); + Assert.AreEqual("Test name", pfassert.CustomName, "CustomName save failure"); + AssertFieldOptions(pfassert, withDefaults); + } + } + } + } + + [Test] + public void PivotTableStyleFormatsTest() + { + using (var ms = new MemoryStream()) + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\PivotTables\PivotTables.xlsx"))) + using (var wbSource = new XLWorkbook(stream)) + using (var wbDestination = new XLWorkbook()) + { + var ws = wbSource.Worksheet("PastrySalesData"); + wbDestination.AddWorksheet(ws); + ws = wbDestination.Worksheet("PastrySalesData"); + + var table = ws.Table("PastrySalesData"); + var ptSheet = wbDestination.Worksheets.Add("PivotTableStyleFormats"); + var pt = ptSheet.PivotTables.Add("pvtStyleFormats", ptSheet.Cell(1, 1), table); + pt.Layout = XLPivotLayout.Tabular; + + pt.SetSubtotals(XLPivotSubtotals.AtBottom); + + var monthPivotField = pt.ColumnLabels.Add("Month"); + + var namePivotField = pt.RowLabels.Add("Name") + .SetSubtotalCaption("Test caption") + .SetCustomName("Test name") + .AddSubtotal(XLSubtotalFunction.Sum); + + ptSheet.SetTabActive(); + + var numberOfOrdersPivotValue = pt.Values.Add("NumberOfOrders") + .SetSummaryFormula(XLPivotSummary.Sum); + + var qualityPivotValue = pt.Values.Add("Quality").SetSummaryFormula(XLPivotSummary.Sum); + + pt.StyleFormats.RowGrandTotalFormats.ForElement(XLPivotStyleFormatElement.All).Style.Font.FontColor = XLColor.VenetianRed; + + namePivotField.StyleFormats.Subtotal.Style.Fill.BackgroundColor = XLColor.Blue; + monthPivotField.StyleFormats.Label.Style.Fill.BackgroundColor = XLColor.Amber; + monthPivotField.StyleFormats.Header.Style.Font.FontColor = XLColor.Yellow; + namePivotField.StyleFormats.DataValuesFormat + .AndWith(monthPivotField, v => v.ToString() == "May") + .ForValueField(numberOfOrdersPivotValue) + .Style.Font.FontColor = XLColor.Green; + + wbDestination.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheet("PivotTableStyleFormats"); + var pt = ws.PivotTable("pvtStyleFormats").CastTo(); + + Assert.AreEqual(0, pt.StyleFormats.ColumnGrandTotalFormats.Count()); + + Assert.NotNull(pt.StyleFormats.RowGrandTotalFormats); + Assert.AreEqual(1, pt.StyleFormats.RowGrandTotalFormats.Count()); + Assert.AreEqual(XLPivotStyleFormatElement.All, pt.StyleFormats.RowGrandTotalFormats.First().AppliesTo); + Assert.AreEqual(XLColor.VenetianRed, pt.StyleFormats.RowGrandTotalFormats.ForElement(XLPivotStyleFormatElement.All).Style.Font.FontColor); + + var namePivotField = pt.RowLabels.Get("Name"); + var monthPivotField = pt.ColumnLabels.Get("Month"); + var numberOfOrdersPivotValue = pt.Values.Get("NumberOfOrders"); + + Assert.AreEqual(XLStyle.Default, namePivotField.StyleFormats.Label.Style); + Assert.AreEqual(XLColor.Blue, namePivotField.StyleFormats.Subtotal.Style.Fill.BackgroundColor); + + Assert.AreEqual(XLStyle.Default, monthPivotField.StyleFormats.Subtotal.Style); + Assert.AreEqual(XLColor.Amber, monthPivotField.StyleFormats.Label.Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, monthPivotField.StyleFormats.Header.Style.Font.FontColor); + + var nameDataValuesFormat = namePivotField.StyleFormats.DataValuesFormat as XLPivotValueStyleFormat; + Assert.AreEqual(2, nameDataValuesFormat.FieldReferences.Count()); + + Assert.AreEqual(monthPivotField, nameDataValuesFormat.FieldReferences.First().CastTo().PivotField); + + Assert.AreEqual(numberOfOrdersPivotValue.CustomName, nameDataValuesFormat.FieldReferences.Last().CastTo().Value); + + wb.Save(); + } + } + } + + private class Pastry + { + public Pastry(string name, int? code, int numberOfOrders, double quality, string month, DateTime? bakeDate) + { + Name = name; + Code = code; + NumberOfOrders = numberOfOrders; + Quality = quality; + Month = month; + BakeDate = bakeDate; + } + + public string Name { get; set; } + public int? Code { get; } + public int NumberOfOrders { get; set; } + public double Quality { get; set; } + public string Month { get; set; } + public DateTime? BakeDate { get; set; } + } + + [Test] + public void BlankPivotTableField() + { + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + // Based on .\ClosedXML\ClosedXML_Examples\PivotTables\PivotTables.cs + // But with empty column for Month + var pastries = new List + { + new Pastry("Croissant", 101, 150, 60.2, "", new DateTime(2016, 04, 21)), + new Pastry("Croissant", 101, 250, 50.42, "", new DateTime(2016, 05, 03)), + new Pastry("Croissant", 101, 134, 22.12, "", new DateTime(2016, 06, 24)), + new Pastry("Doughnut", 102, 250, 89.99, "", new DateTime(2017, 04, 23)), + new Pastry("Doughnut", 102, 225, 70, "", new DateTime(2016, 05, 24)), + new Pastry("Doughnut", 102, 210, 75.33, "", new DateTime(2016, 06, 02)), + new Pastry("Bearclaw", 103, 134, 10.24, "", new DateTime(2016, 04, 27)), + new Pastry("Bearclaw", 103, 184, 33.33, "", new DateTime(2016, 05, 20)), + new Pastry("Bearclaw", 103, 124, 25, "", new DateTime(2017, 06, 05)), + new Pastry("Danish", 104, 394, -20.24, "", null), + new Pastry("Danish", 104, 190, 60, "", new DateTime(2017, 05, 08)), + new Pastry("Danish", 104, 221, 24.76, "", new DateTime(2016, 06, 21)), + + // Deliberately add different casings of same string to ensure pivot table doesn't duplicate it. + new Pastry("Scone", 105, 135, 0, "", new DateTime(2017, 04, 22)), + new Pastry("SconE", 105, 122, 5.19, "", new DateTime(2017, 05, 03)), + new Pastry("SCONE", 105, 243, 44.2, "", new DateTime(2017, 06, 14)), + + // For ContainsBlank and integer rows/columns test + new Pastry("Scone", null, 255, 18.4, "", null), + }; + + var wb = new XLWorkbook(); + + var sheet = wb.Worksheets.Add("PastrySalesData"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = sheet.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + sheet.Cell("F11").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + sheet.Columns().AdjustToContents(); + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + for (var i = 1; i <= 5; i++) + { + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt" + i); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("pvt" + i, ptSheet.Cell(1, 1), table); + + if (i == 1 || i == 4 || i == 5) + pt.ColumnLabels.Add("Name"); + else if (i == 2 || i == 3) + pt.RowLabels.Add("Name"); + + if (i == 1 || i == 3) + pt.RowLabels.Add("Month"); + else if (i == 2 || i == 4) + pt.ColumnLabels.Add("Month"); + else if (i == 5) + pt.RowLabels.Add("BakeDate"); + + // The values in our table will come from the "NumberOfOrders" field + // The default calculation setting is a total of each row/column + pt.Values.Add("NumberOfOrders", "NumberOfOrdersPercentageOfBearclaw") + .ShowAsPercentageFrom("Name").And("Bearclaw") + .NumberFormat.Format = "0%"; + + ptSheet.Columns().AdjustToContents(); + } + + return wb; + }, @"Other\PivotTableReferenceFiles\BlankPivotTableField\BlankPivotTableField.xlsx"); + } + } + + [Test] + public void SourceSheetWithWhitespace() + { + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + // Based on .\ClosedXML\ClosedXML_Examples\PivotTables\PivotTables.cs + // But with empty column for Month + var pastries = new List + { + new Pastry("Croissant", 101, 150, 60.2, "", new DateTime(2016, 04, 21)), + new Pastry("Croissant", 101, 250, 50.42, "", new DateTime(2016, 05, 03)), + new Pastry("Croissant", 101, 134, 22.12, "", new DateTime(2016, 06, 24)), + new Pastry("Doughnut", 102, 250, 89.99, "", new DateTime(2017, 04, 23)), + new Pastry("Doughnut", 102, 225, 70, "", new DateTime(2016, 05, 24)), + new Pastry("Doughnut", 102, 210, 75.33, "", new DateTime(2016, 06, 02)), + new Pastry("Bearclaw", 103, 134, 10.24, "", new DateTime(2016, 04, 27)), + new Pastry("Bearclaw", 103, 184, 33.33, "", new DateTime(2016, 05, 20)), + new Pastry("Bearclaw", 103, 124, 25, "", new DateTime(2017, 06, 05)), + new Pastry("Danish", 104, 394, -20.24, "", null), + new Pastry("Danish", 104, 190, 60, "", new DateTime(2017, 05, 08)), + new Pastry("Danish", 104, 221, 24.76, "", new DateTime(2016, 06, 21)), + + // Deliberately add different casings of same string to ensure pivot table doesn't duplicate it. + new Pastry("Scone", 105, 135, 0, "", new DateTime(2017, 04, 22)), + new Pastry("SconE", 105, 122, 5.19, "", new DateTime(2017, 05, 03)), + new Pastry("SCONE", 105, 243, 44.2, "", new DateTime(2017, 06, 14)), + + // For ContainsBlank and integer rows/columns test + new Pastry("Scone", null, 255, 18.4, "", null), + }; + + var wb = new XLWorkbook(); + + var sheet = wb.Worksheets.Add("Pastry Sales Data"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = sheet.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + sheet.Cell("F11").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + sheet.Columns().AdjustToContents(); + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt"); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("pvt", ptSheet.Cell(1, 1), table.AsRange()); + pt.ColumnLabels.Add("Name"); + pt.RowLabels.Add("Month"); + + // The values in our table will come from the "NumberOfOrders" field + // The default calculation setting is a total of each row/column + pt.Values.Add("NumberOfOrders", "NumberOfOrdersPercentageOfBearclaw") + .ShowAsPercentageFrom("Name").And("Bearclaw") + .NumberFormat.Format = "0%"; + + ptSheet.Columns().AdjustToContents(); + + return wb; + }, @"Other\PivotTableReferenceFiles\SourceSheetWithWhitespace\outputfile.xlsx"); + } + } + + [Test] + public void PivotTableWithNoneTheme() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\PivotTableWithNoneTheme\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"Other\PivotTableReferenceFiles\PivotTableWithNoneTheme\outputfile.xlsx"); + } + } + + [Test] + public void MaintainPivotTableLabelsOrder() + { + var pastries = new List + { + new Pastry("Croissant", 101, 150, 60.2, "", new DateTime(2016, 04, 21)), + new Pastry("Croissant", 101, 250, 50.42, "", new DateTime(2016, 05, 03)), + new Pastry("Croissant", 101, 134, 22.12, "", new DateTime(2016, 06, 24)), + new Pastry("Doughnut", 102, 250, 89.99, "", new DateTime(2017, 04, 23)), + new Pastry("Doughnut", 102, 225, 70, "", new DateTime(2016, 05, 24)), + new Pastry("Doughnut", 102, 210, 75.33, "", new DateTime(2016, 06, 02)), + new Pastry("Bearclaw", 103, 134, 10.24, "", new DateTime(2016, 04, 27)), + new Pastry("Bearclaw", 103, 184, 33.33, "", new DateTime(2016, 05, 20)), + new Pastry("Bearclaw", 103, 124, 25, "", new DateTime(2017, 06, 05)), + new Pastry("Danish", 104, 394, -20.24, "", null), + new Pastry("Danish", 104, 190, 60, "", new DateTime(2017, 05, 08)), + new Pastry("Danish", 104, 221, 24.76, "", new DateTime(2016, 06, 21)), + + // Deliberately add different casings of same string to ensure pivot table doesn't duplicate it. + new Pastry("Scone", 105, 135, 0, "", new DateTime(2017, 04, 22)), + new Pastry("SconE", 105, 122, 5.19, "", new DateTime(2017, 05, 03)), + new Pastry("SCONE", 105, 243, 44.2, "", new DateTime(2017, 06, 14)), + + // For ContainsBlank and integer rows/columns test + new Pastry("Scone", null, 255, 18.4, "", null), + }; + + using (var ms = new MemoryStream()) + { + // Page fields + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("PastrySalesData"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = sheet.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + sheet.Cell("F11").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + sheet.Columns().AdjustToContents(); + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt"); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("PastryPivot", ptSheet.Cell(1, 1), table); + + pt.ReportFilters.Add("Month"); + pt.ReportFilters.Add("Name"); + + pt.RowLabels.Add("BakeDate"); + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var pageFields = wb.Worksheets.SelectMany(ws => ws.PivotTables) + .First() + .ReportFilters + .ToArray(); + + Assert.AreEqual("Month", pageFields[0].SourceName); + Assert.AreEqual("Name", pageFields[1].SourceName); + } + } + + using (var ms = new MemoryStream()) + { + // Column labels + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("PastrySalesData"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = sheet.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + sheet.Cell("F11").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + sheet.Columns().AdjustToContents(); + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt"); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("PastryPivot", ptSheet.Cell(1, 1), table); + + pt.ColumnLabels.Add("Month"); + pt.ColumnLabels.Add("Name"); + + pt.RowLabels.Add("BakeDate"); + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var columnLabels = wb.Worksheets.SelectMany(ws => ws.PivotTables) + .First() + .ColumnLabels + .ToArray(); + + Assert.AreEqual("Month", columnLabels[0].SourceName); + Assert.AreEqual("Name", columnLabels[1].SourceName); + } + } + + using (var ms = new MemoryStream()) + { + // Row labels + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("PastrySalesData"); + // Insert our list of pastry data into the "PastrySalesData" sheet at cell 1,1 + var table = sheet.Cell(1, 1).InsertTable(pastries, "PastrySalesData", true); + sheet.Cell("F11").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + sheet.Columns().AdjustToContents(); + + IXLWorksheet ptSheet; + IXLPivotTable pt; + + // Add a new sheet for our pivot table + ptSheet = wb.Worksheets.Add("pvt"); + + // Create the pivot table, using the data from the "PastrySalesData" table + pt = ptSheet.PivotTables.Add("PastryPivot", ptSheet.Cell(1, 1), table); + + pt.RowLabels.Add("Month"); + pt.RowLabels.Add("Name"); + pt.RowLabels.Add(XLConstants.PivotTable.ValuesSentinalLabel); + + pt.ColumnLabels.Add("BakeDate"); + pt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var rowLabels = wb.Worksheets.SelectMany(ws => ws.PivotTables) + .First() + .RowLabels + .ToArray(); + + Assert.AreEqual("Month", rowLabels[0].SourceName); + Assert.AreEqual("Name", rowLabels[1].SourceName); + Assert.AreEqual("{{Values}}", rowLabels[2].SourceName); + } + } + } + + [Test] + public void MaintainPivotTableIntegrityOnMultipleSaves() + { + var pastries = new List + { + new Pastry("Croissant", 101, 150, 60.2, "", new DateTime(2016, 04, 21)), + new Pastry("Croissant", 101, 250, 50.42, "", new DateTime(2016, 05, 03)), + new Pastry("Croissant", 101, 134, 22.12, "", new DateTime(2016, 06, 24)), + new Pastry("Doughnut", 102, 250, 89.99, "", new DateTime(2017, 04, 23)), + new Pastry("Doughnut", 102, 225, 70, "", new DateTime(2016, 05, 24)), + new Pastry("Doughnut", 102, 210, 75.33, "", new DateTime(2016, 06, 02)), + new Pastry("Bearclaw", 103, 134, 10.24, "", new DateTime(2016, 04, 27)), + new Pastry("Bearclaw", 103, 184, 33.33, "", new DateTime(2016, 05, 20)), + new Pastry("Bearclaw", 103, 124, 25, "", new DateTime(2017, 06, 05)), + new Pastry("Danish", 104, 394, -20.24, "", null), + new Pastry("Danish", 104, 190, 60, "", new DateTime(2017, 05, 08)), + new Pastry("Danish", 104, 221, 24.76, "", new DateTime(2016, 06, 21)), + + // Deliberately add different casings of same string to ensure pivot table doesn't duplicate it. + new Pastry("Scone", 105, 135, 0, "", new DateTime(2017, 04, 22)), + new Pastry("SconE", 105, 122, 5.19, "", new DateTime(2017, 05, 03)), + new Pastry("SCONE", 105, 243, 44.2, "", new DateTime(2017, 06, 14)), + + // For ContainsBlank and integer rows/columns test + new Pastry("Scone", null, 255, 18.4, "", null), + }; + + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("PastrySalesData"); + var table = ws.FirstCell().InsertTable(pastries, "PastrySalesData", true); + + var pvtSheet = wb.Worksheets.Add("pvt"); + var pvt = table.CreatePivotTable(pvtSheet.FirstCell(), "PastryPvt"); + + pvt.ColumnLabels.Add("Month"); + pvt.RowLabels.Add("Name"); + pvt.Values.Add("NumberOfOrders").SetSummaryFormula(XLPivotSummary.Sum); + + //Deliberately try to save twice + wb.SaveAs(ms); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual(1, wb.Worksheets.SelectMany(ws => ws.PivotTables).Count()); + } + } + } + + [Test] + public void TwoPivotWithOneSourceTest() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\TwoPivotTablesWithSingleSource\input.xlsx"))) + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + var srcRange = wb.Range("Sheet1!$B$2:$H$207"); + + foreach (var pt in wb.Worksheets.SelectMany(ws => ws.PivotTables)) + { + pt.SourceRange = srcRange; + } + + return wb; + }, @"Other\PivotTableReferenceFiles\TwoPivotTablesWithSingleSource\output.xlsx"); + } + + [Test] + public void PivotSubtotalsLoadingTest() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\PivotSubtotalsSource\input.xlsx"))) + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + return wb; + }, @"Other\PivotTableReferenceFiles\PivotSubtotalsSource\input.xlsx"); + } + + [Test] + public void ClearPivotTableTenderedTange() + { + // https://github.com/ClosedXML/ClosedXML/pull/856 + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\ClearPivotTableRenderedRangeWhenLoading\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("Sheet1"); + Assert.IsTrue(ws.Cell("B1").IsEmpty()); + Assert.IsTrue(ws.Cell("C2").IsEmpty()); + Assert.IsTrue(ws.Cell("D5").IsEmpty()); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheet("Sheet1"); + Assert.IsTrue(ws.Cell("B1").IsEmpty()); + Assert.IsTrue(ws.Cell("C2").IsEmpty()); + Assert.IsTrue(ws.Cell("D5").IsEmpty()); + } + } + } + + private static void SetFieldOptions(IXLPivotField field, bool withDefaults) + { + field.SubtotalsAtTop = !withDefaults; + field.ShowBlankItems = !withDefaults; + field.Outline = !withDefaults; + field.Compact = !withDefaults; + field.Collapsed = withDefaults; + field.InsertBlankLines = withDefaults; + field.RepeatItemLabels = withDefaults; + field.InsertPageBreaks = withDefaults; + field.IncludeNewItemsInFilter = withDefaults; + } + + private static void AssertFieldOptions(IXLPivotField field, bool withDefaults) + { + Assert.AreEqual(!withDefaults, field.SubtotalsAtTop, "SubtotalsAtTop save failure"); + Assert.AreEqual(!withDefaults, field.ShowBlankItems, "ShowBlankItems save failure"); + Assert.AreEqual(!withDefaults, field.Outline, "Outline save failure"); + Assert.AreEqual(!withDefaults, field.Compact, "Compact save failure"); + Assert.AreEqual(withDefaults, field.Collapsed, "Collapsed save failure"); + Assert.AreEqual(withDefaults, field.InsertBlankLines, "InsertBlankLines save failure"); + Assert.AreEqual(withDefaults, field.RepeatItemLabels, "RepeatItemLabels save failure"); + Assert.AreEqual(withDefaults, field.InsertPageBreaks, "InsertPageBreaks save failure"); + Assert.AreEqual(withDefaults, field.IncludeNewItemsInFilter, "IncludeNewItemsInFilter save failure"); + } + } +} diff --git a/ClosedXML_Tests/Excel/Protection/HashAlgorithmTests.cs b/ClosedXML_Tests/Excel/Protection/HashAlgorithmTests.cs new file mode 100644 index 0000000..5955212 --- /dev/null +++ b/ClosedXML_Tests/Excel/Protection/HashAlgorithmTests.cs @@ -0,0 +1,30 @@ +using ClosedXML.Utils; +using NUnit.Framework; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML_Tests.Excel.Protection +{ + public class HashAlgorithmTests + { + [Test] + public void TestEmptyPassword() + { + Assert.IsEmpty(CryptographicAlgorithms.GetPasswordHash(Algorithm.SHA512, string.Empty)); + Assert.IsEmpty(CryptographicAlgorithms.GetPasswordHash(Algorithm.SimpleHash, string.Empty)); + } + + [Test] + public void TestSHA512() + { + var hash = CryptographicAlgorithms.GetPasswordHash(Algorithm.SHA512, "12345", "aVvPw1DNH3evPqRAd/y3UQ==", 100000); + Assert.AreEqual("E+qAhyIg/HM0dUrPaENfimFOZp7wlOkJsf/sdG+AGHOA9grOv7VLb1ik2vuYohljI9G36e0ea9wnixCK0MMuyQ==", hash); + } + + [Test] + public void TestSimple() + { + var hash = CryptographicAlgorithms.GetPasswordHash(Algorithm.SimpleHash, "12345"); + Assert.AreEqual("CA9C", hash); + } + } +} diff --git a/ClosedXML_Tests/Excel/Protection/XLSheetProtectionTests.cs b/ClosedXML_Tests/Excel/Protection/XLSheetProtectionTests.cs new file mode 100644 index 0000000..19498b5 --- /dev/null +++ b/ClosedXML_Tests/Excel/Protection/XLSheetProtectionTests.cs @@ -0,0 +1,193 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML_Tests.Excel.Worksheets +{ + [TestFixture] + public class XLSheetProtectionTests + { + [Test] + public void AllowEverything() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Protect().AllowedElements = XLSheetProtectionElements.Everything; + + foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast()) + Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString()); + } + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Protect().AllowElement(XLSheetProtectionElements.Everything); + + foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast()) + Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString()); + } + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Protect().AllowEverything(); + + foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast()) + Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString()); + } + } + + [Test] + public void AllowNothing() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Protect().AllowedElements = XLSheetProtectionElements.None; + + foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)) + .Cast() + .Where(e => e != XLSheetProtectionElements.None)) + + Assert.IsFalse(ws.Protection.AllowedElements.HasFlag(element), element.ToString()); + } + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Protect().AllowNone(); + + foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)) + .Cast() + .Where(e => e != XLSheetProtectionElements.None)) + + Assert.IsFalse(ws.Protection.AllowedElements.HasFlag(element), element.ToString()); + } + } + + [Test] + public void ChangeHashingAlgorithm() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + ws.Protect("123", Algorithm.SimpleHash); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.IsTrue(ws.Protection.IsProtected); + Assert.AreEqual(Algorithm.SimpleHash, ws.Protection.Algorithm); + + ws.Unprotect("123"); + ws.Protect("123", Algorithm.SHA512); + wb.Save(); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.IsTrue(ws.Protection.IsProtected); + Assert.AreEqual(Algorithm.SHA512, ws.Protection.Algorithm); + + Assert.DoesNotThrow(() => ws.Unprotect("123")); + } + } + } + + [Test] + public void CopyProtectionFromAnotherSheet() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\SheetProtection.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws1 = wb.Worksheet("Protected Password = 123"); + var p1 = ws1.Protection.CastTo(); + Assert.IsTrue(p1.IsProtected); + + var ws2 = ws1.CopyTo("New worksheet"); + Assert.IsFalse(ws2.Protection.IsProtected); + var p2 = ws2.Protection.CopyFrom(p1).CastTo(); + + Assert.IsTrue(p2.IsProtected); + Assert.IsTrue(p2.IsPasswordProtected); + Assert.AreEqual(p1.Algorithm, p2.Algorithm); + Assert.AreEqual(p1.PasswordHash, p2.PasswordHash); + Assert.AreEqual(p1.Base64EncodedSalt, p2.Base64EncodedSalt); + Assert.AreEqual(p1.SpinCount, p2.SpinCount); + + Assert.IsTrue(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertColumns)); + Assert.IsTrue(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertRows)); + Assert.IsFalse(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertHyperlinks)); + + Assert.Throws(() => ws2.Unprotect()); + ws2.Unprotect("123"); + } + } + + [Test] + public void SetWorksheetProtectionCloning() + { + var ws1 = new XLWorkbook().AddWorksheet(); + var ws2 = new XLWorkbook().AddWorksheet(); + + ws1.Protect("123") + .AllowElement(XLSheetProtectionElements.FormatEverything) + .DisallowElement(XLSheetProtectionElements.FormatCells); + + Assert.AreEqual(XLSheetProtectionElements.FormatColumns | XLSheetProtectionElements.FormatRows | XLSheetProtectionElements.SelectEverything, ws1.Protection.AllowedElements); + + ws2.Protection = ws1.Protection; + + Assert.IsFalse(ReferenceEquals(ws1.Protection, ws2.Protection)); + Assert.IsTrue(ws2.Protection.IsProtected); + Assert.AreEqual(XLSheetProtectionElements.FormatColumns | XLSheetProtectionElements.FormatRows | XLSheetProtectionElements.SelectEverything, ws2.Protection.AllowedElements); + Assert.AreEqual((ws1.Protection as XLSheetProtection).PasswordHash, (ws2.Protection as XLSheetProtection).PasswordHash); + } + + [Test] + public void TestUnprotectWorksheetWithNoPassword() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\SHA512PasswordProtection.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("Sheet1"); + Assert.IsTrue(ws.Protection.IsProtected); + ws.Unprotect(); + Assert.IsFalse(ws.Protection.IsProtected); + } + } + + [Test] + public void TestWorksheetWithSHA512Protection() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\SHA512PasswordProtection.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("Sheet2"); + Assert.IsTrue(ws.Protection.IsProtected); + + // Password required + Assert.Throws(() => ws.Unprotect()); + + Assert.AreEqual(Algorithm.SHA512, ws.Protection.Algorithm); + ws.Unprotect("abc"); + Assert.IsFalse(ws.Protection.IsProtected); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Protection/XLWorkbookProtectionTests.cs b/ClosedXML_Tests/Excel/Protection/XLWorkbookProtectionTests.cs new file mode 100644 index 0000000..4a690e2 --- /dev/null +++ b/ClosedXML_Tests/Excel/Protection/XLWorkbookProtectionTests.cs @@ -0,0 +1,329 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static ClosedXML.Excel.XLProtectionAlgorithm; + +namespace ClosedXML_Tests +{ + public class XLWorkbookProtectionTests + { + [Test] + public void CanChangeProtectionAlgorithm() + { + using (var ms = new MemoryStream()) + { + using (var stream = GetProtectedWorkbookStreamWithPassword()) + using (var wb = new XLWorkbook(stream)) + { + Assert.AreEqual(Algorithm.SHA512, wb.Protection.Algorithm); + wb.Unprotect("12345"); + wb.Protect("12345", Algorithm.SimpleHash); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsTrue(wb.IsPasswordProtected); + Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm); + } + } + } + + [Test] + public void CanChangeToPasswordProtected() + { + using (var ms = new MemoryStream()) + { + using (var stream = GetProtectedWorkbookStreamWithoutPassword()) + using (var wb = new XLWorkbook(stream)) + + { + wb.Unprotect(); + wb.Protection.Protect("12345"); + + Assert.IsTrue(wb.Protection.IsPasswordProtected); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsTrue(wb.Protection.IsPasswordProtected); + Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm); + Assert.AreNotEqual("", wb.Protection.PasswordHash); + } + } + } + + [Test] + public void CanChangeToProtectedWithoutPassword() + { + using (var ms = new MemoryStream()) + { + using (var stream = GetProtectedWorkbookStreamWithPassword()) + using (var wb = new XLWorkbook(stream)) + + { + wb.Unprotect("12345"); + wb.Protection.Protect(); + + Assert.IsFalse(wb.Protection.IsPasswordProtected); + Assert.IsTrue(wb.Protection.IsProtected); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsFalse(wb.Protection.IsPasswordProtected); + Assert.IsTrue(wb.Protection.IsProtected); + Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm); + Assert.AreEqual("", wb.Protection.PasswordHash); + } + } + } + + [Test] + public void CannotUnprotectIfNoPassword() + { + using (var stream = GetProtectedWorkbookStreamWithoutPassword()) + using (var wb = new XLWorkbook(stream)) + { + var ex = Assert.Throws(() => wb.Unprotect("dummy password")); + Assert.AreEqual("Invalid password", ex.Message); + } + } + + [Test] + public void CannotUnprotectWithoutPassword() + { + using (var stream = GetProtectedWorkbookStreamWithPassword()) + using (var wb = new XLWorkbook(stream)) + { + var ex = Assert.Throws(() => wb.Unprotect()); + Assert.AreEqual("The workbook structure is password protected", ex.Message); + } + } + + [Test] + [Theory] + public void CanProtectWithPassword(Algorithm algorithm) + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet(); + + Assert.IsFalse(wb.Protection.IsProtected); + + wb.Protection.Protect("12345", algorithm); + + wb.Protection.AllowNone(); + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure)); + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows)); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsTrue(wb.Protection.IsPasswordProtected); + Assert.IsTrue(wb.Protection.IsProtected); + + Assert.AreEqual(algorithm, wb.Protection.Algorithm); + Assert.AreNotEqual("", wb.Protection.PasswordHash); + + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure)); + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows)); + + var ex = Assert.Throws(() => wb.Unprotect("dummy password")); + Assert.AreEqual("Invalid password", ex.Message); + + wb.Protection.Unprotect("12345"); + + wb.Save(); + } + } + } + + [Test] + public void CanUnprotectWithoutPassword() + { + using (var ms = new MemoryStream()) + { + using (var stream = GetProtectedWorkbookStreamWithoutPassword()) + using (var wb = new XLWorkbook(stream)) + { + // Unprotect without password + wb.Unprotect(); + + Assert.IsFalse(wb.Protection.IsProtected); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsFalse(wb.Protection.IsProtected); + } + } + } + + [Test] + public void CanUnprotectWithPassword() + { + using (var ms = new MemoryStream()) + { + using (var stream = GetProtectedWorkbookStreamWithPassword()) + using (var wb = new XLWorkbook(stream)) + { + // Unprotect with password + wb.Unprotect("12345"); + + Assert.IsFalse(wb.Protection.IsProtected); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + Assert.IsFalse(wb.Protection.IsProtected); + } + } + } + + [Test] + public void CopyProtectionFromAnotherWorkbook() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\WorkbookProtection.xlsx"))) + using (var wb1 = new XLWorkbook(stream)) + using (var wb2 = new XLWorkbook()) + { + wb2.AddWorksheet(); + + var p1 = wb1.Protection.CastTo(); + Assert.IsTrue(p1.IsProtected); + + Assert.IsFalse(wb2.Protection.IsProtected); + var p2 = wb2.Protection.CopyFrom(wb1.Protection).CastTo(); + + Assert.IsTrue(p2.IsProtected); + Assert.IsTrue(p2.IsPasswordProtected); + Assert.AreEqual(p1.Algorithm, p2.Algorithm); + Assert.AreEqual(p1.PasswordHash, p2.PasswordHash); + Assert.AreEqual(p1.Base64EncodedSalt, p2.Base64EncodedSalt); + Assert.AreEqual(p1.SpinCount, p2.SpinCount); + + Assert.IsTrue(p2.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows)); + Assert.IsFalse(p2.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure)); + + Assert.Throws(() => wb2.Unprotect()); + wb2.Unprotect("Abc@123"); + } + } + + [Test] + public void IXLProtectableTests() + { + using var wb = new XLWorkbook(); + Enumerable.Range(1, 5).ForEach(i => wb.AddWorksheet()); + + var list = new List() { wb }; + list.AddRange(wb.Worksheets); + + list.ForEach(el => el.Protect()); + + list.ForEach(el => Assert.IsTrue(el.IsProtected)); + list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected)); + + list.ForEach(el => el.Unprotect()); + + list.ForEach(el => Assert.IsFalse(el.IsProtected)); + list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected)); + + list.ForEach(el => el.Protect("password")); + + list.ForEach(el => Assert.IsTrue(el.IsProtected)); + list.ForEach(el => Assert.IsTrue(el.IsPasswordProtected)); + + list.ForEach(el => el.Unprotect("password")); + + list.ForEach(el => Assert.IsFalse(el.IsProtected)); + list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected)); + } + + [Test] + public void LoadProtectionWithoutPasswordFromFile() + { + using (var stream = GetProtectedWorkbookStreamWithoutPassword()) + using (var wb = new XLWorkbook(stream)) + { + Assert.IsFalse(wb.Protection.IsPasswordProtected); + Assert.IsTrue(wb.Protection.IsProtected); + Assert.AreEqual("", wb.Protection.PasswordHash); + Assert.IsTrue(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows)); + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure)); + } + } + + [Test] + public void LoadProtectionWithPasswordFromFile() + { + using (var stream = GetProtectedWorkbookStreamWithPassword()) + using (var wb = new XLWorkbook(stream)) + { + Assert.IsTrue(wb.Protection.IsPasswordProtected); + Assert.AreNotEqual("", wb.Protection.PasswordHash); + Assert.IsTrue(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows)); + Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure)); + } + } + + [Test] + public void SetWorkbookProtectionCloning() + { + var wb1 = new XLWorkbook(); + var wb2 = new XLWorkbook(); + + wb1.AddWorksheet(); + wb2.AddWorksheet(); + + wb1.Protect("123", Algorithm.SHA512) + .AllowElement(XLWorkbookProtectionElements.Windows) + .DisallowElement(XLWorkbookProtectionElements.Structure); + + Assert.IsTrue(wb1.Protection.IsProtected); + + Assert.AreEqual(XLWorkbookProtectionElements.Windows, wb1.Protection.AllowedElements); + + wb2.Protection = wb1.Protection; + + Assert.IsFalse(ReferenceEquals(wb1.Protection, wb2.Protection)); + Assert.IsTrue(wb2.Protection.IsProtected); + Assert.AreEqual(XLWorkbookProtectionElements.Windows, wb2.Protection.AllowedElements); + Assert.AreEqual(wb1.Protection.PasswordHash, wb2.Protection.PasswordHash); + } + + private Stream GetProtectedWorkbookStreamWithoutPassword() => TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Protection\protectstructurewithoutpassword.xlsx")); + + private Stream GetProtectedWorkbookStreamWithPassword() => TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Protection\protectstructurewithpassword.xlsx")); + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/CopyingRangesTests.cs b/ClosedXML_Tests/Excel/Ranges/CopyingRangesTests.cs new file mode 100644 index 0000000..1925298 --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/CopyingRangesTests.cs @@ -0,0 +1,152 @@ +using System.Drawing; +using System.Linq; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class CopyingRangesTests + { + [Test] + public void CopyingColumns() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + IXLColumn column1 = ws.Column(1); + column1.Cell(1).Style.Fill.SetBackgroundColor(XLColor.Red); + column1.Cell(2).Style.Fill.SetBackgroundColor(XLColor.FromArgb(1, 1, 1)); + column1.Cell(3).Style.Fill.SetBackgroundColor(XLColor.FromHtml("#CCCCCC")); + column1.Cell(4).Style.Fill.SetBackgroundColor(XLColor.FromIndex(26)); + column1.Cell(5).Style.Fill.SetBackgroundColor(XLColor.FromColor(Color.MediumSeaGreen)); + column1.Cell(6).Style.Fill.SetBackgroundColor(XLColor.FromName("Blue")); + column1.Cell(7).Style.Fill.SetBackgroundColor(XLColor.FromTheme(XLThemeColor.Accent3)); + + ws.Cell(1, 2).Value = column1; + ws.Cell(1, 3).Value = column1.Column(1, 7); + + IXLColumn column2 = ws.Column(2); + Assert.AreEqual(XLColor.Red, column2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromArgb(1, 1, 1), column2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromHtml("#CCCCCC"), column2.Cell(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromIndex(26), column2.Cell(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromColor(Color.MediumSeaGreen), + column2.Cell(5).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromName("Blue"), column2.Cell(6).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromTheme(XLThemeColor.Accent3), column2.Cell(7).Style.Fill.BackgroundColor); + + IXLColumn column3 = ws.Column(3); + Assert.AreEqual(XLColor.Red, column3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromArgb(1, 1, 1), column3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromHtml("#CCCCCC"), column3.Cell(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromIndex(26), column3.Cell(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromColor(Color.MediumSeaGreen), + column3.Cell(5).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromName("Blue"), column3.Cell(6).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromTheme(XLThemeColor.Accent3), column3.Cell(7).Style.Fill.BackgroundColor); + } + + [Test] + public void CopyingRows() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + IXLRow row1 = ws.Row(1); + FillRow(row1); + + ws.Cell(2, 1).Value = row1; + ws.Cell(3, 1).Value = row1.Row(1, 7); + + IXLRow row2 = ws.Row(2); + Assert.AreEqual(XLColor.Red, row2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromArgb(1, 1, 1), row2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromHtml("#CCCCCC"), row2.Cell(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromIndex(26), row2.Cell(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromColor(Color.MediumSeaGreen), row2.Cell(5).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromName("Blue"), row2.Cell(6).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromTheme(XLThemeColor.Accent3), row2.Cell(7).Style.Fill.BackgroundColor); + + IXLRow row3 = ws.Row(3); + Assert.AreEqual(XLColor.Red, row3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromArgb(1, 1, 1), row3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromHtml("#CCCCCC"), row3.Cell(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromIndex(26), row3.Cell(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromColor(Color.MediumSeaGreen), row3.Cell(5).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromName("Blue"), row3.Cell(6).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FromTheme(XLThemeColor.Accent3), row3.Cell(7).Style.Fill.BackgroundColor); + + Assert.AreEqual(3, ws.ConditionalFormats.Count()); + Assert.IsTrue(ws.ConditionalFormats.Single(x => x.Range.RangeAddress.ToStringRelative() == "B1:B1").Values.Any(v => v.Value.Value == "G1" && v.Value.IsFormula)); + Assert.IsTrue(ws.ConditionalFormats.Single(x => x.Range.RangeAddress.ToStringRelative() == "B2:B2").Values.Any(v => v.Value.Value == "G2" && v.Value.IsFormula)); + Assert.IsTrue(ws.ConditionalFormats.Single(x => x.Range.RangeAddress.ToStringRelative() == "B3:B3").Values.Any(v => v.Value.Value == "G3" && v.Value.IsFormula)); + } + + [Test] + public void CopyingConditionalFormats() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + FillRow(ws.Row(1)); + FillRow(ws.Row(2)); + FillRow(ws.Row(3)); + + ((XLConditionalFormats)ws.ConditionalFormats).Consolidate(); + + ws.Cell(5, 2).Value = ws.Row(2).Row(1, 7); + + Assert.AreEqual(2, ws.ConditionalFormats.Count()); + Assert.IsTrue(ws.ConditionalFormats.Single(x => x.Range.RangeAddress.ToStringRelative() == "B1:B3").Values.Any(v => v.Value.Value == "G1" && v.Value.IsFormula)); + Assert.IsTrue(ws.ConditionalFormats.Single(x => x.Range.RangeAddress.ToStringRelative() == "C5:C5").Values.Any(v => v.Value.Value == "H5" && v.Value.IsFormula)); + } + + [Test] + public void CopyingConditionalFormatsDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var format = ws1.Range("A1:J2").AddConditionalFormat(); + + var address = format.Ranges + .First() + .FirstCell() + .CellRight(4) + .Address + .ToStringRelative(); + + format.WhenEquals("=" + address) + .Fill + .SetBackgroundColor(XLColor.Blue); + + var ws2 = wb.Worksheets.Add("Sheet2"); + + ws2.FirstCell().Value = ws1.Range("B1:B4"); + + Assert.AreEqual(1, ws2.ConditionalFormats.Count()); + Assert.IsTrue(ws2.ConditionalFormats.All(x => x.Ranges.All(s => s.Worksheet == ws2)), "A conditional format was created for another worksheet."); + Assert.IsTrue(ws2.ConditionalFormats + .Single(x => x.Range.RangeAddress.ToStringRelative() == "A1:A2") + .Values.Any(v => v.Value.Value == "E1" && v.Value.IsFormula), "The formula has not been transferred correctly."); + + Assert.AreEqual("Sheet1", ws1.ConditionalFormats.First().Ranges.First().Worksheet.Name); + Assert.AreEqual("Sheet2", ws2.ConditionalFormats.First().Ranges.First().Worksheet.Name); + Assert.AreEqual("A1:J2", ws1.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + Assert.AreEqual("A1:A2", ws2.ConditionalFormats.First().Ranges.First().RangeAddress.ToString()); + } + + private static void FillRow(IXLRow row1) + { + row1.Cell(1).Style.Fill.SetBackgroundColor(XLColor.Red); + row1.Cell(2).Style.Fill.SetBackgroundColor(XLColor.FromArgb(1, 1, 1)); + row1.Cell(3).Style.Fill.SetBackgroundColor(XLColor.FromHtml("#CCCCCC")); + row1.Cell(4).Style.Fill.SetBackgroundColor(XLColor.FromIndex(26)); + row1.Cell(5).Style.Fill.SetBackgroundColor(XLColor.FromColor(Color.MediumSeaGreen)); + row1.Cell(6).Style.Fill.SetBackgroundColor(XLColor.FromName("Blue")); + row1.Cell(7).Style.Fill.SetBackgroundColor(XLColor.FromTheme(XLThemeColor.Accent3)); + + row1.Cell(2).AddConditionalFormat().WhenEquals("=" + row1.FirstCell().CellRight(6).Address.ToStringRelative()).Fill.SetBackgroundColor(XLColor.Blue); + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs b/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs new file mode 100644 index 0000000..2f22547 --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs @@ -0,0 +1,298 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Linq; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class InsertingRangesTests + { + [Test] + public void InsertingColumnsPreservesFormatting() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + IXLColumn column1 = ws.Column(1); + column1.Style.Fill.SetBackgroundColor(XLColor.FrenchLilac); + column1.Cell(2).Style.Fill.SetBackgroundColor(XLColor.Fulvous); + IXLColumn column2 = ws.Column(2); + column2.Style.Fill.SetBackgroundColor(XLColor.Xanadu); + column2.Cell(2).Style.Fill.SetBackgroundColor(XLColor.MacaroniAndCheese); + + column1.InsertColumnsAfter(1); + column1.InsertColumnsBefore(1); + column2.InsertColumnsBefore(1); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Column(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Column(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Column(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Column(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Xanadu, ws.Column(5).Style.Fill.BackgroundColor); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Cell(2, 1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(2, 2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(2, 3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(2, 4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.MacaroniAndCheese, ws.Cell(2, 5).Style.Fill.BackgroundColor); + } + + [Test] + public void InsertingRowsAbove() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + ws.Cell("B3").SetValue("X") + .CellBelow().SetValue("B"); + + IXLRangeRow r = ws.Range("B4").InsertRowsAbove(1).First(); + r.Cell(1).SetValue("A"); + + Assert.AreEqual("X", ws.Cell("B3").GetString()); + Assert.AreEqual("A", ws.Cell("B4").GetString()); + Assert.AreEqual("B", ws.Cell("B5").GetString()); + } + + [Test] + public void InsertingRowsPreservesFormatting() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + IXLRow row1 = ws.Row(1); + row1.Style.Fill.SetBackgroundColor(XLColor.FrenchLilac); + row1.Cell(2).Style.Fill.SetBackgroundColor(XLColor.Fulvous); + IXLRow row2 = ws.Row(2); + row2.Style.Fill.SetBackgroundColor(XLColor.Xanadu); + row2.Cell(2).Style.Fill.SetBackgroundColor(XLColor.MacaroniAndCheese); + + row1.InsertRowsBelow(1); + row1.InsertRowsAbove(1); + row2.InsertRowsAbove(1); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Row(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Row(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Row(3).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.FrenchLilac, ws.Row(4).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Xanadu, ws.Row(5).Style.Fill.BackgroundColor); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Cell(1, 2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(2, 2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(3, 2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Fulvous, ws.Cell(4, 2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.MacaroniAndCheese, ws.Cell(5, 2).Style.Fill.BackgroundColor); + } + + [Test] + public void InsertingRowsPreservesComments() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Cell("A1").SetValue("Insert Below"); + ws.Cell("A2").SetValue("Already existing cell"); + ws.Cell("A3").SetValue("Cell with comment").Comment.AddText("Comment here"); + + ws.Row(1).InsertRowsBelow(2); + Assert.AreEqual("Comment here", ws.Cell("A5").Comment.Text); + } + + [Test] + public void InsertingColumnsPreservesComments() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Cell("A1").SetValue("Insert to the right"); + ws.Cell("B1").SetValue("Already existing cell"); + ws.Cell("C1").SetValue("Cell with comment").Comment.AddText("Comment here"); + + ws.Column(1).InsertColumnsAfter(2); + Assert.AreEqual("Comment here", ws.Cell("E1").Comment.Text); + } + + [Test] + [TestCase("C4:F7", "C4:F7", 2, "E4:H7")] // Coincide, shift right + [TestCase("C4:F7", "C4:F7", -2, "C4:D7")] // Coincide, shift left + [TestCase("D5:E6", "C4:F7", 2, "F5:G6")] // Inside, shift right + [TestCase("D5:E6", "C4:F7", -2, "C5:C6")] // Inside, shift left + [TestCase("B4:G7", "C4:F7", 2, "B4:I7")] // Includes, shift right + [TestCase("B4:G7", "C4:F7", -2, "B4:E7")] // Includes, shift left + [TestCase("B4:E7", "C4:F7", 2, "B4:G7")] // Intersects at left, shift right + [TestCase("B4:E7", "C4:F7", -2, "B4:C7")] // Intersects at left, shift left + [TestCase("D4:G7", "C4:F7", 2, "F4:I7")] // Intersects at right, shift right + [TestCase("D4:G7", "C4:F7", -2, "C4:E7")] // Intersects at right, shift left + [TestCase("A5:B6", "C4:F7", 2, "A5:B6")] // No intersection, at left, shift right + [TestCase("A5:B6", "C4:F7", -1, "A5:B6")] // No intersection, at left, shift left + [TestCase("H5:I6", "C4:F7", 2, "J5:K6")] // No intersection, at right, shift right + [TestCase("H5:I6", "C4:F7", -2, "F5:G6")] // No intersection, at right, shift left + [TestCase("C8:F11", "C4:F7", 2, "C8:F11")] // Different rows + [TestCase("B1:B8", "A1:C4", 1, "B1:B8")] // More rows, shift right + [TestCase("B1:B8", "A1:C4", -1, "B1:B8")] // More rows, shift left + public void ShiftColumnsValid(string thisRangeAddress, string shiftedRangeAddress, int shiftedColumns, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + var thisRange = ws.Range(thisRangeAddress) as XLRange; + var shiftedRange = ws.Range(shiftedRangeAddress) as XLRange; + + thisRange.WorksheetRangeShiftedColumns(shiftedRange, shiftedColumns); + + Assert.IsTrue(thisRange.RangeAddress.IsValid); + Assert.AreEqual(expectedRange, thisRange.RangeAddress.ToString()); + } + } + + [Test] + [TestCase("B1:B4", "A1:C4", -2)] // Shift left too much + public void ShiftColumnsInvalid(string thisRangeAddress, string shiftedRangeAddress, int shiftedColumns) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + var thisRange = ws.Range(thisRangeAddress) as XLRange; + var shiftedRange = ws.Range(shiftedRangeAddress) as XLRange; + + thisRange.WorksheetRangeShiftedColumns(shiftedRange, shiftedColumns); + + Assert.IsFalse(thisRange.RangeAddress.IsValid); + } + } + + [Test] + [TestCase("C4:F7", "C4:F7", 2, "C6:F9")] // Coincide, shift down + [TestCase("C4:F7", "C4:F7", -2, "C4:F5")] // Coincide, shift up + [TestCase("D5:E6", "C4:F7", 2, "D7:E8")] // Inside, shift down + [TestCase("D5:E6", "C4:F7", -2, "D4:E4")] // Inside, shift up + [TestCase("C3:F8", "C4:F7", 2, "C3:F10")] // Includes, shift down + [TestCase("C3:F8", "C4:F7", -2, "C3:F6")] // Includes, shift up + [TestCase("C3:F6", "C4:F7", 2, "C3:F8")] // Intersects at top, shift down + [TestCase("C2:F6", "C4:F7", -3, "C2:F3")] // Intersects at top, shift up to the sheet boundary + [TestCase("C3:F6", "C4:F7", -2, "C3:F4")] // Intersects at top, shift up + [TestCase("C5:F8", "C4:F7", 2, "C7:F10")] // Intersects at bottom, shift down + [TestCase("C5:F8", "C4:F7", -2, "C4:F6")] // Intersects at bottom, shift up + [TestCase("C1:F3", "C4:F7", 2, "C1:F3")] // No intersection, at top, shift down + [TestCase("C1:F3", "C4:F7", -2, "C1:F3")] // No intersection, at top, shift up + [TestCase("C8:F10", "C4:F7", 2, "C10:F12")] // No intersection, at bottom, shift down + [TestCase("C8:F10", "C4:F7", -2, "C6:F8")] // No intersection, at bottom, shift up + [TestCase("G4:J7", "C4:F7", 2, "G4:J7")] // Different columns + [TestCase("A2:D2", "A1:C4", 1, "A2:D2")] // More columns, shift down + [TestCase("A2:D2", "A1:C4", -1, "A2:D2")] // More columns, shift up + public void ShiftRowsValid(string thisRangeAddress, string shiftedRangeAddress, int shiftedRows, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + var thisRange = ws.Range(thisRangeAddress) as XLRange; + var shiftedRange = ws.Range(shiftedRangeAddress) as XLRange; + + thisRange.WorksheetRangeShiftedRows(shiftedRange, shiftedRows); + + Assert.IsTrue(thisRange.RangeAddress.IsValid); + Assert.AreEqual(expectedRange, thisRange.RangeAddress.ToString()); + } + } + + [Test] + [TestCase("A2:C2", "A1:C4", -2)] // Shift up too much + public void ShiftRowsInvalid(string thisRangeAddress, string shiftedRangeAddress, int shiftedRows) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + var thisRange = ws.Range(thisRangeAddress) as XLRange; + var shiftedRange = ws.Range(shiftedRangeAddress) as XLRange; + + thisRange.WorksheetRangeShiftedRows(shiftedRange, shiftedRows); + + Assert.IsFalse(thisRange.RangeAddress.IsValid); + } + } + + [Test] + public void InsertZeroColumnsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsAfter(0)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsBefore(0)); + } + + [Test] + public void InsertNegativeNumberOfColumnsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsAfter(-1)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsBefore(-1)); + } + + [Test] + public void InsertTooLargeNumberOfColumnsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsAfter(16385)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertColumnsBefore(16385)); + } + + [Test] + public void InsertZeroRowsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsAbove(0)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsBelow(0)); + } + + [Test] + public void InsertNegativeNumberOfRowsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsAbove(-1)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsBelow(-1)); + } + + [Test] + public void InsertTooLargeNumberOrRowsFails() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var range = ws.FirstCell().AsRange(); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsAbove(1048577)); + Assert.Throws(typeof(ArgumentOutOfRangeException), () => range.InsertRowsBelow(1048577)); + } + + [Test] + public void MergedRangesConsistencyWhenInsertingRows() + { + // https://github.com/ClosedXML/ClosedXML/issues/1013 + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + //create merged row + ws.Cell("A1").Value = "Merged Row(1) of Range (A1:F1)"; + ws.Range("A1:F1").Row(1).Merge(); + + var row = ws.FirstRow(); + + // Add some lines and copy format & merging + for (var r = 1; r <= 10; r++) + { + row.InsertRowsBelow(1); // insert a row below row 1, as a row 2 + row.CopyTo(row.RowBelow()); // copy format and merging from row 1 to row 2 + + var duplicates = ws.MergedRanges + .GroupBy(s => s.ToString()) + .Where(g => g.Count() > 1) + .Select(y => new { Element = y.Key, Counter = y.Count() }) + .ToList(); + + Assert.AreEqual(0, duplicates.Count); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/MergedRangesTests.cs b/ClosedXML_Tests/Excel/Ranges/MergedRangesTests.cs new file mode 100644 index 0000000..25599f8 --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/MergedRangesTests.cs @@ -0,0 +1,443 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class MergedRangesTests + { + [Test] + public void LastCellFromMerge() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + ws.Range("B2:D4").Merge(); + + string first = ws.FirstCellUsed(XLCellsUsedOptions.All).Address.ToStringRelative(); + string last = ws.LastCellUsed(XLCellsUsedOptions.All).Address.ToStringRelative(); + + Assert.AreEqual("B2", first); + Assert.AreEqual("D4", last); + } + + [TestCase("A1:A2", "A1:A2")] + [TestCase("A2:B2", "A2:B2")] + [TestCase("A3:C3", "A3:E3")] + [TestCase("B4:B6", "B4:B6")] + [TestCase("C7:D7", "E7:F7")] + public void MergedRangesShiftedOnColumnInsert(string originalRange, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + var range = ws.Range(originalRange).Merge(); + + ws.Column(2).InsertColumnsAfter(2); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(1, mr.Length); + Assert.AreSame(range, mr.Single()); + Assert.AreEqual(expectedRange, range.RangeAddress.ToString()); + } + } + + [TestCase("A1:B1", "A1:B1")] + [TestCase("B1:B2", "B1:B2")] + [TestCase("C1:C3", "C1:C5")] + [TestCase("D2:F2", "D2:F2")] + [TestCase("G4:G5", "G6:G7")] + public void MergedRangesShiftedOnRowInsert(string originalRange, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + var range = ws.Range(originalRange).Merge(); + + ws.Row(2).InsertRowsBelow(2); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(1, mr.Length); + Assert.AreSame(range, mr.Single()); + Assert.AreEqual(expectedRange, range.RangeAddress.ToString()); + } + } + + [TestCase("A1:A2", true, "A1:A2")] + [TestCase("A2:B2", true, "A2:A2")] + [TestCase("A3:C3", true, "A3:B3")] + [TestCase("B4:B6", false, "")] + [TestCase("C7:D7", true, "B7:C7")] + public void MergedRangesShiftedOnColumnDelete(string originalRange, bool expectedExist, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + var range = ws.Range(originalRange).Merge(); + + ws.Column(2).Delete(); + + var mr = ws.MergedRanges.ToArray(); + if (expectedExist) + { + Assert.AreEqual(1, mr.Length); + Assert.AreSame(range, mr.Single()); + Assert.AreEqual(expectedRange, range.RangeAddress.ToString()); + } + else + { + Assert.AreEqual(0, mr.Length); + Assert.IsFalse(range.RangeAddress.IsValid); + } + } + } + + [TestCase("A1:B1", true, "A1:B1")] + [TestCase("B1:B2", true, "B1:B1")] + [TestCase("C1:C3", true, "C1:C2")] + [TestCase("D2:F2", false, "")] + [TestCase("G4:G5", true, "G3:G4")] + public void MergedRangesShiftedOnRowDelete(string originalRange, bool expectedExist, string expectedRange) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + var range = ws.Range(originalRange).Merge(); + + ws.Row(2).Delete(); + + var mr = ws.MergedRanges.ToArray(); + if (expectedExist) + { + Assert.AreEqual(1, mr.Length); + Assert.AreSame(range, mr.Single()); + Assert.AreEqual(expectedRange, range.RangeAddress.ToString()); + } + else + { + Assert.AreEqual(0, mr.Length); + Assert.IsFalse(range.RangeAddress.IsValid); + } + } + } + + [Test] + public void ShiftRangeRightBreaksMerges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + ws.Range("B2:C3").Merge(); + ws.Range("B4:C5").Merge(); + ws.Range("F2:G3").Merge(); // to be broken + ws.Range("F4:G5").Merge(); // to be broken + ws.Range("H1:I2").Merge(); + ws.Range("H5:I6").Merge(); + + ws.Range("D3:E4").InsertColumnsAfter(2); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(4, mr.Length); + Assert.AreEqual("H1:I2", mr[0].RangeAddress.ToString()); + Assert.AreEqual("B2:C3", mr[1].RangeAddress.ToString()); + Assert.AreEqual("B4:C5", mr[2].RangeAddress.ToString()); + Assert.AreEqual("H5:I6", mr[3].RangeAddress.ToString()); + } + } + + [Test] + public void ShiftRangeLeftBreaksMerges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + ws.Range("B2:C3").Merge(); + ws.Range("B4:C5").Merge(); + ws.Range("F2:G3").Merge(); // to be broken + ws.Range("F4:G5").Merge(); // to be broken + ws.Range("H1:I2").Merge(); + ws.Range("H5:I6").Merge(); + + ws.Range("D3:E4").Delete(XLShiftDeletedCells.ShiftCellsLeft); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(4, mr.Length); + Assert.AreEqual("H1:I2", mr[0].RangeAddress.ToString()); + Assert.AreEqual("B2:C3", mr[1].RangeAddress.ToString()); + Assert.AreEqual("B4:C5", mr[2].RangeAddress.ToString()); + Assert.AreEqual("H5:I6", mr[3].RangeAddress.ToString()); + } + } + + [Test] + public void RangeShiftDownBreaksMerges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + ws.Range("B2:C3").Merge(); + ws.Range("D2:E3").Merge(); + ws.Range("B6:C7").Merge(); // to be broken + ws.Range("D6:E7").Merge(); // to be broken + ws.Range("A8:B9").Merge(); + ws.Range("E8:F9").Merge(); + + ws.Range("C4:D5").InsertRowsBelow(2); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(4, mr.Length); + Assert.AreEqual("B2:C3", mr[0].RangeAddress.ToString()); + Assert.AreEqual("D2:E3", mr[1].RangeAddress.ToString()); + Assert.AreEqual("A8:B9", mr[2].RangeAddress.ToString()); + Assert.AreEqual("E8:F9", mr[3].RangeAddress.ToString()); + } + } + + [Test] + public void RangeShiftUpBreaksMerges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("MRShift"); + ws.Range("B2:C3").Merge(); + ws.Range("D2:E3").Merge(); + ws.Range("B6:C7").Merge(); // to be broken + ws.Range("D6:E7").Merge(); // to be broken + ws.Range("A8:B9").Merge(); + ws.Range("E8:F9").Merge(); + + ws.Range("C4:D5").Delete(XLShiftDeletedCells.ShiftCellsUp); + + var mr = ws.MergedRanges.ToArray(); + Assert.AreEqual(4, mr.Length); + Assert.AreEqual("B2:C3", mr[0].RangeAddress.ToString()); + Assert.AreEqual("D2:E3", mr[1].RangeAddress.ToString()); + Assert.AreEqual("A8:B9", mr[2].RangeAddress.ToString()); + Assert.AreEqual("E8:F9", mr[3].RangeAddress.ToString()); + } + } + + [Test] + public void MergedCellsAcquireFirstCellStyle() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell("A2").Style.Fill.BackgroundColor = XLColor.Yellow; + ws.Cell("A3").Style.Fill.BackgroundColor = XLColor.Green; + ws.Range("A1:A3").Merge(); + + Assert.AreEqual(XLColor.Red, ws.Cell("A1").Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Cell("A2").Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Cell("A3").Style.Fill.BackgroundColor); + } + } + + [Test] + public void MergedCellsLooseData() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Range("A1:A3").SetValue(100); + ws.Range("A1:A3").Merge(); + + Assert.AreEqual(100, ws.Cell("A1").Value); + Assert.AreEqual("", ws.Cell("A2").Value); + Assert.AreEqual("", ws.Cell("A3").Value); + } + } + + [Test] + public void MergedCellsLooseConditionalFormats() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").AddConditionalFormat().WhenContains("1").Fill.BackgroundColor = XLColor.Red; + ws.Cell("A2").AddConditionalFormat().WhenContains("2").Fill.BackgroundColor = XLColor.Yellow; + + ws.Range("A1:A2").Merge(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("A1:A1", ws.ConditionalFormats.Single().Ranges.Single().RangeAddress.ToString()); + } + } + + [Test] + public void MergedCellsLooseDataValidation() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").NewDataValidation.WholeNumber.Between(1, 2); + ws.Cell("A2").NewDataValidation.Date.GreaterThan(new System.DateTime(2018, 1, 1)); + + ws.Range("A1:A2").Merge(); + + Assert.IsTrue(ws.Cell("A1").HasDataValidation); + Assert.AreEqual("1", ws.Cell("A1").DataValidation.MinValue); + Assert.AreEqual("2", ws.Cell("A1").DataValidation.MaxValue); + Assert.IsFalse(ws.Cell("A2").HasDataValidation); + } + } + + [Test] + public void UnmergedCellsPreserveStyle() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var range = ws.Range("B2:D4"); + range.Style.Fill.SetBackgroundColor(XLColor.Yellow); + range.Style.Border.SetOutsideBorder(XLBorderStyleValues.Thick) + .Border.SetOutsideBorderColor(XLColor.DarkBlue) + .Border.SetInsideBorder(XLBorderStyleValues.Thin) + .Border.SetInsideBorderColor(XLColor.Pink); + range.Cells().ForEach(c => c.Value = c.Address.ToString()); + + var firstCell = ws.Cell("B2"); + firstCell.Style.Fill.SetBackgroundColor(XLColor.Red) + .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center) + .Font.SetBold(); + + range.Merge(); + range.Unmerge(); + + Assert.IsTrue(range.Cells().All(c => c.Style.Fill.BackgroundColor == XLColor.Red)); + Assert.IsTrue(range.Cells().Where(c => c != firstCell).All(c => c.GetString().Length == 0)); + Assert.AreEqual("B2", firstCell.Value); + + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("B2").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B2").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B2").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("B2").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("C2").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C2").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C2").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C2").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("D2").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("D2").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D2").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D2").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B3").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B3").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B3").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("B3").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C3").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C3").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C3").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C3").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D3").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("D3").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D3").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D3").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B4").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B4").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("B4").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("B4").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C4").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C4").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("C4").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C4").Style.Border.LeftBorder); + + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D4").Style.Border.TopBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("D4").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.Thick, ws.Cell("D4").Style.Border.BottomBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("D4").Style.Border.LeftBorder); + } + } + + [Test] + public void MergedRangesCellValuesShouldNotBeSet() + { + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge(); + ws.Cell("A2").Value = "1"; + ws.Cell("A3").Value = "1"; + ws.Cell("A4").Value = "1"; + ws.Cell("B1").FormulaA1 = "SUM(A:A)"; + Assert.AreEqual(1, ws.Cell("B1").Value); + } + + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge().SetValue(1); + ws.Cell("B1").FormulaA1 = "SUM(A:A)"; + Assert.AreEqual(1, ws.Cell("B1").Value); + } + } + + [Test] + public void MergedRangesCellFormulasShouldNotBeSet() + { + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge(); + ws.Cell("A2").FormulaA1 = "=1"; + ws.Cell("A3").FormulaA1 = "=1"; + ws.Cell("A4").FormulaA1 = "=1"; + ws.Cell("B1").FormulaA1 = "SUM(A:A)"; + Assert.AreEqual(1, ws.Cell("B1").Value); + } + + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge(); + ws.Cell("A2").SetFormulaA1("=1"); + ws.Cell("A3").SetFormulaA1("=1"); + ws.Cell("A4").SetFormulaA1("=1"); + ws.Cell("B1").SetFormulaA1("SUM(A:A)"); + Assert.AreEqual(1, ws.Cell("B1").Value); + } + + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge(); + ws.Cell("A2").FormulaR1C1 = "=1"; + ws.Cell("A3").FormulaR1C1 = "=1"; + ws.Cell("A4").FormulaR1C1 = "=1"; + ws.Cell("B1").FormulaR1C1 = "SUM(A:A)"; + Assert.AreEqual(1, ws.Cell("B1").Value); + } + + using (var workbook = new XLWorkbook()) + { + var ws = workbook.AddWorksheet(); + ws.Range("A2:A4").Merge(); + ws.Cell("A2").SetFormulaR1C1("=1"); + ws.Cell("A3").SetFormulaR1C1("=1"); + ws.Cell("A4").SetFormulaR1C1("=1"); + ws.Cell("B1").SetFormulaR1C1("SUM(A:A)"); + Assert.AreEqual(1, ws.Cell("B1").Value); + } + } + + [Test] + public void MergeSingleCellRangeDoesNothing() + { + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + var range = ws.Range(1, 1, 1, 1); + + range.Merge(); + + Assert.IsFalse(range.IsMerged()); + Assert.AreEqual(0, ws.MergedRanges.Count); + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs b/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs new file mode 100644 index 0000000..93826fa --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs @@ -0,0 +1,285 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Patterns; +using ClosedXML.Excel.Ranges.Index; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Ranges +{ + [TestFixture] + public class RangeIndexTest + { + private const int TEST_COUNT = 10000; + + [Test] + public void FindExistingMatches() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + for (int i = 1; i <= TEST_COUNT; i++) + { + for (int j = 2; j <= 4; j++) + { + var address = new XLAddress(ws, i * 2, j, false, false); + Assert.True(index.Contains(in address)); + } + } + } + } + + [Test] + public void FindNonExistingMatches() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + for (int i = 1; i <= TEST_COUNT; i++) + { + var address = new XLAddress(ws, i * 2 + 1, 3, false, false); + Assert.False(index.Contains(in address)); + } + } + } + + [Test] + public void FindExistingIntersections() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + for (int i = 1; i <= TEST_COUNT; i++) + { + var rangeAddress = new XLRangeAddress( + new XLAddress(ws, i * 2, 1 + i % 4, false, false), + new XLAddress(ws, i * 2 + 1, 8 - i % 3, false, false)); + + Assert.True(index.Intersects(in rangeAddress)); + } + + for (int i = 2; i < 4; i++) + { + var columnAddress = XLRangeAddress.EntireColumn(ws, i); + Assert.True(index.Intersects(in columnAddress)); + } + } + } + + [Test] + public void FindNonExistingIntersections() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + for (int i = 1; i <= TEST_COUNT; i++) + { + var rangeAddress = new XLRangeAddress( + new XLAddress(ws, i * 2 + 1, 1 + i % 4, false, false), + new XLAddress(ws, i * 2 + 1, 8 - i % 3, false, false)); + + Assert.False(index.Intersects(in rangeAddress)); + } + + var columnAddress = XLRangeAddress.EntireColumn(ws, 1); + Assert.False(index.Intersects(in columnAddress)); + columnAddress = XLRangeAddress.EntireColumn(ws, 5); + Assert.False(index.Intersects(in columnAddress)); + } + } + + [Test] + public void FindMatchAfterColumnShifting() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + ws.Column(1).InsertColumnsBefore(1000); + + var address = new XLAddress(ws, 102, 1004, false, false); + + Assert.True(index.Contains(in address)); + } + } + + [Test] + public void FindIntersectionsAfterColumnShifting() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + ws.Column(3).InsertColumnsBefore(2); + + var rangeAddress = new XLRangeAddress(ws, "F102:E103"); + + Assert.True(index.Intersects(in rangeAddress)); + } + } + + [Test] + public void FindMatchAfterRowShifting() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + ws.Row(10).InsertRowsBelow(3); + + var address = new XLAddress(ws, 103, 4, false, false); + + Assert.True(index.Contains(in address)); + } + } + + [Test] + public void FindIntersectionsAfterRowShifting() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var index = FillIndexWithTestData(ws); + + ws.Row(10).InsertRowsBelow(3); + + var rangeAddress = new XLRangeAddress(ws, "C103:E103"); + + Assert.True(index.Intersects(in rangeAddress)); + } + } + + [Test] + public void CreateQuadTree() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var quadTree = new Quadrant(); + var range = ws.Range("BT76:CA87"); + + quadTree.Add(range); + + var level0 = quadTree; + Assert.AreEqual(1, level0.MinimumColumn); + Assert.AreEqual(XLHelper.MaxColumnNumber, level0.MaximumColumn); + Assert.AreEqual(1, level0.MinimumRow); + Assert.AreEqual(XLHelper.MaxRowNumber, level0.MaximumRow); + Assert.IsNull(level0.Ranges); + Assert.AreEqual(128, level0.Children.Count()); + Assert.True(level0.Children.All(child => child.Level == 1)); + Assert.AreEqual(64, level0.Children.Count(child => + child.MinimumColumn == 1 && + child.MaximumColumn == 8192 && + child.X == 0)); + Assert.AreEqual(64, level0.Children.Count(child => + child.MinimumColumn == 8193 && + child.MaximumColumn == 16384 && + child.X == 1)); + Assert.AreEqual(2, level0.Children.Count(child => + child.MinimumRow == 1 && + child.MaximumRow == 8192 && + child.Y == 0)); + Assert.AreEqual(2, level0.Children.Count(child => + child.MinimumRow == 16385 && + child.MaximumRow == 24576 && + child.Y == 2)); + + Assert.True(level0.Children.ElementAt(0).Children.Any()); + Assert.True(level0.Children.Skip(1).All(child => child.Children == null)); + + var level8 = level0 + .Children.First() // 1 + .Children.First() // 2 + .Children.First() // 3 + .Children.First() // 4 + .Children.First() // 5 + .Children.First() // 6 + .Children.First() // 7 + .Children.Last(); // 8 + + Assert.AreEqual(65, level8.MinimumColumn); + Assert.AreEqual(65, level8.MinimumRow); + Assert.AreEqual(128, level8.MaximumColumn); + Assert.AreEqual(128, level8.MaximumRow); + + var level9 = level8.Children.First(); + Assert.NotNull(level9.Ranges); + Assert.AreEqual(range, level9.Ranges.Single()); + } + } + + [Test] + public void XLRangesCountChangesCorrectly() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1") as XLWorksheet; + var range1 = ws.Range("A1:B2"); + var range2 = ws.Range("A2:B3"); + var range3 = ws.Range("A1:B2"); // same as range1 + + var ranges = new XLRanges(); + ranges.Add(range1); + Assert.AreEqual(1, ranges.Count); + ranges.Add(range2); + Assert.AreEqual(2, ranges.Count); + ranges.Add(range3); + Assert.AreEqual(2, ranges.Count); + + Assert.AreEqual(ranges.Count, ranges.Count()); + + // Add many entries to activate QuadTree + for (int i = 1; i <= TEST_COUNT; i++) + { + ranges.Add(ws.Range(i * 2, 2, i * 2, 4)); + } + + Assert.AreEqual(2 + TEST_COUNT, ranges.Count); + + for (int i = 1; i <= TEST_COUNT; i++) + { + ranges.Remove(ws.Range(i * 2, 2, i * 2, 4)); + } + + Assert.AreEqual(2, ranges.Count); + + ranges.Remove(range3); + Assert.AreEqual(1, ranges.Count); + ranges.Remove(range2); + Assert.AreEqual(0, ranges.Count); + ranges.Remove(range1); + Assert.AreEqual(0, ranges.Count); + } + } + + private IXLRangeIndex CreateRangeIndex(IXLWorksheet worksheet) + { + return new XLRangeIndex((XLWorksheet)worksheet); + } + + private IXLRangeIndex FillIndexWithTestData(IXLWorksheet worksheet) + { + var ranges = new List(); + for (int i = 1; i <= TEST_COUNT; i++) + { + ranges.Add(worksheet.Range(i * 2, 2, i * 2, 4)); + } + + var index = CreateRangeIndex(worksheet); + ranges.ForEach(r => index.Add(r)); + return index; + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/RangeShiftingTests.cs b/ClosedXML_Tests/Excel/Ranges/RangeShiftingTests.cs new file mode 100644 index 0000000..02a7c0d --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/RangeShiftingTests.cs @@ -0,0 +1,82 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.Ranges +{ + public class RangeShiftingTests + { + [Test] + public void CellReferenceRemainAfterColumnDeleted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var d4 = ws.Cell("D4"); + + ws.Column("C").Delete(); + + Assert.AreSame(d4, ws.Cell("C4")); + } + } + + [Test] + public void CellReferenceRemainAfterRowDeleted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var d4 = ws.Cell("D4"); + + ws.Row(3).Delete(); + + Assert.AreSame(d4, ws.Cell("D3")); + } + } + + [Test] + public void CellReferenceRemainAfterColumnInserted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var d4 = ws.Cell("D4"); + + ws.Column("C").InsertColumnsBefore(1); + + Assert.AreSame(d4, ws.Cell("E4")); + } + } + + [Test] + public void CellReferenceRemainAfterRowInserted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var d4 = ws.Cell("D4"); + + ws.Row(3).InsertRowsAbove(1); + + Assert.AreSame(d4, ws.Cell("D5")); + } + } + + [Test] + public void CellReferenceRemainAfterRangeDeleted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var d4 = ws.Cell("D4"); + var f8 = ws.Cell("F8"); + + ws.Range("B2:C5").Delete(XLShiftDeletedCells.ShiftCellsLeft); + ws.Range("E5:F7").Delete(XLShiftDeletedCells.ShiftCellsUp); + + Assert.AreSame(d4, ws.Cell("B4")); + Assert.AreSame(f8, ws.Cell("F5")); + } + } + + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/RangesConsolidationTests.cs b/ClosedXML_Tests/Excel/Ranges/RangesConsolidationTests.cs new file mode 100644 index 0000000..8ed5df7 --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/RangesConsolidationTests.cs @@ -0,0 +1,126 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Ranges +{ + [TestFixture] + public class RangesConsolidationTests + { + [Test] + public void ConsolidateRangesSameWorksheet() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + var ranges = new XLRanges(); + ranges.Add(ws.Range("A1:E3")); + ranges.Add(ws.Range("A4:B10")); + ranges.Add(ws.Range("E2:F12")); + ranges.Add(ws.Range("C6:I8")); + ranges.Add(ws.Range("G9:G9")); + ranges.Add(ws.Range("C9:D9")); + ranges.Add(ws.Range("H9:H9")); + ranges.Add(ws.Range("I9:I13")); + ranges.Add(ws.Range("C4:D5")); + + var consRanges = ranges.Consolidate().ToList(); + + Assert.AreEqual(6, consRanges.Count); + Assert.AreEqual("A1:E9", consRanges[0].RangeAddress.ToString()); + Assert.AreEqual("F2:F12", consRanges[1].RangeAddress.ToString()); + Assert.AreEqual("G6:I9", consRanges[2].RangeAddress.ToString()); + Assert.AreEqual("A10:B10", consRanges[3].RangeAddress.ToString()); + Assert.AreEqual("E10:E12", consRanges[4].RangeAddress.ToString()); + Assert.AreEqual("I10:I13", consRanges[5].RangeAddress.ToString()); + } + + [Test] + public void ConsolidateWideRangesSameWorksheet() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + var ranges = new XLRanges(); + ranges.Add(ws.Row(5)); + ranges.Add(ws.Row(7)); + ranges.Add(ws.Row(6)); + ranges.Add(ws.Column("D")); + ranges.Add(ws.Column("F")); + ranges.Add(ws.Column("E")); + + var consRanges = ranges.Consolidate() + .OrderBy(r => r.Worksheet.Name) + .ThenBy(r => r.RangeAddress.FirstAddress.RowNumber) + .ThenBy(r => r.RangeAddress.FirstAddress.ColumnNumber) + .ToList(); + + Assert.AreEqual(3, consRanges.Count); + Assert.AreEqual("D:F", consRanges[0].RangeAddress.ToString()); + Assert.AreEqual("A5:C7", consRanges[1].RangeAddress.ToString()); + Assert.AreEqual("G5:XFD7", consRanges[2].RangeAddress.ToString()); + } + + [Test] + public void ConsolidateRangesDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var ws2 = wb.Worksheets.Add("Sheet2"); + var ranges = new XLRanges(); + ranges.Add(ws1.Range("A1:E3")); + ranges.Add(ws1.Range("A4:B10")); + ranges.Add(ws1.Range("E2:F12")); + ranges.Add(ws1.Range("C6:I8")); + ranges.Add(ws1.Range("G9:G9")); + + ranges.Add(ws2.Row(5)); + ranges.Add(ws2.Row(7)); + ranges.Add(ws2.Row(6)); + ranges.Add(ws2.Column("D")); + ranges.Add(ws2.Column("F")); + ranges.Add(ws2.Column("E")); + + ranges.Add(ws1.Range("C9:D9")); + ranges.Add(ws1.Range("H9:H9")); + ranges.Add(ws1.Range("I9:I13")); + ranges.Add(ws1.Range("C4:D5")); + + var consRanges = ranges.Consolidate() + .OrderBy(r => r.Worksheet.Name) + .ThenBy(r => r.RangeAddress.FirstAddress.RowNumber) + .ThenBy(r => r.RangeAddress.FirstAddress.ColumnNumber) + .ToList(); + + Assert.AreEqual(9, consRanges.Count); + Assert.AreEqual("Sheet1!$A$1:$E$9", consRanges[0].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet1!$F$2:$F$12", consRanges[1].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet1!$G$6:$I$9", consRanges[2].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet1!$A$10:$B$10", consRanges[3].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet1!$E$10:$E$12", consRanges[4].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet1!$I$10:$I$13", consRanges[5].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + + Assert.AreEqual("Sheet2!$D:$F", consRanges[6].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet2!$A$5:$C$7", consRanges[7].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("Sheet2!$G$5:$XFD$7", consRanges[8].RangeAddress.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void ConsolidateSparsedRanges() + { + var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + var ranges = new XLRanges(); + ranges.Add(ws.Range("A1:C1")); + ranges.Add(ws.Range("E1:G1")); + ranges.Add(ws.Range("A3:C3")); + ranges.Add(ws.Range("E3:G3")); + + var consRanges = ranges.Consolidate().ToList(); + + Assert.AreEqual(4, consRanges.Count); + Assert.AreEqual("A1:C1", consRanges[0].RangeAddress.ToString()); + Assert.AreEqual("E1:G1", consRanges[1].RangeAddress.ToString()); + Assert.AreEqual("A3:C3", consRanges[2].RangeAddress.ToString()); + Assert.AreEqual("E3:G3", consRanges[3].RangeAddress.ToString()); + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/UsedAndUnusedCellsTests.cs b/ClosedXML_Tests/Excel/Ranges/UsedAndUnusedCellsTests.cs new file mode 100644 index 0000000..630deae --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/UsedAndUnusedCellsTests.cs @@ -0,0 +1,401 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Ranges +{ + [TestFixture] + public class UsedAndUnusedCellsTests + { + private XLWorkbook workbook; + + [SetUp] + public void SetupWorkbook() + { + workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + ws.Cell(1, 1).Value = "A1"; + ws.Cell(1, 3).Value = "C1"; + ws.Cell(2, 2).Value = "B2"; + ws.Cell(4, 1).Value = "A4"; + ws.Cell(5, 2).Value = "B5"; + ws.Cell(6, 2).Style.Fill.BackgroundColor = XLColor.Red; + } + + [Test] + public void CountUsedCellsInRow() + { + int i = 0; + var row = workbook.Worksheets.First().FirstRow(); + foreach (var cell in row.Cells()) // Cells() returns UnUsed cells by default + { + i++; + } + Assert.AreEqual(2, i); + + i = 0; + row = workbook.Worksheets.First().FirstRow().RowBelow(); + foreach (var cell in row.Cells()) + { + i++; + } + Assert.AreEqual(1, i); + + i = 0; + row = workbook.Worksheets.First().LastRowUsed(XLCellsUsedOptions.All); + Assert.AreEqual(6, row.RowNumber()); + foreach (var cell in row.Cells()) + { + i++; + } + Assert.AreEqual(1, i); + + i = 0; + row = workbook.Worksheets.First().LastRowUsed(XLCellsUsedOptions.All); + Assert.AreEqual(6, row.RowNumber()); + foreach (var cell in row.CellsUsed()) + { + i++; + } + Assert.AreEqual(0, i); + } + + [Test(Description = "See 1443")] + public void FirstRowUsedRegression() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + + ws.Range("B3:F6").SetValue(100); + + Assert.AreEqual(3, ws.FirstRowUsed(XLCellsUsedOptions.AllContents).RowNumber()); + } + } + + [Test] + public void CountAllCellsInRow() + { + int i = 0; + var row = workbook.Worksheets.First().FirstRow(); + foreach (var cell in row.Cells(false)) // All cells in range between first and last cells used + { + i++; + } + Assert.AreEqual(3, i); + + i = 0; + row = workbook.Worksheets.First().FirstRow().RowBelow(); //This row has no empty cells BETWEEN used cells + foreach (var cell in row.Cells(false)) + { + i++; + } + Assert.AreEqual(1, i); + } + + [Test] + public void CountUsedCellsInColumn() + { + int i = 0; + var column = workbook.Worksheets.First().FirstColumn(); + foreach (var cell in column.Cells()) // Cells() returns UnUsed cells by default + { + i++; + } + Assert.AreEqual(2, i); + + i = 0; + column = workbook.Worksheets.First().FirstColumn().ColumnRight().ColumnRight(); + foreach (var cell in column.Cells()) + { + i++; + } + Assert.AreEqual(1, i); + + i = 0; + column = workbook.Worksheets.First().Column(2); + foreach (var cell in column.Cells()) + { + i++; + } + Assert.AreEqual(3, i); + + i = 0; + column = workbook.Worksheets.First().Column(2); + foreach (var cell in column.CellsUsed()) + { + i++; + } + Assert.AreEqual(2, i); + } + + [Test] + public void CountAllCellsInColumn() + { + int i = 0; + var column = workbook.Worksheets.First().FirstColumn(); + foreach (var cell in column.Cells(false)) // All cells in range between first and last cells used + { + i++; + } + Assert.AreEqual(4, i); + + i = 0; + column = workbook.Worksheets.First().FirstColumn().ColumnRight().ColumnRight(); //This column has no empty cells BETWEEN used cells + foreach (var cell in column.Cells(false)) + { + i++; + } + Assert.AreEqual(1, i); + } + + [Test] + public void CountCellsInWorksheet() + { + var ws = workbook.Worksheets.First(); + int i = 0; + + foreach (var cell in ws.Cells()) // All cells with content or formats + { + i++; + } + Assert.AreEqual(6, i); + } + + [Test] + public void CountUsedCellsInWorksheet() + { + var ws = workbook.Worksheets.First(); + int i = 0; + + foreach (var cell in ws.CellsUsed()) // Only used cells in worksheet + { + i++; + } + Assert.AreEqual(5, i); + } + + [Test] + public void CountAllCellsInWorksheet() + { + var ws = workbook.Worksheets.First(); + int i = 0; + + foreach (var cell in ws.Cells(false)) // All cells in range between first and last cells used (cartesian product of range) + { + i++; + } + Assert.AreEqual(18, i); + } + + [Test] + public void GetCellsUsedNonRectangular() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var sheet = wb.AddWorksheet("page1"); + + sheet.Range("C1:E1").Value = "row1"; + sheet.Range("A2:E2").Value = "row2"; + + var used = sheet.RangeUsed().RangeAddress.ToString(XLReferenceStyle.A1); + + Assert.AreEqual("A1:E2", used); + } + } + + [TestCase(true, "A1:D2", "A1")] + [TestCase(true, "A2:D2", "A2")] + [TestCase(true, "A1:D2", "A1", "B2")] + [TestCase(true, "B2:D3", "C3")] + [TestCase(true, "B2:F4", "F4")] + [TestCase(false, "A1:D2", "A1")] + [TestCase(false, "A2:D2", "A2")] + [TestCase(false, "A1:D2", "A1", "B2")] + [TestCase(false, "B2:D3", "C3")] + [TestCase(false, "B2:F4", "F4")] + public void RangeUsedIncludesMergedCells(bool includeFormatting, string expectedRange, + params string[] cellsWithValues) + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + foreach (var cellAddress in cellsWithValues) + { + ws.Cell(cellAddress).Value = "Not empty"; + } + ws.Range("B2:D2").Merge(); + + var options = includeFormatting + ? XLCellsUsedOptions.All + : XLCellsUsedOptions.AllContents | XLCellsUsedOptions.MergedRanges; + var actual = ws.RangeUsed(options).RangeAddress; + + Assert.AreEqual(expectedRange, actual.ToString()); + } + } + + [Test] + public void LastCellUsedPredicateConsidersMergedRanges() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell("A2").Style.Fill.BackgroundColor = XLColor.Yellow; + ws.Cell("A3").Style.Fill.BackgroundColor = XLColor.Green; + ws.Range("A1:C1").Merge(); + ws.Range("A2:C2").Merge(); + ws.Range("A3:C3").Merge(); + + var actual = ws.LastCellUsed(XLCellsUsedOptions.All, + c => c.Style.Fill.BackgroundColor == XLColor.Yellow); + + Assert.AreEqual("C2", actual.Address.ToString()); + } + } + + [Test] + public void FirstCellUsedPredicateConsidersMergedRanges() + { + using (XLWorkbook wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Style.Fill.BackgroundColor = XLColor.Red; + ws.Cell("A2").Style.Fill.BackgroundColor = XLColor.Yellow; + ws.Cell("A3").Style.Fill.BackgroundColor = XLColor.Green; + ws.Range("A1:C1").Merge(); + ws.Range("A2:C2").Merge(); + ws.Range("A3:C3").Merge(); + + var actual = ws.FirstCellUsed(XLCellsUsedOptions.All, + c => c.Style.Fill.BackgroundColor == XLColor.Yellow); + + Assert.AreEqual("A2", actual.Address.ToString()); + } + } + + [Test] + public void ApplyingDataValidationMakesCellNotEmpty() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Range("B2:B12").SetDataValidation() + .Decimal.EqualOrGreaterThan(0); + + var usedCells = ws.CellsUsed(XLCellsUsedOptions.All).ToList(); + + Assert.AreEqual(11, usedCells.Count); + Assert.AreEqual("B2", usedCells.First().Address.ToString()); + Assert.AreEqual("B12", usedCells.Last().Address.ToString()); + } + } + + [Test] + public void MergeMakesCellNotEmpty() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Range("B2:B12").Merge(); + + var usedCells = ws.CellsUsed(XLCellsUsedOptions.All).ToList(); + + Assert.AreEqual(11, usedCells.Count); + Assert.AreEqual("B2", usedCells.First().Address.ToString()); + Assert.AreEqual("B12", usedCells.Last().Address.ToString()); + } + } + + [Test] + public void FirstCellUsedNotHangingOnLargeCFRules() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.AddConditionalFormat().WhenIsBlank().Fill.SetBackgroundColor(XLColor.Gold); + + var firstCell = ws.FirstCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual("A1", firstCell.Address.ToString()); + } + } + + [Test] + public void LastCellUsedNotHangingOnLargeCFRules() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.AddConditionalFormat().WhenIsBlank().Fill.SetBackgroundColor(XLColor.Gold); + + var lastCell = ws.LastCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual(XLHelper.LastCell, lastCell.Address.ToString()); + } + } + + [Test] + public void FirstCellUsedNotHangingOnLargeDVRules() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.SetDataValidation().WholeNumber.GreaterThan(0); + + var firstCell = ws.FirstCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual("A1", firstCell.Address.ToString()); + } + } + + [Test] + public void LastCellUsedNotHangingOnLargeDVRules() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.SetDataValidation().WholeNumber.GreaterThan(0); + + var lastCell = ws.LastCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual(XLHelper.LastCell, lastCell.Address.ToString()); + } + } + + [Test] + public void FirstCellUsedNotHangingOnLargeMergedRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Merge(); + + var firstCell = ws.FirstCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual("A1", firstCell.Address.ToString()); + } + } + + [Test] + public void LastCellUsedNotHangingOnLargeMergedRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Merge(); + + var lastCell = ws.LastCellUsed(XLCellsUsedOptions.All); + + Assert.AreEqual(2, (ws as XLWorksheet).Internals.CellsCollection.Count); + Assert.AreEqual(XLHelper.LastCell, lastCell.Address.ToString()); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/XLRangeAddressTests.cs b/ClosedXML_Tests/Excel/Ranges/XLRangeAddressTests.cs new file mode 100644 index 0000000..9d3c3cc --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/XLRangeAddressTests.cs @@ -0,0 +1,349 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLRangeAddressTests + { + [Test] + public void ToStringTest() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRangeAddress address = ws.Cell(1, 1).AsRange().RangeAddress; + + Assert.AreEqual("A1:A1", address.ToString()); + Assert.AreEqual("Sheet1!R1C1:R1C1", address.ToString(XLReferenceStyle.R1C1, true)); + + Assert.AreEqual("A1:A1", address.ToStringRelative()); + Assert.AreEqual("Sheet1!A1:A1", address.ToStringRelative(true)); + + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed()); + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1:R1C1", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("Sheet1!$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("Sheet1!R1C1:R1C1", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("Sheet1!$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [Test] + public void ToStringTestWithSpace() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + IXLRangeAddress address = ws.Cell(1, 1).AsRange().RangeAddress; + + Assert.AreEqual("A1:A1", address.ToString()); + Assert.AreEqual("'Sheet 1'!R1C1:R1C1", address.ToString(XLReferenceStyle.R1C1, true)); + + Assert.AreEqual("A1:A1", address.ToStringRelative()); + Assert.AreEqual("'Sheet 1'!A1:A1", address.ToStringRelative(true)); + + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed()); + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("R1C1:R1C1", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!R1C1:R1C1", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + Assert.AreEqual("'Sheet 1'!$A$1:$A$1", address.ToStringFixed(XLReferenceStyle.Default, true)); + } + + [TestCase("B2:E5", "B2:E5")] + [TestCase("E5:B2", "B2:E5")] + [TestCase("B5:E2", "B2:E5")] + [TestCase("B2:E$5", "B2:E$5")] + [TestCase("B2:$E$5", "B2:$E$5")] + [TestCase("B$2:$E$5", "B$2:$E$5")] + [TestCase("$B$2:$E$5", "$B$2:$E$5")] + [TestCase("B5:E$2", "B$2:E5")] + [TestCase("$B$5:E2", "$B2:E$5")] + [TestCase("$B$5:E$2", "$B$2:E$5")] + [TestCase("$B$5:$E$2", "$B$2:$E$5")] + public void RangeAddressNormalizeTest(string inputAddress, string expectedAddress) + { + XLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1") as XLWorksheet; + var rangeAddress = new XLRangeAddress(ws, inputAddress); + + var normalizedAddress = rangeAddress.Normalize(); + + Assert.AreSame(ws, rangeAddress.Worksheet); + Assert.AreEqual(expectedAddress, normalizedAddress.ToString()); + } + + [Test] + public void InvalidRangeAddressToStringTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToString()); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToString(XLReferenceStyle.Default, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToString(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void InvalidRangeAddressToStringFixedTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToStringFixed()); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void InvalidRangeAddressToStringRelativeTest() + { + var address = ProduceInvalidAddress(); + + Assert.AreEqual("#REF!", address.ToStringRelative()); + Assert.AreEqual("'Sheet 1'!#REF!", address.ToStringRelative(true)); + } + + [Test] + public void RangeAddressOnDeletedWorksheetToStringTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!A1:B2", address.ToString()); + Assert.AreEqual("#REF!A1:B2", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!A1:B2", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!R1C1:R2C2", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!A1:B2", address.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!A1:B2", address.ToString(XLReferenceStyle.Default, true)); + Assert.AreEqual("#REF!R1C1:R2C2", address.ToString(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void RangeAddressOnDeletedWorksheetToStringFixedTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!$A$1:$B$2", address.ToStringFixed()); + Assert.AreEqual("#REF!$A$1:$B$2", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!$A$1:$B$2", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!R1C1:R2C2", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!$A$1:$B$2", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!$A$1:$B$2", address.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("#REF!R1C1:R2C2", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void RangeAddressOnDeletedWorksheetToStringRelativeTest() + { + var address = ProduceAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!A1:B2", address.ToStringRelative()); + Assert.AreEqual("#REF!A1:B2", address.ToStringRelative(true)); + } + + [Test] + public void InvalidRangeAddressOnDeletedWorksheetToStringTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!#REF!", address.ToString()); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.Default, true)); + Assert.AreEqual("#REF!#REF!", address.ToString(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void InvalidRangeAddressOnDeletedWorksheetToStringFixedTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!#REF!", address.ToStringFixed()); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.A1)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.Default)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.R1C1)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.A1, true)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.Default, true)); + Assert.AreEqual("#REF!#REF!", address.ToStringFixed(XLReferenceStyle.R1C1, true)); + } + + [Test] + public void InvalidRangeAddressOnDeletedWorksheetToStringRelativeTest() + { + var address = ProduceInvalidAddressOnDeletedWorksheet(); + + Assert.AreEqual("#REF!#REF!", address.ToStringRelative()); + Assert.AreEqual("#REF!#REF!", address.ToStringRelative(true)); + } + + [Test] + public void FullSpanAddressCannotChange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var wsRange = ws.AsRange(); + var row = ws.FirstRow().RowBelow(4).AsRange(); + var column = ws.FirstColumn().ColumnRight(4).AsRange(); + + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", wsRange.RangeAddress.ToString()); + Assert.AreEqual("5:5", row.RangeAddress.ToString()); + Assert.AreEqual("E:E", column.RangeAddress.ToString()); + + ws.Columns("Y:Z").Delete(); + ws.Rows("9:10").Delete(); + + Assert.AreEqual($"1:{XLHelper.MaxRowNumber}", wsRange.RangeAddress.ToString()); + Assert.AreEqual("5:5", row.RangeAddress.ToString()); + Assert.AreEqual("E:E", column.RangeAddress.ToString()); + } + } + + [Test] + public void RangeAddressIsNormalized() + { + var ws = new XLWorkbook().AddWorksheet(); + + XLRangeAddress rangeAddress; + + rangeAddress = (XLRangeAddress)ws.Range(ws.Cell("A1"), ws.Cell("C3")).RangeAddress; + Assert.IsTrue(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.Range(ws.Cell("C3"), ws.Cell("A1")).RangeAddress; + Assert.IsFalse(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.Range("B2:B1").RangeAddress; + Assert.IsFalse(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.Range("B2:B10").RangeAddress; + Assert.IsTrue(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.Range("B:B").RangeAddress; + Assert.IsTrue(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.Range("2:2").RangeAddress; + Assert.IsTrue(rangeAddress.IsNormalized); + + rangeAddress = (XLRangeAddress)ws.RangeAddress; + Assert.IsTrue(rangeAddress.IsNormalized); + } + + [Test] + public void AsRangeTests() + { + XLRangeAddress rangeAddress; + rangeAddress = new XLRangeAddress + ( + new XLAddress(1, 1, false, false), + new XLAddress(5, 5, false, false) + ); + + Assert.IsTrue(rangeAddress.IsValid); + Assert.IsTrue(rangeAddress.IsNormalized); + Assert.Throws(() => rangeAddress.AsRange()); + + var ws = new XLWorkbook().AddWorksheet() as XLWorksheet; + rangeAddress = new XLRangeAddress + ( + new XLAddress(ws, 1, 1, false, false), + new XLAddress(ws, 5, 5, false, false) + ); + + Assert.IsTrue(rangeAddress.IsValid); + Assert.IsTrue(rangeAddress.IsNormalized); + Assert.DoesNotThrow(() => rangeAddress.AsRange()); + } + + [Test] + public void RelativeRanges() + { + var ws = new XLWorkbook().AddWorksheet(); + + IXLRangeAddress rangeAddress; + + rangeAddress = ws.Range("D4:E4").RangeAddress.Relative(ws.Range("A1:E4").RangeAddress, ws.Range("B10:F14").RangeAddress); + Assert.IsTrue(rangeAddress.IsValid); + Assert.AreEqual("E13:F13", rangeAddress.ToString()); + + rangeAddress = ws.Range("D4:E4").RangeAddress.Relative(ws.Range("B10:F14").RangeAddress, ws.Range("A1:E4").RangeAddress); + Assert.IsFalse(rangeAddress.IsValid); + Assert.AreEqual("#REF!", rangeAddress.ToString()); + + rangeAddress = ws.Range("C3").RangeAddress.Relative(ws.Range("A1:B2").RangeAddress, ws.Range("C3").RangeAddress); + Assert.IsTrue(rangeAddress.IsValid); + Assert.AreEqual("E5:E5", rangeAddress.ToString()); + + rangeAddress = ws.Range("B2").RangeAddress.Relative(ws.Range("A1").RangeAddress, ws.Range("C3").RangeAddress); + Assert.IsTrue(rangeAddress.IsValid); + Assert.AreEqual("D4:D4", rangeAddress.ToString()); + + rangeAddress = ws.Range("A1").RangeAddress.Relative(ws.Range("B2").RangeAddress, ws.Range("A1").RangeAddress); + Assert.IsFalse(rangeAddress.IsValid); + Assert.AreEqual("#REF!", rangeAddress.ToString()); + } + + [Test] + public void TestSpanProperties() + { + var ws = new XLWorkbook().AddWorksheet() as XLWorksheet; + + var range = ws.Range("B3:E5"); + var rangeAddress = range.RangeAddress as IXLRangeAddress; + Assert.AreEqual(4, rangeAddress.ColumnSpan); + Assert.AreEqual(3, rangeAddress.RowSpan); + Assert.AreEqual(12, rangeAddress.NumberOfCells); + + range = ws.Range("E5:B3"); + rangeAddress = range.RangeAddress as IXLRangeAddress; + Assert.AreEqual(4, rangeAddress.ColumnSpan); + Assert.AreEqual(3, rangeAddress.RowSpan); + Assert.AreEqual(12, rangeAddress.NumberOfCells); + + rangeAddress = ProduceAddressOnDeletedWorksheet(); + Assert.AreEqual(2, rangeAddress.ColumnSpan); + Assert.AreEqual(2, rangeAddress.RowSpan); + Assert.AreEqual(4, rangeAddress.NumberOfCells); + + rangeAddress = ProduceInvalidAddress(); + Assert.Throws(() => { var x = rangeAddress.ColumnSpan; }); + Assert.Throws(() => { var x = rangeAddress.RowSpan; }); + Assert.Throws(() => { var x = rangeAddress.NumberOfCells; }); + } + + #region Private Methods + + private IXLRangeAddress ProduceInvalidAddress() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + var range = ws.Range("A1:B2"); + + ws.Rows(1, 5).Delete(); + return range.RangeAddress; + } + + private IXLRangeAddress ProduceAddressOnDeletedWorksheet() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet 1"); + var address = ws.Range("A1:B2").RangeAddress; + + ws.Delete(); + return address; + } + + private IXLRangeAddress ProduceInvalidAddressOnDeletedWorksheet() + { + var address = ProduceInvalidAddress(); + address.Worksheet.Delete(); + return address; + } + + #endregion Private Methods + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs b/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs new file mode 100644 index 0000000..7bc919b --- /dev/null +++ b/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs @@ -0,0 +1,520 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLRangeBaseTests + { + [Test] + public void IsEmpty1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(XLCellsUsedOptions.All); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty4() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(XLCellsUsedOptions.AllContents); + bool expected = true; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty5() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Style.Fill.BackgroundColor = XLColor.Red; + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(XLCellsUsedOptions.All); + bool expected = false; + Assert.AreEqual(expected, actual); + } + + [Test] + public void IsEmpty6() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.Value = "X"; + IXLRange range = ws.Range("A1:B2"); + bool actual = range.IsEmpty(); + bool expected = false; + Assert.AreEqual(expected, actual); + } + + [Test] + public void SingleCell() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).Value = "Hello World!"; + wb.NamedRanges.Add("SingleCell", "Sheet1!$A$1"); + IXLRange range = wb.Range("SingleCell"); + Assert.AreEqual(1, range.CellsUsed().Count()); + Assert.AreEqual("Hello World!", range.CellsUsed().Single().GetString()); + } + + [Test] + public void TableRange() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + IXLRangeColumn rangeColumn = ws.Column(1).Column(1, 4); + rangeColumn.Cell(1).Value = "FName"; + rangeColumn.Cell(2).Value = "John"; + rangeColumn.Cell(3).Value = "Hank"; + rangeColumn.Cell(4).Value = "Dagny"; + IXLTable table = rangeColumn.CreateTable(); + wb.NamedRanges.Add("FNameColumn", String.Format("{0}[{1}]", table.Name, "FName")); + + IXLRange namedRange = wb.Range("FNameColumn"); + Assert.AreEqual(3, namedRange.Cells().Count()); + Assert.IsTrue( + namedRange.CellsUsed().Select(cell => cell.GetString()).SequenceEqual(new[] { "John", "Hank", "Dagny" })); + } + + [Test] + public void WsNamedCell() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("Test").AddToNamed("TestCell", XLScope.Worksheet); + Assert.AreEqual("Test", ws.Cell("TestCell").GetString()); + } + + [Test] + public void WsNamedCells() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("Test").AddToNamed("TestCell", XLScope.Worksheet); + ws.Cell(2, 1).SetValue("B"); + IXLCells cells = ws.Cells("TestCell, A2"); + Assert.AreEqual("Test", cells.First().GetString()); + Assert.AreEqual("B", cells.Last().GetString()); + } + + [Test] + public void WsNamedRange() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("A"); + ws.Cell(2, 1).SetValue("B"); + IXLRange original = ws.Range("A1:A2"); + original.AddToNamed("TestRange", XLScope.Worksheet); + IXLRange named = ws.Range("TestRange"); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), named.RangeAddress.ToString()); + } + + [Test] + public void WsNamedRanges() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 1).SetValue("A"); + ws.Cell(2, 1).SetValue("B"); + ws.Cell(3, 1).SetValue("C"); + IXLRange original = ws.Range("A1:A2"); + original.AddToNamed("TestRange", XLScope.Worksheet); + IXLRanges namedRanges = ws.Ranges("TestRange, A3"); + Assert.AreEqual(original.RangeAddress.ToStringFixed(), namedRanges.First().RangeAddress.ToString()); + Assert.AreEqual("$A$3:$A$3", namedRanges.Last().RangeAddress.ToStringFixed()); + } + + [Test] + public void WsNamedRangesOneString() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.NamedRanges.Add("TestRange", "Sheet1!$A$1,Sheet1!$A$3"); + IXLRanges namedRanges = ws.Ranges("TestRange"); + + Assert.AreEqual("$A$1:$A$1", namedRanges.First().RangeAddress.ToStringFixed()); + Assert.AreEqual("$A$3:$A$3", namedRanges.Last().RangeAddress.ToStringFixed()); + } + + //[Test] + //public void WsNamedRangeLiteral() + //{ + // var wb = new XLWorkbook(); + // var ws = wb.Worksheets.Add("Sheet1"); + // ws.NamedRanges.Add("TestRange", "\"Hello\""); + // using (MemoryStream memoryStream = new MemoryStream()) + // { + // wb.SaveAs(memoryStream, true); + // var wb2 = new XLWorkbook(memoryStream); + // var text = wb2.Worksheet("Sheet1").NamedRanges.First() + // memoryStream.Close(); + // } + + //} + + [Test] + public void GrowRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + Assert.AreEqual("A1:B2", ws.Cell("A1").AsRange().Grow().RangeAddress.ToString()); + Assert.AreEqual("A1:B3", ws.Cell("A2").AsRange().Grow().RangeAddress.ToString()); + Assert.AreEqual("A1:C2", ws.Cell("B1").AsRange().Grow().RangeAddress.ToString()); + + Assert.AreEqual("E4:G6", ws.Cell("F5").AsRange().Grow().RangeAddress.ToString()); + Assert.AreEqual("D3:H7", ws.Cell("F5").AsRange().Grow(2).RangeAddress.ToString()); + Assert.AreEqual("A1:DB105", ws.Cell("F5").AsRange().Grow(100).RangeAddress.ToString()); + } + } + + [Test] + public void ShrinkRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + Assert.Null(ws.Cell("A1").AsRange().Shrink()); + Assert.Null(ws.Range("B2:C3").Shrink()); + Assert.AreEqual("C3:C3", ws.Range("B2:D4").Shrink().RangeAddress.ToString()); + Assert.AreEqual("K11:P16", ws.Range("A1:Z26").Shrink(10).RangeAddress.ToString()); + + // Grow and shrink back + Assert.AreEqual("Z26:Z26", ws.Cell("Z26").AsRange().Grow(10).Shrink(10).RangeAddress.ToString()); + } + } + + [Test] + public void Intersection() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + Assert.AreEqual("D9:G11", ws.Range("B9:I11").Intersection(ws.Range("D4:G16")).ToString()); + Assert.AreEqual("E9:G11", ws.Range("E9:I11").Intersection(ws.Range("D4:G16")).ToString()); + Assert.AreEqual("E9:E9", ws.Cell("E9").AsRange().Intersection(ws.Range("D4:G16")).ToString()); + Assert.AreEqual("E9:E9", ws.Range("D4:G16").Intersection(ws.Cell("E9").AsRange()).ToString()); + + XLRangeAddress rangeAddress; + + rangeAddress = (XLRangeAddress)ws.Cell("C3").AsRange().Intersection(ws.Cell("A1").AsRange()); + Assert.IsFalse(rangeAddress.IsValid); + + rangeAddress = (XLRangeAddress)ws.Cell("A1").AsRange().Intersection(ws.Cell("C3").AsRange()); + Assert.IsFalse(rangeAddress.IsValid); + + Assert.Null(ws.Range("A1:C3").Intersection(null)); + + var otherWs = wb.AddWorksheet("Sheet2"); + Assert.Null(ws.Intersection(otherWs)); + Assert.Null(ws.Cell("A1").AsRange().Intersection(otherWs.Cell("A2").AsRange())); + } + } + + [Test] + public void Union() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + Assert.AreEqual(64, ws.Range("B9:I11").Union(ws.Range("D4:G16")).Count()); + Assert.AreEqual(58, ws.Range("E9:I11").Union(ws.Range("D4:G16")).Count()); + Assert.AreEqual(52, ws.Cell("E9").AsRange().Union(ws.Range("D4:G16")).Count()); + Assert.AreEqual(52, ws.Range("D4:G16").Union(ws.Cell("E9").AsRange()).Count()); + + Assert.AreEqual(2, ws.Cell("A1").AsRange().Union(ws.Cell("C3").AsRange()).Count()); + + Assert.AreEqual(9, ws.Range("A1:C3").Union(null).Count()); + + var otherWs = wb.AddWorksheet("Sheet2"); + Assert.False(ws.Union(otherWs).Any()); + Assert.False(ws.Cell("A1").AsRange().Union(otherWs.Cell("A2").AsRange()).Any()); + } + } + + [Test] + public void Difference() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + Assert.AreEqual(12, ws.Range("B9:I11").Difference(ws.Range("D4:G16")).Count()); + Assert.AreEqual(6, ws.Range("E9:I11").Difference(ws.Range("D4:G16")).Count()); + Assert.AreEqual(0, ws.Cell("E9").AsRange().Difference(ws.Range("D4:G16")).Count()); + Assert.AreEqual(51, ws.Range("D4:G16").Difference(ws.Cell("E9").AsRange()).Count()); + + Assert.AreEqual(1, ws.Cell("A1").AsRange().Difference(ws.Cell("C3").AsRange()).Count()); + + Assert.AreEqual(9, ws.Range("A1:C3").Difference(null).Count()); + + var otherWs = wb.AddWorksheet("Sheet2"); + Assert.False(ws.Difference(otherWs).Any()); + Assert.False(ws.Cell("A1").AsRange().Difference(otherWs.Cell("A2").AsRange()).Any()); + } + } + + [Test] + public void SurroundingCells() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + Assert.AreEqual(3, ws.FirstCell().AsRange().SurroundingCells().Count()); + Assert.AreEqual(8, ws.Cell("C3").AsRange().SurroundingCells().Count()); + Assert.AreEqual(16, ws.Range("C3:D6").AsRange().SurroundingCells().Count()); + + Assert.AreEqual(0, ws.Range("C3:D6").AsRange().SurroundingCells(c => !c.IsEmpty()).Count()); + } + } + + [Test] + public void ClearConditionalFormattingsWhenRangeAbove1() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:D7").AddConditionalFormat(); + ws.Range("B2:E3").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C4:D7", ws.ConditionalFormats.Single().Range.RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeAbove2() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:D7").AddConditionalFormat(); + ws.Range("C3:D3").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C4:D7", ws.ConditionalFormats.Single().Range.RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeBelow1() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:D7").AddConditionalFormat(); + ws.Range("B7:E8").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C3:D6", ws.ConditionalFormats.Single().Range.RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeBelow2() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:D7").AddConditionalFormat(); + ws.Range("C7:D7").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C3:D6", ws.ConditionalFormats.Single().Range.RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeRowInMiddle() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:D7").AddConditionalFormat(); + ws.Range("C5:E5").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C3:D4", ws.ConditionalFormats.First().Ranges.First().RangeAddress.ToStringRelative()); + Assert.AreEqual("C6:D7", ws.ConditionalFormats.First().Ranges.Last().RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeColumnInMiddle() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:G4").AddConditionalFormat(); + ws.Range("E2:E4").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual("C3:D4", ws.ConditionalFormats.First().Ranges.First().RangeAddress.ToStringRelative()); + Assert.AreEqual("F3:G4", ws.ConditionalFormats.First().Ranges.Last().RangeAddress.ToStringRelative()); + } + + [Test] + public void ClearConditionalFormattingsWhenRangeContainsFormatWhole() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:G4").AddConditionalFormat(); + ws.Range("B2:G4").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(0, ws.ConditionalFormats.Count()); + } + + [Test] + public void NoClearConditionalFormattingsWhenRangePartiallySuperimposed() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("C3:G4").AddConditionalFormat(); + ws.Range("C2:D3").Clear(XLClearOptions.ConditionalFormats); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + Assert.AreEqual(1, ws.ConditionalFormats.Single().Ranges.Count); + Assert.AreEqual("C3:G4", ws.ConditionalFormats.Single().Ranges.Single().RangeAddress.ToStringRelative()); + } + + [Test] + public void RangesRemoveAllWithoutDispose() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var ranges = new XLRanges(); + ranges.Add(ws.Range("A1:A2")); + ranges.Add(ws.Range("B1:B2")); + var rangesCopy = ranges.ToList(); + + ranges.RemoveAll(null, false); + ws.FirstColumn().InsertColumnsBefore(1); + + Assert.AreEqual(0, ranges.Count); + // if ranges were not disposed they addresses should change + Assert.AreEqual("B1:B2", rangesCopy.First().RangeAddress.ToString()); + Assert.AreEqual("C1:C2", rangesCopy.Last().RangeAddress.ToString()); + } + + + [Test] + public void RangesRemoveAllByCriteria() + { + var ws = new XLWorkbook().Worksheets.Add("Sheet1"); + var ranges = new XLRanges(); + ranges.Add(ws.Range("A1:A2")); + ranges.Add(ws.Range("B1:B3")); + ranges.Add(ws.Range("C1:C4")); + var otherRange = ws.Range("A3:D3"); + + ranges.RemoveAll(r => r.Intersects(otherRange)); + + Assert.AreEqual(1, ranges.Count); + Assert.AreEqual("A1:A2", ranges.Single().RangeAddress.ToString()); + } + + [Test] + public void XLRangesReturnsRangesInDeterministicOrder() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + var ws2 = wb.Worksheets.Add("Another sheet"); + + var ranges = new XLRanges(); + ranges.Add(ws2.Range("F1:F12")); + ranges.Add(ws1.Range("F12:F16")); + ranges.Add(ws1.Range("B1:F2")); + ranges.Add(ws2.Range("A13:B14")); + ranges.Add(ws2.Range("E1:E2")); + ranges.Add(ws1.Range("E1:H2")); + ranges.Add(ws1.Range("G2:G13")); + ranges.Add(ws1.Range("G20:G20")); + + var expectedRanges = new List + { + ws1.Range("B1:F2"), + ws1.Range("E1:H2"), + ws1.Range("G2:G13"), + ws1.Range("F12:F16"), + ws1.Range("G20:G20"), + + ws2.Range("E1:E2"), + ws2.Range("F1:F12"), + ws2.Range("A13:B14"), + }; + + var actualRanges = ranges.ToList(); + + Assert.AreEqual(expectedRanges.Count, actualRanges.Count); + for (int i = 0; i < actualRanges.Count; i++) + { + Assert.AreEqual(expectedRanges[i], actualRanges[i]); + } + } + + [Test] + public void ClearRangeRemovesSparklines() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.SparklineGroups.Add("B1:B3", "C1:E3"); + + ws.Range("B1:C1").Clear(XLClearOptions.All); + ws.Range("B2:C2").Clear(XLClearOptions.Sparklines); + + Assert.AreEqual(1, ws.SparklineGroups.Single().Count()); + Assert.IsFalse(ws.Cell("B1").HasSparkline); + Assert.IsFalse(ws.Cell("B2").HasSparkline); + Assert.IsTrue(ws.Cell("B3").HasSparkline); + + } + + [TestCase("B2:G7", "D4:E5", true, "B2:G3,B4:C5,D4:E5,F4:G5,B6:G7")] + [TestCase("B2:G7", "D4:E5", false, "B2:G3,B4:C5,F4:G5,B6:G7")] + [TestCase("B2:G7", "B2:G7", true, "B2:G7")] + [TestCase("B2:G7", "B2:G7", false, "")] + [TestCase("B2:G7", "A1:H8", true, "B2:G7")] + [TestCase("B2:G7", "A1:H8", false, "")] + [TestCase("B2:G7", "A1:B2", true, "B2:B2,C2:G2,B3:G7")] + [TestCase("B2:G7", "A1:B2", false, "C2:G2,B3:G7")] + [TestCase("B2:G7", "E4:J5", true, "B2:G3,B4:D5,E4:G5,B6:G7")] + [TestCase("B2:G7", "E4:J5", false, "B2:G3,B4:D5,B6:G7")] + [TestCase("B2:G7", "A11:H18", true, "B2:G7")] + [TestCase("B2:G7", "A11:H18", false, "B2:G7")] + [TestCase("B2:G7", "A1:H1", true, "B2:G7")] + [TestCase("B2:G7", "A1:A12", true, "B2:G7")] + [TestCase("B2:G7", "A8:H8", true, "B2:G7")] + [TestCase("B2:G7", "H1:H8", true, "B2:G7")] + public void CanSplitRange(string rangeAddress, string splitBy, bool includeIntersection, string expectedResult) + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + var range = ws.Range(rangeAddress) as XLRange; + var splitter = ws.Range(splitBy); + + var result = range.Split(splitter.RangeAddress, includeIntersection); + + var actualAddresses = string.Join(",", result.Select(r => r.RangeAddress.ToString())); + + Assert.AreEqual(expectedResult, actualAddresses); + + } + } +} diff --git a/ClosedXML_Tests/Excel/RichText/XLRichStringTests.cs b/ClosedXML_Tests/Excel/RichText/XLRichStringTests.cs new file mode 100644 index 0000000..667633d --- /dev/null +++ b/ClosedXML_Tests/Excel/RichText/XLRichStringTests.cs @@ -0,0 +1,762 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests +{ + /// + /// This is a test class for XLRichStringTests and is intended + /// to contain all XLRichStringTests Unit Tests + /// + [TestFixture] + public class XLRichStringTests + { + [Test] + public void AccessRichTextTest1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.RichText.AddText("12"); + cell.DataType = XLDataType.Number; + + Assert.AreEqual(12.0, cell.GetDouble()); + + IXLRichText richText = cell.RichText; + + Assert.AreEqual("12", richText.ToString()); + + richText.AddText("34"); + + Assert.AreEqual("1234", cell.GetString()); + + Assert.AreEqual(XLDataType.Number, cell.DataType); + + Assert.AreEqual(1234.0, cell.GetDouble()); + } + + /// + /// A test for AddText + /// + [Test] + public void AddTextTest1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + IXLRichText richString = cell.RichText; + + string text = "Hello"; + richString.AddText(text).SetBold().SetFontColor(XLColor.Red); + + Assert.AreEqual(cell.GetString(), text); + Assert.AreEqual(cell.RichText.First().Bold, true); + Assert.AreEqual(cell.RichText.First().FontColor, XLColor.Red); + + Assert.AreEqual(1, richString.Count); + + richString.AddText("World"); + Assert.AreEqual(richString.First().Text, text, "Item in collection is not the same as the one returned"); + } + + [Test] + public void AddTextTest2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + Int32 number = 123; + + cell.SetValue(number).Style + .Font.SetBold() + .Font.SetFontColor(XLColor.Red); + + string text = number.ToString(); + + Assert.AreEqual(cell.RichText.ToString(), text); + Assert.AreEqual(cell.RichText.First().Bold, true); + Assert.AreEqual(cell.RichText.First().FontColor, XLColor.Red); + + Assert.AreEqual(1, cell.RichText.Count); + + cell.RichText.AddText("World"); + Assert.AreEqual(cell.RichText.First().Text, text, "Item in collection is not the same as the one returned"); + } + + [Test] + public void AddTextTest3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + Int32 number = 123; + cell.Value = number; + cell.Style + .Font.SetBold() + .Font.SetFontColor(XLColor.Red); + + string text = number.ToString(); + + Assert.AreEqual(cell.RichText.ToString(), text); + Assert.AreEqual(cell.RichText.First().Bold, true); + Assert.AreEqual(cell.RichText.First().FontColor, XLColor.Red); + + Assert.AreEqual(1, cell.RichText.Count); + + cell.RichText.AddText("World"); + Assert.AreEqual(cell.RichText.First().Text, text, "Item in collection is not the same as the one returned"); + } + + /// + /// A test for Clear + /// + [Test] + public void ClearTest() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + richString.AddText(" "); + richString.AddText("World!"); + + richString.ClearText(); + String expected = String.Empty; + String actual = richString.ToString(); + Assert.AreEqual(expected, actual); + + Assert.AreEqual(0, richString.Count); + } + + [Test] + public void CountTest() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + richString.AddText(" "); + richString.AddText("World!"); + + Assert.AreEqual(3, richString.Count); + } + + [Test] + public void HasRichTextTest1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLCell cell = ws.Cell(1, 1); + cell.RichText.AddText("123"); + + Assert.AreEqual(true, cell.HasRichText); + + cell.DataType = XLDataType.Text; + + Assert.AreEqual(true, cell.HasRichText); + + cell.DataType = XLDataType.Number; + + Assert.AreEqual(false, cell.HasRichText); + + cell.RichText.AddText("123"); + + Assert.AreEqual(true, cell.HasRichText); + + cell.Value = 123; + + Assert.AreEqual(false, cell.HasRichText); + + cell.RichText.AddText("123"); + + Assert.AreEqual(true, cell.HasRichText); + + cell.SetValue("123"); + + Assert.AreEqual(false, cell.HasRichText); + } + + /// + /// A test for Characters + /// + [Test] + public void Substring_All_From_OneString() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + IXLFormattedText actual = richString.Substring(0); + + Assert.AreEqual(richString.First(), actual.First()); + + Assert.AreEqual(1, actual.Count); + + actual.First().SetBold(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.First().Bold); + } + + [Test] + public void Substring_All_From_ThreeStrings() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(0); + + Assert.AreEqual(richString.ElementAt(0), actual.ElementAt(0)); + Assert.AreEqual(richString.ElementAt(1), actual.ElementAt(1)); + Assert.AreEqual(richString.ElementAt(2), actual.ElementAt(2)); + + Assert.AreEqual(3, actual.Count); + Assert.AreEqual(3, richString.Count); + + actual.First().SetBold(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.First().Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.Last().Bold); + } + + [Test] + public void Substring_From_OneString_End() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + IXLFormattedText actual = richString.Substring(2); + + Assert.AreEqual(1, actual.Count); // substring was in one piece + + Assert.AreEqual(2, richString.Count); // The text was split because of the substring + + Assert.AreEqual("llo", actual.First().Text); + + Assert.AreEqual("He", richString.First().Text); + Assert.AreEqual("llo", richString.Last().Text); + + actual.First().SetBold(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.First().Bold); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.Last().Bold); + + richString.Last().SetItalic(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.First().Italic); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.Last().Italic); + + Assert.AreEqual(true, actual.First().Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.First().FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.Last().FontSize); + + Assert.AreEqual(20, actual.First().FontSize); + } + + [Test] + public void Substring_From_OneString_Middle() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + IXLFormattedText actual = richString.Substring(2, 2); + + Assert.AreEqual(1, actual.Count); // substring was in one piece + + Assert.AreEqual(3, richString.Count); // The text was split because of the substring + + Assert.AreEqual("ll", actual.First().Text); + + Assert.AreEqual("He", richString.First().Text); + Assert.AreEqual("ll", richString.ElementAt(1).Text); + Assert.AreEqual("o", richString.Last().Text); + + actual.First().SetBold(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.First().Bold); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.Last().Bold); + + richString.Last().SetItalic(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.First().Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Italic); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.Last().Italic); + + Assert.AreEqual(false, actual.First().Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.First().FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(1).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.Last().FontSize); + + Assert.AreEqual(20, actual.First().FontSize); + } + + [Test] + public void Substring_From_OneString_Start() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + IXLFormattedText actual = richString.Substring(0, 2); + + Assert.AreEqual(1, actual.Count); // substring was in one piece + + Assert.AreEqual(2, richString.Count); // The text was split because of the substring + + Assert.AreEqual("He", actual.First().Text); + + Assert.AreEqual("He", richString.First().Text); + Assert.AreEqual("llo", richString.Last().Text); + + actual.First().SetBold(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.First().Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.Last().Bold); + + richString.Last().SetItalic(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.First().Italic); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.Last().Italic); + + Assert.AreEqual(false, actual.First().Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.First().FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.Last().FontSize); + + Assert.AreEqual(20, actual.First().FontSize); + } + + [Test] + public void Substring_From_ThreeStrings_End1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(21); + + Assert.AreEqual(1, actual.Count); // substring was in one piece + + Assert.AreEqual(4, richString.Count); // The text was split because of the substring + + Assert.AreEqual("bors!", actual.First().Text); + + Assert.AreEqual("Good Morning", richString.ElementAt(0).Text); + Assert.AreEqual(" my ", richString.ElementAt(1).Text); + Assert.AreEqual("neigh", richString.ElementAt(2).Text); + Assert.AreEqual("bors!", richString.ElementAt(3).Text); + + actual.First().SetBold(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(0).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Bold); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(3).Bold); + + richString.Last().SetItalic(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(0).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Italic); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(3).Italic); + + Assert.AreEqual(true, actual.First().Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(0).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(1).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(2).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(3).FontSize); + + Assert.AreEqual(20, actual.First().FontSize); + } + + [Test] + public void Substring_From_ThreeStrings_End2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(13); + + Assert.AreEqual(2, actual.Count); + + Assert.AreEqual(4, richString.Count); // The text was split because of the substring + + Assert.AreEqual("my ", actual.ElementAt(0).Text); + Assert.AreEqual("neighbors!", actual.ElementAt(1).Text); + + Assert.AreEqual("Good Morning", richString.ElementAt(0).Text); + Assert.AreEqual(" ", richString.ElementAt(1).Text); + Assert.AreEqual("my ", richString.ElementAt(2).Text); + Assert.AreEqual("neighbors!", richString.ElementAt(3).Text); + + actual.ElementAt(1).SetBold(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(0).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Bold); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(3).Bold); + + richString.Last().SetItalic(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(0).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Italic); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(3).Italic); + + Assert.AreEqual(false, actual.ElementAt(0).Italic); + Assert.AreEqual(true, actual.ElementAt(1).Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(0).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(1).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(2).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(3).FontSize); + + Assert.AreEqual(20, actual.ElementAt(0).FontSize); + Assert.AreEqual(20, actual.ElementAt(1).FontSize); + } + + [Test] + public void Substring_From_ThreeStrings_Mid1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(5, 10); + + Assert.AreEqual(2, actual.Count); + + Assert.AreEqual(5, richString.Count); // The text was split because of the substring + + Assert.AreEqual("Morning", actual.ElementAt(0).Text); + Assert.AreEqual(" my", actual.ElementAt(1).Text); + + Assert.AreEqual("Good ", richString.ElementAt(0).Text); + Assert.AreEqual("Morning", richString.ElementAt(1).Text); + Assert.AreEqual(" my", richString.ElementAt(2).Text); + Assert.AreEqual(" ", richString.ElementAt(3).Text); + Assert.AreEqual("neighbors!", richString.ElementAt(4).Text); + } + + [Test] + public void Substring_From_ThreeStrings_Mid2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(5, 15); + + Assert.AreEqual(3, actual.Count); + + Assert.AreEqual(5, richString.Count); // The text was split because of the substring + + Assert.AreEqual("Morning", actual.ElementAt(0).Text); + Assert.AreEqual(" my ", actual.ElementAt(1).Text); + Assert.AreEqual("neig", actual.ElementAt(2).Text); + + Assert.AreEqual("Good ", richString.ElementAt(0).Text); + Assert.AreEqual("Morning", richString.ElementAt(1).Text); + Assert.AreEqual(" my ", richString.ElementAt(2).Text); + Assert.AreEqual("neig", richString.ElementAt(3).Text); + Assert.AreEqual("hbors!", richString.ElementAt(4).Text); + } + + [Test] + public void Substring_From_ThreeStrings_Start1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(0, 4); + + Assert.AreEqual(1, actual.Count); // substring was in one piece + + Assert.AreEqual(4, richString.Count); // The text was split because of the substring + + Assert.AreEqual("Good", actual.First().Text); + + Assert.AreEqual("Good", richString.ElementAt(0).Text); + Assert.AreEqual(" Morning", richString.ElementAt(1).Text); + Assert.AreEqual(" my ", richString.ElementAt(2).Text); + Assert.AreEqual("neighbors!", richString.ElementAt(3).Text); + + actual.First().SetBold(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(0).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(3).Bold); + + richString.First().SetItalic(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(0).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(3).Italic); + + Assert.AreEqual(true, actual.First().Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(0).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(1).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(2).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(3).FontSize); + + Assert.AreEqual(20, actual.First().FontSize); + } + + [Test] + public void Substring_From_ThreeStrings_Start2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Good Morning"); + richString.AddText(" my "); + richString.AddText("neighbors!"); + + IXLFormattedText actual = richString.Substring(0, 15); + + Assert.AreEqual(2, actual.Count); + + Assert.AreEqual(4, richString.Count); // The text was split because of the substring + + Assert.AreEqual("Good Morning", actual.ElementAt(0).Text); + Assert.AreEqual(" my", actual.ElementAt(1).Text); + + Assert.AreEqual("Good Morning", richString.ElementAt(0).Text); + Assert.AreEqual(" my", richString.ElementAt(1).Text); + Assert.AreEqual(" ", richString.ElementAt(2).Text); + Assert.AreEqual("neighbors!", richString.ElementAt(3).Text); + + actual.ElementAt(1).SetBold(); + + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(0).Bold); + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(1).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Bold); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(3).Bold); + + richString.First().SetItalic(); + + Assert.AreEqual(true, ws.Cell(1, 1).RichText.ElementAt(0).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(1).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(2).Italic); + Assert.AreEqual(false, ws.Cell(1, 1).RichText.ElementAt(3).Italic); + + Assert.AreEqual(true, actual.ElementAt(0).Italic); + Assert.AreEqual(false, actual.ElementAt(1).Italic); + + richString.SetFontSize(20); + + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(0).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(1).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(2).FontSize); + Assert.AreEqual(20, ws.Cell(1, 1).RichText.ElementAt(3).FontSize); + + Assert.AreEqual(20, actual.ElementAt(0).FontSize); + Assert.AreEqual(20, actual.ElementAt(1).FontSize); + } + + [Test] + public void Substring_IndexOutsideRange1() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + Assert.That(() => richString.Substring(50), Throws.TypeOf()); + } + + [Test] + public void Substring_IndexOutsideRange2() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + richString.AddText("World"); + + Assert.That(() => richString.Substring(50), Throws.TypeOf()); + } + + [Test] + public void Substring_IndexOutsideRange3() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + + Assert.That(() => richString.Substring(1, 10), Throws.TypeOf()); + } + + [Test] + public void Substring_IndexOutsideRange4() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + richString.AddText("World"); + + Assert.That(() => richString.Substring(5, 20), Throws.TypeOf()); + } + + /// + /// A test for ToString + /// + [Test] + public void ToStringTest() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + IXLRichText richString = ws.Cell(1, 1).RichText; + + richString.AddText("Hello"); + richString.AddText(" "); + richString.AddText("World"); + string expected = "Hello World"; + string actual = richString.ToString(); + Assert.AreEqual(expected, actual); + + richString.AddText("!"); + expected = "Hello World!"; + actual = richString.ToString(); + Assert.AreEqual(expected, actual); + + richString.ClearText(); + expected = String.Empty; + actual = richString.ToString(); + Assert.AreEqual(expected, actual); + } + + [Test(Description = "See #1361")] + public void CanClearInlinedRichText() + { + using (var outputStream = new MemoryStream()) + { + using (var inputStream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\InlinedRichText\ChangeRichText\inputfile.xlsx"))) + using (var workbook = new XLWorkbook(inputStream)) + { + workbook.Worksheets.First().Cell("A1").Value = ""; + workbook.SaveAs(outputStream); + } + + using (var wb = new XLWorkbook(outputStream)) + { + Assert.AreEqual("", wb.Worksheets.First().Cell("A1").Value); + } + } + } + + [Test] + public void CanChangeInlinedRichText() + { + void testRichText(IXLRichText richText) + { + Assert.IsNotNull(richText); + Assert.IsTrue(richText.Any()); + Assert.AreEqual("3", richText.ElementAt(2).Text); + Assert.AreEqual(XLColor.Red, richText.ElementAt(2).FontColor); + } + + using (var outputStream = new MemoryStream()) + { + using (var inputStream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\InlinedRichText\ChangeRichText\inputfile.xlsx"))) + using (var workbook = new XLWorkbook(inputStream)) + { + var richText = workbook.Worksheets.First().Cell("A1").RichText; + testRichText(richText); + richText.AddText(" - changed"); + workbook.SaveAs(outputStream); + } + + using (var wb = new XLWorkbook(outputStream)) + { + var cell = wb.Worksheets.First().Cell("A1"); + Assert.IsFalse(cell.ShareString); + Assert.IsTrue(cell.HasRichText); + var rt = cell.RichText; + Assert.AreEqual("Year (range: 3 yrs) - changed", rt.ToString()); + testRichText(rt); + } + } + } + + [Test] + public void ClearInlineRichTextWhenRelevant() + { + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell = ws.FirstCell(); + + cell.RichText.AddText("Bold").SetBold().AddText(" and red").SetBold().SetFontColor(XLColor.Red); + cell.ShareString = false; + + //wb.SaveAs(ms); + wb.SaveAs(ms); + } + ms.Seek(0, SeekOrigin.Begin); + + var wb2 = new XLWorkbook(ms); + { + var ws = wb2.Worksheets.First(); + var cell = ws.FirstCell(); + + cell.FormulaA1 = "=1 + 2"; + wb2.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + return wb2; + }, @"Other\InlinedRichText\ChangeRichTextToFormula\output.xlsx"); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Rows/RowTests.cs b/ClosedXML_Tests/Excel/Rows/RowTests.cs new file mode 100644 index 0000000..89dc857 --- /dev/null +++ b/ClosedXML_Tests/Excel/Rows/RowTests.cs @@ -0,0 +1,272 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class RowTests + { + [Test] + public void CopyRow() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Test").Style.Font.SetBold(); + ws.FirstRow().CopyTo(ws.Row(2)); + + Assert.IsTrue(ws.Cell("A2").Style.Font.Bold); + } + + [Test] + public void InsertingRowsAbove1() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Rows("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Row(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLRow row1 = ws.Row(1); + IXLRow row2 = ws.Row(2); + IXLRow row3 = ws.Row(3); + + IXLRow rowIns = ws.Row(1).InsertRowsAbove(1).First(); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Row(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Row(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, ws.Row(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Row(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Row(3).Cell(2).GetString()); + + Assert.AreEqual(ws.Style.Fill.BackgroundColor, rowIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, rowIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(ws.Style.Fill.BackgroundColor, rowIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, row2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, row2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, row2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", row2.Cell(2).GetString()); + } + + [Test] + public void InsertingRowsAbove2() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Rows("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Row(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLRow row1 = ws.Row(1); + IXLRow row2 = ws.Row(2); + IXLRow row3 = ws.Row(3); + + IXLRow rowIns = ws.Row(2).InsertRowsAbove(1).First(); + + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Row(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Row(3).Cell(2).GetString()); + + Assert.AreEqual(XLColor.Red, rowIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, rowIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, rowIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, row2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, row2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, row2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", row2.Cell(2).GetString()); + } + + [Test] + public void InsertingRowsAbove3() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + + ws.Rows("1,3").Style.Fill.SetBackgroundColor(XLColor.Red); + ws.Row(2).Style.Fill.SetBackgroundColor(XLColor.Yellow); + ws.Cell(2, 2).SetValue("X").Style.Fill.SetBackgroundColor(XLColor.Green); + + IXLRow row1 = ws.Row(1); + IXLRow row2 = ws.Row(2); + IXLRow row3 = ws.Row(3); + + IXLRow rowIns = ws.Row(3).InsertRowsAbove(1).First(); + + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(1).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Row(2).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Row(2).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Row(2).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Row(3).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, ws.Row(3).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, ws.Row(4).Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", ws.Row(2).Cell(2).GetString()); + + Assert.AreEqual(XLColor.Yellow, rowIns.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, rowIns.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, rowIns.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row1.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row1.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Yellow, row2.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, row2.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Yellow, row2.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual(XLColor.Red, row3.Cell(1).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(2).Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Red, row3.Cell(3).Style.Fill.BackgroundColor); + + Assert.AreEqual("X", row2.Cell(2).GetString()); + } + + [Test] + public void InsertingRowsAbove4() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + + ws.Row(2).Height = 15; + ws.Row(3).Height = 20; + ws.Row(4).Height = 25; + ws.Row(5).Height = 35; + + ws.Row(2).FirstCell().SetValue("Row height: 15"); + ws.Row(3).FirstCell().SetValue("Row height: 20"); + ws.Row(4).FirstCell().SetValue("Row height: 25"); + ws.Row(5).FirstCell().SetValue("Row height: 35"); + + ws.Range("3:3").InsertRowsAbove(1); + + Assert.AreEqual(15, ws.Row(2).Height); + Assert.AreEqual(20, ws.Row(4).Height); + Assert.AreEqual(25, ws.Row(5).Height); + Assert.AreEqual(35, ws.Row(6).Height); + + Assert.AreEqual(20, ws.Row(3).Height); + ws.Row(3).ClearHeight(); + Assert.AreEqual(ws.RowHeight, ws.Row(3).Height); + } + } + + [Test] + public void NoRowsUsed() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + Int32 count = 0; + + foreach (IXLRow row in ws.RowsUsed()) + count++; + + foreach (IXLRangeRow row in ws.Range("A1:C3").RowsUsed()) + count++; + + Assert.AreEqual(0, count); + } + + [Test] + public void RowUsed() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + ws.Cell(1, 2).SetValue("Test"); + ws.Cell(1, 3).SetValue("Test"); + + IXLRangeRow fromRow = ws.Row(1).RowUsed(); + Assert.AreEqual("B1:C1", fromRow.RangeAddress.ToStringRelative()); + + IXLRangeRow fromRange = ws.Range("A1:E1").FirstRow().RowUsed(); + Assert.AreEqual("B1:C1", fromRange.RangeAddress.ToStringRelative()); + } + + [Test] + public void UngroupFromAll() + { + IXLWorksheet ws = new XLWorkbook().AddWorksheet("Sheet1"); + ws.Rows(1, 2).Group(); + ws.Rows(1, 2).Ungroup(true); + } + + [Test] + public void NegativeRowNumberIsInvalid() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1") as XLWorksheet; + + var row = new XLRow(ws, -1); + + Assert.IsFalse(row.RangeAddress.IsValid); + } + + [Test] + public void DeleteRowOnWorksheetWithComment() + { + var ws = new XLWorkbook().AddWorksheet(); + ws.Cell(4, 1).Comment.AddText("test"); + ws.Column(1).Width = 100; + Assert.DoesNotThrow(() => ws.Row(1).Delete()); + } + } +} diff --git a/ClosedXML_Tests/Excel/Saving/SavingTests.cs b/ClosedXML_Tests/Excel/Saving/SavingTests.cs new file mode 100644 index 0000000..9ddf9d6 --- /dev/null +++ b/ClosedXML_Tests/Excel/Saving/SavingTests.cs @@ -0,0 +1,733 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using ClosedXML_Tests.Utils; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using NUnit.Framework; +using System; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace ClosedXML_Tests.Excel.Saving +{ + [TestFixture] + public class SavingTests + { + [Test] + public void CanSaveEmptyFile() + { + using (var ms = new MemoryStream()) + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet("Sheet1"); + wb.SaveAs(ms); + } + } + + [Test] + public void CanSuccessfullySaveFileMultipleTimes() + { + using (var memoryStream = new MemoryStream()) + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + + // Comments might cause duplicate VmlDrawing Id's - ensure it's tested: + sheet.Cell(1, 1).Comment.AddText("abc"); + + wb.SaveAs(memoryStream, validate: true); + + for (int i = 1; i <= 3; i++) + { + sheet.Cell(i, 1).Value = "test" + i; + wb.SaveAs(memoryStream, validate: true); + } + } + } + + [Test] + public void CanEscape_xHHHH_Correctly() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().Value = "Reserve_TT_A_BLOCAGE_CAG_x6904_2"; + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("Reserve_TT_A_BLOCAGE_CAG_x6904_2", ws.FirstCell().Value); + } + } + } + + [Test] + public void CanSaveFileMultipleTimesAfterDeletingWorksheet() + { + // https://github.com/ClosedXML/ClosedXML/issues/435 + + using (var ms = new MemoryStream()) + { + using (XLWorkbook book1 = new XLWorkbook()) + { + book1.AddWorksheet("sheet1"); + book1.AddWorksheet("sheet2"); + + book1.SaveAs(ms); + } + ms.Position = 0; + + using (XLWorkbook book2 = new XLWorkbook(ms)) + { + var ws = book2.Worksheet(1); + Assert.AreEqual("sheet1", ws.Name); + ws.Delete(); + book2.Save(); + book2.Save(); + } + } + } + + [Test] + public void CanSaveAndValidateFileInAnotherCulture() + { + string[] cultures = new string[] { "it", "de-AT" }; + + foreach (var culture in cultures) + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); + + using (var wb = new XLWorkbook()) + { + var memoryStream = new MemoryStream(); + var ws = wb.Worksheets.Add("Sheet1"); + + wb.SaveAs(memoryStream, true); + } + } + } + + [Test] + public void NotSaveCachedValueWhenFlagIsFalse() + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook book1 = new XLWorkbook()) + { + var sheet = book1.AddWorksheet("sheet1"); + sheet.Cell("A1").Value = 123; + sheet.Cell("A2").FormulaA1 = "A1*10"; + book1.RecalculateAllFormulas(); + var options = new SaveOptions { EvaluateFormulasBeforeSaving = false }; + + book1.SaveAs(ms, options); + } + ms.Position = 0; + + using (XLWorkbook book2 = new XLWorkbook(ms)) + { + var ws = book2.Worksheet(1); + + Assert.IsNull(ws.Cell("A2").CachedValue); + } + } + } + + [Test] + public void SaveCachedValueWhenFlagIsTrue() + { + using (var ms = new MemoryStream()) + { + using (XLWorkbook book1 = new XLWorkbook()) + { + var sheet = book1.AddWorksheet("sheet1"); + sheet.Cell("A1").Value = 123; + sheet.Cell("A2").FormulaA1 = "A1*10"; + sheet.Cell("A3").FormulaA1 = "TEXT(A2, \"# ###\")"; + var options = new SaveOptions { EvaluateFormulasBeforeSaving = true }; + + book1.SaveAs(ms, options); + } + ms.Position = 0; + + using (XLWorkbook book2 = new XLWorkbook(ms)) + { + var ws = book2.Worksheet(1); + + Assert.AreEqual(1230, ws.Cell("A2").CachedValue); + + Assert.AreEqual("1 230", ws.Cell("A3").CachedValue); + } + } + } + + [Test] + public void CanSaveAsCopyReadOnlyFile() + { + using (var original = new TemporaryFile()) + { + try + { + using (var copy = new TemporaryFile()) + { + // Arrange + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + wb.SaveAs(original.Path); + } + File.SetAttributes(original.Path, FileAttributes.ReadOnly); + + // Act + using (var wb = new XLWorkbook(original.Path)) + { + wb.SaveAs(copy.Path); + } + + // Assert + Assert.IsTrue(File.Exists(copy.Path)); + Assert.IsFalse(File.GetAttributes(copy.Path).HasFlag(FileAttributes.ReadOnly)); + } + } + finally + { + // Tear down + File.SetAttributes(original.Path, FileAttributes.Normal); + } + } + } + + [Test] + public void CanSaveAsOverwriteExistingFile() + { + using (var existing = new TemporaryFile()) + { + // Arrange + File.WriteAllText(existing.Path, ""); + + // Act + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + wb.SaveAs(existing.Path); + } + + // Assert + Assert.IsTrue(File.Exists(existing.Path)); + Assert.Greater(new FileInfo(existing.Path).Length, 0); + } + } + + [Test] + public void CannotSaveAsOverwriteExistingReadOnlyFile() + { + using (var existing = new TemporaryFile()) + { + try + { + // Arrange + File.WriteAllText(existing.Path, ""); + File.SetAttributes(existing.Path, FileAttributes.ReadOnly); + + // Act + TestDelegate saveAs = () => + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("TestSheet"); + wb.SaveAs(existing.Path); + } + }; + + // Assert + Assert.Throws(typeof(UnauthorizedAccessException), saveAs); + } + finally + { + // Tear down + File.SetAttributes(existing.Path, FileAttributes.Normal); + } + } + } + + [Test] + public void PageBreaksDontDuplicateAtSaving() + { + // https://github.com/ClosedXML/ClosedXML/issues/666 + + using (var ms = new MemoryStream()) + { + using (var wb1 = new XLWorkbook()) + { + var ws = wb1.Worksheets.Add("Page Breaks"); + ws.PageSetup.PrintAreas.Add("A1:D5"); + ws.PageSetup.AddHorizontalPageBreak(2); + ws.PageSetup.AddVerticalPageBreak(2); + wb1.SaveAs(ms); + wb1.Save(); + } + using (var wb2 = new XLWorkbook(ms)) + { + var ws = wb2.Worksheets.First(); + + Assert.AreEqual(1, ws.PageSetup.ColumnBreaks.Count); + Assert.AreEqual(1, ws.PageSetup.RowBreaks.Count); + } + } + } + + [Test] + public void CanSaveFileWithPictureAndComment() + { + using (var ms = new MemoryStream()) + using (var wb = new XLWorkbook()) + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)).GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + using (var bitmap = Bitmap.FromStream(resourceStream) as Bitmap) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("D4").Value = "Hello world."; + + ws.AddPicture(bitmap, "MyPicture") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50) + .WithSize(200, 200); + + ws.Cell("D4").Comment.SetVisible().AddText("This is a comment"); + + wb.SaveAs(ms); + } + } + + [Test] + public void PreserveChartsWhenSaving() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Charts\PreserveCharts\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"Other\Charts\PreserveCharts\outputfile.xlsx"); + } + } + + [Test] + public void DeletingAllPicturesRemovesDrawingPart() + { + TestHelper.CreateAndCompare(() => + { + var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx")); + var wb = new XLWorkbook(stream); + foreach (var ws in wb.Worksheets) + { + var pictureNames = ws.Pictures.Select(pic => pic.Name).ToArray(); + foreach (var name in pictureNames) + ws.Pictures.Delete(name); + } + + return wb; + }, @"Other\Drawings\NoDrawings\outputfile.xlsx"); + } + + [Test] + [TestCase("xlsx", SpreadsheetDocumentType.Workbook)] + [TestCase("xlsm", SpreadsheetDocumentType.MacroEnabledWorkbook)] + [TestCase("xltx", SpreadsheetDocumentType.Template)] + [TestCase("xltm", SpreadsheetDocumentType.MacroEnabledTemplate)] + public void SavesAsProperSpreadsheetDocumentType(string extension, SpreadsheetDocumentType expectedType) + { + using (var tf = new TemporaryFile(Path.ChangeExtension(Path.GetTempFileName(), extension))) + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add("Sheet1"); + wb.SaveAs(tf.Path); + } + + using (var package = SpreadsheetDocument.Open(tf.Path, false)) + { + Assert.AreEqual(expectedType, package.DocumentType); + } + } + } + + [Test] + public void CanSaveTemplateAsWorkbook() + { + // See #1375 + using (var template = new TemporaryFile(Path.ChangeExtension(Path.GetTempFileName(), "xltx"))) + using (var workbook = new TemporaryFile()) + { + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet(); + wb.SaveAs(template.Path); + } + using (var wb = new XLWorkbook(template.Path)) + { + wb.SaveAs(workbook.Path); + } + using (var package = SpreadsheetDocument.Open(workbook.Path, false)) + { + Assert.AreEqual(SpreadsheetDocumentType.Workbook, package.DocumentType); + } + } + } + + [Test] + public void SaveAsWithNoExtensionFails() + { + using (var tf = new TemporaryFile("FileWithNoExtension")) + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add("Sheet1"); + TestDelegate action = () => wb.SaveAs(tf.Path); + + Assert.Throws(action); + } + } + + [Test] + public void SaveAsWithUnsupportedExtensionFails() + { + using (var tf = new TemporaryFile("FileWithBadExtension.bad")) + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add("Sheet1"); + TestDelegate action = () => wb.SaveAs(tf.Path); + + Assert.Throws(action); + } + } + + [Test] + public void SaveCellValueWithLeadingQuotationMarkCorrectly() + { + var quotedFormulaValue = "'=IF(TRUE, 1, 0)"; + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.FirstCell(); + cell.SetValue(quotedFormulaValue); + Assert.IsFalse(cell.HasFormula); + Assert.AreEqual(quotedFormulaValue, cell.Value); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var cell = ws.FirstCell(); + Assert.IsFalse(cell.HasFormula); + Assert.AreEqual(quotedFormulaValue, cell.Value); + } + } + } + + [Test] + public void PreserveHeightOfEmptyRowsOnSaving() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.RowHeight = 50; + ws.Row(2).Height = 0; + ws.Row(3).Height = 20; + ws.Row(4).Height = 100; + + ws.CopyTo("Sheet2"); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + foreach (var sheetName in new[] { "Sheet1", "Sheet2" }) + { + var ws = wb.Worksheet(sheetName); + + Assert.AreEqual(50, ws.Row(1).Height); + Assert.AreEqual(0, ws.Row(2).Height); + Assert.AreEqual(20, ws.Row(3).Height); + Assert.AreEqual(100, ws.Row(4).Height); + } + } + } + } + + [Test] + public void PreserveWidthOfEmptyColumnsOnSaving() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Column(2).Width = 0; + ws.Column(3).Width = 20; + ws.Column(4).Width = 100; + + ws.CopyTo("Sheet2"); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + foreach (var sheetName in new[] { "Sheet1", "Sheet2" }) + { + var ws = wb.Worksheet(sheetName); + + Assert.AreEqual(ws.ColumnWidth, ws.Column(1).Width); + Assert.AreEqual(0, ws.Column(2).Width); + Assert.AreEqual(20, ws.Column(3).Width); + Assert.AreEqual(100, ws.Column(4).Width); + } + } + } + } + + [Test] + public void PreserveAlignmentOnSaving() + { + using (var input = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\HorizontalAlignment.xlsx"))) + using (var output = new MemoryStream()) + { + using (var wb = new XLWorkbook(input)) + { + wb.SaveAs(output); + } + + using (var wb = new XLWorkbook(output)) + { + Assert.AreEqual(XLAlignmentHorizontalValues.Center, wb.Worksheets.First().Cell("B1").Style.Alignment.Horizontal); + } + } + } + + [Test] + public void PreserveMultipleColorScalesOnSaving() + { + using (var output = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var sheet = wb.Worksheets.Add("test"); + sheet.Column(1).AddConditionalFormat().ColorScale().LowestValue(XLColor.Red) + .HighestValue(XLColor.Green); + + sheet.Column(2).AddConditionalFormat().ColorScale().LowestValue(XLColor.Alizarin) + .HighestValue(XLColor.Blue); + + wb.SaveAs(output); + } + + using (var wb = new XLWorkbook(output)) + { + var sheet = wb.Worksheets.First(); + var cf = sheet.ConditionalFormats + .OrderBy(x => x.Range.RangeAddress.FirstAddress.ColumnNumber) + .ToArray(); + Assert.AreEqual(2, cf.Length); + Assert.AreEqual(XLConditionalFormatType.ColorScale, cf[0].ConditionalFormatType); + Assert.AreEqual(XLColor.Red, cf[0].Colors[1]); + Assert.AreEqual(XLCFContentType.Minimum, cf[0].ContentTypes[1]); + Assert.AreEqual(XLColor.Green, cf[0].Colors[2]); + Assert.AreEqual(XLCFContentType.Maximum, cf[0].ContentTypes[2]); + Assert.AreEqual(XLConditionalFormatType.ColorScale, cf[1].ConditionalFormatType); + Assert.AreEqual(XLColor.Alizarin, cf[1].Colors[1]); + Assert.AreEqual(XLCFContentType.Minimum, cf[1].ContentTypes[1]); + Assert.AreEqual(XLColor.Blue, cf[1].Colors[2]); + Assert.AreEqual(XLCFContentType.Maximum, cf[1].ContentTypes[2]); + } + } + } + + [Test] + public void RemoveExistingInlineStringsIfRequired() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\InlineStrings\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + var ws = wb.Worksheet(1); + + var numericCells = ws.CellsUsed(c => double.TryParse(c.GetString(), out double _)); + var textCells = ws.CellsUsed(c => !double.TryParse(c.GetString(), out double _)); + + foreach (var cell in numericCells) + { + cell.Clear(XLClearOptions.AllFormats); + cell.SetDataType(XLDataType.Number); + } + + foreach (var cell in textCells) + { + cell.ShareString = true; + } + + wb.SaveAs(ms); + + return wb; + }, @"Other\InlineStrings\outputfile.xlsx"); + } + } + + [Test] + public void CanSaveFileWithEmptyFill() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\EmptyFill.xlsx"))) + using (var wb = new XLWorkbook(stream)) + using (var ms = new MemoryStream()) + { + Assert.DoesNotThrow(() => wb.SaveAs(ms, false)); + } + } + + [Test] + public void CanSaveSingleRowAutoFilter() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\SingleRowAutoFilter.xlsx"))) + using (var wb = new XLWorkbook(stream)) + using (var ms = new MemoryStream()) + { + Assert.DoesNotThrow(() => wb.SaveAs(ms, false)); + } + } + + [Test] + public void PivotTableWithVeryLongField() + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + + var longText = string.Join(" ", Enumerable.Range(0, 40).Select(i => "1234567890")); + + var data = new[] + { + new { Col1 = longText, Col2 = 2} + }; + + var table = ws.FirstCell().InsertTable(data); + + var pvtSheet = wb.AddWorksheet("pvt"); + + var pvt = table.CreatePivotTable(pvtSheet.FirstCell(), "PivotTable1"); + pvt.RowLabels.Add("Col1"); + + return wb; + }, @"Other\PivotTableReferenceFiles\LongText\outputfile.xlsx"); + } + + [Test] + public void CanSaveFileWithVml_NoComments() + { + //See #1285 + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\FileWithButton.xlsm"))) + using (var wb = new XLWorkbook(stream)) + using (var ms = new MemoryStream()) + { + Assert.DoesNotThrow(() => wb.SaveAs(ms)); + } + } + + [Test] + public void CanEnableWorkbookFilterPrivacyAndSaveInWorkbook() + { + using var ms = new MemoryStream(); + + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet(); + wb.SaveAs(ms, new SaveOptions { FilterPrivacy = true }); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = SpreadsheetDocument.Open(ms, false)) + { + Assert.IsTrue(wb.WorkbookPart.Workbook.WorkbookProperties.FilterPrivacy); + } + } + + [Test] + public void WorkbookFilterPrivacyIsNotSetByDefault() + { + using var ms = new MemoryStream(); + + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet(); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = SpreadsheetDocument.Open(ms, false)) + { + Assert.IsNull(wb.WorkbookPart.Workbook.WorkbookProperties.FilterPrivacy); + } + } + + [Test] + public void WorkbookFilterPrivacyIsReadCorrectly() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\FilterPrivacyEnabledWorkbook.xlsx"))) + using (var wb = SpreadsheetDocument.Open(stream, false)) + { + Assert.IsTrue(wb.WorkbookPart.Workbook.WorkbookProperties.FilterPrivacy); + } + } + + [Test] + public void CanSaveAsWithDataValidationAfterInsertFirstRowsAboveAndInsertFirstColumnsBefore() + { + using (var wb = new XLWorkbook()) + using (var ms = new MemoryStream()) + { + var ws = wb.AddWorksheet("WithDataValidation"); + ws.Range("B4:B4").SetDataValidation().WholeNumber.Between(0, 1); + + ws.Row(1).InsertRowsAbove(1); + var dv = ws.DataValidations.ToArray(); + Assert.AreEqual(1, dv.Length); + Assert.AreEqual("B5:B5", dv[0].Ranges.Single().RangeAddress.ToString()); + + Assert.DoesNotThrow(() => wb.SaveAs(ms)); + + + ws.Column(1).InsertColumnsBefore(1); + dv = ws.DataValidations.ToArray(); + Assert.AreEqual(1, dv.Length); + Assert.AreEqual("C5:C5", dv[0].Ranges.Single().RangeAddress.ToString()); + + Assert.DoesNotThrow(() => wb.SaveAs(ms)); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Sparklines/SparklinesTests.cs b/ClosedXML_Tests/Excel/Sparklines/SparklinesTests.cs new file mode 100644 index 0000000..8f8a372 --- /dev/null +++ b/ClosedXML_Tests/Excel/Sparklines/SparklinesTests.cs @@ -0,0 +1,1075 @@ +using ClosedXML.Excel; +using ClosedXML_Examples.Sparklines; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Sparklines +{ + [TestFixture] + public class SparklinesTests + { + #region Add sparklines + + [Test] + public void CannotCreateSparklineGroupsWithoutWorksheet() + { + TestDelegate action = () => new XLSparklineGroups(null); + Assert.Throws(action); + } + + [Test] + public void CannotCreateSparklineGroupWithoutWorksheet() + { + TestDelegate action = () => new XLSparklineGroup(null); + Assert.Throws(action); + } + + [Test] + public void CannotCreateSparklineWithoutGroup() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + TestDelegate action = () => new XLSparkline(null, ws.Cell("A1"), ws.Range("A2:A5")); + Assert.Throws(action); + } + + [Test] + public void CannotCreateSparklineWithoutLocation() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var group = new XLSparklineGroup(ws); + TestDelegate action = () => new XLSparkline(group, null, ws.Range("A2:A5")); + Assert.Throws(action); + } + + [Test] + public void CanCreateInvalidSparklineWithoutSourceData() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1"); + var group = new XLSparklineGroup(ws); + var sparkline = new XLSparkline(group, ws.FirstCell(), null); + Assert.IsFalse(sparkline.IsValid); + } + + [Test] + public void CanAddSparklineGroupForSingleCell() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add(new XLSparklineGroup(ws, "A1", "B1:E1")); + ws.SparklineGroups.Add("A2", "B2:E2"); + ws.SparklineGroups.Add(ws.Cell("A3"), ws.Range("B3:E3")); + + Assert.AreEqual(3, ws.SparklineGroups.Count()); + + Assert.AreEqual("A1", ws.SparklineGroups.ElementAt(0).Single().Location.Address.ToString()); + Assert.AreEqual("A2", ws.SparklineGroups.ElementAt(1).Single().Location.Address.ToString()); + Assert.AreEqual("A3", ws.SparklineGroups.ElementAt(2).Single().Location.Address.ToString()); + + Assert.AreEqual("B1:E1", ws.SparklineGroups.ElementAt(0).Single().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:E2", ws.SparklineGroups.ElementAt(1).Single().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3:E3", ws.SparklineGroups.ElementAt(2).Single().SourceData.RangeAddress.ToString()); + + Assert.IsTrue(ws.SparklineGroups.All(g => g.Worksheet == ws)); + } + + [Test] + public void CanAddSparklineGroupForVerticalRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add(ws.Range("A1:A3"), ws.Range("B1:E3")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + + Assert.AreEqual("A1", ws.SparklineGroups.Single().ElementAt(0).Location.Address.ToString()); + Assert.AreEqual("A2", ws.SparklineGroups.Single().ElementAt(1).Location.Address.ToString()); + Assert.AreEqual("A3", ws.SparklineGroups.Single().ElementAt(2).Location.Address.ToString()); + + Assert.AreEqual("B1:E1", ws.SparklineGroups.Single().ElementAt(0).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:E2", ws.SparklineGroups.Single().ElementAt(1).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3:E3", ws.SparklineGroups.Single().ElementAt(2).SourceData.RangeAddress.ToString()); + } + + [Test] + public void CanAddSparklineGroupForHorizontalRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add(ws.Range("A1:C1"), ws.Range("A2:C4")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + + Assert.AreEqual("A1", ws.SparklineGroups.Single().ElementAt(0).Location.Address.ToString()); + Assert.AreEqual("B1", ws.SparklineGroups.Single().ElementAt(1).Location.Address.ToString()); + Assert.AreEqual("C1", ws.SparklineGroups.Single().ElementAt(2).Location.Address.ToString()); + + Assert.AreEqual("A2:A4", ws.SparklineGroups.Single().ElementAt(0).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:B4", ws.SparklineGroups.Single().ElementAt(1).SourceData.RangeAddress.ToString()); + Assert.AreEqual("C2:C4", ws.SparklineGroups.Single().ElementAt(2).SourceData.RangeAddress.ToString()); + } + + [Test] + public void CannotAddSparklineForNonLinearRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + TestDelegate action = () => ws.SparklineGroups.Add(ws.Range("A1:C2"), ws.Range("A3:C4")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("locationRange must have either a single row or a single column", message); + } + + [Test] + public void CannotAddSparklineWhenRangesHaveDifferentWidths() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + TestDelegate action = () => ws.SparklineGroups.Add(ws.Range("A1:C1"), ws.Range("A3:D4")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("locationRange and sourceDataRange must have the same width", message); + } + + [Test] + public void CannotAddSparklineWhenRangesHaveDifferentHeights() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + TestDelegate action = () => ws.SparklineGroups.Add(ws.Range("A1:A3"), ws.Range("B1:B4")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("locationRange and sourceDataRange must have the same height", message); + } + + [Test] + public void CannotAddSparklineForCellWhenDataRangeIsNotLinear() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + TestDelegate action = () => ws.SparklineGroups.Add(ws.Range("A1:A1"), ws.Range("B1:C4")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("SourceData range must have either a single row or a single column", message); + } + + [Test] + public void CanAddSparklineToExistingGroup() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + var group = new XLSparklineGroup(ws); + + group.Add("A2", "B2:E2"); + group.Add(ws.Cell("A3"), ws.Range("B3:E3")); + + Assert.AreEqual(0, ws.SparklineGroups.Count()); + + Assert.AreEqual("A2", group.ElementAt(0).Location.Address.ToString()); + Assert.AreEqual("A3", group.ElementAt(1).Location.Address.ToString()); + + Assert.AreEqual("B2:E2", group.ElementAt(0).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3:E3", group.ElementAt(1).SourceData.RangeAddress.ToString()); + } + + [Test] + public void CannotAddSparklineGroupFromDifferentWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + + var group = new XLSparklineGroup(ws1); + + TestDelegate action = () => ws2.SparklineGroups.Add(group); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("The specified sparkline group belongs to the different worksheet", message); + } + + [Test] + public void CannotAddSparklineFromDifferentWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + + var group = new XLSparklineGroup(ws1); + + TestDelegate action = () => group.Add(ws2.Cell("A3"), ws1.Range("B3:E3")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("The specified sparkline belongs to the different worksheet", message); + } + + [Test] + public void AddSparklineToSameCellOverwritesItWhenSameGroup() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + var group = ws.SparklineGroups.Add("A1", "B1:E1"); + group.Add("A1", "B2:E2"); + + Assert.AreEqual(1, group.Count()); + + Assert.AreEqual("A1", group.Single().Location.Address.ToString()); + Assert.AreEqual("B2:E2", group.Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void AddSparklineToSameCellOverwritesItWhenDifferentGroup() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1", "B1:E1"); + ws.SparklineGroups.Add("A1", "B2:E2"); + + Assert.AreEqual(2, ws.SparklineGroups.Count()); + Assert.IsFalse(ws.SparklineGroups.First().Any()); + Assert.AreEqual("A1", ws.SparklineGroups.Last().Single().Location.Address.ToString()); + Assert.AreEqual("B2:E2", ws.SparklineGroups.Last().Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void CanAddSparklineReferringToDifferentWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws3 = wb.AddWorksheet("Sheet 3"); + + var group = ws1.SparklineGroups.Add("A1", "'Sheet 3'!B1:F1"); + + Assert.AreSame(ws3, group.Single().SourceData.Worksheet); + } + + #endregion Add sparklines + + #region Get sparklines + + [TestCase("A2", "B2:Z2")] + [TestCase("A50", "B50:Z50")] + [TestCase("A100", "B100:Z100")] + [TestCase("B1", "B2:B100")] + [TestCase("K1", "K2:K100")] + [TestCase("Z1", "Z2:Z100")] + public void CanGetSparklineForExistingCell(string cellAddress, string expectedSourceDataRange) + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A2:A100", "B2:Z100"); + ws.SparklineGroups.Add("B1:Z1", "B2:Z100"); + + var sp = ws.SparklineGroups.GetSparkline(ws.Cell(cellAddress)); + Assert.IsNotNull(sp); + Assert.AreEqual(cellAddress, sp.Location.Address.ToString()); + Assert.AreEqual(expectedSourceDataRange, sp.SourceData.RangeAddress.ToString()); + } + + [TestCase("A1")] + [TestCase("B2")] + [TestCase("A101")] + [TestCase("AA1")] + public void CannotGetSparklineForNonExistingCell(string cellAddress) + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A2:A100", "B2:Z100"); + ws.SparklineGroups.Add("B1:Z1", "B2:Z100"); + + var sp = ws.SparklineGroups.GetSparkline(ws.Cell(cellAddress)); + Assert.IsNull(sp); + } + + [Test] + public void CanGetSparklinesForRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A2:A100", "B2:Z100"); + ws.SparklineGroups.Add("B1:Z1", "B2:Z100"); + + var sparklines1 = ws.SparklineGroups.GetSparklines(ws.Range("A1:B2")); + var sparklines2 = ws.SparklineGroups.GetSparklines(ws.Range("B2:E4")); + var sparklines3 = ws.SparklineGroups.GetSparklines(ws.Range("A1:Z100")); + var sparklines4 = ws.SparklineGroups.GetSparklines(ws.Range("A:A")); + var sparklines5 = ws.SparklineGroups.GetSparklines(ws.Range("1:1")); + + Assert.AreEqual(2, sparklines1.Count()); + Assert.AreEqual(0, sparklines2.Count()); + Assert.AreEqual(99 + 25, sparklines3.Count()); + Assert.AreEqual(99, sparklines4.Count()); + Assert.AreEqual(25, sparklines5.Count()); + + Assert.AreEqual("A2", sparklines1.First().Location.Address.ToString()); + Assert.AreEqual("B1", sparklines1.Last().Location.Address.ToString()); + Assert.AreEqual("B2:Z2", sparklines1.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:B100", sparklines1.Last().SourceData.RangeAddress.ToString()); + } + + #endregion Get sparklines + + #region Remove sparklines + + [Test] + public void CanRemoveSparklineFromCell() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A3", "B1:Z3"); + ws.SparklineGroups.Remove(ws.Cell("A2")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(2, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().First().Location.Address.ToString()); + Assert.AreEqual("A3", ws.SparklineGroups.Single().Last().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.Single().First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3:Z3", ws.SparklineGroups.Single().Last().SourceData.RangeAddress.ToString()); + } + + [Test] + public void CanRemoveSparklineFromRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A5", "B1:Z5"); + ws.SparklineGroups.Remove(ws.Range("A2:D4")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(2, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().First().Location.Address.ToString()); + Assert.AreEqual("A5", ws.SparklineGroups.Single().Last().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.Single().First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B5:Z5", ws.SparklineGroups.Single().Last().SourceData.RangeAddress.ToString()); + } + + [Test] + public void RemoveSparklineFromEmptyCellDoesNothing() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + ws.SparklineGroups.Remove(ws.Cell("F2")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(2, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().First().Location.Address.ToString()); + Assert.AreEqual("A2", ws.SparklineGroups.Single().Last().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.Single().First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:Z2", ws.SparklineGroups.Single().Last().SourceData.RangeAddress.ToString()); + } + + #endregion Remove sparklines + + #region Change sparklines + + [Test] + public void CanChangeSparklineLocationInsideWorksheet() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + ws.SparklineGroups.Single().Last().SetLocation(ws.Cell("F2")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(2, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().First().Location.Address.ToString()); + Assert.AreEqual("F2", ws.SparklineGroups.Single().Last().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.Single().First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:Z2", ws.SparklineGroups.Single().Last().SourceData.RangeAddress.ToString()); + Assert.IsTrue(ws.Cell("A1").HasSparkline); + Assert.IsFalse(ws.Cell("A2").HasSparkline); + Assert.IsTrue(ws.Cell("F2").HasSparkline); + } + + [Test] + public void ChangeSparklineLocationOverwritesExistingSparklineSameGroup() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + ws.SparklineGroups.Single().Last().SetLocation(ws.Cell("A1")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(1, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().Single().Location.Address.ToString()); + Assert.AreEqual("B2:Z2", ws.SparklineGroups.Single().Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void ChangeSparklineLocationOverwritesExistingSparklineDifferentGroups() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + ws.SparklineGroups.Add("A3", "B3:Z3"); + ws.SparklineGroups.Last().Single().SetLocation(ws.Cell("A2")); + + Assert.AreEqual(2, ws.SparklineGroups.Count()); + Assert.AreEqual(1, ws.SparklineGroups.First().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.First().Single().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.First().Single().SourceData.RangeAddress.ToString()); + Assert.AreEqual(1, ws.SparklineGroups.Last().Count()); + Assert.AreEqual("A2", ws.SparklineGroups.Last().Single().Location.Address.ToString()); + Assert.AreEqual("B3:Z3", ws.SparklineGroups.Last().Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void CannotChangeSparklineLocationToAnotherWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + + var group = ws1.SparklineGroups.Add("A1:A2", "B1:Z2"); + + TestDelegate action = () => group.First().SetLocation(ws2.FirstCell()); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("Cannot move the sparkline to a different worksheet", message); + } + + [Test] + public void CanChangeSparklineSourceDataInsideWorksheet() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + + ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + ws.SparklineGroups.Single().Last().SetSourceData(ws.Range("D4:D50")); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.AreEqual(2, ws.SparklineGroups.Single().Count()); + Assert.AreEqual("A1", ws.SparklineGroups.Single().First().Location.Address.ToString()); + Assert.AreEqual("A2", ws.SparklineGroups.Single().Last().Location.Address.ToString()); + Assert.AreEqual("B1:Z1", ws.SparklineGroups.Single().First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("D4:D50", ws.SparklineGroups.Single().Last().SourceData.RangeAddress.ToString()); + } + + [Test] + public void CannotChangeSparklineSourceDataToNonLinearRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1", "B1:Z1"); + var sparkline = group.Single(); + + TestDelegate action = () => sparkline.SetSourceData(ws.Range("B1:Z2")); + + var message = Assert.Throws(action).Message; + Assert.AreEqual("SourceData range must have either a single row or a single column", message); + } + + [Test] + public void CanChangeSparklineStyle() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1", "B1:Z1"); + + group.Style = XLSparklineTheme.Colorful1; + + Assert.AreEqual(XLColor.FromHtml("FF5F5F5F"), group.Style.SeriesColor); + Assert.AreEqual(XLColor.FromHtml("FFFFB620"), group.Style.NegativeColor); + Assert.AreEqual(XLColor.FromHtml("FFD70077"), group.Style.MarkersColor); + Assert.AreEqual(XLColor.FromHtml("FF56BE79"), group.Style.HighMarkerColor); + Assert.AreEqual(XLColor.FromHtml("FFFF5055"), group.Style.LowMarkerColor); + Assert.AreEqual(XLColor.FromHtml("FF5687C2"), group.Style.FirstMarkerColor); + Assert.AreEqual(XLColor.FromHtml("FF359CEB"), group.Style.LastMarkerColor); + } + + [Test] + public void ChangeSparklineStyleDoesNotAffectOriginal() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1", "B1:Z1"); + group.Style = XLSparklineTheme.Colorful1; + + group.Style.NegativeColor = XLColor.Red; + + Assert.AreEqual(XLColor.Red, group.Style.NegativeColor); + Assert.AreNotEqual(XLColor.Red, XLSparklineTheme.Colorful1.NegativeColor); + } + + [Test] + public void CannotSetSparklineStyleToNull() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1", "B1:Z1"); + + TestDelegate action = () => group.Style = null; + + Assert.Throws(action); + } + + [Test] + public void SparklinesShiftOnRowInsert() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group1 = ws.SparklineGroups.Add("B2", "D4:F4"); + var group2 = ws.SparklineGroups.Add("B3", "D4:D8"); + var group3 = ws.SparklineGroups.Add("B4", "E1:E8"); + + ws.Row(2).InsertRowsBelow(3); + + Assert.AreEqual("B2", group1.First().Location.Address.ToString()); + Assert.AreEqual("D7:F7", group1.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B6", group2.First().Location.Address.ToString()); + Assert.AreEqual("D7:D11", group2.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B7", group3.First().Location.Address.ToString()); + Assert.AreEqual("E1:E11", group3.First().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklinesShiftOnRowDelete() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group1 = ws.SparklineGroups.Add("B2", "D7:F7"); + var group2 = ws.SparklineGroups.Add("B6", "D7:D11"); + var group3 = ws.SparklineGroups.Add("B7", "E1:E11"); + + ws.Rows(3, 5).Delete(); + + Assert.AreEqual("B2", group1.First().Location.Address.ToString()); + Assert.AreEqual("D4:F4", group1.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3", group2.First().Location.Address.ToString()); + Assert.AreEqual("D4:D8", group2.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B4", group3.First().Location.Address.ToString()); + Assert.AreEqual("E1:E8", group3.First().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklinesShiftOnColumnInsert() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group1 = ws.SparklineGroups.Add("B2", "D4:F4"); + var group2 = ws.SparklineGroups.Add("C3", "D4:D8"); + var group3 = ws.SparklineGroups.Add("D4", "A4:E4"); + + ws.Column(2).InsertColumnsAfter(3); + + Assert.AreEqual("B2", group1.First().Location.Address.ToString()); + Assert.AreEqual("G4:I4", group1.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("F3", group2.First().Location.Address.ToString()); + Assert.AreEqual("G4:G8", group2.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("G4", group3.First().Location.Address.ToString()); + Assert.AreEqual("A4:H4", group3.First().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklinesShiftOnColumnDelete() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group1 = ws.SparklineGroups.Add("B2", "G4:I4"); + var group2 = ws.SparklineGroups.Add("F3", "G4:G8"); + var group3 = ws.SparklineGroups.Add("G4", "A4:H4"); + + ws.Columns(3, 5).Delete(); + + Assert.AreEqual("B2", group1.First().Location.Address.ToString()); + Assert.AreEqual("D4:F4", group1.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("C3", group2.First().Location.Address.ToString()); + Assert.AreEqual("D4:D8", group2.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("D4", group3.First().Location.Address.ToString()); + Assert.AreEqual("A4:E4", group3.First().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklineRemovedWhenColumnDeleted() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:B1", "C2:D6"); + + ws.Column(2).Delete(); + + Assert.AreEqual(1, group.Count()); + Assert.AreEqual("A1", group.Single().Location.Address.ToString()); + Assert.AreEqual("B2:B6", group.Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklineRemovedWhenRowDeleted() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:A2", "C3:F4"); + + ws.Row(2).Delete(); + + Assert.AreEqual(1, group.Count()); + Assert.AreEqual("A1", group.Single().Location.Address.ToString()); + Assert.AreEqual("C2:F2", group.Single().SourceData.RangeAddress.ToString()); + } + + [Test] + public void SparklineRemovedWhenShiftedTooFarRight() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("XFD1", "A1:Z1"); + + ws.Column(1).InsertColumnsBefore(1); + + Assert.AreEqual(0, group.Count()); + } + + [Test] + public void SparklineRemovedWhenShiftedTooFarDown() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1048576", "A1:Z1"); + + ws.Row(1).InsertRowsAbove(1); + + Assert.AreEqual(0, group.Count()); + } + + [Test] + public void SparklineRangeInvalidatedWhenDeleted() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:B1", "C2:D6"); + + ws.Column(4).Delete(); + + Assert.AreEqual(2, group.Count()); + Assert.AreEqual("A1", group.First().Location.Address.ToString()); + Assert.AreEqual("C2:C6", group.First().SourceData.RangeAddress.ToString()); + Assert.AreEqual("B1", group.Last().Location.Address.ToString()); + Assert.IsFalse(group.Last().SourceData.RangeAddress.IsValid); + } + + #endregion Change sparklines + + #region Load and save sparkline groups + + [Test] + public void CanChangeSaveAndLoadSparklineGroup() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet 1"); + var originalGroup = ws.SparklineGroups.Add("A1:A3", "B1:Z3") + .SetDateRange(ws.Range("B4:Z4")) + .SetLineWeight(5.5) + .SetDisplayHidden(true) + .SetShowMarkers(XLSparklineMarkers.FirstPoint | XLSparklineMarkers.LastPoint | + XLSparklineMarkers.HighPoint | XLSparklineMarkers.LowPoint | + XLSparklineMarkers.NegativePoints | XLSparklineMarkers.Markers) + .SetDisplayEmptyCellsAs(XLDisplayBlanksAsValues.Zero) + .SetType(XLSparklineType.Stacked); + + originalGroup.HorizontalAxis + .SetColor(XLColor.AirForceBlue) + .SetVisible(true) + .SetRightToLeft(true); + + originalGroup.VerticalAxis + .SetManualMax(6.6) + .SetManualMin(1.2) + .SetMaxAxisType(XLSparklineAxisMinMax.Custom) + .SetMinAxisType(XLSparklineAxisMinMax.Custom); + + originalGroup.Style + .SetFirstMarkerColor(XLColor.AliceBlue) + .SetHighMarkerColor(XLColor.Alizarin) + .SetLastMarkerColor(XLColor.Almond) + .SetLowMarkerColor(XLColor.Amaranth) + .SetMarkersColor(XLColor.Amber) + .SetNegativeColor(XLColor.AmberSaeEce) + .SetSeriesColor(XLColor.AmericanRose); + + AssertGroupIsValid(originalGroup); + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + AssertGroupIsValid(ws.SparklineGroups.Single()); + } + } + + void AssertGroupIsValid(IXLSparklineGroup group) + { + Assert.AreEqual(3, group.Count()); + + Assert.AreEqual("A1", group.ElementAt(0).Location.Address.ToString()); + Assert.AreEqual("A2", group.ElementAt(1).Location.Address.ToString()); + Assert.AreEqual("A3", group.ElementAt(2).Location.Address.ToString()); + + Assert.AreEqual("B1:Z1", group.ElementAt(0).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B2:Z2", group.ElementAt(1).SourceData.RangeAddress.ToString()); + Assert.AreEqual("B3:Z3", group.ElementAt(2).SourceData.RangeAddress.ToString()); + + Assert.AreEqual("B4:Z4", group.DateRange.RangeAddress.ToString()); + + Assert.AreEqual(XLColor.AliceBlue, group.Style.FirstMarkerColor); + Assert.AreEqual(XLColor.Alizarin, group.Style.HighMarkerColor); + Assert.AreEqual(XLColor.Almond, group.Style.LastMarkerColor); + Assert.AreEqual(XLColor.Amaranth, group.Style.LowMarkerColor); + Assert.AreEqual(XLColor.Amber, group.Style.MarkersColor); + Assert.AreEqual(XLColor.AmberSaeEce, group.Style.NegativeColor); + Assert.AreEqual(XLColor.AmericanRose, group.Style.SeriesColor); + Assert.IsTrue(group.DisplayHidden); + Assert.AreEqual(5.5, group.LineWeight, XLHelper.Epsilon); + Assert.AreEqual(XLDisplayBlanksAsValues.Zero, group.DisplayEmptyCellsAs); + Assert.AreEqual(XLSparklineType.Stacked, group.Type); + + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.FirstPoint)); + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.LastPoint)); + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.HighPoint)); + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.LowPoint)); + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.NegativePoints)); + Assert.IsTrue(group.ShowMarkers.HasFlag(XLSparklineMarkers.Markers)); + + Assert.AreEqual(XLColor.AirForceBlue, group.HorizontalAxis.Color); + Assert.IsTrue(group.HorizontalAxis.IsVisible); + Assert.IsTrue(group.HorizontalAxis.RightToLeft); + Assert.IsTrue(group.HorizontalAxis.DateAxis); + + Assert.AreEqual(6.6, group.VerticalAxis.ManualMax, XLHelper.Epsilon); + Assert.AreEqual(1.2, group.VerticalAxis.ManualMin, XLHelper.Epsilon); + Assert.AreEqual(XLSparklineAxisMinMax.Custom, group.VerticalAxis.MaxAxisType); + Assert.AreEqual(XLSparklineAxisMinMax.Custom, group.VerticalAxis.MinAxisType); + } + } + + [Test] + public void CanLoadSparklines() + { + using (var ms = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Sparklines\SparklineThemes\inputfile.xlsx"))) + using (var wb = new XLWorkbook(ms)) + { + Assert.IsTrue(wb.Worksheets.All(ws => ws.SparklineGroups.Count() == 6)); + } + } + + [TestCase("Accent!B1", nameof(XLSparklineTheme.Accent1))] + [TestCase("Accent!B2", nameof(XLSparklineTheme.Accent2))] + [TestCase("Accent!B3", nameof(XLSparklineTheme.Accent3))] + [TestCase("Accent!B4", nameof(XLSparklineTheme.Accent4))] + [TestCase("Accent!B5", nameof(XLSparklineTheme.Accent5))] + [TestCase("Accent!B6", nameof(XLSparklineTheme.Accent6))] + [TestCase("'Accent Darker 25%'!B1", nameof(XLSparklineTheme.Accent1Darker25))] + [TestCase("'Accent Darker 25%'!B2", nameof(XLSparklineTheme.Accent2Darker25))] + [TestCase("'Accent Darker 25%'!B3", nameof(XLSparklineTheme.Accent3Darker25))] + [TestCase("'Accent Darker 25%'!B4", nameof(XLSparklineTheme.Accent4Darker25))] + [TestCase("'Accent Darker 25%'!B5", nameof(XLSparklineTheme.Accent5Darker25))] + [TestCase("'Accent Darker 25%'!B6", nameof(XLSparklineTheme.Accent6Darker25))] + [TestCase("'Accent Darker 50%'!B1", nameof(XLSparklineTheme.Accent1Darker50))] + [TestCase("'Accent Darker 50%'!B2", nameof(XLSparklineTheme.Accent2Darker50))] + [TestCase("'Accent Darker 50%'!B3", nameof(XLSparklineTheme.Accent3Darker50))] + [TestCase("'Accent Darker 50%'!B4", nameof(XLSparklineTheme.Accent4Darker50))] + [TestCase("'Accent Darker 50%'!B5", nameof(XLSparklineTheme.Accent5Darker50))] + [TestCase("'Accent Darker 50%'!B6", nameof(XLSparklineTheme.Accent6Darker50))] + [TestCase("'Accent Lighter 40%'!B1", nameof(XLSparklineTheme.Accent1Lighter40))] + [TestCase("'Accent Lighter 40%'!B2", nameof(XLSparklineTheme.Accent2Lighter40))] + [TestCase("'Accent Lighter 40%'!B3", nameof(XLSparklineTheme.Accent3Lighter40))] + [TestCase("'Accent Lighter 40%'!B4", nameof(XLSparklineTheme.Accent4Lighter40))] + [TestCase("'Accent Lighter 40%'!B5", nameof(XLSparklineTheme.Accent5Lighter40))] + [TestCase("'Accent Lighter 40%'!B6", nameof(XLSparklineTheme.Accent6Lighter40))] + [TestCase("Dark!B1", nameof(XLSparklineTheme.Dark1))] + [TestCase("Dark!B2", nameof(XLSparklineTheme.Dark2))] + [TestCase("Dark!B3", nameof(XLSparklineTheme.Dark3))] + [TestCase("Dark!B4", nameof(XLSparklineTheme.Dark4))] + [TestCase("Dark!B5", nameof(XLSparklineTheme.Dark5))] + [TestCase("Dark!B6", nameof(XLSparklineTheme.Dark6))] + [TestCase("Colorful!B1", nameof(XLSparklineTheme.Colorful1))] + [TestCase("Colorful!B2", nameof(XLSparklineTheme.Colorful2))] + [TestCase("Colorful!B3", nameof(XLSparklineTheme.Colorful3))] + [TestCase("Colorful!B4", nameof(XLSparklineTheme.Colorful4))] + [TestCase("Colorful!B5", nameof(XLSparklineTheme.Colorful5))] + [TestCase("Colorful!B6", nameof(XLSparklineTheme.Colorful6))] + public void SparklineThemesAreIdenticalToExcel(string cellAddress, string expectedThemeName) + { + using (var ms = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Sparklines\SparklineThemes\inputfile.xlsx"))) + using (var wb = new XLWorkbook(ms)) + { + var expectedStyle = GetThemeByName(expectedThemeName); + var actualStyle = wb.Cell(cellAddress).Sparkline.SparklineGroup.Style; + + Assert.AreEqual(expectedStyle, actualStyle); + } + + IXLSparklineStyle GetThemeByName(string themeName) + { + var themes = typeof(XLSparklineTheme); + var prop = themes.GetProperty(themeName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + return prop.GetValue(null, null) as IXLSparklineStyle; + } + } + + [Test] + public void DeletedSparklinesRemovedFromFile() + { + using (var input = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Sparklines\SparklineThemes\inputfile.xlsx"))) + using (var output = new MemoryStream()) + { + using (var wb = new XLWorkbook(input)) + { + wb.Worksheet(1).SparklineGroups.RemoveAll(); + wb.Worksheet(2).SparklineGroups.Remove(wb.Worksheet(2).Cell("B1")); + wb.Worksheet(3).SparklineGroups.Remove(wb.Worksheet(3).Range("B2:B6")); + wb.Worksheet(4).SparklineGroups.Remove(wb.Worksheet(4).SparklineGroups.First()); + + wb.SaveAs(output); + } + + using (var wb = new XLWorkbook(output)) + { + Assert.AreEqual(0, wb.Worksheet(1).SparklineGroups.Count()); + Assert.AreEqual(5, wb.Worksheet(2).SparklineGroups.Count()); + Assert.AreEqual(1, wb.Worksheet(3).SparklineGroups.Count()); + Assert.AreEqual(5, wb.Worksheet(4).SparklineGroups.Count()); + Assert.AreEqual(6, wb.Worksheet(5).SparklineGroups.Count()); + Assert.AreEqual(6, wb.Worksheet(6).SparklineGroups.Count()); + } + } + } + + [Test] + public void EmptySparklineGroupsSkippedOnSaving() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + + group.RemoveAll(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual(0, wb.Worksheets.First().SparklineGroups.Count()); + } + } + } + + [Test] + public void CanSaveAndLoadSparklineWithInvalidRange() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + + ws1.SparklineGroups.Add("A1:A3", "'Sheet 2'!B1:F3"); + ws1.SparklineGroups.Add("A4:A6", "B4:F6") + .SetDateRange(ws2.Range("A1:E1")); + + ws2.Delete(); + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.Single(); + + Assert.AreEqual(2, ws.SparklineGroups.Count()); + Assert.IsFalse(ws.Cell("A2").Sparkline.IsValid); + Assert.AreEqual("B5:F5", ws.Cell("A5").Sparkline.SourceData.RangeAddress.ToString()); + Assert.IsNull(ws.Cell("A5").Sparkline.SparklineGroup.DateRange); + } + } + } + + #endregion Load and save sparkline groups + + #region Change sparkline groups + + [Test] + public void SetManualMinChangesAxisTypeToCustom() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var axis = ws.SparklineGroups.Add("A1:A2", "B1:Z2") + .VerticalAxis + .SetMinAxisType(XLSparklineAxisMinMax.SameForAll); + + axis.ManualMin = 100; + + Assert.AreEqual(100, axis.ManualMin, XLHelper.Epsilon); + Assert.AreEqual(XLSparklineAxisMinMax.Custom, axis.MinAxisType); + } + + [Test] + public void SetManualMaxChangesAxisTypeToCustom() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var axis = ws.SparklineGroups.Add("A1:A2", "B1:Z2") + .VerticalAxis + .SetMaxAxisType(XLSparklineAxisMinMax.SameForAll); + + axis.ManualMax = 100; + + Assert.AreEqual(100, axis.ManualMax, XLHelper.Epsilon); + Assert.AreEqual(XLSparklineAxisMinMax.Custom, axis.MaxAxisType); + } + + [TestCase(XLSparklineAxisMinMax.Custom, 100)] + [TestCase(XLSparklineAxisMinMax.SameForAll, null)] + [TestCase(XLSparklineAxisMinMax.Automatic, null)] + public void SetAxisTypeToNonCustomSetsManualMinToNull(XLSparklineAxisMinMax axisType, double? expectedManualMin) + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var axis = ws.SparklineGroups.Add("A1", "B1:Z1") + .VerticalAxis + .SetManualMin(100); + + axis.MinAxisType = axisType; + + if (expectedManualMin.HasValue) + Assert.AreEqual(expectedManualMin.Value, axis.ManualMin.Value, XLHelper.Epsilon); + else + Assert.IsNull(axis.ManualMin); + } + + [TestCase(XLSparklineAxisMinMax.Custom, 100)] + [TestCase(XLSparklineAxisMinMax.SameForAll, null)] + [TestCase(XLSparklineAxisMinMax.Automatic, null)] + public void SetAxisTypeToNonCustomSetsManualMaxToNull(XLSparklineAxisMinMax axisType, double? expectedManualMax) + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var axis = ws.SparklineGroups.Add("A1", "B1:Z1") + .VerticalAxis + .SetManualMax(100); + + axis.MaxAxisType = axisType; + + if (expectedManualMax.HasValue) + Assert.AreEqual(expectedManualMax.Value, axis.ManualMax.Value, XLHelper.Epsilon); + else + Assert.IsNull(axis.ManualMax); + } + + [Test] + public void SetDateRangeChangesAxisType() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + + group.DateRange = ws.Range("B3:Z3"); + + Assert.IsTrue(group.HorizontalAxis.DateAxis); + } + + [Test] + public void SetDateRangeToNullChangesAxisType() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + group.DateRange = ws.Range("B3:Z3"); + + group.DateRange = null; + + Assert.IsFalse(group.HorizontalAxis.DateAxis); + } + + [Test] + public void CannotSetNonLinearDateRange() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + var group = ws.SparklineGroups.Add("A1:A2", "B1:Z2"); + + TestDelegate action = () => group.DateRange = ws.Range("B3:Z4"); + + Assert.Throws(action); + } + + #endregion Change sparkline groups + + #region Copy sparkline groups + + [Test] + public void CopyCellToSameWorksheetCopiesSparkline() + { + var ws = new XLWorkbook().AddWorksheet("Sheet 1"); + ws.SparklineGroups.Add("A1:A3", "B1:F3"); + var target = ws.Cell("D4"); + + ws.Cell("A2").CopyTo(target); + + Assert.AreEqual(1, ws.SparklineGroups.Count()); + Assert.IsTrue(target.HasSparkline); + Assert.AreSame(ws.Cell("A2").Sparkline.SparklineGroup, target.Sparkline.SparklineGroup); + Assert.AreEqual("E4:I4", target.Sparkline.SourceData.RangeAddress.ToString()); + } + + [Test] + public void CopyCellToDifferentWorksheetCopiesSparklineGroup() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + var ws3 = wb.AddWorksheet("Sheet 3"); + ws1.SparklineGroups.Add("A1:A3", "B1:F3"); + ws1.SparklineGroups.Add("A4:A6", "'Sheet 3'!B4:F6"); + var target1 = ws2.Cell("D4"); + var target2 = ws2.Cell("D5"); + + ws1.Cell("A2").CopyTo(target1); + ws1.Cell("A5").CopyTo(target2); + + Assert.AreEqual(2, ws1.SparklineGroups.Count()); + Assert.AreEqual(2, ws2.SparklineGroups.Count()); + Assert.IsTrue(target1.HasSparkline); + Assert.IsTrue(target2.HasSparkline); + Assert.AreEqual("'Sheet 2'!E4:I4", target1.Sparkline.SourceData.RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("'Sheet 3'!E5:I5", target2.Sparkline.SourceData.RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + + [Test] + public void CopySparklineIfDateRangeOnSameWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + var group = ws1.SparklineGroups.Add("A1:A3", "B1:F3"); + group.SetDateRange(ws1.Range("A4:E4")); + var target = ws2.Cell("D4"); + + ws1.Cell("A2").CopyTo(target); + + Assert.AreEqual(1, ws1.SparklineGroups.Count()); + Assert.AreEqual(1, ws2.SparklineGroups.Count()); + Assert.IsTrue(target.HasSparkline); + Assert.AreEqual("'Sheet 2'!D6:H6", target.Sparkline.SparklineGroup.DateRange.RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + + [Test] + public void CopySparklineIfDateRangeSourceOnDifferentWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet("Sheet 1"); + var ws2 = wb.AddWorksheet("Sheet 2"); + var ws3 = wb.AddWorksheet("Sheet 3"); + var group = ws1.SparklineGroups.Add("A1:A3", "B1:F3"); + group.SetDateRange(ws3.Range("A4:E4")); + var target = ws2.Cell("D4"); + + ws1.Cell("A2").CopyTo(target); + + Assert.AreEqual(1, ws1.SparklineGroups.Count()); + Assert.AreEqual(1, ws2.SparklineGroups.Count()); + Assert.IsTrue(target.HasSparkline); + Assert.AreEqual("'Sheet 3'!D6:H6", target.Sparkline.SparklineGroup.DateRange.RangeAddress.ToString(XLReferenceStyle.A1, true)); + } + + #endregion Copy sparkline groups + + #region Test Examples + + [Test] + public void CreateSampleSparklines() + { + TestHelper.RunTestExample(@"Sparklines\SampleSparklines.xlsx"); + } + + #endregion Test Examples + } +} diff --git a/ClosedXML_Tests/Excel/Styles/BorderTests.cs b/ClosedXML_Tests/Excel/Styles/BorderTests.cs new file mode 100644 index 0000000..bc64cae --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/BorderTests.cs @@ -0,0 +1,36 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.Styles +{ + public class BorderTests + { + [Test] + public void SetInsideBorderPreservesOutsideBorders() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + + ws.Cells("B2:C2").Style + .Border.SetOutsideBorder(XLBorderStyleValues.Thin) + .Border.SetOutsideBorderColor(XLColor.FromTheme(XLThemeColor.Accent1, 0.5)); + + //Check pre-conditions + Assert.AreEqual(XLBorderStyleValues.Thin, ws.Cell("B2").Style.Border.LeftBorder); + Assert.AreEqual(XLBorderStyleValues.Thin, ws.Cell("B2").Style.Border.RightBorder); + Assert.AreEqual(XLThemeColor.Accent1, ws.Cell("B2").Style.Border.LeftBorderColor.ThemeColor); + Assert.AreEqual(XLThemeColor.Accent1, ws.Cell("B2").Style.Border.RightBorderColor.ThemeColor); + + ws.Range("B2:C2").Style.Border.SetInsideBorder(XLBorderStyleValues.None); + + Assert.AreEqual(XLBorderStyleValues.Thin, ws.Cell("B2").Style.Border.LeftBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("B2").Style.Border.RightBorder); + Assert.AreEqual(XLBorderStyleValues.None, ws.Cell("C2").Style.Border.LeftBorder); + Assert.AreEqual(XLBorderStyleValues.Thin, ws.Cell("C2").Style.Border.RightBorder); + Assert.AreEqual(XLThemeColor.Accent1, ws.Cell("B2").Style.Border.LeftBorderColor.ThemeColor); + Assert.AreEqual(XLThemeColor.Accent1, ws.Cell("C2").Style.Border.RightBorderColor.ThemeColor); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/ColorTests.cs b/ClosedXML_Tests/Excel/Styles/ColorTests.cs new file mode 100644 index 0000000..d631194 --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/ColorTests.cs @@ -0,0 +1,177 @@ +using ClosedXML.Excel; +using ClosedXML.Utils; +using DocumentFormat.OpenXml.Spreadsheet; +using NUnit.Framework; +using Color = System.Drawing.Color; +using X14 = DocumentFormat.OpenXml.Office2010.Excel; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class ColorTests + { + [Test] + public void ColorEqualOperatorInPlace() + { + Assert.IsTrue(XLColor.Black == XLColor.Black); + } + + [Test] + public void ColorNotEqualOperatorInPlace() + { + Assert.IsFalse(XLColor.Black != XLColor.Black); + } + + [Test] + public void ColorNamedVsHTML() + { + Assert.IsTrue(XLColor.Black == XLColor.FromHtml("#000000")); + } + + [Test] + public void DefaultColorIndex64isTransparentWhite() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + XLColor color = ws.FirstCell().Style.Fill.BackgroundColor; + Assert.AreEqual(XLColorType.Indexed, color.ColorType); + Assert.AreEqual(64, color.Indexed); + Assert.AreEqual(Color.Transparent, color.Color); + } + + [Test] + public void CanConvertXLColorToColorType() + { + var xlColor1 = XLColor.Red; + var xlColor2 = XLColor.FromIndex(20); + var xlColor3 = XLColor.FromTheme(XLThemeColor.Accent1); + var xlColor4 = XLColor.FromTheme(XLThemeColor.Accent2, 0.4); + + var color1 = new ForegroundColor().FromClosedXMLColor(xlColor1); + var color2 = new ForegroundColor().FromClosedXMLColor(xlColor2); + var color3 = new BackgroundColor().FromClosedXMLColor(xlColor3); + var color4 = new BackgroundColor().FromClosedXMLColor(xlColor4); + + Assert.AreEqual("FFFF0000", color1.Rgb.Value); + Assert.IsNull(color1.Indexed); + Assert.IsNull(color1.Theme); + Assert.IsNull(color1.Tint); + + Assert.IsNull(color2.Rgb); + Assert.AreEqual(20, color2.Indexed.Value); + Assert.IsNull(color2.Theme); + Assert.IsNull(color2.Tint); + + Assert.IsNull(color3.Rgb); + Assert.IsNull(color3.Indexed); + Assert.AreEqual(4, color3.Theme.Value); + Assert.IsNull(color3.Tint); + + Assert.IsNull(color4.Rgb); + Assert.IsNull(color4.Indexed); + Assert.AreEqual(5, color4.Theme.Value); + Assert.AreEqual(0.4, color4.Tint.Value); + } + + [Test] + public void CanConvertXlColorToX14ColorType() + { + var xlColor1 = XLColor.Red; + var xlColor2 = XLColor.FromIndex(20); + var xlColor3 = XLColor.FromTheme(XLThemeColor.Accent1); + var xlColor4 = XLColor.FromTheme(XLThemeColor.Accent2, 0.4); + + var color1 = new X14.AxisColor().FromClosedXMLColor(xlColor1); + var color2 = new X14.BorderColor().FromClosedXMLColor(xlColor2); + var color3 = new X14.FillColor().FromClosedXMLColor(xlColor3); + var color4 = new X14.HighMarkerColor().FromClosedXMLColor(xlColor4); + + Assert.AreEqual("FFFF0000", color1.Rgb.Value); + Assert.IsNull(color1.Indexed); + Assert.IsNull(color1.Theme); + Assert.IsNull(color1.Tint); + + Assert.IsNull(color2.Rgb); + Assert.AreEqual(20, color2.Indexed.Value); + Assert.IsNull(color2.Theme); + Assert.IsNull(color2.Tint); + + Assert.IsNull(color3.Rgb); + Assert.IsNull(color3.Indexed); + Assert.AreEqual(4, color3.Theme.Value); + Assert.IsNull(color3.Tint); + + Assert.IsNull(color4.Rgb); + Assert.IsNull(color4.Indexed); + Assert.AreEqual(5, color4.Theme.Value); + Assert.AreEqual(0.4, color4.Tint.Value); + } + + [Test] + public void CanConvertColorTypeToXlColor() + { + var color1 = new ForegroundColor { Rgb = new DocumentFormat.OpenXml.HexBinaryValue("FFFF0000") }; + var color2 = new ForegroundColor { Indexed = new DocumentFormat.OpenXml.UInt32Value((uint)20) }; + var color3 = new BackgroundColor { Theme = new DocumentFormat.OpenXml.UInt32Value((uint)4) }; + var color4 = new BackgroundColor + { + Theme = new DocumentFormat.OpenXml.UInt32Value((uint)4), + Tint = new DocumentFormat.OpenXml.DoubleValue(0.4) + }; + + var xlColor1 = color1.ToClosedXMLColor(); + var xlColor2 = color2.ToClosedXMLColor(); + var xlColor3 = color3.ToClosedXMLColor(); + var xlColor4 = color4.ToClosedXMLColor(); + + Assert.AreEqual(XLColorType.Color, xlColor1.ColorType); + Assert.AreEqual(XLColor.Red.Color, xlColor1.Color); + + Assert.AreEqual(XLColorType.Indexed, xlColor2.ColorType); + Assert.AreEqual(20, xlColor2.Indexed); + + Assert.AreEqual(XLColorType.Theme, xlColor3.ColorType); + Assert.AreEqual(XLThemeColor.Accent1, xlColor3.ThemeColor); + Assert.AreEqual(0, xlColor3.ThemeTint, XLHelper.Epsilon); + + Assert.AreEqual(XLColorType.Theme, xlColor4.ColorType); + Assert.AreEqual(XLThemeColor.Accent1, xlColor4.ThemeColor); + Assert.AreEqual(0.4, xlColor4.ThemeTint, XLHelper.Epsilon); + } + + [Test] + public void CanConvertX14ColorTypeToXlColor() + { + var color1 = new X14.AxisColor { Rgb = new DocumentFormat.OpenXml.HexBinaryValue("FFFF0000") }; + var color2 = new X14.BorderColor { Indexed = new DocumentFormat.OpenXml.UInt32Value((uint)20) }; + var color3 = new X14.FillColor { Theme = new DocumentFormat.OpenXml.UInt32Value((uint)4) }; + var color4 = new X14.HighMarkerColor + { + Theme = new DocumentFormat.OpenXml.UInt32Value((uint)4), + Tint = new DocumentFormat.OpenXml.DoubleValue(0.4) + }; + + var xlColor1 = color1.ToClosedXMLColor(); + var xlColor2 = color2.ToClosedXMLColor(); + var xlColor3 = color3.ToClosedXMLColor(); + var xlColor4 = color4.ToClosedXMLColor(); + + Assert.AreEqual(XLColorType.Color, xlColor1.ColorType); + Assert.AreEqual(XLColor.Red.Color, xlColor1.Color); + + Assert.AreEqual(XLColorType.Indexed, xlColor2.ColorType); + Assert.AreEqual(20, xlColor2.Indexed); + + Assert.AreEqual(XLColorType.Theme, xlColor3.ColorType); + Assert.AreEqual(XLThemeColor.Accent1, xlColor3.ThemeColor); + Assert.AreEqual(0, xlColor3.ThemeTint, XLHelper.Epsilon); + + Assert.AreEqual(XLColorType.Theme, xlColor4.ColorType); + Assert.AreEqual(XLThemeColor.Accent1, xlColor4.ThemeColor); + Assert.AreEqual(0.4, xlColor4.ThemeTint, XLHelper.Epsilon); + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/FontTests.cs b/ClosedXML_Tests/Excel/Styles/FontTests.cs new file mode 100644 index 0000000..9a397d7 --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/FontTests.cs @@ -0,0 +1,30 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.Styles +{ + public class FontTests + { + [Test] + public void XLFontKey_GetHashCode_IsCaseInsensitive() + { + var fontKey1 = new XLFontKey { FontName = "Arial" }; + var fontKey2 = new XLFontKey { FontName = "Times New Roman" }; + var fontKey3 = new XLFontKey { FontName = "TIMES NEW ROMAN" }; + + Assert.AreNotEqual(fontKey1.GetHashCode(), fontKey2.GetHashCode()); + Assert.AreEqual(fontKey2.GetHashCode(), fontKey3.GetHashCode()); + } + + [Test] + public void XLFontKey_Equals_IsCaseInsensitive() + { + var fontKey1 = new XLFontKey { FontName = "Arial" }; + var fontKey2 = new XLFontKey { FontName = "Times New Roman" }; + var fontKey3 = new XLFontKey { FontName = "TIMES NEW ROMAN" }; + + Assert.IsFalse(fontKey1.Equals(fontKey2)); + Assert.IsTrue(fontKey2.Equals(fontKey3)); + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/NumberFormatTests.cs b/ClosedXML_Tests/Excel/Styles/NumberFormatTests.cs new file mode 100644 index 0000000..dfe4e93 --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/NumberFormatTests.cs @@ -0,0 +1,94 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Data; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + public class NumberFormatTests + { + [Test] + public void PreserveCellFormat() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var table = new DataTable(); + table.Columns.Add("Date", typeof(DateTime)); + + for (int i = 0; i < 10; i++) + { + table.Rows.Add(new DateTime(2017, 1, 1).AddMonths(i)); + } + + ws.Column(1).Style.NumberFormat.Format = "yy-MM-dd"; + ws.Cell("A1").InsertData(table); + Assert.AreEqual("yy-MM-dd", ws.Cell("A5").Style.DateFormat.Format); + + ws.Row(1).Style.NumberFormat.Format = "yy-MM-dd"; + ws.Cell("A1").InsertData(table.Rows, true); + Assert.AreEqual("yy-MM-dd", ws.Cell("E1").Style.DateFormat.Format); + } + } + + [Test] + public void TestExcelNumberFormats() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var c = ws.FirstCell() + .SetValue(41573.875) + .SetDataType(XLDataType.DateTime); + + c.Style.NumberFormat.SetFormat("m/d/yy\\ h:mm;@"); + + Assert.AreEqual("10/26/13 21:00", c.GetFormattedString()); + } + } + + [Test] + public void ReadAndWriteColumnNumberFormat() + { + using (var memoryStream = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var sourceColumn = ws.Column(1); + sourceColumn.Style.NumberFormat.Format = "0.000"; + wb.SaveAs(memoryStream); + } + + memoryStream.Position = 0; + + using (var wb = new XLWorkbook(memoryStream)) + { + var column = wb.Worksheets.Single().Column(1); + Assert.AreEqual("0.000", column.Style.NumberFormat.Format); + } + } + } + + [Test] + public void XLNumberFormatKey_GetHashCode_IsCaseSensitive() + { + var numberFormatKey1 = new XLNumberFormatKey { Format = "MM" }; + var numberFormatKey2 = new XLNumberFormatKey { Format = "mm" }; + + Assert.AreNotEqual(numberFormatKey1.GetHashCode(), numberFormatKey2.GetHashCode()); + } + + [Test] + public void XLNumberFormatKey_Equals_IsCaseSensitive() + { + var numberFormatKey1 = new XLNumberFormatKey { Format = "MM" }; + var numberFormatKey2 = new XLNumberFormatKey { Format = "mm" }; + + Assert.IsFalse(numberFormatKey1.Equals(numberFormatKey2)); + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/StyleChangeTests.cs b/ClosedXML_Tests/Excel/Styles/StyleChangeTests.cs new file mode 100644 index 0000000..fcfc50d --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/StyleChangeTests.cs @@ -0,0 +1,136 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ClosedXML_Tests.Excel.Styles +{ + [TestFixture] + public class StyleChangeTests + { + [Test] + public void ChangeFontColorDoesNotAffectOtherProperties() + { + using (var wb = new XLWorkbook()) + { + // Arrange + var ws = wb.AddWorksheet("Sheet1"); + var a1 = ws.Cell("A1"); + var a2 = ws.Cell("A2"); + var b1 = ws.Cell("B1"); + var b2 = ws.Cell("B2"); + + ws.Range("A1:B2").Value = "Test"; + + a1.Style.Fill.BackgroundColor = XLColor.Red; + a2.Style.Fill.BackgroundColor = XLColor.Green; + b1.Style.Fill.BackgroundColor = XLColor.Blue; + b2.Style.Fill.BackgroundColor = XLColor.Pink; + + a1.Style.Font.FontName = "Arial"; + a2.Style.Font.FontName = "Times New Roman"; + b1.Style.Font.FontName = "Calibri"; + b2.Style.Font.FontName = "Cambria"; + + // Act + ws.Range("A1:B2").Style.Font.FontColor = XLColor.PowderBlue; + + //Assert + Assert.AreEqual(XLColor.Red, ws.Cell("A1").Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Green, ws.Cell("A2").Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Blue, ws.Cell("B1").Style.Fill.BackgroundColor); + Assert.AreEqual(XLColor.Pink, ws.Cell("B2").Style.Fill.BackgroundColor); + + Assert.AreEqual("Arial", ws.Cell("A1").Style.Font.FontName); + Assert.AreEqual("Times New Roman", ws.Cell("A2").Style.Font.FontName); + Assert.AreEqual("Calibri", ws.Cell("B1").Style.Font.FontName); + Assert.AreEqual("Cambria", ws.Cell("B2").Style.Font.FontName); + + Assert.AreEqual(XLColor.PowderBlue, ws.Cell("A1").Style.Font.FontColor); + Assert.AreEqual(XLColor.PowderBlue, ws.Cell("A2").Style.Font.FontColor); + Assert.AreEqual(XLColor.PowderBlue, ws.Cell("B1").Style.Font.FontColor); + Assert.AreEqual(XLColor.PowderBlue, ws.Cell("B2").Style.Font.FontColor); + } + } + + + [Test] + public void ChangeDetachedStyleAlignment() + { + var style = XLStyle.Default; + + style.Alignment.Horizontal = XLAlignmentHorizontalValues.Justify; + + Assert.AreEqual(XLAlignmentHorizontalValues.Justify, style.Alignment.Horizontal); + } + + [Test] + public void ChangeDetachedStyleBorder() + { + var style = XLStyle.Default; + + style.Border.DiagonalBorder = XLBorderStyleValues.Double; + + Assert.AreEqual(XLBorderStyleValues.Double, style.Border.DiagonalBorder); + } + + [Test] + public void ChangeDetachedStyleFill() + { + var style = XLStyle.Default; + + style.Fill.BackgroundColor = XLColor.Red; + + Assert.AreEqual(XLColor.Red, style.Fill.BackgroundColor); + } + + + [Test] + public void ChangeDetachedStyleFont() + { + var style = XLStyle.Default; + + style.Font.FontSize = 50; + + Assert.AreEqual(50, style.Font.FontSize); + } + + + [Test] + public void ChangeDetachedStyleNumberFormat() + { + var style = XLStyle.Default; + + style.NumberFormat.Format = "YYYY"; + + Assert.AreEqual("YYYY", style.NumberFormat.Format); + } + + [Test] + public void ChangeDetachedStyleProtection() + { + var style = XLStyle.Default; + + style.Protection.Hidden = true; + + Assert.AreEqual(true, style.Protection.Hidden); + } + + [Test] + public void ChangeAttachedStyleAlignment() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var a1 = ws.Cell("A1"); + + a1.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Justify; + + Assert.AreEqual(XLAlignmentHorizontalValues.Justify, a1.Style.Alignment.Horizontal); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/StyleTests.cs b/ClosedXML_Tests/Excel/Styles/StyleTests.cs new file mode 100644 index 0000000..34e468c --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/StyleTests.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using ClosedXML.Excel; +using NUnit.Framework; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + [TestFixture] + public class StyleTests + { + [Test] + public void EmptyCellWithQuotePrefixNotTreatedAsEmpty() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Empty cell with quote prefix:"); + var cell = ws.FirstCell().CellRight() as XLCell; + + Assert.IsTrue(cell.IsEmpty()); + cell.Style.IncludeQuotePrefix = true; + + Assert.IsTrue(cell.IsEmpty()); + Assert.IsFalse(cell.IsEmpty(XLCellsUsedOptions.All)); + + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var cell = ws.FirstCell().CellRight() as XLCell; + Assert.AreEqual(1, cell.SharedStringId); + + Assert.IsTrue(cell.IsEmpty()); + Assert.IsFalse(cell.IsEmpty(XLCellsUsedOptions.All)); + } + } + } + + [TestCase("A1", TestName = "First cell")] + [TestCase("A2", TestName = "Cell from initialized row")] + [TestCase("B1", TestName = "Cell from initialized column")] + [TestCase("D4", TestName = "Initialized cell")] + [TestCase("F6", TestName = "Non-initialized cell")] + public void CellTakesWorksheetStyle(string cellAddress) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Column(2); + ws.Row(2); + ws.Cell("D4").Value = "Non empty"; + ws.Style.Font.SetFontName("Arial"); + ws.Style.Font.SetFontSize(9); + + var cell = ws.Cell(cellAddress); + Assert.AreEqual("Arial", cell.Style.Font.FontName); + Assert.AreEqual(9, cell.Style.Font.FontSize); + } + } + + [TestCaseSource(nameof(StylizedEntities))] + public void WorksheetStyleAffectsAllNestedEntities(Func getEntityStyle) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + + ws.Style.Font.FontSize = 8; + + var style = getEntityStyle(ws); + + Assert.AreEqual(8, style.Font.FontSize); + } + } + + private static IEnumerable StylizedEntities + { + get + { + var t = nameof(WorksheetStyleAffectsAllNestedEntities); + yield return new TestCaseData(new Func((ws) => ws.Style)).SetName(t + ": Worksheet"); + + yield return new TestCaseData(new Func((ws) => ws.Columns().Style)).SetName(t + ": Columns()"); + yield return new TestCaseData(new Func((ws) => ws.Columns(1, 3).Style)).SetName(t + ": Columns(1, 3)"); + yield return new TestCaseData(new Func((ws) => ws.Columns("B:F").Style)).SetName(t + ": Columns(\"B:F\")"); + yield return new TestCaseData(new Func((ws) => ws.Columns("B", "F").Style)).SetName(t + ": Columns(\"B\", \"F\")"); + yield return new TestCaseData(new Func((ws) => ws.Column(5).Style)).SetName(t + ": Column(5)"); + yield return new TestCaseData(new Func((ws) => ws.Column("D").Style)).SetName(t + ": Column(\"D\")"); + + yield return new TestCaseData(new Func((ws) => ws.Rows().Style)).SetName(t + ": Rows()"); + yield return new TestCaseData(new Func((ws) => ws.Rows(1, 3).Style)).SetName(t + ": Rows(1, 3)"); + yield return new TestCaseData(new Func((ws) => ws.Rows("1:3").Style)).SetName(t + ": Rows(\"1:3\")"); + yield return new TestCaseData(new Func((ws) => ws.Row(5).Style)).SetName(t + ": Row(5)"); + + yield return new TestCaseData(new Func((ws) => ws.Cells().Style)).SetName(t + ": Cells()"); + yield return new TestCaseData(new Func((ws) => ws.Cells("B2,D4").Style)).SetName(t + ": Cells(\"B2, D4\")"); + yield return new TestCaseData(new Func((ws) => ws.Cell("F6").Style)).SetName(t + ": Cell(\"F6\")"); + yield return new TestCaseData(new Func((ws) => ws.Cell(2, 3).Style)).SetName(t + ": Cell(2, 3)"); + + yield return new TestCaseData(new Func((ws) => ws.Ranges("F6:H9,I8:K10").Style)).SetName(t + ": Ranges(\"F6:H9,I8:K10\")"); + yield return new TestCaseData(new Func((ws) => ws.Range("G8:H10").Style)).SetName(t + ": Range(\"G8:H10\")"); + yield return new TestCaseData(new Func((ws) => ws.Range("G8:H10").Column(1).Style)).SetName(t + ": Range(\"G8:H10\").Column(1)"); + yield return new TestCaseData(new Func((ws) => ws.Range("G8:H10").Row(2).Style)).SetName(t + ": Range(\"G8:H10\").Row(2)"); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Styles/XLFillTests.cs b/ClosedXML_Tests/Excel/Styles/XLFillTests.cs new file mode 100644 index 0000000..dcd21c5 --- /dev/null +++ b/ClosedXML_Tests/Excel/Styles/XLFillTests.cs @@ -0,0 +1,119 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System.IO; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class XLFillTests + { + [Test] + public void BackgroundColorSetsPattern() + { + var fill = new XLFill { BackgroundColor = XLColor.Blue }; + Assert.AreEqual(XLFillPatternValues.Solid, fill.PatternType); + } + + [Test] + public void BackgroundNoColorSetsPatternNone() + { + var fill = new XLFill { BackgroundColor = XLColor.NoColor }; + Assert.AreEqual(XLFillPatternValues.None, fill.PatternType); + } + + [Test] + public void BackgroundPatternEqualCheck() + { + var fill1 = new XLFill { BackgroundColor = XLColor.Blue }; + var fill2 = new XLFill { BackgroundColor = XLColor.Blue }; + Assert.IsTrue(fill1.Equals(fill2)); + Assert.AreEqual(fill1.GetHashCode(), fill2.GetHashCode()); + } + + [Test] + public void BackgroundPatternNotEqualCheck() + { + var fill1 = new XLFill { PatternType = XLFillPatternValues.Solid, BackgroundColor = XLColor.Blue }; + var fill2 = new XLFill { PatternType = XLFillPatternValues.Solid, BackgroundColor = XLColor.Red }; + Assert.IsFalse(fill1.Equals(fill2)); + Assert.AreNotEqual(fill1.GetHashCode(), fill2.GetHashCode()); + } + + [Test] + public void FillsWithTransparentColorEqual() + { + var fill1 = new XLFill { BackgroundColor = XLColor.ElectricUltramarine, PatternType = XLFillPatternValues.None }; + var fill2 = new XLFill { BackgroundColor = XLColor.EtonBlue, PatternType = XLFillPatternValues.None }; + var fill3 = new XLFill { BackgroundColor = XLColor.FromIndex(64) }; + var fill4 = new XLFill { BackgroundColor = XLColor.NoColor }; + + Assert.IsTrue(fill1.Equals(fill2)); + Assert.IsTrue(fill1.Equals(fill3)); + Assert.IsTrue(fill1.Equals(fill4)); + Assert.AreEqual(fill1.GetHashCode(), fill2.GetHashCode()); + Assert.AreEqual(fill1.GetHashCode(), fill3.GetHashCode()); + Assert.AreEqual(fill1.GetHashCode(), fill4.GetHashCode()); + } + + [Test] + public void SolidFillsWithDifferentPatternColorEqual() + { + var fill1 = new XLFill + { + PatternType = XLFillPatternValues.Solid, + BackgroundColor = XLColor.Red, + PatternColor = XLColor.Blue + }; + + var fill2 = new XLFill { + PatternType = XLFillPatternValues.Solid, + BackgroundColor = XLColor.Red, + PatternColor = XLColor.Green + }; + + Assert.IsTrue(fill1.Equals(fill2)); + Assert.AreEqual(fill1.GetHashCode(), fill2.GetHashCode()); + } + + [Test] + public void BackgroundWithConditionalFormat() + { + var workbook = new XLWorkbook(); + var worksheet = workbook.AddWorksheet("Test"); + worksheet.Cell(2, 2).SetValue("Text"); + var cf = worksheet.Cell(2, 2).AddConditionalFormat(); + var style = cf.WhenNotBlank(); + style = style + .Border.SetOutsideBorder(XLBorderStyleValues.Thick) + .Border.SetOutsideBorderColor(XLColor.Blue); + + Assert.AreEqual(style.Border.BottomBorder, XLBorderStyleValues.Thick); + Assert.AreEqual(style.Border.TopBorder, XLBorderStyleValues.Thick); + Assert.AreEqual(style.Border.LeftBorder, XLBorderStyleValues.Thick); + Assert.AreEqual(style.Border.RightBorder, XLBorderStyleValues.Thick); + + Assert.AreEqual(style.Border.BottomBorderColor, XLColor.Blue); + Assert.AreEqual(style.Border.TopBorderColor, XLColor.Blue); + Assert.AreEqual(style.Border.LeftBorderColor, XLColor.Blue); + Assert.AreEqual(style.Border.RightBorderColor, XLColor.Blue); + } + + [Test] + public void LoadAndSaveTransparentBackgroundFill() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\StyleReferenceFiles\TransparentBackgroundFill\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"Other\StyleReferenceFiles\TransparentBackgroundFill\TransparentBackgroundFill.xlsx"); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Tables/AddingAndReplacingTableDataTests.cs b/ClosedXML_Tests/Excel/Tables/AddingAndReplacingTableDataTests.cs new file mode 100644 index 0000000..ccae4ad --- /dev/null +++ b/ClosedXML_Tests/Excel/Tables/AddingAndReplacingTableDataTests.cs @@ -0,0 +1,675 @@ +using ClosedXML.Attributes; +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Tables +{ + [TestFixture] + public class AppendingAndReplacingTableDataTests + { + public class TestObjectWithoutAttributes + { + public String Column1 { get; set; } + public String Column2 { get; set; } + } + + public class Person + { + public int Age { get; set; } + + [XLColumn(Header = "Last name", Order = 2)] + public String LastName { get; set; } + + [XLColumn(Header = "First name", Order = 1)] + public String FirstName { get; set; } + + [XLColumn(Header = "Full name", Order = 0)] + public String FullName { get => string.Concat(FirstName, " ", LastName); } + + [XLColumn(Order = 3)] + public DateTime DateOfBirth { get; set; } + + [XLColumn(Header = "Is active", Order = 4)] + public bool IsActive; + } + + private XLWorkbook PrepareWorkbook() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Tables"); + + var data = new[] + { + new Person{FirstName = "Francois", LastName = "Botha", Age = 39, DateOfBirth = new DateTime(1980,1,1), IsActive = true}, + new Person{FirstName = "Leon", LastName = "Oosthuizen", Age = 40, DateOfBirth = new DateTime(1979,1,1), IsActive = false}, + new Person{FirstName = "Rian", LastName = "Prinsloo", Age = 41, DateOfBirth = new DateTime(1978,1,1), IsActive = false} + }; + + ws.FirstCell().CellRight().CellBelow().InsertTable(data); + + ws.Columns().AdjustToContents(); + + return wb; + } + + private XLWorkbook PrepareWorkbookWithAdditionalColumns() + { + var wb = PrepareWorkbook(); + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + table.HeadersRow() + .LastCell().CellRight() + .InsertData(new[] { "CumulativeAge", "NameLength", "IsOld", "HardCodedValue" }, transpose: true); + + table.Resize(ws.Range(table.FirstCell(), table.LastCell().CellRight(4))); + + table.Field("CumulativeAge").DataCells.ForEach(c => c.FormulaA1 = $"SUM($G$3:G{c.WorksheetRow().RowNumber()})"); + table.Field("NameLength").DataCells.ForEach(c => c.FormulaA1 = $"LEN(B{c.WorksheetRow().RowNumber()})"); + table.Field("IsOld").DataCells.ForEach(c => c.FormulaA1 = $"=G{c.WorksheetRow().RowNumber()}>=40"); + table.Field("HardCodedValue").DataCells.Value = "40 is not old!"; + + return wb; + } + + private Person[] NewData + { + get + { + return new[] + { + new Person{FirstName = "Michelle", LastName = "de Beer", Age = 35, DateOfBirth = new DateTime(1983,1,1), IsActive = false}, + new Person{FirstName = "Marichen", LastName = "van der Gryp", Age = 30, DateOfBirth = new DateTime(1990,1,1), IsActive = true} + }; + } + } + + [Test] + public void AddingEmptyEnumerables() + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = null; + Assert.AreEqual(null, table.AppendData(personEnumerable)); + + personEnumerable = new Person[] { }; + Assert.AreEqual(null, table.AppendData(personEnumerable)); + + IEnumerable enumerable = null; + Assert.AreEqual(null, table.AppendData(enumerable)); + + enumerable = new Person[] { }; + Assert.AreEqual(null, table.AppendData(enumerable)); + } + } + + [Test] + public void ReplaceWithEmptyEnumerables() + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = null; + Assert.Throws(() => table.ReplaceData(personEnumerable)); + + personEnumerable = new Person[] { }; + Assert.Throws(() => table.ReplaceData(personEnumerable)); + + IEnumerable enumerable = null; + Assert.Throws(() => table.ReplaceData(enumerable)); + + enumerable = new Person[] { }; + Assert.Throws(() => table.ReplaceData(enumerable)); + } + } + + [Test] + public void CanAppendTypedEnumerable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = NewData; + var addedRange = table.AppendData(personEnumerable); + + Assert.AreEqual("B6:G7", addedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(5, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanAppendToTableWithTotalsRow() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + table.SetShowTotalsRow(true); + table.Fields.Last().TotalsRowFunction = XLTotalsRowFunction.Average; + + IEnumerable personEnumerable = NewData; + var addedRange = table.AppendData(personEnumerable); + + Assert.AreEqual("B6:G7", addedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(5, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanAppendTypedEnumerableAndPushDownCellsBelowTable() + { + using (var ms = new MemoryStream()) + { + var value = "Some value that will be overwritten"; + IXLAddress address; + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + var cell = table.LastRow().FirstCell().CellRight(2).CellBelow(1); + address = cell.Address; + cell.Value = value; + + IEnumerable personEnumerable = NewData; + var addedRange = table.AppendData(personEnumerable); + + Assert.AreEqual("B6:G7", addedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + var cell = ws.Cell(address); + Assert.AreEqual("de Beer", cell.Value); + Assert.AreEqual(5, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + + Assert.AreEqual(value, cell.CellBelow(NewData.Count()).Value); + } + } + } + + [Test] + public void CanAppendUntypedEnumerable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + var list = new ArrayList(); + list.AddRange(NewData); + + var addedRange = table.AppendData(list); + + Assert.AreEqual("B6:G7", addedRange.RangeAddress.ToString()); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(5, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanAppendDataTable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = NewData; + + var ws2 = wb.AddWorksheet("temp"); + var dataTable = ws2.FirstCell().InsertTable(personEnumerable).AsNativeDataTable(); + + var addedRange = table.AppendData(dataTable); + + Assert.AreEqual("B6:G7", addedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(5, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceWithTypedEnumerable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = NewData; + var replacedRange = table.ReplaceData(personEnumerable); + + Assert.AreEqual("B3:G4", replacedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(2, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceWithUntypedEnumerable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + var list = new ArrayList(); + list.AddRange(NewData); + + var replacedRange = table.ReplaceData(list); + + Assert.AreEqual("B3:G4", replacedRange.RangeAddress.ToString()); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(2, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceWithDataTable() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = NewData; + + var ws2 = wb.AddWorksheet("temp"); + var dataTable = ws2.FirstCell().InsertTable(personEnumerable).AsNativeDataTable(); + + var replacedRange = table.ReplaceData(dataTable); + + Assert.AreEqual("B3:G4", replacedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(2, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceToTableWithTablesRow1() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + table.SetShowTotalsRow(true); + table.Fields.Last().TotalsRowFunction = XLTotalsRowFunction.Average; + + // Will cause table to overflow + IEnumerable personEnumerable = NewData.Union(NewData).Union(NewData); + var replacedRange = table.ReplaceData(personEnumerable); + + Assert.AreEqual("B3:G8", replacedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(6, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceToTableWithTablesRow2() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbook()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + table.SetShowTotalsRow(true); + table.Fields.Last().TotalsRowFunction = XLTotalsRowFunction.Average; + + // Will cause table to shrink + IEnumerable personEnumerable = NewData.Take(1); + var replacedRange = table.ReplaceData(personEnumerable); + + Assert.AreEqual("B3:G3", replacedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(1, table.DataRange.RowCount()); + Assert.AreEqual(6, table.DataRange.ColumnCount()); + } + } + } + + [Test] + public void CanReplaceWithUntypedEnumerableAndPropagateExtraColumns() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbookWithAdditionalColumns()) + { + var ws = wb.Worksheets.First(); + var table = ws.Tables.First(); + + var list = new ArrayList(); + list.AddRange(NewData); + list.AddRange(NewData); + + var replacedRange = table.ReplaceData(list, propagateExtraColumns: true); + + Assert.AreEqual("B3:G6", replacedRange.RangeAddress.ToString()); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(4, table.DataRange.RowCount()); + Assert.AreEqual(10, table.DataRange.ColumnCount()); + + Assert.AreEqual("SUM($G$3:G5)", table.Worksheet.Cell("H5").FormulaA1); + Assert.AreEqual("SUM($G$3:G6)", table.Worksheet.Cell("H6").FormulaA1); + Assert.AreEqual(100, table.Worksheet.Cell("H5").Value); + Assert.AreEqual(130, table.Worksheet.Cell("H6").Value); + + Assert.AreEqual("LEN(B5)", table.Worksheet.Cell("I5").FormulaA1); + Assert.AreEqual("LEN(B6)", table.Worksheet.Cell("I6").FormulaA1); + Assert.AreEqual(16, table.Worksheet.Cell("I5").Value); + Assert.AreEqual(21, table.Worksheet.Cell("I6").Value); + + Assert.AreEqual("G5>=40", table.Worksheet.Cell("J5").FormulaA1); + Assert.AreEqual("G6>=40", table.Worksheet.Cell("J6").FormulaA1); + Assert.AreEqual(false, table.Worksheet.Cell("J5").Value); + Assert.AreEqual(false, table.Worksheet.Cell("J6").Value); + + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K5").Value); + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K6").Value); + } + } + } + + [Test] + public void CanReplaceWithTypedEnumerableAndPropagateExtraColumns() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbookWithAdditionalColumns()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = NewData.Concat(NewData).OrderBy(p => p.Age); + var replacedRange = table.ReplaceData(personEnumerable, propagateExtraColumns: true); + + Assert.AreEqual("B3:G6", replacedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(4, table.DataRange.RowCount()); + Assert.AreEqual(10, table.DataRange.ColumnCount()); + + Assert.AreEqual("SUM($G$3:G5)", table.Worksheet.Cell("H5").FormulaA1); + Assert.AreEqual("SUM($G$3:G6)", table.Worksheet.Cell("H6").FormulaA1); + Assert.AreEqual(95, table.Worksheet.Cell("H5").Value); + Assert.AreEqual(130, table.Worksheet.Cell("H6").Value); + + Assert.AreEqual("LEN(B5)", table.Worksheet.Cell("I5").FormulaA1); + Assert.AreEqual("LEN(B6)", table.Worksheet.Cell("I6").FormulaA1); + Assert.AreEqual(16, table.Worksheet.Cell("I5").Value); + Assert.AreEqual(16, table.Worksheet.Cell("I6").Value); + + Assert.AreEqual("G5>=40", table.Worksheet.Cell("J5").FormulaA1); + Assert.AreEqual("G6>=40", table.Worksheet.Cell("J6").FormulaA1); + Assert.AreEqual(false, table.Worksheet.Cell("J5").Value); + Assert.AreEqual(false, table.Worksheet.Cell("J6").Value); + + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K5").Value); + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K6").Value); + } + } + } + + [Test] + public void CanAppendWithUntypedEnumerableAndPropagateExtraColumns() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbookWithAdditionalColumns()) + { + var ws = wb.Worksheets.First(); + var table = ws.Tables.First(); + + var list = new ArrayList(); + list.AddRange(NewData); + list.AddRange(NewData); + + var appendedRange = table.AppendData(list, propagateExtraColumns: true); + + Assert.AreEqual("B6:G9", appendedRange.RangeAddress.ToString()); + + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(7, table.DataRange.RowCount()); + Assert.AreEqual(10, table.DataRange.ColumnCount()); + + Assert.AreEqual("SUM($G$3:G8)", table.Worksheet.Cell("H8").FormulaA1); + Assert.AreEqual("SUM($G$3:G9)", table.Worksheet.Cell("H9").FormulaA1); + Assert.AreEqual(220, table.Worksheet.Cell("H8").Value); + Assert.AreEqual(250, table.Worksheet.Cell("H9").Value); + + Assert.AreEqual("LEN(B8)", table.Worksheet.Cell("I8").FormulaA1); + Assert.AreEqual("LEN(B9)", table.Worksheet.Cell("I9").FormulaA1); + Assert.AreEqual(16, table.Worksheet.Cell("I8").Value); + Assert.AreEqual(21, table.Worksheet.Cell("I9").Value); + + Assert.AreEqual("G8>=40", table.Worksheet.Cell("J8").FormulaA1); + Assert.AreEqual("G9>=40", table.Worksheet.Cell("J9").FormulaA1); + Assert.AreEqual(false, table.Worksheet.Cell("J8").Value); + Assert.AreEqual(false, table.Worksheet.Cell("J9").Value); + + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K8").Value); + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K9").Value); + } + } + } + + [Test] + public void CanAppendTypedEnumerableAndPropagateExtraColumns() + { + using (var ms = new MemoryStream()) + { + using (var wb = PrepareWorkbookWithAdditionalColumns()) + { + var ws = wb.Worksheets.First(); + + var table = ws.Tables.First(); + + IEnumerable personEnumerable = + NewData + .Concat(NewData) + .Concat(NewData) + .OrderBy(p => p.FirstName); + + var addedRange = table.AppendData(personEnumerable); + + Assert.AreEqual("B6:G11", addedRange.RangeAddress.ToString()); + ws.Columns().AdjustToContents(); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var table = wb.Worksheets.SelectMany(ws => ws.Tables).First(); + + Assert.AreEqual(9, table.DataRange.RowCount()); + Assert.AreEqual(10, table.DataRange.ColumnCount()); + + Assert.AreEqual("SUM($G$3:G10)", table.Worksheet.Cell("H10").FormulaA1); + Assert.AreEqual("SUM($G$3:G11)", table.Worksheet.Cell("H11").FormulaA1); + Assert.AreEqual(280, table.Worksheet.Cell("H10").Value); + Assert.AreEqual(315, table.Worksheet.Cell("H11").Value); + + Assert.AreEqual("LEN(B10)", table.Worksheet.Cell("I10").FormulaA1); + Assert.AreEqual("LEN(B11)", table.Worksheet.Cell("I11").FormulaA1); + Assert.AreEqual(16, table.Worksheet.Cell("I10").Value); + Assert.AreEqual(16, table.Worksheet.Cell("I11").Value); + + Assert.AreEqual("G10>=40", table.Worksheet.Cell("J10").FormulaA1); + Assert.AreEqual("G11>=40", table.Worksheet.Cell("J11").FormulaA1); + Assert.AreEqual(false, table.Worksheet.Cell("J10").Value); + Assert.AreEqual(false, table.Worksheet.Cell("J11").Value); + + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K10").Value); + Assert.AreEqual("40 is not old!", table.Worksheet.Cell("K11").Value); + } + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Tables/TablesTests.cs b/ClosedXML_Tests/Excel/Tables/TablesTests.cs new file mode 100644 index 0000000..d35e118 --- /dev/null +++ b/ClosedXML_Tests/Excel/Tables/TablesTests.cs @@ -0,0 +1,1178 @@ +using ClosedXML.Attributes; +using ClosedXML.Excel; +using ClosedXML.Excel.Exceptions; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel +{ + /// + /// Summary description for UnitTest1 + /// + [TestFixture] + public class TablesTests + { + public class TestObjectWithoutAttributes + { + public String Column1 { get; set; } + public String Column2 { get; set; } + } + + public class TestObjectWithAttributes + { + public int UnOrderedColumn { get; set; } + + [XLColumn(Header = "SecondColumn", Order = 1)] + public String Column1 { get; set; } + + [XLColumn(Header = "FirstColumn", Order = 0)] + public String Column2 { get; set; } + + [XLColumn(Header = "SomeFieldNotProperty", Order = 2)] + public int MyField; + } + + [Test] + public void CanSaveTableCreatedFromEmptyDataTable() + { + var dt = new DataTable("sheet1"); + dt.Columns.Add("col1", typeof(string)); + dt.Columns.Add("col2", typeof(double)); + + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet(dt); + + using (var ms = new MemoryStream()) + wb.SaveAs(ms, true); + } + } + + [Test] + public void PreventAddingOfEmptyDataTable() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var dt = new DataTable(); + var table = ws.FirstCell().InsertTable(dt); + + Assert.AreEqual(null, table); + } + } + + [Test] + public void CanSaveTableCreatedFromSingleRow() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Title"); + ws.Range("A1").CreateTable(); + + using (var ms = new MemoryStream()) + wb.SaveAs(ms, true); + } + } + + [Test] + public void CreatingATableFromHeadersPushCellsBelow() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Title") + .CellBelow().SetValue("X"); + ws.Range("A1").CreateTable(); + + Assert.AreEqual(String.Empty, ws.Cell("A2").GetString()); + Assert.AreEqual("X", ws.Cell("A3").GetString()); + } + } + + [Test] + public void Inserting_Column_Sets_Header() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("A") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C"); + + IXLTable table = ws.RangeUsed().CreateTable(); + table.InsertColumnsAfter(1); + Assert.AreEqual("Column2", table.HeadersRow().LastCell().GetString()); + } + } + + [Test] + public void DataRange_returns_null_if_empty() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("A") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C"); + + IXLTable table = ws.RangeUsed().CreateTable(); + + ws.Rows("2:4").Delete(); + + Assert.IsNull(table.DataRange); + } + } + + [Test] + public void SavingLoadingTableWithNewLineInHeader() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + string columnName = "Line1" + Environment.NewLine + "Line2"; + ws.FirstCell().SetValue(columnName) + .CellBelow().SetValue("A"); + ws.RangeUsed().CreateTable(); + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + var wb2 = new XLWorkbook(ms); + IXLWorksheet ws2 = wb2.Worksheet(1); + IXLTable table2 = ws2.Table(0); + string fieldName = table2.Field(0).Name; + Assert.AreEqual("Line1\nLine2", fieldName); + } + } + } + + [Test] + public void SavingLoadingTableWithNewLineInHeader2() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.Worksheets.Add("Test"); + + var dt = new DataTable(); + string columnName = "Line1" + Environment.NewLine + "Line2"; + dt.Columns.Add(columnName); + + DataRow dr = dt.NewRow(); + dr[columnName] = "some text"; + dt.Rows.Add(dr); + ws.Cell(1, 1).InsertTable(dt); + + IXLTable table1 = ws.Table(0); + string fieldName1 = table1.Field(0).Name; + Assert.AreEqual(columnName, fieldName1); + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, true); + var wb2 = new XLWorkbook(ms); + IXLWorksheet ws2 = wb2.Worksheet(1); + IXLTable table2 = ws2.Table(0); + string fieldName2 = table2.Field(0).Name; + Assert.AreEqual("Line1\nLine2", fieldName2); + } + } + } + + [Test] + public void TableCreatedFromEmptyDataTable() + { + var dt = new DataTable("sheet1"); + dt.Columns.Add("col1", typeof(string)); + dt.Columns.Add("col2", typeof(double)); + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(dt); + Assert.AreEqual(2, ws.Tables.First().ColumnCount()); + } + } + + [Test] + public void TableCreatedFromEmptyListOfInt() + { + var l = new List(); + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l); + Assert.AreEqual(1, ws.Tables.First().ColumnCount()); + } + } + + [Test] + public void TableCreatedFromEmptyListOfObject() + { + var l = new List(); + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l); + Assert.AreEqual(2, ws.Tables.First().ColumnCount()); + } + } + + [Test] + public void TableCreatedFromListOfObjectWithPropertyAttributes() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l); + Assert.AreEqual(4, ws.Tables.First().ColumnCount()); + Assert.AreEqual("FirstColumn", ws.FirstCell().Value); + Assert.AreEqual("SecondColumn", ws.FirstCell().CellRight().Value); + Assert.AreEqual("SomeFieldNotProperty", ws.FirstCell().CellRight().CellRight().Value); + Assert.AreEqual("UnOrderedColumn", ws.FirstCell().CellRight().CellRight().CellRight().Value); + } + } + + [Test] + public void TableInsertAboveFromData() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Value"); + + IXLTable table = ws.Range("A1:A2").CreateTable(); + table.SetShowTotalsRow() + .Field(0).TotalsRowFunction = XLTotalsRowFunction.Sum; + + IXLTableRow row = table.DataRange.FirstRow(); + row.Field("Value").Value = 3; + row = table.DataRange.InsertRowsAbove(1).First(); + row.Field("Value").Value = 2; + row = table.DataRange.InsertRowsAbove(1).First(); + row.Field("Value").Value = 1; + + Assert.AreEqual(1, ws.Cell(2, 1).GetDouble()); + Assert.AreEqual(2, ws.Cell(3, 1).GetDouble()); + Assert.AreEqual(3, ws.Cell(4, 1).GetDouble()); + } + } + + [Test] + public void TableInsertAboveFromRows() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Value"); + + IXLTable table = ws.Range("A1:A2").CreateTable(); + table.SetShowTotalsRow() + .Field(0).TotalsRowFunction = XLTotalsRowFunction.Sum; + + IXLTableRow row = table.DataRange.FirstRow(); + row.Field("Value").Value = 3; + row = row.InsertRowsAbove(1).First(); + row.Field("Value").Value = 2; + row = row.InsertRowsAbove(1).First(); + row.Field("Value").Value = 1; + + Assert.AreEqual(1, ws.Cell(2, 1).GetDouble()); + Assert.AreEqual(2, ws.Cell(3, 1).GetDouble()); + Assert.AreEqual(3, ws.Cell(4, 1).GetDouble()); + } + } + + [Test] + public void TableInsertBelowFromData() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Value"); + + IXLTable table = ws.Range("A1:A2").CreateTable(); + table.SetShowTotalsRow() + .Field(0).TotalsRowFunction = XLTotalsRowFunction.Sum; + + IXLTableRow row = table.DataRange.FirstRow(); + row.Field("Value").Value = 1; + row = table.DataRange.InsertRowsBelow(1).First(); + row.Field("Value").Value = 2; + row = table.DataRange.InsertRowsBelow(1).First(); + row.Field("Value").Value = 3; + + Assert.AreEqual(1, ws.Cell(2, 1).GetDouble()); + Assert.AreEqual(2, ws.Cell(3, 1).GetDouble()); + Assert.AreEqual(3, ws.Cell(4, 1).GetDouble()); + } + } + + [Test] + public void TableInsertBelowFromRows() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Value"); + + IXLTable table = ws.Range("A1:A2").CreateTable(); + table.SetShowTotalsRow() + .Field(0).TotalsRowFunction = XLTotalsRowFunction.Sum; + + IXLTableRow row = table.DataRange.FirstRow(); + row.Field("Value").Value = 1; + row = row.InsertRowsBelow(1).First(); + row.Field("Value").Value = 2; + row = row.InsertRowsBelow(1).First(); + row.Field("Value").Value = 3; + + Assert.AreEqual(1, ws.Cell(2, 1).GetDouble()); + Assert.AreEqual(2, ws.Cell(3, 1).GetDouble()); + Assert.AreEqual(3, ws.Cell(4, 1).GetDouble()); + } + } + + [Test] + public void TableShowHeader() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().SetValue("Categories") + .CellBelow().SetValue("A") + .CellBelow().SetValue("B") + .CellBelow().SetValue("C"); + + IXLTable table = ws.RangeUsed().CreateTable(); + + Assert.AreEqual("Categories", table.Fields.First().Name); + + table.SetShowHeaderRow(false); + + Assert.AreEqual("Categories", table.Fields.First().Name); + + Assert.IsTrue(ws.Cell(1, 1).IsEmpty(XLCellsUsedOptions.All)); + Assert.AreEqual(null, table.HeadersRow()); + Assert.AreEqual("A", table.DataRange.FirstRow().Field("Categories").GetString()); + Assert.AreEqual("C", table.DataRange.LastRow().Field("Categories").GetString()); + Assert.AreEqual("A", table.DataRange.FirstCell().GetString()); + Assert.AreEqual("C", table.DataRange.LastCell().GetString()); + + table.SetShowHeaderRow(); + IXLRangeRow headerRow = table.HeadersRow(); + Assert.AreNotEqual(null, headerRow); + Assert.AreEqual("Categories", headerRow.Cell(1).GetString()); + + table.SetShowHeaderRow(false); + + ws.FirstCell().SetValue("x"); + + table.SetShowHeaderRow(); + + Assert.AreEqual("x", ws.FirstCell().GetString()); + Assert.AreEqual("Categories", ws.Cell("A2").GetString()); + Assert.AreNotEqual(null, headerRow); + Assert.AreEqual("A", table.DataRange.FirstRow().Field("Categories").GetString()); + Assert.AreEqual("C", table.DataRange.LastRow().Field("Categories").GetString()); + Assert.AreEqual("A", table.DataRange.FirstCell().GetString()); + Assert.AreEqual("C", table.DataRange.LastCell().GetString()); + } + } + + [Test] + public void ChangeFieldName() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet"); + ws.Cell("A1").SetValue("FName") + .CellBelow().SetValue("John"); + + ws.Cell("B1").SetValue("LName") + .CellBelow().SetValue("Doe"); + + var tbl = ws.RangeUsed().CreateTable(); + var nameBefore = tbl.Field(tbl.Fields.Last().Index).Name; + tbl.Field(tbl.Fields.Last().Index).Name = "LastName"; + var nameAfter = tbl.Field(tbl.Fields.Last().Index).Name; + + var cellValue = ws.Cell("B1").GetString(); + + Assert.AreEqual("LName", nameBefore); + Assert.AreEqual("LastName", nameAfter); + Assert.AreEqual("LastName", cellValue); + + tbl.ShowHeaderRow = false; + tbl.Field(tbl.Fields.Last().Index).Name = "LastNameChanged"; + nameAfter = tbl.Field(tbl.Fields.Last().Index).Name; + Assert.AreEqual("LastNameChanged", nameAfter); + + tbl.SetShowHeaderRow(true); + nameAfter = tbl.Cell("B1").Value.ToString(); + Assert.AreEqual("LastNameChanged", nameAfter); + + var field = tbl.Field("LastNameChanged"); + Assert.AreEqual("LastNameChanged", field.Name); + + tbl.Cell(1, 1).Value = "FirstName"; + Assert.AreEqual("FirstName", tbl.Field(0).Name); + } + } + + [Test] + public void CanDeleteTableColumn() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var table = ws.FirstCell().InsertTable(l); + + table.Column("C").Delete(); + + Assert.AreEqual(3, table.Fields.Count()); + + Assert.AreEqual("FirstColumn", table.Fields.First().Name); + Assert.AreEqual(0, table.Fields.First().Index); + + Assert.AreEqual("UnOrderedColumn", table.Fields.Last().Name); + Assert.AreEqual(2, table.Fields.Last().Index); + } + } + + [Test] + public void TestFieldCellTypes() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var table = ws.Cell("B2").InsertTable(l); + + Assert.AreEqual(4, table.Fields.Count()); + + Assert.AreEqual("B2", table.Field(0).HeaderCell.Address.ToString()); + Assert.AreEqual("C2", table.Field(1).HeaderCell.Address.ToString()); + Assert.AreEqual("D2", table.Field(2).HeaderCell.Address.ToString()); + Assert.AreEqual("E2", table.Field(3).HeaderCell.Address.ToString()); + + Assert.IsNull(table.Field(0).TotalsCell); + Assert.IsNull(table.Field(1).TotalsCell); + Assert.IsNull(table.Field(2).TotalsCell); + Assert.IsNull(table.Field(3).TotalsCell); + + table.SetShowTotalsRow(); + + Assert.AreEqual("B5", table.Field(0).TotalsCell.Address.ToString()); + Assert.AreEqual("C5", table.Field(1).TotalsCell.Address.ToString()); + Assert.AreEqual("D5", table.Field(2).TotalsCell.Address.ToString()); + Assert.AreEqual("E5", table.Field(3).TotalsCell.Address.ToString()); + + var field = table.Fields.Last(); + + Assert.AreEqual("E2:E5", field.Column.RangeAddress.ToString()); + Assert.AreEqual("E3", field.DataCells.First().Address.ToString()); + Assert.AreEqual("E4", field.DataCells.Last().Address.ToString()); + } + } + + [Test] + public void CanDeleteTable() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l); + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + var table = ws.Tables.First(); + + ws.Tables.Remove(table.Name); + Assert.AreEqual(0, ws.Tables.Count()); + wb.Save(); + } + } + } + + [Test] + public void TableNameCannotBeValidCellName() + { + var dt = new DataTable("sheet1"); + dt.Columns.Add("Patient", typeof(string)); + dt.Rows.Add("David"); + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "May2019")); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "A1")); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "R1C2")); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "r3c2")); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "R2C33333")); + Assert.Throws(() => ws.Cell(1, 1).InsertTable(dt, "RC")); + } + } + + [Test] + public void CanDeleteTableField() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var table = ws.Cell("B2").InsertTable(l); + + Assert.AreEqual("B2:E4", table.RangeAddress.ToString()); + + table.Field("SomeFieldNotProperty").Delete(); + + Assert.AreEqual(3, table.Fields.Count()); + + Assert.AreEqual("FirstColumn", table.Fields.First().Name); + Assert.AreEqual(0, table.Fields.First().Index); + + Assert.AreEqual("UnOrderedColumn", table.Fields.Last().Name); + Assert.AreEqual(2, table.Fields.Last().Index); + + Assert.AreEqual("B2:D4", table.RangeAddress.ToString()); + } + } + + [Test] + public void CanDeleteTableRows() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 }, + new TestObjectWithAttributes() { Column1 = "e", Column2 = "f", MyField = 6, UnOrderedColumn = 555 }, + new TestObjectWithAttributes() { Column1 = "g", Column2 = "h", MyField = 7, UnOrderedColumn = 333 } + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var table = ws.Cell("B2").InsertTable(l); + + Assert.AreEqual("B2:E6", table.RangeAddress.ToString()); + + table.DataRange.Rows(3, 4).Delete(); + + Assert.AreEqual(2, table.DataRange.Rows().Count()); + + Assert.AreEqual("b", table.DataRange.FirstCell().Value); + Assert.AreEqual(777, table.DataRange.LastCell().Value); + + Assert.AreEqual("B2:E4", table.RangeAddress.ToString()); + } + } + + [Test] + public void OverlappingTablesThrowsException() + { + var dt = new DataTable("sheet1"); + dt.Columns.Add("col1", typeof(string)); + dt.Columns.Add("col2", typeof(double)); + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(dt, true); + Assert.Throws(() => ws.FirstCell().CellRight().InsertTable(dt, true)); + } + } + + [Test] + public void OverwritingTableTotalsRow() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var data1 = Enumerable.Range(1, 10) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('a', i) + }); + + var table = ws.FirstCell().InsertTable(data1, true) + .SetShowHeaderRow() + .SetShowTotalsRow(); + table.Fields.First().TotalsRowFunction = XLTotalsRowFunction.Sum; + + var data2 = Enumerable.Range(1, 20) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('b', i), + Int = 64 + i + }); + + ws.FirstCell().CellBelow().InsertData(data2); + + table.Fields.ForEach(f => Assert.AreEqual(XLTotalsRowFunction.None, f.TotalsRowFunction)); + + Assert.AreEqual("11", table.Field(0).TotalsRowLabel); + Assert.AreEqual("K", table.Field(1).TotalsRowLabel); + Assert.AreEqual("bbbbbbbbbbb", table.Field(2).TotalsRowLabel); + } + } + + [Test] + public void TableRenameTests() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + var table1 = ws.FirstCell().InsertTable(l); + var table2 = ws.Cell("A10").InsertTable(l); + + Assert.AreEqual("Table1", table1.Name); + Assert.AreEqual("Table2", table2.Name); + + table1.Name = "table1"; + Assert.AreEqual("table1", table1.Name); + + table1.Name = "_table1"; + Assert.AreEqual("_table1", table1.Name); + + table1.Name = "\\table1"; + Assert.AreEqual("\\table1", table1.Name); + + Assert.Throws(() => table1.Name = ""); + Assert.Throws(() => table1.Name = "R"); + Assert.Throws(() => table1.Name = "C"); + Assert.Throws(() => table1.Name = "r"); + Assert.Throws(() => table1.Name = "c"); + + Assert.Throws(() => table1.Name = "123"); + Assert.Throws(() => table1.Name = new String('A', 256)); + + Assert.Throws(() => table1.Name = "Table2"); + Assert.Throws(() => table1.Name = "TABLE2"); + } + } + + [Test] + public void CanResizeTable() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var data1 = Enumerable.Range(1, 10) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('a', i) + }); + + var table = ws.FirstCell().InsertTable(data1, true) + .SetShowHeaderRow() + .SetShowTotalsRow(); + table.Fields.First().TotalsRowFunction = XLTotalsRowFunction.Sum; + + var data2 = Enumerable.Range(1, 10) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('b', i), + Integer = 64 + i + }); + + ws.FirstCell().CellBelow().InsertData(data2); + table.Resize(table.FirstCell().Address, table.AsRange().LastCell().CellRight().Address); + + Assert.AreEqual(4, table.Fields.Count()); + + Assert.AreEqual("Column4", table.Field(3).Name); + + ws.Cell("D1").Value = "Integer"; + Assert.AreEqual("Integer", table.Field(3).Name); + } + } + + [Test] + public void TableAsDynamicEnumerable() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + var table = ws.FirstCell().InsertTable(l); + + foreach (var d in table.AsDynamicEnumerable()) + { + Assert.DoesNotThrow(() => + { + object value; + value = d.FirstColumn; + value = d.SecondColumn; + value = d.UnOrderedColumn; + value = d.SomeFieldNotProperty; + }); + } + } + } + + [Test] + public void TableAsDotNetDataTable() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + var table = ws.FirstCell().InsertTable(l).AsNativeDataTable(); + + Assert.AreEqual(4, table.Columns.Count); + Assert.AreEqual("FirstColumn", table.Columns[0].ColumnName); + Assert.AreEqual("SecondColumn", table.Columns[1].ColumnName); + Assert.AreEqual("SomeFieldNotProperty", table.Columns[2].ColumnName); + Assert.AreEqual("UnOrderedColumn", table.Columns[3].ColumnName); + + Assert.AreEqual(typeof(String), table.Columns[0].DataType); + Assert.AreEqual(typeof(String), table.Columns[1].DataType); + Assert.AreEqual(typeof(Double), table.Columns[2].DataType); + Assert.AreEqual(typeof(Double), table.Columns[3].DataType); + + var dr = table.Rows[0]; + Assert.AreEqual("b", dr["FirstColumn"]); + Assert.AreEqual("a", dr["SecondColumn"]); + Assert.AreEqual(4, dr["SomeFieldNotProperty"]); + Assert.AreEqual(999, dr["UnOrderedColumn"]); + + dr = table.Rows[1]; + Assert.AreEqual("d", dr["FirstColumn"]); + Assert.AreEqual("c", dr["SecondColumn"]); + Assert.AreEqual(5, dr["SomeFieldNotProperty"]); + Assert.AreEqual(777, dr["UnOrderedColumn"]); + } + } + + [Test] + public void TestTableCellTypes() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var data1 = Enumerable.Range(1, 10) + .Select(i => + new + { + Index = i, + Character = Convert.ToChar(64 + i), + String = new String('a', i) + }); + + var table = ws.FirstCell().InsertTable(data1, true) + .SetShowHeaderRow() + .SetShowTotalsRow(); + table.Fields.First().TotalsRowFunction = XLTotalsRowFunction.Sum; + + Assert.AreEqual(XLTableCellType.Header, table.HeadersRow().Cell(1).TableCellType()); + Assert.AreEqual(XLTableCellType.Data, table.HeadersRow().Cell(1).CellBelow().TableCellType()); + Assert.AreEqual(XLTableCellType.Total, table.TotalsRow().Cell(1).TableCellType()); + Assert.AreEqual(XLTableCellType.None, ws.Cell("Z100").TableCellType()); + } + } + + [Test] + public void TotalsFunctionsOfHeadersWithWeirdCharacters() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l, false); + + // Give the headings weird names (i.e. spaces, hashes, single quotes + ws.Cell("A1").Value = "ABCD "; + ws.Cell("B1").Value = " #BCD"; + ws.Cell("C1").Value = " as'df "; + ws.Cell("D1").Value = "Normal"; + + var table = ws.RangeUsed().CreateTable(); + Assert.IsNotNull(table); + + table.ShowTotalsRow = true; + table.Field(0).TotalsRowFunction = XLTotalsRowFunction.Count; + table.Field(1).TotalsRowFunction = XLTotalsRowFunction.Count; + table.Field(2).TotalsRowFunction = XLTotalsRowFunction.Sum; + table.Field(3).TotalsRowFunction = XLTotalsRowFunction.Sum; + + Assert.AreEqual("SUBTOTAL(103,Table1[[ABCD ]])", table.Field(0).TotalsRowFormulaA1); + Assert.AreEqual("SUBTOTAL(103,Table1[[ '#BCD]])", table.Field(1).TotalsRowFormulaA1); + Assert.AreEqual("SUBTOTAL(109,Table1[[ as''df ]])", table.Field(2).TotalsRowFormulaA1); + Assert.AreEqual("SUBTOTAL(109,[Normal])", table.Field(3).TotalsRowFormulaA1); + } + } + + [Test] + public void CannotCreateDuplicateTablesOverSameRange() + { + var l = new List() + { + new TestObjectWithAttributes() { Column1 = "a", Column2 = "b", MyField = 4, UnOrderedColumn = 999 }, + new TestObjectWithAttributes() { Column1 = "c", Column2 = "d", MyField = 5, UnOrderedColumn = 777 } + }; + + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.FirstCell().InsertTable(l); + Assert.Throws(() => ws.RangeUsed().CreateTable()); + } + } + + [Test] + public void CannotCreateTableOverExistingAutoFilter() + { + using var wb = new XLWorkbook(); + + var data = Enumerable.Range(1, 10).Select(i => new + { + Index = i, + String = $"String {i}" + }); + + var ws = wb.AddWorksheet(); + ws.FirstCell().InsertTable(data, createTable: false); + ws.RangeUsed().SetAutoFilter().Column(1).AddFilter(5); + + Assert.Throws(() => ws.RangeUsed().CreateTable()); + } + + [Test] + public void CopyTableSameWorksheet() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + + var table = ws1.Range("A1:C2").AsTable(); + + TestDelegate action = () => table.CopyTo(ws1); + + Assert.Throws(typeof(InvalidOperationException), action); + } + + [Test] + public void CopyDetachedTableDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Cell("A1").Value = "Custom column 1"; + ws1.Cell("B1").Value = "Custom column 2"; + ws1.Cell("C1").Value = "Custom column 3"; + ws1.Cell("A2").Value = "Value 1"; + ws1.Cell("B2").Value = 123.45; + ws1.Cell("C2").Value = new DateTime(2018, 5, 10); + var original = ws1.Range("A1:C2").AsTable("Detached table"); + var ws2 = wb.Worksheets.Add("Sheet2"); + + var copy = original.CopyTo(ws2); + + Assert.AreEqual(0, ws1.Tables.Count()); // We did not add it + Assert.AreEqual(1, ws2.Tables.Count()); + + AssertTablesAreEqual(original, copy); + + Assert.AreEqual("Sheet2!A1:C2", copy.RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Custom column 1", ws2.Cell("A1").Value); + Assert.AreEqual("Custom column 2", ws2.Cell("B1").Value); + Assert.AreEqual("Custom column 3", ws2.Cell("C1").Value); + Assert.AreEqual("Value 1", ws2.Cell("A2").Value); + Assert.AreEqual(123.45, (double)ws2.Cell("B2").Value, XLHelper.Epsilon); + Assert.AreEqual(new DateTime(2018, 5, 10), ws2.Cell("C2").Value); + } + + [Test] + public void CopyTableDifferentWorksheets() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Cell("A1").Value = "Custom column 1"; + ws1.Cell("B1").Value = "Custom column 2"; + ws1.Cell("C1").Value = "Custom column 3"; + ws1.Cell("A2").Value = "Value 1"; + ws1.Cell("B2").Value = 123.45; + ws1.Cell("C2").Value = new DateTime(2018, 5, 10); + var original = ws1.Range("A1:C2").AsTable("Attached table"); + ws1.Tables.Add(original); + var ws2 = wb.Worksheets.Add("Sheet2"); + + original.CopyTo(ws2); + + Assert.AreEqual(1, ws1.Tables.Count()); + Assert.AreEqual(1, ws2.Tables.Count()); + + var copy = ws2.Tables.First(); + + AssertTablesAreEqual(original, copy); + + Assert.AreEqual("Sheet2!A1:C2", copy.RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Custom column 1", ws2.Cell("A1").Value); + Assert.AreEqual("Custom column 2", ws2.Cell("B1").Value); + Assert.AreEqual("Custom column 3", ws2.Cell("C1").Value); + Assert.AreEqual("Value 1", ws2.Cell("A2").Value); + Assert.AreEqual(123.45, (double)ws2.Cell("B2").Value, XLHelper.Epsilon); + Assert.AreEqual(new DateTime(2018, 5, 10), ws2.Cell("C2").Value); + } + + [Test] + public void NewTableHasNullRelId() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.Worksheets.Add("Sheet1"); + ws.Cell("A1").Value = "Custom column 1"; + ws.Cell("B1").Value = "Custom column 2"; + ws.Cell("C1").Value = "Custom column 3"; + ws.Cell("A2").Value = "Value 1"; + ws.Cell("B2").Value = 123.45; + ws.Cell("C2").Value = new DateTime(2018, 5, 10); + var original = ws.Range("A1:C2").CreateTable("Attached table"); + + Assert.AreEqual(1, ws.Tables.Count()); + Assert.IsNull((original as XLTable).RelId); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.Add("Sheet2"); + var original = wb.Worksheets.First().Tables.First(); + + Assert.IsNotNull((original as XLTable).RelId); + + var copy = original.CopyTo(ws); + + Assert.AreEqual(1, ws.Tables.Count()); + Assert.IsNull((copy as XLTable).RelId); + + AssertTablesAreEqual(original, copy); + + Assert.AreEqual("Sheet2!A1:C2", copy.RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Custom column 1", ws.Cell("A1").Value); + Assert.AreEqual("Custom column 2", ws.Cell("B1").Value); + Assert.AreEqual("Custom column 3", ws.Cell("C1").Value); + Assert.AreEqual("Value 1", ws.Cell("A2").Value); + Assert.AreEqual(123.45, (double)ws.Cell("B2").Value, XLHelper.Epsilon); + Assert.AreEqual(new DateTime(2018, 5, 10), ws.Cell("C2").Value); + } + } + } + + [Test] + public void CopyTableWithoutData() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add("Sheet1"); + ws1.Cell("A1").Value = "Custom column 1"; + ws1.Cell("B1").Value = "Custom column 2"; + ws1.Cell("C1").Value = "Custom column 3"; + ws1.Cell("A2").Value = "Value 1"; + ws1.Cell("B2").Value = 123.45; + ws1.Cell("C2").Value = new DateTime(2018, 5, 10); + var original = ws1.Range("A1:C2").AsTable("Attached table"); + ws1.Tables.Add(original); + var ws2 = wb.Worksheets.Add("Sheet2") as XLWorksheet; + + var copy = (original as XLTable).CopyTo(ws2, false); + + AssertTablesAreEqual(original, copy); + + Assert.AreEqual("Sheet2!A1:C2", copy.RangeAddress.ToString(XLReferenceStyle.A1, true)); + Assert.AreEqual("Custom column 1", ws2.Cell("A1").Value); + Assert.AreEqual("Custom column 2", ws2.Cell("B1").Value); + Assert.AreEqual("Custom column 3", ws2.Cell("C1").Value); + Assert.AreEqual("", ws2.Cell("A2").Value); + Assert.AreEqual("", ws2.Cell("B2").Value); + Assert.AreEqual("", ws2.Cell("C2").Value); + } + + [Test] + public void SavingTableWithNullDataRangeThrowsException() + { + using (var ms = new MemoryStream()) + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + var data = Enumerable.Range(1, 10) + .Select(i => new + { + Number = i, + NumberString = String.Concat("Number", i.ToString()) + }); + + var table = ws.FirstCell() + .InsertTable(data) + .SetShowTotalsRow(); + + table.Fields.Last().TotalsRowFunction = XLTotalsRowFunction.Count; + + table.DataRange.Rows() + .OrderByDescending(r => r.RowNumber()) + .ToList() + .ForEach(r => r.WorksheetRow().Delete()); + + Assert.IsNull(table.DataRange); + Assert.Throws(() => wb.SaveAs(ms)); + } + } + + [Test] + public void CanCreateTableWithWhiteSpaceColumnHeaders() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.Cell("A1").SetValue("Header"); + ws.Cell("B1").SetValue(new string(' ', 1)); + ws.Cell("C1").SetValue(new string(' ', 2)); + ws.Cell("D1").SetValue(new string(' ', 3)); + + var table = ws.Range("A1:E3").CreateTable("Table1"); + + Assert.AreEqual("Header", table.Field(0).Name); + Assert.AreEqual(new string(' ', 1), table.Field(1).Name); + Assert.AreEqual(new string(' ', 2), table.Field(2).Name); + Assert.AreEqual(new string(' ', 3), table.Field(3).Name); + Assert.AreEqual("Column5", table.Field(4).Name); + } + } + + [Test] + public void TableNotFound() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + Assert.Throws(() => ws.Table("dummy")); + Assert.Throws(() => wb.Table("dummy")); + } + } + + [Test] + public void SecondTableOnNewSheetHasUniqueName() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + var t1 = ws1.FirstCell().InsertTable(Enumerable.Range(1, 10).Select(i => new { Number = i })); + Assert.AreEqual("Table1", t1.Name); + + var ws2 = wb.AddWorksheet(); + var t2 = ws2.FirstCell().InsertTable(Enumerable.Range(1, 10).Select(i => new { Number = i })); + Assert.AreEqual("Table2", t2.Name); + } + } + + private void AssertTablesAreEqual(IXLTable table1, IXLTable table2) + { + Assert.AreEqual(table1.RangeAddress.ToString(XLReferenceStyle.A1, false), table2.RangeAddress.ToString(XLReferenceStyle.A1, false)); + Assert.AreEqual(table1.Fields.Count(), table2.Fields.Count()); + for (int j = 0; j < table1.Fields.Count(); j++) + { + var originalField = table1.Fields.ElementAt(j); + var copyField = table2.Fields.ElementAt(j); + Assert.AreEqual(originalField.Name, copyField.Name); + if (table1.ShowTotalsRow) + { + Assert.AreEqual(originalField.TotalsRowFormulaA1, copyField.TotalsRowFormulaA1); + Assert.AreEqual(originalField.TotalsRowFunction, copyField.TotalsRowFunction); + } + } + + Assert.AreEqual(table1.Name, table2.Name); + Assert.AreEqual(table1.ShowAutoFilter, table2.ShowAutoFilter); + Assert.AreEqual(table1.ShowColumnStripes, table2.ShowColumnStripes); + Assert.AreEqual(table1.ShowHeaderRow, table2.ShowHeaderRow); + Assert.AreEqual(table1.ShowRowStripes, table2.ShowRowStripes); + Assert.AreEqual(table1.ShowTotalsRow, table2.ShowTotalsRow); + Assert.AreEqual((table1.Style as XLStyle).Value, (table2.Style as XLStyle).Value); + Assert.AreEqual(table1.Theme, table2.Theme); + } + + //TODO: Delete table (not underlying range) + } +} diff --git a/ClosedXML_Tests/Excel/Worksheets/XLSheetViewTests.cs b/ClosedXML_Tests/Excel/Worksheets/XLSheetViewTests.cs new file mode 100644 index 0000000..ce594f4 --- /dev/null +++ b/ClosedXML_Tests/Excel/Worksheets/XLSheetViewTests.cs @@ -0,0 +1,73 @@ +// Keep this file CodeMaid organised and cleaned +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.Excel.Worksheets +{ + [TestFixture] + public class XLSheetViewTests + { + [Test] + public void CopyWorksheetSheetViews() + { + using var wb1 = new XLWorkbook(); + using var wb2 = new XLWorkbook(); + + var ws1 = wb1.AddWorksheet("WS1"); + ws1.SheetView.TopLeftCellAddress = ws1.Cell("AZ2000").Address; + + var ws2 = ws1.CopyTo(wb2, "WS2"); + + Assert.AreEqual(ws2, ws2.SheetView.Worksheet); + Assert.AreEqual("AZ2000", ws2.SheetView.TopLeftCellAddress.ToString()); + } + + [Test] + public void InvalidTopLeftCell() + { + using var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet(); + var ws2 = wb.AddWorksheet(); + + Assert.Throws(() => ws1.SheetView.TopLeftCellAddress = ws2.Cell("A1").Address); + } + + [Test] + public void SheetViews() + { + using var ms = new MemoryStream(); + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + ws.SheetView.TopLeftCellAddress = ws.Cell("AZ2000").Address; + wb.SaveAs(ms); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("AZ2000", ws.SheetView.TopLeftCellAddress.ToString()); + + ws.SheetView.TopLeftCellAddress = ws.Cell("AZ2000") + .CellBelow() + .CellRight() + .Address; + + wb.Save(); + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + Assert.AreEqual("BA2001", ws.SheetView.TopLeftCellAddress.ToString()); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs new file mode 100644 index 0000000..76db567 --- /dev/null +++ b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs @@ -0,0 +1,1184 @@ +using ClosedXML.Excel; +using ClosedXML.Excel.Drawings; +using NUnit.Framework; +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class XLWorksheetTests + { + [Test] + public void ColumnCountTime() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + DateTime start = DateTime.Now; + ws.ColumnCount(); + DateTime end = DateTime.Now; + Assert.IsTrue((end - start).TotalMilliseconds < 500); + } + + [Test] + public void CopyConditionalFormatsCount() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.AddWorksheet("Sheet1"); + ws.Range("A1:C3").AddConditionalFormat().WhenContains("1").Fill.SetBackgroundColor(XLColor.Blue); + ws.Range("A1:C3").Value = 1; + IXLWorksheet ws2 = ws.CopyTo("Sheet2"); + Assert.AreEqual(1, ws2.ConditionalFormats.Count()); + } + + [Test] + public void CopyColumnVisibility() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + ws.Columns(10, 20).Hide(); + ws.CopyTo("Sheet2"); + Assert.IsTrue(wb.Worksheet("Sheet2").Column(10).IsHidden); + } + + [Test] + public void CopyRowVisibility() + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet1"); + ws.Rows(2, 5).Hide(); + ws.CopyTo("Sheet2"); + Assert.IsTrue(wb.Worksheet("Sheet2").Row(4).IsHidden); + } + + [Test] + public void DeletingSheets1() + { + var wb = new XLWorkbook(); + wb.Worksheets.Add("Sheet3"); + wb.Worksheets.Add("Sheet2"); + wb.Worksheets.Add("Sheet1", 1); + + wb.Worksheet("Sheet3").Delete(); + + Assert.AreEqual("Sheet1", wb.Worksheet(1).Name); + Assert.AreEqual("Sheet2", wb.Worksheet(2).Name); + Assert.AreEqual(2, wb.Worksheets.Count); + } + + [Test] + public void InsertingSheets1() + { + var wb = new XLWorkbook(); + wb.Worksheets.Add("Sheet1"); + wb.Worksheets.Add("Sheet2"); + wb.Worksheets.Add("Sheet3"); + + Assert.AreEqual("Sheet1", wb.Worksheet(1).Name); + Assert.AreEqual("Sheet2", wb.Worksheet(2).Name); + Assert.AreEqual("Sheet3", wb.Worksheet(3).Name); + } + + [Test] + public void InsertingSheets2() + { + var wb = new XLWorkbook(); + wb.Worksheets.Add("Sheet2"); + wb.Worksheets.Add("Sheet1", 1); + wb.Worksheets.Add("Sheet3"); + + Assert.AreEqual("Sheet1", wb.Worksheet(1).Name); + Assert.AreEqual("Sheet2", wb.Worksheet(2).Name); + Assert.AreEqual("Sheet3", wb.Worksheet(3).Name); + } + + [Test] + public void InsertingSheets3() + { + var wb = new XLWorkbook(); + wb.Worksheets.Add("Sheet3"); + wb.Worksheets.Add("Sheet2", 1); + wb.Worksheets.Add("Sheet1", 1); + + Assert.AreEqual("Sheet1", wb.Worksheet(1).Name); + Assert.AreEqual("Sheet2", wb.Worksheet(2).Name); + Assert.AreEqual("Sheet3", wb.Worksheet(3).Name); + } + + [Test] + public void InsertingSheets4() + { + var wb = new XLWorkbook(); + var ws1 = wb.Worksheets.Add(); + + Assert.AreEqual("Sheet1", ws1.Name); + ws1.Name = "shEEt1"; + + var ws2 = wb.Worksheets.Add(); + Assert.AreEqual("Sheet2", ws2.Name); + + wb.Worksheets.Add("SHEET4"); + + Assert.AreEqual("Sheet5", wb.Worksheets.Add().Name); + Assert.AreEqual("Sheet6", wb.Worksheets.Add().Name); + + wb.Worksheets.Add(1); + + Assert.AreEqual("Sheet7", wb.Worksheet(1).Name); + } + + [Test] + public void AddingDuplicateSheetNameThrowsException() + { + using (var wb = new XLWorkbook()) + { + IXLWorksheet ws; + ws = wb.AddWorksheet("Sheet1"); + + Assert.Throws(() => wb.AddWorksheet("Sheet1")); + + //Sheet names are case insensitive + Assert.Throws(() => wb.AddWorksheet("sheet1")); + } + } + + [Test] + public void MergedRanges() + { + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); + ws.Range("A1:B2").Merge(); + ws.Range("C1:D3").Merge(); + ws.Range("D2:E2").Merge(); + + Assert.AreEqual(2, ws.MergedRanges.Count); + Assert.AreEqual("A1:B2", ws.MergedRanges.First().RangeAddress.ToStringRelative()); + Assert.AreEqual("D2:E2", ws.MergedRanges.Last().RangeAddress.ToStringRelative()); + + Assert.AreEqual("A1:B2", ws.Cell("A2").MergedRange().RangeAddress.ToStringRelative()); + Assert.AreEqual("D2:E2", ws.Cell("D2").MergedRange().RangeAddress.ToStringRelative()); + + Assert.AreEqual(null, ws.Cell("Z10").MergedRange()); + } + + [Test] + public void RowCountTime() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet1"); + DateTime start = DateTime.Now; + ws.RowCount(); + DateTime end = DateTime.Now; + Assert.IsTrue((end - start).TotalMilliseconds < 500); + } + + [Test] + public void SheetsWithCommas() + { + using (var wb = new XLWorkbook()) + { + var sourceSheetName = "Sheet1, Sheet3"; + var ws = wb.Worksheets.Add(sourceSheetName); + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 2; + ws.Cell("B2").Value = 3; + + ws = wb.Worksheets.Add("Formula"); + ws.FirstCell().FormulaA1 = string.Format("=SUM('{0}'!A1:A2,'{0}'!B1:B2)", sourceSheetName); + + var value = ws.FirstCell().Value; + Assert.AreEqual(6, value); + } + } + + [Test] + public void CanRenameWorksheet() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet("Sheet1"); + var ws2 = wb.AddWorksheet("Sheet2"); + + ws1.Name = "New sheet name"; + Assert.AreEqual("New sheet name", ws1.Name); + + ws2.Name = "sheet2"; + Assert.AreEqual("sheet2", ws2.Name); + + Assert.Throws(() => ws1.Name = "SHEET2"); + } + } + + [Test] + public void TryGetWorksheet() + { + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet("Sheet1"); + wb.AddWorksheet("Sheet2"); + + IXLWorksheet ws; + Assert.IsTrue(wb.Worksheets.TryGetWorksheet("Sheet1", out ws)); + Assert.IsTrue(wb.Worksheets.TryGetWorksheet("sheet1", out ws)); + Assert.IsTrue(wb.Worksheets.TryGetWorksheet("sHEeT1", out ws)); + Assert.IsFalse(wb.Worksheets.TryGetWorksheet("Sheeeet2", out ws)); + + Assert.IsTrue(wb.TryGetWorksheet("Sheet1", out ws)); + Assert.IsTrue(wb.TryGetWorksheet("sheet1", out ws)); + Assert.IsTrue(wb.TryGetWorksheet("sHEeT1", out ws)); + Assert.IsFalse(wb.TryGetWorksheet("Sheeeet2", out ws)); + } + } + + [Test] + public void HideWorksheet() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add("VisibleSheet"); + wb.Worksheets.Add("HiddenSheet").Hide(); + wb.SaveAs(ms); + } + + // unhide the hidden sheet + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual(XLWorksheetVisibility.Visible, wb.Worksheet("VisibleSheet").Visibility); + Assert.AreEqual(XLWorksheetVisibility.Hidden, wb.Worksheet("HiddenSheet").Visibility); + + var ws = wb.Worksheet("HiddenSheet"); + ws.Unhide().Name = "NoAlsoVisible"; + + Assert.AreEqual(XLWorksheetVisibility.Visible, ws.Visibility); + + wb.Save(); + } + + using (var wb = new XLWorkbook(ms)) + { + Assert.AreEqual(XLWorksheetVisibility.Visible, wb.Worksheet("VisibleSheet").Visibility); + Assert.AreEqual(XLWorksheetVisibility.Visible, wb.Worksheet("NoAlsoVisible").Visibility); + } + } + } + + [Test] + public void CanCopySheetsWithAllAnchorTypes() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + ws.CopyTo("Copy1"); + + var ws2 = wb.Worksheets.Skip(1).First(); + ws2.CopyTo("Copy2"); + + var ws3 = wb.Worksheets.Skip(2).First(); + ws3.CopyTo("Copy3"); + + var ws4 = wb.Worksheets.Skip(3).First(); + ws3.CopyTo("Copy4"); + } + } + + [Test] + public void CannotCopyDeletedWorksheet() + { + using (var wb = new XLWorkbook()) + { + wb.AddWorksheet("Sheet1"); + var ws = wb.AddWorksheet("Sheet2"); + + ws.Delete(); + Assert.Throws(() => ws.CopyTo("Copy of Sheet2")); + } + } + + [Test] + public void WorksheetNameCannotStartWithApostrophe() + { + var title = "'StartsWithApostrophe"; + TestDelegate addWorksheet = () => + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add(title); + } + }; + + Assert.Throws(typeof(ArgumentException), addWorksheet); + } + + [Test] + public void WorksheetNameCannotEndWithApostrophe() + { + var title = "EndsWithApostrophe'"; + TestDelegate addWorksheet = () => + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add(title); + } + }; + + Assert.Throws(typeof(ArgumentException), addWorksheet); + } + + [Test] + public void WorksheetNameCanContainApostrophe() + { + var title = "With'Apostrophe"; + var savedTitle = ""; + TestDelegate saveAndOpenWorkbook = () => + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + wb.Worksheets.Add(title); + wb.Worksheets.First().Cell(1, 1).FormulaA1 = $"{title}!A2"; + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + savedTitle = wb.Worksheets.First().Name; + } + } + }; + + Assert.DoesNotThrow(saveAndOpenWorkbook); + Assert.AreEqual(title, savedTitle); + } + + [Test] + public void CopyWorksheetPreservesContents() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Cell("A1").Value = "A1 value"; + ws1.Cell("A2").Value = 100; + ws1.Cell("D4").Value = new DateTime(2018, 5, 1); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual("A1 value", ws2.Cell("A1").Value); + Assert.AreEqual(100, ws2.Cell("A2").Value); + Assert.AreEqual(new DateTime(2018, 5, 1), ws2.Cell("D4").Value); + } + } + + [Test] + public void CopyWorksheetPreservesFormulae() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Cell("A1").FormulaA1 = "10*10"; + ws1.Cell("A2").FormulaA1 = "A1 * 2"; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual("10*10", ws2.Cell("A1").FormulaA1); + Assert.AreEqual("A1 * 2", ws2.Cell("A2").FormulaA1); + } + } + + [Test] + public void CopyWorksheetPreservesRowHeights() + { + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + using (var wb2 = new XLWorkbook()) + { + ws1.RowHeight = 55; + ws1.Row(2).Height = 0; + ws1.Row(3).Height = 20; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.RowHeight, ws2.RowHeight); + for (int i = 1; i <= 3; i++) + { + Assert.AreEqual(ws1.Row(i).Height, ws2.Row(i).Height); + } + } + } + } + + [Test] + public void CopyWorksheetPreservesColumnWidths() + { + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + using (var wb2 = new XLWorkbook()) + { + ws1.ColumnWidth = 160; + ws1.Column(2).Width = 0; + ws1.Column(3).Width = 240; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.ColumnWidth, ws2.ColumnWidth); + for (int i = 1; i <= 3; i++) + { + Assert.AreEqual(ws1.Column(i).Width, ws2.Column(i).Width); + } + } + } + } + + [Test] + public void CopyWorksheetPreservesMergedCells() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Range("A:A").Merge(); + ws1.Range("B1:C2").Merge(); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.MergedRanges.Count, ws2.MergedRanges.Count); + for (int i = 0; i < ws1.MergedRanges.Count; i++) + { + Assert.AreEqual(ws1.MergedRanges.ElementAt(i).RangeAddress.ToString(), + ws2.MergedRanges.ElementAt(i).RangeAddress.ToString()); + } + } + } + + [Test] + public void CopyWorksheetAcrossWorkbooksPreservesNamedRanges() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Range("A1:A2").AddToNamed("GLOBAL", XLScope.Workbook); + ws1.Ranges("B1:B2,D1:D2").AddToNamed("LOCAL", XLScope.Worksheet); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.NamedRanges.Count(), ws2.NamedRanges.Count()); + for (int i = 0; i < ws1.NamedRanges.Count(); i++) + { + var nr1 = ws1.NamedRanges.ElementAt(i); + var nr2 = ws2.NamedRanges.ElementAt(i); + Assert.AreEqual(nr1.Ranges.ToString(), nr2.Ranges.ToString()); + Assert.AreEqual(nr1.Scope, nr2.Scope); + Assert.AreEqual(nr1.Name, nr2.Name); + Assert.AreEqual(nr1.Visible, nr2.Visible); + Assert.AreEqual(nr1.Comment, nr2.Comment); + } + } + } + + [Test] + public void CopyWorksheeInsideWorkbookMakesNamedRangesLocal() + { + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Range("A1:A2").AddToNamed("GLOBAL", XLScope.Workbook); + ws1.Ranges("B1:B2,D1:D2").AddToNamed("LOCAL", XLScope.Worksheet); + + var ws2 = ws1.CopyTo("Copy"); + + Assert.AreEqual(ws1.NamedRanges.Count(), ws2.NamedRanges.Count()); + for (int i = 0; i < ws1.NamedRanges.Count(); i++) + { + var nr1 = ws1.NamedRanges.ElementAt(i); + var nr2 = ws2.NamedRanges.ElementAt(i); + + Assert.AreEqual(XLScope.Worksheet, nr2.Scope); + + Assert.AreEqual(nr1.Ranges.ToString(), nr2.Ranges.ToString()); + Assert.AreEqual(nr1.Name, nr2.Name); + Assert.AreEqual(nr1.Visible, nr2.Visible); + Assert.AreEqual(nr1.Comment, nr2.Comment); + } + } + } + + [Test] + public void CopyWorksheetPreservesStyles() + { + using (var ms = new MemoryStream()) + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + ws1.Range("A1:B2").Style.Font.FontSize = 25; + ws1.Cell("C3").Style.Fill.BackgroundColor = XLColor.Red; + ws1.Cell("C4").Style.Fill.BackgroundColor = XLColor.AliceBlue; + ws1.Cell("C4").Value = "Non empty"; + + using (var wb2 = new XLWorkbook()) + { + var ws2 = ws1.CopyTo(wb2, "Copy"); + AssertStylesAreEqual(ws1, ws2); + wb2.SaveAs(ms); + } + + using (var wb2 = new XLWorkbook(ms)) + { + var ws2 = wb2.Worksheet("Copy"); + AssertStylesAreEqual(ws1, ws2); + } + } + + void AssertStylesAreEqual(IXLWorksheet ws1, IXLWorksheet ws2) + { + Assert.AreEqual((ws1.Style as XLStyle).Value, (ws2.Style as XLStyle).Value, + "Worksheet styles differ"); + var cellsUsed = ws1.Range(ws1.FirstCell(), ws1.LastCellUsed()).Cells(); + foreach (var cell in cellsUsed) + { + var style1 = (cell.Style as XLStyle).Value; + var style2 = (ws2.Cell(cell.Address.ToString()).Style as XLStyle).Value; + Assert.AreEqual(style1, style2, $"Cell {cell.Address} styles differ"); + } + } + } + + [Test] + public void CopyWorksheetPreservesConditionalFormats() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Range("A:A").AddConditionalFormat() + .WhenContains("0").Fill.SetBackgroundColor(XLColor.Red); + var cf = ws1.Range("B1:C2").AddConditionalFormat(); + cf.Ranges.Add(ws1.Range("D4:D5")); + cf.WhenEqualOrGreaterThan(100).Font.SetBold(); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.ConditionalFormats.Count(), ws2.ConditionalFormats.Count()); + for (int i = 0; i < ws1.ConditionalFormats.Count(); i++) + { + var original = ws1.ConditionalFormats.ElementAt(i); + var copy = ws2.ConditionalFormats.ElementAt(i); + Assert.AreEqual(original.Ranges.Count, copy.Ranges.Count); + for (int j = 0; j < original.Ranges.Count; j++) + { + Assert.AreEqual(original.Ranges.ElementAt(j).RangeAddress.ToString(XLReferenceStyle.A1, false), + copy.Ranges.ElementAt(j).RangeAddress.ToString(XLReferenceStyle.A1, false)); + } + + Assert.AreEqual((original.Style as XLStyle).Value, (copy.Style as XLStyle).Value); + Assert.AreEqual(original.Values.Single().Value.Value, copy.Values.Single().Value.Value); + } + } + } + + [Test] + public void CopyWorksheetPreservesTables() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Cell("A2").Value = "Name"; + ws1.Cell("B2").Value = "Count"; + ws1.Cell("A3").Value = "John Smith"; + ws1.Cell("B3").Value = 50; + ws1.Cell("A4").Value = "Ivan Ivanov"; + ws1.Cell("B4").Value = 40; + var table1 = ws1.Range("A2:B4").CreateTable("Test table 1"); + table1 + .SetShowAutoFilter(true) + .SetShowTotalsRow(true) + .SetEmphasizeFirstColumn(true) + .SetShowColumnStripes(true) + .SetShowRowStripes(true); + table1.Theme = XLTableTheme.TableStyleDark8; + table1.Field(1).TotalsRowFunction = XLTotalsRowFunction.Sum; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.Tables.Count(), ws2.Tables.Count()); + for (int i = 0; i < ws1.Tables.Count(); i++) + { + var original = ws1.Tables.ElementAt(i); + var copy = ws2.Tables.ElementAt(i); + Assert.AreEqual(original.RangeAddress.ToString(XLReferenceStyle.A1, false), copy.RangeAddress.ToString(XLReferenceStyle.A1, false)); + Assert.AreEqual(original.Fields.Count(), copy.Fields.Count()); + for (int j = 0; j < original.Fields.Count(); j++) + { + var originalField = original.Fields.ElementAt(j); + var copyField = copy.Fields.ElementAt(j); + Assert.AreEqual(originalField.Name, copyField.Name); + Assert.AreEqual(originalField.TotalsRowFormulaA1, copyField.TotalsRowFormulaA1); + Assert.AreEqual(originalField.TotalsRowFunction, copyField.TotalsRowFunction); + } + + Assert.AreEqual(original.Name, copy.Name); + Assert.AreEqual(original.ShowAutoFilter, copy.ShowAutoFilter); + Assert.AreEqual(original.ShowColumnStripes, copy.ShowColumnStripes); + Assert.AreEqual(original.ShowHeaderRow, copy.ShowHeaderRow); + Assert.AreEqual(original.ShowRowStripes, copy.ShowRowStripes); + Assert.AreEqual(original.ShowTotalsRow, copy.ShowTotalsRow); + Assert.AreEqual((original.Style as XLStyle).Value, (copy.Style as XLStyle).Value); + Assert.AreEqual(original.Theme, copy.Theme); + } + } + } + + [Test] + public void CopyWorksheetPreservesDataValidation() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + var dv1 = ws1.Range("A:A").SetDataValidation(); + dv1.WholeNumber.EqualTo(2); + dv1.ErrorStyle = XLErrorStyle.Warning; + dv1.ErrorTitle = "Number out of range"; + dv1.ErrorMessage = "This cell only allows the number 2."; + + var dv2 = ws1.Ranges("B2:C3,D4:E5").SetDataValidation(); + dv2.Decimal.GreaterThan(5); + dv2.ErrorStyle = XLErrorStyle.Stop; + dv2.ErrorTitle = "Decimal number out of range"; + dv2.ErrorMessage = "This cell only allows decimals greater than 5."; + + var dv3 = ws1.Cell("D1").SetDataValidation(); + dv3.TextLength.EqualOrLessThan(10); + dv3.ErrorStyle = XLErrorStyle.Information; + dv3.ErrorTitle = "Text length out of range"; + dv3.ErrorMessage = "You entered more than 10 characters."; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.DataValidations.Count(), ws2.DataValidations.Count()); + for (int i = 0; i < ws1.DataValidations.Count(); i++) + { + var original = ws1.DataValidations.ElementAt(i); + var copy = ws2.DataValidations.ElementAt(i); + + var originalRanges = string.Join(",", original.Ranges.Select(r => r.RangeAddress.ToString())); + var copyRanges = string.Join(",", original.Ranges.Select(r => r.RangeAddress.ToString())); + + Assert.AreEqual(originalRanges, copyRanges); + Assert.AreEqual(original.AllowedValues, copy.AllowedValues); + Assert.AreEqual(original.Operator, copy.Operator); + Assert.AreEqual(original.ErrorStyle, copy.ErrorStyle); + Assert.AreEqual(original.ErrorTitle, copy.ErrorTitle); + Assert.AreEqual(original.ErrorMessage, copy.ErrorMessage); + } + } + } + + [Test] + public void CopyWorksheetPreservesPictures() + { + using (var ms = new MemoryStream()) + using (var resourceStream = Assembly.GetAssembly(typeof(ClosedXML_Examples.BasicTable)) + .GetManifestResourceStream("ClosedXML_Examples.Resources.SampleImage.jpg")) + using (var bitmap = Bitmap.FromStream(resourceStream) as Bitmap) + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + var picture = ws1.AddPicture(bitmap, "MyPicture") + .WithPlacement(XLPicturePlacement.FreeFloating) + .MoveTo(50, 50) + .WithSize(200, 200); + + using (var wb2 = new XLWorkbook()) + { + var ws2 = ws1.CopyTo(wb2, "Copy"); + AssertPicturesAreEqual(ws1, ws2); + wb2.SaveAs(ms); + } + + using (var wb2 = new XLWorkbook(ms)) + { + var ws2 = wb2.Worksheet("Copy"); + AssertPicturesAreEqual(ws1, ws2); + } + } + + void AssertPicturesAreEqual(IXLWorksheet ws1, IXLWorksheet ws2) + { + Assert.AreEqual(ws1.Pictures.Count(), ws2.Pictures.Count()); + + for (int i = 0; i < ws1.Pictures.Count(); i++) + { + var original = ws1.Pictures.ElementAt(i); + var copy = ws2.Pictures.ElementAt(i); + Assert.AreEqual(ws2, copy.Worksheet); + + Assert.AreEqual(original.Format, copy.Format); + Assert.AreEqual(original.Height, copy.Height); + Assert.AreEqual(original.Id, copy.Id); + Assert.AreEqual(original.Left, copy.Left); + Assert.AreEqual(original.Name, copy.Name); + Assert.AreEqual(original.Placement, copy.Placement); + Assert.AreEqual(original.Top, copy.Top); + Assert.AreEqual(original.TopLeftCell.Address.ToString(), copy.TopLeftCell.Address.ToString()); + Assert.AreEqual(original.Width, copy.Width); + Assert.AreEqual(original.ImageStream.ToArray(), copy.ImageStream.ToArray(), "Image streams differ"); + } + } + } + + [Test] + public void CopyWorksheetPreservesSelectedRanges() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.SelectedRanges.RemoveAll(); + ws1.SelectedRanges.Add(ws1.Range("E12:H20")); + ws1.SelectedRanges.Add(ws1.Range("B:B")); + ws1.SelectedRanges.Add(ws1.Range("3:6")); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.SelectedRanges.Count, ws2.SelectedRanges.Count); + for (int i = 0; i < ws1.SelectedRanges.Count; i++) + { + Assert.AreEqual(ws1.SelectedRanges.ElementAt(i).RangeAddress.ToString(), + ws2.SelectedRanges.ElementAt(i).RangeAddress.ToString()); + } + } + } + + [Test] + public void CopyWorksheetPreservesPageSetup() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.PageSetup.AddHorizontalPageBreak(15); + ws1.PageSetup.AddVerticalPageBreak(5); + ws1.PageSetup + .SetBlackAndWhite() + .SetCenterHorizontally() + .SetCenterVertically() + .SetFirstPageNumber(200) + .SetPageOrientation(XLPageOrientation.Landscape) + .SetPaperSize(XLPaperSize.A5Paper) + .SetScale(89) + .SetShowGridlines() + .SetHorizontalDpi(200) + .SetVerticalDpi(300) + .SetPagesTall(5) + .SetPagesWide(2) + .SetColumnsToRepeatAtLeft(1, 3); + ws1.PageSetup.PrintAreas.Clear(); + ws1.PageSetup.PrintAreas.Add("A1:Z200"); + ws1.PageSetup.Margins.SetBottom(5).SetTop(6).SetLeft(7).SetRight(8).SetFooter(9).SetHeader(10); + ws1.PageSetup.Header.Left.AddText(XLHFPredefinedText.FullPath, XLHFOccurrence.AllPages); + ws1.PageSetup.Footer.Right.AddText(XLHFPredefinedText.PageNumber, XLHFOccurrence.OddPages); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(ws1.PageSetup.FirstRowToRepeatAtTop, ws2.PageSetup.FirstRowToRepeatAtTop); + Assert.AreEqual(ws1.PageSetup.LastRowToRepeatAtTop, ws2.PageSetup.LastRowToRepeatAtTop); + Assert.AreEqual(ws1.PageSetup.FirstColumnToRepeatAtLeft, ws2.PageSetup.FirstColumnToRepeatAtLeft); + Assert.AreEqual(ws1.PageSetup.LastColumnToRepeatAtLeft, ws2.PageSetup.LastColumnToRepeatAtLeft); + Assert.AreEqual(ws1.PageSetup.PageOrientation, ws2.PageSetup.PageOrientation); + Assert.AreEqual(ws1.PageSetup.PagesWide, ws2.PageSetup.PagesWide); + Assert.AreEqual(ws1.PageSetup.PagesTall, ws2.PageSetup.PagesTall); + Assert.AreEqual(ws1.PageSetup.Scale, ws2.PageSetup.Scale); + Assert.AreEqual(ws1.PageSetup.HorizontalDpi, ws2.PageSetup.HorizontalDpi); + Assert.AreEqual(ws1.PageSetup.VerticalDpi, ws2.PageSetup.VerticalDpi); + Assert.AreEqual(ws1.PageSetup.FirstPageNumber, ws2.PageSetup.FirstPageNumber); + Assert.AreEqual(ws1.PageSetup.CenterHorizontally, ws2.PageSetup.CenterHorizontally); + Assert.AreEqual(ws1.PageSetup.CenterVertically, ws2.PageSetup.CenterVertically); + Assert.AreEqual(ws1.PageSetup.PaperSize, ws2.PageSetup.PaperSize); + Assert.AreEqual(ws1.PageSetup.Margins.Bottom, ws2.PageSetup.Margins.Bottom); + Assert.AreEqual(ws1.PageSetup.Margins.Top, ws2.PageSetup.Margins.Top); + Assert.AreEqual(ws1.PageSetup.Margins.Left, ws2.PageSetup.Margins.Left); + Assert.AreEqual(ws1.PageSetup.Margins.Right, ws2.PageSetup.Margins.Right); + Assert.AreEqual(ws1.PageSetup.Margins.Footer, ws2.PageSetup.Margins.Footer); + Assert.AreEqual(ws1.PageSetup.Margins.Header, ws2.PageSetup.Margins.Header); + Assert.AreEqual(ws1.PageSetup.ScaleHFWithDocument, ws2.PageSetup.ScaleHFWithDocument); + Assert.AreEqual(ws1.PageSetup.AlignHFWithMargins, ws2.PageSetup.AlignHFWithMargins); + Assert.AreEqual(ws1.PageSetup.ShowGridlines, ws2.PageSetup.ShowGridlines); + Assert.AreEqual(ws1.PageSetup.ShowRowAndColumnHeadings, ws2.PageSetup.ShowRowAndColumnHeadings); + Assert.AreEqual(ws1.PageSetup.BlackAndWhite, ws2.PageSetup.BlackAndWhite); + Assert.AreEqual(ws1.PageSetup.DraftQuality, ws2.PageSetup.DraftQuality); + Assert.AreEqual(ws1.PageSetup.PageOrder, ws2.PageSetup.PageOrder); + Assert.AreEqual(ws1.PageSetup.ShowComments, ws2.PageSetup.ShowComments); + Assert.AreEqual(ws1.PageSetup.PrintErrorValue, ws2.PageSetup.PrintErrorValue); + + Assert.AreEqual(ws1.PageSetup.PrintAreas.Count(), ws2.PageSetup.PrintAreas.Count()); + + Assert.AreEqual(ws1.PageSetup.Header.Left.GetText(XLHFOccurrence.AllPages), ws2.PageSetup.Header.Left.GetText(XLHFOccurrence.AllPages)); + Assert.AreEqual(ws1.PageSetup.Footer.Right.GetText(XLHFOccurrence.OddPages), ws2.PageSetup.Footer.Right.GetText(XLHFOccurrence.OddPages)); + } + } + + [Test] + public void CopyWorksheetPreservesSparklineGroups() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + var original = ws1.SparklineGroups.Add("A1:A10", "D1:Z10") + .SetDateRange(ws1.Range("D11:Z11")) + .SetDisplayEmptyCellsAs(XLDisplayBlanksAsValues.Zero) + .SetDisplayHidden(true) + .SetLineWeight(1.5) + .SetShowMarkers(XLSparklineMarkers.All) + .SetStyle(XLSparklineTheme.Colorful3) + .SetType(XLSparklineType.Column); + + original.HorizontalAxis + .SetColor(XLColor.Blue) + .SetRightToLeft(true) + .SetVisible(true); + + original.VerticalAxis + .SetManualMin(-100.0) + .SetManualMax(100.0); + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual(1, ws2.SparklineGroups.Count()); + var copy = ws2.SparklineGroups.Single(); + + Assert.AreEqual(original.Count(), copy.Count()); + for (int i = 0; i < original.Count(); i++) + { + Assert.AreSame(ws2, copy.ElementAt(i).Location.Worksheet); + Assert.AreSame(ws2, copy.ElementAt(i).SourceData.Worksheet); + Assert.AreEqual(original.ElementAt(i).Location.Address.ToString(), copy.ElementAt(i).Location.Address.ToString()); + Assert.AreEqual(original.ElementAt(i).SourceData.RangeAddress.ToString(), copy.ElementAt(i).SourceData.RangeAddress.ToString()); + } + + Assert.AreEqual(original.DateRange.RangeAddress.ToString(), copy.DateRange.RangeAddress.ToString()); + Assert.AreSame(ws2, copy.DateRange.Worksheet); + + Assert.AreEqual(original.DisplayEmptyCellsAs, copy.DisplayEmptyCellsAs); + Assert.AreEqual(original.DisplayHidden, copy.DisplayHidden); + Assert.AreEqual(original.LineWeight, copy.LineWeight, XLHelper.Epsilon); + Assert.AreEqual(original.ShowMarkers, copy.ShowMarkers); + Assert.AreEqual(original.Style, copy.Style); + Assert.AreNotSame(original.Style, copy.Style); + Assert.AreEqual(original.Type, copy.Type); + + Assert.AreEqual(original.HorizontalAxis.Color, copy.HorizontalAxis.Color); + Assert.AreEqual(original.HorizontalAxis.DateAxis, copy.HorizontalAxis.DateAxis); + Assert.AreEqual(original.HorizontalAxis.IsVisible, copy.HorizontalAxis.IsVisible); + Assert.AreEqual(original.HorizontalAxis.RightToLeft, copy.HorizontalAxis.RightToLeft); + + Assert.AreEqual(original.VerticalAxis.ManualMax, copy.VerticalAxis.ManualMax); + Assert.AreEqual(original.VerticalAxis.ManualMin, copy.VerticalAxis.ManualMin); + Assert.AreEqual(original.VerticalAxis.MaxAxisType, copy.VerticalAxis.MaxAxisType); + Assert.AreEqual(original.VerticalAxis.MinAxisType, copy.VerticalAxis.MinAxisType); + } + } + + [Test, Ignore("Muted until #836 is fixed")] + public void CopyWorksheetChangesAbsoluteReferencesInFormulae() + { + using (var wb1 = new XLWorkbook()) + using (var wb2 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Cell("A1").FormulaA1 = "10*10"; + ws1.Cell("A2").FormulaA1 = "Original!A1 * 3"; + + var ws2 = ws1.CopyTo(wb2, "Copy"); + + Assert.AreEqual("Copy!A1 * 3", ws2.Cell("A2").FormulaA1); + } + } + + [Test, Ignore("Muted until #836 is fixed")] + public void RenameWorksheetChangesAbsoluteReferencesInFormulae() + { + using (var wb1 = new XLWorkbook()) + { + var ws1 = wb1.Worksheets.Add("Original"); + + ws1.Cell("A1").FormulaA1 = "10*10"; + ws1.Cell("A2").FormulaA1 = "Original!A1 * 3"; + + ws1.Name = "Renamed"; + + Assert.AreEqual("Renamed!A1 * 3", ws1.Cell("A2").FormulaA1); + } + } + + [Test] + public void RangesFromDeletedWorksheetContainREF() + { + using (var wb1 = new XLWorkbook()) + { + wb1.Worksheets.Add("Sheet1"); + var ws2 = wb1.Worksheets.Add("Sheet2"); + var range = ws2.Range("A1:B2"); + + ws2.Delete(); + + Assert.AreEqual("#REF!A1:B2", range.RangeAddress.ToString()); + } + } + + [Test] + public void InvalidRowAndColumnIndices() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + Assert.Throws(() => ws.Row(-1)); + Assert.Throws(() => ws.Row(XLHelper.MaxRowNumber + 1)); + + Assert.Throws(() => ws.Column(-1)); + Assert.Throws(() => ws.Column(XLHelper.MaxColumnNumber + 1)); + } + } + + [Test] + public void InvalidSelectedRangeExcluded() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var range1 = ws.Range("B2:C2"); + var range2 = ws.Range("B4:C4"); + ws.SelectedRanges.Clear(); + + ws.SelectedRanges.Add(range1); + ws.SelectedRanges.Add(range2); + + ws.Row(4).Delete(); + + Assert.IsFalse(range2.RangeAddress.IsValid); + Assert.AreEqual(range1, ws.SelectedRanges.Single()); + } + } + + [Test] + public void InsertColumnsDoesNotIncreaseCellsCount() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell("AAA50"); + var originalCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + ws.Column(1).InsertColumnsBefore(1); + + Assert.AreEqual(originalCount, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void InsertRowsDoesNotIncreaseCellsCount() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell("AAA500"); + var originalCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + ws.Row(1).InsertRowsAbove(1); + + Assert.AreEqual(originalCount, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void InsertCellsBeforeDoesNotIncreaseCellsCount() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell("AAA50"); + var originalCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + cell1.InsertCellsBefore(1); + + Assert.AreEqual(originalCount, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void InsertCellsAboveDoesNotIncreaseCellsCount() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell("AAA500"); + var originalCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + cell1.InsertCellsAbove(1); + + Assert.AreEqual(originalCount, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void CellsShiftedTooFarRightArePurged() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell(1, XLHelper.MaxColumnNumber); + var cell3 = ws.Cell(2, XLHelper.MaxColumnNumber); + + cell1.InsertCellsBefore(1); + Assert.AreEqual(2, (ws as XLWorksheet).Internals.CellsCollection.Count); + ws.Column(1).InsertColumnsBefore(1); + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void CellsShiftedTooFarDownArePurged() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A1"); + var cell2 = ws.Cell(XLHelper.MaxRowNumber, 1); + var cell3 = ws.Cell(XLHelper.MaxRowNumber, 2); + + cell1.InsertCellsAbove(1); + Assert.AreEqual(2, (ws as XLWorksheet).Internals.CellsCollection.Count); + ws.Row(1).InsertRowsAbove(1); + Assert.AreEqual(1, (ws as XLWorksheet).Internals.CellsCollection.Count); + } + } + + [Test] + public void MaxColumnUsedUpdatedWhenColumnDeleted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("C1"); + var cell2 = ws.Cell(1, XLHelper.MaxColumnNumber); + + ws.Column(XLHelper.MaxColumnNumber).Delete(); + + Assert.AreEqual(3, (ws as XLWorksheet).Internals.CellsCollection.MaxColumnUsed); + } + } + + [Test] + public void MaxRowUsedUpdatedWhenRowDeleted() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var cell1 = ws.Cell("A3"); + var cell2 = ws.Cell(XLHelper.MaxRowNumber, 1); + + ws.Row(XLHelper.MaxRowNumber).Delete(); + + Assert.AreEqual(3, (ws as XLWorksheet).Internals.CellsCollection.MaxRowUsed); + } + } + + [Test] + public void ChangeColumnStyleFirst() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("ColumnFirst"); + + ws.Column(2).Style.Font.SetBold(true); + ws.Row(2).Style.Font.SetItalic(true); + + Assert.IsTrue(ws.Cell("B2").Style.Font.Bold); + Assert.IsTrue(ws.Cell("B2").Style.Font.Italic); + } + } + + [Test] + public void ChangeRowStyleFirst() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("RowFirst"); + + ws.Row(2).Style.Font.SetItalic(true); + ws.Column(2).Style.Font.SetBold(true); + + Assert.IsTrue(ws.Cell("B2").Style.Font.Bold); + Assert.IsTrue(ws.Cell("B2").Style.Font.Italic); + } + } + + [Test] + public void SelectedTabIsActive_WhenInsertBefore() + { + using (var ms = new MemoryStream()) + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + ws1.TabSelected = true; + var ws2 = wb.Worksheets.Add(1); + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws1 = wb.Worksheets.First(); + var ws2 = wb.Worksheets.Last(); + + Assert.IsFalse(ws1.TabActive); + Assert.IsFalse(ws1.TabSelected); + Assert.IsTrue(ws2.TabActive); + Assert.IsTrue(ws2.TabSelected); + } + } + } + + [TestCase("noactive_noselected.xlsx")] + [TestCase("noactive_twoselected.xlsx")] + public void FirstSheetIsActive_WhenNotSpecified(string fileName) + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\NoActiveSheet\"+fileName))) + using (var wb = new XLWorkbook(stream)) + { + Assert.IsTrue(wb.Worksheets.First().TabActive); + Assert.AreEqual(XLWorksheetVisibility.Visible, wb.Worksheets.First().Visibility); + } + } + } +} diff --git a/ClosedXML_Tests/ExcelDocsComparerTests.cs b/ClosedXML_Tests/ExcelDocsComparerTests.cs new file mode 100644 index 0000000..dd32014 --- /dev/null +++ b/ClosedXML_Tests/ExcelDocsComparerTests.cs @@ -0,0 +1,59 @@ +using ClosedXML_Examples; +using NUnit.Framework; +using System.IO; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class ExcelDocsComparerTests + { + [Test] + public void CheckEqual() + { + string left = ExampleHelper.GetTempFilePath("left.xlsx"); + string right = ExampleHelper.GetTempFilePath("right.xlsx"); + try + { + new BasicTable().Create(left); + new BasicTable().Create(right); + Assert.IsTrue(ExcelDocsComparer.Compare(left, right, out string message)); + } + finally + { + if (File.Exists(left)) + { + File.Delete(left); + } + if (File.Exists(right)) + { + File.Delete(right); + } + } + } + + [Test] + public void CheckNonEqual() + { + string left = ExampleHelper.GetTempFilePath("left.xlsx"); + string right = ExampleHelper.GetTempFilePath("right.xlsx"); + try + { + new BasicTable().Create(left); + new HelloWorld().Create(right); + + Assert.IsFalse(ExcelDocsComparer.Compare(left, right, out string message)); + } + finally + { + if (File.Exists(left)) + { + File.Delete(left); + } + if (File.Exists(right)) + { + File.Delete(right); + } + } + } + } +} diff --git a/ClosedXML_Tests/OleDb/OleDbTests.cs b/ClosedXML_Tests/OleDb/OleDbTests.cs new file mode 100644 index 0000000..c20184f --- /dev/null +++ b/ClosedXML_Tests/OleDb/OleDbTests.cs @@ -0,0 +1,120 @@ +#if !APPVEYOR && _NETFRAMEWORK_ +using ClosedXML.Excel; +using ClosedXML_Tests.Utils; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.OleDb; +using System.IO; +using System.Linq; + +namespace ClosedXML_Tests.OleDb +{ + [TestFixture] + public class OleDbTests + { + [Test] + public void TestOleDbValues() + { + using (var tf = new TemporaryFile(CreateTestFile())) + { + Console.Write("Using temporary file\t{0}", tf.Path); + var connectionString = string.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties='Excel 12.0 Xml;HDR=YES;IMEX=1';", tf.Path); + using (var connection = new OleDbConnection(connectionString)) + { + // Install driver from https://www.microsoft.com/en-za/download/details.aspx?id=13255 if required + // Also check that test runner is running under correct architecture: + connection.Open(); + using (var command = new OleDbCommand("select * from [Sheet1$]", connection)) + using (var dataAdapter = new OleDbDataAdapter()) + { + dataAdapter.SelectCommand = command; + var dt = new DataTable(); + dataAdapter.Fill(dt); + + Assert.AreEqual("Base", dt.Columns[0].ColumnName); + Assert.AreEqual("Ref", dt.Columns[1].ColumnName); + + Assert.AreEqual(2, dt.Rows.Count); + + Assert.AreEqual(42, dt.Rows.Cast().First()[0]); + Assert.AreEqual(42, dt.Rows.Cast().First()[1]); + + Assert.AreEqual(41, dt.Rows.Cast().Last()[0]); + Assert.AreEqual(41, dt.Rows.Cast().Last()[1]); + } + + using (var command = new OleDbCommand("select * from [Sheet2$]", connection)) + using (var dataAdapter = new OleDbDataAdapter()) + { + dataAdapter.SelectCommand = command; + var dt = new DataTable(); + dataAdapter.Fill(dt); + + Assert.AreEqual("Ref1", dt.Columns[0].ColumnName); + Assert.AreEqual("Ref2", dt.Columns[1].ColumnName); + Assert.AreEqual("Sum", dt.Columns[2].ColumnName); + Assert.AreEqual("SumRef", dt.Columns[3].ColumnName); + + var expected = new Dictionary() + { + {"Ref1", 42 }, + {"Ref2", 41 }, + {"Sum", 83 }, + {"SumRef", 83 }, + }; + + foreach (var col in dt.Columns.Cast()) + foreach (var row in dt.Rows.Cast()) + { + Assert.AreEqual(expected[col.ColumnName], row[col]); + } + + Assert.AreEqual(2, dt.Rows.Count); + } + + connection.Close(); + } + } + } + + private string CreateTestFile() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "Base"; + ws.Cell("B1").Value = "Ref"; + + ws.Cell("A2").Value = 42; + ws.Cell("A3").Value = 41; + + ws.Cell("B2").FormulaA1 = "=A2"; + ws.Cell("B3").FormulaA1 = "=A3"; + + ws = wb.AddWorksheet("Sheet2"); + ws.Cell("A1").Value = "Ref1"; + ws.Cell("B1").Value = "Ref2"; + ws.Cell("C1").Value = "Sum"; + ws.Cell("D1").Value = "SumRef"; + + ws.Cell("A2").FormulaA1 = "=Sheet1!A2"; + ws.Cell("B2").FormulaA1 = "=Sheet1!A3"; + ws.Cell("C2").FormulaA1 = "=SUM(A2:B2)"; + ws.Cell("D2").FormulaA1 = "=SUM(Sheet1!A2:Sheet1!A3)"; + + ws.Cell("A3").FormulaA1 = "=Sheet1!B2"; + ws.Cell("B3").FormulaA1 = "=Sheet1!B3"; + ws.Cell("C3").FormulaA1 = "=SUM(A3:B3)"; + ws.Cell("D3").FormulaA1 = "=SUM(Sheet1!B2:Sheet1!B3)"; + + var path = Path.ChangeExtension(Path.GetTempFileName(), "xlsx"); + wb.SaveAs(path, true, true); + + return path; + } + } + } +} +#endif diff --git a/ClosedXML_Tests/OpenXMLTests.cs b/ClosedXML_Tests/OpenXMLTests.cs new file mode 100644 index 0000000..c0d0ddf --- /dev/null +++ b/ClosedXML_Tests/OpenXMLTests.cs @@ -0,0 +1,30 @@ +using DocumentFormat.OpenXml.Packaging; +using NUnit.Framework; +using System.IO; + +namespace ClosedXML_Tests +{ + [TestFixture] + public class OpenXMLTests + { + [Test] + [Ignore("Workaround has been included in ClosedXML")] + public static void SetPackagePropertiesEntryToNullWithOpenXml() + { + // Fixed in .NET Standard 2.1 + // See: + // https://github.com/OfficeDev/Open-XML-SDK/issues/235 + // https://github.com/dotnet/corefx/issues/23795 + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\PivotTables\PivotTables.xlsx"))) + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + + using (var document = SpreadsheetDocument.Open(ms, true)) + { + document.PackageProperties.Creator = null; + } + } + } + } +} diff --git a/ClosedXML_Tests/Resource/Examples/AutoFilter/CustomAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/AutoFilter/CustomAutoFilter.xlsx new file mode 100644 index 0000000..156af42 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/AutoFilter/CustomAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/AutoFilter/DateTimeGroupAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/AutoFilter/DateTimeGroupAutoFilter.xlsx new file mode 100644 index 0000000..f251dd5 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/AutoFilter/DateTimeGroupAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/AutoFilter/DynamicAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/AutoFilter/DynamicAutoFilter.xlsx new file mode 100644 index 0000000..ca26674 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/AutoFilter/DynamicAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/AutoFilter/RegularAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/AutoFilter/RegularAutoFilter.xlsx new file mode 100644 index 0000000..08b4b9d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/AutoFilter/RegularAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/AutoFilter/TopBottomAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/AutoFilter/TopBottomAutoFilter.xlsx new file mode 100644 index 0000000..e396345 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/AutoFilter/TopBottomAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Columns/ColumnCells.xlsx b/ClosedXML_Tests/Resource/Examples/Columns/ColumnCells.xlsx new file mode 100644 index 0000000..70c8e96 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Columns/ColumnCells.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Columns/ColumnCollection.xlsx b/ClosedXML_Tests/Resource/Examples/Columns/ColumnCollection.xlsx new file mode 100644 index 0000000..be26aca Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Columns/ColumnCollection.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Columns/ColumnSettings.xlsx b/ClosedXML_Tests/Resource/Examples/Columns/ColumnSettings.xlsx new file mode 100644 index 0000000..ea65770 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Columns/ColumnSettings.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Columns/DeletingColumns.xlsx b/ClosedXML_Tests/Resource/Examples/Columns/DeletingColumns.xlsx new file mode 100644 index 0000000..f430cbf Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Columns/DeletingColumns.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Columns/InsertColumns.xlsx b/ClosedXML_Tests/Resource/Examples/Columns/InsertColumns.xlsx new file mode 100644 index 0000000..49515b1 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Columns/InsertColumns.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Comments/AddingComments.xlsx b/ClosedXML_Tests/Resource/Examples/Comments/AddingComments.xlsx new file mode 100644 index 0000000..dde4446 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Comments/AddingComments.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Comments/EditingComments.xlsx b/ClosedXML_Tests/Resource/Examples/Comments/EditingComments.xlsx new file mode 100644 index 0000000..b0bde50 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Comments/EditingComments.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowHigh.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowHigh.xlsx new file mode 100644 index 0000000..80e81a7 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowHigh.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowMidHigh.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowMidHigh.xlsx new file mode 100644 index 0000000..797dc0b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleLowMidHigh.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleMinimumMaximum.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleMinimumMaximum.xlsx new file mode 100644 index 0000000..e58bfc1 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFColorScaleMinimumMaximum.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx new file mode 100644 index 0000000..8e4adf1 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx new file mode 100644 index 0000000..3ea2dc3 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx new file mode 100644 index 0000000..a8c97a8 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx new file mode 100644 index 0000000..67dd233 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx new file mode 100644 index 0000000..5593abf Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx new file mode 100644 index 0000000..80d08b7 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx new file mode 100644 index 0000000..13f828d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx new file mode 100644 index 0000000..8a25637 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIconSet.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIconSet.xlsx new file mode 100644 index 0000000..0ef2377 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIconSet.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx new file mode 100644 index 0000000..e6fa5ec Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx new file mode 100644 index 0000000..ad1c538 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx new file mode 100644 index 0000000..73bb457 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx new file mode 100644 index 0000000..8861be4 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx new file mode 100644 index 0000000..8656701 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx new file mode 100644 index 0000000..6e23b6a Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx new file mode 100644 index 0000000..d39adb0 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx new file mode 100644 index 0000000..1ca0131 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStartsWith.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStartsWith.xlsx new file mode 100644 index 0000000..cc432eb Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStartsWith.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStopIfTrue.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStopIfTrue.xlsx new file mode 100644 index 0000000..de67d1e Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFStopIfTrue.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Delete/DeleteFewWorksheets.xlsx b/ClosedXML_Tests/Resource/Examples/Delete/DeleteFewWorksheets.xlsx new file mode 100644 index 0000000..df718c9 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Delete/DeleteFewWorksheets.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Delete/RemoveRows.xlsx b/ClosedXML_Tests/Resource/Examples/Delete/RemoveRows.xlsx new file mode 100644 index 0000000..31ef954 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Delete/RemoveRows.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageAnchors.xlsx b/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageAnchors.xlsx new file mode 100644 index 0000000..40e2d5d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageAnchors.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageFormats.xlsx b/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageFormats.xlsx new file mode 100644 index 0000000..ecd11ca Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/ImageHandling/ImageFormats.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Loading/ChangingBasicTable.xlsx b/ClosedXML_Tests/Resource/Examples/Loading/ChangingBasicTable.xlsx new file mode 100644 index 0000000..891cfca Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Loading/ChangingBasicTable.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/AddingDataSet.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/AddingDataSet.xlsx new file mode 100644 index 0000000..91e6370 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/AddingDataSet.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/AddingDataTableAsWorksheet.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/AddingDataTableAsWorksheet.xlsx new file mode 100644 index 0000000..e63a050 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/AddingDataTableAsWorksheet.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContents.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContents.xlsx new file mode 100644 index 0000000..7c902ac Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContents.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContentsWithAutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContentsWithAutoFilter.xlsx new file mode 100644 index 0000000..cbc6465 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/AdjustToContentsWithAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/AutoFilter.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/AutoFilter.xlsx new file mode 100644 index 0000000..da9b21d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/AutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/BasicTable.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/BasicTable.xlsx new file mode 100644 index 0000000..62fbfa5 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/BasicTable.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/BlankCells.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/BlankCells.xlsx new file mode 100644 index 0000000..6c5eccc Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/BlankCells.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx new file mode 100644 index 0000000..e73de6b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/Collections.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/Collections.xlsx new file mode 100644 index 0000000..b562697 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/Collections.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/CopyingRowsAndColumns.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/CopyingRowsAndColumns.xlsx new file mode 100644 index 0000000..0036320 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/CopyingRowsAndColumns.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/CopyingWorksheets.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/CopyingWorksheets.xlsx new file mode 100644 index 0000000..fa88953 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/CopyingWorksheets.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/DataTypes.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/DataTypes.xlsx new file mode 100644 index 0000000..ace0780 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/DataTypes.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/DataTypesUnderDifferentCulture.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/DataTypesUnderDifferentCulture.xlsx new file mode 100644 index 0000000..b68d649 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/DataTypesUnderDifferentCulture.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx new file mode 100644 index 0000000..1b92f80 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/Formulas.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/Formulas.xlsx new file mode 100644 index 0000000..513d951 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/Formulas.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx new file mode 100644 index 0000000..7cb0a2a Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/FreezePanes.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/FreezePanes.xlsx new file mode 100644 index 0000000..5110cc2 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/FreezePanes.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/HideSheets.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/HideSheets.xlsx new file mode 100644 index 0000000..2587ee6 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/HideSheets.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/HideUnhide.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/HideUnhide.xlsx new file mode 100644 index 0000000..f2d07a5 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/HideUnhide.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/Hyperlinks.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/Hyperlinks.xlsx new file mode 100644 index 0000000..a4ab6f6 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/Hyperlinks.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/InsertingData.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/InsertingData.xlsx new file mode 100644 index 0000000..eb57e22 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/InsertingData.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/LambdaExpressions.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/LambdaExpressions.xlsx new file mode 100644 index 0000000..0466c97 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/LambdaExpressions.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/MergeCells.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/MergeCells.xlsx new file mode 100644 index 0000000..edb7c87 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/MergeCells.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/MergeMoves.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/MergeMoves.xlsx new file mode 100644 index 0000000..56fd1cc Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/MergeMoves.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/Outline.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/Outline.xlsx new file mode 100644 index 0000000..9c90baa Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/Outline.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/RightToLeft.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/RightToLeft.xlsx new file mode 100644 index 0000000..1017afd Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/RightToLeft.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/SheetProtection.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/SheetProtection.xlsx new file mode 100644 index 0000000..05bb3df Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/SheetProtection.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/SheetViews.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/SheetViews.xlsx new file mode 100644 index 0000000..fd6a779 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/SheetViews.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/ShiftingFormulas.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/ShiftingFormulas.xlsx new file mode 100644 index 0000000..377c23e Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/ShiftingFormulas.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/ShowCase.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/ShowCase.xlsx new file mode 100644 index 0000000..ba6e051 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/ShowCase.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/TabColors.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/TabColors.xlsx new file mode 100644 index 0000000..5c55455 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/TabColors.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProperties.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProperties.xlsx new file mode 100644 index 0000000..969d71b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProperties.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProtection.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProtection.xlsx new file mode 100644 index 0000000..5dea2d7 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Misc/WorkbookProtection.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/HeaderFooters.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/HeaderFooters.xlsx new file mode 100644 index 0000000..f0d7374 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/HeaderFooters.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/Margins.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/Margins.xlsx new file mode 100644 index 0000000..2f6805b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/Margins.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/Page.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/Page.xlsx new file mode 100644 index 0000000..c55e060 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/Page.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/SheetTab.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/SheetTab.xlsx new file mode 100644 index 0000000..30bfbcc Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/SheetTab.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/Sheets.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/Sheets.xlsx new file mode 100644 index 0000000..2ac5d1d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/Sheets.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PageSetup/TwoPages.xlsx b/ClosedXML_Tests/Resource/Examples/PageSetup/TwoPages.xlsx new file mode 100644 index 0000000..6096e00 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PageSetup/TwoPages.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/PivotTables/PivotTables.xlsx b/ClosedXML_Tests/Resource/Examples/PivotTables/PivotTables.xlsx new file mode 100644 index 0000000..ed2d986 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/PivotTables/PivotTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/AddingRowToTables.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/AddingRowToTables.xlsx new file mode 100644 index 0000000..29ece6a Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/AddingRowToTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/ClearingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/ClearingRanges.xlsx new file mode 100644 index 0000000..9b8602f Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/ClearingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/CopyingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/CopyingRanges.xlsx new file mode 100644 index 0000000..0fe825b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/CopyingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/CurrentRowColumn.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/CurrentRowColumn.xlsx new file mode 100644 index 0000000..44ebfb8 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/CurrentRowColumn.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/DefiningRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/DefiningRanges.xlsx new file mode 100644 index 0000000..b1ac96f Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/DefiningRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/DeletingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/DeletingRanges.xlsx new file mode 100644 index 0000000..1289e35 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/DeletingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingColumns.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingColumns.xlsx new file mode 100644 index 0000000..0b17544 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingColumns.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingRows.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingRows.xlsx new file mode 100644 index 0000000..bfafa65 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/InsertingDeletingRows.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/MultipleRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/MultipleRanges.xlsx new file mode 100644 index 0000000..b0621bc Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/MultipleRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/NamedRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/NamedRanges.xlsx new file mode 100644 index 0000000..a4a86da Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/NamedRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/SelectingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/SelectingRanges.xlsx new file mode 100644 index 0000000..2c89368 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/SelectingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/ShiftingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/ShiftingRanges.xlsx new file mode 100644 index 0000000..7098270 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/ShiftingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/SortExample.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/SortExample.xlsx new file mode 100644 index 0000000..7f8ea39 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/SortExample.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/Sorting.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/Sorting.xlsx new file mode 100644 index 0000000..5227d40 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/Sorting.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRanges.xlsx new file mode 100644 index 0000000..9805bc1 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRangesPlus.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRangesPlus.xlsx new file mode 100644 index 0000000..347f8c5 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/TransposeRangesPlus.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Ranges/WalkingRanges.xlsx b/ClosedXML_Tests/Resource/Examples/Ranges/WalkingRanges.xlsx new file mode 100644 index 0000000..1b82a8f Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Ranges/WalkingRanges.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Rows/InsertRows.xlsx b/ClosedXML_Tests/Resource/Examples/Rows/InsertRows.xlsx new file mode 100644 index 0000000..8dc30a9 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Rows/InsertRows.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Rows/RowCells.xlsx b/ClosedXML_Tests/Resource/Examples/Rows/RowCells.xlsx new file mode 100644 index 0000000..f7a57c8 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Rows/RowCells.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Rows/RowCollection.xlsx b/ClosedXML_Tests/Resource/Examples/Rows/RowCollection.xlsx new file mode 100644 index 0000000..8855354 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Rows/RowCollection.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Rows/RowSettings.xlsx b/ClosedXML_Tests/Resource/Examples/Rows/RowSettings.xlsx new file mode 100644 index 0000000..457bb99 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Rows/RowSettings.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Sparklines/SampleSparklines.xlsx b/ClosedXML_Tests/Resource/Examples/Sparklines/SampleSparklines.xlsx new file mode 100644 index 0000000..8f4b947 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Sparklines/SampleSparklines.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/DefaultStyles.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/DefaultStyles.xlsx new file mode 100644 index 0000000..9e9a109 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/DefaultStyles.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/PurpleWorksheet.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/PurpleWorksheet.xlsx new file mode 100644 index 0000000..ea00b0c Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/PurpleWorksheet.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleAlignment.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleAlignment.xlsx new file mode 100644 index 0000000..ec3232f Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleAlignment.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleBorder.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleBorder.xlsx new file mode 100644 index 0000000..bc01898 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleBorder.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleFill.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleFill.xlsx new file mode 100644 index 0000000..9e19202 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleFill.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleFont.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleFont.xlsx new file mode 100644 index 0000000..af7d4b4 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleFont.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleIncludeQuotePrefix.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleIncludeQuotePrefix.xlsx new file mode 100644 index 0000000..2a86402 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleIncludeQuotePrefix.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleNumberFormat.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleNumberFormat.xlsx new file mode 100644 index 0000000..23184a3 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleNumberFormat.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleRowsColumns.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleRowsColumns.xlsx new file mode 100644 index 0000000..fdadbd3 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleRowsColumns.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/StyleWorksheet.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/StyleWorksheet.xlsx new file mode 100644 index 0000000..c7f0652 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/StyleWorksheet.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/UsingColors.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/UsingColors.xlsx new file mode 100644 index 0000000..2d49b85 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/UsingColors.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/UsingPhonetics.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/UsingPhonetics.xlsx new file mode 100644 index 0000000..b334a6d Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/UsingPhonetics.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Styles/UsingRichText.xlsx b/ClosedXML_Tests/Resource/Examples/Styles/UsingRichText.xlsx new file mode 100644 index 0000000..b83ccde Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Styles/UsingRichText.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Tables/InsertingTables.xlsx b/ClosedXML_Tests/Resource/Examples/Tables/InsertingTables.xlsx new file mode 100644 index 0000000..929b38b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Tables/InsertingTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Tables/ResizingTables.xlsx b/ClosedXML_Tests/Resource/Examples/Tables/ResizingTables.xlsx new file mode 100644 index 0000000..ab6e44b Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Tables/ResizingTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/Examples/Tables/UsingTables.xlsx b/ClosedXML_Tests/Resource/Examples/Tables/UsingTables.xlsx new file mode 100644 index 0000000..70bdab7 Binary files /dev/null and b/ClosedXML_Tests/Resource/Examples/Tables/UsingTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/Images/ImageHandling.png b/ClosedXML_Tests/Resource/Images/ImageHandling.png new file mode 100644 index 0000000..cf23e54 Binary files /dev/null and b/ClosedXML_Tests/Resource/Images/ImageHandling.png differ diff --git a/ClosedXML_Tests/Resource/Other/AutoFilter/AutoFilterWithThousandsSeparator.xlsx b/ClosedXML_Tests/Resource/Other/AutoFilter/AutoFilterWithThousandsSeparator.xlsx new file mode 100644 index 0000000..4d5c238 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/AutoFilter/AutoFilterWithThousandsSeparator.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/inputfile.xlsx new file mode 100644 index 0000000..f2c41f2 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/outputfile.xlsx new file mode 100644 index 0000000..fe5db26 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Charts/PreserveCharts/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Drawings/Comments/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/Drawings/Comments/inputfile.xlsx new file mode 100644 index 0000000..8b42ae4 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Drawings/Comments/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Drawings/NoDrawings/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/Drawings/NoDrawings/outputfile.xlsx new file mode 100644 index 0000000..4cf9bea Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Drawings/NoDrawings/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/ExternalLinks/WorkbookWithExternalLink.xlsx b/ClosedXML_Tests/Resource/Other/ExternalLinks/WorkbookWithExternalLink.xlsx new file mode 100644 index 0000000..0f7532f Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/ExternalLinks/WorkbookWithExternalLink.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/InlineStrings/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/InlineStrings/inputfile.xlsx new file mode 100644 index 0000000..976629d Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/InlineStrings/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/InlineStrings/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/InlineStrings/outputfile.xlsx new file mode 100644 index 0000000..20f73ca Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/InlineStrings/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichText/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichText/inputfile.xlsx new file mode 100644 index 0000000..16ea00d Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichText/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichTextToFormula/output.xlsx b/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichTextToFormula/output.xlsx new file mode 100644 index 0000000..cbe3495 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/InlinedRichText/ChangeRichTextToFormula/output.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_noselected.xlsx b/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_noselected.xlsx new file mode 100644 index 0000000..2b86cec Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_noselected.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_twoselected.xlsx b/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_twoselected.xlsx new file mode 100644 index 0000000..cf3bfe4 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/NoActiveSheet/noactive_twoselected.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/BlankPivotTableField/BlankPivotTableField.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/BlankPivotTableField/BlankPivotTableField.xlsx new file mode 100644 index 0000000..a3b6196 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/BlankPivotTableField/BlankPivotTableField.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/ClearPivotTableRenderedRangeWhenLoading/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/ClearPivotTableRenderedRangeWhenLoading/inputfile.xlsx new file mode 100644 index 0000000..4d0d6aa Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/ClearPivotTableRenderedRangeWhenLoading/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/LongText/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/LongText/outputfile.xlsx new file mode 100644 index 0000000..cc72fc3 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/LongText/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotSubtotalsSource/input.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotSubtotalsSource/input.xlsx new file mode 100644 index 0000000..97c8bd4 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotSubtotalsSource/input.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/inputfile.xlsx new file mode 100644 index 0000000..915c374 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/outputfile.xlsx new file mode 100644 index 0000000..3fbd241 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/PivotTableWithNoneTheme/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/SourceSheetWithWhitespace/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/SourceSheetWithWhitespace/outputfile.xlsx new file mode 100644 index 0000000..6a72c39 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/SourceSheetWithWhitespace/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/input.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/input.xlsx new file mode 100644 index 0000000..7ab44e8 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/input.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/output.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/output.xlsx new file mode 100644 index 0000000..cc39e97 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/TwoPivotTablesWithSingleSource/output.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/inputfile.xlsx new file mode 100644 index 0000000..836701e Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/outputfile.xlsx b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/outputfile.xlsx new file mode 100644 index 0000000..64dae0a Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/PivotTableReferenceFiles/VersioningAttributes/outputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithoutpassword.xlsx b/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithoutpassword.xlsx new file mode 100644 index 0000000..1c41cc1 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithoutpassword.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithpassword.xlsx b/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithpassword.xlsx new file mode 100644 index 0000000..9c79f85 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Protection/protectstructurewithpassword.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/SelectedRanges/InvalidSelectedRange.xlsx b/ClosedXML_Tests/Resource/Other/SelectedRanges/InvalidSelectedRange.xlsx new file mode 100644 index 0000000..f43bd24 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/SelectedRanges/InvalidSelectedRange.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/Sparklines/SparklineThemes/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/Sparklines/SparklineThemes/inputfile.xlsx new file mode 100644 index 0000000..46cc446 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/Sparklines/SparklineThemes/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx new file mode 100644 index 0000000..84c186d Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx new file mode 100644 index 0000000..28a7fcd Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/MergedCellsBorder/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/MergedCellsBorder/inputfile.xlsx new file mode 100644 index 0000000..721e67c Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/MergedCellsBorder/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/RowAndColumnStyles/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/RowAndColumnStyles/inputfile.xlsx new file mode 100644 index 0000000..584241e Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/RowAndColumnStyles/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx new file mode 100644 index 0000000..8e76ffd Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx differ diff --git a/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx new file mode 100644 index 0000000..6d6f1a8 Binary files /dev/null and b/ClosedXML_Tests/Resource/Other/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/AllShapes.xlsx b/ClosedXML_Tests/Resource/TryToLoad/AllShapes.xlsx new file mode 100644 index 0000000..2c5b324 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/AllShapes.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/BaseColumnWidth.xlsx b/ClosedXML_Tests/Resource/TryToLoad/BaseColumnWidth.xlsx new file mode 100644 index 0000000..88a3904 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/BaseColumnWidth.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/CellWithStringDataType.xlsx b/ClosedXML_Tests/Resource/TryToLoad/CellWithStringDataType.xlsx new file mode 100644 index 0000000..eeb2e18 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/CellWithStringDataType.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/CommentAndButton.xlsx b/ClosedXML_Tests/Resource/TryToLoad/CommentAndButton.xlsx new file mode 100644 index 0000000..3c662ea Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/CommentAndButton.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/CommentsWithIndexedColor81.xlsx b/ClosedXML_Tests/Resource/TryToLoad/CommentsWithIndexedColor81.xlsx new file mode 100644 index 0000000..715c073 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/CommentsWithIndexedColor81.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/CopyRowContents.xlsx b/ClosedXML_Tests/Resource/TryToLoad/CopyRowContents.xlsx new file mode 100644 index 0000000..ce9e7b1 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/CopyRowContents.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/Date1904System.xlsx b/ClosedXML_Tests/Resource/TryToLoad/Date1904System.xlsx new file mode 100644 index 0000000..8a7da48 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/Date1904System.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/DrawingSizeInPx.xlsx b/ClosedXML_Tests/Resource/TryToLoad/DrawingSizeInPx.xlsx new file mode 100644 index 0000000..34ddf19 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/DrawingSizeInPx.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/DuplicateImageNames.xlsx b/ClosedXML_Tests/Resource/TryToLoad/DuplicateImageNames.xlsx new file mode 100644 index 0000000..29df50b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/DuplicateImageNames.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/EmptyCellValue.xlsx b/ClosedXML_Tests/Resource/TryToLoad/EmptyCellValue.xlsx new file mode 100644 index 0000000..f90b0fc Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/EmptyCellValue.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/EmptyFill.xlsx b/ClosedXML_Tests/Resource/TryToLoad/EmptyFill.xlsx new file mode 100644 index 0000000..4a250f9 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/EmptyFill.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/EmptyNumberFormatId.xlsx b/ClosedXML_Tests/Resource/TryToLoad/EmptyNumberFormatId.xlsx new file mode 100644 index 0000000..80ba48c Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/EmptyNumberFormatId.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/EmptyTable.xlsx b/ClosedXML_Tests/Resource/TryToLoad/EmptyTable.xlsx new file mode 100644 index 0000000..e902376 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/EmptyTable.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/EscapedApostrophe.xlsx b/ClosedXML_Tests/Resource/TryToLoad/EscapedApostrophe.xlsx new file mode 100644 index 0000000..29d15ff Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/EscapedApostrophe.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/ExcelProducedWorkbookWithImages.xlsx b/ClosedXML_Tests/Resource/TryToLoad/ExcelProducedWorkbookWithImages.xlsx new file mode 100644 index 0000000..62a8107 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/ExcelProducedWorkbookWithImages.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/FileWithButton.xlsm b/ClosedXML_Tests/Resource/TryToLoad/FileWithButton.xlsm new file mode 100644 index 0000000..da6cfde Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/FileWithButton.xlsm differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/FileWithDefaultStyleNameNotInEnglish.xlsx b/ClosedXML_Tests/Resource/TryToLoad/FileWithDefaultStyleNameNotInEnglish.xlsx new file mode 100644 index 0000000..e94bc6a Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/FileWithDefaultStyleNameNotInEnglish.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/FileWithMismatchSheetIdAndRelId.xlsx b/ClosedXML_Tests/Resource/TryToLoad/FileWithMismatchSheetIdAndRelId.xlsx new file mode 100644 index 0000000..23c9a88 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/FileWithMismatchSheetIdAndRelId.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/FilterPrivacyEnabledWorkbook.xlsx b/ClosedXML_Tests/Resource/TryToLoad/FilterPrivacyEnabledWorkbook.xlsx new file mode 100644 index 0000000..97007e6 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/FilterPrivacyEnabledWorkbook.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/HorizontalAlignment.xlsx b/ClosedXML_Tests/Resource/TryToLoad/HorizontalAlignment.xlsx new file mode 100644 index 0000000..319da43 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/HorizontalAlignment.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/InvalidFirstPageNumber.xlsx b/ClosedXML_Tests/Resource/TryToLoad/InvalidFirstPageNumber.xlsx new file mode 100644 index 0000000..0f0ac6d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/InvalidFirstPageNumber.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintArea.xlsx b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintArea.xlsx new file mode 100644 index 0000000..361a7de Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintArea.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles.xlsx b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles.xlsx new file mode 100644 index 0000000..86bef09 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles2.xlsx b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles2.xlsx new file mode 100644 index 0000000..f9e609d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/InvalidPrintTitles2.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/pass/CVE-2016-0122-1.xlsm b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/pass/CVE-2016-0122-1.xlsm new file mode 100644 index 0000000..a86f92f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/pass/CVE-2016-0122-1.xlsm differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/tdf111974.xlsm b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/tdf111974.xlsm new file mode 100644 index 0000000..bb6ac6d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/tdf111974.xlsm differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/vba-user-function.xlsm b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/vba-user-function.xlsm new file mode 100644 index 0000000..a43a8eb Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsm/vba-user-function.xlsm differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ProtecteSheet1234Pass.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ProtecteSheet1234Pass.xlsx new file mode 100644 index 0000000..509ebe2 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ProtecteSheet1234Pass.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/TextColor.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/TextColor.xlsx new file mode 100644 index 0000000..4de9387 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/TextColor.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/activex_checkbox.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/activex_checkbox.xlsx new file mode 100644 index 0000000..b37bf59 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/activex_checkbox.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/autofilter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/autofilter.xlsx new file mode 100644 index 0000000..a8287c1 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/autofilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bnc762542.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bnc762542.xlsx new file mode 100644 index 0000000..1cda1fd Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bnc762542.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bug-fixes.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bug-fixes.xlsx new file mode 100644 index 0000000..e0e179f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/bug-fixes.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/built-in_ranges.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/built-in_ranges.xlsx new file mode 100644 index 0000000..b18a486 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/built-in_ranges.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ceiling-floor.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ceiling-floor.xlsx new file mode 100644 index 0000000..a98a355 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ceiling-floor.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-anchored-hidden-shapes.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-anchored-hidden-shapes.xlsx new file mode 100644 index 0000000..7e79cc3 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-anchored-hidden-shapes.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-borders.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-borders.xlsx new file mode 100644 index 0000000..ed9c2e2 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-borders.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-value.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-value.xlsx new file mode 100644 index 0000000..70c8d0d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cell-value.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/check-boolean.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/check-boolean.xlsx new file mode 100644 index 0000000..a7643d1 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/check-boolean.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale.xlsx new file mode 100644 index 0000000..c70a207 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale_num_with_ref.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale_num_with_ref.xlsx new file mode 100644 index 0000000..931fe7a Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/colorscale_num_with_ref.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style-autofilter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style-autofilter.xlsx new file mode 100644 index 0000000..67b2dd4 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style-autofilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style.xlsx new file mode 100644 index 0000000..57afe3e Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column-style.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column_style.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column_style.xlsx new file mode 100644 index 0000000..d228443 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/column_style.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/complex_icon_set.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/complex_icon_set.xlsx new file mode 100644 index 0000000..32f1e52 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/complex_icon_set.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_formula_listener.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_formula_listener.xlsx new file mode 100644 index 0000000..97d7845 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_formula_listener.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color2.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color2.xlsx new file mode 100644 index 0000000..61c0d58 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color2.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color3.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color3.xlsx new file mode 100644 index 0000000..2e97684 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_format_theme_color3.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_parent.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_parent.xlsx new file mode 100644 index 0000000..90f9007 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/cond_parent.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/condformat_theme_color.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/condformat_theme_color.xlsx new file mode 100644 index 0000000..8bab237 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/condformat_theme_color.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/customxml.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/customxml.xlsx new file mode 100644 index 0000000..53619ae Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/customxml.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/multi-table.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/multi-table.xlsx new file mode 100644 index 0000000..c2bf488 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/multi-table.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/one-variable.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/one-variable.xlsx new file mode 100644 index 0000000..7ff098b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/data-table/one-variable.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/databar.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/databar.xlsx new file mode 100644 index 0000000..bf43cd6 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/databar.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/database.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/database.xlsx new file mode 100644 index 0000000..646b79d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/database.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/different-column-width-excel2010.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/different-column-width-excel2010.xlsx new file mode 100644 index 0000000..93f133f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/different-column-width-excel2010.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty-noconf.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty-noconf.xlsx new file mode 100644 index 0000000..21f80b4 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty-noconf.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty.xlsx new file mode 100644 index 0000000..c186485 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/empty.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/enhanced-protection.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/enhanced-protection.xlsx new file mode 100644 index 0000000..d9225a2 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/enhanced-protection.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/external-refs.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/external-refs.xlsx new file mode 100644 index 0000000..805e36c Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/external-refs.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/fontSize.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/fontSize.xlsx new file mode 100644 index 0000000..8811df7 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/fontSize.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/formats.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/formats.xlsx new file mode 100644 index 0000000..c75bbc7 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/formats.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/functions-excel-2010.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/functions-excel-2010.xlsx new file mode 100644 index 0000000..152fbbc Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/functions-excel-2010.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hiddenShape.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hiddenShape.xlsx new file mode 100644 index 0000000..3f38909 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hiddenShape.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hidden_sheets.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hidden_sheets.xlsx new file mode 100644 index 0000000..771de7f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hidden_sheets.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlink.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlink.xlsx new file mode 100644 index 0000000..059e649 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlink.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlinks.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlinks.xlsx new file mode 100644 index 0000000..5faea7d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/hyperlinks.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/matrix-multiplication.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/matrix-multiplication.xlsx new file mode 100644 index 0000000..f79d6b6 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/matrix-multiplication.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/miscrowheights.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/miscrowheights.xlsx new file mode 100644 index 0000000..dbdbc13 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/miscrowheights.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/named-ranges-global.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/named-ranges-global.xlsx new file mode 100644 index 0000000..fe90d6d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/named-ranges-global.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test.xlsx new file mode 100644 index 0000000..0750188 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test_export.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test_export.xlsx new file mode 100644 index 0000000..0750188 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/new_cond_format_test_export.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/page_scale.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/page_scale.xlsx new file mode 100644 index 0000000..070f218 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/page_scale.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/many-fields-in-cache.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/many-fields-in-cache.xlsx new file mode 100644 index 0000000..2b39849 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/many-fields-in-cache.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-dategroup.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-dategroup.xlsx new file mode 100644 index 0000000..60cbd66 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-dategroup.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-group-field.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-group-field.xlsx new file mode 100644 index 0000000..2bc5cdd Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-group-field.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-nested-dategroup.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-nested-dategroup.xlsx new file mode 100644 index 0000000..4dfb55d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-nested-dategroup.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-numgroup.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-numgroup.xlsx new file mode 100644 index 0000000..36f36ef Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/shared-numgroup.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/two-data-fields.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/two-data-fields.xlsx new file mode 100644 index 0000000..793af59 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/two-data-fields.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/with-strings-integers-and-dates.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/with-strings-integers-and-dates.xlsx new file mode 100644 index 0000000..882ed1a Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot-table/with-strings-integers-and-dates.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot_table_first_header_row.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot_table_first_header_row.xlsx new file mode 100644 index 0000000..664637d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivot_table_first_header_row.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_bool_field_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_bool_field_filter.xlsx new file mode 100644 index 0000000..8fcdb1b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_bool_field_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_date_field_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_date_field_filter.xlsx new file mode 100644 index 0000000..316f201 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_date_field_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_double_field_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_double_field_filter.xlsx new file mode 100644 index 0000000..f2d3597 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_double_field_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_duplicated_member_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_duplicated_member_filter.xlsx new file mode 100644 index 0000000..21291a4 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_duplicated_member_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_error_item_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_error_item_filter.xlsx new file mode 100644 index 0000000..19d9b3c Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_error_item_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_outline_mode.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_outline_mode.xlsx new file mode 100644 index 0000000..6b575c0 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_outline_mode.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_rowcolpage_field_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_rowcolpage_field_filter.xlsx new file mode 100644 index 0000000..4aaa327 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_rowcolpage_field_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_string_field_filter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_string_field_filter.xlsx new file mode 100644 index 0000000..3bb6a77 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_string_field_filter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_tabular_mode.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_tabular_mode.xlsx new file mode 100644 index 0000000..0bcfe17 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/pivottable_tabular_mode.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve-whitespace.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve-whitespace.xlsx new file mode 100644 index 0000000..c9f9794 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve-whitespace.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve_space.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve_space.xlsx new file mode 100644 index 0000000..401a908 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/preserve_space.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ref_string.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ref_string.xlsx new file mode 100644 index 0000000..08076cb Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/ref_string.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/row-index-1-based.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/row-index-1-based.xlsx new file mode 100644 index 0000000..1f60c3b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/row-index-1-based.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/3d-reference.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/3d-reference.xlsx new file mode 100644 index 0000000..2fb95b5 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/3d-reference.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/basic.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/basic.xlsx new file mode 100644 index 0000000..64d0501 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/basic.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/text-results.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/text-results.xlsx new file mode 100644 index 0000000..3f59bc3 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/shared-formula/text-results.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-names.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-names.xlsx new file mode 100644 index 0000000..97e0488 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-names.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-tab-color.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-tab-color.xlsx new file mode 100644 index 0000000..896bc3b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/sheet-tab-color.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/singlecontrol.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/singlecontrol.xlsx new file mode 100644 index 0000000..513c05b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/singlecontrol.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/strike-through.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/strike-through.xlsx new file mode 100644 index 0000000..ca56768 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/strike-through.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf100709.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf100709.xlsx new file mode 100644 index 0000000..87698bb Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf100709.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310-2.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310-2.xlsx new file mode 100644 index 0000000..dc5e9ac Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310-2.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310.xlsx new file mode 100644 index 0000000..95570d3 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf104310.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf105840_allRowsHidden.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf105840_allRowsHidden.xlsx new file mode 100644 index 0000000..e2c22cd Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf105840_allRowsHidden.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf110440.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf110440.xlsx new file mode 100644 index 0000000..da849d0 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf110440.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf111876.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf111876.xlsx new file mode 100644 index 0000000..a12f14c Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf111876.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf112106.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf112106.xlsx new file mode 100644 index 0000000..37203d1 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf112106.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf81939.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf81939.xlsx new file mode 100644 index 0000000..6614a1e Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf81939.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf83672.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf83672.xlsx new file mode 100644 index 0000000..0469682 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf83672.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf89139_pivot_table.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf89139_pivot_table.xlsx new file mode 100644 index 0000000..83d5b3d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf89139_pivot_table.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf97598_scenarios.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf97598_scenarios.xlsx new file mode 100644 index 0000000..ba3f43f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/tdf97598_scenarios.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CharKerningSpace.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CharKerningSpace.xlsx new file mode 100644 index 0000000..64b6856 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CharKerningSpace.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CondensedCharacterSpace.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CondensedCharacterSpace.xlsx new file mode 100644 index 0000000..fb46fef Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-CondensedCharacterSpace.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-hyperlink.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-hyperlink.xlsx new file mode 100644 index 0000000..bbebca9 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/textbox-hyperlink.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/track-changes/simple-cell-changes.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/track-changes/simple-cell-changes.xlsx new file mode 100644 index 0000000..29e8027 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/track-changes/simple-cell-changes.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/underlineColor.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/underlineColor.xlsx new file mode 100644 index 0000000..124391b Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/underlineColor.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content-strict.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content-strict.xlsx new file mode 100644 index 0000000..32beb4d Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content-strict.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content.xlsx new file mode 100644 index 0000000..687a583 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/universal-content.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/writingMode.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/writingMode.xlsx new file mode 100644 index 0000000..3cf9512 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/writingMode.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/xf_default_values.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/xf_default_values.xlsx new file mode 100644 index 0000000..79aaea7 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LO/xlsx/xf_default_values.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LibreOfficeFileWithDates.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LibreOfficeFileWithDates.xlsx new file mode 100644 index 0000000..9556636 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LibreOfficeFileWithDates.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LoadFileWithCustomSheetViews.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LoadFileWithCustomSheetViews.xlsx new file mode 100644 index 0000000..e6d28c1 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LoadFileWithCustomSheetViews.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LoadImageWithoutTransform2D.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LoadImageWithoutTransform2D.xlsx new file mode 100644 index 0000000..f02d8c0 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LoadImageWithoutTransform2D.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LoadPivotTables.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LoadPivotTables.xlsx new file mode 100644 index 0000000..fb63015 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LoadPivotTables.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/LoadSheetsWithCommas.xlsx b/ClosedXML_Tests/Resource/TryToLoad/LoadSheetsWithCommas.xlsx new file mode 100644 index 0000000..d86bd21 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/LoadSheetsWithCommas.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/NamedRangeWithInvalidCharacter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/NamedRangeWithInvalidCharacter.xlsx new file mode 100644 index 0000000..16fe87f Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/NamedRangeWithInvalidCharacter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/NoReferencesOnRowsOrCells.xlsx b/ClosedXML_Tests/Resource/TryToLoad/NoReferencesOnRowsOrCells.xlsx new file mode 100644 index 0000000..10ab415 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/NoReferencesOnRowsOrCells.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithMissingReference.xlsx b/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithMissingReference.xlsx new file mode 100644 index 0000000..66c949c Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithMissingReference.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithTableSource.xlsx b/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithTableSource.xlsx new file mode 100644 index 0000000..2a866e7 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/PivotTableWithTableSource.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/PrintAreaRefersToExternalWorksheet.xlsx b/ClosedXML_Tests/Resource/TryToLoad/PrintAreaRefersToExternalWorksheet.xlsx new file mode 100644 index 0000000..4708305 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/PrintAreaRefersToExternalWorksheet.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/SHA512PasswordProtection.xlsx b/ClosedXML_Tests/Resource/TryToLoad/SHA512PasswordProtection.xlsx new file mode 100644 index 0000000..61bf9c3 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/SHA512PasswordProtection.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/SingleRowAutoFilter.xlsx b/ClosedXML_Tests/Resource/TryToLoad/SingleRowAutoFilter.xlsx new file mode 100644 index 0000000..f690040 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/SingleRowAutoFilter.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/TableHeadersWithLineBreaks.xlsx b/ClosedXML_Tests/Resource/TryToLoad/TableHeadersWithLineBreaks.xlsx new file mode 100644 index 0000000..8e7dbdc Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/TableHeadersWithLineBreaks.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/TableWithCustomTheme.xlsx b/ClosedXML_Tests/Resource/TryToLoad/TableWithCustomTheme.xlsx new file mode 100644 index 0000000..ecca2ee Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/TableWithCustomTheme.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/TableWithNameNull.xlsx b/ClosedXML_Tests/Resource/TryToLoad/TableWithNameNull.xlsx new file mode 100644 index 0000000..0a5a8de Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/TableWithNameNull.xlsx differ diff --git a/ClosedXML_Tests/Resource/TryToLoad/TemplateWithTableSourcePivotTables.xlsx b/ClosedXML_Tests/Resource/TryToLoad/TemplateWithTableSourcePivotTables.xlsx new file mode 100644 index 0000000..d2d5a95 Binary files /dev/null and b/ClosedXML_Tests/Resource/TryToLoad/TemplateWithTableSourcePivotTables.xlsx differ diff --git a/ClosedXML_Tests/TestHelper.cs b/ClosedXML_Tests/TestHelper.cs new file mode 100644 index 0000000..f9777ff --- /dev/null +++ b/ClosedXML_Tests/TestHelper.cs @@ -0,0 +1,164 @@ +using ClosedXML.Excel; +using ClosedXML_Examples; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Path = System.IO.Path; + +namespace ClosedXML_Tests +{ + internal static class TestHelper + { + public static string CurrencySymbol + { + get { return Thread.CurrentThread.CurrentCulture.NumberFormat.CurrencySymbol; } + } + + //Note: Run example tests parameters + public static string TestsOutputDirectory + { + get + { + return Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "Generated"); + } + } + + public const string ActualTestResultPostFix = ""; + public static readonly string ExampleTestsOutputDirectory = Path.Combine(TestsOutputDirectory, "Examples"); + + private const bool CompareWithResources = true; + + private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(".Resource."); + + public static void SaveWorkbook(XLWorkbook workbook, params string[] fileNameParts) + { + workbook.SaveAs(Path.Combine(new string[] { TestsOutputDirectory }.Concat(fileNameParts).ToArray()), true); + } + + // Because different fonts are installed on Unix, + // the columns widths after AdjustToContents() will + // cause the tests to fail. + // Therefore we ignore the width attribute when running on Unix + public static bool StripColumnWidths { get { return IsRunningOnUnix; } } + + public static bool IsRunningOnUnix + { + get + { + int p = (int)Environment.OSVersion.Platform; + return ((p == 4) || (p == 6) || (p == 128)); + } + } + + public static void RunTestExample(string filePartName, bool evaluateFormulae = false) + where T : IXLExample, new() + { + // Make sure tests run on a deterministic culture + Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + + var example = new T(); + string[] pathParts = filePartName.Split(new char[] { '\\' }); + string filePath1 = Path.Combine(new List() { ExampleTestsOutputDirectory }.Concat(pathParts).ToArray()); + + var extension = Path.GetExtension(filePath1); + var directory = Path.GetDirectoryName(filePath1); + + var fileName = Path.GetFileNameWithoutExtension(filePath1); + fileName += ActualTestResultPostFix; + fileName = Path.ChangeExtension(fileName, extension); + + filePath1 = Path.Combine(directory, "z" + fileName); + var filePath2 = Path.Combine(directory, fileName); + + //Run test + example.Create(filePath1); + using (var wb = new XLWorkbook(filePath1)) + wb.SaveAs(filePath2, validate: true, evaluateFormulae); + + // Also load from template and save it again - but not necessary to test against reference file + // We're just testing that it can save. + using (var ms = new MemoryStream()) + using (var wb = XLWorkbook.OpenFromTemplate(filePath1)) + wb.SaveAs(ms, validate: true, evaluateFormulae); + + if (CompareWithResources) + { + string resourcePath = "Examples." + filePartName.Replace('\\', '.').TrimStart('.'); + using (var streamExpected = _extractor.ReadFileFromResourceToStream(resourcePath)) + using (var streamActual = File.OpenRead(filePath2)) + { + var success = ExcelDocsComparer.Compare(streamActual, streamExpected, out string message); + var formattedMessage = + String.Format( + "Actual file '{0}' is different than the expected file '{1}'. The difference is: '{2}'", + filePath2, resourcePath, message); + + Assert.IsTrue(success, formattedMessage); + } + } + } + + public static void CreateAndCompare(Func workbookGenerator, string referenceResource, bool evaluateFormulae = false) + { + Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + + string[] pathParts = referenceResource.Split(new char[] { '\\' }); + string filePath1 = Path.Combine(new List() { TestsOutputDirectory }.Concat(pathParts).ToArray()); + + var extension = Path.GetExtension(filePath1); + var directory = Path.GetDirectoryName(filePath1); + + var fileName = Path.GetFileNameWithoutExtension(filePath1); + fileName += ActualTestResultPostFix; + fileName = Path.ChangeExtension(fileName, extension); + + var filePath2 = Path.Combine(directory, fileName); + + using (var wb = workbookGenerator.Invoke()) + wb.SaveAs(filePath2, true, evaluateFormulae); + + if (CompareWithResources) + { + string resourcePath = referenceResource.Replace('\\', '.').TrimStart('.'); + using (var streamExpected = _extractor.ReadFileFromResourceToStream(resourcePath)) + using (var streamActual = File.OpenRead(filePath2)) + { + var success = ExcelDocsComparer.Compare(streamActual, streamExpected, out string message); + var formattedMessage = + String.Format( + "Actual file '{0}' is different than the expected file '{1}'. The difference is: '{2}'", + filePath2, resourcePath, message); + + Assert.IsTrue(success, formattedMessage); + } + } + } + + public static string GetResourcePath(string filePartName) + { + return filePartName.Replace('\\', '.').TrimStart('.'); + } + + public static Stream GetStreamFromResource(string resourcePath) + { + return _extractor.ReadFileFromResourceToStream(resourcePath); + } + + public static void LoadFile(string filePartName) + { + IXLWorkbook wb; + using (var stream = GetStreamFromResource(GetResourcePath(filePartName))) + { + Assert.DoesNotThrow(() => wb = new XLWorkbook(stream), "Unable to load resource {0}", filePartName); + } + } + + public static IEnumerable ListResourceFiles(Func predicate = null) + { + return _extractor.GetFileNames(predicate); + } + } +} diff --git a/ClosedXML_Tests/Utils/ExcelDocsComparer.cs b/ClosedXML_Tests/Utils/ExcelDocsComparer.cs new file mode 100644 index 0000000..9c35602 --- /dev/null +++ b/ClosedXML_Tests/Utils/ExcelDocsComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.IO.Packaging; + +namespace ClosedXML_Tests +{ + internal static class ExcelDocsComparer + { + public static bool Compare(string left, string right, out string message) + { + using (FileStream leftStream = File.OpenRead(left)) + using (FileStream rightStream = File.OpenRead(right)) + { + return Compare(leftStream, rightStream, out message); + } + } + + public static bool Compare(Stream left, Stream right, out string message) + { + using (Package leftPackage = Package.Open(left, FileMode.Open, FileAccess.Read)) + using (Package rightPackage = Package.Open(right, FileMode.Open, FileAccess.Read)) + { + return PackageHelper.Compare(leftPackage, rightPackage, false, ExcludeMethod, out message); + } + } + + private static bool ExcludeMethod(Uri uri) + { + //Exclude service data + if (uri.OriginalString.EndsWith(".rels") || + uri.OriginalString.EndsWith(".psmdcp")) + { + return true; + } + return false; + } + } +} diff --git a/ClosedXML_Tests/Utils/PackageHelper.cs b/ClosedXML_Tests/Utils/PackageHelper.cs new file mode 100644 index 0000000..1e642a6 --- /dev/null +++ b/ClosedXML_Tests/Utils/PackageHelper.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Xml.Serialization; + +namespace ClosedXML_Tests +{ + public static class PackageHelper + { + public static void WriteXmlPart(Package package, Uri uri, object content, XmlSerializer serializer) + { + if (package.PartExists(uri)) + { + package.DeletePart(uri); + } + PackagePart part = package.CreatePart(uri, MediaTypeNames.Text.Xml, CompressionOption.Fast); + using (Stream stream = part.GetStream()) + { + serializer.Serialize(stream, content); + } + } + + public static object ReadXmlPart(Package package, Uri uri, XmlSerializer serializer) + { + if (!package.PartExists(uri)) + { + throw new ApplicationException(string.Format("Package part '{0}' doesn't exists!", uri.OriginalString)); + } + PackagePart part = package.GetPart(uri); + using (Stream stream = part.GetStream()) + { + return serializer.Deserialize(stream); + } + } + + public static void WriteBinaryPart(Package package, Uri uri, Stream content) + { + if (package.PartExists(uri)) + { + package.DeletePart(uri); + } + PackagePart part = package.CreatePart(uri, MediaTypeNames.Application.Octet, CompressionOption.Fast); + using (Stream stream = part.GetStream()) + { + StreamHelper.StreamToStreamAppend(content, stream); + } + } + + /// + /// Returns part's stream + /// + /// + /// + /// + public static Stream ReadBinaryPart(Package package, Uri uri) + { + if (!package.PartExists(uri)) + { + throw new ApplicationException("Package part doesn't exists!"); + } + PackagePart part = package.GetPart(uri); + return part.GetStream(); + } + + public static void CopyPart(Uri uri, Package source, Package dest) + { + CopyPart(uri, source, dest, true); + } + + public static void CopyPart(Uri uri, Package source, Package dest, bool overwrite) + { + #region Check + + if (ReferenceEquals(uri, null)) + { + throw new ArgumentNullException("uri"); + } + if (ReferenceEquals(source, null)) + { + throw new ArgumentNullException("source"); + } + if (ReferenceEquals(dest, null)) + { + throw new ArgumentNullException("dest"); + } + + #endregion Check + + if (dest.PartExists(uri)) + { + if (!overwrite) + { + throw new ArgumentException("Specified part already exists", "uri"); + } + dest.DeletePart(uri); + } + + PackagePart sourcePart = source.GetPart(uri); + PackagePart destPart = dest.CreatePart(uri, sourcePart.ContentType, sourcePart.CompressionOption); + + using (Stream sourceStream = sourcePart.GetStream()) + { + using (Stream destStream = destPart.GetStream()) + { + StreamHelper.StreamToStreamAppend(sourceStream, destStream); + } + } + } + + public static void WritePart(Package package, PackagePartDescriptor descriptor, T content, + Action serializeAction) + { + #region Check + + if (ReferenceEquals(package, null)) + { + throw new ArgumentNullException("package"); + } + if (ReferenceEquals(descriptor, null)) + { + throw new ArgumentNullException("descriptor"); + } + if (ReferenceEquals(serializeAction, null)) + { + throw new ArgumentNullException("serializeAction"); + } + + #endregion Check + + if (package.PartExists(descriptor.Uri)) + { + package.DeletePart(descriptor.Uri); + } + PackagePart part = package.CreatePart(descriptor.Uri, descriptor.ContentType, descriptor.CompressOption); + using (Stream stream = part.GetStream()) + { + serializeAction(stream, content); + } + } + + public static void WritePart(Package package, PackagePartDescriptor descriptor, Action serializeAction) + { + #region Check + + if (ReferenceEquals(package, null)) + { + throw new ArgumentNullException("package"); + } + if (ReferenceEquals(descriptor, null)) + { + throw new ArgumentNullException("descriptor"); + } + if (ReferenceEquals(serializeAction, null)) + { + throw new ArgumentNullException("serializeAction"); + } + + #endregion Check + + if (package.PartExists(descriptor.Uri)) + { + package.DeletePart(descriptor.Uri); + } + PackagePart part = package.CreatePart(descriptor.Uri, descriptor.ContentType, descriptor.CompressOption); + using (Stream stream = part.GetStream()) + { + serializeAction(stream); + } + } + + public static T ReadPart(Package package, Uri uri, Func deserializeFunc) + { + #region Check + + if (ReferenceEquals(package, null)) + { + throw new ArgumentNullException("package"); + } + if (ReferenceEquals(uri, null)) + { + throw new ArgumentNullException("uri"); + } + if (ReferenceEquals(deserializeFunc, null)) + { + throw new ArgumentNullException("deserializeFunc"); + } + + #endregion Check + + if (!package.PartExists(uri)) + { + throw new ApplicationException(string.Format("Package part '{0}' doesn't exists!", uri.OriginalString)); + } + PackagePart part = package.GetPart(uri); + using (Stream stream = part.GetStream()) + { + return deserializeFunc(stream); + } + } + + public static void ReadPart(Package package, Uri uri, Action deserializeAction) + { + #region Check + + if (ReferenceEquals(package, null)) + { + throw new ArgumentNullException("package"); + } + if (ReferenceEquals(uri, null)) + { + throw new ArgumentNullException("uri"); + } + if (ReferenceEquals(deserializeAction, null)) + { + throw new ArgumentNullException("deserializeAction"); + } + + #endregion Check + + if (!package.PartExists(uri)) + { + throw new ApplicationException(string.Format("Package part '{0}' doesn't exists!", uri.OriginalString)); + } + PackagePart part = package.GetPart(uri); + using (Stream stream = part.GetStream()) + { + deserializeAction(stream); + } + } + + public static bool TryReadPart(Package package, Uri uri, Action deserializeAction) + { + #region Check + + if (ReferenceEquals(package, null)) + { + throw new ArgumentNullException("package"); + } + if (ReferenceEquals(uri, null)) + { + throw new ArgumentNullException("uri"); + } + if (ReferenceEquals(deserializeAction, null)) + { + throw new ArgumentNullException("deserializeAction"); + } + + #endregion Check + + if (!package.PartExists(uri)) + { + return false; + } + PackagePart part = package.GetPart(uri); + using (Stream stream = part.GetStream()) + { + deserializeAction(stream); + } + return true; + } + + /// + /// Compare to packages by parts like streams + /// + /// + /// + /// + /// + /// + /// + public static bool Compare(Package left, Package right, bool compareToFirstDifference, out string message) + { + return Compare(left, right, compareToFirstDifference, null, out message); + } + + /// + /// Compare to packages by parts like streams + /// + /// + /// + /// + /// + /// + /// + public static bool Compare(Package left, Package right, bool compareToFirstDifference, + Func excludeMethod, out string message) + { + #region Check + + if (left == null) + { + throw new ArgumentNullException("left"); + } + if (right == null) + { + throw new ArgumentNullException("right"); + } + + #endregion Check + + excludeMethod = excludeMethod ?? (uri => false); + PackagePartCollection leftParts = left.GetParts(); + PackagePartCollection rightParts = right.GetParts(); + + var pairs = new Dictionary(); + foreach (PackagePart part in leftParts) + { + if (excludeMethod(part.Uri)) + { + continue; + } + pairs.Add(part.Uri, new PartPair(part.Uri, CompareStatus.OnlyOnLeft)); + } + foreach (PackagePart part in rightParts) + { + if (excludeMethod(part.Uri)) + { + continue; + } + if (pairs.TryGetValue(part.Uri, out PartPair pair)) + { + pair.Status = CompareStatus.Equal; + } + else + { + pairs.Add(part.Uri, new PartPair(part.Uri, CompareStatus.OnlyOnRight)); + } + } + + if (compareToFirstDifference && pairs.Any(pair => pair.Value.Status != CompareStatus.Equal)) + { + goto EXIT; + } + + foreach (PartPair pair in pairs.Values) + { + if (pair.Status != CompareStatus.Equal) + { + continue; + } + var leftPart = left.GetPart(pair.Uri); + var rightPart = right.GetPart(pair.Uri); + using (Stream leftPackagePartStream = leftPart.GetStream(FileMode.Open, FileAccess.Read)) + using (Stream rightPackagePartStream = rightPart.GetStream(FileMode.Open, FileAccess.Read)) + using (var leftMemoryStream = new MemoryStream()) + using (var rightMemoryStream = new MemoryStream()) + { + leftPackagePartStream.CopyTo(leftMemoryStream); + rightPackagePartStream.CopyTo(rightMemoryStream); + + leftMemoryStream.Seek(0, SeekOrigin.Begin); + rightMemoryStream.Seek(0, SeekOrigin.Begin); + + bool stripColumnWidthsFromSheet = TestHelper.StripColumnWidths && + leftPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" && + rightPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"; + + var tuple1 = new Tuple(pair.Uri, leftMemoryStream); + var tuple2 = new Tuple(pair.Uri, rightMemoryStream); + + if (!StreamHelper.Compare(tuple1, tuple2, stripColumnWidthsFromSheet)) + { + pair.Status = CompareStatus.NonEqual; + if (compareToFirstDifference) + { + goto EXIT; + } + } + } + } + + EXIT: + List sortedPairs = pairs.Values.ToList(); + sortedPairs.Sort((one, other) => one.Uri.OriginalString.CompareTo(other.Uri.OriginalString)); + var sbuilder = new StringBuilder(); + foreach (PartPair pair in sortedPairs) + { + if (pair.Status == CompareStatus.Equal) + { + continue; + } + sbuilder.AppendFormat("{0} :{1}", pair.Uri, pair.Status); + sbuilder.AppendLine(); + } + message = sbuilder.ToString(); + return message.Length == 0; + } + + #region Nested type: PackagePartDescriptor + + public sealed class PackagePartDescriptor + { + #region Private fields + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly CompressionOption _compressOption; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _contentType; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Uri _uri; + + #endregion Private fields + + #region Constructor + + /// + /// Instance constructor + /// + /// Part uri + /// Content type from + /// + public PackagePartDescriptor(Uri uri, string contentType, CompressionOption compressOption) + { + #region Check + + if (ReferenceEquals(uri, null)) + { + throw new ArgumentNullException("uri"); + } + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + #endregion Check + + _uri = uri; + _contentType = contentType; + _compressOption = compressOption; + } + + #endregion Constructor + + #region Public properties + + public Uri Uri + { + [DebuggerStepThrough] + get { return _uri; } + } + + public string ContentType + { + [DebuggerStepThrough] + get { return _contentType; } + } + + public CompressionOption CompressOption + { + [DebuggerStepThrough] + get { return _compressOption; } + } + + #endregion Public properties + + #region Public methods + + public override string ToString() + { + return string.Format("Uri:{0} ContentType: {1}, Compression: {2}", _uri, _contentType, _compressOption); + } + + #endregion Public methods + } + + #endregion Nested type: PackagePartDescriptor + + #region Nested type: CompareStatus + + private enum CompareStatus + { + OnlyOnLeft, + OnlyOnRight, + Equal, + NonEqual + } + + #endregion Nested type: CompareStatus + + #region Nested type: PartPair + + private sealed class PartPair + { + #region Private fields + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Uri _uri; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CompareStatus _status; + + #endregion Private fields + + #region Constructor + + public PartPair(Uri uri, CompareStatus status) + { + _uri = uri; + _status = status; + } + + #endregion Constructor + + #region Public properties + + public Uri Uri + { + [DebuggerStepThrough] + get { return _uri; } + } + + public CompareStatus Status + { + [DebuggerStepThrough] + get { return _status; } + [DebuggerStepThrough] + set { _status = value; } + } + + #endregion Public properties + } + + #endregion Nested type: PartPair + + //-- + } +} diff --git a/ClosedXML_Tests/Utils/ResourceFileExtractor.cs b/ClosedXML_Tests/Utils/ResourceFileExtractor.cs new file mode 100644 index 0000000..ede7623 --- /dev/null +++ b/ClosedXML_Tests/Utils/ResourceFileExtractor.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace ClosedXML_Tests +{ + /// + /// Summary description for ResourceFileExtractor. + /// + public sealed class ResourceFileExtractor + { + #region Static + + #region Private fields + + private static readonly IDictionary extractors = new ConcurrentDictionary(); + + #endregion Private fields + + #region Public properties + + /// Instance of resource extractor for executing assembly + public static ResourceFileExtractor Instance + { + get + { + Assembly _assembly = Assembly.GetCallingAssembly(); + string _key = _assembly.GetName().FullName; + if (!extractors.TryGetValue(_key, out ResourceFileExtractor extractor) + && !extractors.TryGetValue(_key, out extractor)) + { + extractor = new ResourceFileExtractor(_assembly, true, null); + extractors.Add(_key, extractor); + } + + return extractor; + } + } + + #endregion Public properties + + #endregion Static + + #region Private fields + + private readonly Assembly m_assembly; + private readonly ResourceFileExtractor m_baseExtractor; + + private bool m_isStatic; + //private string ResourceFilePath { get; } + + #endregion Private fields + + #region Constructors + + /// + /// Create instance + /// + /// ResourceFilePath in assembly. Example: .Properties.Scripts. + /// + public ResourceFileExtractor(string resourceFilePath, ResourceFileExtractor baseExtractor) + : this(Assembly.GetCallingAssembly(), baseExtractor) + { + ResourceFilePath = resourceFilePath; + } + + /// + /// Create instance + /// + /// + public ResourceFileExtractor(ResourceFileExtractor baseExtractor) + : this(Assembly.GetCallingAssembly(), baseExtractor) + { + } + + /// + /// Create instance + /// + /// ResourceFilePath in assembly. Example: .Properties.Scripts. + public ResourceFileExtractor(string resourcePath) + : this(Assembly.GetCallingAssembly(), resourcePath) + { + } + + /// + /// Instance constructor + /// + /// + /// + public ResourceFileExtractor(Assembly assembly, string resourcePath) + : this(assembly ?? Assembly.GetCallingAssembly()) + { + ResourceFilePath = resourcePath; + } + + /// + /// Instance constructor + /// + public ResourceFileExtractor() + : this(Assembly.GetCallingAssembly()) + { + } + + /// + /// Instance constructor + /// + /// + public ResourceFileExtractor(Assembly assembly) + : this(assembly ?? Assembly.GetCallingAssembly(), (ResourceFileExtractor)null) + { + } + + /// + /// Instance constructor + /// + /// + /// + public ResourceFileExtractor(Assembly assembly, ResourceFileExtractor baseExtractor) + : this(assembly ?? Assembly.GetCallingAssembly(), false, baseExtractor) + { + } + + /// + /// Instance constructor + /// + /// + /// + /// + /// Argument is null. + private ResourceFileExtractor(Assembly assembly, bool isStatic, ResourceFileExtractor baseExtractor) + { + #region Check + + if (assembly is null) + { + throw new ArgumentNullException("assembly"); + } + + #endregion Check + + Assembly = assembly; + m_baseExtractor = baseExtractor; + AssemblyName = Assembly.GetName().Name; + IsStatic = isStatic; + ResourceFilePath = ".Resources."; + } + + #endregion Constructors + + #region Public properties + + /// Work assembly + public Assembly Assembly { get; } + + /// Work assembly name + public string AssemblyName { get; } + + /// + /// Path to read resource files. Example: .Resources.Upgrades. + /// + public string ResourceFilePath { get; } + + public bool IsStatic { get; set; } + + public IEnumerable GetFileNames(Func predicate = null) + { + predicate = predicate ?? (s => true); + + string _path = AssemblyName + ResourceFilePath; + foreach (string _resourceName in Assembly.GetManifestResourceNames()) + { + if (_resourceName.StartsWith(_path) && predicate(_resourceName)) + { + yield return _resourceName.Replace(_path, string.Empty); + } + } + } + + #endregion Public properties + + #region Public methods + + public string ReadFileFromResource(string fileName) + { + Stream _stream = ReadFileFromResourceToStream(fileName); + string _result; + StreamReader sr = new StreamReader(_stream); + try + { + _result = sr.ReadToEnd(); + } + finally + { + sr.Close(); + } + return _result; + } + + public string ReadFileFromResourceFormat(string fileName, params object[] formatArgs) + { + return string.Format(ReadFileFromResource(fileName), formatArgs); + } + + /// + /// Read file in current assembly by specific path + /// + /// Specific path + /// Read file name + /// + public string ReadSpecificFileFromResource(string specificPath, string fileName) + { + ResourceFileExtractor _ext = new ResourceFileExtractor(Assembly, specificPath); + return _ext.ReadFileFromResource(fileName); + } + + /// + /// Read file in current assembly by specific file name + /// + /// + /// + /// ApplicationException. + public Stream ReadFileFromResourceToStream(string fileName) + { + string _nameResFile = AssemblyName + ResourceFilePath + fileName; + Stream _stream = Assembly.GetManifestResourceStream(_nameResFile); + + #region Not found + + if (_stream is null) + { + #region Get from base extractor + + if (!(m_baseExtractor is null)) + { + return m_baseExtractor.ReadFileFromResourceToStream(fileName); + } + + #endregion Get from base extractor + + throw new ArgumentException("Can't find resource file " + _nameResFile, nameof(fileName)); + } + + #endregion Not found + + return _stream; + } + + #endregion Public methods + } +} diff --git a/ClosedXML_Tests/Utils/StreamHelper.cs b/ClosedXML_Tests/Utils/StreamHelper.cs new file mode 100644 index 0000000..f6fdef6 --- /dev/null +++ b/ClosedXML_Tests/Utils/StreamHelper.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace ClosedXML_Tests +{ + /// + /// Help methods for work with streams + /// + public static class StreamHelper + { + /// + /// Convert stream to byte array + /// + /// Stream + /// Byte array + public static byte[] StreamToArray(Stream pStream) + { + long iLength = pStream.Length; + var bytes = new byte[iLength]; + for (int i = 0; i < iLength; i++) + { + bytes[i] = (byte)pStream.ReadByte(); + } + pStream.Close(); + return bytes; + } + + /// + /// Convert byte array to stream + /// + /// Byte array + /// Open stream + /// + public static Stream ArrayToStreamAppend(byte[] pBynaryArray, Stream pStream) + { + #region Check params + + if (ReferenceEquals(pBynaryArray, null)) + { + throw new ArgumentNullException("pBynaryArray"); + } + if (ReferenceEquals(pStream, null)) + { + throw new ArgumentNullException("pStream"); + } + if (!pStream.CanWrite) + { + throw new ArgumentException("Can't write to stream", "pStream"); + } + + #endregion Check params + + foreach (byte b in pBynaryArray) + { + pStream.WriteByte(b); + } + return pStream; + } + + public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite) + { + StreamToStreamAppend(streamIn, streamToWrite, 0); + } + + public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite, long dataLength) + { + #region Check params + + if (ReferenceEquals(streamIn, null)) + { + throw new ArgumentNullException("streamIn"); + } + if (ReferenceEquals(streamToWrite, null)) + { + throw new ArgumentNullException("streamToWrite"); + } + if (!streamIn.CanRead) + { + throw new ArgumentException("Can't read from stream", "streamIn"); + } + if (!streamToWrite.CanWrite) + { + throw new ArgumentException("Can't write to stream", "streamToWrite"); + } + + #endregion Check params + + var buf = new byte[512]; + long length; + if (dataLength == 0) + { + length = streamIn.Length - streamIn.Position; + } + else + { + length = dataLength; + } + long rest = length; + while (rest > 0) + { + int len1 = streamIn.Read(buf, 0, rest >= 512 ? 512 : (int)rest); + streamToWrite.Write(buf, 0, len1); + rest -= len1; + } + } + + /// + /// Compare two streams by converting them to strings and comparing the strings + /// + /// + /// + /// /// + /// + public static bool Compare(Tuple tuple1, Tuple tuple2, bool stripColumnWidths) + { + #region Check + + if (tuple1 == null || tuple1.Item1 == null || tuple1.Item2 == null) + { + throw new ArgumentNullException("one"); + } + if (tuple2 == null || tuple2.Item1 == null || tuple2.Item2 == null) + { + throw new ArgumentNullException("other"); + } + if (tuple1.Item2.Position != 0) + { + throw new ArgumentException("Must be in position 0", "one"); + } + if (tuple2.Item2.Position != 0) + { + throw new ArgumentException("Must be in position 0", "other"); + } + + #endregion Check + + var stringOne = new StreamReader(tuple1.Item2).ReadToEnd().RemoveIgnoredParts(tuple1.Item1, stripColumnWidths, ignoreGuids: true); + var stringOther = new StreamReader(tuple2.Item2).ReadToEnd().RemoveIgnoredParts(tuple2.Item1, stripColumnWidths, ignoreGuids: true); + return stringOne == stringOther; + } + + private static string RemoveIgnoredParts(this string s, Uri uri, Boolean ignoreColumnWidths, Boolean ignoreGuids) + { + foreach (var pair in uriSpecificIgnores.Where(p => p.Key.Equals(uri.OriginalString))) + s = pair.Value.Replace(s, ""); + + // Collapse empty xml elements + s = emptyXmlElementRegex.Replace(s, "<$1 />"); + + if (ignoreColumnWidths) + s = RemoveColumnWidths(s); + + if (ignoreGuids) + s = RemoveGuids(s); + + return s; + } + + private static IEnumerable> uriSpecificIgnores = new List>() + { + // Remove dcterms elements + new KeyValuePair("/docProps/core.xml", new Regex(@"", RegexOptions.Compiled)) + }; + + private static Regex emptyXmlElementRegex = new Regex(@"<([\w:]+)><\/\1>", RegexOptions.Compiled); + private static Regex columnRegex = new Regex("", RegexOptions.Compiled); + private static Regex widthRegex = new Regex("width=\"\\d+(\\.\\d+)?\"\\s+", RegexOptions.Compiled); + + private static String RemoveColumnWidths(String s) + { + var replacements = new Dictionary(); + + foreach (var m in columnRegex.Matches(s).OfType()) + { + var original = m.Groups[0].Value; + var replacement = widthRegex.Replace(original, ""); + replacements.Add(original, replacement); + } + + foreach (var r in replacements) + { + s = s.Replace(r.Key, r.Value); + } + return s; + } + + private static Regex guidRegex = new Regex(@"{[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}}", RegexOptions.Compiled | RegexOptions.Multiline); + + private static String RemoveGuids(String s) + { + return guidRegex.Replace(s, delegate (Match m) + { + return string.Empty; + }); + } + } +} diff --git a/ClosedXML_Tests/Utils/TemporaryFile.cs b/ClosedXML_Tests/Utils/TemporaryFile.cs new file mode 100644 index 0000000..6d95b1f --- /dev/null +++ b/ClosedXML_Tests/Utils/TemporaryFile.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; + +namespace ClosedXML_Tests.Utils +{ + internal class TemporaryFile : IDisposable + { + internal TemporaryFile() + : this(System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), "xlsx")) + { } + + internal TemporaryFile(string path) + : this(path, false) + { } + + internal TemporaryFile(String path, bool preserve) + { + this.Path = path; + this.Preserve = preserve; + } + + + public string Path { get; private set; } + public bool Preserve { get; private set; } + + public void Dispose() + { + if (!Preserve) + File.Delete(Path); + } + + public override string ToString() + { + return this.Path; + } + } +} diff --git a/ClosedXML_Tests/XLHelperTests.cs b/ClosedXML_Tests/XLHelperTests.cs new file mode 100644 index 0000000..18e43dd --- /dev/null +++ b/ClosedXML_Tests/XLHelperTests.cs @@ -0,0 +1,152 @@ +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests +{ + /// + /// This is a test class for XLHelperTests and is intended + /// to contain all XLHelperTests Unit Tests + /// + [TestFixture] + public class XLHelperTests + { + /// + /// Gets or sets the test context which provides + /// information about and functionality for the current test run. + /// + public TestContext TestContext { get; set; } + + // + //You can use the following additional attributes as you write your tests: + // + //Use ClassInitialize to run code before running the first test in the class + //[ClassInitialize()] + //public static void MyClassInitialize(TestContext testContext) + //{ + //} + // + //Use ClassCleanup to run code after all tests in a class have run + //[ClassCleanup()] + //public static void MyClassCleanup() + //{ + //} + // + //Use TestInitialize to run code before running each test + //[TestInitialize()] + //public void MyTestInitialize() + //{ + //} + // + //Use TestCleanup to run code after each test has run + //[TestCleanup()] + //public void MyTestCleanup() + //{ + //} + // + + /// + /// A test for IsValidColumn + /// + [Test] + public void IsValidColumnTest() + { + Assert.AreEqual(false, XLHelper.IsValidColumn("")); + Assert.AreEqual(false, XLHelper.IsValidColumn("1")); + Assert.AreEqual(false, XLHelper.IsValidColumn("A1")); + Assert.AreEqual(false, XLHelper.IsValidColumn("AA1")); + Assert.AreEqual(true, XLHelper.IsValidColumn("A")); + Assert.AreEqual(true, XLHelper.IsValidColumn("AA")); + Assert.AreEqual(true, XLHelper.IsValidColumn("AAA")); + Assert.AreEqual(true, XLHelper.IsValidColumn("Z")); + Assert.AreEqual(true, XLHelper.IsValidColumn("ZZ")); + Assert.AreEqual(true, XLHelper.IsValidColumn("XFD")); + Assert.AreEqual(false, XLHelper.IsValidColumn("ZAA")); + Assert.AreEqual(false, XLHelper.IsValidColumn("XZA")); + Assert.AreEqual(false, XLHelper.IsValidColumn("XFZ")); + } + + [Test] + public void ReplaceRelative1() + { + string result = XLHelper.ReplaceRelative("A1", 2, "B"); + Assert.AreEqual("B2", result); + } + + [Test] + public void ReplaceRelative2() + { + string result = XLHelper.ReplaceRelative("$A1", 2, "B"); + Assert.AreEqual("$A2", result); + } + + [Test] + public void ReplaceRelative3() + { + string result = XLHelper.ReplaceRelative("A$1", 2, "B"); + Assert.AreEqual("B$1", result); + } + + [Test] + public void ReplaceRelative4() + { + string result = XLHelper.ReplaceRelative("$A$1", 2, "B"); + Assert.AreEqual("$A$1", result); + } + + [Test] + public void ReplaceRelative5() + { + string result = XLHelper.ReplaceRelative("1:1", 2, "B"); + Assert.AreEqual("2:2", result); + } + + [Test] + public void ReplaceRelative6() + { + string result = XLHelper.ReplaceRelative("$1:1", 2, "B"); + Assert.AreEqual("$1:2", result); + } + + [Test] + public void ReplaceRelative7() + { + string result = XLHelper.ReplaceRelative("1:$1", 2, "B"); + Assert.AreEqual("2:$1", result); + } + + [Test] + public void ReplaceRelative8() + { + string result = XLHelper.ReplaceRelative("$1:$1", 2, "B"); + Assert.AreEqual("$1:$1", result); + } + + [Test] + public void ReplaceRelative9() + { + string result = XLHelper.ReplaceRelative("A:A", 2, "B"); + Assert.AreEqual("B:B", result); + } + + [Test] + public void ReplaceRelativeA() + { + string result = XLHelper.ReplaceRelative("$A:A", 2, "B"); + Assert.AreEqual("$A:B", result); + } + + [Test] + public void ReplaceRelativeB() + { + string result = XLHelper.ReplaceRelative("A:$A", 2, "B"); + Assert.AreEqual("B:$A", result); + } + + [Test] + public void ReplaceRelativeC() + { + string result = XLHelper.ReplaceRelative("$A:$A", 2, "B"); + Assert.AreEqual("$A:$A", result); + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3164b3f..f054603 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,21 @@ MIT License -Copyright (c) 2026 bermudalamb +Copyright (c) 2016 ClosedXML -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d7c07e5..71733f4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ -# ClosedXML +![ClosedXML](https://github.com/ClosedXML/ClosedXML/blob/develop/resources/logo/readme.png) +[![Release](https://img.shields.io/badge/release-0.95.4-blue.svg)](https://github.com/ClosedXML/ClosedXML/releases/latest) [![NuGet Badge](https://buildstats.info/nuget/ClosedXML)](https://www.nuget.org/packages/ClosedXML/) [![.NET Framework](https://img.shields.io/badge/.NET%20Framework-%3E%3D%204.0-red.svg)](#) [![.NET Standard](https://img.shields.io/badge/.NET%20Standard-%3E%3D%202.0-red.svg)](#) [![Build status](https://ci.appveyor.com/api/projects/status/wobbmnlbukxejjgb?svg=true)](https://ci.appveyor.com/project/ClosedXML/ClosedXML/branch/develop/artifacts) +[![Open Source Helpers](https://www.codetriage.com/closedxml/closedxml/badges/users.svg)](https://www.codetriage.com/closedxml/closedxml) + +[💾 Download unstable CI build](https://ci.appveyor.com/project/ClosedXML/ClosedXML/branch/develop/artifacts) + +ClosedXML is a .NET library for reading, manipulating and writing Excel 2007+ (.xlsx, .xlsm) files. It aims to provide an intuitive and user-friendly interface to dealing with the underlying [OpenXML](https://github.com/OfficeDev/Open-XML-SDK) API. + +[For more information see the wiki](https://github.com/closedxml/closedxml/wiki) + +### Install ClosedXML via NuGet + +If you want to include ClosedXML in your project, you can [install it directly from NuGet](https://www.nuget.org/packages/ClosedXML) + +To install ClosedXML, run the following command in the Package Manager Console + +``` +PM> Install-Package ClosedXML +``` + +### What can you do with this? + +ClosedXML allows you to create Excel files without the Excel application. The typical example is creating Excel reports on a web server. + +**Example:** +```c# +using (var workbook = new XLWorkbook()) +{ + var worksheet = workbook.Worksheets.Add("Sample Sheet"); + worksheet.Cell("A1").Value = "Hello World!"; + worksheet.Cell("A2").FormulaA1 = "=MID(A1, 7, 5)"; + workbook.SaveAs("HelloWorld.xlsx"); +} +``` + +### Extensions +Be sure to check out our `ClosedXML` extension projects +- https://github.com/ClosedXML/ClosedXML.Report +- https://github.com/ClosedXML/ClosedXML.Extensions.AspNet +- https://github.com/ClosedXML/ClosedXML.Extensions.Mvc +- https://github.com/ClosedXML/ClosedXML.Extensions.WebApi + +## Developer guidelines +The [OpenXML specification](https://www.ecma-international.org/publications/standards/Ecma-376.htm) is a large and complicated beast. In order for ClosedXML, the wrapper around OpenXML, to support all the features, we rely on community contributions. Before opening an issue to request a new feature, we'd like to urge you to try to implement it yourself and log a pull request. + +Please read the [full developer guidelines](CONTRIBUTING.md). + +## Credits +* Project originally created by Manuel de Leon +* Current maintainer: [Francois Botha](https://github.com/igitur) +* Master of Computing Patterns: [Aleksei Pankratev](https://github.com/Pankraty) +* Logo design by [@Tobaloidee](https://github.com/Tobaloidee) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..ed8c238 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,61 @@ +version: 0.95.4.{build} + +os: Visual Studio 2019 +image: Visual Studio 2019 + +environment: + AppVeyor: APPVEYOR + +init: + +- ps: >- + if ($env:APPVEYOR_REPO_TAG -eq 'true') + { + $env:fileVersion = $env:APPVEYOR_REPO_TAG_NAME -replace '(\d+)\.(\d+)\.(\d+)(-.+)?', '$1.$2.$3' + if ($env:fileVersion -eq $env:APPVEYOR_REPO_TAG_NAME) { $env:fileVersion = $($env:fileVersion + '.0') } + else { $env:fileVersion = $($env:fileVersion + '.' + $env:APPVEYOR_BUILD_NUMBER) } + $env:productVersion = $env:APPVEYOR_REPO_TAG_NAME + } + else + { + $env:fileVersion = $env:APPVEYOR_BUILD_VERSION -replace '(\d+)\.(\d+)\.([^.]+)\.(\d+)', '$1.$2.999.$4' + $env:productVersion = $env:fileVersion + } + + Update-AppveyorBuild -Version $env:fileVersion + + Write-Host $env:fileVersion $env:productVersion + + Write-Host $env:APPVEYOR_REPO_TAG $env:APPVEYOR_REPO_TAG_NAME + + +dotnet_csproj: + patch: true + file: '**\*.csproj' + version: '$(productVersion)' + package_version: '$(productVersion)' + assembly_version: '$(fileVersion)' + file_version: '$(fileVersion)' + informational_version: '$(productVersion)' + +# platform: Any CPU +configuration : +- Release +- Release.Signed + +before_build: + - cmd: nuget update -self + - nuget restore + +build: + parallel: true + project: ClosedXML.sln + verbosity: minimal + +artifacts: + - path: ClosedXML/bin/%CONFIGURATION%/*/ClosedXML.dll + - path: ClosedXML/bin/%CONFIGURATION%/*.nupkg + +nuget: + project_feed: true + disable_publish_on_pr: true diff --git a/resources/logo/favicon-01.png b/resources/logo/favicon-01.png new file mode 100644 index 0000000..5b5e744 Binary files /dev/null and b/resources/logo/favicon-01.png differ diff --git a/resources/logo/favicon-01.svg b/resources/logo/favicon-01.svg new file mode 100644 index 0000000..d199dde --- /dev/null +++ b/resources/logo/favicon-01.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/resources/logo/favicon-02.png b/resources/logo/favicon-02.png new file mode 100644 index 0000000..e830ad6 Binary files /dev/null and b/resources/logo/favicon-02.png differ diff --git a/resources/logo/favicon-02.svg b/resources/logo/favicon-02.svg new file mode 100644 index 0000000..7d48c04 --- /dev/null +++ b/resources/logo/favicon-02.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/resources/logo/favicon-03.png b/resources/logo/favicon-03.png new file mode 100644 index 0000000..acfe5d1 Binary files /dev/null and b/resources/logo/favicon-03.png differ diff --git a/resources/logo/favicon-03.svg b/resources/logo/favicon-03.svg new file mode 100644 index 0000000..772bb09 --- /dev/null +++ b/resources/logo/favicon-03.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/resources/logo/favicon-04.png b/resources/logo/favicon-04.png new file mode 100644 index 0000000..bec930f Binary files /dev/null and b/resources/logo/favicon-04.png differ diff --git a/resources/logo/favicon-04.svg b/resources/logo/favicon-04.svg new file mode 100644 index 0000000..25cb752 --- /dev/null +++ b/resources/logo/favicon-04.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-a-05.png b/resources/logo/logotype-a-05.png new file mode 100644 index 0000000..de81445 Binary files /dev/null and b/resources/logo/logotype-a-05.png differ diff --git a/resources/logo/logotype-a-05.svg b/resources/logo/logotype-a-05.svg new file mode 100644 index 0000000..95d422d --- /dev/null +++ b/resources/logo/logotype-a-05.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-a-06.png b/resources/logo/logotype-a-06.png new file mode 100644 index 0000000..8c8b21b Binary files /dev/null and b/resources/logo/logotype-a-06.png differ diff --git a/resources/logo/logotype-a-06.svg b/resources/logo/logotype-a-06.svg new file mode 100644 index 0000000..874b8e9 --- /dev/null +++ b/resources/logo/logotype-a-06.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-a-07.png b/resources/logo/logotype-a-07.png new file mode 100644 index 0000000..be4bf8f Binary files /dev/null and b/resources/logo/logotype-a-07.png differ diff --git a/resources/logo/logotype-a-07.svg b/resources/logo/logotype-a-07.svg new file mode 100644 index 0000000..75d1057 --- /dev/null +++ b/resources/logo/logotype-a-07.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-a-08.png b/resources/logo/logotype-a-08.png new file mode 100644 index 0000000..043d8d7 Binary files /dev/null and b/resources/logo/logotype-a-08.png differ diff --git a/resources/logo/logotype-a-08.svg b/resources/logo/logotype-a-08.svg new file mode 100644 index 0000000..6668c56 --- /dev/null +++ b/resources/logo/logotype-a-08.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-b-09.png b/resources/logo/logotype-b-09.png new file mode 100644 index 0000000..e082c0b Binary files /dev/null and b/resources/logo/logotype-b-09.png differ diff --git a/resources/logo/logotype-b-09.svg b/resources/logo/logotype-b-09.svg new file mode 100644 index 0000000..340e38c --- /dev/null +++ b/resources/logo/logotype-b-09.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-b-10.png b/resources/logo/logotype-b-10.png new file mode 100644 index 0000000..972834c Binary files /dev/null and b/resources/logo/logotype-b-10.png differ diff --git a/resources/logo/logotype-b-10.svg b/resources/logo/logotype-b-10.svg new file mode 100644 index 0000000..f9d7b77 --- /dev/null +++ b/resources/logo/logotype-b-10.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-b-11.png b/resources/logo/logotype-b-11.png new file mode 100644 index 0000000..8757bcb Binary files /dev/null and b/resources/logo/logotype-b-11.png differ diff --git a/resources/logo/logotype-b-11.svg b/resources/logo/logotype-b-11.svg new file mode 100644 index 0000000..6f5de99 --- /dev/null +++ b/resources/logo/logotype-b-11.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/logotype-b-12.png b/resources/logo/logotype-b-12.png new file mode 100644 index 0000000..758e04d Binary files /dev/null and b/resources/logo/logotype-b-12.png differ diff --git a/resources/logo/logotype-b-12.svg b/resources/logo/logotype-b-12.svg new file mode 100644 index 0000000..241d61d --- /dev/null +++ b/resources/logo/logotype-b-12.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo/nuget-logo.png b/resources/logo/nuget-logo.png new file mode 100644 index 0000000..3159920 Binary files /dev/null and b/resources/logo/nuget-logo.png differ diff --git a/resources/logo/readme.png b/resources/logo/readme.png new file mode 100644 index 0000000..99505ed Binary files /dev/null and b/resources/logo/readme.png differ