Skip to content

Commit 1f2deba

Browse files
author
AndrewMorgan1
committed
Added Culture-aware parsing (e.g., commas vs dots for decimals)
1 parent 4f6486f commit 1f2deba

File tree

2 files changed

+107
-22
lines changed

2 files changed

+107
-22
lines changed

src/ByteFlow/ByteFlow/HumanBytesExtensions.cs

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,31 @@ namespace ByteFlow
66
{
77
/// <summary>
88
/// Provides extension methods for converting between raw byte counts and human-readable sizes.
9+
/// Supports both IEC (binary: KiB, MiB, GiB) and SI (decimal: KB, MB, GB) unit standards,
10+
/// and allows culture-aware formatting and parsing.
911
/// </summary>
1012
public static class HumanBytesExtensions
1113
{
12-
private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB" };
13-
1414
/// <summary>
15-
/// Converts a number of bytes into a human-readable string
16-
/// using either SI (decimal: KB, MB, GB) or IEC (binary: KiB, MiB, GiB) units.
15+
/// Converts a number of bytes into a human-readable string using either
16+
/// SI (decimal: KB, MB, GB) or IEC (binary: KiB, MiB, GiB) units.
1717
/// </summary>
1818
/// <param name="bytes">The size in bytes.</param>
1919
/// <param name="decimalPlaces">Number of decimal places to display.</param>
2020
/// <param name="standard">Whether to use SI (base 1000) or IEC (base 1024) units.</param>
21-
/// <returns>A formatted string such as "1.23 MB" (SI) or "1.18 MiB" (IEC).</returns>
22-
public static string ToHumanBytes(this long bytes, int decimalPlaces = 2, UnitStandard standard = UnitStandard.IEC)
21+
/// <param name="formatProvider">
22+
/// The culture to use for formatting (e.g. decimal separator).
23+
/// Defaults to <see cref="CultureInfo.InvariantCulture"/>.
24+
/// </param>
25+
/// <returns>
26+
/// A formatted string such as "1.23 MB" (SI, en-US) or "1,23 MB" (SI, de-DE).
27+
/// </returns>
28+
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="bytes"/> is negative.</exception>
29+
public static string ToHumanBytes(
30+
this long bytes,
31+
int decimalPlaces = 2,
32+
UnitStandard standard = UnitStandard.IEC,
33+
IFormatProvider formatProvider = null)
2334
{
2435
if (bytes < 0)
2536
throw new ArgumentOutOfRangeException(nameof(bytes), "Value must be non-negative.");
@@ -40,18 +51,28 @@ public static string ToHumanBytes(this long bytes, int decimalPlaces = 2, UnitSt
4051
}
4152

4253
double adjusted = bytes / suffixes[mag].Factor;
43-
return string.Format(CultureInfo.InvariantCulture,
44-
$"{{0:F{decimalPlaces}}} {{1}}", adjusted, suffixes[mag].Symbol);
54+
string number = adjusted.ToString($"F{decimalPlaces}", formatProvider ?? CultureInfo.InvariantCulture);
55+
56+
return $"{number} {suffixes[mag].Symbol}";
4557
}
4658

4759
/// <summary>
48-
/// Parses a human-readable size string (e.g. "2.5 GB" or "2.5 GiB")
49-
/// back into bytes, using either SI (decimal) or IEC (binary) interpretation.
60+
/// Parses a human-readable size string (e.g. "2.5 GB" or "2,5 GiB") back into bytes,
61+
/// using either SI (decimal) or IEC (binary) interpretation.
5062
/// </summary>
51-
/// <param name="input">The input string, e.g. "1 KB", "1 KiB", "1 MB".</param>
63+
/// <param name="input">The input string, e.g. "1 KB", "1 KiB", "2.5 MB".</param>
5264
/// <param name="standard">Whether to interpret units as SI (base 1000) or IEC (base 1024).</param>
65+
/// <param name="formatProvider">
66+
/// The culture to use for parsing (e.g. decimal separator).
67+
/// Defaults to <see cref="CultureInfo.InvariantCulture"/>.
68+
/// </param>
5369
/// <returns>The size in bytes.</returns>
54-
public static long ToBytes(this string input, UnitStandard standard = UnitStandard.IEC)
70+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="input"/> is null or whitespace.</exception>
71+
/// <exception cref="FormatException">Thrown if the input string cannot be parsed.</exception>
72+
public static long ToBytes(
73+
this string input,
74+
UnitStandard standard = UnitStandard.IEC,
75+
IFormatProvider formatProvider = null)
5576
{
5677
if (string.IsNullOrWhiteSpace(input))
5778
throw new ArgumentNullException(nameof(input));
@@ -64,29 +85,61 @@ public static long ToBytes(this string input, UnitStandard standard = UnitStanda
6485
if (input.EndsWith(symbol, StringComparison.OrdinalIgnoreCase))
6586
{
6687
var numberPart = input.Substring(0, input.Length - symbol.Length).Trim();
67-
if (!double.TryParse(numberPart, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
88+
if (!double.TryParse(
89+
numberPart,
90+
NumberStyles.Float,
91+
formatProvider ?? CultureInfo.InvariantCulture,
92+
out var value))
6893
throw new FormatException($"Invalid number format: {numberPart}");
94+
6995
return (long)(value * factor);
7096
}
7197
}
7298

7399
throw new FormatException($"Invalid size suffix in: {input}");
74100
}
75101

102+
/// <summary>
103+
/// Safely parses a human-readable string into bytes.
104+
/// Returns <c>true</c> if parsing succeeds; otherwise <c>false</c>.
105+
/// </summary>
106+
/// <param name="input">The input string.</param>
107+
/// <param name="result">The parsed byte value if successful, or 0 otherwise.</param>
108+
/// <returns><c>true</c> if parsing was successful; otherwise <c>false</c>.</returns>
109+
public static bool TryParseHumanBytes(this string input, out long result)
110+
{
111+
try
112+
{
113+
result = input.ToBytes();
114+
return true;
115+
}
116+
catch
117+
{
118+
result = 0;
119+
return false;
120+
}
121+
}
122+
76123
/// <summary>
77124
/// Safely parses a human-readable string into bytes, using either SI (decimal) or IEC (binary) units.
78125
/// </summary>
79126
/// <param name="input">The input string (e.g. "1 KB", "1 KiB", "2.5 MB").</param>
80-
/// <param name="result">The parsed byte value if successful, or 0 if parsing fails.</param>
127+
/// <param name="result">The parsed byte value if successful, or 0 otherwise.</param>
81128
/// <param name="standard">Whether to interpret units as SI (base 1000) or IEC (base 1024).</param>
82-
/// <returns>
83-
/// <c>true</c> if parsing was successful; otherwise <c>false</c>.
84-
/// </returns>
85-
public static bool TryParseHumanBytes(this string input, out long result, UnitStandard standard = UnitStandard.IEC)
129+
/// <param name="formatProvider">
130+
/// The culture to use for parsing (e.g. decimal separator).
131+
/// Defaults to <see cref="CultureInfo.InvariantCulture"/>.
132+
/// </param>
133+
/// <returns><c>true</c> if parsing was successful; otherwise <c>false</c>.</returns>
134+
public static bool TryParseHumanBytes(
135+
this string input,
136+
out long result,
137+
UnitStandard standard = UnitStandard.IEC,
138+
IFormatProvider formatProvider = null)
86139
{
87140
try
88141
{
89-
result = input.ToBytes(standard);
142+
result = input.ToBytes(standard, formatProvider);
90143
return true;
91144
}
92145
catch
@@ -96,6 +149,8 @@ public static bool TryParseHumanBytes(this string input, out long result, UnitSt
96149
}
97150
}
98151

152+
// --- Unit suffix definitions ---
153+
99154
private static readonly (string Symbol, double Factor)[] SiSuffixes =
100155
{
101156
("B", 1d),

tests/ByteFlow.Tests/HumanBytesExtensionsTests.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace ByteFlow.Tests
1+
using System.Globalization;
2+
3+
namespace ByteFlow.Tests
24
{
35
public class HumanBytesExtensionsTests
46
{
@@ -26,6 +28,14 @@ public void ToHumanBytes_ShouldRespectDecimalPlaces()
2628
Assert.Equal("1.235 MB", result); // 1234567 / 1e6 = 1.234567
2729
}
2830

31+
[Fact]
32+
public void ToHumanBytes_ShouldRespectCulture()
33+
{
34+
var de = new CultureInfo("de-DE"); // comma decimal separator
35+
string result = 1234567L.ToHumanBytes(2, UnitStandard.SI, de);
36+
Assert.Equal("1,23 MB", result);
37+
}
38+
2939
[Fact]
3040
public void ToHumanBytes_ShouldThrowOnNegativeInput()
3141
{
@@ -43,7 +53,7 @@ public void ToHumanBytes_ShouldHandleZero()
4353
public void ToHumanBytes_ShouldHandleLongMaxValue()
4454
{
4555
string result = long.MaxValue.ToHumanBytes();
46-
Assert.Contains("PiB", result); // should be expressed in largest unit
56+
Assert.Contains("PiB", result); // expressed in largest IEC unit
4757
}
4858

4959
// --- Parsing (ToBytes) ---
@@ -64,6 +74,17 @@ public void ToBytes_ShouldParseCorrectly(string input, long expected, UnitStanda
6474
Assert.Equal(expected, result);
6575
}
6676

77+
[Theory]
78+
[InlineData("1,5 MB", 1500000, "de-DE", UnitStandard.SI)]
79+
[InlineData("1.5 MB", 1500000, "en-US", UnitStandard.SI)]
80+
[InlineData("2,5 GiB", 2684354560, "de-DE", UnitStandard.IEC)]
81+
public void ToBytes_ShouldParseAccordingToCulture(string input, long expected, string cultureName, UnitStandard standard)
82+
{
83+
var culture = new CultureInfo(cultureName);
84+
long result = input.ToBytes(standard, culture);
85+
Assert.Equal(expected, result);
86+
}
87+
6788
[Fact]
6889
public void ToBytes_InvalidString_ShouldThrow()
6990
{
@@ -133,6 +154,16 @@ public void TryParseHumanBytes_ValidInput_ShouldReturnTrue(string input, long ex
133154
Assert.Equal(expected, result);
134155
}
135156

157+
[Fact]
158+
public void TryParseHumanBytes_ShouldRespectCulture()
159+
{
160+
var de = new CultureInfo("de-DE");
161+
bool success = "2,5 MiB".TryParseHumanBytes(out long result, UnitStandard.IEC, de);
162+
163+
Assert.True(success);
164+
Assert.Equal((long)(2.5 * 1024 * 1024), result);
165+
}
166+
136167
[Theory]
137168
[InlineData("1 XB", UnitStandard.SI)]
138169
[InlineData("ten MB", UnitStandard.SI)]
@@ -158,7 +189,6 @@ public void RoundTrip_BytesToHumanAndBack_ShouldBeConsistent(long original, Unit
158189
{
159190
string human = original.ToHumanBytes(2, standard);
160191
long parsed = human.ToBytes(standard);
161-
162192
Assert.Equal(original, parsed);
163193
}
164194
}

0 commit comments

Comments
 (0)