不少人資系統有個有趣功能:顯示從到職日到今天你的年資已經有幾年幾個月又幾天。

熊熊想到:若是在 C# 這個邏輯要怎麼寫呢?

在 C#,我們將兩個 DateTime 相減可以得到 TimeSpan 結構,其中有 Days、Hours、Minutes、Seconds 可算出幾天幾小時幾秒 (另外有 TotalDays、TotalHours... 等,換算成單一單位帶小數的值):

不過,TimeSpan 的計量單元只到日,不支援月跟年。網路上看到有人想出看似精巧的解法:DateTime.MinValue 加上 TimeSpan,等於從西元 0001 年 1 月 1 日加上 TimeSpan 時間長度,再取 Years - 1、Months - 1、Days - 1 就等於幾年幾月幾天:
(註:怕有同學不認識,(int y, int m, int d) 這種寫法是 C# 7 加入的具名 ValueTuple,型別內部傳遞多組結果值很好用,程式也順便展示用 Tuple 解構對調變數 (st, ed) = (ed, st)。延伸閱讀:重新認識 C# - C# 7 ValueTuple重新認識 C# - C# 7 Tuple 解構)

(int y, int m, int d) CalcYMDByMinValue(DateTime st, DateTime ed) {
	// 若結束日早於起始,用 Tuple 解構二者對調
	if (ed.CompareTo(st) < 0) (st, ed) = (ed, st); 
	// 0001/01/01 加上 ed - st 的日期長度
	var d = DateTime.MinValue + (ed - st);
	// 傳回年、月、日
	return (d.Year - 1, d.Month - 1, d.Day - 1);
}	

看起來巧妙的做法實際上亂算一通,一戳就破:

DateTime st, DateTime ed, int y, int m, int d) CalcYMDByMinValue(DateTime st, DateTime ed) {
	if (ed.CompareTo(st) < 0) (st, ed) = (ed, st); 
	var d = DateTime.MinValue + (ed - st);
	return (st, ed, d.Year - 1, d.Month - 1, d.Day - 1);
}

void Main()
{
	var tests = new List<(DateTime st, DateTime ed)> {
		(new DateTime(2023, 1, 1), new DateTime(2024, 2, 1)),
		(new DateTime(2010, 1, 15), new DateTime(2020, 3, 15)),
		(new DateTime(2022, 11, 15), new DateTime(2023, 1, 21)),
		(new DateTime(2023, 2, 28), new DateTime(2024, 2, 28)),
		(new DateTime(2024, 2, 28), new DateTime(2025, 2, 28))
	};

	foreach (var data in tests) {
		Print(CalcYMDByMinValue(data.st, data.ed));
	}
}

void Print((DateTime from, DateTime to, int y, int m, int d) data) =>
	Console.WriteLine($"{data.from:yyyy/MM/dd} ~ {data.to:yyyy/MM/dd} 共 {data.y} 年 {data.m} 個月又 {data.d} 天");		

這個算法沒考慮每個月天數不等、也忽略閏年二月天數,會得到一堆離譜結果:

照慣例,原本這時該開始想一版自己想個演算法,但都 AI 年代了:

thumbnail

問了 ChatGPT,他先鬼扯「用 TotalDays、TotalYears 再用 DateTime 相關方法可求出相差月份跟天數」,但程式範例倒是有模有樣:

博學的 ChatGPT 知道用 DateTime.DaysInMonth() 取得某年某月的天數(這個我沒學過,趕緊筆記),架勢十足... 來驗證一下:

(DateTime st, DateTime ed, int y, int m, int d) CalcYMDByChatGPT(DateTime start, DateTime end) {

	if (end.CompareTo(start) < 0) (start, end) = (end, start); 
	
	int years = (end.Year - start.Year);
	int months = (end.Month - start.Month);
	int days = (end.Day - start.Day);

	if (days < 0)
	{
	    months--;
	    days += DateTime.DaysInMonth(end.Year, end.Month);
	}

	if (months < 0)
	{
	    years--;
	    months += 12;
	}
	return (start, end, years, months, days);
}

void Main()
{
	var tests = new List<(DateTime st, DateTime ed)> {
		(new DateTime(2023, 1, 1), new DateTime(2024, 2, 1)),
		(new DateTime(2010, 1, 15), new DateTime(2020, 3, 15)),
		(new DateTime(2022, 11, 15), new DateTime(2023, 1, 21)),
		(new DateTime(2023, 2, 28), new DateTime(2024, 2, 28)),
		(new DateTime(2024, 2, 28), new DateTime(2025, 2, 28)),
		(new DateTime(2021, 5, 15), new DateTime(2022, 4, 15)),
		(new DateTime(2022, 1, 28), new DateTime(2022, 3, 1))		
	};

	foreach (var data in tests) {
		Print(CalcYMDByChatGPT(data.st, data.ed));
	}
}

基本上是對的,但遇到 end.Day - start.Day 小於零,計算到月底天數 ChaptGPT 抓錯月份了,取 2 月 28 天時格外明顯。

只要能看懂邏輯,我們很快可以找出問題,小改一下程式:

	if (days < 0)
	{
	    months--;
		var lastMon = end.AddMonths(-1);
	    days += DateTime.DaysInMonth(lastMon.Year, lastMon.Month);
	}

搞定!!

【心得】

這回 ChatGPT 輔助程式開發的經驗不錯,相較之前問過的冷門問題(SharePoint、Outlook VBA)總得到瞎扯亂回答的經驗,常見需求獲得正確解答的機率高多了。

常見的演算法議題,即便 ChatGPT 沒給出正確解答,但提供的範例多半簡潔易懂還常接近正解,有能力解讀程式碼應不難改到可正確執行,或從中獲得啟發寫出自己風格的版本。這回它露了一手用 DateTime.DaysInMonth() 取某年某月的總天數,讓我意外學會新技巧。 現階段 ChatGPT 所能提供的程式碼建議,不完美但有一定精確度,尤其知識廣度上常帶來驚喜。依此特性,ChatGPT 對熟練開發者的價值遠大於缺乏經驗技能的初學者。

因此,現階段較好的策略還是先採傳統方式學習奠定基礎,之後再靠 ChatGPT 省時省力加速成長,尤其從範例學到新 API、新做法時多找些相關資料補充,讓知識區塊更厚實穩固,將可發揮 ChatGPT 更大的價值。

Discussion of how to get years, months and days between two DateTime. This time, let's use ChatGPT to help us to solve the problem.


Comments

# by Fatina

年資(包括但不限於年終)的計算上好像每一家公司都會有差別? 印象中我看過法令是寫一個月就是30天,沒有分大月小月, 所以一年就是360天。 這塊人資或是說發薪水的人應該會比較熟…

# by 鳥毅

敝公司的MIS說,還有留職停薪/育嬰假、離開2年餘又回來的同仁年資計算...

# by Jeffrey

to 鳥毅,乍想之下覺得好難,但後來想到,找出每段「到職/復職」到「留停/育嬰/離職」的日期區間分別計算再加總,應該就是答案,呵。

Post a comment