Find the next monthly expiration date
$begingroup$
I have an agreement with a customer. When the agreement ends, the customer needs to pay. The amount which the customer needs to pay increases for each month that has passed since the agreement was initiated. The maximum duration is 3 years, so I have a final expiration date. From this final expiration date, I must be able to find the periodic expiration date (the date when the payment increases). In other words, I need a function which does this:
DateTime GetNextExpirationDate(finalExpirationDate)
One of the issues which I am struggling with is that the algorithm needs to consider whether the expiration date in the current month has already passed (there will be a date each month). It also needs to consider that the day of the month for the final expiration date, might not exist for the periodic expiration dates (e.g. if the final expiration date is the 31st of something).
I have some code which is working, but it appears somewhat redundant and with many steps. I was wondering, if perhaps it could be simplified (either a little or a lot).
public DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
int daysInCurrentMonth = DateTime.DaysInMonth(currentYear, currentMonth);
// If the month has less days that the day of the final expiration date,
// then we need to use the maximum date of the month.
int expirationDayThisMonth = Math.Min(expirationDay, daysInCurrentMonth);
DateTime expirationDateThisMonth = new DateTime(currentYear, currentMonth, expirationDayThisMonth);
// If the periodic expiration date in the current month is already surpassed,
// then we need to the date in the next month.
if (expirationDateThisMonth < DateTime.Now)
{
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
int nextMonthMonth = dateInOneMonth.Month;
int nextMonthYear = dateInOneMonth.Year;
int daysInNextMonth = DateTime.DaysInMonth(nextMonthYear, nextMonthMonth);
int expirationDayNextMonth = Math.Min(expirationDay, daysInNextMonth);
DateTime expirationDateNextMonth = new DateTime(nextMonthYear, nextMonthMonth, expirationDayNextMonth);
return expirationDateNextMonth;
}
else
{
return expirationDateThisMonth;
}
}
Any ways to improve this?
Input/output examples:
Final expiration date: 15.12.2019
DateTime.Now: 12.03.2019
Output: 15.03.2019
Final expiration date 11.10.2019
DateTime.Now: 16.04.2019
Output: 11.05.2019
(I know that I cannot change DateTime.Now but it does work as an pseudo-input, which is why I included it).
c# .net datetime
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
I have an agreement with a customer. When the agreement ends, the customer needs to pay. The amount which the customer needs to pay increases for each month that has passed since the agreement was initiated. The maximum duration is 3 years, so I have a final expiration date. From this final expiration date, I must be able to find the periodic expiration date (the date when the payment increases). In other words, I need a function which does this:
DateTime GetNextExpirationDate(finalExpirationDate)
One of the issues which I am struggling with is that the algorithm needs to consider whether the expiration date in the current month has already passed (there will be a date each month). It also needs to consider that the day of the month for the final expiration date, might not exist for the periodic expiration dates (e.g. if the final expiration date is the 31st of something).
I have some code which is working, but it appears somewhat redundant and with many steps. I was wondering, if perhaps it could be simplified (either a little or a lot).
public DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
int daysInCurrentMonth = DateTime.DaysInMonth(currentYear, currentMonth);
// If the month has less days that the day of the final expiration date,
// then we need to use the maximum date of the month.
int expirationDayThisMonth = Math.Min(expirationDay, daysInCurrentMonth);
DateTime expirationDateThisMonth = new DateTime(currentYear, currentMonth, expirationDayThisMonth);
// If the periodic expiration date in the current month is already surpassed,
// then we need to the date in the next month.
if (expirationDateThisMonth < DateTime.Now)
{
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
int nextMonthMonth = dateInOneMonth.Month;
int nextMonthYear = dateInOneMonth.Year;
int daysInNextMonth = DateTime.DaysInMonth(nextMonthYear, nextMonthMonth);
int expirationDayNextMonth = Math.Min(expirationDay, daysInNextMonth);
DateTime expirationDateNextMonth = new DateTime(nextMonthYear, nextMonthMonth, expirationDayNextMonth);
return expirationDateNextMonth;
}
else
{
return expirationDateThisMonth;
}
}
Any ways to improve this?
Input/output examples:
Final expiration date: 15.12.2019
DateTime.Now: 12.03.2019
Output: 15.03.2019
Final expiration date 11.10.2019
DateTime.Now: 16.04.2019
Output: 11.05.2019
(I know that I cannot change DateTime.Now but it does work as an pseudo-input, which is why I included it).
c# .net datetime
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
I have an agreement with a customer. When the agreement ends, the customer needs to pay. The amount which the customer needs to pay increases for each month that has passed since the agreement was initiated. The maximum duration is 3 years, so I have a final expiration date. From this final expiration date, I must be able to find the periodic expiration date (the date when the payment increases). In other words, I need a function which does this:
DateTime GetNextExpirationDate(finalExpirationDate)
One of the issues which I am struggling with is that the algorithm needs to consider whether the expiration date in the current month has already passed (there will be a date each month). It also needs to consider that the day of the month for the final expiration date, might not exist for the periodic expiration dates (e.g. if the final expiration date is the 31st of something).
I have some code which is working, but it appears somewhat redundant and with many steps. I was wondering, if perhaps it could be simplified (either a little or a lot).
public DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
int daysInCurrentMonth = DateTime.DaysInMonth(currentYear, currentMonth);
// If the month has less days that the day of the final expiration date,
// then we need to use the maximum date of the month.
int expirationDayThisMonth = Math.Min(expirationDay, daysInCurrentMonth);
DateTime expirationDateThisMonth = new DateTime(currentYear, currentMonth, expirationDayThisMonth);
// If the periodic expiration date in the current month is already surpassed,
// then we need to the date in the next month.
if (expirationDateThisMonth < DateTime.Now)
{
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
int nextMonthMonth = dateInOneMonth.Month;
int nextMonthYear = dateInOneMonth.Year;
int daysInNextMonth = DateTime.DaysInMonth(nextMonthYear, nextMonthMonth);
int expirationDayNextMonth = Math.Min(expirationDay, daysInNextMonth);
DateTime expirationDateNextMonth = new DateTime(nextMonthYear, nextMonthMonth, expirationDayNextMonth);
return expirationDateNextMonth;
}
else
{
return expirationDateThisMonth;
}
}
Any ways to improve this?
Input/output examples:
Final expiration date: 15.12.2019
DateTime.Now: 12.03.2019
Output: 15.03.2019
Final expiration date 11.10.2019
DateTime.Now: 16.04.2019
Output: 11.05.2019
(I know that I cannot change DateTime.Now but it does work as an pseudo-input, which is why I included it).
c# .net datetime
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
I have an agreement with a customer. When the agreement ends, the customer needs to pay. The amount which the customer needs to pay increases for each month that has passed since the agreement was initiated. The maximum duration is 3 years, so I have a final expiration date. From this final expiration date, I must be able to find the periodic expiration date (the date when the payment increases). In other words, I need a function which does this:
DateTime GetNextExpirationDate(finalExpirationDate)
One of the issues which I am struggling with is that the algorithm needs to consider whether the expiration date in the current month has already passed (there will be a date each month). It also needs to consider that the day of the month for the final expiration date, might not exist for the periodic expiration dates (e.g. if the final expiration date is the 31st of something).
I have some code which is working, but it appears somewhat redundant and with many steps. I was wondering, if perhaps it could be simplified (either a little or a lot).
public DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
int daysInCurrentMonth = DateTime.DaysInMonth(currentYear, currentMonth);
// If the month has less days that the day of the final expiration date,
// then we need to use the maximum date of the month.
int expirationDayThisMonth = Math.Min(expirationDay, daysInCurrentMonth);
DateTime expirationDateThisMonth = new DateTime(currentYear, currentMonth, expirationDayThisMonth);
// If the periodic expiration date in the current month is already surpassed,
// then we need to the date in the next month.
if (expirationDateThisMonth < DateTime.Now)
{
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
int nextMonthMonth = dateInOneMonth.Month;
int nextMonthYear = dateInOneMonth.Year;
int daysInNextMonth = DateTime.DaysInMonth(nextMonthYear, nextMonthMonth);
int expirationDayNextMonth = Math.Min(expirationDay, daysInNextMonth);
DateTime expirationDateNextMonth = new DateTime(nextMonthYear, nextMonthMonth, expirationDayNextMonth);
return expirationDateNextMonth;
}
else
{
return expirationDateThisMonth;
}
}
Any ways to improve this?
Input/output examples:
Final expiration date: 15.12.2019
DateTime.Now: 12.03.2019
Output: 15.03.2019
Final expiration date 11.10.2019
DateTime.Now: 16.04.2019
Output: 11.05.2019
(I know that I cannot change DateTime.Now but it does work as an pseudo-input, which is why I included it).
c# .net datetime
c# .net datetime
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
edited 19 hours ago
200_success
130k16153417
130k16153417
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
asked 22 hours ago
NoceoNoceo
1264
1264
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
Noceo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
add a comment |
add a comment |
4 Answers
4
active
oldest
votes
$begingroup$
When working with DateTime it's usually a good idea to not use a hardcoded DateTime.Now. This makes testing such methods very difficult because you cannot mock it so you have to write very tricky tests all with Now. You allso cannot easily switch to UtcNow etc. and you have to know which now it uses internally.
A much better solution would be to use another parameter like DateTime referenceDate. This way you can use any combinations of the two values and create more predictable and easier to understand tests.
Additionally I'd make this method static because it does not access any class fields etc.
Oh, I also need to add that you use excelent variable names! This is exactly how it should be done! ;-)
$endgroup$
$begingroup$
Very good points. Especially theDateTime.Nowone, since my primary personal goal is to get better at writing testable code :-). Thanks!
$endgroup$
– Noceo
21 hours ago
add a comment |
$begingroup$
I would try to clean it up by extracting the tricky and repetitive day truncation logic into a reusable function. This would result in something like:
/* Creates a DateTime instance for the given year, month and day.
* It will truncate the possiblyTruncatedDay down to the largest number possible
* for the given month in case if it's out of bounds.
* Eg. trying to create an illegal Feb 31 instance will fallback to Feb 28 or 29) */
private static DateTime CreateTruncating(int year, int month, int possiblyTruncatedDay)
{
return new DateTime(
year,
month,
Math.Min(possiblyTruncatedDay, DateTime.DaysInMonth(year, month)));
}
public static DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
DateTime expirationDateThisMonth = CreateTruncating(currentYear, currentMonth, expirationDay);
if (expirationDateThisMonth >= DateTime.Now)
{
return expirationDateThisMonth;
}
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
return CreateTruncating(dateInOneMonth.Year, dateInOneMonth.Month, expirationDay);
}
$endgroup$
$begingroup$
Not a bad idea. Would it make sense to nest theCreateTruncating()method insideGetNextExpirationDate, since it is exclusively used by this method?
$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
add a comment |
$begingroup$
Naming
Expiration vs Final Expiration seems kind of confusing, imo. Might I suggest PenaltyDate for when amount they owe increases, and ExpirationDate for that final date that they must pay.
Testability
As others have mentioned, passing in a referenceDate will allow you test more easily test outcomes.
Modularity
Breaking the redundant part into a separate function like others have suggested.
Bringing it all together
public static DateTime GetNextPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
var penaltyDate = GetPenaltyDate(expirationDate, referenceDate);
if (penaltyDate < referenceDate)
{
var nextPenaltyDate = GetPenaltyDate(expirationDate, referenceDate.AddMonths(1));
return nextPenaltyDate;
}
else
{
return penaltyDate;
}
}
private static DateTime GetPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
int daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
int penaltyDay = Math.Min(expirationDate.Day, daysInMonth);
return new DateTime(referenceDate.Year, referenceDate.Month, penaltyDay);
}
Testing Mine via Console
class Program
{
static void Main(string args)
{
Print(
expirationDate: new DateTime(2019, 12, 15),
referenceDate: new DateTime(2019, 03, 12)
);
Print(
expirationDate: new DateTime(2019, 10, 11),
referenceDate: new DateTime(2019, 04, 16)
);
Console.ReadKey();
}
private static void Print(DateTime expirationDate, DateTime referenceDate)
{
Console.WriteLine("Final Expiration Date: {0}", expirationDate);
Console.WriteLine("Reference Date: {0}", referenceDate);
Console.WriteLine("Output: {0}", Expiration.GetNextPenaltyDate(expirationDate, referenceDate));
}
}
My Results
My output is formatted differently, but if I'm not mistaken the data matches.
Final Expiration Date: 12/15/2019 12:00:00 AM
Reference Date: 3/12/2019 12:00:00 AM
Output: 3/15/2019 12:00:00 AM
Final Expiration Date: 10/11/2019 12:00:00 AM
Reference Date: 4/16/2019 12:00:00 AM
Output: 5/11/2019 12:00:00 AM
$endgroup$
add a comment |
$begingroup$
Potentially simpler version?
/// <summary>
/// Number of calendar months between two DateTimes, rounding down partial months.
/// Will return x such that startDate.AddMonths(x) < endDate and
/// startDate.AddMonths(x+1) >= endDate.
/// Ignores time components.
/// </summary>
/// <param name="startDate">Start Date</param>
/// <param name="endDate">End Date</param>
/// <returns>Number of Months between Start Date and End Date</returns>
public static int MonthsBetween(DateTime startDate, DateTime endDate)
{
return (endDate.Year - startDate.Year) * 12 +
(endDate.Month - startDate.Month) +
((endDate.Day >= startDate.Day) ? 0 : -1); //rounding down for partial months
}
/// <summary>
/// Gets the next monthly expiration date after the Reference Date
/// with day component aligned with Final Expiration Date
/// Ignores time components.
/// </summary>
/// <param name="finalExpirationDate">Final Expiration Date</param>
/// <param name="referenceDate">Reference Date</param>
/// <returns>Next monthly expiration date after Reference Date</returns>
public DateTime GetNextExpirationDate(DateTime finalExpirationDate, DateTime referenceDate)
{
return finalExpirationDate.AddMonths(-MonthsBetween(referenceDate, finalExpirationDate));
}
It's unclear from your description what you want to do if today is an expiration date. The code above will generate today's date in that scenario, which is what your code does. If you want to generate next month's expiration date in that scenario, change the >= to a > in MonthsBetween (and adjust the comments as appropriate, of course).
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Noceo is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214700%2ffind-the-next-monthly-expiration-date%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
When working with DateTime it's usually a good idea to not use a hardcoded DateTime.Now. This makes testing such methods very difficult because you cannot mock it so you have to write very tricky tests all with Now. You allso cannot easily switch to UtcNow etc. and you have to know which now it uses internally.
A much better solution would be to use another parameter like DateTime referenceDate. This way you can use any combinations of the two values and create more predictable and easier to understand tests.
Additionally I'd make this method static because it does not access any class fields etc.
Oh, I also need to add that you use excelent variable names! This is exactly how it should be done! ;-)
$endgroup$
$begingroup$
Very good points. Especially theDateTime.Nowone, since my primary personal goal is to get better at writing testable code :-). Thanks!
$endgroup$
– Noceo
21 hours ago
add a comment |
$begingroup$
When working with DateTime it's usually a good idea to not use a hardcoded DateTime.Now. This makes testing such methods very difficult because you cannot mock it so you have to write very tricky tests all with Now. You allso cannot easily switch to UtcNow etc. and you have to know which now it uses internally.
A much better solution would be to use another parameter like DateTime referenceDate. This way you can use any combinations of the two values and create more predictable and easier to understand tests.
Additionally I'd make this method static because it does not access any class fields etc.
Oh, I also need to add that you use excelent variable names! This is exactly how it should be done! ;-)
$endgroup$
$begingroup$
Very good points. Especially theDateTime.Nowone, since my primary personal goal is to get better at writing testable code :-). Thanks!
$endgroup$
– Noceo
21 hours ago
add a comment |
$begingroup$
When working with DateTime it's usually a good idea to not use a hardcoded DateTime.Now. This makes testing such methods very difficult because you cannot mock it so you have to write very tricky tests all with Now. You allso cannot easily switch to UtcNow etc. and you have to know which now it uses internally.
A much better solution would be to use another parameter like DateTime referenceDate. This way you can use any combinations of the two values and create more predictable and easier to understand tests.
Additionally I'd make this method static because it does not access any class fields etc.
Oh, I also need to add that you use excelent variable names! This is exactly how it should be done! ;-)
$endgroup$
When working with DateTime it's usually a good idea to not use a hardcoded DateTime.Now. This makes testing such methods very difficult because you cannot mock it so you have to write very tricky tests all with Now. You allso cannot easily switch to UtcNow etc. and you have to know which now it uses internally.
A much better solution would be to use another parameter like DateTime referenceDate. This way you can use any combinations of the two values and create more predictable and easier to understand tests.
Additionally I'd make this method static because it does not access any class fields etc.
Oh, I also need to add that you use excelent variable names! This is exactly how it should be done! ;-)
edited 19 hours ago
answered 21 hours ago
t3chb0tt3chb0t
34.9k752123
34.9k752123
$begingroup$
Very good points. Especially theDateTime.Nowone, since my primary personal goal is to get better at writing testable code :-). Thanks!
$endgroup$
– Noceo
21 hours ago
add a comment |
$begingroup$
Very good points. Especially theDateTime.Nowone, since my primary personal goal is to get better at writing testable code :-). Thanks!
$endgroup$
– Noceo
21 hours ago
$begingroup$
Very good points. Especially the
DateTime.Now one, since my primary personal goal is to get better at writing testable code :-). Thanks!$endgroup$
– Noceo
21 hours ago
$begingroup$
Very good points. Especially the
DateTime.Now one, since my primary personal goal is to get better at writing testable code :-). Thanks!$endgroup$
– Noceo
21 hours ago
add a comment |
$begingroup$
I would try to clean it up by extracting the tricky and repetitive day truncation logic into a reusable function. This would result in something like:
/* Creates a DateTime instance for the given year, month and day.
* It will truncate the possiblyTruncatedDay down to the largest number possible
* for the given month in case if it's out of bounds.
* Eg. trying to create an illegal Feb 31 instance will fallback to Feb 28 or 29) */
private static DateTime CreateTruncating(int year, int month, int possiblyTruncatedDay)
{
return new DateTime(
year,
month,
Math.Min(possiblyTruncatedDay, DateTime.DaysInMonth(year, month)));
}
public static DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
DateTime expirationDateThisMonth = CreateTruncating(currentYear, currentMonth, expirationDay);
if (expirationDateThisMonth >= DateTime.Now)
{
return expirationDateThisMonth;
}
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
return CreateTruncating(dateInOneMonth.Year, dateInOneMonth.Month, expirationDay);
}
$endgroup$
$begingroup$
Not a bad idea. Would it make sense to nest theCreateTruncating()method insideGetNextExpirationDate, since it is exclusively used by this method?
$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
add a comment |
$begingroup$
I would try to clean it up by extracting the tricky and repetitive day truncation logic into a reusable function. This would result in something like:
/* Creates a DateTime instance for the given year, month and day.
* It will truncate the possiblyTruncatedDay down to the largest number possible
* for the given month in case if it's out of bounds.
* Eg. trying to create an illegal Feb 31 instance will fallback to Feb 28 or 29) */
private static DateTime CreateTruncating(int year, int month, int possiblyTruncatedDay)
{
return new DateTime(
year,
month,
Math.Min(possiblyTruncatedDay, DateTime.DaysInMonth(year, month)));
}
public static DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
DateTime expirationDateThisMonth = CreateTruncating(currentYear, currentMonth, expirationDay);
if (expirationDateThisMonth >= DateTime.Now)
{
return expirationDateThisMonth;
}
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
return CreateTruncating(dateInOneMonth.Year, dateInOneMonth.Month, expirationDay);
}
$endgroup$
$begingroup$
Not a bad idea. Would it make sense to nest theCreateTruncating()method insideGetNextExpirationDate, since it is exclusively used by this method?
$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
add a comment |
$begingroup$
I would try to clean it up by extracting the tricky and repetitive day truncation logic into a reusable function. This would result in something like:
/* Creates a DateTime instance for the given year, month and day.
* It will truncate the possiblyTruncatedDay down to the largest number possible
* for the given month in case if it's out of bounds.
* Eg. trying to create an illegal Feb 31 instance will fallback to Feb 28 or 29) */
private static DateTime CreateTruncating(int year, int month, int possiblyTruncatedDay)
{
return new DateTime(
year,
month,
Math.Min(possiblyTruncatedDay, DateTime.DaysInMonth(year, month)));
}
public static DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
DateTime expirationDateThisMonth = CreateTruncating(currentYear, currentMonth, expirationDay);
if (expirationDateThisMonth >= DateTime.Now)
{
return expirationDateThisMonth;
}
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
return CreateTruncating(dateInOneMonth.Year, dateInOneMonth.Month, expirationDay);
}
$endgroup$
I would try to clean it up by extracting the tricky and repetitive day truncation logic into a reusable function. This would result in something like:
/* Creates a DateTime instance for the given year, month and day.
* It will truncate the possiblyTruncatedDay down to the largest number possible
* for the given month in case if it's out of bounds.
* Eg. trying to create an illegal Feb 31 instance will fallback to Feb 28 or 29) */
private static DateTime CreateTruncating(int year, int month, int possiblyTruncatedDay)
{
return new DateTime(
year,
month,
Math.Min(possiblyTruncatedDay, DateTime.DaysInMonth(year, month)));
}
public static DateTime GetNextExpirationDate(DateTime finalExpirationDate)
{
int expirationDay = finalExpirationDate.Day;
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
DateTime expirationDateThisMonth = CreateTruncating(currentYear, currentMonth, expirationDay);
if (expirationDateThisMonth >= DateTime.Now)
{
return expirationDateThisMonth;
}
DateTime dateInOneMonth = DateTime.Now.AddMonths(1);
return CreateTruncating(dateInOneMonth.Year, dateInOneMonth.Month, expirationDay);
}
answered 19 hours ago
Konrad MorawskiKonrad Morawski
1,808711
1,808711
$begingroup$
Not a bad idea. Would it make sense to nest theCreateTruncating()method insideGetNextExpirationDate, since it is exclusively used by this method?
$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
add a comment |
$begingroup$
Not a bad idea. Would it make sense to nest theCreateTruncating()method insideGetNextExpirationDate, since it is exclusively used by this method?
$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
$begingroup$
Not a bad idea. Would it make sense to nest the
CreateTruncating() method inside GetNextExpirationDate, since it is exclusively used by this method?$endgroup$
– Noceo
13 hours ago
$begingroup$
Not a bad idea. Would it make sense to nest the
CreateTruncating() method inside GetNextExpirationDate, since it is exclusively used by this method?$endgroup$
– Noceo
13 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
$begingroup$
A matter of taste; private methods are often only called in just one place... To me it's about whatever you find more readable
$endgroup$
– Konrad Morawski
10 hours ago
add a comment |
$begingroup$
Naming
Expiration vs Final Expiration seems kind of confusing, imo. Might I suggest PenaltyDate for when amount they owe increases, and ExpirationDate for that final date that they must pay.
Testability
As others have mentioned, passing in a referenceDate will allow you test more easily test outcomes.
Modularity
Breaking the redundant part into a separate function like others have suggested.
Bringing it all together
public static DateTime GetNextPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
var penaltyDate = GetPenaltyDate(expirationDate, referenceDate);
if (penaltyDate < referenceDate)
{
var nextPenaltyDate = GetPenaltyDate(expirationDate, referenceDate.AddMonths(1));
return nextPenaltyDate;
}
else
{
return penaltyDate;
}
}
private static DateTime GetPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
int daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
int penaltyDay = Math.Min(expirationDate.Day, daysInMonth);
return new DateTime(referenceDate.Year, referenceDate.Month, penaltyDay);
}
Testing Mine via Console
class Program
{
static void Main(string args)
{
Print(
expirationDate: new DateTime(2019, 12, 15),
referenceDate: new DateTime(2019, 03, 12)
);
Print(
expirationDate: new DateTime(2019, 10, 11),
referenceDate: new DateTime(2019, 04, 16)
);
Console.ReadKey();
}
private static void Print(DateTime expirationDate, DateTime referenceDate)
{
Console.WriteLine("Final Expiration Date: {0}", expirationDate);
Console.WriteLine("Reference Date: {0}", referenceDate);
Console.WriteLine("Output: {0}", Expiration.GetNextPenaltyDate(expirationDate, referenceDate));
}
}
My Results
My output is formatted differently, but if I'm not mistaken the data matches.
Final Expiration Date: 12/15/2019 12:00:00 AM
Reference Date: 3/12/2019 12:00:00 AM
Output: 3/15/2019 12:00:00 AM
Final Expiration Date: 10/11/2019 12:00:00 AM
Reference Date: 4/16/2019 12:00:00 AM
Output: 5/11/2019 12:00:00 AM
$endgroup$
add a comment |
$begingroup$
Naming
Expiration vs Final Expiration seems kind of confusing, imo. Might I suggest PenaltyDate for when amount they owe increases, and ExpirationDate for that final date that they must pay.
Testability
As others have mentioned, passing in a referenceDate will allow you test more easily test outcomes.
Modularity
Breaking the redundant part into a separate function like others have suggested.
Bringing it all together
public static DateTime GetNextPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
var penaltyDate = GetPenaltyDate(expirationDate, referenceDate);
if (penaltyDate < referenceDate)
{
var nextPenaltyDate = GetPenaltyDate(expirationDate, referenceDate.AddMonths(1));
return nextPenaltyDate;
}
else
{
return penaltyDate;
}
}
private static DateTime GetPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
int daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
int penaltyDay = Math.Min(expirationDate.Day, daysInMonth);
return new DateTime(referenceDate.Year, referenceDate.Month, penaltyDay);
}
Testing Mine via Console
class Program
{
static void Main(string args)
{
Print(
expirationDate: new DateTime(2019, 12, 15),
referenceDate: new DateTime(2019, 03, 12)
);
Print(
expirationDate: new DateTime(2019, 10, 11),
referenceDate: new DateTime(2019, 04, 16)
);
Console.ReadKey();
}
private static void Print(DateTime expirationDate, DateTime referenceDate)
{
Console.WriteLine("Final Expiration Date: {0}", expirationDate);
Console.WriteLine("Reference Date: {0}", referenceDate);
Console.WriteLine("Output: {0}", Expiration.GetNextPenaltyDate(expirationDate, referenceDate));
}
}
My Results
My output is formatted differently, but if I'm not mistaken the data matches.
Final Expiration Date: 12/15/2019 12:00:00 AM
Reference Date: 3/12/2019 12:00:00 AM
Output: 3/15/2019 12:00:00 AM
Final Expiration Date: 10/11/2019 12:00:00 AM
Reference Date: 4/16/2019 12:00:00 AM
Output: 5/11/2019 12:00:00 AM
$endgroup$
add a comment |
$begingroup$
Naming
Expiration vs Final Expiration seems kind of confusing, imo. Might I suggest PenaltyDate for when amount they owe increases, and ExpirationDate for that final date that they must pay.
Testability
As others have mentioned, passing in a referenceDate will allow you test more easily test outcomes.
Modularity
Breaking the redundant part into a separate function like others have suggested.
Bringing it all together
public static DateTime GetNextPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
var penaltyDate = GetPenaltyDate(expirationDate, referenceDate);
if (penaltyDate < referenceDate)
{
var nextPenaltyDate = GetPenaltyDate(expirationDate, referenceDate.AddMonths(1));
return nextPenaltyDate;
}
else
{
return penaltyDate;
}
}
private static DateTime GetPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
int daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
int penaltyDay = Math.Min(expirationDate.Day, daysInMonth);
return new DateTime(referenceDate.Year, referenceDate.Month, penaltyDay);
}
Testing Mine via Console
class Program
{
static void Main(string args)
{
Print(
expirationDate: new DateTime(2019, 12, 15),
referenceDate: new DateTime(2019, 03, 12)
);
Print(
expirationDate: new DateTime(2019, 10, 11),
referenceDate: new DateTime(2019, 04, 16)
);
Console.ReadKey();
}
private static void Print(DateTime expirationDate, DateTime referenceDate)
{
Console.WriteLine("Final Expiration Date: {0}", expirationDate);
Console.WriteLine("Reference Date: {0}", referenceDate);
Console.WriteLine("Output: {0}", Expiration.GetNextPenaltyDate(expirationDate, referenceDate));
}
}
My Results
My output is formatted differently, but if I'm not mistaken the data matches.
Final Expiration Date: 12/15/2019 12:00:00 AM
Reference Date: 3/12/2019 12:00:00 AM
Output: 3/15/2019 12:00:00 AM
Final Expiration Date: 10/11/2019 12:00:00 AM
Reference Date: 4/16/2019 12:00:00 AM
Output: 5/11/2019 12:00:00 AM
$endgroup$
Naming
Expiration vs Final Expiration seems kind of confusing, imo. Might I suggest PenaltyDate for when amount they owe increases, and ExpirationDate for that final date that they must pay.
Testability
As others have mentioned, passing in a referenceDate will allow you test more easily test outcomes.
Modularity
Breaking the redundant part into a separate function like others have suggested.
Bringing it all together
public static DateTime GetNextPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
var penaltyDate = GetPenaltyDate(expirationDate, referenceDate);
if (penaltyDate < referenceDate)
{
var nextPenaltyDate = GetPenaltyDate(expirationDate, referenceDate.AddMonths(1));
return nextPenaltyDate;
}
else
{
return penaltyDate;
}
}
private static DateTime GetPenaltyDate(DateTime expirationDate, DateTime referenceDate)
{
int daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
int penaltyDay = Math.Min(expirationDate.Day, daysInMonth);
return new DateTime(referenceDate.Year, referenceDate.Month, penaltyDay);
}
Testing Mine via Console
class Program
{
static void Main(string args)
{
Print(
expirationDate: new DateTime(2019, 12, 15),
referenceDate: new DateTime(2019, 03, 12)
);
Print(
expirationDate: new DateTime(2019, 10, 11),
referenceDate: new DateTime(2019, 04, 16)
);
Console.ReadKey();
}
private static void Print(DateTime expirationDate, DateTime referenceDate)
{
Console.WriteLine("Final Expiration Date: {0}", expirationDate);
Console.WriteLine("Reference Date: {0}", referenceDate);
Console.WriteLine("Output: {0}", Expiration.GetNextPenaltyDate(expirationDate, referenceDate));
}
}
My Results
My output is formatted differently, but if I'm not mistaken the data matches.
Final Expiration Date: 12/15/2019 12:00:00 AM
Reference Date: 3/12/2019 12:00:00 AM
Output: 3/15/2019 12:00:00 AM
Final Expiration Date: 10/11/2019 12:00:00 AM
Reference Date: 4/16/2019 12:00:00 AM
Output: 5/11/2019 12:00:00 AM
answered 12 hours ago
Shelby115Shelby115
1,586517
1,586517
add a comment |
add a comment |
$begingroup$
Potentially simpler version?
/// <summary>
/// Number of calendar months between two DateTimes, rounding down partial months.
/// Will return x such that startDate.AddMonths(x) < endDate and
/// startDate.AddMonths(x+1) >= endDate.
/// Ignores time components.
/// </summary>
/// <param name="startDate">Start Date</param>
/// <param name="endDate">End Date</param>
/// <returns>Number of Months between Start Date and End Date</returns>
public static int MonthsBetween(DateTime startDate, DateTime endDate)
{
return (endDate.Year - startDate.Year) * 12 +
(endDate.Month - startDate.Month) +
((endDate.Day >= startDate.Day) ? 0 : -1); //rounding down for partial months
}
/// <summary>
/// Gets the next monthly expiration date after the Reference Date
/// with day component aligned with Final Expiration Date
/// Ignores time components.
/// </summary>
/// <param name="finalExpirationDate">Final Expiration Date</param>
/// <param name="referenceDate">Reference Date</param>
/// <returns>Next monthly expiration date after Reference Date</returns>
public DateTime GetNextExpirationDate(DateTime finalExpirationDate, DateTime referenceDate)
{
return finalExpirationDate.AddMonths(-MonthsBetween(referenceDate, finalExpirationDate));
}
It's unclear from your description what you want to do if today is an expiration date. The code above will generate today's date in that scenario, which is what your code does. If you want to generate next month's expiration date in that scenario, change the >= to a > in MonthsBetween (and adjust the comments as appropriate, of course).
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
Potentially simpler version?
/// <summary>
/// Number of calendar months between two DateTimes, rounding down partial months.
/// Will return x such that startDate.AddMonths(x) < endDate and
/// startDate.AddMonths(x+1) >= endDate.
/// Ignores time components.
/// </summary>
/// <param name="startDate">Start Date</param>
/// <param name="endDate">End Date</param>
/// <returns>Number of Months between Start Date and End Date</returns>
public static int MonthsBetween(DateTime startDate, DateTime endDate)
{
return (endDate.Year - startDate.Year) * 12 +
(endDate.Month - startDate.Month) +
((endDate.Day >= startDate.Day) ? 0 : -1); //rounding down for partial months
}
/// <summary>
/// Gets the next monthly expiration date after the Reference Date
/// with day component aligned with Final Expiration Date
/// Ignores time components.
/// </summary>
/// <param name="finalExpirationDate">Final Expiration Date</param>
/// <param name="referenceDate">Reference Date</param>
/// <returns>Next monthly expiration date after Reference Date</returns>
public DateTime GetNextExpirationDate(DateTime finalExpirationDate, DateTime referenceDate)
{
return finalExpirationDate.AddMonths(-MonthsBetween(referenceDate, finalExpirationDate));
}
It's unclear from your description what you want to do if today is an expiration date. The code above will generate today's date in that scenario, which is what your code does. If you want to generate next month's expiration date in that scenario, change the >= to a > in MonthsBetween (and adjust the comments as appropriate, of course).
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
Potentially simpler version?
/// <summary>
/// Number of calendar months between two DateTimes, rounding down partial months.
/// Will return x such that startDate.AddMonths(x) < endDate and
/// startDate.AddMonths(x+1) >= endDate.
/// Ignores time components.
/// </summary>
/// <param name="startDate">Start Date</param>
/// <param name="endDate">End Date</param>
/// <returns>Number of Months between Start Date and End Date</returns>
public static int MonthsBetween(DateTime startDate, DateTime endDate)
{
return (endDate.Year - startDate.Year) * 12 +
(endDate.Month - startDate.Month) +
((endDate.Day >= startDate.Day) ? 0 : -1); //rounding down for partial months
}
/// <summary>
/// Gets the next monthly expiration date after the Reference Date
/// with day component aligned with Final Expiration Date
/// Ignores time components.
/// </summary>
/// <param name="finalExpirationDate">Final Expiration Date</param>
/// <param name="referenceDate">Reference Date</param>
/// <returns>Next monthly expiration date after Reference Date</returns>
public DateTime GetNextExpirationDate(DateTime finalExpirationDate, DateTime referenceDate)
{
return finalExpirationDate.AddMonths(-MonthsBetween(referenceDate, finalExpirationDate));
}
It's unclear from your description what you want to do if today is an expiration date. The code above will generate today's date in that scenario, which is what your code does. If you want to generate next month's expiration date in that scenario, change the >= to a > in MonthsBetween (and adjust the comments as appropriate, of course).
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
Potentially simpler version?
/// <summary>
/// Number of calendar months between two DateTimes, rounding down partial months.
/// Will return x such that startDate.AddMonths(x) < endDate and
/// startDate.AddMonths(x+1) >= endDate.
/// Ignores time components.
/// </summary>
/// <param name="startDate">Start Date</param>
/// <param name="endDate">End Date</param>
/// <returns>Number of Months between Start Date and End Date</returns>
public static int MonthsBetween(DateTime startDate, DateTime endDate)
{
return (endDate.Year - startDate.Year) * 12 +
(endDate.Month - startDate.Month) +
((endDate.Day >= startDate.Day) ? 0 : -1); //rounding down for partial months
}
/// <summary>
/// Gets the next monthly expiration date after the Reference Date
/// with day component aligned with Final Expiration Date
/// Ignores time components.
/// </summary>
/// <param name="finalExpirationDate">Final Expiration Date</param>
/// <param name="referenceDate">Reference Date</param>
/// <returns>Next monthly expiration date after Reference Date</returns>
public DateTime GetNextExpirationDate(DateTime finalExpirationDate, DateTime referenceDate)
{
return finalExpirationDate.AddMonths(-MonthsBetween(referenceDate, finalExpirationDate));
}
It's unclear from your description what you want to do if today is an expiration date. The code above will generate today's date in that scenario, which is what your code does. If you want to generate next month's expiration date in that scenario, change the >= to a > in MonthsBetween (and adjust the comments as appropriate, of course).
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
edited 12 hours ago
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
answered 13 hours ago
StoborStobor
1011
1011
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
Stobor is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
add a comment |
add a comment |
Noceo is a new contributor. Be nice, and check out our Code of Conduct.
Noceo is a new contributor. Be nice, and check out our Code of Conduct.
Noceo is a new contributor. Be nice, and check out our Code of Conduct.
Noceo is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214700%2ffind-the-next-monthly-expiration-date%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown