问题描述
我正在开发一个跟踪时间跨度的应用,用户必须能够根据需要设置应用时间格式,例如:
"ss:fff" (seconds:miliseconds)
"ss\s\. fff mils\." (seconds s. miliseconds mils.)
"dd:hh:mm" (days:hours:minutes)
etc...
我存储时间一样长,所以通过简单的 TimeSpan 格式,我可以用用户配置的格式显示它们,简单且实际工作。
当我开始实现“手动”时间添加时出现问题(用户在 TextBox 中输入新时间,然后将其添加到时间列表中并使用配置的时间格式)。
就在用户引入新时间之后,我必须将引入的时间从字符串转换为长整型,目的是存储它(TimeSpan.TryParseExact 提供配置的时间格式可以完成工作),除了一个问题:如果我们有像 mm:ss 这样的格式,解析时间为 90:32,解析失败,因为解析时间不应超过 59 分钟。
我制作了一个小的控制台应用示例来帮助重现我的问题:
static void Main(string[] args)
{
string TimeFormat = @"ss\:fff";
long[] SampleTimes = new long[] { 1000,5000,59666 };
List<long> times = new List<long>(SampleTimes);
string input;
long aux;
do
{
ShowTimes(times,TimeFormat);
Console.Write(">");
input = Console.ReadLine();
if (TryParseTime(input,TimeFormat,out aux))
times.Add(aux);
else
Console.WriteLine("Failed parsing");
} while (input != "Exit");
}
static void ShowTimes(IEnumerable<long> times,string format)
{
Console.WriteLine("-----");
foreach (long time in times)
Console.WriteLine(TimeSpan.FromMilliseconds(time).ToString(format));
Console.WriteLine("-----");
}
static bool TryParseTime(string time,string format,out long parsed)
{
TimeSpan result;
bool ok = TimeSpan.TryParseExact(time,format,null,out result);
parsed = ok ? (long)result.TotalMilliseconds : -1;
return ok;
}
在另一篇文章 [1,2] 中引用了相同的问题,他们解决了将引入时间的部分分离并从代码中计算出来的问题:
//From first post
var temp = "113388";
s_Time = DateTime.ParseExact(temp.Substring(0,4
),"HHmm",null).AddSeconds(int.Parse(temp.Substring(4)));
//From second post
public static decimal Hours(string s)
{
decimal r;
if (decimal.TryParse(s,out r))
return r;
var parts = s.Split(':');
return (decimal)new TimeSpan(int.Parse(parts[0]),int.Parse(parts[1]),0).TotalHours;
}
但我不能这样走,因为没有时间格式作为参考来拆分引入的时间格式,它可以随时更改。
此时我唯一的想法是创建一个 TimeSpan.TryParseExact 扩展方法,该方法通过正则表达式获取最大的时间单位并通过单独的方式对其进行解析...
有没有更好的方法来做到这一点?
解决方法
对我来说似乎很合理;让您的用户输入如下字符串:
T-{hour}h{min}m{sec}s [baby!]
和正则表达式转义它,然后字符串替换它(例如"{hour}"
-> "(?<h>\d+)"
)成为一个正则表达式:
T-(?<h>\d+)h(?<m>\d+)m(?<s>\d+)s \[baby!\]
还有字符串替换它成为时间跨度输出格式:
'T-'hh'h'mm'm'ss's [baby!]'
然后你有你的捕获组..他们可以输入他们奇怪的时间格式,你可以解析它,你可以输出它..
,好的,我最终使用了这个自定义方法来完成这项工作。
这不是一种连续执行多次的方法,因为它会带来巨大的性能问题,但是从前端解析引入的数据是可以接受的:
/// <summary>
/// Given a time and a format it creates a <see cref="TimeSpan"/> ignoring the format digit limitations.
/// The format is not validated,so better ensure a correct one is provided ;)
/// </summary>
/// <param name="time"></param>
/// <param name="format"></param>
/// <param name="timeSpan"></param>
/// <returns></returns>
public static bool TryParseTime(string time,string format,out TimeSpan timeSpan)
{
// Regex to match the components of the time format (ss:fff matches ss and fff)
var formatRegex = new Regex(@"(?<=(?<!\\)(?:\\{2})*)(%?([fFsmhd])(\2*))");
var matches = formatRegex.Matches(format);
if (matches.Count > 0)
{
// We build a big regex to extract the values from time
string formatExtractionRegex = string.Empty;
int pivot = 0;
foreach (Match match in matches)
{
if (match.Success)
{
char c = match.Value.ToLower()[0];
formatExtractionRegex += $@"{format.Substring(pivot,match.Index - pivot)}(?<{c}>\d+)";
pivot = match.Index + match.Length;
}
}
var timeParts = new Regex(formatExtractionRegex).Match(time);
int d,h,m,s,f;
int.TryParse(timeParts.Groups["d"].ToString(),out d);
int.TryParse(timeParts.Groups["h"].ToString(),out h);
int.TryParse(timeParts.Groups["m"].ToString(),out m);
int.TryParse(timeParts.Groups["s"].ToString(),out s);
int.TryParse(timeParts.Groups["f"].ToString(),out f);
timeSpan = new TimeSpan(d,f);
return true;
}
timeSpan = default;
return false;
}
该方法通过构建一个大的正则表达式来替换正则表达式 \d+
的数字类型,从而从时间中提取数据,因此当数字组的长度超过格式指定的长度时,我们会选择整个数字组。
如果我们提供时间 100:100:5000
和格式 mm\:ss\:fff
,生成的正则表达式将为 (?<m>\\d+)\\:(?<s>\\d+)\\:(?<f>\\d+)
。
最后我们解析匹配的组并解析它们以提供给 TimeSpan Constructor。