Monthly数据类型
程序员文章站
2022-05-13 21:53:48
Monthly由来 最近在做关于智能财税的项目,大量用到了账期相关的数据操作。项目已有两年历史了,对于账期数据,前辈们用的是DateTime数据类型,即每个月的最后一天就是账期。而用DateTime来表达账期数据,确实让我人很困惑: 1. 概念不统一: DateTime是时间类型,而账期只跟年月相关 ......
Monthly由来
最近在做关于智能财税的项目,大量用到了账期相关的数据操作。项目已有两年历史了,对于账期数据,前辈们用的是DateTime数据类型,即每个月的最后一天就是账期。而用DateTime来表达账期数据,确实让我人很困惑:
- 概念不统一:
DateTime是时间类型,而账期只跟年月相关,DateTime用在这里确实有点杀鸡用了宰牛刀,而且给人的理解和沟通造成了额外的误解。- 格式不统一:
为了在数据传输和存储中达到数据的统一性,需要大量的字符串与日期的转换、日期格式的转换。- 浪费性能:
DateTime的精确度是可以到毫秒级的,而我们的账期数据只需要精确到月,如:2018年1月账期。 所以DateTime是很影响运算性能和存储空间的。- 操作异常:
由于账期是取月末日期,所以对每次接收了账期参数都要取月末值,以确保数据的准确性。而在实际开发中,任何一个疏忽都会引发表达偏差。
Monthly简介
Monthly是一个跟Datetime类似的,与月份相关的数据类型,适用于表达年月数据,如账单、账期、月刊等信息。
Monthly源码
using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace System { /// <summary> /// 与月份相关的对象,如账单、账期、月刊、月报等 /// </summary> [Serializable] [StructLayout(LayoutKind.Auto)] public struct Monthly : IComparable<Monthly>, IEquatable<Monthly> { private int _year; private int _month; #region Property /// <summary> /// 获取当前实例的年 /// </summary> public int Year => _year; /// <summary> /// 获取当前实例的月 /// </summary> public int Month => _month; /// <summary> /// 获取当前实例的年月标记值,如2018年1月记为 : 201801 /// </summary> public int Dot => this._year * 100 + this._month; /// <summary> /// 获取当前实例从公元零年一月开始的月份累计值 /// </summary> public int Tickes => this._year * 12 + this._month; /// <summary> /// 获取当前实例所在的季度 /// </summary> public int Quarter => (this._month - 1) / 3 + 1; #endregion #region Ctor /// <summary> /// 以指定的年和月初始化Monthly实例。 /// </summary> /// <param name="year"> 年(0 到 9999)</param> /// <param name="month"> 月(1 到 12)</param> public Monthly(int year, int month) { CheckYear(year); CheckMonth(month); this._year = year; this._month = month; } /// <summary> /// 获取以当前时间点为依据的新实例 /// </summary> public static Monthly Current => new Monthly() { _year = DateTime.Now.Year, _month = DateTime.Now.Month }; /// <summary> /// 获取当前时间点的上月为依据的新实例 /// </summary> public Monthly Previous => Monthly.fromTickes(this.Tickes - 1); /// <summary> /// 获取当前时间点的下月为依据的新实例 /// </summary> public Monthly Next => Monthly.fromTickes(this.Tickes + 1); /// <summary> /// 获取当前年份的一月为依据的新实例 /// </summary> public Monthly First => new Monthly() { _year = this._year, _month = 1 }; /// <summary> /// 获取当前年份的十二月为依据的新实例 /// </summary> public Monthly Last => new Monthly() { _year = this._year, _month = 12 }; /// <summary> /// 获取Monthly的最小值实例 /// </summary> public static Monthly MinValue => new Monthly() { _year = 0, _month = 1 }; /// <summary> /// 获取Monthly的最大值实例 /// </summary> public static Monthly MaxValue => new Monthly() { _year = 9999, _month = 12 }; #endregion #region Method private static int yearOfDot(int dot) => dot / 100; private static int monthOfDot(int dot) => dot % 100; /// <summary> /// 获取当前实例的年月标记值,如2018年1月记为 : 201801 /// </summary> /// <returns></returns> public int ToDot() => this.Dot; /// <summary> /// 以当前实例与years的和值为依据创建一个新实例 /// </summary> public Monthly AddYears(int years) => Monthly.FromTickes(this.Tickes + years * 12); /// <summary> /// 以当前实例与months的和值为依据创建一个新实例 /// </summary> public Monthly AddMonths(int months) => Monthly.FromTickes(this.Tickes + months); /// <summary> /// 判断当前实例的值与给定实例的值是否相等 /// </summary> public bool Equals(Monthly other) => this.Tickes == other.Tickes; /// <summary> /// 获取当前实例与给定实例的月份差值 /// </summary> public int SpanMonths(Monthly other) => this - other; /// <summary> /// 获取当前实例与DateTime实例的月份差值 /// </summary> public int SpanMonths(DateTime date) => this.Tickes - date.Year * 12 - date.Month; /// <summary> /// 获取当前实例与给定实例的大小比较的结果标识 /// </summary> /// <param name="other"></param> /// <returns>-1:小于other实例值 ; 0 等于other实例值 ; 1:大于other实例值</returns> public int CompareTo(Monthly other) { if (this.Tickes < other.Tickes) return -1; if (this.Tickes > other.Tickes) return 1; else return 0; } /// <summary> /// 以年月标记值创建一个Monthly新实例 /// </summary> /// <param name="dot">格式:201801</param> /// <returns></returns> public static Monthly FromDot(int dot) { var year = yearOfDot(dot); var month = monthOfDot(dot); if (year < 0 || year > 9999 || month < 1 || month > 12) throw new ArgumentOutOfRangeException("dot", dot, "Please enter correct dot format such as \'201801\'."); return new Monthly { _year = yearOfDot(dot), _month = monthOfDot(dot) }; } private static Monthly fromTickes(int tickes) { return new Monthly { _year = (tickes - 1) / 12, _month = tickes % 12 == 0 ? 12 : tickes % 12 }; } /// <summary> /// 以年月累计值创建一个Monthly新实例 /// </summary> /// <param name="tickes">以公元零年一月为起点的月份计数值(1-120000)</param> public static Monthly FromTickes(int tickes) { if (tickes < 1 || tickes > 120000) throw new ArgumentOutOfRangeException("tickes", tickes, "The tickes must beteen 1 and 120000 ."); return fromTickes(tickes); } /// <summary> /// 以DateTime实例创建一个Monthly新实例 /// </summary> public static Monthly FromDate(DateTime time) => new Monthly() { _year = time.Year, _month = time.Month }; /// <summary> /// 以诸如"2018/01"格式的字符串创建一个Monthly新实例 /// </summary> /// <param name="s">"2018/01"格式的字符串</param> /// <param name="spliter">分隔符</param> public static Monthly FromString(string s) { if (string.IsNullOrEmpty(s)) throw new Exception("The parameter cannot be null or empty."); var nums = Regex.Matches(s, "[0-9]+"); if (nums.Count == 0) throw new Exception("Please give the correct parameters, such as '2018/01' ."); if (nums.Count == 1) return new Monthly(0, Convert.ToInt32(nums[0].ToString().TrimStart('0'))); else return new Monthly(Convert.ToInt32(nums[0].ToString().TrimStart('0')), Convert.ToInt32(nums[1].ToString().TrimStart('0'))); } /// <summary> /// 获取一段时间内的Monthly数轴(包含开始与结束月份) /// </summary> /// <param name="from">开始月份</param> /// <param name="to">结束月份</param> /// <returns></returns> public static List<Monthly> Axis(Monthly from, Monthly to) { var result = new List<Monthly>(); var span = from - to; var len = (span ^ (span >> 31)) - (span >> 31) + 1; for (int i = 0; i < len; i++) { if (span > 0) result.Add(from - i); else result.Add(from + i); } return result; } /// <summary> /// 获取给定时间段内的Monthly集合(包含开始与结束月份) /// </summary> /// <param name="from">开始月份</param> /// <param name="to">结束月份</param> /// <returns></returns> public static List<Monthly> Axis(int from, int to) { return Axis(Monthly.FromDot(from), Monthly.FromDot(to)); } /// <summary> /// 检查year的合法性 /// </summary> private static void CheckYear(int year) { if (year < 0 || year > 9999) throw new ArgumentOutOfRangeException("year", year, "The year must beteen 0 and 9999 ."); } /// <summary> /// 检查month的合法性 /// </summary> private static void CheckMonth(int month) { if (month < 1 || month > 12) throw new ArgumentOutOfRangeException("month", month, "The month must beteen 1 and 12 ."); } #endregion #region Operator /// <summary> /// 以给定实例与months的和值创建一个新实例 /// </summary> /// <param name="months">月分数</param> public static Monthly operator +(Monthly m, int months) => FromTickes(m.Tickes + months); /// <summary> /// 以给定实例与months的差值创建一个新实例 /// </summary> /// <param name="months">月分数</param> public static Monthly operator -(Monthly m, int months) => FromTickes(m.Tickes - months); /// <summary> /// 获取当前实例与给定实例的月份差值 /// </summary> public static int operator -(Monthly m1, Monthly m2) => m1.Tickes - m2.Tickes; /// <summary> ///获取当前实例与DateTime实例的月份差值 /// </summary> public static int operator -(Monthly m, DateTime d) => m.SpanMonths(d); public static Monthly operator ++(Monthly m) => m + 1; public static Monthly operator --(Monthly m) => m - 1; /// <summary> ///判断m1是否等于m2 /// </summary> public static bool operator ==(Monthly m1, Monthly m2) => m1.Tickes == m2.Tickes; /// <summary> /// 判断m1是否不等于m2 /// </summary> public static bool operator !=(Monthly m1, Monthly m2) => m1.Tickes != m2.Tickes; /// <summary> /// 判断m1是否小于m2 /// </summary> public static bool operator <(Monthly m1, Monthly m2) => m1.Tickes < m2.Tickes; /// <summary> /// 判断m1是否大于m2 /// </summary> public static bool operator >(Monthly m1, Monthly m2) { return m1.Tickes > m2.Tickes; ; } /// <summary> /// 判断m1是否小于等于m2 /// </summary> public static bool operator <=(Monthly m1, Monthly m2) { return m1.Tickes <= m2.Tickes; ; } /// <summary> /// 判断m1是否大于等于m2 /// </summary> public static bool operator >=(Monthly m1, Monthly m2) { return m1.Tickes >= m2.Tickes; ; } /// <summary> /// 以年月标识的Monthly实例 /// </summary> /// <param name="dot">格式:201801</param> public static implicit operator Monthly(int dot) { return Monthly.FromDot(dot); } #endregion #region Override /// <summary> /// 获取包含"Y、y、M、m"字符格式的自定义Monthly字符串 /// </summary> /// <param name="format"> /// 如:yyyy/mm ; yy/mm ; yyyy年mm月 ;YYYY-Mm... /// 不区分大小写 /// </param> /// <returns></returns> public string ToString(string format = "yyyy/mm") { return Format(this, format); } /// <summary> /// 判断当前实例的值与给定实例的转换值是否相等 /// </summary> public override bool Equals(object obj) { if (obj is null) throw new ArgumentNullException("obj", "The parameter cannot be null."); if (obj is Monthly) return this == (Monthly)obj; if (obj is DateTime) return this == Monthly.FromDate((DateTime)obj); throw new ArgumentException("The parameter must be System.DateTime type or System.Monthly type .", "obj"); } public override int GetHashCode() { Int64 ticks = Tickes; return unchecked((int)ticks) ^ (int)(ticks >> 32); } private static string Format(Monthly m, string format) { string _y = m.Year.ToString(); string _m = m.Month.ToString(); format = format.ToLower(); if (!(format.Contains("yyyy") || format.Contains("yyyy")) && !(format.Contains("mm") || format.Contains("m"))) throw new ArgumentException("The format expression error. ", nameof(format)); if (format.Contains("yyyy")) format = format.Replace("yyyy", m.Year < 10 ? $"0{_y}" : _y); else if (format.Contains("yy")) format = format.Replace("yy", m.Year < 10 ? $"0{_y}" : _y.PadLeft(4, '0').Substring(2)); if (format.Contains("mm")) format = format.Replace("mm", m.Month < 10 ? $"0{_m}" : _m); else if (format.Contains("m")) format = format.Replace("m", _m.TrimStart('0')); return format; } #endregion } }
测试
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Pilipa.Utility.Test { [TestClass] public class MonthlyTest { [TestMethod] public void TestProps() { var tar = DateTime.Now; Monthly plan = Monthly.FromDate(tar); Assert.AreEqual(Monthly.Current, new Monthly(tar.Year, tar.Month)); Assert.AreEqual(plan.Year, tar.Year); Assert.AreEqual(plan.Month, tar.Month); Assert.AreEqual(plan.Dot, tar.Year * 100 + tar.Month); Assert.AreEqual(plan.Tickes, tar.Year * 12 + tar.Month); Assert.AreEqual(plan.First.ToDot(), tar.Year * 100 + 1); Assert.AreEqual(plan.Last.ToDot(), tar.Year * 100 + 12); Assert.AreEqual(plan.First.Previous.ToDot(), tar.AddYears(-1).Year * 100 + 12); Assert.AreEqual(plan.Last.Next.ToDot(), tar.AddYears(1).Year * 100 + 1); Assert.AreEqual(plan.Quarter, GetQuarter(tar.Month)); Assert.AreEqual(Monthly.MinValue, new Monthly(0, 1)); Assert.AreEqual(Monthly.MaxValue, new Monthly(9999, 12)); } private int GetQuarter(int q) { if (new System.Collections.Generic.List<int>() { 1, 2, 3 }.Contains(q)) return 1; if (new System.Collections.Generic.List<int>() { 4, 5, 6 }.Contains(q)) return 2; if (new System.Collections.Generic.List<int>() { 7, 8, 9 }.Contains(q)) return 3; if (new System.Collections.Generic.List<int>() { 10, 11, 12 }.Contains(q)) return 4; return 0; } [TestMethod] public void TestMethods() { Monthly plan = 201801; var tar = new DateTime(2018, 1, 1); var tip = false; //Dot Assert.AreEqual(new Monthly(0, 11), 11); Assert.AreEqual(new Monthly(1, 1), 101); Assert.AreEqual(new Monthly(100, 12), 10012); Assert.AreEqual(new Monthly(2018, 12), 201812); //Tickes Assert.AreEqual(((Monthly)101).Tickes, 13); Assert.AreEqual(((Monthly)201811).Tickes, 2018 * 12 + 11); //加月 Assert.AreEqual(plan.AddMonths(-1), 201712); Assert.AreEqual(plan.AddMonths(-23), 201602); Assert.AreEqual(plan.AddMonths(22), 201911); //加月(随机) for (int i = 0; i < 100; i++) { var rd = new Random(Guid.NewGuid().GetHashCode()).Next(100); Assert.AreEqual(plan.AddMonths(rd), Monthly.FromDate(tar.AddMonths(rd))); Assert.AreEqual(plan.AddMonths(rd).Dot, tar.AddMonths(rd).Year * 100 + tar.AddMonths(rd).Month); } //加年 Assert.IsTrue(plan.AddYears(6) == 202401); Assert.IsTrue(plan.AddYears(-18) == 200001); //加年(异常) try { var m = Monthly.Current.AddYears(-3000); } catch (Exception e) { if (e.Message.Contains("beteen 1 and 120000")) { tip = true; } } Assert.IsTrue(tip); //月份差 Assert.AreEqual(plan.SpanMonths(201711), 2); Assert.AreEqual(plan.SpanMonths(201902), -13); //比较大小 Assert.AreEqual(plan.CompareTo(201801), 0); Assert.AreEqual(plan.CompareTo(201701), 1); Assert.AreEqual(plan.CompareTo(202001), -1); //构造 Assert.AreEqual(Monthly.FromDot(3), 3); Assert.AreEqual(Monthly.FromTickes(13), 101); Assert.AreEqual(Monthly.FromDate(new DateTime(2018, 12, 12)), 201812); Assert.AreEqual(Monthly.FromString("2018/01"), 201801); Assert.AreEqual(Monthly.FromString("2018年01月"), 201801); Assert.AreEqual(Monthly.FromString("2018@01/01"), 201801); Assert.AreEqual(Monthly.FromString((new DateTime(2018, 1, 1)).ToString()), 201801); Assert.AreEqual(Monthly.FromString("3"), 3); //月份轴 var axis = Monthly.Axis(201711, 201901); Assert.IsTrue(axis.Count == 15); Assert.AreEqual(axis[0], 201711); Assert.AreEqual(axis[3], 201802); Assert.AreEqual(axis[14], 201901); axis = Monthly.Axis(201812, 201712); Assert.IsTrue(axis.Count == 13); Assert.AreEqual(axis[0], 201812); Assert.AreEqual(axis[12], 201712); //异常 tip = false; try { Monthly m = 201800; } catch (Exception e) { if (e.Message.Contains("correct dot format")) { tip = true; } } //dot format Assert.IsTrue(tip); tip = false; try { Monthly m = Monthly.FromDot(13); } catch (Exception e) { if (e.Message.Contains("correct dot format")) { tip = true; } } //13月 Assert.IsTrue(tip); tip = false; try { Monthly m = Monthly.FromTickes(999999); } catch (Exception e) { if (e.Message.Contains("must beteen 1 and 120000")) { tip = true; } } //越界 Assert.IsTrue(tip); tip = false; try { Monthly m = Monthly.FromString(null); } catch (Exception e) { if (e.Message.Contains("null or empty")) { tip = true; } } //IsNullOrEmpty Assert.IsTrue(tip); tip = false; try { Monthly m = Monthly.FromString("abc"); } catch (Exception e) { if (e.Message.Contains("parameters")) { tip = true; } } //格式错误 Assert.IsTrue(tip); tip = false; try { Monthly m = Monthly.FromString("88"); } catch (Exception e) { if (e.Message.Contains("must beteen")) { tip = true; } } //越界 Assert.IsTrue(tip); } [TestMethod] public void TestOps() { Monthly plan = 201801; var tar = Monthly.FromString("2018.01"); Assert.AreEqual(plan + 12, 201901); Assert.AreEqual(plan - 13, 201612); Assert.AreEqual(plan - (Monthly)201701, 12); Assert.AreEqual(plan - (new DateTime(2017, 12, 12)), 1); Assert.AreEqual(--plan, 201712); Assert.AreEqual(++plan, 201801); Assert.IsTrue(plan == Monthly.FromDot(201801)); Assert.IsTrue(plan != Monthly.FromDot(201802)); Assert.IsTrue(plan >= Monthly.FromDot(201801)); Assert.IsTrue(plan < Monthly.FromDot(201803)); } [TestMethod] public void TestOvr() { Monthly plan = 201801; var tar = Monthly.FromString("2018.01"); //哈希码(相同dot具有相同的哈希码) Assert.AreEqual(plan.GetHashCode(), tar.GetHashCode()); tar++; Assert.AreNotEqual(plan.GetHashCode(), tar.GetHashCode()); //格式化 Assert.AreEqual(plan.ToString(), "2018/01"); Assert.AreEqual(plan.ToString("yy/mm"), "18/01"); Assert.AreEqual(Monthly.FromDot(501).ToString("yy/mm"), "05/01"); Assert.AreEqual(plan.ToString("YYYY年m月"), "2018年1月"); Assert.AreEqual(plan.ToString("公元YyYy年mM月,哈哈..."), "公元2018年01月,哈哈..."); //比较相等 Assert.IsTrue(plan.Equals(Monthly.FromDot(201801))); Assert.IsTrue(plan.Equals(new DateTime(2018, 1, 1))); Assert.IsTrue(plan.Equals((object)Monthly.FromDot(201801))); Assert.IsFalse(plan.Equals(Monthly.FromDot(201901))); } } }
Monthly使用介绍
1.Monthly构造
//创建一个“2018年1月”的账期 Monthly m1 = 201801; Monthly m2 = new Monthly(2018, 1); Monthly m3 = Monthly.FromDate(new DateTime(2018, 1, 1)); Monthly m4 = Monthly.FromDot(201801); Monthly m5 = Monthly.FromTickes(2018 * 12 + 1); Monthly m6 = Monthly.FromString("2018年01月"); Monthly cur = Monthly.Current; //当前时间实例 Monthly min = Monthly.MinValue; //Monthly最小实例 Monthly max = Monthly.MaxValue; //Monthly最大实例
2. Monthly属性
属性 | 说明 |
---|---|
Year | 获取当前实例的年 |
Month | 获取当前实例的月 |
Dot | 获取当前实例的年月标记值,如2018年1月记为 : 201801 |
Tickes | 获取当前实例从公元零年一月开始的月份累计值 |
First | 获取当前年份的一月为依据的新实例 |
Last | 获取当前年份的十二月为依据的新实例 |
Previous | 获取当前时间点的上月为依据的新实例 |
Next | 获取当前时间点的下月为依据的新实例 |
Quarter | 获取当前实例所在的季度 |
3.Monthly方法
-
ToDot();
说明:获取当前实例的年月标记值,如2018年1月记为 : 201801
AddYears(int years)
说明:以当前实例与years的和值为依据创建一个新实例-
AddMonths(int months)
说明:以当前实例与months的和值为依据创建一个新实例
-
Equals(Monthly other)
说明:判断当前实例的值与给定实例的值是否相等
-
Equals(object obj)
说明:判断当前实例的值与给定实例的转换值是否相等,obj可以是DateTime类型
-
SpanMonths(Monthly other)
说明:获取当前实例与给定实例的月份差值
-
SpanMonths(DateTime date)
说明:获取当前实例与DateTime实例的月份差值
-
CompareTo(Monthly other)
说明:获取当前实例与给定实例的大小比较的结果标识, -1:小于other实例值 ; 0 等于other实例值 ; 1:大于other实例值
-
List<Monthly> Axis(int from, int to)
说明:获取一段时间内的Monthly数轴(包含开始与结束月份)
-
List<Monthly> Axis(Monthly from, Monthly to)
说明:同 ListAxis(int from, int to)
ToString(string format = "yyyy/mm")
说明:获取包含"Y、y、M、m"字符格式的自定义Monthly字符串,format 格式如:yyyy/mm ; yy/mm ; yyyy年mm月 ;YYYY-Mm...,不区分大小写
示例:
Monthly m = 201801; m.CompareTo(201701); m.Equals(DateTime.Now); m.Equals(201701); m.SpanMonths(new DateTime(2017, 1, 1)); m.SpanMonths(201701); m.ToString(); m.ToString("yy/mm"); Monthly.FromDot(501).ToString("yy/mm"); m.ToString("YYYY年m月"); m.ToString("公元YyYy年mM月,哈哈...");
4.Monthly操作符
Monthly支持+、- 、* 、/ 、> 、>= 、< 、<= 、++ 、-- 、== 、!=
运算符操作。
特别注意:
-
操作,他有operator -(Monthly m, int months)
和operator -(Monthly m1, Monthly m2)
两个重载版本,且方法功能不同,如果是第二个版本,则必须显式标注被减对象的数据类型,如m-(Monthly)201701
参考:
微软Datetime数据类型
推荐阅读
-
python基础知识(安装、数据类型、list列表、字典、函数、类、继承、文件操作)
-
MSSQL报错:参数数据类型 text 对于 replace 函数的参数 1 无效的解决办法
-
sql使用cast进行数据类型转换示例
-
SQL Server数据类型char、nchar、varchar、nvarchar的区别浅析
-
C# double和decimal数据类型以截断的方式保留指定的小数位数
-
Javascript ES6中数据类型Symbol的使用详解
-
PL/SQL数据类型及操作符
-
MySQL 数据类型 详解
-
计算机二级python学习教程(3) python语言基本数据类型
-
JavaScript的数据类型转换原则(干货)