Как создавать "удобочитаемые" строки для представления TimeSpan
У меня есть TimeSpan
, представляющий количество времени, которое клиент подключил к моему серверу. Я хочу показать пользователю TimeSpan
пользователю. Но я не хочу быть слишком многословным для отображения этой информации (например: 2hr 3min 32.2345sec = слишком подробно!)
Например: если время соединения...
> 0 seconds and < 1 minute -----> 0 Seconds
> 1 minute and < 1 hour -----> 0 Minutes, 0 Seconds
> 1 hour and < 1 day -----> 0 Hours, 0 Minutes
> 1 day -----> 0 Days, 0 Hours
И, конечно, в тех случаях, когда цифра равна 1 (например: 1 секунда, 1 минута, 1 час, 1 день), я хотел бы сделать текст сингулярным (например: 1 секунда, 1 минута, 1 час, 1 день).
В любом случае можно легко реализовать это без гигантского набора if/else clauses? Вот что я сейчас делаю.
public string GetReadableTimeSpan(TimeSpan value)
{
string duration;
if (value.TotalMinutes < 1)
duration = value.Seconds + " Seconds";
else if (value.TotalHours < 1)
duration = value.Minutes + " Minutes, " + value.Seconds + " Seconds";
else if (value.TotalDays < 1)
duration = value.Hours + " Hours, " + value.Minutes + " Minutes";
else
duration = value.Days + " Days, " + value.Hours + " Hours";
if (duration.StartsWith("1 Seconds") || duration.EndsWith(" 1 Seconds"))
duration = duration.Replace("1 Seconds", "1 Second");
if (duration.StartsWith("1 Minutes") || duration.EndsWith(" 1 Minutes"))
duration = duration.Replace("1 Minutes", "1 Minute");
if (duration.StartsWith("1 Hours") || duration.EndsWith(" 1 Hours"))
duration = duration.Replace("1 Hours", "1 Hour");
if (duration.StartsWith("1 Days"))
duration = duration.Replace("1 Days", "1 Day");
return duration;
}
Ответы
Ответ 1
Чтобы избавиться от сложных конструкций if и switch, вы можете использовать поиск в Словаре для правильной строки формата на основе TotalSeconds и CustomFormatter для форматирования предоставленного Timespan соответственно.
public string GetReadableTimespan(TimeSpan ts)
{
// formats and its cutoffs based on totalseconds
var cutoff = new SortedList<long, string> {
{60, "{3:S}" },
{60*60-1, "{2:M}, {3:S}"},
{60*60, "{1:H}"},
{24*60*60-1, "{1:H}, {2:M}"},
{24*60*60, "{0:D}"},
{Int64.MaxValue , "{0:D}, {1:H}"}
};
// find nearest best match
var find = cutoff.Keys.ToList()
.BinarySearch((long)ts.TotalSeconds);
// negative values indicate a nearest match
var near = find<0?Math.Abs(find)-1:find;
// use custom formatter to get the string
return String.Format(
new HMSFormatter(),
cutoff[cutoff.Keys[near]],
ts.Days,
ts.Hours,
ts.Minutes,
ts.Seconds);
}
// formatter for forms of
// seconds/hours/day
public class HMSFormatter:ICustomFormatter, IFormatProvider
{
// list of Formats, with a P customformat for pluralization
static Dictionary<string, string> timeformats = new Dictionary<string, string> {
{"S", "{0:P:Seconds:Second}"},
{"M", "{0:P:Minutes:Minute}"},
{"H","{0:P:Hours:Hour}"},
{"D", "{0:P:Days:Day}"}
};
public string Format(string format, object arg, IFormatProvider formatProvider)
{
return String.Format(new PluralFormatter(),timeformats[format], arg);
}
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter)?this:null;
}
}
// formats a numeric value based on a format P:Plural:Singular
public class PluralFormatter:ICustomFormatter, IFormatProvider
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (arg !=null)
{
var parts = format.Split(':'); // ["P", "Plural", "Singular"]
if (parts[0] == "P") // correct format?
{
// which index postion to use
int partIndex = (arg.ToString() == "1")?2:1;
// pick string (safe guard for array bounds) and format
return String.Format("{0} {1}", arg, (parts.Length>partIndex?parts[partIndex]:""));
}
}
return String.Format(format, arg);
}
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter)?this:null;
}
}
Ответ 2
Почему не просто что-то вроде этого?
public static class TimespanExtensions
{
public static string ToHumanReadableString (this TimeSpan t)
{
if (t.TotalSeconds <= 1) {
return [email protected]"{t:s\.ff} seconds";
}
if (t.TotalMinutes <= 1) {
return [email protected]"{t:%s} seconds";
}
if (t.TotalHours <= 1) {
return [email protected]"{t:%m} minutes";
}
if (t.TotalDays <= 1) {
return [email protected]"{t:%h} hours";
}
return [email protected]"{t:%d} days";
}
}
Если вы предпочитаете две единицы времени (например, минуты плюс секунды), это было бы очень просто добавить.
Ответ 3
Я бы предпочел что-то вроде этого, более читаемое, я думаю:
public string GetReadableTimeSpan(TimeSpan value)
{
string duration = "";
var totalDays = (int)value.TotalDays;
if (totalDays >= 1)
{
duration = totalDays + " day" + (totalDays > 1 ? "s" : string.Empty);
value = value.Add(TimeSpan.FromDays(-1 * totalDays));
}
var totalHours = (int)value.TotalHours;
if (totalHours >= 1)
{
if (totalDays >= 1)
{
duration += ", ";
}
duration += totalHours + " hour" + (totalHours > 1 ? "s" : string.Empty);
value = value.Add(TimeSpan.FromHours(-1 * totalHours));
}
var totalMinutes = (int)value.TotalMinutes;
if (totalMinutes >= 1)
{
if (totalHours >= 1)
{
duration += ", ";
}
duration += totalMinutes + " minute" + (totalMinutes > 1 ? "s" : string.Empty);
}
return duration;
}
Ответ 4
Я построил на Бьорн ответ, чтобы соответствовать моим потребностям, хотел поделиться, если кто-нибудь еще увидит эту проблему. Может спасти их время. Принятый ответ немного тяжеловес для моих потребностей.
private static string FormatTimeSpan(TimeSpan timeSpan)
{
Func<Tuple<int,string>, string> tupleFormatter = t => $"{t.Item1} {t.Item2}{(t.Item1 == 1 ? string.Empty : "s")}";
var components = new List<Tuple<int, string>>
{
Tuple.Create((int) timeSpan.TotalDays, "day"),
Tuple.Create(timeSpan.Hours, "hour"),
Tuple.Create(timeSpan.Minutes, "minute"),
Tuple.Create(timeSpan.Seconds, "second"),
};
components.RemoveAll(i => i.Item1 == 0);
string extra = "";
if (components.Count > 1)
{
var finalComponent = components[components.Count - 1];
components.RemoveAt(components.Count - 1);
extra = $" and {tupleFormatter(finalComponent)}";
}
return $"{string.Join(", ", components.Select(tupleFormatter))}{extra}";
}
Ответ 5
public string ToHumanDuration(TimeSpan? duration, bool displaySign = true)
{
if (duration == null) return null;
var builder = new StringBuilder();
if (displaySign)
{
builder.Append(duration.Value.TotalMilliseconds < 0 ? "-" : "+");
}
duration = duration.Value.Duration();
if (duration.Value.Days > 0)
{
builder.Append($"{duration.Value.Days}d ");
}
if (duration.Value.Hours > 0)
{
builder.Append($"{duration.Value.Hours}h ");
}
if (duration.Value.Minutes > 0)
{
builder.Append($"{duration.Value.Minutes}m ");
}
if (duration.Value.TotalHours < 1)
{
if (duration.Value.Seconds > 0)
{
builder.Append(duration.Value.Seconds);
if (duration.Value.Milliseconds > 0)
{
builder.Append($".{duration.Value.Milliseconds.ToString().PadLeft(3, '0')}");
}
builder.Append("s ");
}
else
{
if (duration.Value.Milliseconds > 0)
{
builder.Append($"{duration.Value.Milliseconds}ms ");
}
}
}
if (builder.Length <= 1)
{
builder.Append(" <1ms ");
}
builder.Remove(builder.Length - 1, 1);
return builder.ToString();
}
Источник: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Dashboard/HtmlHelper.cs
Ответ 6
Другой подход (на немецком языке)
public static string GetReadableTimeSpan(TimeSpan span)
{
var formatted = string.Format("{0}{1}{2}{3}",
span.Duration().Days > 0
? $"{span.Days:0} Tag{(span.Days == 1 ? string.Empty : "e")}, "
: string.Empty,
span.Duration().Hours > 0
? $"{span.Hours:0} Stunde{(span.Hours == 1 ? string.Empty : "n")}, "
: string.Empty,
span.Duration().Minutes > 0
? $"{span.Minutes:0} Minute{(span.Minutes == 1 ? string.Empty : "n")}, "
: string.Empty,
span.Duration().Seconds > 0
? $"{span.Seconds:0} Sekunde{(span.Seconds == 1 ? string.Empty : "n")}"
: string.Empty);
if (formatted.EndsWith(", ")) formatted = formatted.Substring(0, formatted.Length - 2);
return string.IsNullOrEmpty(formatted) ? "0 Sekunden" : ReplaceLastOccurrence(formatted, ",", " und ").Replace(" ", " ");
}
private static string ReplaceLastOccurrence(string source, string find, string replace)
{
var place = source.LastIndexOf(find, StringComparison.Ordinal);
if (place == -1)
return source;
var result = source.Remove(place, find.Length).Insert(place, replace);
return result;
}
Ответ 7
Здесь мой прием - немного проще, чем принятый ответ, не так ли? Кроме того, нет разделения/разбора строки.
var components = new List<Tuple<int, string>> {
Tuple.Create((int)span.TotalDays, "day"),
Tuple.Create(span.Hours, "hour"),
Tuple.Create(span.Minutes, "minute"),
Tuple.Create(span.Seconds, "second"),
};
while(components.Any() && components[0].Item1 == 0)
{
components.RemoveAt(0);
}
var result = string.Join(", ", components.Select(t => t.Item1 + " " + t.Item2 + (t.Item1 != 1 ? "s" : string.Empty)));