12
\$\begingroup\$

Introduction

A function that adds months to a date (without overflowing ends of months) is implemented in many languages/packages. In Teradata SQL it's ADD_MONTHS, here are some examples:

ADD_MONTHS('2021-01-31', 1)   => 2021-02-28
ADD_MONTHS('2021-01-30', 1)   => 2021-02-28
ADD_MONTHS('2021-02-28', 1)   => 2021-03-28
ADD_MONTHS('2021-02-28', -12) => 2020-02-28

Teradata SQL has also a function that goes a step further, namely OADD_MONTHS. Here, when given an end-of-month date, it always returns an end-of-month date.
To illustrate the difference:

ADD_MONTHS('2021-02-28', 1)  => 2021-03-28
OADD_MONTHS('2021-02-28', 1) => 2021-03-31

The task

You are given a date and an integer. Your output should mimic the behaviour of OADD_MONTHS described above.

Any reasonable input/output form is acceptable (including your language's native date/datetime type, a string, number of days/seconds from a fixed point, etc.)

You may assume the input and target dates are after 1600-01-01 and the date is well defined (so no 2021-03-32). You may use the Georgian calendar or any similar calendar implementing standard month lengths and taking into account leap years.

If you have a builtin specifically for this, consider including a non-builtin answer as well to make your answer more interesting.

Test cases

Date      , offset => output      (explanation)
2021-01-31, 0      => 2021-01-31  (boring)
2021-01-31, 1      => 2021-02-28  (no overflow)
2020-12-31, 1      => 2021-01-31  (next year)
2020-12-07, 1      => 2021-01-07  (next year)
2021-01-31, -1     => 2020-12-31  (previous year)
2021-01-30, -1     => 2020-12-30  (previous year)
2021-01-01, -1     => 2020-12-01  (previous year)
2020-12-30, 2      => 2021-02-28  (no overflow)
2021-02-28, 1      => 2021-03-31  (end-of-month -> end-of-month)
2021-09-30, 1      => 2021-10-31  (end-of-month -> end-of-month)
2021-02-28, -12    => 2020-02-29  (end-of-month -> end-of-month)
2020-02-28, 1      => 2020-03-28  (leap year - 28.02 is not end-of-month)
1995-02-28, -1140  => 1900-02-28  (not a leap year)
\$\endgroup\$

6 Answers 6

5
\$\begingroup\$

Julia 1.0, 53 bytes

using Dates
~ =lastdayofmonth
a\b=a<~a ? a+b : ~(a+b)

Try it online!

expects Date("2021-02-28")\Month(1), returns a Date

\$\endgroup\$
0
5
\$\begingroup\$

JavaScript (Node.js), 142 140 135 131 130 bytes

d=>f=x=>O(x)>O((D=a=>new Date(x-a*864e5))(-1))?D(1,x=f(D(-1))):O(y=D``,y.setMonth(x.getMonth()+d))<O(x)?f(D(-1),d):y
O=x=>(x+x)[8]

Try it online!

\$\endgroup\$
2
\$\begingroup\$

Charcoal, 95 bytes

≔I⪪S-θ≔⊟θζ≔⊟θη≔⊟θθF²«≔⎇⁼η²⁺²⁸¬﹪∨﹪θ¹⁰⁰÷θ¹⁰⁰¦⁴⁻³¹﹪﹪⊖η⁷¦²ε¿ι⪫⟦θη⌊⟦ζε⟧⟧-«≧⁺⊖Nη≧⁺÷η¹²θ≔⊕﹪η¹²η¿⁼ζε≔φζ

Try it online! Link is to verbose version of code. Date I/O is in YYYY-m-d format i.e. no leading zeros. Explanation:

≔I⪪S-θ≔⊟θζ≔⊟θη≔⊟θθ

Read in the date.

F²«

Calculate the number of days in the month twice (first for the input month and second for the output month).

≔⎇⁼η²⁺²⁸¬﹪∨﹪θ¹⁰⁰÷θ¹⁰⁰¦⁴⁻³¹﹪﹪⊖η⁷¦²ε

The formula for the number of days in the month is m == 2 ? 28 + !((m % 100 || m / 100) % 4) : 31 - (m - 1) % 7 % 2, the latter part of which I appropriated from this Stack Overflow answer.

¿ι⪫⟦θη⌊⟦ζε⟧⟧-«

If this is the second pass, then output the year and month, and the minimum of the day and the number of days in the month (in case the original month had more days).

≧⁺⊖Nη

Add the input number of months.

≧⁺÷η¹²θ≔⊕﹪η¹²η

Adjust the years to bring the month to between 1 and 12 again.

¿⁼ζε≔φζ

If this was the last day of the input month then set the day to a large number so that it gets truncated to the last day of the output month.

\$\endgroup\$
2
\$\begingroup\$

Wolfram Language (Mathematica), 63 61 bytes

Without considering d= which was used to apply to the input.

t="EndOfMonth";d=DatePlus[#,{#2,If[DayMatchQ[#,t],t,"Month"]}]&

Try it online!

\$\endgroup\$
1
  • 1
    \$\begingroup\$ 58 bytes \$\endgroup\$
    – att
    Feb 10, 2022 at 22:56
2
\$\begingroup\$

05AB1E, 66 63 bytes

¦R1Ý-12β+12‰`>¹н)R¹"`т‰0Kθ4ÖUD<i\28X+ë<7%É31α"©.VQiт0ǝ}D®.V‚ß0ǝ

-3 bytes thanks to @Neil.

Try it online or verify all test cases.

Explanation:

Here we go again.. 05AB1E lacks any date builtins, so everything is done with manual calculations. The isLeapYear calculation is taken from this answer of mine and determining the day at the end of the month is taken from this answer of mine (steps 6a-6d).

Step 1: Add the given input amount of months to the date:

¦            # Remove the day from the (implicit) input date-list
 R           # Reverse it to [y,m]
  1Ý-        # Subtract [0,1] from it, to make the month 0-based
     12β     # Convert from a base-12 list to an integer
        +    # Add the second (implicit) input-integer
         12‰ # Divmod this by 12, pushing the pair [n//12,n%12]
`            # Pop and push both separated to the stack
 >           # Increase the 0-based month back to a 1-based month
  ¹          # Push the first date-input again
   н         # Pop and only leave its day
    )        # Wrap all three values back into a list
     R       # Reverse it to [d,m,y] again

Step 2: If the input-date was at the end of the month, change the resulting date to the end of the month as well:

¹            # Push the first date-input again
 "..."       # Push the string defined below
      ©      # Store it in variable `®` (without popping)
       .V    # Pop and execute it as 05AB1E code
Qi           # If the top two values are equal
             # (thus the input was at the end of the month)
  т0ǝ        #  Replace the current day with 100: [100,m,y]
 }           # Close the if-statement

`            # Pop and push d,m,y separated to the stack
 т‰          # Divmod the year by 100
   0K        # Remove all 0s from this pair
     θ       # Pop and leave just the last value
      4Ö     # Check if this is divisible by 4
             # (1 for leap years; 0 if not)
        U    # Pop and store this isLeapYear check in variable `X`
 D           # Duplicate the month
  <i         # If it's 2 (thus February):
    \        #  Discard the month
     28X+    #  And push 28 + isLeapYear instead
   ë         # Else:
    <        #  Decrease the month by 1 to make it 0-based
     7%      #  Modulo-7
       É     #  Modulo-2
        31α  #  Pop and push its absolute difference with 31
             # (the stack will now contain the day, and the amount of days in
             # this month)

Step 3: Fix potential overflow for the resulting date, after which we can output the result:

D            # Duplicate the result
 ®.V         # Determine the amount of days of this month
    ‚        # Pair the current day and end of month day together
     ß       # Pop and push the minimum of the two
      0ǝ     # Insert this day back into the [d,m,y]
             # (after which the result is output implicitly)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I think D®.V.; can be ₆0ǝ, since it gets corrected in step 3 anyway. \$\endgroup\$
    – Neil
    Feb 11, 2022 at 0:48
  • \$\begingroup\$ @Neil Thanks for the -3! \$\endgroup\$ Feb 11, 2022 at 8:02
1
\$\begingroup\$

JavaScript (Node.js), 100 bytes

(y,m,d,i)=>(D=(m,d)=>(a=new Date(y,m,d)).getMonth(),D(i+=m+!(b=D(m,d+1)==m),b)-D(i,b*d)&&D(i+1,0),a)

Try it online!

Input 4 parameters year, month, date, increase, while month is 0-indexed (as what JavaScript Date type do: Jan = 0, Dec = 11). Output a Date Object. Only work if user is using UTC timezone. If you want change to 1-indexed month, it could be fixed +2 bytes (add a --).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.