前言 程序中经常会遇到需要计算具体年龄的问题,所以花费了一些时间思考了解决方案,根据网上已有的计算方式,做了简单封装与测试调优,最后封装了一个Age
类完成年龄的计算。
单位问题 目前在项目中接触到的年龄单位分别是 岁、月、周、天、时、分,但是由于有些系统要求,年龄需要尽量具体,所以这里提供了一个标志枚举。至于具体什么是标志枚举,这里还没有特别做过介绍,所以大家可以搜索枚举
与Flags
特性了解。以后有机会介绍Logger
帮助类的时候再具体展开为大家介绍。
AgeUnit
枚举代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 [Flags ] public enum AgeUnit{ Year = 64 , Month = 32 , Week = 16 , Day = 8 , Hour = 4 , Minute = 2 , Second = 1 }
周岁年龄 年龄计算其实很简单,这个和我们小学时候做减法是一样的,首先从小单位着手运算,当相减为负数时向上一个单位借1
。
例如:当前时间是 2019-03-30 16:00:00
,宝宝的出生日期是 2019-03-28 20:00:00
,这个时候我们用 16时 - 20时
是不可行的,所以向日期借一天,就可以知道宝宝的实际年龄是 1天20时
。
这样看来是不是其实就是小学问题,但是有些人被年月日时分秒不同的进制搞得晕头转向,于是简单的问题被复杂化。
另外一点就是年月日之间的换算,进制是在变化的,但是这主要是由于每个月的天数不同,但是程序中已经提供了方法可以轻而易举的获取到某一年的某一个月的天数,所以这点我们也不用担心,下面我们看一下周岁年龄的计算:
Age
类型代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 public class Age { public Age (string age, DateTime? current = null ) { Current = current == null ? DateTime.Now : current.Value; ParseAge(age); } public Age (DateTime birthday, DateTime? current = null ) { Birthday = birthday; Current = current == null ? DateTime.Now : current.Value; GetAge(); } public DateTime Birthday { get ; set ; } public DateTime Current { get ; set ; } public TimeSpan TimeSpan { get ; set ; } public AgeUnit AgeUnit { get ; set ; } public int Year { get ; set ; } public int Month { get ; set ; } public int Week { get ; set ; } public int Day { get ; set ; } public int Hour { get ; set ; } public int Minute { get ; set ; } public int Second { get ; set ; } public virtual int GetAgeByAgeUnit (AgeUnit unit ) { if (unit == AgeUnit.Year) return Year; else if (unit == AgeUnit.Month) return Month; else if (unit == AgeUnit.Week) return Week; else if (unit == AgeUnit.Day) return Day; else if (unit == AgeUnit.Hour) return Hour; else if (unit == AgeUnit.Minute) return Minute; else if (unit == AgeUnit.Second) return Second; else throw new ArgumentOutOfRangeException(nameof (unit )); } protected virtual Dictionary<AgeUnit, string > AgeUnits { get ; } = new Dictionary<AgeUnit, string >() { [AgeUnit.Year ] = "岁" , [AgeUnit.Month ] = "月" , [AgeUnit.Week ] = "周" , [AgeUnit.Day ] = "天" , [AgeUnit.Hour ] = "时" , [AgeUnit.Minute ] = "分" , [AgeUnit.Second ] = "秒" , }; protected Regex _regex; protected virtual Regex Regex { get { if (_regex == null ) { _regex = new Regex($@"(\d+)\D*(({string .Join(")|(" , AgeUnits.Values.ToArray())} ))\D*" ); } return _regex; } } public override string ToString ( ) { StringBuilder builder = new StringBuilder(); for (int i = (int )AgeUnit.Year; i > 0 ; i /= 2 ) if (((AgeUnit)i & AgeUnit) != 0 && GetAgeByAgeUnit((AgeUnit)i) != 0 ) builder.Append(GetAgeByAgeUnit((AgeUnit)i)).Append(AgeUnits[(AgeUnit)i]); if (string .IsNullOrEmpty(builder.ToString())) builder.Append(0 ).Append(AgeUnits[AgeUnit.Year]); return builder.ToString(); } protected virtual void GetAge ( ) { if (Birthday > Current) { throw new ArgumentException("出生日期不能大于当前时间!" ); } int bYear = Birthday.Year; int bMonth = Birthday.Month; int bDay = Birthday.Day; int bHour = Birthday.Hour; int bMinute = Birthday.Minute; int bSecond = Birthday.Second; int nYear = Current.Year; int nMonth = Current.Month; int nDay = Current.Day; int nHour = Current.Hour; int nMinute = Current.Minute; int nSecond = Current.Second; if (nSecond < bSecond) { nMinute--; nSecond += 60 ; } if (nMinute < bMinute) { nHour--; nMinute += 60 ; } if (nHour < bHour) { nDay--; nHour += 24 ; } if (nDay < bDay) { nMonth--; nDay += DateTime.DaysInMonth(bYear, bMonth); } if (nMonth < bMonth) { nYear--; nMonth += 12 ; } Year = nYear - bYear; Month = nMonth - bMonth; Day = nDay - bDay; Hour = nHour - bHour; Minute = nMinute - bMinute; Second = nSecond - bSecond; TimeSpan = Current - Birthday; if (Year > 0 ) AgeUnit = AgeUnit | AgeUnit.Year; if (Month > 0 ) AgeUnit = AgeUnit | AgeUnit.Month; if (Week > 0 ) AgeUnit = AgeUnit | AgeUnit.Week; if (Day > 0 ) AgeUnit = AgeUnit | AgeUnit.Day; if (Hour > 0 ) AgeUnit = AgeUnit | AgeUnit.Hour; if (Minute > 0 ) AgeUnit = AgeUnit | AgeUnit.Minute; if (Second > 0 ) AgeUnit = AgeUnit | AgeUnit.Second; } protected virtual void ParseAge (string input ) { Match match = Regex.Match(input); MatchCollection collection = Regex.Matches(input); if (collection.Count <= 0 ) throw new ArgumentException(nameof (input)); else { foreach (Match item in collection) { if (int .TryParse(item.Groups[1 ].Value, out int age)) { AgeUnit unit = AgeUnits.FirstOrDefault(kv => kv.Value == item.Groups[2 ].Value).Key; SetAge(unit , age); } else throw new ArgumentException(nameof (input)); } } GetBirthdayByAge(); TimeSpan = Current - Birthday; } protected virtual void GetBirthdayByAge ( ) { Birthday = Current.AddYears(-Year).AddMonths(-Month).AddDays(-Week * 7 ).AddDays(-Day).AddHours(-Hour).AddMinutes(-Minute).AddSeconds(-Second); } protected virtual void SetAge (AgeUnit unit , int age ) { if (age == 0 ) return ; AgeUnit = AgeUnit | unit ; if (unit == AgeUnit.Year) Year += age; else if (unit == AgeUnit.Month) Month += age; else if (unit == AgeUnit.Week) Week += age; else if (unit == AgeUnit.Day) Day += age; else if (unit == AgeUnit.Hour) Hour += age; else if (unit == AgeUnit.Minute) Minute += age; else if (unit == AgeUnit.Second) Second += age; else throw new ArgumentException(nameof (unit )); } }
以上主要需要看一下 GetAge
方法,通过 DateTime.DaysInMonth(year, month)
获取到出生月份的总天数,后面的运算就水到渠成了。
医学年龄 使用Age
类进行计算虽然可以准确的计算一个人的周岁年龄,但是碰到一些极端情况,仍然存在问题。
例如一个人的年龄是 1月30天
,这是由于出生月份的天数是31天
导致的;再比如一个人的年龄是1月
,但实际上只有29天
或者28天
,这是由于出生月份在2月份
。
这样可能会在一些对年龄计算有要求的系统中比如医疗机构的系统,引起用户的误解。当然我们可以与客户沟通,了解对方需求后对Age
类型继承,然后提供出满足用户需求的计算年龄的解决方案。
例如以下是HIS
提供商与医院沟通后确定的:固定1岁=365天
,1月=30天
进行年龄计算的方案:
MedicalAge
类型代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 public class MedicalAge : Age { public MedicalAge (string age, DateTime? current = null ) : base (age, current ) { } public MedicalAge (DateTime birthday, DateTime? current = null ) : base (birthday, current ) { } public int ? Age1 { get ; set ; } public int ? Age2 { get ; set ; } public AgeUnit? AgeUnit1 { get ; set ; } public AgeUnit? AgeUnit2 { get ; set ; } public override string ToString ( ) { StringBuilder builder = new StringBuilder(); if (Age1 != null ) builder.Append(Age1.Value).Append(AgeUnits[AgeUnit1.Value]); if (Age2 != null ) builder.Append(Age2.Value).Append(AgeUnits[AgeUnit2.Value]); if (string .IsNullOrEmpty(builder.ToString())) builder.Append(0 ).Append(AgeUnits[AgeUnit.Year]); return builder.ToString(); } protected override void GetAge ( ) { if (Birthday > Current) { throw new ArgumentException("出生日期不能大于当前时间!" ); } TimeSpan = Current - Birthday; Year = TimeSpan.Days / 365 ; Month = TimeSpan.Days % 365 / 30 ; Week = TimeSpan.Days % 365 % 30 / 7 ; Day = TimeSpan.Days % 365 % 30 % 7 ; Hour = TimeSpan.Hours; Minute = TimeSpan.Minutes; Second = TimeSpan.Seconds; if (Year > 0 ) AgeUnit = AgeUnit | AgeUnit.Year; if (Month > 0 ) AgeUnit = AgeUnit | AgeUnit.Month; if (Week > 0 ) AgeUnit = AgeUnit | AgeUnit.Week; if (Day > 0 ) AgeUnit = AgeUnit | AgeUnit.Day; if (Hour > 0 ) AgeUnit = AgeUnit | AgeUnit.Hour; if (Minute > 0 ) AgeUnit = AgeUnit | AgeUnit.Minute; if (Second > 0 ) AgeUnit = AgeUnit | AgeUnit.Second; for (int i = (int )AgeUnit.Year; i > 0 ; i /= 2 ) { if (((AgeUnit)i & AgeUnit) != 0 ) { if (AgeUnit1 == null ) { AgeUnit1 = (AgeUnit)i; Age1 = GetAgeByAgeUnit(AgeUnit1.Value); } else if (AgeUnit2 == null ) { AgeUnit2 = (AgeUnit)i; Age2 = GetAgeByAgeUnit(AgeUnit2.Value); } else break ; } } } protected override void GetBirthdayByAge ( ) { Birthday = Current.AddDays(-Year * 365 - Month * 30 - Week * 7 - Day).AddHours(-Hour).AddMinutes(-Minute).AddSeconds(-Second); GetMedicalAge(); } protected virtual void GetMedicalAge ( ) { for (int i = (int )AgeUnit.Year; i > 0 ; i /= 2 ) { if (((AgeUnit)i & AgeUnit) != 0 ) { if (AgeUnit1 == null ) { AgeUnit1 = (AgeUnit)i; Age1 = GetAgeByAgeUnit(AgeUnit1.Value); } else if (AgeUnit2 == null ) { AgeUnit2 = (AgeUnit)i; Age2 = GetAgeByAgeUnit(AgeUnit2.Value); } else break ; } } } }
测试 除提供了根据出生日期计算年龄以外,上文还简单提供了一个根据年龄获取出生日期的方案,可以参看上文Age
与MedicalAge
第一个入参为字符串类型的构造函数进行了解。
至于年龄输出的话,暂时重写了ToString
方法,进行格式化的输出,以下是简单的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void Main ( ) { TestGetAge(new DateTime(1931 , 09 , 18 )); TestGetAge(new DateTime(1931 , 09 , 18 , 23 , 59 , 59 )); TestGetAge(new DateTime(1949 , 10 , 01 )); TestGetAge(DateTime.Now.Date.AddDays(-50 )); TestGetAge(DateTime.Now); Console.ReadKey(); } public static void TestGetAge (DateTime birthday ) { Age age = new Age(birthday); Console.WriteLine($"出生时间为:{age.Birthday.ToString("yyyy-MM-dd HH:mm:ss" )} 的 年龄为:{age.ToString()} " ); Age tempAge = new Age(age.ToString()); Console.WriteLine($"出生时间为:{tempAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss" )} 的 年龄为:{tempAge.ToString()} " ); Age medicalAge = new MedicalAge(birthday); Console.WriteLine($"出生时间为:{medicalAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss" )} 的 年龄为:{medicalAge.ToString()} " ); Age tempMedicalAge = new MedicalAge($"{medicalAge.Year} 岁{medicalAge.Month} 月{medicalAge.Week} 周{medicalAge.Day} 天{medicalAge.Hour} 时{medicalAge.Minute} 分{medicalAge.Second} 秒" ); Console.WriteLine($"出生时间为:{tempMedicalAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss" )} 的 年龄为:{tempMedicalAge.ToString()} " ); }
总结 年龄计算从上文来看其实从技术上来讲并不困难,但是主要的问题在于,要与用户进行沟通达成共识。
否则从日常理解来说,特别是小单位需要细致到月周天的年龄的计算,很容易让人误解。