Initial commit of the ClosedXML library
Establishes the foundational structure, core Excel functionality, and tooling. - Implements base components for cells, ranges, columns, and rows. - Integrates a custom calculation engine with a wide range of Excel functions. - Adds comprehensive support for styling, conditional formatting, data validation, comments, pictures, and charts. - Configures build setup, project metadata (NuGet), and developer guidelines. - Includes editor and Git attributes for consistent code style and line endings.
This commit is contained in:
@@ -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<XLColumnAttribute>()) return null;
|
||||
return mi.GetAttributes<XLColumnAttribute>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net40;net46</TargetFrameworks>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<AssemblyName>ClosedXML</AssemblyName>
|
||||
<PackageId>ClosedXML</PackageId>
|
||||
<Version>0.95.4</Version>
|
||||
<Authors>Francois Botha, Aleksei Pankratev, Manuel de Leon, Amir Ghezelbash</Authors>
|
||||
<Owners>Francois Botha, Aleksei Pankratev</Owners>
|
||||
<Company />
|
||||
<Product>ClosedXML</Product>
|
||||
<PackageReleaseNotes>See https://github.com/ClosedXML/ClosedXML/releases/tag/$(productVersion)</PackageReleaseNotes>
|
||||
<Description>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.</Description>
|
||||
<Copyright>MIT</Copyright>
|
||||
<PackageProjectUrl>https://github.com/ClosedXML/ClosedXML</PackageProjectUrl>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageIcon>nuget-logo.png</PackageIcon>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/ClosedXML/ClosedXML/develop/resources/logo/nuget-logo.png</PackageIconUrl>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<NoWarn>$(NoWarn);NU1605;CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Configurations>Debug;Release;Release.Signed</Configurations>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release.Signed'">
|
||||
<PackageId>ClosedXML.Signed</PackageId>
|
||||
<OutputPath>bin\Release.Signed\</OutputPath>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<Optimize>true</Optimize>
|
||||
<AssemblyOriginatorKeyFile>ClosedXML.snk</AssemblyOriginatorKeyFile>
|
||||
<DefineConstants>$(DefineConstants);RELEASE;STRONGNAME</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<DefineConstants>$(DefineConstants);_NETSTANDARD_;_NETSTANDARD2_0_</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net40' ">
|
||||
<DefineConstants>$(DefineConstants);_NETFRAMEWORK_;_NET40_</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net46' ">
|
||||
<DefineConstants>$(DefineConstants);_NETFRAMEWORK_;_NET46_</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="Fody" Version="6.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Janitor.Fody" Version="1.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net46'">
|
||||
<PackageReference Include="Fody" Version="6.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Janitor.Fody" Version="1.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
<None Include="..\resources\logo\nuget-logo.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="2.7.2" />
|
||||
<PackageReference Include="ExcelNumberFormat" Version="1.0.10" />
|
||||
|
||||
<!-- SourceLink , see https://github.com/ctaggart/SourceLink -->
|
||||
<PackageReference Include="SourceLink.Copy.PdbFiles" Version="2.8.3" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0"?>
|
||||
<package>
|
||||
<metadata>
|
||||
<id>$id$</id>
|
||||
<version>$version$</version>
|
||||
<title>$title$</title>
|
||||
<authors>$author$</authors>
|
||||
<owners>$author$</owners>
|
||||
<description>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.</description>
|
||||
<licenseUrl>https://github.com/ClosedXML/ClosedXML/blob/master/LICENSE</licenseUrl>
|
||||
<projectUrl>https://github.com/ClosedXML/ClosedXML</projectUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<copyright>@ClosedXML</copyright>
|
||||
<tags>ClosedXML</tags>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="bin\Release\net40\ClosedXML.dll" target="lib\net40\ClosedXML.dll" />
|
||||
<file src="bin\Release\net40\ClosedXML.pdb" target="lib\net40\ClosedXML.pdb" />
|
||||
<file src="bin\Release\net40\ClosedXML.xml" target="lib\net40\ClosedXML.xml" />
|
||||
<file src="bin\Release\net46\ClosedXML.dll" target="lib\net46\ClosedXML.dll" />
|
||||
<file src="bin\Release\net46\ClosedXML.pdb" target="lib\net46\ClosedXML.pdb" />
|
||||
<file src="bin\Release\net46\ClosedXML.xml" target="lib\net46\ClosedXML.xml" />
|
||||
<file src="bin\Release\netstandard2.0\ClosedXML.dll" target="lib\netstandard2.0\ClosedXML.dll" />
|
||||
<file src="bin\Release\netstandard2.0\ClosedXML.pdb" target="lib\netstandard2.0\ClosedXML.pdb" />
|
||||
<file src="bin\Release\netstandard2.0\ClosedXML.xml" target="lib\netstandard2.0\ClosedXML.xml" />
|
||||
</files>
|
||||
</package>
|
||||
Binary file not shown.
@@ -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<IXLRangeRow> HiddenRows { get; }
|
||||
Boolean IsEnabled { get; set; }
|
||||
IXLRange Range { get; set; }
|
||||
Int32 SortColumn { get; set; }
|
||||
Boolean Sorted { get; set; }
|
||||
XLSortOrder SortOrder { get; set; }
|
||||
IEnumerable<IXLRangeRow> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLCustomFilteredColumn
|
||||
{
|
||||
void EqualTo<T>(T value) where T : IComparable<T>;
|
||||
void NotEqualTo<T>(T value) where T : IComparable<T>;
|
||||
void GreaterThan<T>(T value) where T : IComparable<T>;
|
||||
void LessThan<T>(T value) where T : IComparable<T>;
|
||||
void EqualOrGreaterThan<T>(T value) where T : IComparable<T>;
|
||||
void EqualOrLessThan<T>(T value) where T : IComparable<T>;
|
||||
void BeginsWith(String value);
|
||||
void NotBeginsWith(String value);
|
||||
void EndsWith(String value);
|
||||
void NotEndsWith(String value);
|
||||
void Contains(String value);
|
||||
void NotContains(String value);
|
||||
}
|
||||
}
|
||||
@@ -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>(T value) where T : IComparable<T>;
|
||||
|
||||
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>(T value) where T : IComparable<T>;
|
||||
|
||||
IXLFilterConnector NotEqualTo<T>(T value) where T : IComparable<T>;
|
||||
|
||||
IXLFilterConnector GreaterThan<T>(T value) where T : IComparable<T>;
|
||||
|
||||
IXLFilterConnector LessThan<T>(T value) where T : IComparable<T>;
|
||||
|
||||
IXLFilterConnector EqualOrGreaterThan<T>(T value) where T : IComparable<T>;
|
||||
|
||||
IXLFilterConnector EqualOrLessThan<T>(T value) where T : IComparable<T>;
|
||||
|
||||
void Between<T>(T minValue, T maxValue) where T : IComparable<T>;
|
||||
|
||||
void NotBetween<T>(T minValue, T maxValue) where T : IComparable<T>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLFilterConnector
|
||||
{
|
||||
IXLCustomFilteredColumn And { get; }
|
||||
IXLCustomFilteredColumn Or { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLFilteredColumn
|
||||
{
|
||||
IXLFilteredColumn AddFilter<T>(T value) where T : IComparable<T>;
|
||||
}
|
||||
}
|
||||
@@ -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<Int32, XLFilterColumn> _columns = new Dictionary<int, XLFilterColumn>();
|
||||
|
||||
public XLAutoFilter()
|
||||
{
|
||||
Filters = new Dictionary<int, List<XLFilter>>();
|
||||
}
|
||||
|
||||
public Dictionary<Int32, List<XLFilter>> Filters { get; private set; }
|
||||
|
||||
#region IXLAutoFilter Members
|
||||
|
||||
[Obsolete("Use IsEnabled")]
|
||||
public Boolean Enabled { get => IsEnabled; set => IsEnabled = value; }
|
||||
public IEnumerable<IXLRangeRow> 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<IXLRangeRow> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>(T value) where T: IComparable<T>
|
||||
{
|
||||
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<T>().CompareTo(value) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void NotEqualTo<T>(T value) where T: IComparable<T>
|
||||
{
|
||||
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<T>().CompareTo(value) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void GreaterThan<T>(T value) where T: IComparable<T>
|
||||
{
|
||||
ApplyCustomFilter(value, XLFilterOperator.GreaterThan,
|
||||
v => v.CastTo<T>().CompareTo(value) > 0);
|
||||
}
|
||||
|
||||
public void LessThan<T>(T value) where T: IComparable<T>
|
||||
{
|
||||
ApplyCustomFilter(value, XLFilterOperator.LessThan, v => v.CastTo<T>().CompareTo(value) < 0);
|
||||
}
|
||||
|
||||
public void EqualOrGreaterThan<T>(T value) where T: IComparable<T>
|
||||
{
|
||||
ApplyCustomFilter(value, XLFilterOperator.EqualOrGreaterThan,
|
||||
v => v.CastTo<T>().CompareTo(value) >= 0);
|
||||
}
|
||||
|
||||
public void EqualOrLessThan<T>(T value) where T: IComparable<T>
|
||||
{
|
||||
ApplyCustomFilter(value, XLFilterOperator.EqualOrLessThan,
|
||||
v => v.CastTo<T>().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>(T value, XLFilterOperator op, Func<Object, Boolean> condition)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
_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<T>()))
|
||||
row.WorksheetRow().Unhide();
|
||||
else
|
||||
row.WorksheetRow().Hide();
|
||||
}
|
||||
}
|
||||
else if (condition(row.Cell(_column).GetValue<T>()))
|
||||
row.WorksheetRow().Unhide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Object, Boolean> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Object, Boolean> Condition { get; set; }
|
||||
public XLConnector Connector { get; set; }
|
||||
public XLDateTimeGrouping DateTimeGrouping { get; set; }
|
||||
public XLFilterOperator Operator { get; set; }
|
||||
public Object Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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>(T value) where T : IComparable<T>
|
||||
{
|
||||
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<T>().CompareTo(value) == 0, XLFilterType.Regular);
|
||||
}
|
||||
return new XLFilteredColumn(_autoFilter, _column);
|
||||
}
|
||||
|
||||
public IXLDateTimeGroupFilteredColumn AddDateGroupFilter(DateTime date, XLDateTimeGrouping dateTimeGrouping)
|
||||
{
|
||||
Func<Object, Boolean> condition = date2 => XLDateTimeGroupFilteredColumn.IsMatch(date, (DateTime)date2, dateTimeGrouping);
|
||||
|
||||
_autoFilter.IsEnabled = true;
|
||||
|
||||
if (_autoFilter.Filters.TryGetValue(_column, out List<XLFilter> filterList))
|
||||
filterList.Add(
|
||||
new XLFilter
|
||||
{
|
||||
Value = date,
|
||||
Operator = XLFilterOperator.Equal,
|
||||
Connector = XLConnector.Or,
|
||||
Condition = condition,
|
||||
DateTimeGrouping = dateTimeGrouping
|
||||
}
|
||||
);
|
||||
else
|
||||
{
|
||||
_autoFilter.Filters.Add(
|
||||
_column,
|
||||
new List<XLFilter>
|
||||
{
|
||||
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>(T value) where T : IComparable<T>
|
||||
{
|
||||
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<T>().CompareTo(value) == 0);
|
||||
}
|
||||
|
||||
public IXLFilterConnector NotEqualTo<T>(T value) where T : IComparable<T>
|
||||
{
|
||||
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<T>().CompareTo(value) != 0);
|
||||
}
|
||||
|
||||
public IXLFilterConnector GreaterThan<T>(T value) where T : IComparable<T>
|
||||
{
|
||||
return ApplyCustomFilter(value, XLFilterOperator.GreaterThan,
|
||||
v => v.CastTo<T>().CompareTo(value) > 0);
|
||||
}
|
||||
|
||||
public IXLFilterConnector LessThan<T>(T value) where T : IComparable<T>
|
||||
{
|
||||
return ApplyCustomFilter(value, XLFilterOperator.LessThan,
|
||||
v => v.CastTo<T>().CompareTo(value) < 0);
|
||||
}
|
||||
|
||||
public IXLFilterConnector EqualOrGreaterThan<T>(T value) where T : IComparable<T>
|
||||
{
|
||||
return ApplyCustomFilter(value, XLFilterOperator.EqualOrGreaterThan,
|
||||
v => v.CastTo<T>().CompareTo(value) >= 0);
|
||||
}
|
||||
|
||||
public IXLFilterConnector EqualOrLessThan<T>(T value) where T : IComparable<T>
|
||||
{
|
||||
return ApplyCustomFilter(value, XLFilterOperator.EqualOrLessThan,
|
||||
v => v.CastTo<T>().CompareTo(value) <= 0);
|
||||
}
|
||||
|
||||
public void Between<T>(T minValue, T maxValue) where T : IComparable<T>
|
||||
{
|
||||
EqualOrGreaterThan(minValue).And.EqualOrLessThan(maxValue);
|
||||
}
|
||||
|
||||
public void NotBetween<T>(T minValue, T maxValue) where T : IComparable<T>
|
||||
{
|
||||
LessThan(minValue).Or.GreaterThan(maxValue);
|
||||
}
|
||||
|
||||
public static Func<String, Object, Boolean> 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<String, Object, Boolean> 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<String, Object, Boolean> 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<XLFilter>());
|
||||
|
||||
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<Object, Boolean> 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<double> 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<XLFilter>());
|
||||
|
||||
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<Object, Boolean> 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<double> 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>(T value, XLFilterOperator op, Func<Object, Boolean> condition,
|
||||
XLFilterType filterType = XLFilterType.Custom)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
_autoFilter.IsEnabled = true;
|
||||
if (filterType == XLFilterType.Custom)
|
||||
{
|
||||
Clear();
|
||||
_autoFilter.Filters.Add(_column,
|
||||
new List<XLFilter>
|
||||
{
|
||||
new XLFilter
|
||||
{
|
||||
Value = value,
|
||||
Operator = op,
|
||||
Connector = XLConnector.Or,
|
||||
Condition = condition
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_autoFilter.Filters.TryGetValue(_column, out List<XLFilter> filterList))
|
||||
filterList.Add(new XLFilter
|
||||
{
|
||||
Value = value,
|
||||
Operator = op,
|
||||
Connector = XLConnector.Or,
|
||||
Condition = condition
|
||||
});
|
||||
else
|
||||
{
|
||||
_autoFilter.Filters.Add(_column,
|
||||
new List<XLFilter>
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>(T value) where T : IComparable<T>
|
||||
{
|
||||
Func<Object, Boolean> condition;
|
||||
Boolean isText;
|
||||
if (typeof(T) == typeof(String))
|
||||
{
|
||||
condition = v => v.ToString().Equals(value.ToString(), StringComparison.InvariantCultureIgnoreCase);
|
||||
isText = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
condition = v => v.CastTo<T>().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<T>())))
|
||||
{
|
||||
row.WorksheetRow().Unhide();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion IXLFilteredColumn Members
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Base interface for an abstract repository.
|
||||
/// </summary>
|
||||
internal interface IXLRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Clear the repository;
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
internal interface IXLRepository<Tkey, Tvalue> : IXLRepository, IEnumerable<Tvalue>
|
||||
where Tkey : struct, IEquatable<Tkey>
|
||||
where Tvalue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Put the <paramref name="value"/> into the repository under the specified <paramref name="key"/>
|
||||
/// if there is no such key present.
|
||||
/// </summary>
|
||||
/// <param name="key">Key to identify the value.</param>
|
||||
/// <param name="value">Value to put into the repository if key does not exist.</param>
|
||||
/// <returns>Value stored in the repository under the specified <paramref name="key"/>. If key already existed
|
||||
/// returned value may differ from the input one.</returns>
|
||||
Tvalue Store(ref Tkey key, Tvalue value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLAlignmentRepository : XLRepositoryBase<XLAlignmentKey, XLAlignmentValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLAlignmentRepository(Func<XLAlignmentKey, XLAlignmentValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLAlignmentRepository(Func<XLAlignmentKey, XLAlignmentValue> createNew, IEqualityComparer<XLAlignmentKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLBorderRepository : XLRepositoryBase<XLBorderKey, XLBorderValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLBorderRepository(Func<XLBorderKey, XLBorderValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLBorderRepository(Func<XLBorderKey, XLBorderValue> createNew, IEqualityComparer<XLBorderKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLColorRepository : XLRepositoryBase<XLColorKey, XLColor>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLColorRepository(Func<XLColorKey, XLColor> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLColorRepository(Func<XLColorKey, XLColor> createNew, IEqualityComparer<XLColorKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLFillRepository : XLRepositoryBase<XLFillKey, XLFillValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLFillRepository(Func<XLFillKey, XLFillValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLFillRepository(Func<XLFillKey, XLFillValue> createNew, IEqualityComparer<XLFillKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLFontRepository : XLRepositoryBase<XLFontKey, XLFontValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLFontRepository(Func<XLFontKey, XLFontValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLFontRepository(Func<XLFontKey, XLFontValue> createNew, IEqualityComparer<XLFontKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLNumberFormatRepository : XLRepositoryBase<XLNumberFormatKey, XLNumberFormatValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLNumberFormatRepository(Func<XLNumberFormatKey, XLNumberFormatValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLNumberFormatRepository(Func<XLNumberFormatKey, XLNumberFormatValue> createNew, IEqualityComparer<XLNumberFormatKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLProtectionRepository : XLRepositoryBase<XLProtectionKey, XLProtectionValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLProtectionRepository(Func<XLProtectionKey, XLProtectionValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLProtectionRepository(Func<XLProtectionKey, XLProtectionValue> createNew, IEqualityComparer<XLProtectionKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal class XLRangeRepository : XLWorkbookElementRepositoryBase<XLRangeKey, XLRangeBase>
|
||||
{
|
||||
public XLRangeRepository(XLWorkbook workbook, Func<XLRangeKey, XLRangeBase> createNew) : base(workbook, createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLRangeRepository(XLWorkbook workbook, Func<XLRangeKey, XLRangeBase> createNew, IEqualityComparer<XLRangeKey> сomparer) : base(workbook, createNew, сomparer)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Tkey, Tvalue> : XLRepositoryBase, IXLRepository<Tkey, Tvalue>
|
||||
where Tkey : struct, IEquatable<Tkey>
|
||||
where Tvalue : class
|
||||
{
|
||||
const int CONCURRENCY_LEVEL = 4;
|
||||
const int INITIAL_CAPACITY = 1000;
|
||||
|
||||
private readonly ConcurrentDictionary<Tkey, WeakReference> _storage;
|
||||
private readonly Func<Tkey, Tvalue> _createNew;
|
||||
|
||||
protected XLRepositoryBase(Func<Tkey, Tvalue> createNew)
|
||||
: this(createNew, EqualityComparer<Tkey>.Default)
|
||||
{
|
||||
}
|
||||
|
||||
protected XLRepositoryBase(Func<Tkey, Tvalue> createNew, IEqualityComparer<Tkey> comparer)
|
||||
{
|
||||
_storage = new ConcurrentDictionary<Tkey, WeakReference>(CONCURRENCY_LEVEL, INITIAL_CAPACITY, comparer);
|
||||
_createNew = createNew;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified key is presented in the repository.
|
||||
/// </summary>
|
||||
/// <param name="key">Key to look for.</param>
|
||||
/// <param name="value">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.</param>
|
||||
/// <returns>True if entry exists and alive, false otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Put the entity into the repository under the specified key if no other entity with
|
||||
/// the same key is presented.
|
||||
/// </summary>
|
||||
/// <param name="key">Key to identify the entity.</param>
|
||||
/// <param name="value">Entity to store.</param>
|
||||
/// <returns>Entity that is stored in the repository under the specified key
|
||||
/// (it can be either the <paramref name="value"/> or another entity that has been added to
|
||||
/// the repository before.)</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate items in repository removing "dead" entries.
|
||||
/// </summary>
|
||||
public IEnumerator<Tvalue> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
internal sealed class XLStyleRepository : XLRepositoryBase<XLStyleKey, XLStyleValue>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public XLStyleRepository(Func<XLStyleKey, XLStyleValue> createNew)
|
||||
: base(createNew)
|
||||
{
|
||||
}
|
||||
|
||||
public XLStyleRepository(Func<XLStyleKey, XLStyleValue> createNew, IEqualityComparer<XLStyleKey> comparer)
|
||||
: base(createNew, comparer)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Base repository for <see cref="XLWorkbook"/> elements.
|
||||
/// </summary>
|
||||
internal abstract class XLWorkbookElementRepositoryBase<Tkey, Tvalue> : XLRepositoryBase<Tkey, Tvalue>
|
||||
where Tkey : struct, IEquatable<Tkey>
|
||||
where Tvalue : class
|
||||
{
|
||||
public XLWorkbook Workbook { get; private set; }
|
||||
|
||||
public XLWorkbookElementRepositoryBase(XLWorkbook workbook, Func<Tkey, Tvalue> createNew)
|
||||
: this(workbook, createNew, EqualityComparer<Tkey>.Default)
|
||||
{
|
||||
}
|
||||
|
||||
public XLWorkbookElementRepositoryBase(XLWorkbook workbook, Func<Tkey, Tvalue> createNew, IEqualityComparer<Tkey> сomparer)
|
||||
: base(createNew, сomparer)
|
||||
{
|
||||
Workbook = workbook;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// CalcEngine parses strings and returns Expression objects that can
|
||||
/// be evaluated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This class has three extensibility points:</para>
|
||||
/// <para>Use the <b>DataContext</b> property to add an object's properties to the engine scope.</para>
|
||||
/// <para>Use the <b>RegisterFunction</b> method to define custom functions.</para>
|
||||
/// <para>Override the <b>GetExternalObject</b> method to add arbitrary variables to the engine scope.</para>
|
||||
/// </remarks>
|
||||
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<object, Token> _tkTbl; // table with tokens (+, -, etc)
|
||||
private Dictionary<string, FunctionDefinition> _fnTbl; // table with constants and functions (pi, sin, etc)
|
||||
private Dictionary<string, object> _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<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
_cache = new ExpressionCache(this);
|
||||
_optimize = true;
|
||||
#if DEBUG
|
||||
//this.Test();
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion ** ctor
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#region ** object model
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into an <see cref="Expression"/>.
|
||||
/// </summary>
|
||||
/// <param name="expression">String to parse.</param>
|
||||
/// <returns>An <see cref="Expression"/> object that can be evaluated.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a string.
|
||||
/// </summary>
|
||||
/// <param name="expression">Expression to evaluate.</param>
|
||||
/// <returns>The value of the expression.</returns>
|
||||
/// <remarks>
|
||||
/// If you are going to evaluate the same expression several times,
|
||||
/// it is more efficient to parse it only once using the <see cref="Parse"/>
|
||||
/// method and then using the Expression.Evaluate method to evaluate
|
||||
/// the parsed expression.
|
||||
/// </remarks>
|
||||
public object Evaluate(string expression)
|
||||
{
|
||||
var x = _cache != null
|
||||
? _cache[expression]
|
||||
: Parse(expression);
|
||||
return x.Evaluate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the calc engine should keep a cache with parsed
|
||||
/// expressions.
|
||||
/// </summary>
|
||||
public bool CacheExpressions
|
||||
{
|
||||
get { return _cache != null; }
|
||||
set
|
||||
{
|
||||
if (value != CacheExpressions)
|
||||
{
|
||||
_cache = value
|
||||
? new ExpressionCache(this)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the calc engine should optimize expressions when
|
||||
/// they are parsed.
|
||||
/// </summary>
|
||||
public bool OptimizeExpressions
|
||||
{
|
||||
get { return _optimize; }
|
||||
set { _optimize = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a string that specifies special characters that are valid for identifiers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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).
|
||||
/// </remarks>
|
||||
public char[] IdentifierChars
|
||||
{
|
||||
get { return _idChars; }
|
||||
set { _idChars = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a function that can be evaluated by this <see cref="CalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="functionName">Function name.</param>
|
||||
/// <param name="parmMin">Minimum parameter count.</param>
|
||||
/// <param name="parmMax">Maximum parameter count.</param>
|
||||
/// <param name="fn">Delegate that evaluates the function.</param>
|
||||
public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn)
|
||||
{
|
||||
_fnTbl.Add(functionName, new FunctionDefinition(parmMin, parmMax, fn));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a function that can be evaluated by this <see cref="CalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <param name="functionName">Function name.</param>
|
||||
/// <param name="parmCount">Parameter count.</param>
|
||||
/// <param name="fn">Delegate that evaluates the function.</param>
|
||||
public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn)
|
||||
{
|
||||
RegisterFunction(functionName, parmCount, parmCount, fn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an external object based on an identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.)
|
||||
/// </remarks>
|
||||
public virtual object GetExternalObject(string identifier)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DataContext for this <see cref="CalcEngine"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public virtual object DataContext
|
||||
{
|
||||
get { return _dataContext; }
|
||||
set { _dataContext = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dictionary that contains function definitions.
|
||||
/// </summary>
|
||||
public Dictionary<string, FunctionDefinition> Functions
|
||||
{
|
||||
get { return _fnTbl; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dictionary that contains simple variables (not in the DataContext).
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Variables
|
||||
{
|
||||
get { return _vars; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="CultureInfo"/> to use when parsing numbers and dates.
|
||||
/// </summary>
|
||||
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<string, ErrorExpression.ExpressionErrorType> ErrorMap = new Dictionary<string, ErrorExpression.ExpressionErrorType>()
|
||||
{
|
||||
["#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<object, Token> GetSymbolTable()
|
||||
{
|
||||
if (_tkTbl == null)
|
||||
{
|
||||
_tkTbl = new Dictionary<object, Token>();
|
||||
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<string, FunctionDefinition> GetFunctionTable()
|
||||
{
|
||||
if (_fnTbl == null)
|
||||
{
|
||||
// create table
|
||||
_fnTbl = new Dictionary<string, FunctionDefinition>(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<char, char> matchingClosingSymbols = new Dictionary<char, char>()
|
||||
{
|
||||
{ '\'', '\'' },
|
||||
{ '[', ']' }
|
||||
};
|
||||
|
||||
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<char>() { '\\', '/', '*', '[', ':', '?' };
|
||||
if (isEnclosed && disallowedSymbols.Contains(c))
|
||||
break;
|
||||
|
||||
var allowedSymbols = new List<char>() { '_', '.' };
|
||||
|
||||
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<Expression> 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<Expression>();
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate that represents CalcEngine functions.
|
||||
/// </summary>
|
||||
/// <param name="parms">List of <see cref="Expression"/> objects that represent the
|
||||
/// parameters to be used in the function call.</param>
|
||||
/// <returns>The function result.</returns>
|
||||
internal delegate object CalcEngineFunction(List<Expression> parms);
|
||||
}
|
||||
@@ -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<Dictionary<string, Tuple<string, string>>> patternReplacements =
|
||||
new Lazy<Dictionary<string, Tuple<string, string>>>(() =>
|
||||
{
|
||||
// key: the literal string to match
|
||||
// value: a tuple: first item: the search pattern, second item: the replacement
|
||||
return new Dictionary<string, Tuple<string, string>>()
|
||||
{
|
||||
[@"~~"] = new Tuple<string, string>(@"~~", "~"),
|
||||
[@"~*"] = new Tuple<string, string>(@"~\*", @"\*"),
|
||||
[@"~?"] = new Tuple<string, string>(@"~\?", @"\?"),
|
||||
[@"?"] = new Tuple<string, string>(@"\?", ".?"),
|
||||
[@"*"] = new Tuple<string, string>(@"\*", ".*"),
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get total count of cells in the specified range without initalizing them all
|
||||
/// (which might cause serious performance issues on column-wide calculations).
|
||||
/// </summary>
|
||||
/// <param name="rangeExpression">Expression referring to the cell range.</param>
|
||||
/// <returns>Total number of cells in the range.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="ClosedXML.Excel.CalcEngine.Exceptions.CalcEngineException" />
|
||||
public class CellReferenceException : CalcEngineException
|
||||
{
|
||||
internal CellReferenceException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal CellReferenceException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal CellReferenceException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="ClosedXML.Excel.CalcEngine.Exceptions.CalcEngineException" />
|
||||
public class CellValueException : CalcEngineException
|
||||
{
|
||||
internal CellValueException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal CellValueException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal CellValueException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="System.DivideByZeroException" />
|
||||
public class DivisionByZeroException : CalcEngineException
|
||||
{
|
||||
internal DivisionByZeroException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal DivisionByZeroException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal DivisionByZeroException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="System.ApplicationException" />
|
||||
public class NameNotRecognizedException : CalcEngineException
|
||||
{
|
||||
internal NameNotRecognizedException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal NameNotRecognizedException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal NameNotRecognizedException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.ApplicationException" />
|
||||
public class NoValueAvailableException : CalcEngineException
|
||||
{
|
||||
internal NoValueAvailableException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal NoValueAvailableException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal NoValueAvailableException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="ClosedXML.Excel.CalcEngine.Exceptions.CalcEngineException" />
|
||||
public class NullValueException : CalcEngineException
|
||||
{
|
||||
internal NullValueException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal NullValueException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal NullValueException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <seealso cref="ClosedXML.Excel.CalcEngine.Exceptions.CalcEngineException" />
|
||||
public class NumberException : CalcEngineException
|
||||
{
|
||||
internal NumberException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
internal NumberException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
internal NumberException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class that represents parsed expressions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example:
|
||||
/// <code>
|
||||
/// Expression expr = scriptEngine.Parse(strExpression);
|
||||
/// object val = expr.Evaluate();
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
internal class Expression : ExpressionBase, IComparable<Expression>
|
||||
{
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#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<Expression>
|
||||
|
||||
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<Expression>
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#region ** ExpressionBase
|
||||
|
||||
public override string LastParseItem
|
||||
{
|
||||
get { return _token?.Value?.ToString() ?? "Unknown value"; }
|
||||
}
|
||||
|
||||
#endregion ** ExpressionBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unary expression, e.g. +123
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary expression, e.g. 1+2
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function call expression, e.g. sin(0.5)
|
||||
/// </summary>
|
||||
internal class FunctionExpression : Expression
|
||||
{
|
||||
// ** ctor
|
||||
internal FunctionExpression()
|
||||
{ }
|
||||
|
||||
public FunctionExpression(FunctionDefinition function, List<Expression> parms)
|
||||
{
|
||||
FunctionDefinition = function;
|
||||
Parameters = parms;
|
||||
}
|
||||
|
||||
// ** object model
|
||||
override public object Evaluate()
|
||||
{
|
||||
return FunctionDefinition.Function(Parameters);
|
||||
}
|
||||
|
||||
public FunctionDefinition FunctionDefinition { get; }
|
||||
public List<Expression> 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; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple variable reference.
|
||||
/// </summary>
|
||||
internal class VariableExpression : Expression
|
||||
{
|
||||
private readonly Dictionary<string, object> _dct;
|
||||
private readonly string _name;
|
||||
|
||||
public VariableExpression(Dictionary<string, object> dct, string name)
|
||||
{
|
||||
_dct = dct;
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public override object Evaluate()
|
||||
{
|
||||
return _dct[_name];
|
||||
}
|
||||
|
||||
public override string LastParseItem
|
||||
{
|
||||
get { return _name; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expression that represents an external object.
|
||||
/// </summary>
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expression that represents an omitted parameter.
|
||||
/// </summary>
|
||||
internal class EmptyValueExpression : Expression
|
||||
{
|
||||
internal EmptyValueExpression()
|
||||
{
|
||||
}
|
||||
|
||||
public override string LastParseItem
|
||||
{
|
||||
get { return "<EMPTY VALUE>"; }
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public interface IValueObject
|
||||
{
|
||||
object GetValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Caches expressions based on their string representation.
|
||||
/// This saves parsing time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses weak references to avoid accumulating unused expressions.
|
||||
/// </remarks>
|
||||
class ExpressionCache
|
||||
{
|
||||
Dictionary<string, WeakReference> _dct;
|
||||
CalcEngine _ce;
|
||||
int _hitCount;
|
||||
|
||||
public ExpressionCache(CalcEngine ce)
|
||||
{
|
||||
_ce = ce;
|
||||
_dct = new Dictionary<string, WeakReference>();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when the strings to be parsed to an expression is invalid.
|
||||
/// </summary>
|
||||
public class ExpressionParseException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExpressionParseException class with a
|
||||
/// specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public ExpressionParseException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Function definition class (keeps function name, parameter counts, and delegate).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Expression> p)
|
||||
{
|
||||
var b = true;
|
||||
foreach (var v in p)
|
||||
{
|
||||
b = b && (bool)v;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates number of business days, taking into account:
|
||||
/// - weekends (Saturdays and Sundays)
|
||||
/// - bank holidays in the middle of the week
|
||||
/// </summary>
|
||||
/// <param name="firstDay">First day in the time interval</param>
|
||||
/// <param name="lastDay">Last day in the time interval</param>
|
||||
/// <param name="bankHolidays">List of bank holidays excluding weekends</param>
|
||||
/// <returns>Number of business days during the 'span'</returns>
|
||||
private static int BusinessDaysUntil(DateTime firstDay, DateTime lastDay, IEnumerable<DateTime> 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<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
var date = (string)p[0];
|
||||
|
||||
return (int)Math.Floor(DateTime.Parse(date).ToOADate());
|
||||
}
|
||||
|
||||
private static object Day(List<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return date.Day;
|
||||
}
|
||||
|
||||
private static object Days(List<Expression> p)
|
||||
{
|
||||
Type type;
|
||||
|
||||
int end_date;
|
||||
|
||||
type = p[0]._token.Value.GetType();
|
||||
if (type == typeof(string))
|
||||
end_date = (int)Datevalue(new List<Expression>() { 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<Expression>() { p[1] });
|
||||
else
|
||||
start_date = (int)p[1];
|
||||
|
||||
return end_date - start_date;
|
||||
}
|
||||
|
||||
private static object Days360(List<Expression> 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<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
var mod = (int)p[1];
|
||||
|
||||
var retDate = date.AddMonths(mod);
|
||||
return retDate;
|
||||
}
|
||||
|
||||
private static object Eomonth(List<Expression> 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<Int32>();
|
||||
for (int year = date1.Year; year <= date2.Year; year++)
|
||||
daysInYears.Add(DateTime.IsLeapYear(year) ? 366 : 365);
|
||||
return daysInYears.Average();
|
||||
}
|
||||
|
||||
private static object Hour(List<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return date.Minute;
|
||||
}
|
||||
|
||||
private static object Month(List<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return date.Month;
|
||||
}
|
||||
|
||||
private static object Networkdays(List<Expression> p)
|
||||
{
|
||||
var date1 = (DateTime)p[0];
|
||||
var date2 = (DateTime)p[1];
|
||||
var bankHolidays = new List<DateTime>();
|
||||
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<Expression> p)
|
||||
{
|
||||
return DateTime.Now;
|
||||
}
|
||||
|
||||
private static object Second(List<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return date.Second;
|
||||
}
|
||||
|
||||
private static object Time(List<Expression> 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<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return (DateTime.MinValue + date.TimeOfDay).ToOADate();
|
||||
}
|
||||
|
||||
private static object Today(List<Expression> p)
|
||||
{
|
||||
return DateTime.Today;
|
||||
}
|
||||
|
||||
private static object Weekday(List<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
var startDate = (DateTime)p[0];
|
||||
var daysRequired = (int)p[1];
|
||||
|
||||
if (daysRequired == 0) return startDate;
|
||||
|
||||
var bankHolidays = new List<DateTime>();
|
||||
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<DateTime> 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<Expression> p)
|
||||
{
|
||||
var date = (DateTime)p[0];
|
||||
|
||||
return date.Year;
|
||||
}
|
||||
|
||||
private static object Yearfrac(List<Expression> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ErrorExpression.ExpressionErrorType, int> errorTypes = new Dictionary<ErrorExpression.ExpressionErrorType, int>()
|
||||
{
|
||||
[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<Expression> p)
|
||||
{
|
||||
var v = p[0].Evaluate();
|
||||
|
||||
if (v is ErrorExpression.ExpressionErrorType)
|
||||
return errorTypes[(ErrorExpression.ExpressionErrorType)v];
|
||||
else
|
||||
throw new NoValueAvailableException();
|
||||
}
|
||||
|
||||
static object IsBlank(List<Expression> 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<Expression> p)
|
||||
{
|
||||
var v = p[0].Evaluate();
|
||||
|
||||
return v is ErrorExpression.ExpressionErrorType
|
||||
&& ((ErrorExpression.ExpressionErrorType)v) != ErrorExpression.ExpressionErrorType.NoValueAvailable;
|
||||
}
|
||||
|
||||
static object IsError(List<Expression> p)
|
||||
{
|
||||
var v = p[0].Evaluate();
|
||||
|
||||
return v is ErrorExpression.ExpressionErrorType;
|
||||
}
|
||||
|
||||
static object IsEven(List<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
var v = p[0].Evaluate();
|
||||
|
||||
return v is ErrorExpression.ExpressionErrorType
|
||||
&& ((ErrorExpression.ExpressionErrorType)v) == ErrorExpression.ExpressionErrorType.NoValueAvailable;
|
||||
}
|
||||
|
||||
static object IsNonText(List<Expression> p)
|
||||
{
|
||||
return !(bool) IsText(p);
|
||||
}
|
||||
|
||||
static object IsNumber(List<Expression> 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<Expression> p)
|
||||
{
|
||||
return !(bool) IsEven(p);
|
||||
}
|
||||
|
||||
static object IsRef(List<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
return (double) p[0];
|
||||
}
|
||||
|
||||
static object NA(List<Expression> p)
|
||||
{
|
||||
return ErrorExpression.ExpressionErrorType.NoValueAvailable;
|
||||
}
|
||||
|
||||
static object Type(List<Expression> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Expression> p)
|
||||
{
|
||||
var b = true;
|
||||
foreach (var v in p)
|
||||
{
|
||||
b = b && v;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static object Or(List<Expression> p)
|
||||
{
|
||||
var b = false;
|
||||
foreach (var v in p)
|
||||
{
|
||||
b = b || v;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static object Not(List<Expression> p)
|
||||
{
|
||||
return !p[0];
|
||||
}
|
||||
|
||||
static object If(List<Expression> 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<Expression> p)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static object False(List<Expression> p)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static object IfError(List<Expression> p)
|
||||
{
|
||||
try
|
||||
{
|
||||
return p[0].Evaluate();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return p[1].Evaluate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Expression> 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<Expression> p)
|
||||
{
|
||||
String address = p[0];
|
||||
String toolTip = p.Count == 2 ? p[1] : String.Empty;
|
||||
return new XLHyperlink(address, toolTip);
|
||||
}
|
||||
|
||||
private static object Index(List<Expression> 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<Expression> 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<int> 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<Expression> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Average();
|
||||
}
|
||||
|
||||
private static object AverageA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Average();
|
||||
}
|
||||
|
||||
private static object Count(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Count();
|
||||
}
|
||||
|
||||
private static object CountA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Count();
|
||||
}
|
||||
|
||||
private static object CountBlank(List<Expression> 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<Expression> 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<Expression> 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<object, List<object>>[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<object>();
|
||||
foreach (var value in criteriaRange)
|
||||
{
|
||||
criteriaRangeValues.Add(value);
|
||||
}
|
||||
|
||||
criteriaRanges[criteriaPair] = new Tuple<object, List<object>>(
|
||||
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<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Max();
|
||||
}
|
||||
|
||||
private static object MaxA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Max();
|
||||
}
|
||||
|
||||
private static object Min(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Min();
|
||||
}
|
||||
|
||||
private static object MinA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Min();
|
||||
}
|
||||
|
||||
private static object StDev(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Std();
|
||||
}
|
||||
|
||||
private static object StDevA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Std();
|
||||
}
|
||||
|
||||
private static object StDevP(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).StdP();
|
||||
}
|
||||
|
||||
private static object StDevPA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).StdP();
|
||||
}
|
||||
|
||||
private static object Var(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).Var();
|
||||
}
|
||||
|
||||
private static object VarA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).Var();
|
||||
}
|
||||
|
||||
private static object VarP(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, true).VarP();
|
||||
}
|
||||
|
||||
private static object VarPA(List<Expression> p)
|
||||
{
|
||||
return GetTally(p, false).VarP();
|
||||
}
|
||||
|
||||
// utility for tallying statistics
|
||||
private static Tally GetTally(List<Expression> p, bool numbersOnly)
|
||||
{
|
||||
return new Tally(p, numbersOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Object>
|
||||
{
|
||||
private readonly List<object> _list = new List<object>();
|
||||
private readonly bool NumbersOnly;
|
||||
|
||||
private double[] _numericValues;
|
||||
|
||||
public Tally()
|
||||
: this(false)
|
||||
{ }
|
||||
|
||||
public Tally(bool numbersOnly)
|
||||
: this(null, numbersOnly)
|
||||
{ }
|
||||
|
||||
public Tally(IEnumerable<Expression> p)
|
||||
: this(p, false)
|
||||
{ }
|
||||
|
||||
public Tally(IEnumerable<Expression> 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<double> 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<double> 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<double> 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<object> GetEnumerator()
|
||||
{
|
||||
return _list.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Expression> 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<Expression> p)
|
||||
{
|
||||
var s = (string)p[0];
|
||||
return (int)s[0];
|
||||
}
|
||||
|
||||
private static object Concat(List<Expression> 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<Expression> 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<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
return ((string)p[0]).Length;
|
||||
}
|
||||
|
||||
private static object Lower(List<Expression> p)
|
||||
{
|
||||
return ((string)p[0]).ToLower();
|
||||
}
|
||||
|
||||
private static object Mid(List<Expression> 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<Expression> 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<Expression> 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<Expression> 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<Expression> 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<Expression> 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<Expression> 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<Expression> p)
|
||||
{
|
||||
if (p[0]._token.Value.GetType() == typeof(string))
|
||||
return (string)p[0];
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
private static object _Text(List<Expression> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A function to Join text https://support.office.com/en-us/article/textjoin-function-357b449a-ec91-49d0-80c3-0e8fc845691c
|
||||
/// </summary>
|
||||
/// <param name="p">Parameters</param>
|
||||
/// <returns> string </returns>
|
||||
/// <exception cref="ApplicationException">
|
||||
/// Delimiter in first param must be a string
|
||||
/// or
|
||||
/// Second param must be a boolen (TRUE/FALSE)
|
||||
/// </exception>
|
||||
private static object TextJoin(List<Expression> p)
|
||||
{
|
||||
var values = new List<string>();
|
||||
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<string> cellValues;
|
||||
if (ignoreEmptyStrings)
|
||||
cellValues = range.CellsUsed()
|
||||
.Select(c => c.GetString())
|
||||
.Where(s => !string.IsNullOrEmpty(s));
|
||||
else
|
||||
cellValues = (range as XLRange).CellValues()
|
||||
.Cast<object>()
|
||||
.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<Expression> 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<Expression> p)
|
||||
{
|
||||
return ((string)p[0]).ToUpper();
|
||||
}
|
||||
|
||||
private static object Value(List<Expression> p)
|
||||
{
|
||||
return double.Parse(p[0], NumberStyles.Any, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static object NumberValue(List<Expression> 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<Expression> p)
|
||||
{
|
||||
return (string)p[0];
|
||||
}
|
||||
|
||||
private static object Clean(List<Expression> 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<Expression> p)
|
||||
{
|
||||
Double value = p[0];
|
||||
int dec = p.Count == 2 ? (int)p[1] : 2;
|
||||
|
||||
return value.ToString("C" + dec);
|
||||
}
|
||||
|
||||
private static object Exact(List<Expression> p)
|
||||
{
|
||||
var t1 = (string)p[0];
|
||||
var t2 = (string)p[1];
|
||||
|
||||
return t1 == t2;
|
||||
}
|
||||
|
||||
private static object Fixed(List<Expression> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a node in the expression tree.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token types (used when building expressions, sequence defines operator priority)
|
||||
/// </summary>
|
||||
internal enum TKTYPE
|
||||
{
|
||||
COMPARE, // < > = <= >=
|
||||
ADDSUB, // + -
|
||||
MULDIV, // * /
|
||||
POWER, // ^
|
||||
MULDIV_UNARY,// %
|
||||
GROUP, // ( ) , .
|
||||
LITERAL, // 123.32, "Hello", etc.
|
||||
IDENTIFIER, // functions, external objects, bindings
|
||||
ERROR // e.g. #REF!
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token ID (used when evaluating expressions)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
internal class XLAddressComparer : IEqualityComparer<IXLAddress>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IXLRange> _cellRanges;
|
||||
|
||||
public ExpressionCache ExpressionCache => this._cache;
|
||||
|
||||
/// <summary>
|
||||
/// Get a collection of cell ranges included into the expression. Order is not preserved.
|
||||
/// </summary>
|
||||
/// <param name="expression">Formula to parse.</param>
|
||||
/// <returns>Collection of ranges included into the expression.</returns>
|
||||
public IEnumerable<IXLRange> GetPrecedentRanges(string expression)
|
||||
{
|
||||
_cellRanges = new List<IXLRange>();
|
||||
Parse(expression);
|
||||
var ranges = _cellRanges;
|
||||
_cellRanges = null;
|
||||
var visitedRanges = new HashSet<IXLRangeAddress>(new XLRangeAddressComparer(true));
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
if (!visitedRanges.Contains(range.RangeAddress))
|
||||
{
|
||||
visitedRanges.Add(range.RangeAddress);
|
||||
yield return range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<IXLCell> GetPrecedentCells(string expression)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
var ranges = GetPrecedentRanges(expression);
|
||||
var visitedCells = new HashSet<IXLAddress>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel.CalcEngine
|
||||
{
|
||||
internal class XLRangeAddressComparer : IEqualityComparer<IXLRangeAddress>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
/// <summary>Gets this cell's address, relative to the worksheet.</summary>
|
||||
/// <value>The cell's address.</value>
|
||||
IXLAddress Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculated value of cell formula. Is used for decreasing number of computations perfromed.
|
||||
/// May hold invalid value when <see cref="NeedsRecalculation"/> flag is True.
|
||||
/// </summary>
|
||||
Object CachedValue { get; }
|
||||
|
||||
IXLComment Comment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current region. The current region is a range bounded by any combination of blank rows and blank columns
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The current region.
|
||||
/// </value>
|
||||
IXLRange CurrentRegion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of this cell's data.
|
||||
/// <para>Changing the data type will cause ClosedXML to covert the current value to the new data type.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to the new data type.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the cell's data.
|
||||
/// </value>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
XLDataType DataType { get; set; }
|
||||
|
||||
IXLDataValidation DataValidation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cell's formula with A1 references.
|
||||
/// </summary>
|
||||
/// <value>The formula with A1 references.</value>
|
||||
String FormulaA1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cell's formula with R1C1 references.
|
||||
/// </summary>
|
||||
/// <value>The formula with R1C1 references.</value>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating that previously calculated cell value may be not valid anymore and has to be re-evaluated.
|
||||
/// </summary>
|
||||
Boolean NeedsRecalculation { get; }
|
||||
|
||||
IXLDataValidation NewDataValidation { get; }
|
||||
|
||||
IXLRichText RichText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this cell's text should be shared or not.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// If false the cell's text will not be shared and stored as an inline value.
|
||||
/// </value>
|
||||
Boolean ShareString { get; set; }
|
||||
|
||||
IXLSparkline Sparkline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cell's style.
|
||||
/// </summary>
|
||||
IXLStyle Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cell's value. To get or set a strongly typed value, use the GetValue<T> and SetValue methods.
|
||||
/// <para>ClosedXML will try to detect the data type through parsing. If it can't then the value will be left as a string.</para>
|
||||
/// <para>If the object is an IEnumerable, ClosedXML will copy the collection's data into a table starting from this cell.</para>
|
||||
/// <para>If the object is a range, ClosedXML will copy the range starting from this cell.</para>
|
||||
/// <para>Setting the value to an object (not IEnumerable/range) will call the object's ToString() method.</para>
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The object containing the value(s) to set.
|
||||
/// </value>
|
||||
Object Value { get; set; }
|
||||
|
||||
IXLWorksheet Worksheet { get; }
|
||||
|
||||
IXLConditionalFormat AddConditionalFormat();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a named range out of this cell.
|
||||
/// <para>If the named range exists, it will add this range to that named range.</para>
|
||||
/// <para>The default scope for the named range is Workbook.</para>
|
||||
/// </summary>
|
||||
/// <param name="rangeName">Name of the range.</param>
|
||||
IXLCell AddToNamed(String rangeName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a named range out of this cell.
|
||||
/// <para>If the named range exists, it will add this range to that named range.</para>
|
||||
/// <param name="rangeName">Name of the range.</param>
|
||||
/// <param name="scope">The scope for the named range.</param>
|
||||
/// </summary>
|
||||
IXLCell AddToNamed(String rangeName, XLScope scope);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a named range out of this cell.
|
||||
/// <para>If the named range exists, it will add this range to that named range.</para>
|
||||
/// <param name="rangeName">Name of the range.</param>
|
||||
/// <param name="scope">The scope for the named range.</param>
|
||||
/// <param name="comment">The comments for the named range.</param>
|
||||
/// </summary>
|
||||
IXLCell AddToNamed(String rangeName, XLScope scope, String comment);
|
||||
|
||||
/// <summary>
|
||||
/// Returns this cell as an IXLRange.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the contents of this cell.
|
||||
/// </summary>
|
||||
/// <param name="clearOptions">Specify what you want to clear.</param>
|
||||
IXLCell Clear(XLClearOptions clearOptions = XLClearOptions.All);
|
||||
|
||||
IXLCell CopyFrom(IXLCell otherCell);
|
||||
|
||||
IXLCell CopyFrom(String otherCell);
|
||||
|
||||
IXLCell CopyTo(IXLCell target);
|
||||
|
||||
IXLCell CopyTo(String target);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the current cell and shifts the surrounding cells according to the shiftDeleteCells parameter.
|
||||
/// </summary>
|
||||
/// <param name="shiftDeleteCells">How to shift the surrounding cells.</param>
|
||||
void Delete(XLShiftDeletedCells shiftDeleteCells);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to Boolean.
|
||||
/// <para>ClosedXML will try to covert the current value to Boolean.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to Boolean.</para>
|
||||
/// </summary>
|
||||
Boolean GetBoolean();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to DateTime.
|
||||
/// <para>ClosedXML will try to covert the current value to DateTime.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to DateTime.</para>
|
||||
/// </summary>
|
||||
DateTime GetDateTime();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to Double.
|
||||
/// <para>ClosedXML will try to covert the current value to Double.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to Double.</para>
|
||||
/// </summary>
|
||||
Double GetDouble();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value formatted depending on the cell's data type and style.
|
||||
/// </summary>
|
||||
String GetFormattedString();
|
||||
|
||||
XLHyperlink GetHyperlink();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to a String.
|
||||
/// </summary>
|
||||
String GetString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to TimeSpan.
|
||||
/// <para>ClosedXML will try to covert the current value to TimeSpan.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to TimeSpan.</para>
|
||||
/// </summary>
|
||||
TimeSpan GetTimeSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell's value converted to the T type.
|
||||
/// <para>ClosedXML will try to covert the current value to the T type.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to the T type.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type.</typeparam>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
T GetValue<T>();
|
||||
|
||||
IXLCells InsertCellsAbove(int numberOfRows);
|
||||
|
||||
IXLCells InsertCellsAfter(int numberOfColumns);
|
||||
|
||||
IXLCells InsertCellsBefore(int numberOfColumns);
|
||||
|
||||
IXLCells InsertCellsBelow(int numberOfRows);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the IEnumerable data elements and returns the range it occupies.
|
||||
/// </summary>
|
||||
/// <param name="data">The IEnumerable data.</param>
|
||||
IXLRange InsertData(IEnumerable data);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the IEnumerable data elements and returns the range it occupies.
|
||||
/// </summary>
|
||||
/// <param name="data">The IEnumerable data.</param>
|
||||
/// <param name="transpose">if set to <c>true</c> the data will be transposed before inserting.</param>
|
||||
/// <returns></returns>
|
||||
IXLRange InsertData(IEnumerable data, Boolean transpose);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the data of a data table.
|
||||
/// </summary>
|
||||
/// <param name="dataTable">The data table.</param>
|
||||
/// <returns>The range occupied by the inserted data</returns>
|
||||
IXLRange InsertData(DataTable dataTable);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the IEnumerable data elements as a table and returns it.
|
||||
/// <para>The new table will receive a generic name: Table#</para>
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
IXLTable InsertTable<T>(IEnumerable<T> data);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the IEnumerable data elements as a table and returns it.
|
||||
/// <para>The new table will receive a generic name: Table#</para>
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="createTable">
|
||||
/// if set to <c>true</c> it will create an Excel table.
|
||||
/// <para>if set to <c>false</c> the table will be created in memory.</para>
|
||||
/// </param>
|
||||
IXLTable InsertTable<T>(IEnumerable<T> data, Boolean createTable);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Excel table from the given IEnumerable data elements.
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="tableName">Name of the table.</param>
|
||||
IXLTable InsertTable<T>(IEnumerable<T> data, String tableName);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the IEnumerable data elements as a table and returns it.
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="tableName">Name of the table.</param>
|
||||
/// <param name="createTable">
|
||||
/// if set to <c>true</c> it will create an Excel table.
|
||||
/// <para>if set to <c>false</c> the table will be created in memory.</para>
|
||||
/// </param>
|
||||
IXLTable InsertTable<T>(IEnumerable<T> data, String tableName, Boolean createTable);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the DataTable data elements as a table and returns it.
|
||||
/// <para>The new table will receive a generic name: Table#</para>
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
IXLTable InsertTable(DataTable data);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the DataTable data elements as a table and returns it.
|
||||
/// <para>The new table will receive a generic name: Table#</para>
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="createTable">
|
||||
/// if set to <c>true</c> it will create an Excel table.
|
||||
/// <para>if set to <c>false</c> the table will be created in memory.</para>
|
||||
/// </param>
|
||||
IXLTable InsertTable(DataTable data, Boolean createTable);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Excel table from the given DataTable data elements.
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="tableName">Name of the table.</param>
|
||||
IXLTable InsertTable(DataTable data, String tableName);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the DataTable data elements as a table and returns it.
|
||||
/// </summary>
|
||||
/// <param name="data">The table data.</param>
|
||||
/// <param name="tableName">Name of the table.</param>
|
||||
/// <param name="createTable">
|
||||
/// if set to <c>true</c> it will create an Excel table.
|
||||
/// <para>if set to <c>false</c> the table will be created in memory.</para>
|
||||
/// </param>
|
||||
IXLTable InsertTable(DataTable data, String tableName, Boolean createTable);
|
||||
|
||||
/// <summary>
|
||||
/// Invalidate <see cref="CachedValue"/> so the formula will be re-evaluated next time <see cref="Value"/> is accessed.
|
||||
/// If cell does not contain formula nothing happens.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type of this cell's data.
|
||||
/// <para>Changing the data type will cause ClosedXML to covert the current value to the new data type.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to the new data type.</para>
|
||||
/// </summary>
|
||||
/// <param name="dataType">Type of the data.</param>
|
||||
/// <returns></returns>
|
||||
IXLCell SetDataType(XLDataType dataType);
|
||||
|
||||
IXLDataValidation SetDataValidation();
|
||||
|
||||
IXLCell SetFormulaA1(String formula);
|
||||
|
||||
IXLCell SetFormulaR1C1(String formula);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cell's value.
|
||||
/// <para>If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from this cell.</para>
|
||||
/// <para>If the object is a range ClosedXML will copy the range starting from this cell.</para>
|
||||
/// <para>Setting the value to an object (not IEnumerable/range) will call the object's ToString() method.</para>
|
||||
/// <para>ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The object containing the value(s) to set.
|
||||
/// </value>
|
||||
IXLCell SetValue<T>(T value);
|
||||
|
||||
XLTableCellType TableCellType();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current state of the cell according to the format.
|
||||
/// </summary>
|
||||
/// <param name="format">A: address, F: formula, NF: number format, BG: background color, FG: foreground color, V: formatted value</param>
|
||||
/// <returns></returns>
|
||||
string ToString(string format);
|
||||
|
||||
Boolean TryGetValue<T>(out T value);
|
||||
|
||||
IXLColumn WorksheetColumn();
|
||||
|
||||
IXLRow WorksheetRow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLCells : IEnumerable<IXLCell>
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the cells' value.
|
||||
/// <para>If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell.</para>
|
||||
/// <para>If the object is a range ClosedXML will copy the range starting from each cell.</para>
|
||||
/// <para>Setting the value to an object (not IEnumerable/range) will call the object's ToString() method.</para>
|
||||
/// <para>ClosedXML will try to translate it to the corresponding type, if it can't then the value will be left as a string.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The object containing the value(s) to set.
|
||||
/// </value>
|
||||
Object Value { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type of the cells' data.
|
||||
/// <para>Changing the data type will cause ClosedXML to covert the current value to the new data type.</para>
|
||||
/// <para>An exception will be thrown if the current value cannot be converted to the new data type.</para>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the cell's data.
|
||||
/// </value>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
XLDataType DataType { set; }
|
||||
|
||||
IXLCells SetDataType(XLDataType dataType);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the contents of these cells.
|
||||
/// </summary>
|
||||
/// <param name="clearOptions">Specify what you want to clear.</param>
|
||||
IXLCells Clear(XLClearOptions clearOptions = XLClearOptions.All);
|
||||
|
||||
/// <summary>
|
||||
/// Delete the comments of these cells.
|
||||
/// </summary>
|
||||
void DeleteComments();
|
||||
|
||||
/// <summary>
|
||||
/// Delete the sparklines of these cells.
|
||||
/// </summary>
|
||||
void DeleteSparklines();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cells' formula with A1 references.
|
||||
/// </summary>
|
||||
/// <value>The formula with A1 references.</value>
|
||||
String FormulaA1 { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cells' formula with R1C1 references.
|
||||
/// </summary>
|
||||
/// <value>The formula with R1C1 references.</value>
|
||||
String FormulaR1C1 { set; }
|
||||
|
||||
IXLStyle Style { get; set; }
|
||||
|
||||
void Select();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<XLCell>
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly List<XLRangeAddress> _rangeAddresses = new List<XLRangeAddress>();
|
||||
private readonly bool _usedCellsOnly;
|
||||
private readonly Func<IXLCell, Boolean> _predicate;
|
||||
private readonly XLCellsUsedOptions _options;
|
||||
private bool _styleInitialized = false;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructor
|
||||
|
||||
public XLCells(bool usedCellsOnly, XLCellsUsedOptions options, Func<IXLCell, Boolean> predicate = null)
|
||||
: base(XLStyle.Default.Value)
|
||||
{
|
||||
_usedCellsOnly = usedCellsOnly;
|
||||
_options = options;
|
||||
_predicate = predicate ?? (_ => true);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region IEnumerable<XLCell> Members
|
||||
|
||||
private IEnumerable<XLCell> 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<XLSheetPoint> 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<XLCell> 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<XLAddress>();
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (visitedCells.Contains(cell.Address)) continue;
|
||||
|
||||
visitedCells.Add(cell.Address);
|
||||
|
||||
yield return cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<XLCell> GetUsedCellsInRange(XLRangeAddress rangeAddress, XLWorksheet worksheet, IEnumerable<XLSheetPoint> 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<XLSheetPoint> GetUsedCellsCandidates(XLWorksheet worksheet)
|
||||
{
|
||||
var candidates = Enumerable.Empty<XLSheetPoint>();
|
||||
|
||||
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<XLCell> GetEnumerator()
|
||||
{
|
||||
var cells = (_usedCellsOnly) ? GetUsedCells() : GetAllCells();
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
yield return cell;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IEnumerable<XLCell> Members
|
||||
|
||||
#region IXLCells Members
|
||||
|
||||
IEnumerator<IXLCell> IEnumerable<IXLCell>.GetEnumerator()
|
||||
{
|
||||
foreach (XLCell cell in this)
|
||||
yield return cell;
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public Object Value
|
||||
{
|
||||
set { this.ForEach<XLCell>(c => c.Value = value); }
|
||||
}
|
||||
|
||||
public IXLCells SetDataType(XLDataType dataType)
|
||||
{
|
||||
this.ForEach<XLCell>(c => c.DataType = dataType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public XLDataType DataType
|
||||
{
|
||||
set { this.ForEach<XLCell>(c => c.DataType = value); }
|
||||
}
|
||||
|
||||
public IXLCells Clear(XLClearOptions clearOptions = XLClearOptions.All)
|
||||
{
|
||||
this.ForEach<XLCell>(c => c.Clear(clearOptions));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void DeleteComments()
|
||||
{
|
||||
this.ForEach<XLCell>(c => c.DeleteComment());
|
||||
}
|
||||
|
||||
public void DeleteSparklines()
|
||||
{
|
||||
this.ForEach<XLCell>(c => c.DeleteSparkline());
|
||||
}
|
||||
|
||||
public String FormulaA1
|
||||
{
|
||||
set { this.ForEach<XLCell>(c => c.FormulaA1 = value); }
|
||||
}
|
||||
|
||||
public String FormulaR1C1
|
||||
{
|
||||
set { this.ForEach<XLCell>(c => c.FormulaR1C1 = value); }
|
||||
}
|
||||
|
||||
#endregion IXLCells Members
|
||||
|
||||
#region IXLStylized Members
|
||||
|
||||
public override IEnumerable<IXLStyle> Styles
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return Style;
|
||||
foreach (XLCell c in this)
|
||||
yield return c.Style;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<XLStylizedBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (XLCell c in this)
|
||||
yield return c;
|
||||
}
|
||||
}
|
||||
|
||||
public override IXLRanges RangesUsed
|
||||
{
|
||||
get
|
||||
{
|
||||
var retVal = new XLRanges();
|
||||
this.ForEach<XLCell>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
internal class XLCellsCollection
|
||||
{
|
||||
internal Dictionary<Int32, Int32> ColumnsUsed { get; } = new Dictionary<int, int>();
|
||||
internal Dictionary<Int32, HashSet<Int32>> Deleted { get; } = new Dictionary<int, HashSet<int>>();
|
||||
internal Dictionary<int, Dictionary<int, XLCell>> RowsCollection { get; } = new Dictionary<int, Dictionary<int, XLCell>>();
|
||||
|
||||
public Int32 MaxColumnUsed;
|
||||
public Int32 MaxRowUsed;
|
||||
public Dictionary<Int32, Int32> RowsUsed = new Dictionary<int, int>();
|
||||
|
||||
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<int, XLCell> columnsCollection))
|
||||
{
|
||||
columnsCollection = new Dictionary<int, XLCell>();
|
||||
RowsCollection.Add(row, columnsCollection);
|
||||
}
|
||||
columnsCollection.Add(column, cell);
|
||||
if (row > MaxRowUsed) MaxRowUsed = row;
|
||||
if (column > MaxColumnUsed) MaxColumnUsed = column;
|
||||
|
||||
if (Deleted.TryGetValue(row, out HashSet<int> delHash))
|
||||
delHash.Remove(column);
|
||||
}
|
||||
|
||||
private static void IncrementUsage(Dictionary<int, int> dictionary, Int32 key)
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out Int32 value))
|
||||
dictionary[key] = value + 1;
|
||||
else
|
||||
dictionary.Add(key, 1);
|
||||
}
|
||||
|
||||
/// <summary/>
|
||||
/// <returns>True if the number was lowered to zero so MaxColumnUsed or MaxRowUsed may require
|
||||
/// recomputation.</returns>
|
||||
private static bool DecrementUsage(Dictionary<int, int> 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<Int32> delHash))
|
||||
{
|
||||
if (!delHash.Contains(column))
|
||||
delHash.Add(column);
|
||||
}
|
||||
else
|
||||
{
|
||||
delHash = new HashSet<int>();
|
||||
delHash.Add(column);
|
||||
Deleted.Add(row, delHash);
|
||||
}
|
||||
|
||||
if (RowsCollection.TryGetValue(row, out Dictionary<Int32, XLCell> columnsCollection))
|
||||
{
|
||||
columnsCollection.Remove(column);
|
||||
if (columnsCollection.Count == 0)
|
||||
{
|
||||
RowsCollection.Remove(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable<XLCell> GetCells(Int32 rowStart, Int32 columnStart,
|
||||
Int32 rowEnd, Int32 columnEnd,
|
||||
Func<IXLCell, Boolean> 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<Int32, XLCell> 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<IXLCell, Boolean> 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<Int32, XLCell> 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<IXLCell, Boolean> 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<Int32, XLCell> 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<IXLCell, Boolean> 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<Int32, XLCell> 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<IXLCell, Boolean> 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<int, XLCell> 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<int, XLCell> columnsCollection))
|
||||
{
|
||||
for (int co = columnStart; co <= finalColumn; co++)
|
||||
{
|
||||
if (columnsCollection.ContainsKey(co))
|
||||
Remove(ro, co);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<XLSheetPoint> 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<Int32, XLCell> 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<Int32, XLCell> 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<XLCell> GetCells()
|
||||
{
|
||||
return GetCells(1, 1, MaxRowUsed, MaxColumnUsed);
|
||||
}
|
||||
|
||||
internal IEnumerable<XLCell> GetCells(Func<IXLCell, Boolean> predicate)
|
||||
{
|
||||
for (int ro = 1; ro <= MaxRowUsed; ro++)
|
||||
{
|
||||
if (RowsCollection.TryGetValue(ro, out Dictionary<Int32, XLCell> 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<Int32, XLCell> columnsCollection)
|
||||
&& columnsCollection.ContainsKey(column);
|
||||
}
|
||||
|
||||
public Int32 MinRowInColumn(Int32 column)
|
||||
{
|
||||
for (int row = 1; row <= MaxRowUsed; row++)
|
||||
{
|
||||
if (RowsCollection.TryGetValue(row, out Dictionary<Int32, XLCell> 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<Int32, XLCell> columnsCollection)
|
||||
&& columnsCollection.ContainsKey(column))
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Int32 MinColumnInRow(Int32 row)
|
||||
{
|
||||
if (RowsCollection.TryGetValue(row, out Dictionary<Int32, XLCell> columnsCollection)
|
||||
&& columnsCollection.Any())
|
||||
|
||||
return columnsCollection.Keys.Min();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Int32 MaxColumnInRow(Int32 row)
|
||||
{
|
||||
if (RowsCollection.TryGetValue(row, out Dictionary<Int32, XLCell> columnsCollection)
|
||||
&& columnsCollection.Any())
|
||||
|
||||
return columnsCollection.Keys.Max();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public IEnumerable<XLCell> GetCellsInColumn(Int32 column)
|
||||
{
|
||||
return GetCells(1, column, MaxRowUsed, column);
|
||||
}
|
||||
|
||||
public IEnumerable<XLCell> GetCellsInRow(Int32 row)
|
||||
{
|
||||
return GetCells(row, 1, row, MaxColumnUsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IXLChart>
|
||||
{
|
||||
Boolean RightAngleAxes { get; set; }
|
||||
IXLChart SetRightAngleAxes();
|
||||
IXLChart SetRightAngleAxes(Boolean rightAngleAxes);
|
||||
|
||||
XLChartType ChartType { get; set; }
|
||||
IXLChart SetChartType(XLChartType chartType);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLCharts: IEnumerable<IXLChart>
|
||||
{
|
||||
void Add(IXLChart chart);
|
||||
}
|
||||
}
|
||||
@@ -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>, 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<XLChartType> Bar3DCharts = new HashSet<XLChartType> {
|
||||
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<XLChartType> HorizontalCharts = new HashSet<XLChartType>{
|
||||
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<XLChartType> ClusteredCharts = new HashSet<XLChartType>()
|
||||
{
|
||||
XLChartType.BarClustered,
|
||||
XLChartType.BarClustered3D,
|
||||
XLChartType.ColumnClustered,
|
||||
XLChartType.ColumnClustered3D,
|
||||
XLChartType.ConeClustered,
|
||||
XLChartType.ConeHorizontalClustered,
|
||||
XLChartType.CylinderClustered,
|
||||
XLChartType.CylinderHorizontalClustered,
|
||||
XLChartType.PyramidClustered,
|
||||
XLChartType.PyramidHorizontalClustered
|
||||
};
|
||||
|
||||
public HashSet<XLChartType> PercentCharts = new HashSet<XLChartType>() {
|
||||
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<XLChartType> StackedCharts = new HashSet<XLChartType>()
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
internal class XLCharts: IXLCharts
|
||||
{
|
||||
private List<IXLChart> charts = new List<IXLChart>();
|
||||
public IEnumerator<IXLChart> GetEnumerator()
|
||||
{
|
||||
return charts.GetEnumerator();
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void Add(IXLChart chart)
|
||||
{
|
||||
charts.Add(chart);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLColumn : IXLRangeBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the width of this column.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The width of this column.
|
||||
/// </value>
|
||||
Double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deletes this column and shifts the columns at the right of this one accordingly.
|
||||
/// </summary>
|
||||
void Delete();
|
||||
|
||||
/// <summary>
|
||||
/// Gets this column's number
|
||||
/// </summary>
|
||||
Int32 ColumnNumber();
|
||||
|
||||
/// <summary>
|
||||
/// Gets this column's letter
|
||||
/// </summary>
|
||||
String ColumnLetter();
|
||||
|
||||
/// <summary>
|
||||
/// Inserts X number of columns at the right of this one.
|
||||
/// <para>All columns at the right will be shifted accordingly.</para>
|
||||
/// </summary>
|
||||
/// <param name="numberOfColumns">The number of columns to insert.</param>
|
||||
IXLColumns InsertColumnsAfter(Int32 numberOfColumns);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts X number of columns at the left of this one.
|
||||
/// <para>This column and all at the right will be shifted accordingly.</para>
|
||||
/// </summary>
|
||||
/// <param name="numberOfColumns">The number of columns to insert.</param>
|
||||
IXLColumns InsertColumnsBefore(Int32 numberOfColumns);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell in the specified row.
|
||||
/// </summary>
|
||||
/// <param name="rowNumber">The cell's row.</param>
|
||||
IXLCell Cell(Int32 rowNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the specified group of cells, separated by commas.
|
||||
/// <para>e.g. Cells("1"), Cells("1:5"), Cells("1,3:5")</para>
|
||||
/// </summary>
|
||||
/// <param name="cellsInColumn">The column cells to return.</param>
|
||||
new IXLCells Cells(String cellsInColumn);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the specified group of cells.
|
||||
/// </summary>
|
||||
/// <param name="firstRow">The first row in the group of cells to return.</param>
|
||||
/// <param name="lastRow">The last row in the group of cells to return.</param>
|
||||
IXLCells Cells(Int32 firstRow, Int32 lastRow);
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of the column based on its contents.
|
||||
/// </summary>
|
||||
IXLColumn AdjustToContents();
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of the column based on its contents, starting from the startRow.
|
||||
/// </summary>
|
||||
/// <param name="startRow">The row to start calculating the column width.</param>
|
||||
IXLColumn AdjustToContents(Int32 startRow);
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of the column based on its contents, starting from the startRow and ending at endRow.
|
||||
/// </summary>
|
||||
/// <param name="startRow">The row to start calculating the column width.</param>
|
||||
/// <param name="endRow">The row to end calculating the column width.</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Hides this column.
|
||||
/// </summary>
|
||||
IXLColumn Hide();
|
||||
|
||||
/// <summary>Unhides this column.</summary>
|
||||
IXLColumn Unhide();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this column is hidden or not.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this column is hidden; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
Boolean IsHidden { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the outline level of this column.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The outline level of this column.
|
||||
/// </value>
|
||||
Int32 OutlineLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds this column to the next outline level (Increments the outline level for this column by 1).
|
||||
/// </summary>
|
||||
IXLColumn Group();
|
||||
|
||||
/// <summary>
|
||||
/// Adds this column to the next outline level (Increments the outline level for this column by 1).
|
||||
/// </summary>
|
||||
/// <param name="collapse">If set to <c>true</c> the column will be shown collapsed.</param>
|
||||
IXLColumn Group(Boolean collapse);
|
||||
|
||||
/// <summary>
|
||||
/// Sets outline level for this column.
|
||||
/// </summary>
|
||||
/// <param name="outlineLevel">The outline level.</param>
|
||||
IXLColumn Group(Int32 outlineLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Sets outline level for this column.
|
||||
/// </summary>
|
||||
/// <param name="outlineLevel">The outline level.</param>
|
||||
/// <param name="collapse">If set to <c>true</c> the column will be shown collapsed.</param>
|
||||
IXLColumn Group(Int32 outlineLevel, Boolean collapse);
|
||||
|
||||
/// <summary>
|
||||
/// Adds this column to the previous outline level (decrements the outline level for this column by 1).
|
||||
/// </summary>
|
||||
IXLColumn Ungroup();
|
||||
|
||||
/// <summary>
|
||||
/// Adds this column to the previous outline level (decrements the outline level for this column by 1).
|
||||
/// </summary>
|
||||
/// <param name="fromAll">If set to <c>true</c> it will remove this column from all outline levels.</param>
|
||||
IXLColumn Ungroup(Boolean fromAll);
|
||||
|
||||
/// <summary>
|
||||
/// Show this column as collapsed.
|
||||
/// </summary>
|
||||
IXLColumn Collapse();
|
||||
|
||||
/// <summary>Expands this column (if it's collapsed).</summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vertical page break after this column.
|
||||
/// </summary>
|
||||
IXLColumn AddVerticalPageBreak();
|
||||
|
||||
IXLColumn SetDataType(XLDataType dataType);
|
||||
|
||||
IXLColumn ColumnLeft();
|
||||
|
||||
IXLColumn ColumnLeft(Int32 step);
|
||||
|
||||
IXLColumn ColumnRight();
|
||||
|
||||
IXLColumn ColumnRight(Int32 step);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the contents of this column.
|
||||
/// </summary>
|
||||
/// <param name="clearOptions">Specify what you want to clear.</param>
|
||||
new IXLColumn Clear(XLClearOptions clearOptions = XLClearOptions.All);
|
||||
|
||||
[Obsolete("Use the overload with XLCellsUsedOptions")]
|
||||
IXLRangeColumn ColumnUsed(Boolean includeFormats);
|
||||
|
||||
IXLRangeColumn ColumnUsed(XLCellsUsedOptions options = XLCellsUsedOptions.AllContents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLColumns : IEnumerable<IXLColumn>
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the width of all columns.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The width of all columns.
|
||||
/// </value>
|
||||
Double Width { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all columns and shifts the columns at the right of them accordingly.
|
||||
/// </summary>
|
||||
void Delete();
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of all columns based on its contents.
|
||||
/// </summary>
|
||||
IXLColumns AdjustToContents();
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of all columns based on its contents, starting from the startRow.
|
||||
/// </summary>
|
||||
/// <param name="startRow">The row to start calculating the column width.</param>
|
||||
IXLColumns AdjustToContents(Int32 startRow);
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of all columns based on its contents, starting from the startRow and ending at endRow.
|
||||
/// </summary>
|
||||
/// <param name="startRow">The row to start calculating the column width.</param>
|
||||
/// <param name="endRow">The row to end calculating the column width.</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Hides all columns.
|
||||
/// </summary>
|
||||
void Hide();
|
||||
|
||||
/// <summary>Unhides all columns.</summary>
|
||||
void Unhide();
|
||||
|
||||
/// <summary>
|
||||
/// Increments the outline level of all columns by 1.
|
||||
/// </summary>
|
||||
void Group();
|
||||
|
||||
/// <summary>
|
||||
/// Increments the outline level of all columns by 1.
|
||||
/// </summary>
|
||||
/// <param name="collapse">If set to <c>true</c> the columns will be shown collapsed.</param>
|
||||
void Group(Boolean collapse);
|
||||
|
||||
/// <summary>
|
||||
/// Sets outline level for all columns.
|
||||
/// </summary>
|
||||
/// <param name="outlineLevel">The outline level.</param>
|
||||
void Group(Int32 outlineLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Sets outline level for all columns.
|
||||
/// </summary>
|
||||
/// <param name="outlineLevel">The outline level.</param>
|
||||
/// <param name="collapse">If set to <c>true</c> the columns will be shown collapsed.</param>
|
||||
void Group(Int32 outlineLevel, Boolean collapse);
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the outline level of all columns by 1.
|
||||
/// </summary>
|
||||
void Ungroup();
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the outline level of all columns by 1.
|
||||
/// </summary>
|
||||
/// <param name="fromAll">If set to <c>true</c> it will remove the columns from all outline levels.</param>
|
||||
void Ungroup(Boolean fromAll);
|
||||
|
||||
/// <summary>
|
||||
/// Show all columns as collapsed.
|
||||
/// </summary>
|
||||
void Collapse();
|
||||
|
||||
/// <summary>Expands all columns (if they're collapsed).</summary>
|
||||
void Expand();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the collection of cells.
|
||||
/// </summary>
|
||||
IXLCells Cells();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the collection of cells that have a value.
|
||||
/// </summary>
|
||||
IXLCells CellsUsed();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the collection of cells that have a value.
|
||||
/// </summary>
|
||||
/// <param name="includeFormats">if set to <c>true</c> will return all cells with a value or a style different than the default.</param>
|
||||
IXLCells CellsUsed(Boolean includeFormats);
|
||||
|
||||
IXLStyle Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vertical page break after these columns.
|
||||
/// </summary>
|
||||
IXLColumns AddVerticalPageBreaks();
|
||||
|
||||
IXLColumns SetDataType(XLDataType dataType);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the contents of these columns.
|
||||
/// </summary>
|
||||
/// <param name="clearOptions">Specify what you want to clear.</param>
|
||||
IXLColumns Clear(XLClearOptions clearOptions = XLClearOptions.All);
|
||||
|
||||
void Select();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// The direct contructor should only be used in <see cref="XLWorksheet.RangeFactory"/>.
|
||||
/// </summary>
|
||||
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<IXLStyle> Styles
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return Style;
|
||||
|
||||
int column = ColumnNumber();
|
||||
|
||||
foreach (XLCell cell in Worksheet.Internals.CellsCollection.GetCellsInColumn(column))
|
||||
yield return cell.Style;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<XLStylizedBase> 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<IXLFontBase, Font>();
|
||||
|
||||
Double colMaxWidth = minWidth;
|
||||
|
||||
List<Int32> autoFilterRows = new List<Int32>();
|
||||
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<XLCell>())
|
||||
{
|
||||
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<KeyValuePair<IXLFontBase, string>>();
|
||||
|
||||
#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<IXLFontBase, String>(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<IXLFontBase, String>(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<IXLFontBase, string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vertical page break after this column.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
internal class XLColumnsCollection : IDictionary<Int32, XLColumn>
|
||||
{
|
||||
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<Int32, XLColumn> _dictionary = new Dictionary<Int32, XLColumn>();
|
||||
|
||||
public void Add(int key, XLColumn value)
|
||||
{
|
||||
_dictionary.Add(key, value);
|
||||
}
|
||||
|
||||
public bool ContainsKey(int key)
|
||||
{
|
||||
return _dictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
public ICollection<int> 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<XLColumn> Values
|
||||
{
|
||||
get { return _dictionary.Values; }
|
||||
}
|
||||
|
||||
public XLColumn this[int key]
|
||||
{
|
||||
get
|
||||
{
|
||||
return _dictionary[key];
|
||||
}
|
||||
set
|
||||
{
|
||||
_dictionary[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<int, XLColumn> item)
|
||||
{
|
||||
_dictionary.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_dictionary.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<int, XLColumn> item)
|
||||
{
|
||||
return _dictionary.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<int, XLColumn>[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { return _dictionary.Count; }
|
||||
}
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<int, XLColumn> item)
|
||||
{
|
||||
return _dictionary.Remove(item.Key);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<int, XLColumn>> GetEnumerator()
|
||||
{
|
||||
return _dictionary.GetEnumerator();
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||
{
|
||||
return _dictionary.GetEnumerator();
|
||||
}
|
||||
|
||||
public void RemoveAll(Func<XLColumn, Boolean> predicate)
|
||||
{
|
||||
_dictionary.RemoveAll(predicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<XLColumn> _columns = new List<XLColumn>();
|
||||
private readonly XLWorksheet _worksheet;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="XLColumns"/>.
|
||||
/// </summary>
|
||||
/// <param name="worksheet">If worksheet is specified it means that the created instance represents
|
||||
/// all columns on a worksheet so changing its width will affect all columns.</param>
|
||||
/// <param name="defaultStyle">Default style to use when initializing child entries.</param>
|
||||
public XLColumns(XLWorksheet worksheet, XLStyleValue defaultStyle = null)
|
||||
: base(defaultStyle)
|
||||
{
|
||||
_worksheet = worksheet;
|
||||
}
|
||||
|
||||
#region IXLColumns Members
|
||||
|
||||
public IEnumerator<IXLColumn> GetEnumerator()
|
||||
{
|
||||
return _columns.Cast<IXLColumn>().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<IXLWorksheet, List<Int32>>();
|
||||
foreach (XLColumn c in _columns)
|
||||
{
|
||||
if (!toDelete.TryGetValue(c.Worksheet, out List<Int32> list))
|
||||
{
|
||||
list = new List<Int32>();
|
||||
toDelete.Add(c.Worksheet, list);
|
||||
}
|
||||
|
||||
list.Add(c.ColumnNumber());
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<IXLWorksheet, List<int>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vertical page break after this column.
|
||||
/// </summary>
|
||||
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<IXLStyle> 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<XLStylizedBase> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLComment : IXLFormattedText<IXLComment>, IXLDrawing<IXLComment>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets this comment's author's name
|
||||
/// </summary>
|
||||
String Author { get; set; }
|
||||
/// <summary>
|
||||
/// Sets the name of the comment's author
|
||||
/// </summary>
|
||||
/// <param name="value">Author's name</param>
|
||||
IXLComment SetAuthor(String value);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a bolded line with the author's name
|
||||
/// </summary>
|
||||
IXLRichString AddSignature();
|
||||
|
||||
void Delete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
internal class XLComment : XLFormattedText<IXLComment>, 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<IXLComment> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// The first of the <see cref="Ranges"/>.
|
||||
/// </summary>
|
||||
IXLRange Range { get; set; }
|
||||
|
||||
IXLRanges Ranges { get; }
|
||||
|
||||
XLDictionary<XLFormula> Values { get; }
|
||||
|
||||
XLDictionary<XLColor> Colors { get; }
|
||||
|
||||
XLDictionary<XLCFContentType> ContentTypes { get; }
|
||||
|
||||
XLDictionary<XLCFIconSetOperator> IconSetOperators { get; }
|
||||
|
||||
XLCFOperator Operator { get; }
|
||||
|
||||
Boolean Bottom { get; }
|
||||
|
||||
Boolean Percent { get; }
|
||||
|
||||
IXLConditionalFormat SetStopIfTrue();
|
||||
|
||||
IXLConditionalFormat SetStopIfTrue(Boolean value);
|
||||
|
||||
IXLConditionalFormat CopyTo(IXLWorksheet targetSheet);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
public interface IXLConditionalFormats : IEnumerable<IXLConditionalFormat>
|
||||
{
|
||||
void Add(IXLConditionalFormat conditionalFormat);
|
||||
|
||||
void RemoveAll();
|
||||
|
||||
void Remove(Predicate<IXLConditionalFormat> predicate);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("\"", "\"\""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClosedXML.Excel
|
||||
{
|
||||
internal class XLCFConverters
|
||||
{
|
||||
private static readonly Dictionary<XLConditionalFormatType, IXLCFConverter> Converters;
|
||||
static XLCFConverters()
|
||||
{
|
||||
Converters = new Dictionary<XLConditionalFormatType, IXLCFConverter>
|
||||
{
|
||||
{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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<XLConditionalFormatType, IXLCFConverterExtension> Converters;
|
||||
|
||||
static XLCFConvertersExtension()
|
||||
{
|
||||
XLCFConvertersExtension.Converters = new Dictionary<XLConditionalFormatType, IXLCFConverterExtension>()
|
||||
{
|
||||
{ XLConditionalFormatType.DataBar, new XLCFDataBarConverterExtension() }
|
||||
};
|
||||
}
|
||||
|
||||
public XLCFConvertersExtension()
|
||||
{
|
||||
}
|
||||
|
||||
public static ConditionalFormattingRule Convert(IXLConditionalFormat conditionalFormat, XLWorkbook.SaveContext context)
|
||||
{
|
||||
return XLCFConvertersExtension.Converters[conditionalFormat.ConditionalFormatType].Convert(conditionalFormat, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<XLTimePeriod, string> formulaTemplates = new Dictionary<XLTimePeriod, string>()
|
||||
{
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user