├── .gitignore ├── ExcelNumberFormat.sln ├── LICENSE ├── README.md ├── appveyor.yml ├── src ├── Example │ ├── Example.csproj │ └── Program.cs └── ExcelNumberFormat │ ├── AssemblyInfo.cs │ ├── Color.cs │ ├── CompatibleConvert.cs │ ├── Condition.cs │ ├── DecimalSection.cs │ ├── Evaluator.cs │ ├── ExcelDateTime.cs │ ├── ExcelNumberFormat.csproj │ ├── ExcelNumberFormat.snk │ ├── ExponentialSection.cs │ ├── Formatter.cs │ ├── FractionSection.cs │ ├── NumberFormat.cs │ ├── Parser.cs │ ├── Section.cs │ ├── SectionType.cs │ ├── Token.cs │ ├── Tokenizer.cs │ └── icon.png └── test └── ExcelNumberFormat.Tests ├── Class1.cs └── ExcelNumberFormat.Tests.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # MSTest test Results 20 | [Tt]est[Rr]esult*/ 21 | [Bb]uild[Ll]og.* 22 | 23 | #NUNIT 24 | *.VisualState.xml 25 | TestResult.xml 26 | 27 | # Build Results of an ATL Project 28 | [Dd]ebugPS/ 29 | [Rr]eleasePS/ 30 | dlldata.c 31 | 32 | *_i.c 33 | *_p.c 34 | *_i.h 35 | *.ilk 36 | *.meta 37 | *.obj 38 | *.pch 39 | *.pdb 40 | *.pgc 41 | *.pgd 42 | *.rsp 43 | *.sbr 44 | *.tlb 45 | *.tli 46 | *.tlh 47 | *.tmp 48 | *.tmp_proj 49 | *.log 50 | *.vspscc 51 | *.vssscc 52 | .builds 53 | *.pidb 54 | *.svclog 55 | *.scc 56 | *.lock.json 57 | /.vs/ 58 | 59 | # Chutzpah Test files 60 | _Chutzpah* 61 | 62 | # Visual C++ cache files 63 | ipch/ 64 | *.aps 65 | *.ncb 66 | *.opensdf 67 | *.sdf 68 | *.cachefile 69 | 70 | # Visual Studio profiler 71 | *.psess 72 | *.vsp 73 | *.vspx 74 | 75 | # TFS 2012 Local Workspace 76 | $tf/ 77 | 78 | # Guidance Automation Toolkit 79 | *.gpState 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper*/ 83 | *.[Rr]e[Ss]harper 84 | *.DotSettings.user 85 | 86 | # JustCode is a .NET coding addin-in 87 | .JustCode 88 | 89 | # TeamCity is a build add-in 90 | _TeamCity* 91 | 92 | # DotCover is a Code Coverage Tool 93 | *.dotCover 94 | 95 | # NCrunch 96 | *.ncrunch* 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | 127 | # NuGet Packages Directory 128 | packages/ 129 | ## TODO: If the tool you use requires repositories.config uncomment the next line 130 | #!packages/repositories.config 131 | 132 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 133 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) 134 | !packages/build/ 135 | 136 | # Windows Azure Build Output 137 | csx/ 138 | *.build.csdef 139 | 140 | # Windows Store app package directory 141 | AppPackages/ 142 | 143 | # Others 144 | sql/ 145 | *.Cache 146 | ClientBin/ 147 | [Ss]tyle[Cc]op.* 148 | ~$* 149 | *~ 150 | *.dbmdl 151 | *.dbproj.schemaview 152 | *.pfx 153 | *.publishsettings 154 | node_modules/ 155 | 156 | # RIA/Silverlight projects 157 | Generated_Code/ 158 | 159 | # Backup & report files from converting an old project file to a newer 160 | # Visual Studio version. Backup files are not needed, because we have git ;-) 161 | _UpgradeReport_Files/ 162 | Backup*/ 163 | UpgradeLog*.XML 164 | UpgradeLog*.htm 165 | 166 | # SQL Server files 167 | *.mdf 168 | *.ldf 169 | 170 | # Business Intelligence projects 171 | *.rdl.data 172 | *.bim.layout 173 | *.bim_*.settings 174 | 175 | # Microsoft Fakes 176 | FakesAssemblies/ 177 | 178 | -------------------------------------------------------------------------------- /ExcelNumberFormat.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExcelNumberFormat", "src\ExcelNumberFormat\ExcelNumberFormat.csproj", "{0A6915CD-52FB-4410-80B3-0CAB867146FE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExcelNumberFormat.Tests", "test\ExcelNumberFormat.Tests\ExcelNumberFormat.Tests.csproj", "{D7F7A13B-C3FE-44FB-BF07-F5CBC9EC3B78}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "src\Example\Example.csproj", "{220224FB-FFBE-43F9-A70B-3411E583AE00}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {0A6915CD-52FB-4410-80B3-0CAB867146FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {0A6915CD-52FB-4410-80B3-0CAB867146FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {0A6915CD-52FB-4410-80B3-0CAB867146FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {0A6915CD-52FB-4410-80B3-0CAB867146FE}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D7F7A13B-C3FE-44FB-BF07-F5CBC9EC3B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D7F7A13B-C3FE-44FB-BF07-F5CBC9EC3B78}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D7F7A13B-C3FE-44FB-BF07-F5CBC9EC3B78}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D7F7A13B-C3FE-44FB-BF07-F5CBC9EC3B78}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {220224FB-FFBE-43F9-A70B-3411E583AE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {220224FB-FFBE-43F9-A70B-3411E583AE00}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {220224FB-FFBE-43F9-A70B-3411E583AE00}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {220224FB-FFBE-43F9-A70B-3411E583AE00}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 andersnm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExcelNumberFormat 2 | ================= 3 | 4 | .NET library to parse ECMA-376 number format strings and format values like Excel and other spreadsheet softwares. 5 | 6 | [![Build status](https://ci.appveyor.com/api/projects/status/pg23vtba9wjr138f?svg=true)](https://ci.appveyor.com/project/andersnm/excelnumberformat) 7 | 8 | ## Install via NuGet 9 | 10 | If you want to include ExcelNumberFormat in your project, you can [install it directly from NuGet](https://www.nuget.org/packages/ExcelNumberFormat) 11 | 12 | To install ExcelNumberFormat, run the following command in the Package Manager Console 13 | 14 | ``` 15 | PM> Install-Package ExcelNumberFormat 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```C# 21 | var format = new NumberFormat("#.##"); 22 | Console.WriteLine(format.Format(1234.56, CultureInfo.InvariantCulture)); 23 | ``` 24 | 25 | ## Features 26 | 27 | - Parses and formats most custom number formats as expected: decimal, percent, thousands, exponential, fraction, currency, date/time, duration, text. 28 | - Supports multiple sections with conditions. 29 | - Formats values with relevant constants from CultureInfo. 30 | - Supports DateTime, TimeSpan and numeric values for date and duration formats. 31 | - Supports both 1900- and 1904-based numeric datetimes (Excel on Mac uses 1904-based dates). 32 | - Targets net20 and netstandard1.0 for max compatibility. 33 | 34 | ## Formatting .NET types 35 | 36 | The `Format()` method takes a value of type `object` as parameter. Internally, the value is cast or converted to a specific .NET type depending on the kind of number format: 37 | 38 | Format Kind | Example | .NET type|Conversion strategy 39 | -|-|-|- 40 | Number | 0.00 |double|Convert.ToDouble() 41 | Fraction | 0/0 |double|Convert.ToDouble() 42 | Exponent | \#0.0E+0 |double|Convert.ToDouble() 43 | Date/Time| hh\:mm |DateTime|ExcelDateTime.TryConvert() 44 | Duration | \[hh\]\:mm|TimeSpan|Cast or TimeSpan.FromDays() 45 | General | General |(any)|CompatibleConvert.ToString() 46 | Text | ;;;"Text: "@|string|Convert.ToString() 47 | 48 | In case of errors, `Format()` returns the value from `CompatibleConvert.ToString()`. 49 | 50 | `CompatibleConvert.ToString()` formats floats and doubles with explicit precision, or falls back to `Convert.ToString()` for any other types. 51 | `ExcelDateTime.TryConvert()` uses DateTimes as is, or converts numeric values to a DateTime with adjustments for legacy Excel behaviors. 52 | 53 | ## TODO/notes 54 | 55 | - 'General' is formatted with `.ToString()` instead of Excel conventions. 56 | - No errors: Invalid format strings and incompatible input values are formatted with `.ToString()`. 57 | - No color information. 58 | - Variable width space is returned as regular space. 59 | - Repeat-to-fill characters are printed once, not repeated. 60 | - No alignment hinting. 61 | - No date conditions. 62 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: build-{build} 2 | skip_tags: true 3 | image: Visual Studio 2019 4 | configuration: Release 5 | build_script: 6 | - ps: |- 7 | msbuild /t:Restore 8 | msbuild ExcelNumberFormat.sln /verbosity:minimal /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" /p:Configuration=$Env:CONFIGURATION /p:TreatWarningsAsErrors=true 9 | test_script: 10 | - ps: |- 11 | # run tests 12 | dotnet test test\ExcelNumberFormat.Tests\ExcelNumberFormat.Tests.csproj --no-build --logger "trx;LogFileName=tests.trx" -c $Env:CONFIGURATION 13 | # upload results to AppVeyor 14 | $wc = New-Object 'System.Net.WebClient' 15 | $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test\ExcelNumberFormat.Tests\TestResults\tests*.trx)) 16 | artifacts: 17 | - path: '**\*.nupkg' 18 | -------------------------------------------------------------------------------- /src/Example/Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp1.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ExcelNumberFormat; 3 | using System.Globalization; 4 | 5 | namespace Example 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | var format = new NumberFormat("[<=6e+3]# ??/\"a\"[Blue]?\"a\"0\"a\""); 12 | var result = format.Format(0.12, CultureInfo.InvariantCulture); 13 | Console.WriteLine("\"" + result.ToString() + "\""); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly:CLSCompliant(true)] -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Color.cs: -------------------------------------------------------------------------------- 1 | namespace ExcelNumberFormat 2 | { 3 | internal class Color 4 | { 5 | public string Value { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/CompatibleConvert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal static class CompatibleConvert 6 | { 7 | /// 8 | /// A backward-compatible version of . 9 | /// Starting from .net Core 3.0 the default precision used for formatting floating point number has changed. 10 | /// To always format numbers the same way, no matter what version of runtime is used, we specify the precision explicitly. 11 | /// 12 | public static string ToString(object value, IFormatProvider provider) 13 | { 14 | switch (value) 15 | { 16 | case double d: 17 | return d.ToString("G15", provider); 18 | case float f: 19 | return f.ToString("G7", provider); 20 | default: 21 | return Convert.ToString(value, provider); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Condition.cs: -------------------------------------------------------------------------------- 1 | namespace ExcelNumberFormat 2 | { 3 | internal class Condition 4 | { 5 | public string Operator { get; set; } 6 | public double Value { get; set; } 7 | 8 | public bool Evaluate(double lhs) 9 | { 10 | switch (Operator) 11 | { 12 | case "<": 13 | return lhs < Value; 14 | case "<=": 15 | return lhs <= Value; 16 | case ">": 17 | return lhs > Value; 18 | case ">=": 19 | return lhs >= Value; 20 | case "<>": 21 | return lhs != Value; 22 | case "=": 23 | return lhs == Value; 24 | } 25 | 26 | return false; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/DecimalSection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal class DecimalSection 6 | { 7 | public bool ThousandSeparator { get; set; } 8 | 9 | public double ThousandDivisor { get; set; } 10 | 11 | public double PercentMultiplier { get; set; } 12 | 13 | public List BeforeDecimal { get; set; } 14 | 15 | public bool DecimalSeparator { get; set; } 16 | 17 | public List AfterDecimal { get; set; } 18 | 19 | public static bool TryParse(List tokens, out DecimalSection format) 20 | { 21 | if (Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal) == tokens.Count) 22 | { 23 | bool thousandSeparator; 24 | var divisor = GetTrailingCommasDivisor(tokens, out thousandSeparator); 25 | var multiplier = GetPercentMultiplier(tokens); 26 | 27 | format = new DecimalSection() 28 | { 29 | BeforeDecimal = beforeDecimal, 30 | DecimalSeparator = decimalSeparator, 31 | AfterDecimal = afterDecimal, 32 | PercentMultiplier = multiplier, 33 | ThousandDivisor = divisor, 34 | ThousandSeparator = thousandSeparator 35 | }; 36 | 37 | return true; 38 | } 39 | 40 | format = null; 41 | return false; 42 | } 43 | 44 | static double GetPercentMultiplier(List tokens) 45 | { 46 | // If there is a percentage literal in the part list, multiply the result by 100 47 | foreach (var token in tokens) 48 | { 49 | if (token == "%") 50 | return 100; 51 | } 52 | 53 | return 1; 54 | } 55 | 56 | static double GetTrailingCommasDivisor(List tokens, out bool thousandSeparator) 57 | { 58 | // This parses all comma literals in the part list: 59 | // Each comma after the last digit placeholder divides the result by 1000. 60 | // If there are any other commas, display the result with thousand separators. 61 | bool hasLastPlaceholder = false; 62 | var divisor = 1.0; 63 | 64 | for (var j = 0; j < tokens.Count; j++) 65 | { 66 | var tokenIndex = tokens.Count - 1 - j; 67 | var token = tokens[tokenIndex]; 68 | 69 | if (!hasLastPlaceholder) 70 | { 71 | if (Token.IsPlaceholder(token)) 72 | { 73 | // Each trailing comma multiplies the divisor by 1000 74 | for (var k = tokenIndex + 1; k < tokens.Count; k++) 75 | { 76 | token = tokens[k]; 77 | if (token == ",") 78 | divisor *= 1000.0; 79 | else 80 | break; 81 | } 82 | 83 | // Continue scanning backwards from the last digit placeholder, 84 | // but now look for a thousand separator comma 85 | hasLastPlaceholder = true; 86 | } 87 | } 88 | else 89 | { 90 | if (token == ",") 91 | { 92 | thousandSeparator = true; 93 | return divisor; 94 | } 95 | } 96 | } 97 | 98 | thousandSeparator = false; 99 | return divisor; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Evaluator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ExcelNumberFormat 6 | { 7 | internal static class Evaluator 8 | { 9 | public static Section GetSection(List
sections, object value) 10 | { 11 | // Standard format has up to 4 sections: 12 | // Positive;Negative;Zero;Text 13 | switch (value) 14 | { 15 | case string s: 16 | if (sections.Count >= 4) 17 | return sections[3]; 18 | 19 | return null; 20 | 21 | case DateTime dt: 22 | // TODO: Check date conditions need date helpers and Date1904 knowledge 23 | return GetFirstSection(sections, SectionType.Date); 24 | 25 | case TimeSpan ts: 26 | return GetNumericSection(sections, ts.TotalDays); 27 | 28 | case double d: 29 | return GetNumericSection(sections, d); 30 | 31 | case int i: 32 | return GetNumericSection(sections, i); 33 | 34 | case short s: 35 | return GetNumericSection(sections, s); 36 | 37 | default: 38 | return null; 39 | } 40 | } 41 | 42 | public static Section GetFirstSection(List
sections, SectionType type) 43 | { 44 | foreach (var section in sections) 45 | if (section.Type == type) 46 | return section; 47 | return null; 48 | } 49 | 50 | private static Section GetNumericSection(List
sections, double value) 51 | { 52 | // First section applies if 53 | // - Has a condition: 54 | // - There is 1 section, or 55 | // - There are 2 sections, and the value is 0 or positive, or 56 | // - There are >2 sections, and the value is positive 57 | if (sections.Count < 1) 58 | { 59 | return null; 60 | } 61 | 62 | var section0 = sections[0]; 63 | 64 | if (section0.Condition != null) 65 | { 66 | if (section0.Condition.Evaluate(value)) 67 | { 68 | return section0; 69 | } 70 | } 71 | else if (sections.Count == 1 || (sections.Count == 2 && value >= 0) || (sections.Count >= 2 && value > 0)) 72 | { 73 | return section0; 74 | } 75 | 76 | if (sections.Count < 2) 77 | { 78 | return null; 79 | } 80 | 81 | var section1 = sections[1]; 82 | 83 | // First condition didnt match, or was a negative number. Second condition applies if: 84 | // - Has a condition, or 85 | // - Value is negative, or 86 | // - There are two sections, and the first section had a non-matching condition 87 | if (section1.Condition != null) 88 | { 89 | if (section1.Condition.Evaluate(value)) 90 | { 91 | return section1; 92 | } 93 | } 94 | else if (value < 0 || (sections.Count == 2 && section0.Condition != null)) 95 | { 96 | return section1; 97 | } 98 | 99 | // Second condition didnt match, or was positive. The following 100 | // sections cannot have conditions, always fall back to the third 101 | // section (for zero formatting) if specified. 102 | if (sections.Count < 3) 103 | { 104 | return null; 105 | } 106 | 107 | return sections[2]; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/ExcelDateTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace ExcelNumberFormat 5 | { 6 | /// 7 | /// Similar to regular .NET DateTime, but also supports 0/1 1900 and 29/2 1900. 8 | /// 9 | internal class ExcelDateTime 10 | { 11 | /// 12 | /// The closest .NET DateTime to the specified excel date. 13 | /// 14 | public DateTime AdjustedDateTime { get; } 15 | 16 | /// 17 | /// Number of days to adjust by in post. 18 | /// 19 | public int AdjustDaysPost { get; } 20 | 21 | /// 22 | /// Constructs a new ExcelDateTime from a numeric value. 23 | /// 24 | public ExcelDateTime(double numericDate, bool isDate1904) 25 | { 26 | if (isDate1904) 27 | { 28 | numericDate += 1462.0; 29 | AdjustedDateTime = new DateTime(DoubleDateToTicks(numericDate), DateTimeKind.Unspecified); 30 | } 31 | else 32 | { 33 | // internal dates before 30/12/1899 should add two days to get the real date 34 | // internal dates on 30/12 19899 should add two days, but subtract a day post to get the real date 35 | // internal dates before 28/2/1900 should add one day to get the real date 36 | // internal dates on 28/2 1900 should use the same date, but add a day post to get the real date 37 | 38 | var internalDateTime = new DateTime(DoubleDateToTicks(numericDate), DateTimeKind.Unspecified); 39 | if (internalDateTime < Excel1900ZeroethMinDate) 40 | { 41 | AdjustDaysPost = 0; 42 | AdjustedDateTime = internalDateTime.AddDays(2); 43 | } 44 | 45 | else if (internalDateTime < Excel1900ZeroethMaxDate) 46 | { 47 | AdjustDaysPost = -1; 48 | AdjustedDateTime = internalDateTime.AddDays(2); 49 | } 50 | 51 | else if (internalDateTime < Excel1900LeapMinDate) 52 | { 53 | AdjustDaysPost = 0; 54 | AdjustedDateTime = internalDateTime.AddDays(1); 55 | } 56 | 57 | else if (internalDateTime < Excel1900LeapMaxDate) 58 | { 59 | AdjustDaysPost = 1; 60 | AdjustedDateTime = internalDateTime; 61 | } 62 | else 63 | { 64 | AdjustDaysPost = 0; 65 | AdjustedDateTime = internalDateTime; 66 | } 67 | } 68 | } 69 | 70 | static DateTime Excel1900LeapMinDate = new DateTime(1900, 2, 28); 71 | static DateTime Excel1900LeapMaxDate = new DateTime(1900, 3, 1); 72 | static DateTime Excel1900ZeroethMinDate = new DateTime(1899, 12, 30); 73 | static DateTime Excel1900ZeroethMaxDate = new DateTime(1899, 12, 31); 74 | 75 | /// 76 | /// Wraps a regular .NET datetime. 77 | /// 78 | /// 79 | public ExcelDateTime(DateTime value) 80 | { 81 | AdjustedDateTime = value; 82 | AdjustDaysPost = 0; 83 | } 84 | 85 | public int Year => AdjustedDateTime.Year; 86 | 87 | public int Month => AdjustedDateTime.Month; 88 | 89 | public int Day => AdjustedDateTime.Day + AdjustDaysPost; 90 | 91 | public int Hour => AdjustedDateTime.Hour; 92 | 93 | public int Minute => AdjustedDateTime.Minute; 94 | 95 | public int Second => AdjustedDateTime.Second; 96 | 97 | public int Millisecond => AdjustedDateTime.Millisecond; 98 | 99 | public DayOfWeek DayOfWeek => AdjustedDateTime.DayOfWeek; 100 | 101 | public string ToString(string numberFormat, CultureInfo culture) 102 | { 103 | return AdjustedDateTime.ToString(numberFormat, culture); 104 | } 105 | 106 | public static bool TryConvert(object value, bool isDate1904, CultureInfo culture, out ExcelDateTime result) 107 | { 108 | if (value is double doubleValue) 109 | { 110 | result = new ExcelDateTime(doubleValue, isDate1904); 111 | return true; 112 | } 113 | if (value is int intValue) 114 | { 115 | result = new ExcelDateTime(intValue, isDate1904); 116 | return true; 117 | } 118 | if (value is short shortValue) 119 | { 120 | result = new ExcelDateTime(shortValue, isDate1904); 121 | return true; 122 | } 123 | else if (value is DateTime dateTimeValue) 124 | { 125 | result = new ExcelDateTime(dateTimeValue); 126 | return true; 127 | } 128 | 129 | result = null; 130 | return false; 131 | } 132 | 133 | // From DateTime class to enable OADate in PCL 134 | // Number of 100ns ticks per time unit 135 | private const long TicksPerMillisecond = 10000; 136 | private const long TicksPerSecond = TicksPerMillisecond * 1000; 137 | private const long TicksPerMinute = TicksPerSecond * 60; 138 | private const long TicksPerHour = TicksPerMinute * 60; 139 | private const long TicksPerDay = TicksPerHour * 24; 140 | 141 | private const int MillisPerSecond = 1000; 142 | private const int MillisPerMinute = MillisPerSecond * 60; 143 | private const int MillisPerHour = MillisPerMinute * 60; 144 | private const int MillisPerDay = MillisPerHour * 24; 145 | 146 | // Number of days in a non-leap year 147 | private const int DaysPerYear = 365; 148 | 149 | // Number of days in 4 years 150 | private const int DaysPer4Years = DaysPerYear * 4 + 1; 151 | 152 | // Number of days in 100 years 153 | private const int DaysPer100Years = DaysPer4Years * 25 - 1; 154 | 155 | // Number of days in 400 years 156 | private const int DaysPer400Years = DaysPer100Years * 4 + 1; 157 | 158 | // Number of days from 1/1/0001 to 12/30/1899 159 | private const int DaysTo1899 = DaysPer400Years * 4 + DaysPer100Years * 3 - 367; 160 | 161 | private const long DoubleDateOffset = DaysTo1899 * TicksPerDay; 162 | 163 | internal static long DoubleDateToTicks(double value) 164 | { 165 | long millis = (long)(value * MillisPerDay + (value >= 0 ? 0.5 : -0.5)); 166 | 167 | // 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 168 | // 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 169 | // This line below fixes up the millis in the negative case 170 | if (millis < 0) 171 | { 172 | millis -= millis % MillisPerDay * 2; 173 | } 174 | 175 | millis += DoubleDateOffset / TicksPerMillisecond; 176 | return millis * TicksPerMillisecond; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/ExcelNumberFormat.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net20;netstandard1.0;netstandard2.0 5 | 1.1.0 6 | true 7 | true 8 | .NET library to parse ECMA-376 number format strings and format values like Excel and other spreadsheet softwares. 9 | ExcelNumberFormat developers 10 | excel,formatting,numfmt,formatcode 11 | https://github.com/andersnm/ExcelNumberFormat 12 | icon.png 13 | ExcelNumberFormat.snk 14 | true 15 | true 16 | MIT 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/ExcelNumberFormat.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersnm/ExcelNumberFormat/38c6a71919ad3895e3ba61d7a888012670d26303/src/ExcelNumberFormat/ExcelNumberFormat.snk -------------------------------------------------------------------------------- /src/ExcelNumberFormat/ExponentialSection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal class ExponentialSection 6 | { 7 | public List BeforeDecimal { get; set; } 8 | 9 | public bool DecimalSeparator { get; set; } 10 | 11 | public List AfterDecimal { get; set; } 12 | 13 | public string ExponentialToken { get; set; } 14 | 15 | public List Power { get; set; } 16 | 17 | public static bool TryParse(List tokens, out ExponentialSection format) 18 | { 19 | format = null; 20 | 21 | string exponentialToken; 22 | 23 | int partCount = Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal); 24 | 25 | if (partCount == 0) 26 | return false; 27 | 28 | int position = partCount; 29 | if (position < tokens.Count && Token.IsExponent(tokens[position])) 30 | { 31 | exponentialToken = tokens[position]; 32 | position++; 33 | } 34 | else 35 | { 36 | return false; 37 | } 38 | 39 | format = new ExponentialSection() 40 | { 41 | BeforeDecimal = beforeDecimal, 42 | DecimalSeparator = decimalSeparator, 43 | AfterDecimal = afterDecimal, 44 | ExponentialToken = exponentialToken, 45 | Power = tokens.GetRange(position, tokens.Count - position) 46 | }; 47 | 48 | return true; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Formatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | 6 | namespace ExcelNumberFormat 7 | { 8 | static internal class Formatter 9 | { 10 | static public string Format(object value, string formatString, CultureInfo culture, bool isDate1904) 11 | { 12 | var format = new NumberFormat(formatString); 13 | if (!format.IsValid) 14 | return CompatibleConvert.ToString(value, culture); 15 | 16 | var section = Evaluator.GetSection(format.Sections, value); 17 | if (section == null) 18 | return CompatibleConvert.ToString(value, culture); 19 | 20 | return Format(value, section, culture, isDate1904); 21 | } 22 | 23 | static public string Format(object value, Section node, CultureInfo culture, bool isDate1904) 24 | { 25 | switch (node.Type) 26 | { 27 | case SectionType.Number: 28 | // Hide sign under certain conditions and section index 29 | var number = Convert.ToDouble(value, culture); 30 | if ((node.SectionIndex == 0 && node.Condition != null) || node.SectionIndex == 1) 31 | number = Math.Abs(number); 32 | 33 | return FormatNumber(number, node.Number, culture); 34 | 35 | case SectionType.Date: 36 | if (ExcelDateTime.TryConvert(value, isDate1904, culture, out var excelDateTime)) 37 | { 38 | return FormatDate(excelDateTime, node.GeneralTextDateDurationParts, culture); 39 | } 40 | else 41 | { 42 | throw new FormatException("Unexpected date value"); 43 | } 44 | 45 | case SectionType.Duration: 46 | if (value is TimeSpan ts) 47 | { 48 | return FormatTimeSpan(ts, node.GeneralTextDateDurationParts, culture); 49 | } 50 | else 51 | { 52 | var d = Convert.ToDouble(value); 53 | return FormatTimeSpan(TimeSpan.FromDays(d), node.GeneralTextDateDurationParts, culture); 54 | } 55 | 56 | case SectionType.General: 57 | case SectionType.Text: 58 | return FormatGeneralText(CompatibleConvert.ToString(value, culture), node.GeneralTextDateDurationParts); 59 | 60 | case SectionType.Exponential: 61 | return FormatExponential(Convert.ToDouble(value, culture), node, culture); 62 | 63 | case SectionType.Fraction: 64 | return FormatFraction(Convert.ToDouble(value, culture), node, culture); 65 | 66 | default: 67 | throw new InvalidOperationException("Unknown number format section"); 68 | } 69 | } 70 | 71 | static string FormatGeneralText(string text, List tokens) 72 | { 73 | var result = new StringBuilder(); 74 | for (var i = 0; i < tokens.Count; i++) 75 | { 76 | var token = tokens[i]; 77 | if (Token.IsGeneral(token) || token == "@") 78 | { 79 | result.Append(text); 80 | } 81 | else 82 | { 83 | FormatLiteral(token, result); 84 | } 85 | } 86 | return result.ToString(); 87 | } 88 | 89 | private static string FormatTimeSpan(TimeSpan timeSpan, List tokens, CultureInfo culture) 90 | { 91 | // NOTE/TODO: assumes there is exactly one [hh], [mm] or [ss] using the integer part of TimeSpan.TotalXXX when formatting. 92 | // The timeSpan input is then truncated to the remainder fraction, which is used to format mm and/or ss. 93 | var result = new StringBuilder(); 94 | var containsMilliseconds = false; 95 | for (var i = tokens.Count - 1; i >= 0; i--) 96 | { 97 | if (tokens[i].StartsWith(".0")) 98 | { 99 | containsMilliseconds = true; 100 | break; 101 | } 102 | } 103 | 104 | for (var i = 0; i < tokens.Count; i++) 105 | { 106 | var token = tokens[i]; 107 | 108 | if (token.StartsWith("m", StringComparison.OrdinalIgnoreCase)) 109 | { 110 | var value = timeSpan.Minutes; 111 | var digits = token.Length; 112 | result.Append(value.ToString("D" + digits)); 113 | } 114 | else if (token.StartsWith("s", StringComparison.OrdinalIgnoreCase)) 115 | { 116 | // If format does not include ms, then include ms in seconds and round before printing 117 | var formatMs = containsMilliseconds ? 0 : timeSpan.Milliseconds / 1000D; 118 | var value = (int)Math.Round(timeSpan.Seconds + formatMs, 0, MidpointRounding.AwayFromZero); 119 | var digits = token.Length; 120 | result.Append(value.ToString("D" + digits)); 121 | } 122 | else if (token.StartsWith("[h", StringComparison.OrdinalIgnoreCase)) 123 | { 124 | var value = (int)timeSpan.TotalHours; 125 | var digits = token.Length - 2; 126 | result.Append(value.ToString("D" + digits)); 127 | timeSpan = new TimeSpan(0, 0, Math.Abs(timeSpan.Minutes), Math.Abs(timeSpan.Seconds), Math.Abs(timeSpan.Milliseconds)); 128 | } 129 | else if (token.StartsWith("[m", StringComparison.OrdinalIgnoreCase)) 130 | { 131 | var value = (int)timeSpan.TotalMinutes; 132 | var digits = token.Length - 2; 133 | result.Append(value.ToString("D" + digits)); 134 | timeSpan = new TimeSpan(0, 0, 0, Math.Abs(timeSpan.Seconds), Math.Abs(timeSpan.Milliseconds)); 135 | } 136 | else if (token.StartsWith("[s", StringComparison.OrdinalIgnoreCase)) 137 | { 138 | var value = (int)timeSpan.TotalSeconds; 139 | var digits = token.Length - 2; 140 | result.Append(value.ToString("D" + digits)); 141 | timeSpan = new TimeSpan(0, 0, 0, 0, Math.Abs(timeSpan.Milliseconds)); 142 | } 143 | else if (token.StartsWith(".0")) { 144 | var value = timeSpan.Milliseconds; 145 | var digits = token.Length - 1; 146 | result.Append("." + value.ToString("D" + digits)); 147 | } 148 | else 149 | { 150 | FormatLiteral(token, result); 151 | } 152 | } 153 | 154 | return result.ToString(); 155 | } 156 | 157 | private static string FormatDate(ExcelDateTime date, List tokens, CultureInfo culture) 158 | { 159 | var containsAmPm = ContainsAmPm(tokens); 160 | 161 | var result = new StringBuilder(); 162 | for (var i = 0; i < tokens.Count; i++) 163 | { 164 | var token = tokens[i]; 165 | 166 | if (token.StartsWith("y", StringComparison.OrdinalIgnoreCase)) 167 | { 168 | // year 169 | var digits = token.Length; 170 | if (digits < 2) 171 | digits = 2; 172 | if (digits == 3) 173 | digits = 4; 174 | 175 | var year = date.Year; 176 | if (digits == 2) 177 | year = year % 100; 178 | 179 | result.Append(year.ToString("D" + digits)); 180 | } 181 | else if (token.StartsWith("m", StringComparison.OrdinalIgnoreCase)) 182 | { 183 | // If "m" or "mm" code is used immediately after the "h" or "hh" code (for hours) or immediately before 184 | // the "ss" code (for seconds), the application shall display minutes instead of the month. 185 | if (LookBackDatePart(tokens, i - 1, "h") || LookAheadDatePart(tokens, i + 1, "s")) 186 | { 187 | var digits = token.Length; 188 | result.Append(date.Minute.ToString("D" + digits)); 189 | } 190 | else 191 | { 192 | var digits = token.Length; 193 | if (digits == 3) 194 | { 195 | result.Append(culture.DateTimeFormat.AbbreviatedMonthNames[date.Month - 1]); 196 | } 197 | else if (digits == 4) 198 | { 199 | result.Append(culture.DateTimeFormat.MonthNames[date.Month - 1]); 200 | } 201 | else if (digits == 5) 202 | { 203 | result.Append(culture.DateTimeFormat.MonthNames[date.Month - 1][0]); 204 | } 205 | else 206 | { 207 | result.Append(date.Month.ToString("D" + digits)); 208 | } 209 | } 210 | } 211 | else if (token.StartsWith("d", StringComparison.OrdinalIgnoreCase)) 212 | { 213 | var digits = token.Length; 214 | if (digits == 3) 215 | { 216 | // Sun-Sat 217 | result.Append(culture.DateTimeFormat.AbbreviatedDayNames[(int)date.DayOfWeek]); 218 | } 219 | else if (digits == 4) 220 | { 221 | // Sunday-Saturday 222 | result.Append(culture.DateTimeFormat.DayNames[(int)date.DayOfWeek]); 223 | } 224 | else 225 | { 226 | result.Append(date.Day.ToString("D" + digits)); 227 | } 228 | } 229 | else if (token.StartsWith("h", StringComparison.OrdinalIgnoreCase)) 230 | { 231 | var digits = token.Length; 232 | if (containsAmPm) 233 | result.Append(((date.Hour + 11) % 12 + 1).ToString("D" + digits)); 234 | else 235 | result.Append(date.Hour.ToString("D" + digits)); 236 | } 237 | else if (token.StartsWith("s", StringComparison.OrdinalIgnoreCase)) 238 | { 239 | var digits = token.Length; 240 | result.Append(date.Second.ToString("D" + digits)); 241 | } 242 | else if (token.StartsWith("g", StringComparison.OrdinalIgnoreCase)) 243 | { 244 | var era = culture.DateTimeFormat.Calendar.GetEra(date.AdjustedDateTime); 245 | var digits = token.Length; 246 | if (digits < 3) 247 | { 248 | result.Append(culture.DateTimeFormat.GetAbbreviatedEraName(era)); 249 | } 250 | else 251 | { 252 | result.Append(culture.DateTimeFormat.GetEraName(era)); 253 | } 254 | } 255 | else if (string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0) 256 | { 257 | var ampm = date.ToString("tt", CultureInfo.InvariantCulture); 258 | result.Append(ampm.ToUpperInvariant()); 259 | } 260 | else if (string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0) 261 | { 262 | var ampm = date.ToString("%t", CultureInfo.InvariantCulture); 263 | if (char.IsUpper(token[0])) 264 | { 265 | result.Append(ampm.ToUpperInvariant()); 266 | } 267 | else 268 | { 269 | result.Append(ampm.ToLowerInvariant()); 270 | } 271 | } 272 | else if (token.StartsWith(".0")) 273 | { 274 | var value = date.Millisecond; 275 | var digits = token.Length - 1; 276 | result.Append("." + value.ToString("D" + digits)); 277 | } 278 | else if (token == "/") 279 | { 280 | #if NETSTANDARD1_0 281 | result.Append(DateTime.MaxValue.ToString("/d", culture)[0]); 282 | #else 283 | result.Append(culture.DateTimeFormat.DateSeparator); 284 | #endif 285 | } 286 | else if (token == ",") 287 | { 288 | while (i < tokens.Count - 1 && tokens[i + 1] == ",") 289 | { 290 | i++; 291 | } 292 | 293 | result.Append(","); 294 | } 295 | else 296 | { 297 | FormatLiteral(token, result); 298 | } 299 | } 300 | 301 | return result.ToString(); 302 | } 303 | 304 | private static bool LookAheadDatePart(List tokens, int fromIndex, string startsWith) 305 | { 306 | for (var i = fromIndex; i < tokens.Count; i++) 307 | { 308 | var token = tokens[i]; 309 | if (token.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) 310 | return true; 311 | if (Token.IsDatePart(token)) 312 | return false; 313 | } 314 | 315 | return false; 316 | } 317 | 318 | private static bool LookBackDatePart(List tokens, int fromIndex, string startsWith) 319 | { 320 | for (var i = fromIndex; i >= 0; i--) 321 | { 322 | var token = tokens[i]; 323 | if (token.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) 324 | return true; 325 | if (Token.IsDatePart(token)) 326 | return false; 327 | } 328 | 329 | return false; 330 | } 331 | 332 | private static bool ContainsAmPm(List tokens) 333 | { 334 | foreach (var token in tokens) 335 | { 336 | if (string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0) 337 | { 338 | return true; 339 | } 340 | 341 | if (string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0) 342 | { 343 | return true; 344 | } 345 | } 346 | 347 | return false; 348 | } 349 | 350 | static string FormatNumber(double value, DecimalSection format, CultureInfo culture) 351 | { 352 | bool thousandSeparator = format.ThousandSeparator; 353 | value = value / format.ThousandDivisor; 354 | value = value * format.PercentMultiplier; 355 | 356 | var result = new StringBuilder(); 357 | FormatNumber(value, format.BeforeDecimal, format.DecimalSeparator, format.AfterDecimal, thousandSeparator, culture, result); 358 | return result.ToString(); 359 | } 360 | 361 | static void FormatNumber(double value, List beforeDecimal, bool decimalSeparator, List afterDecimal, bool thousandSeparator, CultureInfo culture, StringBuilder result) 362 | { 363 | int signitificantDigits = 0; 364 | if (afterDecimal != null) 365 | signitificantDigits = GetDigitCount(afterDecimal); 366 | 367 | var valueString = Math.Abs(value).ToString("F" + signitificantDigits, CultureInfo.InvariantCulture); 368 | var valueStrings = valueString.Split('.'); 369 | var thousandsString = valueStrings[0]; 370 | var decimalString = valueStrings.Length > 1 ? valueStrings[1].TrimEnd('0') : ""; 371 | 372 | if (value < 0) 373 | { 374 | result.Append("-"); 375 | } 376 | 377 | if (beforeDecimal != null) 378 | { 379 | FormatThousands(thousandsString, thousandSeparator, false, beforeDecimal, culture, result); 380 | } 381 | 382 | if (decimalSeparator) { 383 | result.Append(culture.NumberFormat.NumberDecimalSeparator); 384 | } 385 | 386 | if (afterDecimal != null) 387 | { 388 | FormatDecimals(decimalString, afterDecimal, result); 389 | } 390 | } 391 | 392 | /// 393 | /// Prints right-aligned, left-padded integer before the decimal separator. With optional most-significant zero. 394 | /// 395 | public static void FormatThousands(string valueString, bool thousandSeparator, bool significantZero, List tokens, CultureInfo culture, StringBuilder result) 396 | { 397 | var significant = false; 398 | var formatDigits = GetDigitCount(tokens); 399 | valueString = valueString.PadLeft(formatDigits, '0'); 400 | 401 | // Print literals occurring before any placeholders 402 | var tokenIndex = 0; 403 | for (; tokenIndex < tokens.Count; tokenIndex++) 404 | { 405 | var token = tokens[tokenIndex]; 406 | if (Token.IsPlaceholder(token)) 407 | break; 408 | else 409 | FormatLiteral(token, result); 410 | } 411 | 412 | // Print value digits until there are as many digits remaining as there are placeholders 413 | var digitIndex = 0; 414 | for (; digitIndex < (valueString.Length - formatDigits); digitIndex++) 415 | { 416 | significant = true; 417 | result.Append(valueString[digitIndex]); 418 | 419 | if (thousandSeparator) 420 | FormatThousandSeparator(valueString, digitIndex, culture, result); 421 | } 422 | 423 | // Print remaining value digits and format literals 424 | for (; tokenIndex < tokens.Count; ++tokenIndex) 425 | { 426 | var token = tokens[tokenIndex]; 427 | if (Token.IsPlaceholder(token)) 428 | { 429 | var c = valueString[digitIndex]; 430 | if (c != '0' || (significantZero && digitIndex == valueString.Length - 1)) significant = true; 431 | 432 | FormatPlaceholder(token, c, significant, result); 433 | 434 | if (thousandSeparator && (significant || token.Equals("0"))) 435 | FormatThousandSeparator(valueString, digitIndex, culture, result); 436 | 437 | digitIndex++; 438 | } 439 | else 440 | { 441 | FormatLiteral(token, result); 442 | } 443 | } 444 | } 445 | 446 | static void FormatThousandSeparator(string valueString, int digit, CultureInfo culture, StringBuilder result) 447 | { 448 | var positionInTens = valueString.Length - 1 - digit; 449 | if (positionInTens > 0 && (positionInTens % 3) == 0) 450 | { 451 | result.Append(culture.NumberFormat.NumberGroupSeparator); 452 | } 453 | } 454 | 455 | /// 456 | /// Prints left-aligned, right-padded integer after the decimal separator. Does not print significant zero. 457 | /// 458 | public static void FormatDecimals(string valueString, List tokens, StringBuilder result) 459 | { 460 | var significant = true; 461 | var unpaddedDigits = valueString.Length; 462 | var formatDigits = GetDigitCount(tokens); 463 | 464 | valueString = valueString.PadRight(formatDigits, '0'); 465 | 466 | // Print all format digits 467 | var valueIndex = 0; 468 | for (var tokenIndex = 0; tokenIndex < tokens.Count; ++tokenIndex) 469 | { 470 | var token = tokens[tokenIndex]; 471 | if (Token.IsPlaceholder(token)) 472 | { 473 | var c = valueString[valueIndex]; 474 | significant = valueIndex < unpaddedDigits; 475 | 476 | FormatPlaceholder(token, c, significant, result); 477 | valueIndex++; 478 | } 479 | else 480 | { 481 | FormatLiteral(token, result); 482 | } 483 | } 484 | } 485 | 486 | static string FormatExponential(double value, Section format, CultureInfo culture) 487 | { 488 | // The application shall display a number to the right of 489 | // the "E" symbol that corresponds to the number of places that 490 | // the decimal point was moved. 491 | 492 | var baseDigits = 0; 493 | if (format.Exponential.BeforeDecimal != null) 494 | { 495 | baseDigits = GetDigitCount(format.Exponential.BeforeDecimal); 496 | } 497 | 498 | var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))); 499 | var mantissa = value / Math.Pow(10, exponent); 500 | 501 | var shift = Math.Abs(exponent) % baseDigits; 502 | if (shift > 0) 503 | { 504 | if (exponent < 0) 505 | shift = (baseDigits - shift); 506 | 507 | mantissa *= Math.Pow(10, shift); 508 | exponent -= shift; 509 | } 510 | 511 | var result = new StringBuilder(); 512 | FormatNumber(mantissa, format.Exponential.BeforeDecimal, format.Exponential.DecimalSeparator, format.Exponential.AfterDecimal, false, culture, result); 513 | 514 | result.Append(format.Exponential.ExponentialToken[0]); 515 | 516 | if (format.Exponential.ExponentialToken[1] == '+' && exponent >= 0) 517 | { 518 | result.Append("+"); 519 | } 520 | else if (exponent < 0) 521 | { 522 | result.Append("-"); 523 | } 524 | 525 | FormatThousands(Math.Abs(exponent).ToString(CultureInfo.InvariantCulture), false, false, format.Exponential.Power, culture, result); 526 | return result.ToString(); 527 | } 528 | 529 | static string FormatFraction(double value, Section format, CultureInfo culture) 530 | { 531 | int integral = 0; 532 | int numerator, denominator; 533 | 534 | bool sign = value < 0; 535 | 536 | if (format.Fraction.IntegerPart != null) 537 | { 538 | integral = (int)Math.Truncate(value); 539 | value = Math.Abs(value - integral); 540 | } 541 | 542 | if (format.Fraction.DenominatorConstant != 0) 543 | { 544 | denominator = format.Fraction.DenominatorConstant; 545 | var rr = Math.Round(value * denominator); 546 | var b = Math.Floor(rr / denominator); 547 | numerator = (int)(rr - b * denominator); 548 | } 549 | else 550 | { 551 | var denominatorDigits = Math.Min(GetDigitCount(format.Fraction.Denominator), 7); 552 | GetFraction(value, (int)Math.Pow(10, denominatorDigits) - 1, out numerator, out denominator); 553 | } 554 | 555 | // Don't hide fraction if at least one zero in the numerator format 556 | var numeratorZeros = GetZeroCount(format.Fraction.Numerator); 557 | var hideFraction = (format.Fraction.IntegerPart != null && numerator == 0 && numeratorZeros == 0); 558 | 559 | var result = new StringBuilder(); 560 | 561 | if (sign) 562 | result.Append("-"); 563 | 564 | // Print integer part with significant zero if fraction part is hidden 565 | if (format.Fraction.IntegerPart != null) 566 | FormatThousands(Math.Abs(integral).ToString("F0", CultureInfo.InvariantCulture), false, hideFraction, format.Fraction.IntegerPart, culture, result); 567 | 568 | var numeratorString = Math.Abs(numerator).ToString("F0", CultureInfo.InvariantCulture); 569 | var denominatorString = denominator.ToString("F0", CultureInfo.InvariantCulture); 570 | 571 | var fraction = new StringBuilder(); 572 | 573 | FormatThousands(numeratorString, false, true, format.Fraction.Numerator, culture, fraction); 574 | 575 | fraction.Append("/"); 576 | 577 | if (format.Fraction.DenominatorPrefix != null) 578 | FormatThousands("", false, false, format.Fraction.DenominatorPrefix, culture, fraction); 579 | 580 | if (format.Fraction.DenominatorConstant != 0) 581 | fraction.Append(format.Fraction.DenominatorConstant.ToString()); 582 | else 583 | FormatDenominator(denominatorString, format.Fraction.Denominator, fraction); 584 | 585 | if (format.Fraction.DenominatorSuffix != null) 586 | FormatThousands("", false, false, format.Fraction.DenominatorSuffix, culture, fraction); 587 | 588 | if (hideFraction) 589 | result.Append(new string(' ', fraction.ToString().Length)); 590 | else 591 | result.Append(fraction.ToString()); 592 | 593 | if (format.Fraction.FractionSuffix != null) 594 | FormatThousands("", false, false, format.Fraction.FractionSuffix, culture, result); 595 | 596 | return result.ToString(); 597 | } 598 | 599 | // Adapted from ssf.js 'frac()' helper 600 | static void GetFraction(double x, int D, out int nom, out int den) 601 | { 602 | var sgn = x < 0 ? -1 : 1; 603 | var B = x * sgn; 604 | var P_2 = 0.0; 605 | var P_1 = 1.0; 606 | var P = 0.0; 607 | var Q_2 = 1.0; 608 | var Q_1 = 0.0; 609 | var Q = 0.0; 610 | var A = Math.Floor(B); 611 | while (Q_1 < D) 612 | { 613 | A = Math.Floor(B); 614 | P = A * P_1 + P_2; 615 | Q = A * Q_1 + Q_2; 616 | if ((B - A) < 0.00000005) break; 617 | B = 1 / (B - A); 618 | P_2 = P_1; P_1 = P; 619 | Q_2 = Q_1; Q_1 = Q; 620 | } 621 | if (Q > D) { if (Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } } 622 | nom = (int)(sgn * P); 623 | den = (int)Q; 624 | } 625 | 626 | /// 627 | /// Prints left-aligned, left-padded fraction integer denominator. 628 | /// Assumes tokens contain only placeholders, valueString has fewer or equal number of digits as tokens. 629 | /// 630 | public static void FormatDenominator(string valueString, List tokens, StringBuilder result) 631 | { 632 | var formatDigits = GetDigitCount(tokens); 633 | valueString = valueString.PadLeft(formatDigits, '0'); 634 | 635 | bool significant = false; 636 | var valueIndex = 0; 637 | for (var tokenIndex = 0; tokenIndex < tokens.Count; ++tokenIndex) 638 | { 639 | var token = tokens[tokenIndex]; 640 | char c; 641 | if (valueIndex < valueString.Length) { 642 | c = GetLeftAlignedValueDigit(token, valueString, valueIndex, significant, out valueIndex); 643 | 644 | if (c != '0') 645 | significant = true; 646 | } else { 647 | c = '0'; 648 | significant = false; 649 | } 650 | 651 | FormatPlaceholder(token, c, significant, result); 652 | } 653 | } 654 | 655 | /// 656 | /// Returns the first digit from valueString. If the token is '?' 657 | /// returns the first significant digit from valueString, or '0' if there are no significant digits. 658 | /// The out valueIndex parameter contains the offset to the next digit in valueString. 659 | /// 660 | static char GetLeftAlignedValueDigit(string token, string valueString, int startIndex, bool significant, out int valueIndex) 661 | { 662 | char c; 663 | valueIndex = startIndex; 664 | if (valueIndex < valueString.Length) 665 | { 666 | c = valueString[valueIndex]; 667 | valueIndex++; 668 | 669 | if (c != '0') 670 | significant = true; 671 | 672 | if (token == "?" && !significant) 673 | { 674 | // Eat insignificant zeros to left align denominator 675 | while (valueIndex < valueString.Length) 676 | { 677 | c = valueString[valueIndex]; 678 | valueIndex++; 679 | 680 | if (c != '0') 681 | { 682 | significant = true; 683 | break; 684 | } 685 | } 686 | } 687 | } 688 | else 689 | { 690 | c = '0'; 691 | significant = false; 692 | } 693 | 694 | return c; 695 | } 696 | 697 | static void FormatPlaceholder(string token, char c, bool significant, StringBuilder result) 698 | { 699 | if (token == "0") 700 | { 701 | if (significant) 702 | result.Append(c); 703 | else 704 | result.Append("0"); 705 | } 706 | else if (token == "#") 707 | { 708 | if (significant) 709 | result.Append(c); 710 | } 711 | else if (token == "?") 712 | { 713 | if (significant) 714 | result.Append(c); 715 | else 716 | result.Append(" "); 717 | } 718 | } 719 | 720 | static int GetDigitCount(List tokens) 721 | { 722 | var counter = 0; 723 | foreach (var token in tokens) 724 | { 725 | if (Token.IsPlaceholder(token)) 726 | { 727 | counter++; 728 | } 729 | } 730 | return counter; 731 | } 732 | 733 | static int GetZeroCount(List tokens) 734 | { 735 | var counter = 0; 736 | foreach (var token in tokens) 737 | { 738 | if (token == "0") 739 | { 740 | counter++; 741 | } 742 | } 743 | return counter; 744 | } 745 | 746 | static void FormatLiteral(string token, StringBuilder result) 747 | { 748 | string literal = string.Empty; 749 | if (token == ",") 750 | { 751 | ; // skip commas 752 | } 753 | else if (token.Length == 2 && (token[0] == '*' || token[0] == '\\')) 754 | { 755 | // TODO: * = repeat to fill cell 756 | literal = token[1].ToString(); 757 | } 758 | else if (token.Length == 2 && token[0] == '_') 759 | { 760 | literal = " "; 761 | } 762 | else if (token.StartsWith("\"")) 763 | { 764 | literal = token.Substring(1, token.Length - 2); 765 | } 766 | else 767 | { 768 | literal = token; 769 | } 770 | result.Append(literal); 771 | } 772 | } 773 | } 774 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/FractionSection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace ExcelNumberFormat 5 | { 6 | internal class FractionSection 7 | { 8 | public List IntegerPart { get; set; } 9 | 10 | public List Numerator { get; set; } 11 | 12 | public List DenominatorPrefix { get; set; } 13 | 14 | public List Denominator { get; set; } 15 | 16 | public int DenominatorConstant { get; set; } 17 | 18 | public List DenominatorSuffix { get; set; } 19 | 20 | public List FractionSuffix { get; set; } 21 | 22 | static public bool TryParse(List tokens, out FractionSection format) 23 | { 24 | List numeratorParts = null; 25 | List denominatorParts = null; 26 | 27 | for (var i = 0; i < tokens.Count; i++) 28 | { 29 | var part = tokens[i]; 30 | if (part == "/") 31 | { 32 | numeratorParts = tokens.GetRange(0, i); 33 | i++; 34 | denominatorParts = tokens.GetRange(i, tokens.Count - i); 35 | break; 36 | } 37 | } 38 | 39 | if (numeratorParts == null) 40 | { 41 | format = null; 42 | return false; 43 | } 44 | 45 | GetNumerator(numeratorParts, out var integerPart, out var numeratorPart); 46 | 47 | if (!TryGetDenominator(denominatorParts, out var denominatorPrefix, out var denominatorPart, out var denominatorConstant, out var denominatorSuffix, out var fractionSuffix)) 48 | { 49 | format = null; 50 | return false; 51 | } 52 | 53 | format = new FractionSection() 54 | { 55 | IntegerPart = integerPart, 56 | Numerator = numeratorPart, 57 | DenominatorPrefix = denominatorPrefix, 58 | Denominator = denominatorPart, 59 | DenominatorConstant = denominatorConstant, 60 | DenominatorSuffix = denominatorSuffix, 61 | FractionSuffix = fractionSuffix 62 | }; 63 | 64 | return true; 65 | } 66 | 67 | static void GetNumerator(List tokens, out List integerPart, out List numeratorPart) 68 | { 69 | var hasPlaceholder = false; 70 | var hasSpace = false; 71 | var hasIntegerPart = false; 72 | var numeratorIndex = -1; 73 | var index = tokens.Count - 1; 74 | while (index >= 0) 75 | { 76 | var token = tokens[index]; 77 | if (Token.IsPlaceholder(token)) 78 | { 79 | hasPlaceholder = true; 80 | 81 | if (hasSpace) 82 | { 83 | hasIntegerPart = true; 84 | break; 85 | } 86 | } 87 | else 88 | { 89 | if (hasPlaceholder && !hasSpace) 90 | { 91 | // First time we get here marks the end of the integer part 92 | hasSpace = true; 93 | numeratorIndex = index + 1; 94 | } 95 | } 96 | index--; 97 | } 98 | 99 | if (hasIntegerPart) 100 | { 101 | integerPart = tokens.GetRange(0, numeratorIndex); 102 | numeratorPart = tokens.GetRange(numeratorIndex, tokens.Count - numeratorIndex); 103 | } 104 | else 105 | { 106 | integerPart = null; 107 | numeratorPart = tokens; 108 | } 109 | } 110 | 111 | static bool TryGetDenominator(List tokens, out List denominatorPrefix, out List denominatorPart, out int denominatorConstant, out List denominatorSuffix, out List fractionSuffix) 112 | { 113 | var index = 0; 114 | var hasPlaceholder = false; 115 | var hasConstant = false; 116 | 117 | var constant = new StringBuilder(); 118 | 119 | // Read literals until the first number placeholder or digit 120 | while (index < tokens.Count) 121 | { 122 | var token = tokens[index]; 123 | if (Token.IsPlaceholder(token)) 124 | { 125 | hasPlaceholder = true; 126 | break; 127 | } 128 | else 129 | if (Token.IsDigit19(token)) 130 | { 131 | hasConstant = true; 132 | break; 133 | } 134 | index++; 135 | } 136 | 137 | if (!hasPlaceholder && !hasConstant) 138 | { 139 | denominatorPrefix = null; 140 | denominatorPart = null; 141 | denominatorConstant = 0; 142 | denominatorSuffix = null; 143 | fractionSuffix = null; 144 | return false; 145 | } 146 | 147 | // The denominator starts here, keep the index 148 | var denominatorIndex = index; 149 | 150 | // Read placeholders or digits in sequence 151 | while (index < tokens.Count) 152 | { 153 | var token = tokens[index]; 154 | if (hasPlaceholder && Token.IsPlaceholder(token)) 155 | { 156 | ; // OK 157 | } 158 | else 159 | if (hasConstant && (Token.IsDigit09(token))) 160 | { 161 | constant.Append(token); 162 | } 163 | else 164 | { 165 | break; 166 | } 167 | index++; 168 | } 169 | 170 | // 'index' is now at the first token after the denominator placeholders. 171 | // The remaining, if anything, is to be treated in one or two parts: 172 | // Any ultimately terminating literals are considered the "Fraction suffix". 173 | // Anything between the denominator and the fraction suffix is the "Denominator suffix". 174 | // Placeholders in the denominator suffix are treated as insignificant zeros. 175 | 176 | // Scan backwards to determine the fraction suffix 177 | int fractionSuffixIndex = tokens.Count; 178 | while (fractionSuffixIndex > index) 179 | { 180 | var token = tokens[fractionSuffixIndex - 1]; 181 | if (Token.IsPlaceholder(token)) 182 | { 183 | break; 184 | } 185 | 186 | fractionSuffixIndex--; 187 | } 188 | 189 | // Finally extract the detected token ranges 190 | 191 | if (denominatorIndex > 0) 192 | denominatorPrefix = tokens.GetRange(0, denominatorIndex); 193 | else 194 | denominatorPrefix = null; 195 | 196 | if (hasConstant) 197 | denominatorConstant = int.Parse(constant.ToString()); 198 | else 199 | denominatorConstant = 0; 200 | 201 | denominatorPart = tokens.GetRange(denominatorIndex, index - denominatorIndex); 202 | 203 | if (index < fractionSuffixIndex) 204 | denominatorSuffix = tokens.GetRange(index, fractionSuffixIndex - index); 205 | else 206 | denominatorSuffix = null; 207 | 208 | if (fractionSuffixIndex < tokens.Count) 209 | fractionSuffix = tokens.GetRange(fractionSuffixIndex, tokens.Count - fractionSuffixIndex); 210 | else 211 | fractionSuffix = null; 212 | 213 | return true; 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/NumberFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | 5 | namespace ExcelNumberFormat 6 | { 7 | /// 8 | /// Parse ECMA-376 number format strings and format values like Excel and other spreadsheet softwares. 9 | /// 10 | public class NumberFormat 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The number format string. 16 | public NumberFormat(string formatString) 17 | { 18 | var sections = Parser.ParseSections(formatString, out bool syntaxError); 19 | 20 | IsValid = !syntaxError; 21 | FormatString = formatString; 22 | 23 | if (IsValid) 24 | { 25 | Sections = sections; 26 | IsDateTimeFormat = Evaluator.GetFirstSection(Sections, SectionType.Date) != null; 27 | IsTimeSpanFormat = Evaluator.GetFirstSection(Sections, SectionType.Duration) != null; 28 | } 29 | else 30 | { 31 | Sections = new List
(); 32 | } 33 | } 34 | 35 | /// 36 | /// Gets a value indicating whether the number format string is valid. 37 | /// 38 | public bool IsValid { get; } 39 | 40 | /// 41 | /// Gets the number format string. 42 | /// 43 | public string FormatString { get; } 44 | 45 | /// 46 | /// Gets a value indicating whether the format represents a DateTime 47 | /// 48 | public bool IsDateTimeFormat { get; } 49 | 50 | /// 51 | /// Gets a value indicating whether the format represents a TimeSpan 52 | /// 53 | public bool IsTimeSpanFormat { get; } 54 | 55 | internal List
Sections { get; } 56 | 57 | /// 58 | /// Formats a value with this number format in a specified culture. 59 | /// 60 | /// The value to format. 61 | /// The culture to use for formatting. 62 | /// 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. 63 | /// The formatted string. 64 | public string Format(object value, CultureInfo culture, bool isDate1904 = false) 65 | { 66 | var section = Evaluator.GetSection(Sections, value); 67 | if (section == null) 68 | return CompatibleConvert.ToString(value, culture); 69 | 70 | try 71 | { 72 | return Formatter.Format(value, section, culture, isDate1904); 73 | } 74 | catch (InvalidCastException) 75 | { 76 | // TimeSpan cast exception 77 | return CompatibleConvert.ToString(value, culture); 78 | } 79 | catch (FormatException) 80 | { 81 | // Convert.ToDouble/ToDateTime exceptions 82 | return CompatibleConvert.ToString(value, culture); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | 5 | namespace ExcelNumberFormat 6 | { 7 | internal static class Parser 8 | { 9 | public static List
ParseSections(string formatString, out bool syntaxError) 10 | { 11 | var tokenizer = new Tokenizer(formatString); 12 | var sections = new List
(); 13 | syntaxError = false; 14 | while (true) 15 | { 16 | var section = ParseSection(tokenizer, sections.Count, out var sectionSyntaxError); 17 | 18 | if (sectionSyntaxError) 19 | syntaxError = true; 20 | 21 | if (section == null) 22 | break; 23 | 24 | sections.Add(section); 25 | } 26 | 27 | return sections; 28 | } 29 | 30 | private static Section ParseSection(Tokenizer reader, int index, out bool syntaxError) 31 | { 32 | bool hasDateParts = false; 33 | bool hasDurationParts = false; 34 | bool hasGeneralPart = false; 35 | bool hasTextPart = false; 36 | bool hasPlaceholders = false; 37 | Condition condition = null; 38 | Color color = null; 39 | string token; 40 | List tokens = new List(); 41 | 42 | syntaxError = false; 43 | while ((token = ReadToken(reader, out syntaxError)) != null) 44 | { 45 | if (token == ";") 46 | break; 47 | 48 | hasPlaceholders |= Token.IsPlaceholder(token); 49 | 50 | if (Token.IsDatePart(token)) 51 | { 52 | hasDateParts |= true; 53 | hasDurationParts |= Token.IsDurationPart(token); 54 | tokens.Add(token); 55 | } 56 | else if (Token.IsGeneral(token)) 57 | { 58 | hasGeneralPart |= true; 59 | tokens.Add(token); 60 | } 61 | else if (token == "@") 62 | { 63 | hasTextPart |= true; 64 | tokens.Add(token); 65 | } 66 | else if (token.StartsWith("[")) 67 | { 68 | // Does not add to tokens. Absolute/elapsed time tokens 69 | // also start with '[', but handled as date part above 70 | var expression = token.Substring(1, token.Length - 2); 71 | if (TryParseCondition(expression, out var parseCondition)) 72 | condition = parseCondition; 73 | else if (TryParseColor(expression, out var parseColor)) 74 | color = parseColor; 75 | else if (TryParseCurrencySymbol(expression, out var parseCurrencySymbol)) 76 | tokens.Add("\"" + parseCurrencySymbol + "\""); 77 | } 78 | else 79 | { 80 | tokens.Add(token); 81 | } 82 | } 83 | 84 | if (syntaxError || tokens.Count == 0) 85 | { 86 | return null; 87 | } 88 | 89 | if ( 90 | (hasDateParts && (hasGeneralPart || hasTextPart)) || 91 | (hasGeneralPart && (hasDateParts || hasTextPart)) || 92 | (hasTextPart && (hasGeneralPart || hasDateParts))) 93 | { 94 | // Cannot mix date, general and/or text parts 95 | syntaxError = true; 96 | return null; 97 | } 98 | 99 | SectionType type; 100 | FractionSection fraction = null; 101 | ExponentialSection exponential = null; 102 | DecimalSection number = null; 103 | List generalTextDateDuration = null; 104 | 105 | if (hasDateParts) 106 | { 107 | if (hasDurationParts) 108 | { 109 | type = SectionType.Duration; 110 | } 111 | else 112 | { 113 | type = SectionType.Date; 114 | } 115 | 116 | ParseMilliseconds(tokens, out generalTextDateDuration); 117 | } 118 | else if (hasGeneralPart) 119 | { 120 | type = SectionType.General; 121 | generalTextDateDuration = tokens; 122 | } 123 | else if (hasTextPart || !hasPlaceholders) 124 | { 125 | type = SectionType.Text; 126 | generalTextDateDuration = tokens; 127 | } 128 | else if (FractionSection.TryParse(tokens, out fraction)) 129 | { 130 | type = SectionType.Fraction; 131 | } 132 | else if (ExponentialSection.TryParse(tokens, out exponential)) 133 | { 134 | type = SectionType.Exponential; 135 | } 136 | else if (DecimalSection.TryParse(tokens, out number)) 137 | { 138 | type = SectionType.Number; 139 | } 140 | else 141 | { 142 | // Unable to parse format string 143 | syntaxError = true; 144 | return null; 145 | } 146 | 147 | return new Section() 148 | { 149 | Type = type, 150 | SectionIndex = index, 151 | Color = color, 152 | Condition = condition, 153 | Fraction = fraction, 154 | Exponential = exponential, 155 | Number = number, 156 | GeneralTextDateDurationParts = generalTextDateDuration 157 | }; 158 | } 159 | 160 | /// 161 | /// Parses as many placeholders and literals needed to format a number with optional decimals. 162 | /// Returns number of tokens parsed, or 0 if the tokens didn't form a number. 163 | /// 164 | internal static int ParseNumberTokens(List tokens, int startPosition, out List beforeDecimal, out bool decimalSeparator, out List afterDecimal) 165 | { 166 | beforeDecimal = null; 167 | afterDecimal = null; 168 | decimalSeparator = false; 169 | 170 | List remainder = new List(); 171 | var index = 0; 172 | for (index = 0; index < tokens.Count; ++index) 173 | { 174 | var token = tokens[index]; 175 | if (token == "." && beforeDecimal == null) 176 | { 177 | decimalSeparator = true; 178 | beforeDecimal = tokens.GetRange(0, index); // TODO: why not remainder? has only valid tokens... 179 | 180 | remainder = new List(); 181 | } 182 | else if (Token.IsNumberLiteral(token)) 183 | { 184 | remainder.Add(token); 185 | } 186 | else if (token.StartsWith("[")) 187 | { 188 | // ignore 189 | } 190 | else 191 | { 192 | break; 193 | } 194 | } 195 | 196 | if (remainder.Count > 0) 197 | { 198 | if (beforeDecimal != null) 199 | { 200 | afterDecimal = remainder; 201 | } 202 | else 203 | { 204 | beforeDecimal = remainder; 205 | } 206 | } 207 | 208 | return index; 209 | } 210 | 211 | private static void ParseMilliseconds(List tokens, out List result) 212 | { 213 | // if tokens form .0 through .000.., combine to single subsecond token 214 | result = new List(); 215 | for (var i = 0; i < tokens.Count; i++) 216 | { 217 | var token = tokens[i]; 218 | if (token == ".") 219 | { 220 | var zeros = 0; 221 | while (i + 1 < tokens.Count && tokens[i + 1] == "0") 222 | { 223 | i++; 224 | zeros++; 225 | } 226 | 227 | if (zeros > 0) 228 | result.Add("." + new string('0', zeros)); 229 | else 230 | result.Add("."); 231 | } 232 | else 233 | { 234 | result.Add(token); 235 | } 236 | } 237 | } 238 | 239 | private static string ReadToken(Tokenizer reader, out bool syntaxError) 240 | { 241 | var offset = reader.Position; 242 | if ( 243 | ReadLiteral(reader) || 244 | reader.ReadEnclosed('[', ']') || 245 | 246 | // Symbols 247 | reader.ReadOneOf("#?,!&%+-$€£0123456789{}():;/.@ ") || 248 | reader.ReadString("e+", true) || 249 | reader.ReadString("e-", true) || 250 | reader.ReadString("General", true) || 251 | 252 | // Date 253 | reader.ReadString("am/pm", true) || 254 | reader.ReadString("a/p", true) || 255 | reader.ReadOneOrMore('y') || 256 | reader.ReadOneOrMore('Y') || 257 | reader.ReadOneOrMore('m') || 258 | reader.ReadOneOrMore('M') || 259 | reader.ReadOneOrMore('d') || 260 | reader.ReadOneOrMore('D') || 261 | reader.ReadOneOrMore('h') || 262 | reader.ReadOneOrMore('H') || 263 | reader.ReadOneOrMore('s') || 264 | reader.ReadOneOrMore('S') || 265 | reader.ReadOneOrMore('g') || 266 | reader.ReadOneOrMore('G')) 267 | { 268 | syntaxError = false; 269 | var length = reader.Position - offset; 270 | return reader.Substring(offset, length); 271 | } 272 | 273 | syntaxError = reader.Position < reader.Length; 274 | return null; 275 | } 276 | 277 | private static bool ReadLiteral(Tokenizer reader) 278 | { 279 | if (reader.Peek() == '\\' || reader.Peek() == '*' || reader.Peek() == '_') 280 | { 281 | reader.Advance(2); 282 | return true; 283 | } 284 | else if (reader.ReadEnclosed('"', '"')) 285 | { 286 | return true; 287 | } 288 | 289 | return false; 290 | } 291 | 292 | private static bool TryParseCondition(string token, out Condition result) 293 | { 294 | var tokenizer = new Tokenizer(token); 295 | 296 | if (tokenizer.ReadString("<=") || 297 | tokenizer.ReadString("<>") || 298 | tokenizer.ReadString("<") || 299 | tokenizer.ReadString(">=") || 300 | tokenizer.ReadString(">") || 301 | tokenizer.ReadString("=")) 302 | { 303 | var conditionPosition = tokenizer.Position; 304 | var op = tokenizer.Substring(0, conditionPosition); 305 | 306 | if (ReadConditionValue(tokenizer)) 307 | { 308 | var valueString = tokenizer.Substring(conditionPosition, tokenizer.Position - conditionPosition); 309 | 310 | result = new Condition() 311 | { 312 | Operator = op, 313 | Value = double.Parse(valueString, CultureInfo.InvariantCulture) 314 | }; 315 | return true; 316 | } 317 | } 318 | 319 | result = null; 320 | return false; 321 | } 322 | 323 | private static bool ReadConditionValue(Tokenizer tokenizer) 324 | { 325 | // NFPartCondNum = [ASCII-HYPHEN-MINUS] NFPartIntNum [INTL-CHAR-DECIMAL-SEP NFPartIntNum] [NFPartExponential NFPartIntNum] 326 | tokenizer.ReadString("-"); 327 | while (tokenizer.ReadOneOf("0123456789")) 328 | { 329 | } 330 | 331 | if (tokenizer.ReadString(".")) 332 | { 333 | while (tokenizer.ReadOneOf("0123456789")) 334 | { 335 | } 336 | } 337 | 338 | if (tokenizer.ReadString("e+", true) || tokenizer.ReadString("e-", true)) 339 | { 340 | if (tokenizer.ReadOneOf("0123456789")) 341 | { 342 | while (tokenizer.ReadOneOf("0123456789")) 343 | { 344 | } 345 | } 346 | else 347 | { 348 | return false; 349 | } 350 | } 351 | 352 | return true; 353 | } 354 | 355 | private static bool TryParseColor(string token, out Color color) 356 | { 357 | // TODO: Color1..59 358 | var tokenizer = new Tokenizer(token); 359 | if ( 360 | tokenizer.ReadString("black", true) || 361 | tokenizer.ReadString("blue", true) || 362 | tokenizer.ReadString("cyan", true) || 363 | tokenizer.ReadString("green", true) || 364 | tokenizer.ReadString("magenta", true) || 365 | tokenizer.ReadString("red", true) || 366 | tokenizer.ReadString("white", true) || 367 | tokenizer.ReadString("yellow", true)) 368 | { 369 | color = new Color() 370 | { 371 | Value = tokenizer.Substring(0, tokenizer.Position) 372 | }; 373 | return true; 374 | } 375 | 376 | color = null; 377 | return false; 378 | } 379 | 380 | private static bool TryParseCurrencySymbol(string token, out string currencySymbol) 381 | { 382 | if (string.IsNullOrEmpty(token) 383 | || !token.StartsWith("$")) 384 | { 385 | currencySymbol = null; 386 | return false; 387 | } 388 | 389 | 390 | if (token.Contains("-")) 391 | currencySymbol = token.Substring(1, token.IndexOf('-') - 1); 392 | else 393 | currencySymbol = token.Substring(1); 394 | 395 | return true; 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Section.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal class Section 6 | { 7 | public int SectionIndex { get; set; } 8 | 9 | public SectionType Type { get; set; } 10 | 11 | public Color Color { get; set; } 12 | 13 | public Condition Condition { get; set; } 14 | 15 | public ExponentialSection Exponential { get; set; } 16 | 17 | public FractionSection Fraction { get; set; } 18 | 19 | public DecimalSection Number { get; set; } 20 | 21 | public List GeneralTextDateDurationParts { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/SectionType.cs: -------------------------------------------------------------------------------- 1 | namespace ExcelNumberFormat 2 | { 3 | internal enum SectionType 4 | { 5 | General, 6 | Number, 7 | Fraction, 8 | Exponential, 9 | Date, 10 | Duration, 11 | Text, 12 | } 13 | } -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Token.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal static class Token 6 | { 7 | public static bool IsExponent(string token) 8 | { 9 | return 10 | (string.Compare(token, "e+", StringComparison.OrdinalIgnoreCase) == 0) || 11 | (string.Compare(token, "e-", StringComparison.OrdinalIgnoreCase) == 0); 12 | } 13 | 14 | public static bool IsLiteral(string token) 15 | { 16 | return 17 | token.StartsWith("_") || 18 | token.StartsWith("\\") || 19 | token.StartsWith("\"") || 20 | token.StartsWith("*") || 21 | token == "," || 22 | token == "!" || 23 | token == "&" || 24 | token == "%" || 25 | token == "+" || 26 | token == "-" || 27 | token == "$" || 28 | token == "€" || 29 | token == "£" || 30 | token == "1" || 31 | token == "2" || 32 | token == "3" || 33 | token == "4" || 34 | token == "5" || 35 | token == "6" || 36 | token == "7" || 37 | token == "8" || 38 | token == "9" || 39 | token == "{" || 40 | token == "}" || 41 | token == "(" || 42 | token == ")" || 43 | token == " "; 44 | } 45 | 46 | public static bool IsNumberLiteral(string token) 47 | { 48 | return 49 | IsPlaceholder(token) || 50 | IsLiteral(token) || 51 | token == "."; 52 | } 53 | 54 | public static bool IsPlaceholder(string token) 55 | { 56 | return token == "0" || token == "#" || token == "?"; 57 | } 58 | 59 | public static bool IsGeneral(string token) 60 | { 61 | return string.Compare(token, "general", StringComparison.OrdinalIgnoreCase) == 0; 62 | } 63 | 64 | public static bool IsDatePart(string token) 65 | { 66 | return 67 | token.StartsWith("y", StringComparison.OrdinalIgnoreCase) || 68 | token.StartsWith("m", StringComparison.OrdinalIgnoreCase) || 69 | token.StartsWith("d", StringComparison.OrdinalIgnoreCase) || 70 | token.StartsWith("s", StringComparison.OrdinalIgnoreCase) || 71 | token.StartsWith("h", StringComparison.OrdinalIgnoreCase) || 72 | (token.StartsWith("g", StringComparison.OrdinalIgnoreCase) && !IsGeneral(token)) || 73 | string.Compare(token, "am/pm", StringComparison.OrdinalIgnoreCase) == 0 || 74 | string.Compare(token, "a/p", StringComparison.OrdinalIgnoreCase) == 0 || 75 | IsDurationPart(token); 76 | } 77 | 78 | public static bool IsDurationPart(string token) 79 | { 80 | return 81 | token.StartsWith("[h", StringComparison.OrdinalIgnoreCase) || 82 | token.StartsWith("[m", StringComparison.OrdinalIgnoreCase) || 83 | token.StartsWith("[s", StringComparison.OrdinalIgnoreCase); 84 | } 85 | 86 | public static bool IsDigit09(string token) 87 | { 88 | return token == "0" || IsDigit19(token); 89 | } 90 | 91 | public static bool IsDigit19(string token) 92 | { 93 | switch (token) 94 | { 95 | case "1": 96 | case "2": 97 | case "3": 98 | case "4": 99 | case "5": 100 | case "6": 101 | case "7": 102 | case "8": 103 | case "9": 104 | return true; 105 | default: 106 | return false; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/Tokenizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ExcelNumberFormat 4 | { 5 | internal class Tokenizer 6 | { 7 | private string formatString; 8 | private int formatStringPosition = 0; 9 | 10 | public Tokenizer(string fmt) 11 | { 12 | formatString = fmt; 13 | } 14 | 15 | public int Position => formatStringPosition; 16 | 17 | public int Length => formatString?.Length ?? 0; 18 | 19 | public string Substring(int startIndex, int length) 20 | { 21 | return formatString.Substring(startIndex, length); 22 | } 23 | 24 | public int Peek(int offset = 0) 25 | { 26 | if (formatStringPosition + offset >= Length) 27 | return -1; 28 | return formatString[formatStringPosition + offset]; 29 | } 30 | 31 | public int PeekUntil(int startOffset, int until) 32 | { 33 | int offset = startOffset; 34 | while (true) 35 | { 36 | var c = Peek(offset++); 37 | if (c == -1) 38 | break; 39 | if (c == until) 40 | return offset - startOffset; 41 | } 42 | return 0; 43 | } 44 | 45 | public bool PeekOneOf(int offset, string s) 46 | { 47 | foreach (var c in s) 48 | { 49 | if (Peek(offset) == c) 50 | { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | public void Advance(int characters = 1) 58 | { 59 | formatStringPosition = Math.Min(formatStringPosition + characters, formatString.Length); 60 | } 61 | 62 | public bool ReadOneOrMore(int c) 63 | { 64 | if (Peek() != c) 65 | return false; 66 | 67 | while (Peek() == c) 68 | Advance(); 69 | 70 | return true; 71 | } 72 | 73 | public bool ReadOneOf(string s) 74 | { 75 | if (PeekOneOf(0, s)) 76 | { 77 | Advance(); 78 | return true; 79 | } 80 | return false; 81 | } 82 | 83 | public bool ReadString(string s, bool ignoreCase = false) 84 | { 85 | if (formatStringPosition + s.Length > Length) 86 | return false; 87 | 88 | for (var i = 0; i < s.Length; i++) 89 | { 90 | var c1 = s[i]; 91 | var c2 = (char)Peek(i); 92 | if (ignoreCase) 93 | { 94 | if (char.ToLower(c1) != char.ToLower(c2)) return false; 95 | } 96 | else 97 | { 98 | if (c1 != c2) return false; 99 | } 100 | } 101 | 102 | Advance(s.Length); 103 | return true; 104 | } 105 | 106 | public bool ReadEnclosed(char open, char close) 107 | { 108 | if (Peek() == open) 109 | { 110 | int length = PeekUntil(1, close); 111 | if (length > 0) 112 | { 113 | Advance(1 + length); 114 | return true; 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ExcelNumberFormat/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersnm/ExcelNumberFormat/38c6a71919ad3895e3ba61d7a888012670d26303/src/ExcelNumberFormat/icon.png -------------------------------------------------------------------------------- /test/ExcelNumberFormat.Tests/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NUnit.Framework; 4 | using TestClass = NUnit.Framework.TestFixtureAttribute; 5 | using TestMethod = NUnit.Framework.TestAttribute; 6 | 7 | // Much of the test data was adapted from the SheetJS/ssf project. 8 | // Copyright (C) 2013-present SheetJS. 9 | // Licensed under the Apache License, Version 2.0 10 | 11 | namespace ExcelNumberFormat.Tests 12 | { 13 | [TestClass] 14 | public class Class1 15 | { 16 | 17 | string Format(object value, string formatString, CultureInfo culture, bool isDate1904 = false) 18 | { 19 | var format = new NumberFormat(formatString); 20 | if (format.IsValid) 21 | return format.Format(value, culture, isDate1904); 22 | 23 | return null; 24 | } 25 | 26 | bool IsDateFormatString(string formatString) 27 | { 28 | var format = new NumberFormat(formatString); 29 | return format?.IsDateTimeFormat ?? false; 30 | } 31 | 32 | [TestMethod] 33 | public void TestCondition() 34 | { 35 | Test("Hello", "\"p\"0;\"m\"0;\"z\"0;\"t\"@", "tHello"); 36 | Test("Hello", "\"num\"0", "Hello"); 37 | 38 | Test(-1, "\"p\"0;\"m\"0;\"z\"0;\"t\"@", "m1"); 39 | Test(0, "\"p\"0;\"m\"0;\"z\"0;\"t\"@", "z0"); 40 | Test(1, "\"p\"0;\"m\"0;\"z\"0;\"t\"@", "p1"); 41 | 42 | Test(-1, "[<0]\"p\"0;\"m\"0;\"z\"0;\"t\"@", "p1"); 43 | Test(0, "[<0]\"p\"0;\"m\"0;\"z\"0;\"t\"@", "z0"); 44 | Test(1, "[<0]\"p\"0;\"m\"0;\"z\"0;\"t\"@", "z1"); 45 | 46 | Test(-1, "\"p\"0;[>0]\"m\"0;\"z\"0;\"t\"@", "-z1"); 47 | Test(0, "\"p\"0;[>0]\"m\"0;\"z\"0;\"t\"@", "z0"); 48 | Test(1, "\"p\"0;[>0]\"m\"0;\"z\"0;\"t\"@", "p1"); 49 | 50 | Test(-1, "[<0]\"LT0\";\"ELSE\"", "LT0"); 51 | Test(0, "[<0]\"LT0\";\"ELSE\"", "ELSE"); 52 | Test(1, "[<0]\"LT0\";\"ELSE\"", "ELSE"); 53 | 54 | Test(-1, "[<=0]\"LTE0\";\"ELSE\"", "LTE0"); 55 | Test(0, "[<=0]\"LTE0\";\"ELSE\"", "LTE0"); 56 | Test(1, "[<=0]\"LTE0\";\"ELSE\"", "ELSE"); 57 | 58 | Test(-1, "[>0]\"GT0\";\"ELSE\"", "ELSE"); 59 | Test(0, "[>0]\"GT0\";\"ELSE\"", "ELSE"); 60 | Test(1, "[>0]\"GT0\";\"ELSE\"", "GT0"); 61 | 62 | Test(-1, "[>=0]\"GTE0\";\"ELSE\"", "ELSE"); 63 | Test(0, "[>=0]\"GTE0\";\"ELSE\"", "GTE0"); 64 | Test(1, "[>=0]\"GTE0\";\"ELSE\"", "GTE0"); 65 | 66 | Test(-1, "[=0]\"EQ0\";\"ELSE\"", "ELSE"); 67 | Test(0, "[=0]\"EQ0\";\"ELSE\"", "EQ0"); 68 | Test(1, "[=0]\"EQ0\";\"ELSE\"", "ELSE"); 69 | 70 | Test(-1, "[<>0]\"NEQ0\";\"ELSE\"", "NEQ0"); 71 | Test(0, "[<>0]\"NEQ0\";\"ELSE\"", "ELSE"); 72 | Test(1, "[<>0]\"NEQ0\";\"ELSE\"", "NEQ0"); 73 | } 74 | 75 | [TestMethod] 76 | public void TestFractionAlignmentSuffix() 77 | { 78 | Test(0, "??/??", " 0/1 "); 79 | Test(1.5, "??/??", " 3/2 "); 80 | Test(3.4, "??/??", "17/5 "); 81 | Test(4.3, "??/??", "43/10"); 82 | 83 | Test(0, "00/00", "00/01"); 84 | Test(1.5, "00/00", "03/02"); 85 | Test(3.4, "00/00", "17/05"); 86 | Test(4.3, "00/00", "43/10"); 87 | 88 | Test(0.00, "# ??/\"a\"?\"a\"0\"a\"", "0 a"); 89 | Test(0.10, "# ??/\"a\"?\"a\"0\"a\"", "0 a"); 90 | Test(0.12, "# ??/\"a\"?\"a\"0\"a\"", " 1/a8a0a"); 91 | 92 | Test(1.00, "# ??/\"a\"?\"a\"0\"a\"", "1 a"); 93 | Test(1.10, "# ??/\"a\"?\"a\"0\"a\"", "1 1/a9a0a"); 94 | Test(1.12, "# ??/\"a\"?\"a\"0\"a\"", "1 1/a8a0a"); 95 | } 96 | 97 | 98 | [TestMethod] 99 | public void TestIsDateFormatString() 100 | { 101 | Assert.IsTrue(IsDateFormatString("dd/mm/yyyy")); 102 | Assert.IsTrue(IsDateFormatString("dd-mmm-yy")); 103 | Assert.IsTrue(IsDateFormatString("dd-mmmm")); 104 | Assert.IsTrue(IsDateFormatString("mmm-yy")); 105 | Assert.IsTrue(IsDateFormatString("h:mm AM/PM")); 106 | Assert.IsTrue(IsDateFormatString("h:mm:ss AM/PM")); 107 | Assert.IsTrue(IsDateFormatString("hh:mm")); 108 | Assert.IsTrue(IsDateFormatString("hh:mm:ss")); 109 | Assert.IsTrue(IsDateFormatString("dd/mm/yyyy hh:mm")); 110 | Assert.IsTrue(IsDateFormatString("mm:ss")); 111 | Assert.IsTrue(IsDateFormatString("mm:ss.0")); 112 | Assert.IsTrue(IsDateFormatString("[$-809]dd mmmm yyyy")); 113 | Assert.IsFalse(IsDateFormatString("#,##0;[Red]-#,##0")); 114 | Assert.IsFalse(IsDateFormatString("0_);[Red](0)")); 115 | Assert.IsFalse(IsDateFormatString(@"0\h")); 116 | Assert.IsFalse(IsDateFormatString("0\"h\"")); 117 | Assert.IsFalse(IsDateFormatString("0%")); 118 | Assert.IsFalse(IsDateFormatString("General")); 119 | Assert.IsFalse(IsDateFormatString(@"_-* #,##0\ _P_t_s_-;\-* #,##0\ _P_t_s_-;_-* "" - ""??\ _P_t_s_-;_-@_- ")); 120 | } 121 | 122 | [TestMethod] 123 | public void TestDate() 124 | { 125 | Test(new DateTime(2000, 1, 1), "d-mmm-yy", "1-Jan-00"); 126 | Test(new DateTime(2000, 1, 1, 12, 34, 56), "m/d/yyyy\\ h:mm:ss;@", "1/1/2000 12:34:56"); 127 | Test(new DateTime(2010, 9, 26), "yyyy-MMM-dd", "2010-Sep-26"); 128 | Test(new DateTime(2010, 9, 26), "yyyy-MM-dd", "2010-09-26"); 129 | Test(new DateTime(2010, 9, 26), "mm/dd/yyyy", "09/26/2010"); 130 | Test(new DateTime(2010, 9, 26), "m/d/yy", "9/26/10"); 131 | Test(new DateTime(2010, 9, 26, 12, 34, 56, 123), "m/d/yy hh:mm:ss.000", "9/26/10 12:34:56.123"); 132 | Test(new DateTime(2010, 9, 26, 12, 34, 56, 123), "YYYY-MM-DD HH:MM:SS", "2010-09-26 12:34:56"); 133 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss AM/PM;@", "1/1/2020 2:35:55 PM"); 134 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss aM/Pm;@", "1/1/2020 2:35:55 PM"); 135 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss am/PM;@", "1/1/2020 2:35:55 PM"); 136 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss A/P;@", "1/1/2020 2:35:55 P"); 137 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss a/P;@", "1/1/2020 2:35:55 p"); 138 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss A/p;@", "1/1/2020 2:35:55 P"); 139 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ h:mm:ss;@", "1/1/2020 14:35:55"); 140 | Test(new DateTime(2020, 1, 1, 14, 35, 55), "m/d/yyyy\\ hh:mm:ss AM/PM;@", "1/1/2020 02:35:55 PM"); 141 | Test(new DateTime(2020, 1, 1, 16, 5, 6), "m/d/yyyy\\ h:m:s AM/PM;@", "1/1/2020 4:5:6 PM"); 142 | Test(new DateTime(2017, 10, 16, 0, 0, 0), "dddd, MMMM d, yyyy", "Monday, October 16, 2017"); 143 | Test(new DateTime(2017, 10, 16, 0, 0, 0), "dddd,,, MMMM d,, yyyy,,,,", "Monday, October 16, 2017,"); 144 | Test(new DateTime(2020, 1, 1, 0, 35, 55), "m/d/yyyy\\ hh:mm:ss AM/PM;@", "1/1/2020 12:35:55 AM"); 145 | Test(new DateTime(2020, 1, 1, 12, 35, 55), "m/d/yyyy\\ hh:mm:ss AM/PM;@", "1/1/2020 12:35:55 PM"); 146 | } 147 | 148 | [TestMethod] 149 | public void TestNumericDate1900() 150 | { 151 | Test("0", "dd/mm/yyyy", "0"); 152 | Test(0, "dd/mm/yyyy", "00/01/1900"); 153 | Test(0d, "dd/mm/yyyy", "00/01/1900"); 154 | Test((short)0, "dd/mm/yyyy", "00/01/1900"); 155 | Test(1, "dd/mm/yyyy", "01/01/1900"); 156 | Test(60, "dd/mm/yyyy", "29/02/1900"); 157 | Test(61, "dd/mm/yyyy", "01/03/1900"); 158 | Test(43648, "[$-409]d\\-mmm\\-yyyy;@", "2-Jul-2019"); 159 | } 160 | 161 | [TestMethod] 162 | public void TestNumericDate1904() 163 | { 164 | Test(0, "dd/mm/yyyy", "01/01/1904", true); 165 | Test(0d, "dd/mm/yyyy", "01/01/1904", true); 166 | Test((short)0, "dd/mm/yyyy", "01/01/1904", true); 167 | Test(1, "dd/mm/yyyy", "02/01/1904", true); 168 | Test(60, "dd/mm/yyyy", "01/03/1904", true); 169 | Test(61, "dd/mm/yyyy", "02/03/1904", true); 170 | } 171 | 172 | [TestMethod] 173 | public void TestNumericDuration() 174 | { 175 | Test(0, "[hh]:mm", "00:00"); 176 | Test(1, "[hh]:mm", "24:00"); 177 | Test(1.5, "[hh]:mm", "36:00"); 178 | } 179 | 180 | [TestMethod] 181 | public void TestTimeSpan() 182 | { 183 | Test(TimeSpan.FromHours(100), "[hh]:mm:ss", "100:00:00"); 184 | Test(TimeSpan.FromHours(100), "[mm]:ss", "6000:00"); 185 | Test(TimeSpan.FromMilliseconds(100 * 60 * 60 * 1000 + 123), "[mm]:ss.000", "6000:00.123"); 186 | 187 | Test(new TimeSpan(1, 2, 31, 45), "[hh]:mm:ss", "26:31:45"); 188 | Test(new TimeSpan(1, 2, 31, 44, 500), "[hh]:mm:ss", "26:31:45"); 189 | Test(new TimeSpan(1, 2, 31, 44, 500), "[hh]:mm:ss.000", "26:31:44.500"); 190 | 191 | Test(new TimeSpan(-1, -2, -31, -45), "[hh]:mm:ss", "-26:31:45"); 192 | Test(new TimeSpan(0, -2, -31, -45), "[hh]:mm:ss", "-02:31:45"); 193 | Test(new TimeSpan(0, -2, -31, -44, -500), "[hh]:mm:ss", "-02:31:45"); 194 | Test(new TimeSpan(0, -2, -31, -44, -500), "[hh]:mm:ss.000", "-02:31:44.500"); 195 | } 196 | 197 | void Test(object value, string format, string expected, bool isDate1904 = false) 198 | { 199 | var result = Format(value, format, CultureInfo.InvariantCulture, isDate1904); 200 | Assert.AreEqual(expected, result); 201 | } 202 | 203 | [TestMethod] 204 | public void TestFraction() 205 | { 206 | Test(1, "# ?/?", "1 "); 207 | Test(-1.2, "# ?/?", "-1 1/5"); 208 | Test(12.3, "# ?/?", "12 1/3"); 209 | Test(-12.34, "# ?/?", "-12 1/3"); 210 | Test(123.45, "# ?/?", "123 4/9"); 211 | Test(-123.456, "# ?/?", "-123 1/2"); 212 | Test(1234.567, "# ?/?", "1234 4/7"); 213 | Test(-1234.5678, "# ?/?", "-1234 4/7"); 214 | Test(12345.6789, "# ?/?", "12345 2/3"); 215 | Test(-12345.67891, "# ?/?", "-12345 2/3"); 216 | 217 | Test(1, "# ??/??", "1 "); 218 | Test(-1.2, "# ??/??", "-1 1/5 "); 219 | Test(12.3, "# ??/??", "12 3/10"); 220 | Test(-12.34, "# ??/??", "-12 17/50"); 221 | Test(123.45, "# ??/??", "123 9/20"); 222 | Test(-123.456, "# ??/??", "-123 26/57"); 223 | Test(1234.567, "# ??/??", "1234 55/97"); 224 | Test(-1234.5678, "# ??/??", "-1234 46/81"); 225 | Test(12345.6789, "# ??/??", "12345 55/81"); 226 | Test(-12345.67891, "# ??/??", "-12345 55/81"); 227 | 228 | Test(1, "# ???/???", "1 "); 229 | Test(-1.2, "# ???/???", "-1 1/5 "); 230 | Test(12.3, "# ???/???", "12 3/10 "); 231 | Test(-12.34, "# ???/???", "-12 17/50 "); 232 | Test(123.45, "# ???/???", "123 9/20 "); 233 | Test(-123.456, "# ???/???", "-123 57/125"); 234 | Test(1234.567, "# ???/???", "1234 55/97 "); 235 | Test(-1234.5678, "# ???/???", "-1234 67/118"); 236 | Test(12345.6789, "# ???/???", "12345 74/109"); 237 | Test(-12345.67891, "# ???/???", "-12345 573/844"); 238 | 239 | 240 | Test(1, "# ?/2", "1 "); 241 | Test(-1.2, "# ?/2", "-1 "); 242 | Test(12.3, "# ?/2", "12 1/2"); 243 | Test(-12.34, "# ?/2", "-12 1/2"); 244 | Test(123.45, "# ?/2", "123 1/2"); 245 | Test(-123.456, "# ?/2", "-123 1/2"); 246 | Test(1234.567, "# ?/2", "1234 1/2"); 247 | Test(-1234.5678, "# ?/2", "-1234 1/2"); 248 | Test(12345.6789, "# ?/2", "12345 1/2"); 249 | Test(-12345.67891, "# ?/2", "-12345 1/2"); 250 | 251 | Test(1, "# ?/4", "1 "); 252 | Test(-1.2, "# ?/4", "-1 1/4"); 253 | Test(12.3, "# ?/4", "12 1/4"); 254 | Test(-12.34, "# ?/4", "-12 1/4"); 255 | Test(123.45, "# ?/4", "123 2/4"); 256 | Test(-123.456, "# ?/4", "-123 2/4"); 257 | Test(1234.567, "# ?/4", "1234 2/4"); 258 | Test(-1234.5678, "# ?/4", "-1234 2/4"); 259 | Test(12345.6789, "# ?/4", "12345 3/4"); 260 | Test(-12345.67891, "# ?/4", "-12345 3/4"); 261 | 262 | Test(1, "# ?/8", "1 "); 263 | Test(-1.2, "# ?/8", "-1 2/8"); 264 | Test(12.3, "# ?/8", "12 2/8"); 265 | Test(-12.34, "# ?/8", "-12 3/8"); 266 | Test(123.45, "# ?/8", "123 4/8"); 267 | Test(-123.456, "# ?/8", "-123 4/8"); 268 | Test(1234.567, "# ?/8", "1234 5/8"); 269 | Test(-1234.5678, "# ?/8", "-1234 5/8"); 270 | Test(12345.6789, "# ?/8", "12345 5/8"); 271 | Test(-12345.67891, "# ?/8", "-12345 5/8"); 272 | 273 | Test(1, "# ??/16", "1 "); 274 | Test(-1.2, "# ??/16", "-1 3/16"); 275 | Test(12.3, "# ??/16", "12 5/16"); 276 | Test(-12.34, "# ??/16", "-12 5/16"); 277 | Test(123.45, "# ??/16", "123 7/16"); 278 | Test(-123.456, "# ??/16", "-123 7/16"); 279 | Test(1234.567, "# ??/16", "1234 9/16"); 280 | Test(-1234.5678, "# ??/16", "-1234 9/16"); 281 | Test(12345.6789, "# ??/16", "12345 11/16"); 282 | Test(-12345.67891, "# ??/16", "-12345 11/16"); 283 | 284 | Test(1, "# ?/10", "1 "); 285 | Test(-1.2, "# ?/10", "-1 2/10"); 286 | Test(12.3, "# ?/10", "12 3/10"); 287 | Test(-12.34, "# ?/10", "-12 3/10"); 288 | Test(123.45, "# ?/10", "123 5/10"); 289 | Test(-123.456, "# ?/10", "-123 5/10"); 290 | Test(1234.567, "# ?/10", "1234 6/10"); 291 | Test(-1234.5678, "# ?/10", "-1234 6/10"); 292 | Test(12345.6789, "# ?/10", "12345 7/10"); 293 | Test(-12345.67891, "# ?/10", "-12345 7/10"); 294 | 295 | Test(1, "# ??/100", "1 "); 296 | Test(-1.2, "# ??/100", "-1 20/100"); 297 | Test(12.3, "# ??/100", "12 30/100"); 298 | Test(-12.34, "# ??/100", "-12 34/100"); 299 | Test(123.45, "# ??/100", "123 45/100"); 300 | Test(-123.456, "# ??/100", "-123 46/100"); 301 | Test(1234.567, "# ??/100", "1234 57/100"); 302 | Test(-1234.5678, "# ??/100", "-1234 57/100"); 303 | Test(12345.6789, "# ??/100", "12345 68/100"); 304 | Test(-12345.67891, "# ??/100", "-12345 68/100"); 305 | 306 | Test(1, "??/??", " 1/1 "); 307 | Test(-1.2, "??/??", "- 6/5 "); 308 | Test(12.3, "??/??", "123/10"); 309 | Test(-12.34, "??/??", "-617/50"); 310 | Test(123.45, "??/??", "2469/20"); 311 | Test(-123.456, "??/??", "-7037/57"); 312 | Test(1234.567, "??/??", "119753/97"); 313 | Test(-1234.5678, "??/??", "-100000/81"); 314 | Test(12345.6789, "??/??", "1000000/81"); 315 | Test(-12345.67891, "??/??", "-1000000/81"); 316 | 317 | Test(0.3, "# ?/?", " 2/7"); 318 | Test(1.3, "# ?/?", "1 1/3"); 319 | Test(2.3, "# ?/?", "2 2/7"); 320 | 321 | // Not sure what/why ssf does here: 322 | // Test(0.123251512342345, "# ??/?????????", " 480894/3901729"); 323 | // Test(0.123251512342345, "# ?? / ?????????", " 480894 / 3901729"); 324 | // This implementation instead renders like this: 325 | Test(0.123251512342345, "# ??/?????????", " 480894/3901729 "); 326 | Test(0.123251512342345, "# ?? / ?????????", " 480894 / 3901729 "); 327 | 328 | Test(0, "0", "0"); 329 | 330 | } 331 | 332 | void TestExponents(double value, string expected1, string expected2, string expected3, string expected4) 333 | { 334 | // value #0.0E+0 ##0.0E+0 ###0.0E+0 ####0.0E+0 335 | var result1 = Format(value, "#0.0E+0", CultureInfo.InvariantCulture); 336 | Assert.AreEqual(expected1, result1); 337 | 338 | var result2 = Format(value, "##0.0E+0", CultureInfo.InvariantCulture); 339 | Assert.AreEqual(expected2, result2); 340 | 341 | var result3 = Format(value, "###0.0E+0", CultureInfo.InvariantCulture); 342 | Assert.AreEqual(expected3, result3); 343 | 344 | var result4 = Format(value, "####0.0E+0", CultureInfo.InvariantCulture); 345 | Assert.AreEqual(expected4, result4); 346 | } 347 | 348 | void TestNumber(double value, string expected1, string expected2, string expected3, string expected4, string expected5) 349 | { 350 | // value ?.? ??.?? ???.??? ???.?0? ???.?#? 351 | var result1 = Format(value, "?.?", CultureInfo.InvariantCulture); 352 | Assert.AreEqual(expected1, result1); 353 | 354 | var result2 = Format(value, "??.??", CultureInfo.InvariantCulture); 355 | Assert.AreEqual(expected2, result2); 356 | 357 | var result3 = Format(value, "???.???", CultureInfo.InvariantCulture); 358 | Assert.AreEqual(expected3, result3); 359 | 360 | var result4 = Format(value, "???.?0?", CultureInfo.InvariantCulture); 361 | Assert.AreEqual(expected4, result4); 362 | 363 | var result5 = Format(value, "???.?#?", CultureInfo.InvariantCulture); 364 | Assert.AreEqual(expected5, result5); 365 | } 366 | 367 | 368 | [TestMethod] 369 | public void TestNumber() 370 | { 371 | TestNumber( 0.0, " . ", " . ", " . ", " . 0 ", " . "); 372 | TestNumber( 0.1, " .1", " .1 ", " .1 ", " .10 ", " .1 "); 373 | TestNumber( 0.12, " .1", " .12", " .12 ", " .12 ", " .12 "); 374 | TestNumber(0.123, " .1", " .12", " .123", " .123", " .123"); 375 | 376 | TestNumber( 1.0, "1. ", " 1. ", " 1. ", " 1. 0 ", " 1. "); 377 | TestNumber( 1.1, "1.1", " 1.1 ", " 1.1 ", " 1.10 ", " 1.1 "); 378 | TestNumber( 1.12, "1.1", " 1.12", " 1.12 ", " 1.12 ", " 1.12 "); 379 | TestNumber(1.123, "1.1", " 1.12", " 1.123", " 1.123", " 1.123"); 380 | } 381 | 382 | [TestMethod] 383 | public void TestExponent() 384 | { 385 | TestExponents(-1.23457E-13, "-12.3E-14", "-123.5E-15", "-1234.6E-16", "-123.5E-15"); 386 | TestExponents(-12345.6789, "-1.2E+4", "-12.3E+3", "-1.2E+4", "-12345.7E+0"); 387 | 388 | TestExponents(1.23457E-13, "12.3E-14", "123.5E-15", "1234.6E-16", "123.5E-15"); 389 | TestExponents(1.23457E-12, "1.2E-12", "1.2E-12", "1.2E-12", "1234.6E-15"); 390 | TestExponents(1.23457E-11, "12.3E-12", "12.3E-12", "12.3E-12", "12345.7E-15"); 391 | TestExponents(1.23457E-10, "1.2E-10", "123.5E-12", "123.5E-12", "1.2E-10"); 392 | TestExponents(1.23457E-09, "12.3E-10", "1.2E-9", "1234.6E-12", "12.3E-10"); 393 | TestExponents(1.23457E-08, "1.2E-8", "12.3E-9", "1.2E-8", "123.5E-10"); 394 | TestExponents(0.000000123457, "12.3E-8", "123.5E-9", "12.3E-8", "1234.6E-10"); 395 | TestExponents(0.00000123457, "1.2E-6", "1.2E-6", "123.5E-8", "12345.7E-10"); 396 | TestExponents(0.0000123457, "12.3E-6", "12.3E-6", "1234.6E-8", "1.2E-5"); 397 | TestExponents(0.000123457, "1.2E-4", "123.5E-6", "1.2E-4", "12.3E-5"); 398 | TestExponents(0.001234568, "12.3E-4", "1.2E-3", "12.3E-4", "123.5E-5"); 399 | TestExponents(0.012345679, "1.2E-2", "12.3E-3", "123.5E-4", "1234.6E-5"); 400 | TestExponents(0.123456789, "12.3E-2", "123.5E-3", "1234.6E-4", "12345.7E-5"); 401 | TestExponents(1.23456789, "1.2E+0", "1.2E+0", "1.2E+0", "1.2E+0"); 402 | TestExponents(12.3456789, "12.3E+0", "12.3E+0", "12.3E+0", "12.3E+0"); 403 | TestExponents(123.456789, "1.2E+2", "123.5E+0", "123.5E+0", "123.5E+0"); 404 | TestExponents(1234.56789, "12.3E+2", "1.2E+3", "1234.6E+0", "1234.6E+0"); 405 | TestExponents(12345.6789, "1.2E+4", "12.3E+3", "1.2E+4", "12345.7E+0"); 406 | TestExponents(123456.789, "12.3E+4", "123.5E+3", "12.3E+4", "1.2E+5"); 407 | TestExponents(1234567.89, "1.2E+6", "1.2E+6", "123.5E+4", "12.3E+5"); 408 | TestExponents(12345678.9, "12.3E+6", "12.3E+6", "1234.6E+4", "123.5E+5"); 409 | TestExponents(123456789D, "1.2E+8", "123.5E+6", "1.2E+8", "1234.6E+5"); 410 | TestExponents(1234567890D, "12.3E+8", "1.2E+9", "12.3E+8", "12345.7E+5"); 411 | TestExponents(12345678900D, "1.2E+10", "12.3E+9", "123.5E+8", "1.2E+10"); 412 | TestExponents(123456789000D, "12.3E+10", "123.5E+9", "1234.6E+8", "12.3E+10"); 413 | TestExponents(1234567890000D, "1.2E+12", "1.2E+12", "1.2E+12", "123.5E+10"); 414 | TestExponents(12345678900000D, "12.3E+12", "12.3E+12", "12.3E+12", "1234.6E+10"); 415 | TestExponents(123456789000000D, "1.2E+14", "123.5E+12", "123.5E+12", "12345.7E+10"); 416 | TestExponents(1234567890000000D, "12.3E+14", "1.2E+15", "1234.6E+12", "1.2E+15"); 417 | TestExponents(12345678900000000D, "1.2E+16", "12.3E+15", "1.2E+16", "12.3E+15"); 418 | TestExponents(123456789000000000D, "12.3E+16", "123.5E+15", "12.3E+16", "123.5E+15"); 419 | TestExponents(1234567890000000000D, "1.2E+18", "1.2E+18", "123.5E+16", "1234.6E+15"); 420 | TestExponents(12345678900000000000D, "12.3E+18", "12.3E+18", "1234.6E+16", "12345.7E+15"); 421 | TestExponents(123456789000000000000D, "1.2E+20", "123.5E+18", "1.2E+20", "1.2E+20"); 422 | TestExponents(1234567890000000000000D, "12.3E+20", "1.2E+21", "12.3E+20", "12.3E+20"); 423 | TestExponents(12345678900000000000000D, "1.2E+22", "12.3E+21", "123.5E+20", "123.5E+20"); 424 | TestExponents(123456789000000000000000D, "12.3E+22", "123.5E+21", "1234.6E+20", "1234.6E+20"); 425 | TestExponents(1234567890000000000000000D, "1.2E+24", "1.2E+24", "1.2E+24", "12345.7E+20"); 426 | TestExponents(12345678900000000000000000D, "12.3E+24", "12.3E+24", "12.3E+24", "1.2E+25"); 427 | TestExponents(123456789000000000000000000D, "1.2E+26", "123.5E+24", "123.5E+24", "12.3E+25"); 428 | TestExponents(1234567890000000000000000000D, "12.3E+26", "1.2E+27", "1234.6E+24", "123.5E+25"); 429 | TestExponents(12345678900000000000000000000D, "1.2E+28", "12.3E+27", "1.2E+28", "1234.6E+25"); 430 | TestExponents(123456789000000000000000000000D, "12.3E+28", "123.5E+27", "12.3E+28", "12345.7E+25"); 431 | TestExponents(1234567890000000000000000000000D, "1.2E+30", "1.2E+30", "123.5E+28", "1.2E+30"); 432 | TestExponents(12345678900000000000000000000000D, "12.3E+30", "12.3E+30", "1234.6E+28", "12.3E+30"); 433 | } 434 | 435 | void TestComma(double value, string expected1, string expected2, string expected3, string expected4, string expected5, string expected6, string expected7) 436 | { 437 | // value #.0000,,, #.0000,, #.0000, #,##0.0 ###,##0 ###,### #,###.00 438 | var result1 = Format(value, "#.0000,,,", CultureInfo.InvariantCulture); 439 | Assert.AreEqual(expected1, result1); 440 | 441 | var result2 = Format(value, "#.0000,,", CultureInfo.InvariantCulture); 442 | Assert.AreEqual(expected2, result2); 443 | 444 | var result3 = Format(value, "#.0000,", CultureInfo.InvariantCulture); 445 | Assert.AreEqual(expected3, result3); 446 | 447 | var result4 = Format(value, "#,##0.0", CultureInfo.InvariantCulture); 448 | Assert.AreEqual(expected4, result4); 449 | 450 | var result5 = Format(value, "###,##0", CultureInfo.InvariantCulture); 451 | Assert.AreEqual(expected5, result5); 452 | 453 | var result6 = Format(value, "###,###", CultureInfo.InvariantCulture); 454 | Assert.AreEqual(expected6, result6); 455 | 456 | var result7 = Format(value, "#,###.00", CultureInfo.InvariantCulture); 457 | Assert.AreEqual(expected7, result7); 458 | } 459 | 460 | [TestMethod] 461 | public void TestComma() 462 | { 463 | TestComma(0.99, ".0000", ".0000", ".0010", "1.0", "1", "1", ".99"); 464 | TestComma(1.2345, ".0000", ".0000", ".0012", "1.2", "1", "1", "1.23"); 465 | TestComma(12.345, ".0000", ".0000", ".0123", "12.3", "12", "12", "12.35"); 466 | TestComma(123.456, ".0000", ".0001", ".1235", "123.5", "123", "123", "123.46"); 467 | TestComma(1234, ".0000", ".0012", "1.2340", "1,234.0", "1,234", "1,234", "1,234.00"); 468 | TestComma(12345, ".0000", ".0123", "12.3450", "12,345.0", "12,345", "12,345", "12,345.00"); 469 | TestComma(123456, ".0001", ".1235", "123.4560", "123,456.0", "123,456", "123,456", "123,456.00"); 470 | TestComma(1234567, ".0012", "1.2346", "1234.5670", "1,234,567.0", "1,234,567", "1,234,567", "1,234,567.00"); 471 | TestComma(12345678, ".0123", "12.3457", "12345.6780", "12,345,678.0", "12,345,678", "12,345,678", "12,345,678.00"); 472 | TestComma(123456789, ".1235", "123.4568", "123456.7890", "123,456,789.0", "123,456,789", "123,456,789", "123,456,789.00"); 473 | TestComma(1234567890, "1.2346", "1234.5679", "1234567.8900", "1,234,567,890.0", "1,234,567,890", "1,234,567,890", "1,234,567,890.00"); 474 | TestComma(12345678901, "12.3457", "12345.6789", "12345678.9010", "12,345,678,901.0", "12,345,678,901", "12,345,678,901", "12,345,678,901.00"); 475 | TestComma(123456789012, "123.4568", "123456.7890", "123456789.0120", "123,456,789,012.0", "123,456,789,012", "123,456,789,012", "123,456,789,012.00"); 476 | TestComma(4321, ".0000", ".0043", "4.3210", "4,321.0", "4,321", "4,321", "4,321.00"); 477 | TestComma(4321234, ".0043", "4.3212", "4321.2340", "4,321,234.0", "4,321,234", "4,321,234", "4,321,234.00"); 478 | 479 | } 480 | 481 | [TestMethod] 482 | public void TestThousandSeparator() 483 | { 484 | var actual = Format(1469.07, "0,000,000.00", CultureInfo.InvariantCulture); 485 | Assert.AreEqual("0,001,469.07", actual); 486 | } 487 | 488 | [TestMethod] 489 | public void TestThousandSeparatorCulture() 490 | { 491 | var actual = Format(1469.07, "0,000,000.00", new CultureInfo("da-DK")); 492 | Assert.AreEqual("0.001.469,07", actual); 493 | } 494 | 495 | [TestMethod] 496 | [TestCase("da-DK", "17-08-1978")] 497 | [TestCase("en-US", "17/08/1978")] 498 | [TestCase("bg-BG", "17.08.1978")] 499 | [TestCase("nb-NO", "17.08.1978")] 500 | public void TestDateSeparatorCulture(string cultureName, string expected) 501 | { 502 | var actual = Format(new DateTime(1978, 8, 17), "DD/MM/YYYY", new CultureInfo(cultureName)); 503 | Assert.AreEqual(expected, actual); 504 | } 505 | 506 | void TestValid(string format) 507 | { 508 | var to = new NumberFormat(format); 509 | Assert.IsTrue(to.IsValid, "Invalid format: {0}", format); 510 | } 511 | 512 | [TestMethod] 513 | public void TestValid() 514 | { 515 | TestValid("\" Excellent\""); 516 | TestValid("\" Fair\""); 517 | TestValid("\" Good\""); 518 | TestValid("\" Poor\""); 519 | TestValid("\" Very Good\""); 520 | TestValid("\"$\"#,##0"); 521 | TestValid("\"$\"#,##0.00"); 522 | TestValid("\"$\"#,##0.00_);[Red]\\(\"$\"#,##0.00\\)"); 523 | TestValid("\"$\"#,##0.00_);\\(\"$\"#,##0.00\\)"); 524 | TestValid("\"$\"#,##0;[Red]\\-\"$\"#,##0"); 525 | TestValid("\"$\"#,##0_);[Red]\\(\"$\"#,##0\\)"); 526 | TestValid("\"$\"#,##0_);\\(\"$\"#,##0\\)"); 527 | TestValid("\"Haha!\"\\ @\\ \"Yeah!\""); 528 | TestValid("\"TRUE\";\"TRUE\";\"FALSE\""); 529 | TestValid("\"True\";\"True\";\"False\";@"); 530 | TestValid("\"Years: \"0"); 531 | TestValid("\"Yes\";\"Yes\";\"No\";@"); 532 | TestValid("\"kl \"hh:mm:ss;@"); 533 | TestValid("\"£\"#,##0.00"); 534 | TestValid("\"£\"#,##0;[Red]\\-\"£\"#,##0"); 535 | TestValid("\"€\"#,##0.00"); 536 | TestValid("\"€\"\\ #,##0.00_-"); 537 | TestValid("\"上午/下午 \"hh\"時\"mm\"分\"ss\"秒 \""); 538 | TestValid("\"¥\"#,##0.00;\"¥\"\\-#,##0.00"); 539 | TestValid("#"); 540 | TestValid("# ?/?"); 541 | TestValid("# ??/??"); 542 | TestValid("#\" \"?/?"); 543 | TestValid("#\" \"??/??"); 544 | TestValid("#\"abded\"\\ ??/??"); 545 | TestValid("###0.00;-###0.00"); 546 | TestValid("###0;-###0"); 547 | TestValid("##0.0E+0"); 548 | TestValid("#,##0"); 549 | TestValid("#,##0 ;(#,##0)"); 550 | TestValid("#,##0 ;[Red](#,##0)"); 551 | TestValid("#,##0\"р.\";[Red]\\-#,##0\"р.\""); 552 | TestValid("#,##0.0"); 553 | TestValid("#,##0.00"); 554 | TestValid("#,##0.00 \"�\""); 555 | TestValid("#,##0.00 €;-#,##0.00 €"); 556 | TestValid("#,##0.00\"р.\";[Red]\\-#,##0.00\"р.\""); 557 | TestValid("#,##0.000"); 558 | TestValid("#,##0.0000"); 559 | TestValid("#,##0.00000"); 560 | TestValid("#,##0.000000"); 561 | TestValid("#,##0.0000000"); 562 | TestValid("#,##0.00000000"); 563 | TestValid("#,##0.000000000"); 564 | TestValid("#,##0.00000000;[Red]#,##0.00000000"); 565 | TestValid("#,##0.0000_ "); 566 | TestValid("#,##0.000_ "); 567 | TestValid("#,##0.000_);\\(#,##0.000\\)"); 568 | TestValid("#,##0.00;(#,##0.00)"); 569 | TestValid("#,##0.00;(#,##0.00);0.00"); 570 | TestValid("#,##0.00;[Red](#,##0.00)"); 571 | TestValid("#,##0.00;[Red]\\(#,##0.00\\)"); 572 | TestValid("#,##0.00;\\(#,##0.00\\)"); 573 | TestValid("#,##0.00[$₹-449]_);\\(#,##0.00[$₹-449]\\)"); 574 | TestValid("#,##0.00\\ \"р.\""); 575 | TestValid("#,##0.00\\ \"р.\";[Red]\\-#,##0.00\\ \"р.\""); 576 | TestValid("#,##0.00\\ [$€-407]"); 577 | TestValid("#,##0.00\\ [$€-40C]"); 578 | TestValid("#,##0.00_);\\(#,##0.00\\)"); 579 | TestValid("#,##0.00_р_.;[Red]\\-#,##0.00_р_."); 580 | TestValid("#,##0.00_р_.;\\-#,##0.00_р_."); 581 | TestValid("#,##0.0;[Red]#,##0.0"); 582 | TestValid("#,##0.0_ ;\\-#,##0.0\\ "); 583 | TestValid("#,##0.0_);[Red]\\(#,##0.0\\)"); 584 | TestValid("#,##0.0_);\\(#,##0.0\\)"); 585 | TestValid("#,##0;\\-#,##0;0"); 586 | TestValid("#,##0\\ \"р.\";[Red]\\-#,##0\\ \"р.\""); 587 | TestValid("#,##0\\ \"р.\";\\-#,##0\\ \"р.\""); 588 | TestValid("#,##0\\ ;[Red]\\(#,##0\\)"); 589 | TestValid("#,##0\\ ;\\(#,##0\\)"); 590 | TestValid("#,##0_ "); 591 | TestValid("#,##0_ ;[Red]\\-#,##0\\ "); 592 | TestValid("#,##0_);[Red]\\(#,##0\\)"); 593 | TestValid("#,##0_р_.;[Red]\\-#,##0_р_."); 594 | TestValid("#,##0_р_.;\\-#,##0_р_."); 595 | TestValid("#.0000,,"); 596 | TestValid("#0"); 597 | TestValid("#0.00"); 598 | TestValid("#0.0000"); 599 | TestValid("#\\ ?/10"); 600 | TestValid("#\\ ?/2"); 601 | TestValid("#\\ ?/4"); 602 | TestValid("#\\ ?/8"); 603 | TestValid("#\\ ?/?"); 604 | TestValid("#\\ ??/100"); 605 | TestValid("#\\ ??/100;[Red]\\(#\\ ??/16\\)"); 606 | TestValid("#\\ ??/16"); 607 | TestValid("#\\ ??/??"); 608 | TestValid("#\\ ??/?????????"); 609 | TestValid("#\\ ???/???"); 610 | TestValid("**\\ #,###,#00,000.00,**"); 611 | TestValid("0"); 612 | TestValid("0\"abde\".0\"??\"000E+00"); 613 | TestValid("0%"); 614 | TestValid("0.0"); 615 | TestValid("0.0%"); 616 | TestValid("0.00"); 617 | TestValid("0.00\"°\""); 618 | TestValid("0.00%"); 619 | TestValid("0.000"); 620 | TestValid("0.000%"); 621 | TestValid("0.0000"); 622 | TestValid("0.000000"); 623 | TestValid("0.00000000"); 624 | TestValid("0.000000000"); 625 | TestValid("0.000000000%"); 626 | TestValid("0.00000000000"); 627 | TestValid("0.000000000000000"); 628 | TestValid("0.00000000E+00"); 629 | TestValid("0.0000E+00"); 630 | TestValid("0.00;[Red]0.00"); 631 | TestValid("0.00E+00"); 632 | TestValid("0.00_);[Red]\\(0.00\\)"); 633 | TestValid("0.00_);\\(0.00\\)"); 634 | TestValid("0.0_ "); 635 | TestValid("00.00.00.000"); 636 | TestValid("00.000%"); 637 | TestValid("0000"); 638 | TestValid("00000"); 639 | TestValid("00000000"); 640 | TestValid("000000000"); 641 | TestValid("00000\\-0000"); 642 | TestValid("00000\\-00000"); 643 | TestValid("000\\-00\\-0000"); 644 | TestValid("0;[Red]0"); 645 | TestValid("0\\-00000\\-00000\\-0"); 646 | TestValid("0_);[Red]\\(0\\)"); 647 | TestValid("0_);\\(0\\)"); 648 | TestValid("@"); 649 | TestValid("A/P"); 650 | TestValid("AM/PM"); 651 | TestValid("AM/PMh\"時\"mm\"分\"ss\"秒\";@"); 652 | TestValid("D"); 653 | TestValid("DD"); 654 | TestValid("DD/MM/YY;@"); 655 | TestValid("DD/MM/YYYY"); 656 | TestValid("DD/MM/YYYY;@"); 657 | TestValid("DDD"); 658 | TestValid("DDDD"); 659 | TestValid("DDDD\", \"MMMM\\ DD\", \"YYYY"); 660 | TestValid("GENERAL"); 661 | TestValid("General"); 662 | TestValid("H"); 663 | TestValid("H:MM:SS\\ AM/PM"); 664 | TestValid("HH:MM"); 665 | TestValid("HH:MM:SS\\ AM/PM"); 666 | TestValid("HHM"); 667 | TestValid("HHMM"); 668 | TestValid("HH[MM]"); 669 | TestValid("HH[M]"); 670 | TestValid("M/D/YYYY"); 671 | TestValid("M/D/YYYY\\ H:MM"); 672 | TestValid("MM/DD/YY"); 673 | TestValid("S"); 674 | TestValid("SS"); 675 | TestValid("YY"); 676 | TestValid("YYM"); 677 | TestValid("YYMM"); 678 | TestValid("YYMMM"); 679 | TestValid("YYMMMM"); 680 | TestValid("YYMMMMM"); 681 | TestValid("YYYY"); 682 | TestValid("YYYY-MM-DD HH:MM:SS"); 683 | TestValid("YYYY\\-MM\\-DD"); 684 | TestValid("[$$-409]#,##0"); 685 | TestValid("[$$-409]#,##0.00"); 686 | TestValid("[$$-409]#,##0.00_);[Red]\\([$$-409]#,##0.00\\)"); 687 | TestValid("[$$-C09]#,##0.00"); 688 | TestValid("[$-100042A]h:mm:ss\\ AM/PM;@"); 689 | TestValid("[$-1010409]0.000%"); 690 | TestValid("[$-1010409]General"); 691 | TestValid("[$-1010409]d/m/yyyy\\ h:mm\\ AM/PM;@"); 692 | TestValid("[$-1010409]dddd, mmmm dd, yyyy"); 693 | TestValid("[$-1010409]m/d/yyyy"); 694 | TestValid("[$-1409]h:mm:ss\\ AM/PM;@"); 695 | TestValid("[$-2000000]h:mm:ss;@"); 696 | TestValid("[$-2010401]d/mm/yyyy\\ h:mm\\ AM/PM;@"); 697 | TestValid("[$-4000439]h:mm:ss\\ AM/PM;@"); 698 | TestValid("[$-4010439]d/m/yyyy\\ h:mm\\ AM/PM;@"); 699 | TestValid("[$-409]AM/PM\\ hh:mm:ss;@"); 700 | TestValid("[$-409]d/m/yyyy\\ hh:mm;@"); 701 | TestValid("[$-409]d\\-mmm;@"); 702 | TestValid("[$-409]d\\-mmm\\-yy;@"); 703 | TestValid("[$-409]d\\-mmm\\-yyyy;@"); 704 | TestValid("[$-409]dd/mm/yyyy\\ hh:mm;@"); 705 | TestValid("[$-409]dd\\-mmm\\-yy;@"); 706 | TestValid("[$-409]h:mm:ss\\ AM/PM;@"); 707 | TestValid("[$-409]h:mm\\ AM/PM;@"); 708 | TestValid("[$-409]m/d/yy\\ h:mm\\ AM/PM;@"); 709 | TestValid("[$-409]mmm\\-yy;@"); 710 | TestValid("[$-409]mmmm\\ d\\,\\ yyyy;@"); 711 | TestValid("[$-409]mmmm\\-yy;@"); 712 | TestValid("[$-409]mmmmm;@"); 713 | TestValid("[$-409]mmmmm\\-yy;@"); 714 | TestValid("[$-40E]h\\ \"óra\"\\ m\\ \"perckor\"\\ AM/PM;@"); 715 | TestValid("[$-412]AM/PM\\ h\"시\"\\ mm\"분\"\\ ss\"초\";@"); 716 | TestValid("[$-41C]h:mm:ss\\.AM/PM;@"); 717 | TestValid("[$-449]hh:mm:ss\\ AM/PM;@"); 718 | TestValid("[$-44E]hh:mm:ss\\ AM/PM;@"); 719 | TestValid("[$-44F]hh:mm:ss\\ AM/PM;@"); 720 | TestValid("[$-D000409]h:mm\\ AM/PM;@"); 721 | TestValid("[$-D010000]d/mm/yyyy\\ h:mm\\ \"น.\";@"); 722 | TestValid("[$-F400]h:mm:ss\\ AM/PM"); 723 | TestValid("[$-F800]dddd\\,\\ mmmm\\ dd\\,\\ yyyy"); 724 | TestValid("[$AUD]\\ #,##0.00"); 725 | TestValid("[$RD$-1C0A]#,##0.00;[Red]\\-[$RD$-1C0A]#,##0.00"); 726 | TestValid("[$SFr.-810]\\ #,##0.00_);[Red]\\([$SFr.-810]\\ #,##0.00\\)"); 727 | TestValid("[$£-809]#,##0.00;[Red][$£-809]#,##0.00"); 728 | TestValid("[$¥-411]#,##0.00"); 729 | TestValid("[$¥-804]#,##0.00"); 730 | TestValid("[<0]\"\";0%"); 731 | TestValid("[<=9999999]###\\-####;\\(###\\)\\ ###\\-####"); 732 | TestValid("[=0]?;#,##0.00"); 733 | TestValid("[=0]?;0%"); 734 | TestValid("[=0]?;[<4.16666666666667][hh]:mm:ss;[hh]:mm"); 735 | TestValid("[>999999]#,,\"M\";[>999]#,\"K\";#"); 736 | TestValid("[>999999]#.000,,\"M\";[>999]#.000,\"K\";#.000"); 737 | TestValid("[>=100000]0.000\\ \\\";[Red]0.000\\ \\<\\ \\>\\ \\\"\\ \\&\\ \\'\\ "); 738 | TestValid("[>=100000]0.000\\ \\<;[Red]0.000\\ \\>"); 739 | TestValid("[BLACK]@"); 740 | TestValid("[BLUE]GENERAL"); 741 | TestValid("[Black]@"); 742 | TestValid("[Blue]General"); 743 | TestValid("[CYAN]@"); 744 | TestValid("[Cyan]@"); 745 | TestValid("[DBNum1][$-804]AM/PMh\"时\"mm\"分\";@"); 746 | TestValid("[DBNum1][$-804]General"); 747 | TestValid("[DBNum1][$-804]h\"时\"mm\"分\";@"); 748 | TestValid("[ENG][$-1004]dddd\\,\\ d\\ mmmm\\,\\ yyyy;@"); 749 | TestValid("[ENG][$-101040D]d\\ mmmm\\ yyyy;@"); 750 | TestValid("[ENG][$-101042A]d\\ mmmm\\ yyyy;@"); 751 | TestValid("[ENG][$-140C]dddd\\ \"YeahWoo!\"\\ ddd\\ mmmm\\ yyyy;@"); 752 | TestValid("[ENG][$-2C0A]dddd\\ d\" de \"mmmm\" de \"yyyy;@"); 753 | TestValid("[ENG][$-402]dd\\ mmmm\\ yyyy\\ \"г.\";@"); 754 | TestValid("[ENG][$-403]dddd\\,\\ d\" / \"mmmm\" / \"yyyy;@"); 755 | TestValid("[ENG][$-405]d\\.\\ mmmm\\ yyyy;@"); 756 | TestValid("[ENG][$-408]d\\ mmmm\\ yyyy;@"); 757 | TestValid("[ENG][$-409]d\\-mmm;@"); 758 | TestValid("[ENG][$-409]d\\-mmm\\-yy;@"); 759 | TestValid("[ENG][$-409]d\\-mmm\\-yyyy;@"); 760 | TestValid("[ENG][$-409]dd\\-mmm\\-yy;@"); 761 | TestValid("[ENG][$-409]mmm\\-yy;@"); 762 | TestValid("[ENG][$-409]mmmm\\ d\\,\\ yyyy;@"); 763 | TestValid("[ENG][$-409]mmmm\\-yy;@"); 764 | TestValid("[ENG][$-40B]d\\.\\ mmmm\\t\\a\\ yyyy;@"); 765 | TestValid("[ENG][$-40C]d/mmm/yyyy;@"); 766 | TestValid("[ENG][$-40E]yyyy/\\ mmmm\\ d\\.;@"); 767 | TestValid("[ENG][$-40F]dd\\.\\ mmmm\\ yyyy;@"); 768 | TestValid("[ENG][$-410]d\\ mmmm\\ yyyy;@"); 769 | TestValid("[ENG][$-415]d\\ mmmm\\ yyyy;@"); 770 | TestValid("[ENG][$-416]d\\ \\ mmmm\\,\\ yyyy;@"); 771 | TestValid("[ENG][$-418]d\\ mmmm\\ yyyy;@"); 772 | TestValid("[ENG][$-41A]d\\.\\ mmmm\\ yyyy\\.;@"); 773 | TestValid("[ENG][$-41B]d\\.\\ mmmm\\ yyyy;@"); 774 | TestValid("[ENG][$-41D]\"den \"\\ d\\ mmmm\\ yyyy;@"); 775 | TestValid("[ENG][$-420]dddd\\,\\ dd\\ mmmm\\,\\ yyyy;@"); 776 | TestValid("[ENG][$-421]dd\\ mmmm\\ yyyy;@"); 777 | TestValid("[ENG][$-424]dddd\\,\\ d\\.\\ mmmm\\ yyyy;@"); 778 | TestValid("[ENG][$-425]dddd\\,\\ d\\.\\ mmmm\\ yyyy;@"); 779 | TestValid("[ENG][$-426]dddd\\,\\ yyyy\". gada \"d\\.\\ mmmm;@"); 780 | TestValid("[ENG][$-427]yyyy\\ \"m.\"\\ mmmm\\ d\\ \"d.\";@"); 781 | TestValid("[ENG][$-42B]dddd\\,\\ d\\ mmmm\\ yyyy;@"); 782 | TestValid("[ENG][$-42C]d\\ mmmm\\ yyyy;@"); 783 | TestValid("[ENG][$-42D]yyyy\"(e)ko\"\\ mmmm\"ren\"\\ d\"a\";@"); 784 | TestValid("[ENG][$-42F]dddd\\,\\ dd\\ mmmm\\ yyyy;@"); 785 | TestValid("[ENG][$-437]yyyy\\ \\წ\\ლ\\ი\\ს\\ dd\\ mm\\,\\ dddd;@"); 786 | TestValid("[ENG][$-438]d\\.\\ mmmm\\ yyyy;@"); 787 | TestValid("[ENG][$-43F]d\\ mmmm\\ yyyy\\ \"ж.\";@"); 788 | TestValid("[ENG][$-444]d\\ mmmm\\ yyyy;@"); 789 | TestValid("[ENG][$-449]dd\\ mmmm\\ yyyy;@"); 790 | TestValid("[ENG][$-44E]d\\ mmmm\\ yyyy;@"); 791 | TestValid("[ENG][$-44F]dd\\ mmmm\\ yyyy\\ dddd;@"); 792 | TestValid("[ENG][$-457]dd\\ mmmm\\ yyyy;@"); 793 | TestValid("[ENG][$-813]dddd\\ d\\ mmmm\\ yyyy;@"); 794 | TestValid("[ENG][$-81A]dddd\\,\\ d\\.\\ mmmm\\ yyyy;@"); 795 | TestValid("[ENG][$-82C]d\\ mmmm\\ yyyy;@"); 796 | TestValid("[ENG][$-843]yyyy\\ \"й\"\"и\"\"л\"\\ d/mmmm;@"); 797 | TestValid("[ENG][$-C07]dddd\\,\\ dd\\.\\ mmmm\\ yyyy;@"); 798 | TestValid("[ENG][$-FC19]yyyy\\,\\ dd\\ mmmm;@"); 799 | TestValid("[ENG][$-FC22]d\\ mmmm\\ yyyy\" р.\";@"); 800 | TestValid("[ENG][$-FC23]d\\ mmmm\\ yyyy;@"); 801 | TestValid("[GREEN]#,###"); 802 | TestValid("[Green]#,###"); 803 | TestValid("[HH]"); 804 | TestValid("[HIJ][$-2060401]d/mm/yyyy\\ h:mm\\ AM/PM;@"); 805 | TestValid("[HIJ][$-2060401]d\\ mmmm\\ yyyy;@"); 806 | TestValid("[H]"); 807 | TestValid("[JPN][$-411]gggyy\"年\"m\"月\"d\"日\"\\ dddd;@"); 808 | TestValid("[MAGENTA]0.00"); 809 | TestValid("[Magenta]0.00"); 810 | TestValid("[RED]#.##"); 811 | TestValid("[Red]#.##"); 812 | TestValid("[Red][<-25]General;[Blue][>25]General;[Green]General;[Yellow]General\\ "); 813 | TestValid("[Red][<=-25]General;[Blue][>=25]General;[Green]General;[Yellow]General"); 814 | TestValid("[Red][<>50]General;[Blue]000"); 815 | TestValid("[Red][=50]General;[Blue]000"); 816 | TestValid("[SS]"); 817 | TestValid("[S]"); 818 | TestValid("[TWN][DBNum1][$-404]y\"年\"m\"月\"d\"日\";@"); 819 | TestValid("[WHITE]0.0"); 820 | TestValid("[White]0.0"); 821 | TestValid("[YELLOW]@"); 822 | TestValid("[Yellow]@"); 823 | TestValid("[h]"); 824 | TestValid("[h]:mm:ss"); 825 | TestValid("[h]:mm:ss;@"); 826 | TestValid("[h]\\.mm\" Uhr \";@"); 827 | TestValid("[hh]"); 828 | TestValid("[s]"); 829 | TestValid("[ss]"); 830 | TestValid("\\#\\r\\e\\c"); 831 | TestValid("\\$#,##0_);[Red]\"($\"#,##0\\)"); 832 | TestValid("\\$0.00"); 833 | TestValid("\\C\\O\\B\\ \\o\\n\\ @"); 834 | TestValid("\\C\\R\\O\\N\\T\\A\\B\\ \\o\\n\\ @"); 835 | TestValid("\\R\\e\\s\\u\\l\\t\\ \\o\\n\\ @"); 836 | TestValid("\\S\\Q\\L\\ \\:\\ @"); 837 | TestValid("\\S\\Q\\L\\ \\R\\e\\q\\u\\e\\s\\t\\ \\f\\o\\r\\ @"); 838 | TestValid("\\c\\c\\c?????0\"aaaa\"0\"bbbb\"000000.00%"); 839 | TestValid("\\u\\n\\t\\i\\l\\ h:mm;@"); 840 | TestValid("_ \"¥\"* #,##0.00_ \"Positive\";_ \"¥\"* \\-#,##0.00_ ;_ \"¥\"* \"-\"??_ \"Negtive\";_ @_ \\ \"Zero\""); 841 | TestValid("_ * #,##0.00_)[$﷼-429]_ ;_ * \\(#,##0.00\\)[$﷼-429]_ ;_ * \"-\"??_)[$﷼-429]_ ;_ @_ "); 842 | TestValid("_ * #,##0_ ;_ * \\-#,##0_ ;[Red]_ * \"-\"_ ;_ @_ "); 843 | TestValid("_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)"); 844 | TestValid("_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"??_);_(@_)"); 845 | TestValid("_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)"); 846 | TestValid("_(* #,##0.0000_);_(* \\(#,##0.0000\\);_(* \"-\"??_);_(@_)"); 847 | TestValid("_(* #,##0.000_);_(* \\(#,##0.000\\);_(* \"-\"??_);_(@_)"); 848 | TestValid("_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)"); 849 | TestValid("_(* #,##0.0_);_(* \\(#,##0.0\\);_(* \"-\"??_);_(@_)"); 850 | TestValid("_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"??_);_(@_)"); 851 | TestValid("_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)"); 852 | TestValid("_([$ANG]\\ * #,##0.0_);_([$ANG]\\ * \\(#,##0.0\\);_([$ANG]\\ * \"-\"?_);_(@_)"); 853 | TestValid("_-\"€\"\\ * #,##0.00_-;_-\"€\"\\ * #,##0.00\\-;_-\"€\"\\ * \"-\"??_-;_-@_-"); 854 | TestValid("_-* #,##0.00\" TL\"_-;\\-* #,##0.00\" TL\"_-;_-* \\-??\" TL\"_-;_-@_-"); 855 | TestValid("_-* #,##0.00\" €\"_-;\\-* #,##0.00\" €\"_-;_-* \\-??\" €\"_-;_-@_-"); 856 | TestValid("_-* #,##0.00\\ \"р.\"_-;\\-* #,##0.00\\ \"р.\"_-;_-* \"-\"??\\ \"р.\"_-;_-@_-"); 857 | TestValid("_-* #,##0.00\\ \"€\"_-;\\-* #,##0.00\\ \"€\"_-;_-* \"-\"??\\ \"€\"_-;_-@_-"); 858 | TestValid("_-* #,##0.00\\ [$€-407]_-;\\-* #,##0.00\\ [$€-407]_-;_-* \\-??\\ [$€-407]_-;_-@_-"); 859 | TestValid("_-* #,##0.0\\ _F_-;\\-* #,##0.0\\ _F_-;_-* \"-\"??\\ _F_-;_-@_-"); 860 | TestValid("_-* #,##0\\ \"€\"_-;\\-* #,##0\\ \"€\"_-;_-* \"-\"\\ \"€\"_-;_-@_-"); 861 | TestValid("_-* #,##0_-;\\-* #,##0_-;_-* \"-\"??_-;_-@_-"); 862 | TestValid("_-\\$* #,##0.0_ ;_-\\$* \\-#,##0.0\\ ;_-\\$* \"-\"?_ ;_-@_ "); 863 | TestValid("d"); 864 | TestValid("d-mmm"); 865 | TestValid("d-mmm-yy"); 866 | TestValid("d/m"); 867 | TestValid("d/m/yy;@"); 868 | TestValid("d/m/yyyy;@"); 869 | TestValid("d/mm/yy;@"); 870 | TestValid("d/mm/yyyy;@"); 871 | TestValid("d\\-mmm"); 872 | TestValid("d\\-mmm\\-yyyy"); 873 | TestValid("dd"); 874 | TestValid("dd\"-\"mmm\"-\"yyyy"); 875 | TestValid("dd/m/yyyy"); 876 | TestValid("dd/mm/yy"); 877 | TestValid("dd/mm/yy;@"); 878 | TestValid("dd/mm/yy\\ hh:mm"); 879 | TestValid("dd/mm/yyyy"); 880 | TestValid("dd/mm/yyyy\\ hh:mm:ss"); 881 | TestValid("dd/mmm"); 882 | TestValid("dd\\-mm\\-yy"); 883 | TestValid("dd\\-mmm\\-yy"); 884 | TestValid("dd\\-mmm\\-yyyy\\ hh:mm:ss.000"); 885 | TestValid("dd\\/mm\\/yy"); 886 | TestValid("dd\\/mm\\/yyyy"); 887 | TestValid("ddd"); 888 | TestValid("dddd"); 889 | TestValid("dddd, mmmm dd, yyyy"); 890 | TestValid("h"); 891 | TestValid("h\"时\"mm\"分\"ss\"秒\";@"); 892 | TestValid("h\"時\"mm\"分\"ss\"秒\";@"); 893 | TestValid("h:mm"); 894 | TestValid("h:mm AM/PM"); 895 | TestValid("h:mm:ss"); 896 | TestValid("h:mm:ss AM/PM"); 897 | TestValid("h:mm:ss;@"); 898 | TestValid("h:mm;@"); 899 | TestValid("h\\.mm\" Uhr \";@"); 900 | TestValid("h\\.mm\" h\";@"); 901 | TestValid("h\\.mm\" u.\";@"); 902 | TestValid("hh\":\"mm AM/PM"); 903 | TestValid("hh:mm:ss"); 904 | TestValid("hh:mm:ss\\ AM/PM"); 905 | TestValid("hh\\.mm\" h\";@"); 906 | TestValid("hhm"); 907 | TestValid("hhmm"); 908 | TestValid("m\"月\"d\"日\""); 909 | TestValid("m/d/yy"); 910 | TestValid("m/d/yy h:mm"); 911 | TestValid("m/d/yy;@"); 912 | TestValid("m/d/yy\\ h:mm"); 913 | TestValid("m/d/yy\\ h:mm;@"); 914 | TestValid("m/d/yyyy"); 915 | TestValid("m/d/yyyy;@"); 916 | TestValid("m/d/yyyy\\ h:mm:ss;@"); 917 | TestValid("m/d;@"); 918 | TestValid("m\\/d\\/yyyy"); 919 | TestValid("mm/dd"); 920 | TestValid("mm/dd/yy"); 921 | TestValid("mm/dd/yy;@"); 922 | TestValid("mm/dd/yyyy"); 923 | TestValid("mm:ss"); 924 | TestValid("mm:ss.0;@"); 925 | TestValid("mmm d, yyyy"); 926 | TestValid("mmm\" \"d\", \"yyyy"); 927 | TestValid("mmm-yy"); 928 | TestValid("mmm-yy;@"); 929 | TestValid("mmm/yy"); 930 | TestValid("mmm\\-yy"); 931 | TestValid("mmm\\-yy;@"); 932 | TestValid("mmm\\-yyyy"); 933 | TestValid("mmmm\\ d\\,\\ yyyy"); 934 | TestValid("mmmm\\ yyyy"); 935 | TestValid("mmss.0"); 936 | TestValid("s"); 937 | TestValid("ss"); 938 | TestValid("yy"); 939 | TestValid("yy/mm/dd"); 940 | TestValid("yy\\.mm\\.dd"); 941 | TestValid("yym"); 942 | TestValid("yymm"); 943 | TestValid("yymmm"); 944 | TestValid("yymmmm"); 945 | TestValid("yymmmmm"); 946 | TestValid("yyyy"); 947 | TestValid("yyyy\"년\"\\ m\"월\"\\ d\"일\";@"); 948 | TestValid("yyyy-m-d h:mm AM/PM"); 949 | TestValid("yyyy-mm-dd"); 950 | TestValid("yyyy/mm/dd"); 951 | TestValid("yyyy\\-m\\-d\\ hh:mm:ss"); 952 | TestValid("yyyy\\-mm\\-dd"); 953 | TestValid("yyyy\\-mm\\-dd;@"); 954 | TestValid("yyyy\\-mm\\-dd\\ h:mm"); 955 | TestValid("yyyy\\-mm\\-dd\\Thh:mm"); 956 | TestValid("yyyy\\-mm\\-dd\\Thhmmss.000"); 957 | } 958 | 959 | [TestCase(null)] 960 | [TestCase("")] 961 | [TestCase("General")] 962 | public void TestDefaultFormatString(string formatString) 963 | { 964 | string result; 965 | 966 | result = Format(1234.56, formatString, CultureInfo.InvariantCulture); 967 | Assert.AreEqual("1234.56", result); 968 | 969 | result = Format(Double.MaxValue, formatString, CultureInfo.InvariantCulture); 970 | Assert.AreEqual("1.79769313486232E+308", result); 971 | 972 | result = Format(float.MaxValue, formatString, CultureInfo.InvariantCulture); 973 | Assert.AreEqual("3.402823E+38", result); 974 | 975 | result = Format(new DateTime(2017, 10, 28), formatString, new CultureInfo("sv-se")); 976 | Assert.AreEqual("2017-10-28 00:00:00", result); 977 | 978 | result = Format(new DateTime(2017, 10, 28), formatString, CultureInfo.InvariantCulture); 979 | Assert.AreEqual("10/28/2017 00:00:00", result); 980 | } 981 | 982 | [TestMethod] 983 | public void TestCurrency() 984 | { 985 | Test(1234.56, "[$€-1809]# ##0.00", "€1 234.56"); 986 | Test(1234.56, "#,##0.00 [$EUR]", "1,234.56 EUR"); 987 | } 988 | } 989 | } 990 | -------------------------------------------------------------------------------- /test/ExcelNumberFormat.Tests/ExcelNumberFormat.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2;netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------