├── .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 | [](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 |
--------------------------------------------------------------------------------