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
參考:
https://referencesource.microsoft.com/#mscorlib/system/datetime.cs,df6b1eba7461813b 微軟Datetime數據類型