protected static bool DoParseMutliple(string text, out TimeSpan[] timeSpans, TimeSpanParserOptions options = null, int max = int.MaxValue) { if (options == null) { options = new TimeSpanParserOptions(); //TODO: default options object } Units[] badDefaults = new Units[] { Units.Error, Units.ErrorTooManyUnits, Units.ErrorAmbiguous }; if (badDefaults.Any(bad => options.UncolonedDefault == bad) || badDefaults.Any(bad => options.ColonedDefault == bad)) { throw new ArgumentException("Bad default selection."); } //TODO (or not: overly limited: requires groups of 3 numbers or fails): https://social.msdn.microsoft.com/Forums/en-US/431d51f9-8003-4c72-ba1f-e830c6ad75ba/regex-to-match-all-number-formats-used-around-the-world?forum=regexp text = text.Normalize(NormalizationForm.FormKC); // fixing any fullwidth characters text = text.Replace('_', ' '); var numberFormatInfo = (options.FormatProvider == null) ? CultureInfo.CurrentCulture.NumberFormat : NumberFormatInfo.GetInstance(options.FormatProvider); string decimalSeparator = numberFormatInfo.NumberDecimalSeparator; bool allowThousands = ((options.NumberStyles & NumberStyles.AllowThousands) > 0); string groupSeparator = allowThousands ? Regex.Escape(numberFormatInfo.NumberGroupSeparator) : string.Empty; string plusMinus = numberFormatInfo.PositiveSign + numberFormatInfo.NegativeSign; // TODO? if (options.AllowDotSeparatedDayHours && decimalSeparator != ".") { decimalSeparator += "."; // always also need a dot for day.hour separation (unless that's off) } string zeroRegexStr = @"([+-]?:)?(([-+]?[0" + groupSeparator + "]*[" + Regex.Escape(decimalSeparator) + @"}]?[0]+(?:[eE][-+]?[0-9]+)?)\:?)+"; // 0:00:00 0e100 0.00:00:00:0.000:0e20:00 string numberRegexStr; //TODO: +- at start or end depending on culture if (allowThousands) { numberRegexStr = @"([+-]?:)?(([-+]?([0-9]+([" + groupSeparator + "]?)(?=[0-9]))*[" + Regex.Escape(decimalSeparator) + @"}]?[0-9]+(?:[eE][-+]?[0-9]+)?)\:?)+"; } else { numberRegexStr = @"([+-]?:)?(([-+]?[0-9]*[" + Regex.Escape(decimalSeparator) + @"}]?[0-9]+(?:[eE][-+]?[0-9]+)?)\:?)+"; } // regex notes: // - floating point numbers separated by (or ending with) with colon. // - matches a number: 30 // - also matches floating point number: +3e-10 // - also allows colons: 10:20:21.70 // - or crazy combo: 10.2e+2:20:21.70 (note: the dot is sometimes a day separator) // - regex101.com for testing // weird things: // - supports mixed formats like "22:11h 10s" (=22:11:10) // may change: // - starting colon will be ignored, ":30" treated as "30" // - but not after: 3: (treated as "3") // - in future, starting-colon numbers may get their own option var numberRegex = new Regex(numberRegexStr); // TODO: re-use regex + RegexOptions.Compiled var zeroRegex = new Regex(zeroRegexStr); List <ParserToken> tokens = new List <ParserToken>(); var matches = numberRegex.Matches(text); for (int i = 0; i < matches.Count; i++) // foreach (Match match in matches) { { Match match = matches[i]; int numberEnd = match.Index + match.Length; int nextMatchIndex = (i + 1 < matches.Count ? matches[i + 1].Index : text.Length); int suffixLength = nextMatchIndex - numberEnd; //Console.WriteLine($"text:{text}. match[{i}]: suffixLength:{suffixLength}"); string number = match.Value; string suffix = text.Substring(numberEnd, suffixLength); bool coloned = number.Contains(':'); //Console.WriteLine($"part[{i}]: num:'{number}', suffix:'{suffix}', colon:{coloned}"); Units suffixUnits = ParseSuffix(suffix); //TODO: ignore initial colon (now) if requested if (coloned) { var parts = number.Split(':'); if (parts.Length <= 1) { timeSpans = null; // timeSpans = builder.FinalSpans(); // foundTimeSpans.ToArray(); return(false); // something went wrong. should never happen } var token = new ColonedToken(); token.options = options; token.GivenUnit = suffixUnits; //TODO: maybe don't do this if parsing a localization that doesn't use a dot separator for days.months ? if (parts != null && parts.Length >= 1 && parts[0].Contains('.')) { token.firstColumnContainsDot = true; //Note: specifically '.' and NOT the regional decimal separator token.firstColumnRightHalf = ParseNumber(parts[0].Split('.')[1], options); //TODO: error checking } if (string.IsNullOrWhiteSpace(parts[0])) { // TODO token.startsWithColon = true; parts[0] = null; } else if (parts != null && parts.Length >= 1 && parts[0] != null && parts[0].Trim() == "-") { //don't attempt to parse parts[0] = null; token.negativeColoned = true; token.startsWithColon = true; } else if (parts != null && parts.Length >= 1 && parts[0] != null && parts[0].Trim() == "+") //TODO tidy { parts[0] = null; token.startsWithColon = true; } token.colonedColumns = parts.Select(p => ParseNumber(p, options)).ToArray(); tokens.Add(token); //Console.WriteLine($"token: {token}"); } else { //decimal parsedNumber; //bool numberSuccess = decimal.TryParse(number, options.NumberStyles, options.FormatProvider, out parsedNumber); var token = new OneUnitToken(); token.options = options; token.GivenUnit = suffixUnits; token.uncolonedValue = ParseNumber(number, options); tokens.Add(token); //Console.WriteLine($"token= {token}"); } } List <TimeSpan?> timespans = new List <TimeSpan?>(); ParserToken last = null; bool willSucceed = true; foreach (ParserToken token in tokens) { if (token.IsUnitlessFailure() || token.IsOtherFailure()) { //Console.WriteLine($"wont succeed..." + (!options.FailOnUnitlessNumber ? "or actually it might" : "")); //throw new ArgumentException("failed to parse because of a unitless number."); willSucceed = false; if (last != null) { timespans.Add(last.ToTimeSpan()); } last = null; continue; } if (last != null) { bool success = last.TryMerge(token, out ParserToken newToken); if (!success) { throw new ArgumentException("Failed to parse. Probably because of a unitless number."); } if (newToken == null) { timespans.Add(last.ToTimeSpan()); last = token; } else { last = newToken; } } else { last = token; } } if (last != null) { timespans.Add(last.ToTimeSpan()); } timeSpans = timespans.Where(t => t.HasValue).Select(t => t.Value).ToArray(); // just the nonnull for now return(!options.FailOnUnitlessNumber || willSucceed); }