Skip to content

Commit 7353c4b

Browse files
committed
Merge pull request #81 from maherkassim/feature/improve-parsing
Improve parsing of unit quantities. Not all the cases below are implemented. For instance it happily parses unit quantity pairs with no space or delimiters between them. This is left for a future improvement. --- // Length.Parse() examples // Multiple quantities are added together // No invalid text is allowed // Allowed delimiters between quantities: 1 comma, 1 word "and", N whitespace, or any combination // Valid strings 1m => 1 meter // single quantity 1m 1" => (1 meter + 1 inch) // valid, but unconventional 1'1" => (1 foot + 1 inch) // special case for feet/inches, allow no space 1dm³ 1L => 2 liters // 2 quantities, separated by space 1m 1m 1m => 3m // 3 quantities, separated by space 1m 1m ... 1m, N times => N m // N quantities, separated by space 1m and 1cm => 1.011m // 2 quantities, separated by "and"-word and whitespace 1m,1cm,1mm => 1.011m // 3 quantities, separated by comma 1m, 1cm, 1mm => 1.011m // 3 quantities, separated by comma 1m, 1cm and 1mm => 1.011m // 3 quantities, separated by a mix of delimiters 1m, 1cm, and 1mm => 1.011m // 3 quantities, separated by a mix of delimiters 1m and 1cm => 1.011m // 3 quantities, separated by a mix of delimiters 1m , and, 1cm => 1.011m // 3 quantities, separated by a mix of delimiters // Invalid strings, throws exception 1m1cm // 2 quantities, no space 1m monkey 1cm // invalid word 1'' // invalid unit 1mmm // invalid unit 1'1"2" // only 2 quantities of exactly foot and inch is supported without space 1"1' // only 2 quantities of exactly foot and inch is supported without space
2 parents d585e55 + 1f1b8c7 commit 7353c4b

32 files changed

+1819
-964
lines changed

UnitsNet.Tests/CustomCode/ParseTests.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2020
// THE SOFTWARE.
2121

22-
using System;
2322
using System.Globalization;
2423
using NUnit.Framework;
2524
using UnitsNet.Units;
@@ -36,14 +35,14 @@ namespace UnitsNet.Tests.CustomCode
3635
[TestFixture]
3736
public class ParseTests
3837
{
39-
[TestCase("1km", Result=1000)]
38+
[TestCase("1km", Result = 1000)]
4039
[TestCase("1 km", Result = 1000)]
4140
[TestCase("1e-3 km", Result = 1)]
4241
[TestCase("5.5 m", Result = 5.5)]
4342
[TestCase("500,005 m", Result = 500005)]
4443
[TestCase(null, ExpectedExceptionName = "System.ArgumentNullException")]
4544
[TestCase("1", ExpectedExceptionName = "System.ArgumentException")]
46-
[TestCase("km", ExpectedExceptionName = "System.ArgumentException")]
45+
[TestCase("km", ExpectedExceptionName = "UnitsNet.UnitsNetException")]
4746
[TestCase("1 kg", ExpectedExceptionName = "UnitsNet.UnitsNetException")]
4847
public double ParseLengthToMetersUsEnglish(string s)
4948
{
@@ -52,6 +51,21 @@ public double ParseLengthToMetersUsEnglish(string s)
5251
return Length.Parse(s, usEnglish).Meters;
5352
}
5453

54+
[TestCase("1 ft 1 in", Result = 13)]
55+
[TestCase("1ft 1in", Result = 13)]
56+
[TestCase("1' 1\"", Result = 13)]
57+
[TestCase("1'1\"", Result = 13)]
58+
[TestCase("1ft1in", Result = 13)]
59+
[TestCase("1ft and 1in", Result = 13)]
60+
[TestCase("1ft monkey 1in", ExpectedExceptionName = "UnitsNet.UnitsNetException")]
61+
[TestCase("1ft 1invalid", ExpectedExceptionName = "UnitsNet.UnitsNetException")]
62+
public double ParseImperialLengthInchesUsEnglish(string s)
63+
{
64+
var usEnglish = CultureInfo.GetCultureInfo("en-US");
65+
66+
return Length.Parse(s, usEnglish).Inches;
67+
}
68+
5569
/// <exception cref="UnitsNetException">Error parsing string.</exception>
5670
[TestCase("5.5 m", Result = 5.5)]
5771
[TestCase("500 005 m", Result = 500005)]

UnitsNet/GeneratedCode/UnitClasses/Acceleration.g.cs

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// THE SOFTWARE.
2121

2222
using System;
23+
using System.Collections.Generic;
2324
using System.Globalization;
2425
using System.Text.RegularExpressions;
2526
using System.Linq;
@@ -232,17 +233,16 @@ public double As(AccelerationUnit unit)
232233
#region Parsing
233234

234235
/// <summary>
235-
/// Parse a string of the format "&lt;quantity&gt; &lt;unit&gt;".
236+
/// Parse a string with one or two quantities of the format "&lt;quantity&gt; &lt;unit&gt;".
236237
/// </summary>
237238
/// <example>
238239
/// Length.Parse("5.5 m", new CultureInfo("en-US"));
239240
/// </example>
240241
/// <exception cref="ArgumentNullException">The value of 'str' cannot be null. </exception>
241242
/// <exception cref="ArgumentException">
242-
/// Expected 2 words. Input string needs to be in the format "&lt;quantity&gt; &lt;unit
243-
/// &gt;".
243+
/// Expected string to have one or two pairs of quantity and unit in the format
244+
/// "&lt;quantity&gt; &lt;unit&gt;". Eg. "5.5 m" or "1ft 2in"
244245
/// </exception>
245-
/// <exception cref="UnitsNetException">Error parsing string.</exception>
246246
public static Acceleration Parse(string str, IFormatProvider formatProvider = null)
247247
{
248248
if (str == null) throw new ArgumentNullException("str");
@@ -254,41 +254,70 @@ public static Acceleration Parse(string str, IFormatProvider formatProvider = nu
254254
var numRegex = string.Format(@"[\d., {0}{1}]*\d", // allows digits, dots, commas, and spaces in the quantity (must end in digit)
255255
numFormat.NumberGroupSeparator, // adds provided (or current) culture's group separator
256256
numFormat.NumberDecimalSeparator); // adds provided (or current) culture's decimal separator
257-
var regexString = string.Format("(?<value>[-+]?{0}{1}{2}{3}",
258-
numRegex, // capture base (integral) Quantity value
259-
@"(?:[eE][-+]?\d+)?)", // capture exponential (if any), end of Quantity capturing
260-
@"\s?", // ignore whitespace (allows both "1kg", "1 kg")
261-
@"(?<unit>\S+)"); // capture Unit (non-whitespace) input
262-
263-
var regex = new Regex(regexString);
264-
GroupCollection groups = regex.Match(str.Trim()).Groups;
265-
266-
var valueString = groups["value"].Value;
267-
var unitString = groups["unit"].Value;
268-
269-
if (valueString == "" || unitString == "")
257+
var exponentialRegex = @"(?:[eE][-+]?\d+)?)";
258+
var regexString = string.Format(@"(?:\s*(?<value>[-+]?{0}{1}{2}{3})?{4}{5}",
259+
numRegex, // capture base (integral) Quantity value
260+
exponentialRegex, // capture exponential (if any), end of Quantity capturing
261+
@"\s?", // ignore whitespace (allows both "1kg", "1 kg")
262+
@"(?<unit>[^\s\d,]+)", // capture Unit (non-whitespace) input
263+
@"(and)?,?", // allow "and" & "," separators between quantities
264+
@"(?<invalid>[a-z]*)?"); // capture invalid input
265+
266+
var quantities = ParseWithRegex(regexString, str, formatProvider);
267+
if (quantities.Count == 0)
270268
{
271-
var ex = new ArgumentException(
272-
"Expected valid quantity and unit. Input string needs to be in the format \"<quantity><unit> or <quantity> <unit>\".", "str");
273-
ex.Data["input"] = str;
274-
ex.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
275-
throw ex;
269+
throw new ArgumentException(
270+
"Expected string to have at least one pair of quantity and unit in the format"
271+
+ " \"&lt;quantity&gt; &lt;unit&gt;\". Eg. \"5.5 m\" or \"1ft 2in\"");
276272
}
273+
return quantities.Aggregate((x, y) => x + y);
274+
}
277275

278-
try
279-
{
280-
AccelerationUnit unit = ParseUnit(unitString, formatProvider);
281-
double value = double.Parse(valueString, formatProvider);
276+
/// <summary>
277+
/// Parse a string given a particular regular expression.
278+
/// </summary>
279+
/// <exception cref="UnitsNetException">Error parsing string.</exception>
280+
private static List<Acceleration> ParseWithRegex(string regexString, string str, IFormatProvider formatProvider = null)
281+
{
282+
var regex = new Regex(regexString);
283+
MatchCollection matches = regex.Matches(str.Trim());
284+
var converted = new List<Acceleration>();
282285

283-
return From(value, unit);
284-
}
285-
catch (Exception e)
286+
foreach (Match match in matches)
286287
{
287-
var newEx = new UnitsNetException("Error parsing string.", e);
288-
newEx.Data["input"] = str;
289-
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
290-
throw newEx;
288+
GroupCollection groups = match.Groups;
289+
290+
var valueString = groups["value"].Value;
291+
var unitString = groups["unit"].Value;
292+
if (groups["invalid"].Value != "")
293+
{
294+
var newEx = new UnitsNetException("Invalid string detected: " + groups["invalid"].Value);
295+
newEx.Data["input"] = str;
296+
newEx.Data["matched value"] = valueString;
297+
newEx.Data["matched unit"] = unitString;
298+
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
299+
throw newEx;
300+
}
301+
if (valueString == "" && unitString == "") continue;
302+
303+
try
304+
{
305+
AccelerationUnit unit = ParseUnit(unitString, formatProvider);
306+
double value = double.Parse(valueString, formatProvider);
307+
308+
converted.Add(From(value, unit));
309+
}
310+
catch (Exception ex)
311+
{
312+
var newEx = new UnitsNetException("Error parsing string.", ex);
313+
newEx.Data["input"] = str;
314+
newEx.Data["matched value"] = valueString;
315+
newEx.Data["matched unit"] = unitString;
316+
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
317+
throw newEx;
318+
}
291319
}
320+
return converted;
292321
}
293322

294323
/// <summary>

UnitsNet/GeneratedCode/UnitClasses/AmplitudeRatio.g.cs

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// THE SOFTWARE.
2121

2222
using System;
23+
using System.Collections.Generic;
2324
using System.Globalization;
2425
using System.Text.RegularExpressions;
2526
using System.Linq;
@@ -280,17 +281,16 @@ public double As(AmplitudeRatioUnit unit)
280281
#region Parsing
281282

282283
/// <summary>
283-
/// Parse a string of the format "&lt;quantity&gt; &lt;unit&gt;".
284+
/// Parse a string with one or two quantities of the format "&lt;quantity&gt; &lt;unit&gt;".
284285
/// </summary>
285286
/// <example>
286287
/// Length.Parse("5.5 m", new CultureInfo("en-US"));
287288
/// </example>
288289
/// <exception cref="ArgumentNullException">The value of 'str' cannot be null. </exception>
289290
/// <exception cref="ArgumentException">
290-
/// Expected 2 words. Input string needs to be in the format "&lt;quantity&gt; &lt;unit
291-
/// &gt;".
291+
/// Expected string to have one or two pairs of quantity and unit in the format
292+
/// "&lt;quantity&gt; &lt;unit&gt;". Eg. "5.5 m" or "1ft 2in"
292293
/// </exception>
293-
/// <exception cref="UnitsNetException">Error parsing string.</exception>
294294
public static AmplitudeRatio Parse(string str, IFormatProvider formatProvider = null)
295295
{
296296
if (str == null) throw new ArgumentNullException("str");
@@ -302,41 +302,70 @@ public static AmplitudeRatio Parse(string str, IFormatProvider formatProvider =
302302
var numRegex = string.Format(@"[\d., {0}{1}]*\d", // allows digits, dots, commas, and spaces in the quantity (must end in digit)
303303
numFormat.NumberGroupSeparator, // adds provided (or current) culture's group separator
304304
numFormat.NumberDecimalSeparator); // adds provided (or current) culture's decimal separator
305-
var regexString = string.Format("(?<value>[-+]?{0}{1}{2}{3}",
306-
numRegex, // capture base (integral) Quantity value
307-
@"(?:[eE][-+]?\d+)?)", // capture exponential (if any), end of Quantity capturing
308-
@"\s?", // ignore whitespace (allows both "1kg", "1 kg")
309-
@"(?<unit>\S+)"); // capture Unit (non-whitespace) input
310-
311-
var regex = new Regex(regexString);
312-
GroupCollection groups = regex.Match(str.Trim()).Groups;
313-
314-
var valueString = groups["value"].Value;
315-
var unitString = groups["unit"].Value;
316-
317-
if (valueString == "" || unitString == "")
305+
var exponentialRegex = @"(?:[eE][-+]?\d+)?)";
306+
var regexString = string.Format(@"(?:\s*(?<value>[-+]?{0}{1}{2}{3})?{4}{5}",
307+
numRegex, // capture base (integral) Quantity value
308+
exponentialRegex, // capture exponential (if any), end of Quantity capturing
309+
@"\s?", // ignore whitespace (allows both "1kg", "1 kg")
310+
@"(?<unit>[^\s\d,]+)", // capture Unit (non-whitespace) input
311+
@"(and)?,?", // allow "and" & "," separators between quantities
312+
@"(?<invalid>[a-z]*)?"); // capture invalid input
313+
314+
var quantities = ParseWithRegex(regexString, str, formatProvider);
315+
if (quantities.Count == 0)
318316
{
319-
var ex = new ArgumentException(
320-
"Expected valid quantity and unit. Input string needs to be in the format \"<quantity><unit> or <quantity> <unit>\".", "str");
321-
ex.Data["input"] = str;
322-
ex.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
323-
throw ex;
317+
throw new ArgumentException(
318+
"Expected string to have at least one pair of quantity and unit in the format"
319+
+ " \"&lt;quantity&gt; &lt;unit&gt;\". Eg. \"5.5 m\" or \"1ft 2in\"");
324320
}
321+
return quantities.Aggregate((x, y) => x + y);
322+
}
325323

326-
try
327-
{
328-
AmplitudeRatioUnit unit = ParseUnit(unitString, formatProvider);
329-
double value = double.Parse(valueString, formatProvider);
324+
/// <summary>
325+
/// Parse a string given a particular regular expression.
326+
/// </summary>
327+
/// <exception cref="UnitsNetException">Error parsing string.</exception>
328+
private static List<AmplitudeRatio> ParseWithRegex(string regexString, string str, IFormatProvider formatProvider = null)
329+
{
330+
var regex = new Regex(regexString);
331+
MatchCollection matches = regex.Matches(str.Trim());
332+
var converted = new List<AmplitudeRatio>();
330333

331-
return From(value, unit);
332-
}
333-
catch (Exception e)
334+
foreach (Match match in matches)
334335
{
335-
var newEx = new UnitsNetException("Error parsing string.", e);
336-
newEx.Data["input"] = str;
337-
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
338-
throw newEx;
336+
GroupCollection groups = match.Groups;
337+
338+
var valueString = groups["value"].Value;
339+
var unitString = groups["unit"].Value;
340+
if (groups["invalid"].Value != "")
341+
{
342+
var newEx = new UnitsNetException("Invalid string detected: " + groups["invalid"].Value);
343+
newEx.Data["input"] = str;
344+
newEx.Data["matched value"] = valueString;
345+
newEx.Data["matched unit"] = unitString;
346+
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
347+
throw newEx;
348+
}
349+
if (valueString == "" && unitString == "") continue;
350+
351+
try
352+
{
353+
AmplitudeRatioUnit unit = ParseUnit(unitString, formatProvider);
354+
double value = double.Parse(valueString, formatProvider);
355+
356+
converted.Add(From(value, unit));
357+
}
358+
catch (Exception ex)
359+
{
360+
var newEx = new UnitsNetException("Error parsing string.", ex);
361+
newEx.Data["input"] = str;
362+
newEx.Data["matched value"] = valueString;
363+
newEx.Data["matched unit"] = unitString;
364+
newEx.Data["formatprovider"] = formatProvider == null ? null : formatProvider.ToString();
365+
throw newEx;
366+
}
339367
}
368+
return converted;
340369
}
341370

342371
/// <summary>

0 commit comments

Comments
 (0)