本文简述了编程中常见的时间显示问题
开发中总会在各种场景下遇到需要显示时间的情况,显示的格式要求又往往五花八门,正常的譬如: “2018年12月29日20点30分15秒”, 简洁一些的则有: “2018-12-29 20:30:15”, 等等种类,不一而足.
其实各种显示方式都可以使用诸如 String.Format 等方法来实现,灵活性也比较高,但是中间的格式细节却比较繁琐,基本库中的 DateTime 类型同样提供了 ToString 方法来帮助我们实现时间日期的格式化显示,但是同样存在格式细节繁复,不便记忆使用的问题.
解决上述问题的一个方法就是将各种显示规则抽象为函数参数,拿上面的 “2018年12月29日20点30分15秒” 和 “2018-12-29 20:30:15” 两种显示格式来说,区别其实就是各个中间的分隔符不同(第一种格式的分隔符为 “年” “月” “日” “点” “分” “秒”,第二种格式的分隔符为"-" “-” " " “:” “:” “”),据此,我们可以编写下面的时间格式化函数:
string GetDateStr(DateTime date, string yearSep, string monthSep, string daySep, string hourSep, string minuteSep, string secondSep)
{
// implementation
}
虽然参数不少,但是借助缺省参数等方式,使用起来还算OK,一般的时间显示需求也足够应付(在我的实际开发工作中,该(类型)接口也确实使用了很长时间).
但是当后面遇到更细致的时间显示需求时,上面的接口便显得有些"无力"了,其中最普遍的需求之一可能就是省略年份的显示了(“2018年12月29日20点30分15秒” 省略年份显示为 “12月29日20点30分15秒”)
沿用之前抽象显示参数的方法,我们可能会扩展出这样的接口:
string GetDateStr(DateTime date, string yearSep, string monthSep, string daySep, string hourSep, string minuteSep, string secondSep, bool yearDisplay, bool monthDisplay, bool dayDisplay, bool hourDisplay, bool minuteDisplay, bool secondDisplay)
{
// implementation
}
再考虑到后续可能还有补全显示时间(前导零补全,例如 “1秒” 显示为 “01秒”)等需求,很显然这里我们遇到了参数组合爆炸的问题.
实际上,我们需要的是一个简化的时间 Format 函数,支持且仅支持必要的控制格式,并且控制格式统一,方便记忆使用,下面的表格列出了可能的一种控制格式设计:
格式 | 说明 |
---|---|
y 或 Y | 年份显示 |
连续两个(包括)以上的 y 或 Y | 两位数的年份显示(例如 2018 显示为 18) |
M | 月份显示 |
连续两个(包括)以上的 M | 两位数的月份显示(例如 5 显示为 05) |
d 或 D | 天数显示 |
连续两个(包括)以上的 d 或 D | 两位数的天数显示(例如 9 显示为 09) |
h 或 H | 小时显示 |
连续两个(包括)以上的 h 或 H | 两位数的小时显示(例如 8 显示为 08) |
m | 分钟显示 |
连续两个(包括)以上的 m | 两位数的秒钟显示(例如 6 显示为 06) |
s 或 S | 秒钟显示 |
连续两个(包括)以上的 s 或 S | 两位数的秒钟显示(例如 16 显示为 16) |
\ | 转义符 |
实现的示例代码如下:
// time format util
// maintainer hugoyu
using System;
using System.Text;
/*
time format desc :
y|Y : whole year display, e.g. 2018
(y|Y)(y|Y)(y|Y)* : short year display, e.g. 18
M : whole month display, e.g. 5
MMM* : whole month display with leading zero(if necessary), e.g. 05
d|D : whole day display, e.g. 9
(d|D)(d|D)(d|D)* : whole day display with leading zero(if necessary), e.g. 09
h|H : whole hour display, e.g. 8
(h|H)(h|H)(h/H)* : whole hour display with leading zero(if necessary), e.g. 08
m : whole minute display, e.g. 6
mmm* : whole minute display with leading zero(if necessary), e.g. 06
s|S : whole second display, e.g. 16
(s|S)(s|S)(s|S)* : whole second display with leading zero(if necessary), e.g. 16
'\' is escape character
*/
public static class TimeFormatUtil
{
enum Token
{
None = 0,
Year,
YearPadding,
Month,
MonthPadding,
Day,
DayPadding,
Hour,
HourPadding,
Minute,
MinutePadding,
Second,
SecondPadding,
Literal,
}
static StringBuilder s_strBuffer = new StringBuilder();
static bool IsEscapeChar(char c)
{
return c == '\\';
}
static bool IsYearChar(char c)
{
return c == 'y' || c == 'Y';
}
static bool IsMonthChar(char c)
{
return c == 'M';
}
static bool IsDayChar(char c)
{
return c == 'd' || c == 'D';
}
static bool IsHourChar(char c)
{
return c == 'h' || c == 'H';
}
static bool IsMinuteChar(char c)
{
return c == 'm';
}
static bool IsSecondChar(char c)
{
return c == 's' || c == 'S';
}
static bool IsLiteralChar(char c)
{
return !IsEscapeChar(c) &&
!IsYearChar(c) &&
!IsMonthChar(c) &&
!IsDayChar(c) &&
!IsHourChar(c) &&
!IsMinuteChar(c) &&
!IsSecondChar(c);
}
static bool GetNextToken(string format, ref int curIndex, out Token token, out string tokenStr)
{
if (format != null)
{
if (curIndex >= 0 && curIndex < format.Length)
{
var curChar = format[curIndex];
if (IsEscapeChar(curChar))
{
// escape character
++curIndex;
if (curIndex < format.Length)
{
token = Token.Literal;
tokenStr = format[curIndex].ToString();
++curIndex;
return true;
}
}
else if (IsYearChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsYearChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Year;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.YearPadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (IsMonthChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsMonthChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Month;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.MonthPadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (IsDayChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsDayChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Day;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.DayPadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (IsHourChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsHourChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Hour;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.HourPadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (IsMinuteChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsMinuteChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Minute;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.MinutePadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (IsSecondChar(curChar))
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsSecondChar(format[curIndex]))
{
++curIndex;
}
if (curIndex - lastIndex == 1)
{
token = Token.Second;
tokenStr = format.Substring(lastIndex, 1);
return true;
}
else
{
token = Token.SecondPadding;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else
{
var lastIndex = curIndex;
++curIndex;
while (curIndex < format.Length && IsLiteralChar(format[curIndex]))
{
++curIndex;
}
token = Token.Literal;
tokenStr = format.Substring(lastIndex, curIndex - lastIndex);
return true;
}
}
else if (curIndex >= format.Length)
{
// end of string
// reuse Token.None here ?
token = Token.None;
tokenStr = null;
return true;
}
}
token = Token.None;
tokenStr = null;
return false;
}
static string GetPaddingString(int value)
{
if (value >= 100)
{
value %= 100;
}
if (value < 10)
{
// [0, 10)
return "0" + value.ToString();
}
else
{
// [10, 100)
return value.ToString();
}
}
public static string GetDateStrFormat(DateTime time, string format)
{
if (format != null)
{
s_strBuffer.Length = 0;
var curIndex = 0;
while (true)
{
Token token = Token.None;
string tokenStr = null;
var getTokenResult = GetNextToken(format, ref curIndex, out token, out tokenStr);
if (getTokenResult)
{
if (token == Token.None)
{
// end of string
return s_strBuffer.ToString();
}
else
{
switch (token)
{
case Token.Year:
s_strBuffer.Append(time.Year);
break;
case Token.YearPadding:
s_strBuffer.Append(GetPaddingString(time.Year));
break;
case Token.Month:
s_strBuffer.Append(time.Month);
break;
case Token.MonthPadding:
s_strBuffer.Append(GetPaddingString(time.Month));
break;
case Token.Day:
s_strBuffer.Append(time.Day);
break;
case Token.DayPadding:
s_strBuffer.Append(GetPaddingString(time.Day));
break;
case Token.Hour:
s_strBuffer.Append(time.Hour);
break;
case Token.HourPadding:
s_strBuffer.Append(GetPaddingString(time.Hour));
break;
case Token.Minute:
s_strBuffer.Append(time.Minute);
break;
case Token.MinutePadding:
s_strBuffer.Append(GetPaddingString(time.Minute));
break;
case Token.Second:
s_strBuffer.Append(time.Second);
break;
case Token.SecondPadding:
s_strBuffer.Append(GetPaddingString(time.Second));
break;
case Token.Literal:
s_strBuffer.Append(tokenStr);
break;
}
}
}
else
{
// error occur
return null;
}
}
}
return null;
}
}
小结 : GetDateStrFormat 方法平衡了接口灵活性和复杂性
参考资料
时间倏忽而过,转眼间 2018 年都到了最后一天,说来也巧,今年这最后一篇博文竟也是关于时间的,说来也可算作是一种纪念了,希冀在即将到来的 2019 年中,大家(包括自己)都继续砥砺前行吧~