diff --git a/src/SheetMe.Core/Catalog/ResidentNumber.cs b/src/SheetMe.Core/Catalog/ResidentNumber.cs
index 881ad0a..a253f99 100644
--- a/src/SheetMe.Core/Catalog/ResidentNumber.cs
+++ b/src/SheetMe.Core/Catalog/ResidentNumber.cs
@@ -105,5 +105,48 @@ public static class ResidentNumber
}
private static string Clean(string? value) => (value ?? string.Empty).Trim();
+
+ ///
+ /// 생년월일부터 오늘까지의 개월수 — 레거시 PAT_나이_개월수
+ /// (DateDiff(Month, PatBthDay, SystemDate) - 1).
+ ///
+ /// VB 의 DateDiff(Month, ...) 는 날짜를 보지 않고 월 경계만 센다
+ /// (연차×12 + 월차). 거기서 1을 뺀 값이 태그의 결과다.
+ /// 일수로 환산하지 않는다 — 환산하면 레거시와 다른 수가 나온다.
+ ///
+ public static string MonthsSince(string? birthDay, string? todayYmd)
+ {
+ var birth = Clean(birthDay);
+ var today = Clean(todayYmd);
+ if (birth.Length < 6 || today.Length < 6
+ || !int.TryParse(birth[..4], out var by) || !int.TryParse(birth.Substring(4, 2), out var bm)
+ || !int.TryParse(today[..4], out var ty) || !int.TryParse(today.Substring(4, 2), out var tm))
+ {
+ return string.Empty;
+ }
+ return (((ty - by) * 12) + (tm - bm) - 1).ToString();
+ }
+
+ ///
+ /// "N개월 N일" — 레거시 PAT_나이_00개월_00일.
+ ///
+ /// 달력이 아니라 30일로 나눈다(days \ 30, days Mod 30).
+ /// 실제 개월수와 다르지만 레거시가 그렇게 찍는다 — 달력으로 고치면 값이 달라진다.
+ ///
+ public static string MonthsDaysSince(string? birthDay, string? todayYmd)
+ {
+ var birth = Clean(birthDay);
+ var today = Clean(todayYmd);
+ if (birth.Length < 8 || today.Length < 8
+ || !DateTime.TryParseExact(birth[..8], "yyyyMMdd", null,
+ System.Globalization.DateTimeStyles.None, out var from)
+ || !DateTime.TryParseExact(today[..8], "yyyyMMdd", null,
+ System.Globalization.DateTimeStyles.None, out var to))
+ {
+ return string.Empty;
+ }
+ var days = (int)(to - from).TotalDays;
+ return $"{days / 30}개월 {days % 30}일";
+ }
#endregion
}
diff --git a/tests/SheetMe.Core.Tests/ResidentNumberTests.cs b/tests/SheetMe.Core.Tests/ResidentNumberTests.cs
index bcdd0de..5830286 100644
--- a/tests/SheetMe.Core.Tests/ResidentNumberTests.cs
+++ b/tests/SheetMe.Core.Tests/ResidentNumberTests.cs
@@ -133,6 +133,31 @@ public sealed class ResidentNumberTests
Assert.AreEqual(string.Empty, ResidentNumber.SexKoreanOf("880101"));
}
+ [TestMethod]
+ public void 개월수는_월_경계만_센다()
+ {
+ // VB DateDiff(Month, ...) 는 날짜를 보지 않는다 — 연차×12 + 월차, 거기서 1을 뺀다.
+ // 일수로 환산하면 레거시와 다른 수가 나온다.
+ // 2025-01 → 2025-12 는 월 경계 11, 거기서 1을 뺀 10 이다(기대값을 처음에 11로 잘못 적었다)
+ Assert.AreEqual("10", ResidentNumber.MonthsSince("20250101", "20251201"));
+ // 같은 달이면 -1 이 된다(레거시 그대로 — 음수도 그대로 찍힌다)
+ Assert.AreEqual("-1", ResidentNumber.MonthsSince("20260101", "20260101"));
+ // 날짜는 보지 않는다: 1일생과 말일생이 같은 값
+ Assert.AreEqual(ResidentNumber.MonthsSince("20250101", "20260101"),
+ ResidentNumber.MonthsSince("20250131", "20260101"));
+ Assert.AreEqual(string.Empty, ResidentNumber.MonthsSince("2025", "20260101"));
+ }
+
+ [TestMethod]
+ public void 개월_일은_달력이_아니라_30일로_나눈다()
+ {
+ // 레거시가 days \ 30 · days Mod 30 이다. 달력으로 고치면 값이 달라진다.
+ Assert.AreEqual("0개월 10일", ResidentNumber.MonthsDaysSince("20260101", "20260111"));
+ Assert.AreEqual("1개월 1일", ResidentNumber.MonthsDaysSince("20260101", "20260201"));
+ Assert.AreEqual("12개월 5일", ResidentNumber.MonthsDaysSince("20250101", "20260101"));
+ Assert.AreEqual(string.Empty, ResidentNumber.MonthsDaysSince("2026", "20260101"));
+ }
+
[TestMethod]
public void 하이픈은_양쪽_공백을_지킨다()
{