mirror of
https://gitee.com/dotnetchina/MiniExcel.git
synced 2024-11-29 18:38:08 +08:00
Fix bug : remove ExcelNumberFormat nuget to resolve error NU3037
This commit is contained in:
parent
9a05683bf1
commit
b1f9acb9bf
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30804.86
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniExcelLibs", "src\MiniExcel\MiniExcelLibs.csproj", "{097903C9-1F81-4427-B4C8-530CB59687B8}"
|
||||
EndProject
|
||||
@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniExcel.Benchmarks", "ben
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{77A8A169-168B-457F-AB5F-48F30D6BB33C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExcelNumberFormat", "src\ExcelNumberFormat\ExcelNumberFormat.csproj", "{7ECB1CFF-955F-4285-8486-756F1CDE2177}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -52,6 +54,10 @@ Global
|
||||
{1888285A-B130-48D5-A628-DC85B623FAB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1888285A-B130-48D5-A628-DC85B623FAB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1888285A-B130-48D5-A628-DC85B623FAB8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7ECB1CFF-955F-4285-8486-756F1CDE2177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7ECB1CFF-955F-4285-8486-756F1CDE2177}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7ECB1CFF-955F-4285-8486-756F1CDE2177}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7ECB1CFF-955F-4285-8486-756F1CDE2177}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -60,6 +66,7 @@ Global
|
||||
{097903C9-1F81-4427-B4C8-530CB59687B8} = {CC1E0601-AEC9-42D7-8F6A-3FB3939EED16}
|
||||
{77F2C86B-0F17-4370-AB38-A089F9DF4ED5} = {359A7094-3353-48F2-B3E1-FE9E59698318}
|
||||
{1888285A-B130-48D5-A628-DC85B623FAB8} = {77A8A169-168B-457F-AB5F-48F30D6BB33C}
|
||||
{7ECB1CFF-955F-4285-8486-756F1CDE2177} = {CC1E0601-AEC9-42D7-8F6A-3FB3939EED16}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {51DF25DA-2DCB-4883-90FE-399DA950D4F2}
|
||||
|
@ -1387,7 +1387,7 @@ public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useH
|
||||
|
||||
### Reference
|
||||
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper)
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat)
|
||||
|
||||
|
||||
|
||||
|
@ -1387,7 +1387,7 @@ public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useH
|
||||
|
||||
### 参考
|
||||
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper)
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat)
|
||||
|
||||
|
||||
|
||||
|
@ -1389,7 +1389,7 @@ public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useH
|
||||
|
||||
### 參考
|
||||
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper)
|
||||
[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat)
|
||||
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
### 0.19.1
|
||||
- [New] QueryAsDataTable remove empty column keys. #298
|
||||
- [Bug] Error NU3037: ExcelNumberFormat 1.1.0 #302
|
||||
|
||||
### 0.19.0
|
||||
- [New] SaveAs default style with autoFilter mode. #190
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
### 0.19.1
|
||||
- [New] QueryAsDataTable 删除空白 Column keys. #298
|
||||
- [Bug] Error NU3037: ExcelNumberFormat 1.1.0 #302
|
||||
|
||||
### 0.19.0
|
||||
- [New] SaveAs 预设样式增加筛选功能. #190
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
### 0.19.1
|
||||
- [New] QueryAsDataTable 刪除空白 Column keys. #298
|
||||
- [Bug] Error NU3037: ExcelNumberFormat 1.1.0 #302
|
||||
|
||||
### 0.19.0
|
||||
- [New] SaveAs 預設樣式增加篩選功能. #190
|
||||
|
3
src/ExcelNumberFormat/AssemblyInfo.cs
Normal file
3
src/ExcelNumberFormat/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System;
|
||||
|
||||
[assembly:CLSCompliant(true)]
|
7
src/ExcelNumberFormat/Color.cs
Normal file
7
src/ExcelNumberFormat/Color.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class Color
|
||||
{
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
25
src/ExcelNumberFormat/CompatibleConvert.cs
Normal file
25
src/ExcelNumberFormat/CompatibleConvert.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal static class CompatibleConvert
|
||||
{
|
||||
/// <summary>
|
||||
/// A backward-compatible version of <see cref="Convert.ToString(object, IFormatProvider)"/>.
|
||||
/// Starting from .net Core 3.0 the default precision used for formatting floating point number has changed.
|
||||
/// To always format numbers the same way, no matter what version of runtime is used, we specify the precision explicitly.
|
||||
/// </summary>
|
||||
public static string ToString(object value, IFormatProvider provider)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case double d:
|
||||
return d.ToString("G15", provider);
|
||||
case float f:
|
||||
return f.ToString("G7", provider);
|
||||
default:
|
||||
return Convert.ToString(value, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/ExcelNumberFormat/Condition.cs
Normal file
29
src/ExcelNumberFormat/Condition.cs
Normal file
@ -0,0 +1,29 @@
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class Condition
|
||||
{
|
||||
public string Operator { get; set; }
|
||||
public double Value { get; set; }
|
||||
|
||||
public bool Evaluate(double lhs)
|
||||
{
|
||||
switch (Operator)
|
||||
{
|
||||
case "<":
|
||||
return lhs < Value;
|
||||
case "<=":
|
||||
return lhs <= Value;
|
||||
case ">":
|
||||
return lhs > Value;
|
||||
case ">=":
|
||||
return lhs >= Value;
|
||||
case "<>":
|
||||
return lhs != Value;
|
||||
case "=":
|
||||
return lhs == Value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
102
src/ExcelNumberFormat/DecimalSection.cs
Normal file
102
src/ExcelNumberFormat/DecimalSection.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class DecimalSection
|
||||
{
|
||||
public bool ThousandSeparator { get; set; }
|
||||
|
||||
public double ThousandDivisor { get; set; }
|
||||
|
||||
public double PercentMultiplier { get; set; }
|
||||
|
||||
public List<string> BeforeDecimal { get; set; }
|
||||
|
||||
public bool DecimalSeparator { get; set; }
|
||||
|
||||
public List<string> AfterDecimal { get; set; }
|
||||
|
||||
public static bool TryParse(List<string> tokens, out DecimalSection format)
|
||||
{
|
||||
if (Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal) == tokens.Count)
|
||||
{
|
||||
bool thousandSeparator;
|
||||
var divisor = GetTrailingCommasDivisor(tokens, out thousandSeparator);
|
||||
var multiplier = GetPercentMultiplier(tokens);
|
||||
|
||||
format = new DecimalSection()
|
||||
{
|
||||
BeforeDecimal = beforeDecimal,
|
||||
DecimalSeparator = decimalSeparator,
|
||||
AfterDecimal = afterDecimal,
|
||||
PercentMultiplier = multiplier,
|
||||
ThousandDivisor = divisor,
|
||||
ThousandSeparator = thousandSeparator
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
format = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static double GetPercentMultiplier(List<string> tokens)
|
||||
{
|
||||
// If there is a percentage literal in the part list, multiply the result by 100
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token == "%")
|
||||
return 100;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static double GetTrailingCommasDivisor(List<string> tokens, out bool thousandSeparator)
|
||||
{
|
||||
// This parses all comma literals in the part list:
|
||||
// Each comma after the last digit placeholder divides the result by 1000.
|
||||
// If there are any other commas, display the result with thousand separators.
|
||||
bool hasLastPlaceholder = false;
|
||||
var divisor = 1.0;
|
||||
|
||||
for (var j = 0; j < tokens.Count; j++)
|
||||
{
|
||||
var tokenIndex = tokens.Count - 1 - j;
|
||||
var token = tokens[tokenIndex];
|
||||
|
||||
if (!hasLastPlaceholder)
|
||||
{
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
// Each trailing comma multiplies the divisor by 1000
|
||||
for (var k = tokenIndex + 1; k < tokens.Count; k++)
|
||||
{
|
||||
token = tokens[k];
|
||||
if (token == ",")
|
||||
divisor *= 1000.0;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Continue scanning backwards from the last digit placeholder,
|
||||
// but now look for a thousand separator comma
|
||||
hasLastPlaceholder = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (token == ",")
|
||||
{
|
||||
thousandSeparator = true;
|
||||
return divisor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thousandSeparator = false;
|
||||
return divisor;
|
||||
}
|
||||
}
|
||||
}
|
110
src/ExcelNumberFormat/Evaluator.cs
Normal file
110
src/ExcelNumberFormat/Evaluator.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal static class Evaluator
|
||||
{
|
||||
public static Section GetSection(List<Section> sections, object value)
|
||||
{
|
||||
// Standard format has up to 4 sections:
|
||||
// Positive;Negative;Zero;Text
|
||||
switch (value)
|
||||
{
|
||||
case string s:
|
||||
if (sections.Count >= 4)
|
||||
return sections[3];
|
||||
|
||||
return null;
|
||||
|
||||
case DateTime dt:
|
||||
// TODO: Check date conditions need date helpers and Date1904 knowledge
|
||||
return GetFirstSection(sections, SectionType.Date);
|
||||
|
||||
case TimeSpan ts:
|
||||
return GetNumericSection(sections, ts.TotalDays);
|
||||
|
||||
case double d:
|
||||
return GetNumericSection(sections, d);
|
||||
|
||||
case int i:
|
||||
return GetNumericSection(sections, i);
|
||||
|
||||
case short s:
|
||||
return GetNumericSection(sections, s);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Section GetFirstSection(List<Section> sections, SectionType type)
|
||||
{
|
||||
foreach (var section in sections)
|
||||
if (section.Type == type)
|
||||
return section;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Section GetNumericSection(List<Section> sections, double value)
|
||||
{
|
||||
// First section applies if
|
||||
// - Has a condition:
|
||||
// - There is 1 section, or
|
||||
// - There are 2 sections, and the value is 0 or positive, or
|
||||
// - There are >2 sections, and the value is positive
|
||||
if (sections.Count < 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var section0 = sections[0];
|
||||
|
||||
if (section0.Condition != null)
|
||||
{
|
||||
if (section0.Condition.Evaluate(value))
|
||||
{
|
||||
return section0;
|
||||
}
|
||||
}
|
||||
else if (sections.Count == 1 || (sections.Count == 2 && value >= 0) || (sections.Count >= 2 && value > 0))
|
||||
{
|
||||
return section0;
|
||||
}
|
||||
|
||||
if (sections.Count < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var section1 = sections[1];
|
||||
|
||||
// First condition didnt match, or was a negative number. Second condition applies if:
|
||||
// - Has a condition, or
|
||||
// - Value is negative, or
|
||||
// - There are two sections, and the first section had a non-matching condition
|
||||
if (section1.Condition != null)
|
||||
{
|
||||
if (section1.Condition.Evaluate(value))
|
||||
{
|
||||
return section1;
|
||||
}
|
||||
}
|
||||
else if (value < 0 || (sections.Count == 2 && section0.Condition != null))
|
||||
{
|
||||
return section1;
|
||||
}
|
||||
|
||||
// Second condition didnt match, or was positive. The following
|
||||
// sections cannot have conditions, always fall back to the third
|
||||
// section (for zero formatting) if specified.
|
||||
if (sections.Count < 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return sections[2];
|
||||
}
|
||||
}
|
||||
}
|
179
src/ExcelNumberFormat/ExcelDateTime.cs
Normal file
179
src/ExcelNumberFormat/ExcelDateTime.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Similar to regular .NET DateTime, but also supports 0/1 1900 and 29/2 1900.
|
||||
/// </summary>
|
||||
internal class ExcelDateTime
|
||||
{
|
||||
/// <summary>
|
||||
/// The closest .NET DateTime to the specified excel date.
|
||||
/// </summary>
|
||||
public DateTime AdjustedDateTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of days to adjust by in post.
|
||||
/// </summary>
|
||||
public int AdjustDaysPost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ExcelDateTime from a numeric value.
|
||||
/// </summary>
|
||||
public ExcelDateTime(double numericDate, bool isDate1904)
|
||||
{
|
||||
if (isDate1904)
|
||||
{
|
||||
numericDate += 1462.0;
|
||||
AdjustedDateTime = new DateTime(DoubleDateToTicks(numericDate), DateTimeKind.Unspecified);
|
||||
}
|
||||
else
|
||||
{
|
||||
// internal dates before 30/12/1899 should add two days to get the real date
|
||||
// internal dates on 30/12 19899 should add two days, but subtract a day post to get the real date
|
||||
// internal dates before 28/2/1900 should add one day to get the real date
|
||||
// internal dates on 28/2 1900 should use the same date, but add a day post to get the real date
|
||||
|
||||
var internalDateTime = new DateTime(DoubleDateToTicks(numericDate), DateTimeKind.Unspecified);
|
||||
if (internalDateTime < Excel1900ZeroethMinDate)
|
||||
{
|
||||
AdjustDaysPost = 0;
|
||||
AdjustedDateTime = internalDateTime.AddDays(2);
|
||||
}
|
||||
|
||||
else if (internalDateTime < Excel1900ZeroethMaxDate)
|
||||
{
|
||||
AdjustDaysPost = -1;
|
||||
AdjustedDateTime = internalDateTime.AddDays(2);
|
||||
}
|
||||
|
||||
else if (internalDateTime < Excel1900LeapMinDate)
|
||||
{
|
||||
AdjustDaysPost = 0;
|
||||
AdjustedDateTime = internalDateTime.AddDays(1);
|
||||
}
|
||||
|
||||
else if (internalDateTime < Excel1900LeapMaxDate)
|
||||
{
|
||||
AdjustDaysPost = 1;
|
||||
AdjustedDateTime = internalDateTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
AdjustDaysPost = 0;
|
||||
AdjustedDateTime = internalDateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DateTime Excel1900LeapMinDate = new DateTime(1900, 2, 28);
|
||||
static DateTime Excel1900LeapMaxDate = new DateTime(1900, 3, 1);
|
||||
static DateTime Excel1900ZeroethMinDate = new DateTime(1899, 12, 30);
|
||||
static DateTime Excel1900ZeroethMaxDate = new DateTime(1899, 12, 31);
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a regular .NET datetime.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public ExcelDateTime(DateTime value)
|
||||
{
|
||||
AdjustedDateTime = value;
|
||||
AdjustDaysPost = 0;
|
||||
}
|
||||
|
||||
public int Year => AdjustedDateTime.Year;
|
||||
|
||||
public int Month => AdjustedDateTime.Month;
|
||||
|
||||
public int Day => AdjustedDateTime.Day + AdjustDaysPost;
|
||||
|
||||
public int Hour => AdjustedDateTime.Hour;
|
||||
|
||||
public int Minute => AdjustedDateTime.Minute;
|
||||
|
||||
public int Second => AdjustedDateTime.Second;
|
||||
|
||||
public int Millisecond => AdjustedDateTime.Millisecond;
|
||||
|
||||
public DayOfWeek DayOfWeek => AdjustedDateTime.DayOfWeek;
|
||||
|
||||
public string ToString(string numberFormat, CultureInfo culture)
|
||||
{
|
||||
return AdjustedDateTime.ToString(numberFormat, culture);
|
||||
}
|
||||
|
||||
public static bool TryConvert(object value, bool isDate1904, CultureInfo culture, out ExcelDateTime result)
|
||||
{
|
||||
if (value is double doubleValue)
|
||||
{
|
||||
result = new ExcelDateTime(doubleValue, isDate1904);
|
||||
return true;
|
||||
}
|
||||
if (value is int intValue)
|
||||
{
|
||||
result = new ExcelDateTime(intValue, isDate1904);
|
||||
return true;
|
||||
}
|
||||
if (value is short shortValue)
|
||||
{
|
||||
result = new ExcelDateTime(shortValue, isDate1904);
|
||||
return true;
|
||||
}
|
||||
else if (value is DateTime dateTimeValue)
|
||||
{
|
||||
result = new ExcelDateTime(dateTimeValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// From DateTime class to enable OADate in PCL
|
||||
// Number of 100ns ticks per time unit
|
||||
private const long TicksPerMillisecond = 10000;
|
||||
private const long TicksPerSecond = TicksPerMillisecond * 1000;
|
||||
private const long TicksPerMinute = TicksPerSecond * 60;
|
||||
private const long TicksPerHour = TicksPerMinute * 60;
|
||||
private const long TicksPerDay = TicksPerHour * 24;
|
||||
|
||||
private const int MillisPerSecond = 1000;
|
||||
private const int MillisPerMinute = MillisPerSecond * 60;
|
||||
private const int MillisPerHour = MillisPerMinute * 60;
|
||||
private const int MillisPerDay = MillisPerHour * 24;
|
||||
|
||||
// Number of days in a non-leap year
|
||||
private const int DaysPerYear = 365;
|
||||
|
||||
// Number of days in 4 years
|
||||
private const int DaysPer4Years = DaysPerYear * 4 + 1;
|
||||
|
||||
// Number of days in 100 years
|
||||
private const int DaysPer100Years = DaysPer4Years * 25 - 1;
|
||||
|
||||
// Number of days in 400 years
|
||||
private const int DaysPer400Years = DaysPer100Years * 4 + 1;
|
||||
|
||||
// Number of days from 1/1/0001 to 12/30/1899
|
||||
private const int DaysTo1899 = DaysPer400Years * 4 + DaysPer100Years * 3 - 367;
|
||||
|
||||
private const long DoubleDateOffset = DaysTo1899 * TicksPerDay;
|
||||
|
||||
internal static long DoubleDateToTicks(double value)
|
||||
{
|
||||
long millis = (long)(value * MillisPerDay + (value >= 0 ? 0.5 : -0.5));
|
||||
|
||||
// The interesting thing here is when you have a value like 12.5 it all positive 12 days and 12 hours from 01/01/1899
|
||||
// However if you a value of -12.25 it is minus 12 days but still positive 6 hours, almost as though you meant -11.75 all negative
|
||||
// This line below fixes up the millis in the negative case
|
||||
if (millis < 0)
|
||||
{
|
||||
millis -= millis % MillisPerDay * 2;
|
||||
}
|
||||
|
||||
millis += DoubleDateOffset / TicksPerMillisecond;
|
||||
return millis * TicksPerMillisecond;
|
||||
}
|
||||
}
|
||||
}
|
22
src/ExcelNumberFormat/ExcelNumberFormat.csproj
Normal file
22
src/ExcelNumberFormat/ExcelNumberFormat.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net45;netstandard2.0;net5.0</TargetFrameworks>
|
||||
<VersionPrefix>1.1.0</VersionPrefix>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Description>.NET library to parse ECMA-376 number format strings and format values like Excel and other spreadsheet softwares.</Description>
|
||||
<Authors>ExcelNumberFormat developers</Authors>
|
||||
<PackageTags>excel,formatting,numfmt,formatcode</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/andersnm/ExcelNumberFormat</PackageProjectUrl>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<AssemblyOriginatorKeyFile>ExcelNumberFormat.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="icon.png" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
BIN
src/ExcelNumberFormat/ExcelNumberFormat.snk
Normal file
BIN
src/ExcelNumberFormat/ExcelNumberFormat.snk
Normal file
Binary file not shown.
51
src/ExcelNumberFormat/ExponentialSection.cs
Normal file
51
src/ExcelNumberFormat/ExponentialSection.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class ExponentialSection
|
||||
{
|
||||
public List<string> BeforeDecimal { get; set; }
|
||||
|
||||
public bool DecimalSeparator { get; set; }
|
||||
|
||||
public List<string> AfterDecimal { get; set; }
|
||||
|
||||
public string ExponentialToken { get; set; }
|
||||
|
||||
public List<string> Power { get; set; }
|
||||
|
||||
public static bool TryParse(List<string> tokens, out ExponentialSection format)
|
||||
{
|
||||
format = null;
|
||||
|
||||
string exponentialToken;
|
||||
|
||||
int partCount = Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal);
|
||||
|
||||
if (partCount == 0)
|
||||
return false;
|
||||
|
||||
int position = partCount;
|
||||
if (position < tokens.Count && Token.IsExponent(tokens[position]))
|
||||
{
|
||||
exponentialToken = tokens[position];
|
||||
position++;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
format = new ExponentialSection()
|
||||
{
|
||||
BeforeDecimal = beforeDecimal,
|
||||
DecimalSeparator = decimalSeparator,
|
||||
AfterDecimal = afterDecimal,
|
||||
ExponentialToken = exponentialToken,
|
||||
Power = tokens.GetRange(position, tokens.Count - position)
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
773
src/ExcelNumberFormat/Formatter.cs
Normal file
773
src/ExcelNumberFormat/Formatter.cs
Normal file
@ -0,0 +1,773 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
static internal class Formatter
|
||||
{
|
||||
static public string Format(object value, string formatString, CultureInfo culture, bool isDate1904)
|
||||
{
|
||||
var format = new NumberFormat(formatString);
|
||||
if (!format.IsValid)
|
||||
return CompatibleConvert.ToString(value, culture);
|
||||
|
||||
var section = Evaluator.GetSection(format.Sections, value);
|
||||
if (section == null)
|
||||
return CompatibleConvert.ToString(value, culture);
|
||||
|
||||
return Format(value, section, culture, isDate1904);
|
||||
}
|
||||
|
||||
static public string Format(object value, Section node, CultureInfo culture, bool isDate1904)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case SectionType.Number:
|
||||
// Hide sign under certain conditions and section index
|
||||
var number = Convert.ToDouble(value, culture);
|
||||
if ((node.SectionIndex == 0 && node.Condition != null) || node.SectionIndex == 1)
|
||||
number = Math.Abs(number);
|
||||
|
||||
return FormatNumber(number, node.Number, culture);
|
||||
|
||||
case SectionType.Date:
|
||||
if (ExcelDateTime.TryConvert(value, isDate1904, culture, out var excelDateTime))
|
||||
{
|
||||
return FormatDate(excelDateTime, node.GeneralTextDateDurationParts, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Unexpected date value");
|
||||
}
|
||||
|
||||
case SectionType.Duration:
|
||||
if (value is TimeSpan ts)
|
||||
{
|
||||
return FormatTimeSpan(ts, node.GeneralTextDateDurationParts, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = Convert.ToDouble(value);
|
||||
return FormatTimeSpan(TimeSpan.FromDays(d), node.GeneralTextDateDurationParts, culture);
|
||||
}
|
||||
|
||||
case SectionType.General:
|
||||
case SectionType.Text:
|
||||
return FormatGeneralText(CompatibleConvert.ToString(value, culture), node.GeneralTextDateDurationParts);
|
||||
|
||||
case SectionType.Exponential:
|
||||
return FormatExponential(Convert.ToDouble(value, culture), node, culture);
|
||||
|
||||
case SectionType.Fraction:
|
||||
return FormatFraction(Convert.ToDouble(value, culture), node, culture);
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Unknown number format section");
|
||||
}
|
||||
}
|
||||
|
||||
static string FormatGeneralText(string text, List<string> tokens)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (Token.IsGeneral(token) || token == "@")
|
||||
{
|
||||
result.Append(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static string FormatTimeSpan(TimeSpan timeSpan, List<string> tokens, CultureInfo culture)
|
||||
{
|
||||
// NOTE/TODO: assumes there is exactly one [hh], [mm] or [ss] using the integer part of TimeSpan.TotalXXX when formatting.
|
||||
// The timeSpan input is then truncated to the remainder fraction, which is used to format mm and/or ss.
|
||||
var result = new StringBuilder();
|
||||
var containsMilliseconds = false;
|
||||
for (var i = tokens.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (tokens[i].StartsWith(".0"))
|
||||
{
|
||||
containsMilliseconds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
|
||||
if (token.StartsWith("m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = timeSpan.Minutes;
|
||||
var digits = token.Length;
|
||||
result.Append(value.ToString("D" + digits));
|
||||
}
|
||||
else if (token.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If format does not include ms, then include ms in seconds and round before printing
|
||||
var formatMs = containsMilliseconds ? 0 : timeSpan.Milliseconds / 1000D;
|
||||
var value = (int)Math.Round(timeSpan.Seconds + formatMs, 0, MidpointRounding.AwayFromZero);
|
||||
var digits = token.Length;
|
||||
result.Append(value.ToString("D" + digits));
|
||||
}
|
||||
else if (token.StartsWith("[h", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = (int)timeSpan.TotalHours;
|
||||
var digits = token.Length - 2;
|
||||
result.Append(value.ToString("D" + digits));
|
||||
timeSpan = new TimeSpan(0, 0, Math.Abs(timeSpan.Minutes), Math.Abs(timeSpan.Seconds), Math.Abs(timeSpan.Milliseconds));
|
||||
}
|
||||
else if (token.StartsWith("[m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = (int)timeSpan.TotalMinutes;
|
||||
var digits = token.Length - 2;
|
||||
result.Append(value.ToString("D" + digits));
|
||||
timeSpan = new TimeSpan(0, 0, 0, Math.Abs(timeSpan.Seconds), Math.Abs(timeSpan.Milliseconds));
|
||||
}
|
||||
else if (token.StartsWith("[s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = (int)timeSpan.TotalSeconds;
|
||||
var digits = token.Length - 2;
|
||||
result.Append(value.ToString("D" + digits));
|
||||
timeSpan = new TimeSpan(0, 0, 0, 0, Math.Abs(timeSpan.Milliseconds));
|
||||
}
|
||||
else if (token.StartsWith(".0")) {
|
||||
var value = timeSpan.Milliseconds;
|
||||
var digits = token.Length - 1;
|
||||
result.Append("." + value.ToString("D" + digits));
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static string FormatDate(ExcelDateTime date, List<string> tokens, CultureInfo culture)
|
||||
{
|
||||
var containsAmPm = ContainsAmPm(tokens);
|
||||
|
||||
var result = new StringBuilder();
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
|
||||
if (token.StartsWith("y", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// year
|
||||
var digits = token.Length;
|
||||
if (digits < 2)
|
||||
digits = 2;
|
||||
if (digits == 3)
|
||||
digits = 4;
|
||||
|
||||
var year = date.Year;
|
||||
if (digits == 2)
|
||||
year = year % 100;
|
||||
|
||||
result.Append(year.ToString("D" + digits));
|
||||
}
|
||||
else if (token.StartsWith("m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If "m" or "mm" code is used immediately after the "h" or "hh" code (for hours) or immediately before
|
||||
// the "ss" code (for seconds), the application shall display minutes instead of the month.
|
||||
if (LookBackDatePart(tokens, i - 1, "h") || LookAheadDatePart(tokens, i + 1, "s"))
|
||||
{
|
||||
var digits = token.Length;
|
||||
result.Append(date.Minute.ToString("D" + digits));
|
||||
}
|
||||
else
|
||||
{
|
||||
var digits = token.Length;
|
||||
if (digits == 3)
|
||||
{
|
||||
result.Append(culture.DateTimeFormat.AbbreviatedMonthNames[date.Month - 1]);
|
||||
}
|
||||
else if (digits == 4)
|
||||
{
|
||||
result.Append(culture.DateTimeFormat.MonthNames[date.Month - 1]);
|
||||
}
|
||||
else if (digits == 5)
|
||||
{
|
||||
result.Append(culture.DateTimeFormat.MonthNames[date.Month - 1][0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(date.Month.ToString("D" + digits));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token.StartsWith("d", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var digits = token.Length;
|
||||
if (digits == 3)
|
||||
{
|
||||
// Sun-Sat
|
||||
result.Append(culture.DateTimeFormat.AbbreviatedDayNames[(int)date.DayOfWeek]);
|
||||
}
|
||||
else if (digits == 4)
|
||||
{
|
||||
// Sunday-Saturday
|
||||
result.Append(culture.DateTimeFormat.DayNames[(int)date.DayOfWeek]);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(date.Day.ToString("D" + digits));
|
||||
}
|
||||
}
|
||||
else if (token.StartsWith("h", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var digits = token.Length;
|
||||
if (containsAmPm)
|
||||
result.Append(((date.Hour + 11) % 12 + 1).ToString("D" + digits));
|
||||
else
|
||||
result.Append(date.Hour.ToString("D" + digits));
|
||||
}
|
||||
else if (token.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var digits = token.Length;
|
||||
result.Append(date.Second.ToString("D" + digits));
|
||||
}
|
||||
else if (token.StartsWith("g", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var era = culture.DateTimeFormat.Calendar.GetEra(date.AdjustedDateTime);
|
||||
var digits = token.Length;
|
||||
if (digits < 3)
|
||||
{
|
||||
result.Append(culture.DateTimeFormat.GetAbbreviatedEraName(era));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(culture.DateTimeFormat.GetEraName(era));
|
||||
}
|
||||
}
|
||||
else if (string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
var ampm = date.ToString("tt", CultureInfo.InvariantCulture);
|
||||
result.Append(ampm.ToUpperInvariant());
|
||||
}
|
||||
else if (string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
var ampm = date.ToString("%t", CultureInfo.InvariantCulture);
|
||||
if (char.IsUpper(token[0]))
|
||||
{
|
||||
result.Append(ampm.ToUpperInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(ampm.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
else if (token.StartsWith(".0"))
|
||||
{
|
||||
var value = date.Millisecond;
|
||||
var digits = token.Length - 1;
|
||||
result.Append("." + value.ToString("D" + digits));
|
||||
}
|
||||
else if (token == "/")
|
||||
{
|
||||
#if NETSTANDARD1_0
|
||||
result.Append(DateTime.MaxValue.ToString("/d", culture)[0]);
|
||||
#else
|
||||
result.Append(culture.DateTimeFormat.DateSeparator);
|
||||
#endif
|
||||
}
|
||||
else if (token == ",")
|
||||
{
|
||||
while (i < tokens.Count - 1 && tokens[i + 1] == ",")
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
result.Append(",");
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static bool LookAheadDatePart(List<string> tokens, int fromIndex, string startsWith)
|
||||
{
|
||||
for (var i = fromIndex; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (token.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
if (Token.IsDatePart(token))
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LookBackDatePart(List<string> tokens, int fromIndex, string startsWith)
|
||||
{
|
||||
for (var i = fromIndex; i >= 0; i--)
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (token.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
if (Token.IsDatePart(token))
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsAmPm(List<string> tokens)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static string FormatNumber(double value, DecimalSection format, CultureInfo culture)
|
||||
{
|
||||
bool thousandSeparator = format.ThousandSeparator;
|
||||
value = value / format.ThousandDivisor;
|
||||
value = value * format.PercentMultiplier;
|
||||
|
||||
var result = new StringBuilder();
|
||||
FormatNumber(value, format.BeforeDecimal, format.DecimalSeparator, format.AfterDecimal, thousandSeparator, culture, result);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
static void FormatNumber(double value, List<string> beforeDecimal, bool decimalSeparator, List<string> afterDecimal, bool thousandSeparator, CultureInfo culture, StringBuilder result)
|
||||
{
|
||||
int signitificantDigits = 0;
|
||||
if (afterDecimal != null)
|
||||
signitificantDigits = GetDigitCount(afterDecimal);
|
||||
|
||||
var valueString = Math.Abs(value).ToString("F" + signitificantDigits, CultureInfo.InvariantCulture);
|
||||
var valueStrings = valueString.Split('.');
|
||||
var thousandsString = valueStrings[0];
|
||||
var decimalString = valueStrings.Length > 1 ? valueStrings[1].TrimEnd('0') : "";
|
||||
|
||||
if (value < 0)
|
||||
{
|
||||
result.Append("-");
|
||||
}
|
||||
|
||||
if (beforeDecimal != null)
|
||||
{
|
||||
FormatThousands(thousandsString, thousandSeparator, false, beforeDecimal, culture, result);
|
||||
}
|
||||
|
||||
if (decimalSeparator) {
|
||||
result.Append(culture.NumberFormat.NumberDecimalSeparator);
|
||||
}
|
||||
|
||||
if (afterDecimal != null)
|
||||
{
|
||||
FormatDecimals(decimalString, afterDecimal, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints right-aligned, left-padded integer before the decimal separator. With optional most-significant zero.
|
||||
/// </summary>
|
||||
public static void FormatThousands(string valueString, bool thousandSeparator, bool significantZero, List<string> tokens, CultureInfo culture, StringBuilder result)
|
||||
{
|
||||
var significant = false;
|
||||
var formatDigits = GetDigitCount(tokens);
|
||||
valueString = valueString.PadLeft(formatDigits, '0');
|
||||
|
||||
// Print literals occurring before any placeholders
|
||||
var tokenIndex = 0;
|
||||
for (; tokenIndex < tokens.Count; tokenIndex++)
|
||||
{
|
||||
var token = tokens[tokenIndex];
|
||||
if (Token.IsPlaceholder(token))
|
||||
break;
|
||||
else
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
|
||||
// Print value digits until there are as many digits remaining as there are placeholders
|
||||
var digitIndex = 0;
|
||||
for (; digitIndex < (valueString.Length - formatDigits); digitIndex++)
|
||||
{
|
||||
significant = true;
|
||||
result.Append(valueString[digitIndex]);
|
||||
|
||||
if (thousandSeparator)
|
||||
FormatThousandSeparator(valueString, digitIndex, culture, result);
|
||||
}
|
||||
|
||||
// Print remaining value digits and format literals
|
||||
for (; tokenIndex < tokens.Count; ++tokenIndex)
|
||||
{
|
||||
var token = tokens[tokenIndex];
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
var c = valueString[digitIndex];
|
||||
if (c != '0' || (significantZero && digitIndex == valueString.Length - 1)) significant = true;
|
||||
|
||||
FormatPlaceholder(token, c, significant, result);
|
||||
|
||||
if (thousandSeparator && (significant || token.Equals("0")))
|
||||
FormatThousandSeparator(valueString, digitIndex, culture, result);
|
||||
|
||||
digitIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void FormatThousandSeparator(string valueString, int digit, CultureInfo culture, StringBuilder result)
|
||||
{
|
||||
var positionInTens = valueString.Length - 1 - digit;
|
||||
if (positionInTens > 0 && (positionInTens % 3) == 0)
|
||||
{
|
||||
result.Append(culture.NumberFormat.NumberGroupSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints left-aligned, right-padded integer after the decimal separator. Does not print significant zero.
|
||||
/// </summary>
|
||||
public static void FormatDecimals(string valueString, List<string> tokens, StringBuilder result)
|
||||
{
|
||||
var significant = true;
|
||||
var unpaddedDigits = valueString.Length;
|
||||
var formatDigits = GetDigitCount(tokens);
|
||||
|
||||
valueString = valueString.PadRight(formatDigits, '0');
|
||||
|
||||
// Print all format digits
|
||||
var valueIndex = 0;
|
||||
for (var tokenIndex = 0; tokenIndex < tokens.Count; ++tokenIndex)
|
||||
{
|
||||
var token = tokens[tokenIndex];
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
var c = valueString[valueIndex];
|
||||
significant = valueIndex < unpaddedDigits;
|
||||
|
||||
FormatPlaceholder(token, c, significant, result);
|
||||
valueIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatLiteral(token, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static string FormatExponential(double value, Section format, CultureInfo culture)
|
||||
{
|
||||
// The application shall display a number to the right of
|
||||
// the "E" symbol that corresponds to the number of places that
|
||||
// the decimal point was moved.
|
||||
|
||||
var baseDigits = 0;
|
||||
if (format.Exponential.BeforeDecimal != null)
|
||||
{
|
||||
baseDigits = GetDigitCount(format.Exponential.BeforeDecimal);
|
||||
}
|
||||
|
||||
var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value)));
|
||||
var mantissa = value / Math.Pow(10, exponent);
|
||||
|
||||
var shift = Math.Abs(exponent) % baseDigits;
|
||||
if (shift > 0)
|
||||
{
|
||||
if (exponent < 0)
|
||||
shift = (baseDigits - shift);
|
||||
|
||||
mantissa *= Math.Pow(10, shift);
|
||||
exponent -= shift;
|
||||
}
|
||||
|
||||
var result = new StringBuilder();
|
||||
FormatNumber(mantissa, format.Exponential.BeforeDecimal, format.Exponential.DecimalSeparator, format.Exponential.AfterDecimal, false, culture, result);
|
||||
|
||||
result.Append(format.Exponential.ExponentialToken[0]);
|
||||
|
||||
if (format.Exponential.ExponentialToken[1] == '+' && exponent >= 0)
|
||||
{
|
||||
result.Append("+");
|
||||
}
|
||||
else if (exponent < 0)
|
||||
{
|
||||
result.Append("-");
|
||||
}
|
||||
|
||||
FormatThousands(Math.Abs(exponent).ToString(CultureInfo.InvariantCulture), false, false, format.Exponential.Power, culture, result);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
static string FormatFraction(double value, Section format, CultureInfo culture)
|
||||
{
|
||||
int integral = 0;
|
||||
int numerator, denominator;
|
||||
|
||||
bool sign = value < 0;
|
||||
|
||||
if (format.Fraction.IntegerPart != null)
|
||||
{
|
||||
integral = (int)Math.Truncate(value);
|
||||
value = Math.Abs(value - integral);
|
||||
}
|
||||
|
||||
if (format.Fraction.DenominatorConstant != 0)
|
||||
{
|
||||
denominator = format.Fraction.DenominatorConstant;
|
||||
var rr = Math.Round(value * denominator);
|
||||
var b = Math.Floor(rr / denominator);
|
||||
numerator = (int)(rr - b * denominator);
|
||||
}
|
||||
else
|
||||
{
|
||||
var denominatorDigits = Math.Min(GetDigitCount(format.Fraction.Denominator), 7);
|
||||
GetFraction(value, (int)Math.Pow(10, denominatorDigits) - 1, out numerator, out denominator);
|
||||
}
|
||||
|
||||
// Don't hide fraction if at least one zero in the numerator format
|
||||
var numeratorZeros = GetZeroCount(format.Fraction.Numerator);
|
||||
var hideFraction = (format.Fraction.IntegerPart != null && numerator == 0 && numeratorZeros == 0);
|
||||
|
||||
var result = new StringBuilder();
|
||||
|
||||
if (sign)
|
||||
result.Append("-");
|
||||
|
||||
// Print integer part with significant zero if fraction part is hidden
|
||||
if (format.Fraction.IntegerPart != null)
|
||||
FormatThousands(Math.Abs(integral).ToString("F0", CultureInfo.InvariantCulture), false, hideFraction, format.Fraction.IntegerPart, culture, result);
|
||||
|
||||
var numeratorString = Math.Abs(numerator).ToString("F0", CultureInfo.InvariantCulture);
|
||||
var denominatorString = denominator.ToString("F0", CultureInfo.InvariantCulture);
|
||||
|
||||
var fraction = new StringBuilder();
|
||||
|
||||
FormatThousands(numeratorString, false, true, format.Fraction.Numerator, culture, fraction);
|
||||
|
||||
fraction.Append("/");
|
||||
|
||||
if (format.Fraction.DenominatorPrefix != null)
|
||||
FormatThousands("", false, false, format.Fraction.DenominatorPrefix, culture, fraction);
|
||||
|
||||
if (format.Fraction.DenominatorConstant != 0)
|
||||
fraction.Append(format.Fraction.DenominatorConstant.ToString());
|
||||
else
|
||||
FormatDenominator(denominatorString, format.Fraction.Denominator, fraction);
|
||||
|
||||
if (format.Fraction.DenominatorSuffix != null)
|
||||
FormatThousands("", false, false, format.Fraction.DenominatorSuffix, culture, fraction);
|
||||
|
||||
if (hideFraction)
|
||||
result.Append(new string(' ', fraction.ToString().Length));
|
||||
else
|
||||
result.Append(fraction.ToString());
|
||||
|
||||
if (format.Fraction.FractionSuffix != null)
|
||||
FormatThousands("", false, false, format.Fraction.FractionSuffix, culture, result);
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
// Adapted from ssf.js 'frac()' helper
|
||||
static void GetFraction(double x, int D, out int nom, out int den)
|
||||
{
|
||||
var sgn = x < 0 ? -1 : 1;
|
||||
var B = x * sgn;
|
||||
var P_2 = 0.0;
|
||||
var P_1 = 1.0;
|
||||
var P = 0.0;
|
||||
var Q_2 = 1.0;
|
||||
var Q_1 = 0.0;
|
||||
var Q = 0.0;
|
||||
var A = Math.Floor(B);
|
||||
while (Q_1 < D)
|
||||
{
|
||||
A = Math.Floor(B);
|
||||
P = A * P_1 + P_2;
|
||||
Q = A * Q_1 + Q_2;
|
||||
if ((B - A) < 0.00000005) break;
|
||||
B = 1 / (B - A);
|
||||
P_2 = P_1; P_1 = P;
|
||||
Q_2 = Q_1; Q_1 = Q;
|
||||
}
|
||||
if (Q > D) { if (Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } }
|
||||
nom = (int)(sgn * P);
|
||||
den = (int)Q;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints left-aligned, left-padded fraction integer denominator.
|
||||
/// Assumes tokens contain only placeholders, valueString has fewer or equal number of digits as tokens.
|
||||
/// </summary>
|
||||
public static void FormatDenominator(string valueString, List<string> tokens, StringBuilder result)
|
||||
{
|
||||
var formatDigits = GetDigitCount(tokens);
|
||||
valueString = valueString.PadLeft(formatDigits, '0');
|
||||
|
||||
bool significant = false;
|
||||
var valueIndex = 0;
|
||||
for (var tokenIndex = 0; tokenIndex < tokens.Count; ++tokenIndex)
|
||||
{
|
||||
var token = tokens[tokenIndex];
|
||||
char c;
|
||||
if (valueIndex < valueString.Length) {
|
||||
c = GetLeftAlignedValueDigit(token, valueString, valueIndex, significant, out valueIndex);
|
||||
|
||||
if (c != '0')
|
||||
significant = true;
|
||||
} else {
|
||||
c = '0';
|
||||
significant = false;
|
||||
}
|
||||
|
||||
FormatPlaceholder(token, c, significant, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first digit from valueString. If the token is '?'
|
||||
/// returns the first significant digit from valueString, or '0' if there are no significant digits.
|
||||
/// The out valueIndex parameter contains the offset to the next digit in valueString.
|
||||
/// </summary>
|
||||
static char GetLeftAlignedValueDigit(string token, string valueString, int startIndex, bool significant, out int valueIndex)
|
||||
{
|
||||
char c;
|
||||
valueIndex = startIndex;
|
||||
if (valueIndex < valueString.Length)
|
||||
{
|
||||
c = valueString[valueIndex];
|
||||
valueIndex++;
|
||||
|
||||
if (c != '0')
|
||||
significant = true;
|
||||
|
||||
if (token == "?" && !significant)
|
||||
{
|
||||
// Eat insignificant zeros to left align denominator
|
||||
while (valueIndex < valueString.Length)
|
||||
{
|
||||
c = valueString[valueIndex];
|
||||
valueIndex++;
|
||||
|
||||
if (c != '0')
|
||||
{
|
||||
significant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
c = '0';
|
||||
significant = false;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
static void FormatPlaceholder(string token, char c, bool significant, StringBuilder result)
|
||||
{
|
||||
if (token == "0")
|
||||
{
|
||||
if (significant)
|
||||
result.Append(c);
|
||||
else
|
||||
result.Append("0");
|
||||
}
|
||||
else if (token == "#")
|
||||
{
|
||||
if (significant)
|
||||
result.Append(c);
|
||||
}
|
||||
else if (token == "?")
|
||||
{
|
||||
if (significant)
|
||||
result.Append(c);
|
||||
else
|
||||
result.Append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
static int GetDigitCount(List<string> tokens)
|
||||
{
|
||||
var counter = 0;
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
|
||||
static int GetZeroCount(List<string> tokens)
|
||||
{
|
||||
var counter = 0;
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token == "0")
|
||||
{
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
|
||||
static void FormatLiteral(string token, StringBuilder result)
|
||||
{
|
||||
string literal = string.Empty;
|
||||
if (token == ",")
|
||||
{
|
||||
; // skip commas
|
||||
}
|
||||
else if (token.Length == 2 && (token[0] == '*' || token[0] == '\\'))
|
||||
{
|
||||
// TODO: * = repeat to fill cell
|
||||
literal = token[1].ToString();
|
||||
}
|
||||
else if (token.Length == 2 && token[0] == '_')
|
||||
{
|
||||
literal = " ";
|
||||
}
|
||||
else if (token.StartsWith("\""))
|
||||
{
|
||||
literal = token.Substring(1, token.Length - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
literal = token;
|
||||
}
|
||||
result.Append(literal);
|
||||
}
|
||||
}
|
||||
}
|
216
src/ExcelNumberFormat/FractionSection.cs
Normal file
216
src/ExcelNumberFormat/FractionSection.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class FractionSection
|
||||
{
|
||||
public List<string> IntegerPart { get; set; }
|
||||
|
||||
public List<string> Numerator { get; set; }
|
||||
|
||||
public List<string> DenominatorPrefix { get; set; }
|
||||
|
||||
public List<string> Denominator { get; set; }
|
||||
|
||||
public int DenominatorConstant { get; set; }
|
||||
|
||||
public List<string> DenominatorSuffix { get; set; }
|
||||
|
||||
public List<string> FractionSuffix { get; set; }
|
||||
|
||||
static public bool TryParse(List<string> tokens, out FractionSection format)
|
||||
{
|
||||
List<string> numeratorParts = null;
|
||||
List<string> denominatorParts = null;
|
||||
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var part = tokens[i];
|
||||
if (part == "/")
|
||||
{
|
||||
numeratorParts = tokens.GetRange(0, i);
|
||||
i++;
|
||||
denominatorParts = tokens.GetRange(i, tokens.Count - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (numeratorParts == null)
|
||||
{
|
||||
format = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
GetNumerator(numeratorParts, out var integerPart, out var numeratorPart);
|
||||
|
||||
if (!TryGetDenominator(denominatorParts, out var denominatorPrefix, out var denominatorPart, out var denominatorConstant, out var denominatorSuffix, out var fractionSuffix))
|
||||
{
|
||||
format = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
format = new FractionSection()
|
||||
{
|
||||
IntegerPart = integerPart,
|
||||
Numerator = numeratorPart,
|
||||
DenominatorPrefix = denominatorPrefix,
|
||||
Denominator = denominatorPart,
|
||||
DenominatorConstant = denominatorConstant,
|
||||
DenominatorSuffix = denominatorSuffix,
|
||||
FractionSuffix = fractionSuffix
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void GetNumerator(List<string> tokens, out List<string> integerPart, out List<string> numeratorPart)
|
||||
{
|
||||
var hasPlaceholder = false;
|
||||
var hasSpace = false;
|
||||
var hasIntegerPart = false;
|
||||
var numeratorIndex = -1;
|
||||
var index = tokens.Count - 1;
|
||||
while (index >= 0)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
hasPlaceholder = true;
|
||||
|
||||
if (hasSpace)
|
||||
{
|
||||
hasIntegerPart = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasPlaceholder && !hasSpace)
|
||||
{
|
||||
// First time we get here marks the end of the integer part
|
||||
hasSpace = true;
|
||||
numeratorIndex = index + 1;
|
||||
}
|
||||
}
|
||||
index--;
|
||||
}
|
||||
|
||||
if (hasIntegerPart)
|
||||
{
|
||||
integerPart = tokens.GetRange(0, numeratorIndex);
|
||||
numeratorPart = tokens.GetRange(numeratorIndex, tokens.Count - numeratorIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
integerPart = null;
|
||||
numeratorPart = tokens;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryGetDenominator(List<string> tokens, out List<string> denominatorPrefix, out List<string> denominatorPart, out int denominatorConstant, out List<string> denominatorSuffix, out List<string> fractionSuffix)
|
||||
{
|
||||
var index = 0;
|
||||
var hasPlaceholder = false;
|
||||
var hasConstant = false;
|
||||
|
||||
var constant = new StringBuilder();
|
||||
|
||||
// Read literals until the first number placeholder or digit
|
||||
while (index < tokens.Count)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
hasPlaceholder = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
if (Token.IsDigit19(token))
|
||||
{
|
||||
hasConstant = true;
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!hasPlaceholder && !hasConstant)
|
||||
{
|
||||
denominatorPrefix = null;
|
||||
denominatorPart = null;
|
||||
denominatorConstant = 0;
|
||||
denominatorSuffix = null;
|
||||
fractionSuffix = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// The denominator starts here, keep the index
|
||||
var denominatorIndex = index;
|
||||
|
||||
// Read placeholders or digits in sequence
|
||||
while (index < tokens.Count)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (hasPlaceholder && Token.IsPlaceholder(token))
|
||||
{
|
||||
; // OK
|
||||
}
|
||||
else
|
||||
if (hasConstant && (Token.IsDigit09(token)))
|
||||
{
|
||||
constant.Append(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// 'index' is now at the first token after the denominator placeholders.
|
||||
// The remaining, if anything, is to be treated in one or two parts:
|
||||
// Any ultimately terminating literals are considered the "Fraction suffix".
|
||||
// Anything between the denominator and the fraction suffix is the "Denominator suffix".
|
||||
// Placeholders in the denominator suffix are treated as insignificant zeros.
|
||||
|
||||
// Scan backwards to determine the fraction suffix
|
||||
int fractionSuffixIndex = tokens.Count;
|
||||
while (fractionSuffixIndex > index)
|
||||
{
|
||||
var token = tokens[fractionSuffixIndex - 1];
|
||||
if (Token.IsPlaceholder(token))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
fractionSuffixIndex--;
|
||||
}
|
||||
|
||||
// Finally extract the detected token ranges
|
||||
|
||||
if (denominatorIndex > 0)
|
||||
denominatorPrefix = tokens.GetRange(0, denominatorIndex);
|
||||
else
|
||||
denominatorPrefix = null;
|
||||
|
||||
if (hasConstant)
|
||||
denominatorConstant = int.Parse(constant.ToString());
|
||||
else
|
||||
denominatorConstant = 0;
|
||||
|
||||
denominatorPart = tokens.GetRange(denominatorIndex, index - denominatorIndex);
|
||||
|
||||
if (index < fractionSuffixIndex)
|
||||
denominatorSuffix = tokens.GetRange(index, fractionSuffixIndex - index);
|
||||
else
|
||||
denominatorSuffix = null;
|
||||
|
||||
if (fractionSuffixIndex < tokens.Count)
|
||||
fractionSuffix = tokens.GetRange(fractionSuffixIndex, tokens.Count - fractionSuffixIndex);
|
||||
else
|
||||
fractionSuffix = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
86
src/ExcelNumberFormat/NumberFormat.cs
Normal file
86
src/ExcelNumberFormat/NumberFormat.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse ECMA-376 number format strings and format values like Excel and other spreadsheet softwares.
|
||||
/// </summary>
|
||||
public class NumberFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NumberFormat"/> class.
|
||||
/// </summary>
|
||||
/// <param name="formatString">The number format string.</param>
|
||||
public NumberFormat(string formatString)
|
||||
{
|
||||
var sections = Parser.ParseSections(formatString, out bool syntaxError);
|
||||
|
||||
IsValid = !syntaxError;
|
||||
FormatString = formatString;
|
||||
|
||||
if (IsValid)
|
||||
{
|
||||
Sections = sections;
|
||||
IsDateTimeFormat = Evaluator.GetFirstSection(Sections, SectionType.Date) != null;
|
||||
IsTimeSpanFormat = Evaluator.GetFirstSection(Sections, SectionType.Duration) != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Sections = new List<Section>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the number format string is valid.
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number format string.
|
||||
/// </summary>
|
||||
public string FormatString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the format represents a DateTime
|
||||
/// </summary>
|
||||
public bool IsDateTimeFormat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the format represents a TimeSpan
|
||||
/// </summary>
|
||||
public bool IsTimeSpanFormat { get; }
|
||||
|
||||
internal List<Section> Sections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Formats a value with this number format in a specified culture.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to format.</param>
|
||||
/// <param name="culture">The culture to use for formatting.</param>
|
||||
/// <param name="isDate1904">If false, numeric dates start on January 0 1900 and include February 29 1900 - like Excel on PC. If true, numeric dates start on January 1 1904 - like Excel on Mac.</param>
|
||||
/// <returns>The formatted string.</returns>
|
||||
public string Format(object value, CultureInfo culture, bool isDate1904 = false)
|
||||
{
|
||||
var section = Evaluator.GetSection(Sections, value);
|
||||
if (section == null)
|
||||
return CompatibleConvert.ToString(value, culture);
|
||||
|
||||
try
|
||||
{
|
||||
return Formatter.Format(value, section, culture, isDate1904);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
// TimeSpan cast exception
|
||||
return CompatibleConvert.ToString(value, culture);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Convert.ToDouble/ToDateTime exceptions
|
||||
return CompatibleConvert.ToString(value, culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
398
src/ExcelNumberFormat/Parser.cs
Normal file
398
src/ExcelNumberFormat/Parser.cs
Normal file
@ -0,0 +1,398 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal static class Parser
|
||||
{
|
||||
public static List<Section> ParseSections(string formatString, out bool syntaxError)
|
||||
{
|
||||
var tokenizer = new Tokenizer(formatString);
|
||||
var sections = new List<Section>();
|
||||
syntaxError = false;
|
||||
while (true)
|
||||
{
|
||||
var section = ParseSection(tokenizer, sections.Count, out var sectionSyntaxError);
|
||||
|
||||
if (sectionSyntaxError)
|
||||
syntaxError = true;
|
||||
|
||||
if (section == null)
|
||||
break;
|
||||
|
||||
sections.Add(section);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private static Section ParseSection(Tokenizer reader, int index, out bool syntaxError)
|
||||
{
|
||||
bool hasDateParts = false;
|
||||
bool hasDurationParts = false;
|
||||
bool hasGeneralPart = false;
|
||||
bool hasTextPart = false;
|
||||
bool hasPlaceholders = false;
|
||||
Condition condition = null;
|
||||
Color color = null;
|
||||
string token;
|
||||
List<string> tokens = new List<string>();
|
||||
|
||||
syntaxError = false;
|
||||
while ((token = ReadToken(reader, out syntaxError)) != null)
|
||||
{
|
||||
if (token == ";")
|
||||
break;
|
||||
|
||||
hasPlaceholders |= Token.IsPlaceholder(token);
|
||||
|
||||
if (Token.IsDatePart(token))
|
||||
{
|
||||
hasDateParts |= true;
|
||||
hasDurationParts |= Token.IsDurationPart(token);
|
||||
tokens.Add(token);
|
||||
}
|
||||
else if (Token.IsGeneral(token))
|
||||
{
|
||||
hasGeneralPart |= true;
|
||||
tokens.Add(token);
|
||||
}
|
||||
else if (token == "@")
|
||||
{
|
||||
hasTextPart |= true;
|
||||
tokens.Add(token);
|
||||
}
|
||||
else if (token.StartsWith("["))
|
||||
{
|
||||
// Does not add to tokens. Absolute/elapsed time tokens
|
||||
// also start with '[', but handled as date part above
|
||||
var expression = token.Substring(1, token.Length - 2);
|
||||
if (TryParseCondition(expression, out var parseCondition))
|
||||
condition = parseCondition;
|
||||
else if (TryParseColor(expression, out var parseColor))
|
||||
color = parseColor;
|
||||
else if (TryParseCurrencySymbol(expression, out var parseCurrencySymbol))
|
||||
tokens.Add("\"" + parseCurrencySymbol + "\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.Add(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (syntaxError || tokens.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
(hasDateParts && (hasGeneralPart || hasTextPart)) ||
|
||||
(hasGeneralPart && (hasDateParts || hasTextPart)) ||
|
||||
(hasTextPart && (hasGeneralPart || hasDateParts)))
|
||||
{
|
||||
// Cannot mix date, general and/or text parts
|
||||
syntaxError = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
SectionType type;
|
||||
FractionSection fraction = null;
|
||||
ExponentialSection exponential = null;
|
||||
DecimalSection number = null;
|
||||
List<string> generalTextDateDuration = null;
|
||||
|
||||
if (hasDateParts)
|
||||
{
|
||||
if (hasDurationParts)
|
||||
{
|
||||
type = SectionType.Duration;
|
||||
}
|
||||
else
|
||||
{
|
||||
type = SectionType.Date;
|
||||
}
|
||||
|
||||
ParseMilliseconds(tokens, out generalTextDateDuration);
|
||||
}
|
||||
else if (hasGeneralPart)
|
||||
{
|
||||
type = SectionType.General;
|
||||
generalTextDateDuration = tokens;
|
||||
}
|
||||
else if (hasTextPart || !hasPlaceholders)
|
||||
{
|
||||
type = SectionType.Text;
|
||||
generalTextDateDuration = tokens;
|
||||
}
|
||||
else if (FractionSection.TryParse(tokens, out fraction))
|
||||
{
|
||||
type = SectionType.Fraction;
|
||||
}
|
||||
else if (ExponentialSection.TryParse(tokens, out exponential))
|
||||
{
|
||||
type = SectionType.Exponential;
|
||||
}
|
||||
else if (DecimalSection.TryParse(tokens, out number))
|
||||
{
|
||||
type = SectionType.Number;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unable to parse format string
|
||||
syntaxError = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Section()
|
||||
{
|
||||
Type = type,
|
||||
SectionIndex = index,
|
||||
Color = color,
|
||||
Condition = condition,
|
||||
Fraction = fraction,
|
||||
Exponential = exponential,
|
||||
Number = number,
|
||||
GeneralTextDateDurationParts = generalTextDateDuration
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses as many placeholders and literals needed to format a number with optional decimals.
|
||||
/// Returns number of tokens parsed, or 0 if the tokens didn't form a number.
|
||||
/// </summary>
|
||||
internal static int ParseNumberTokens(List<string> tokens, int startPosition, out List<string> beforeDecimal, out bool decimalSeparator, out List<string> afterDecimal)
|
||||
{
|
||||
beforeDecimal = null;
|
||||
afterDecimal = null;
|
||||
decimalSeparator = false;
|
||||
|
||||
List<string> remainder = new List<string>();
|
||||
var index = 0;
|
||||
for (index = 0; index < tokens.Count; ++index)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (token == "." && beforeDecimal == null)
|
||||
{
|
||||
decimalSeparator = true;
|
||||
beforeDecimal = tokens.GetRange(0, index); // TODO: why not remainder? has only valid tokens...
|
||||
|
||||
remainder = new List<string>();
|
||||
}
|
||||
else if (Token.IsNumberLiteral(token))
|
||||
{
|
||||
remainder.Add(token);
|
||||
}
|
||||
else if (token.StartsWith("["))
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (remainder.Count > 0)
|
||||
{
|
||||
if (beforeDecimal != null)
|
||||
{
|
||||
afterDecimal = remainder;
|
||||
}
|
||||
else
|
||||
{
|
||||
beforeDecimal = remainder;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static void ParseMilliseconds(List<string> tokens, out List<string> result)
|
||||
{
|
||||
// if tokens form .0 through .000.., combine to single subsecond token
|
||||
result = new List<string>();
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (token == ".")
|
||||
{
|
||||
var zeros = 0;
|
||||
while (i + 1 < tokens.Count && tokens[i + 1] == "0")
|
||||
{
|
||||
i++;
|
||||
zeros++;
|
||||
}
|
||||
|
||||
if (zeros > 0)
|
||||
result.Add("." + new string('0', zeros));
|
||||
else
|
||||
result.Add(".");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadToken(Tokenizer reader, out bool syntaxError)
|
||||
{
|
||||
var offset = reader.Position;
|
||||
if (
|
||||
ReadLiteral(reader) ||
|
||||
reader.ReadEnclosed('[', ']') ||
|
||||
|
||||
// Symbols
|
||||
reader.ReadOneOf("#?,!&%+-$€£0123456789{}():;/.@ ") ||
|
||||
reader.ReadString("e+", true) ||
|
||||
reader.ReadString("e-", true) ||
|
||||
reader.ReadString("General", true) ||
|
||||
|
||||
// Date
|
||||
reader.ReadString("am/pm", true) ||
|
||||
reader.ReadString("a/p", true) ||
|
||||
reader.ReadOneOrMore('y') ||
|
||||
reader.ReadOneOrMore('Y') ||
|
||||
reader.ReadOneOrMore('m') ||
|
||||
reader.ReadOneOrMore('M') ||
|
||||
reader.ReadOneOrMore('d') ||
|
||||
reader.ReadOneOrMore('D') ||
|
||||
reader.ReadOneOrMore('h') ||
|
||||
reader.ReadOneOrMore('H') ||
|
||||
reader.ReadOneOrMore('s') ||
|
||||
reader.ReadOneOrMore('S') ||
|
||||
reader.ReadOneOrMore('g') ||
|
||||
reader.ReadOneOrMore('G'))
|
||||
{
|
||||
syntaxError = false;
|
||||
var length = reader.Position - offset;
|
||||
return reader.Substring(offset, length);
|
||||
}
|
||||
|
||||
syntaxError = reader.Position < reader.Length;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ReadLiteral(Tokenizer reader)
|
||||
{
|
||||
if (reader.Peek() == '\\' || reader.Peek() == '*' || reader.Peek() == '_')
|
||||
{
|
||||
reader.Advance(2);
|
||||
return true;
|
||||
}
|
||||
else if (reader.ReadEnclosed('"', '"'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCondition(string token, out Condition result)
|
||||
{
|
||||
var tokenizer = new Tokenizer(token);
|
||||
|
||||
if (tokenizer.ReadString("<=") ||
|
||||
tokenizer.ReadString("<>") ||
|
||||
tokenizer.ReadString("<") ||
|
||||
tokenizer.ReadString(">=") ||
|
||||
tokenizer.ReadString(">") ||
|
||||
tokenizer.ReadString("="))
|
||||
{
|
||||
var conditionPosition = tokenizer.Position;
|
||||
var op = tokenizer.Substring(0, conditionPosition);
|
||||
|
||||
if (ReadConditionValue(tokenizer))
|
||||
{
|
||||
var valueString = tokenizer.Substring(conditionPosition, tokenizer.Position - conditionPosition);
|
||||
|
||||
result = new Condition()
|
||||
{
|
||||
Operator = op,
|
||||
Value = double.Parse(valueString, CultureInfo.InvariantCulture)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ReadConditionValue(Tokenizer tokenizer)
|
||||
{
|
||||
// NFPartCondNum = [ASCII-HYPHEN-MINUS] NFPartIntNum [INTL-CHAR-DECIMAL-SEP NFPartIntNum] [NFPartExponential NFPartIntNum]
|
||||
tokenizer.ReadString("-");
|
||||
while (tokenizer.ReadOneOf("0123456789"))
|
||||
{
|
||||
}
|
||||
|
||||
if (tokenizer.ReadString("."))
|
||||
{
|
||||
while (tokenizer.ReadOneOf("0123456789"))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenizer.ReadString("e+", true) || tokenizer.ReadString("e-", true))
|
||||
{
|
||||
if (tokenizer.ReadOneOf("0123456789"))
|
||||
{
|
||||
while (tokenizer.ReadOneOf("0123456789"))
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseColor(string token, out Color color)
|
||||
{
|
||||
// TODO: Color1..59
|
||||
var tokenizer = new Tokenizer(token);
|
||||
if (
|
||||
tokenizer.ReadString("black", true) ||
|
||||
tokenizer.ReadString("blue", true) ||
|
||||
tokenizer.ReadString("cyan", true) ||
|
||||
tokenizer.ReadString("green", true) ||
|
||||
tokenizer.ReadString("magenta", true) ||
|
||||
tokenizer.ReadString("red", true) ||
|
||||
tokenizer.ReadString("white", true) ||
|
||||
tokenizer.ReadString("yellow", true))
|
||||
{
|
||||
color = new Color()
|
||||
{
|
||||
Value = tokenizer.Substring(0, tokenizer.Position)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
color = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCurrencySymbol(string token, out string currencySymbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token)
|
||||
|| !token.StartsWith("$"))
|
||||
{
|
||||
currencySymbol = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (token.Contains("-"))
|
||||
currencySymbol = token.Substring(1, token.IndexOf('-') - 1);
|
||||
else
|
||||
currencySymbol = token.Substring(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
23
src/ExcelNumberFormat/Section.cs
Normal file
23
src/ExcelNumberFormat/Section.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class Section
|
||||
{
|
||||
public int SectionIndex { get; set; }
|
||||
|
||||
public SectionType Type { get; set; }
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public Condition Condition { get; set; }
|
||||
|
||||
public ExponentialSection Exponential { get; set; }
|
||||
|
||||
public FractionSection Fraction { get; set; }
|
||||
|
||||
public DecimalSection Number { get; set; }
|
||||
|
||||
public List<string> GeneralTextDateDurationParts { get; set; }
|
||||
}
|
||||
}
|
13
src/ExcelNumberFormat/SectionType.cs
Normal file
13
src/ExcelNumberFormat/SectionType.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal enum SectionType
|
||||
{
|
||||
General,
|
||||
Number,
|
||||
Fraction,
|
||||
Exponential,
|
||||
Date,
|
||||
Duration,
|
||||
Text,
|
||||
}
|
||||
}
|
110
src/ExcelNumberFormat/Token.cs
Normal file
110
src/ExcelNumberFormat/Token.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal static class Token
|
||||
{
|
||||
public static bool IsExponent(string token)
|
||||
{
|
||||
return
|
||||
(string.Compare(token, "e+", StringComparison.OrdinalIgnoreCase) == 0) ||
|
||||
(string.Compare(token, "e-", StringComparison.OrdinalIgnoreCase) == 0);
|
||||
}
|
||||
|
||||
public static bool IsLiteral(string token)
|
||||
{
|
||||
return
|
||||
token.StartsWith("_") ||
|
||||
token.StartsWith("\\") ||
|
||||
token.StartsWith("\"") ||
|
||||
token.StartsWith("*") ||
|
||||
token == "," ||
|
||||
token == "!" ||
|
||||
token == "&" ||
|
||||
token == "%" ||
|
||||
token == "+" ||
|
||||
token == "-" ||
|
||||
token == "$" ||
|
||||
token == "€" ||
|
||||
token == "£" ||
|
||||
token == "1" ||
|
||||
token == "2" ||
|
||||
token == "3" ||
|
||||
token == "4" ||
|
||||
token == "5" ||
|
||||
token == "6" ||
|
||||
token == "7" ||
|
||||
token == "8" ||
|
||||
token == "9" ||
|
||||
token == "{" ||
|
||||
token == "}" ||
|
||||
token == "(" ||
|
||||
token == ")" ||
|
||||
token == " ";
|
||||
}
|
||||
|
||||
public static bool IsNumberLiteral(string token)
|
||||
{
|
||||
return
|
||||
IsPlaceholder(token) ||
|
||||
IsLiteral(token) ||
|
||||
token == ".";
|
||||
}
|
||||
|
||||
public static bool IsPlaceholder(string token)
|
||||
{
|
||||
return token == "0" || token == "#" || token == "?";
|
||||
}
|
||||
|
||||
public static bool IsGeneral(string token)
|
||||
{
|
||||
return string.Compare(token, "general", StringComparison.OrdinalIgnoreCase) == 0;
|
||||
}
|
||||
|
||||
public static bool IsDatePart(string token)
|
||||
{
|
||||
return
|
||||
token.StartsWith("y", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("m", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("d", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("s", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("h", StringComparison.OrdinalIgnoreCase) ||
|
||||
(token.StartsWith("g", StringComparison.OrdinalIgnoreCase) && !IsGeneral(token)) ||
|
||||
string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0 ||
|
||||
string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0 ||
|
||||
IsDurationPart(token);
|
||||
}
|
||||
|
||||
public static bool IsDurationPart(string token)
|
||||
{
|
||||
return
|
||||
token.StartsWith("[h", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("[m", StringComparison.OrdinalIgnoreCase) ||
|
||||
token.StartsWith("[s", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsDigit09(string token)
|
||||
{
|
||||
return token == "0" || IsDigit19(token);
|
||||
}
|
||||
|
||||
public static bool IsDigit19(string token)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
case "7":
|
||||
case "8":
|
||||
case "9":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
src/ExcelNumberFormat/Tokenizer.cs
Normal file
121
src/ExcelNumberFormat/Tokenizer.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
|
||||
namespace ExcelNumberFormat
|
||||
{
|
||||
internal class Tokenizer
|
||||
{
|
||||
private string formatString;
|
||||
private int formatStringPosition = 0;
|
||||
|
||||
public Tokenizer(string fmt)
|
||||
{
|
||||
formatString = fmt;
|
||||
}
|
||||
|
||||
public int Position => formatStringPosition;
|
||||
|
||||
public int Length => formatString?.Length ?? 0;
|
||||
|
||||
public string Substring(int startIndex, int length)
|
||||
{
|
||||
return formatString.Substring(startIndex, length);
|
||||
}
|
||||
|
||||
public int Peek(int offset = 0)
|
||||
{
|
||||
if (formatStringPosition + offset >= Length)
|
||||
return -1;
|
||||
return formatString[formatStringPosition + offset];
|
||||
}
|
||||
|
||||
public int PeekUntil(int startOffset, int until)
|
||||
{
|
||||
int offset = startOffset;
|
||||
while (true)
|
||||
{
|
||||
var c = Peek(offset++);
|
||||
if (c == -1)
|
||||
break;
|
||||
if (c == until)
|
||||
return offset - startOffset;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool PeekOneOf(int offset, string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (Peek(offset) == c)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Advance(int characters = 1)
|
||||
{
|
||||
formatStringPosition = Math.Min(formatStringPosition + characters, formatString.Length);
|
||||
}
|
||||
|
||||
public bool ReadOneOrMore(int c)
|
||||
{
|
||||
if (Peek() != c)
|
||||
return false;
|
||||
|
||||
while (Peek() == c)
|
||||
Advance();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ReadOneOf(string s)
|
||||
{
|
||||
if (PeekOneOf(0, s))
|
||||
{
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ReadString(string s, bool ignoreCase = false)
|
||||
{
|
||||
if (formatStringPosition + s.Length > Length)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
var c1 = s[i];
|
||||
var c2 = (char)Peek(i);
|
||||
if (ignoreCase)
|
||||
{
|
||||
if (char.ToLower(c1) != char.ToLower(c2)) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c1 != c2) return false;
|
||||
}
|
||||
}
|
||||
|
||||
Advance(s.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ReadEnclosed(char open, char close)
|
||||
{
|
||||
if (Peek() == open)
|
||||
{
|
||||
int length = PeekUntil(1, close);
|
||||
if (length > 0)
|
||||
{
|
||||
Advance(1 + length);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/ExcelNumberFormat/icon.png
Normal file
BIN
src/ExcelNumberFormat/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -35,7 +35,7 @@ Todo : https://github.com/shps951023/MiniExcel/projects/1?fullscreen=true
|
||||
<Reference Include="System.IO.Compression" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ExcelNumberFormat" Version="1.1.0" />
|
||||
<ProjectReference Include="..\ExcelNumberFormat\ExcelNumberFormat.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user