10
\$\begingroup\$

Given three non-negative integers y, m, and d (of which at least one must be positive) and a valid date with a positive year (in any reasonable format that includes the year, month, and day, and no additional information), output the date that is y years, m months, and d days after the original date.

The Gregorian calendar is to be used for all dates (even dates prior to the adoption of the Gregorian calendar).

The method for computing the next date is as follows:

  1. Add y to the year
  2. Add m to the month
  3. Normalize the date by applying rollovers (e.g. 2018-13-01 -> 2019-01-01)
  4. If the day is past the last day of the month, change it to the last day in the month (e.g. 2018-02-30 -> 2018-02-28)
  5. Add d to the day
  6. Normalize the date by applying rollovers (e.g. 2019-01-32 -> 2019-02-01)

Leap years (years divisible by 4, but not divisible by 100 unless also divisible by 400) must be handled appropriately. All inputs and outputs will be within the representable integer range of your language.

Test Cases

Test cases are provided in the format input => output, where input is a JSON object.

{"date":"2018-01-01","add":{"d":1}} => 2018-01-02
{"date":"2018-01-01","add":{"M":1}} => 2018-02-01
{"date":"2018-01-01","add":{"Y":1}} => 2019-01-01
{"date":"2018-01-30","add":{"M":1}} => 2018-02-28
{"date":"2018-01-30","add":{"M":2}} => 2018-03-30
{"date":"2000-02-29","add":{"Y":1}} => 2001-02-28
{"date":"2000-02-29","add":{"Y":4}} => 2004-02-29
{"date":"2000-01-30","add":{"d":2}} => 2000-02-01
{"date":"2018-01-01","add":{"Y":2,"M":3,"d":4}} => 2020-04-05
{"date":"2018-01-01","add":{"Y":5,"M":15,"d":40}} => 2024-05-11

You may use this JSFiddle for testing.

This is , so the shortest solution (in each language) wins.

\$\endgroup\$
9
  • \$\begingroup\$ Sandbox post (deleted) \$\endgroup\$
    – user45941
    Jul 20, 2018 at 22:21
  • 2
    \$\begingroup\$ @LuisfelipeDejesusMunoz The input format is not important, as is the norm here on PPCG. \$\endgroup\$
    – user45941
    Jul 20, 2018 at 23:05
  • \$\begingroup\$ Is there any restriction to the upper bounds of y, m and d (e.g. could d be 2147483000?) \$\endgroup\$
    – ErikF
    Jul 21, 2018 at 3:44
  • \$\begingroup\$ @ErikF All inputs and outputs will be within the representable integer range of your language. \$\endgroup\$
    – user45941
    Jul 21, 2018 at 4:00
  • 1
    \$\begingroup\$ What about output formats? Can we output a date object? Can we take a date object? \$\endgroup\$ Jul 21, 2018 at 9:00

10 Answers 10

3
\$\begingroup\$

C (gcc), 291 bytes

This one was pretty fun to get returning the same values as the JS builtin.

z,m=0xEEFBB3;int*y;g(){z=28+(m>>y[1]*2&3)+!(y[1]-1)*(!(*y%4)&&(*y%100)||!(*y%400));}h(a){z=(a>g())?g():a;}j(){*y+=y[1]/12;y[1]%=12;y[2]=h(y[2]);}f(int*a){y=a+6;for(z=0;z<3;z++)y[z]=a[z];y[1]--;j();*y+=a[3];y[1]+=a[4];j();y[2]+=a[5];for(;y[2]>h(y[2]);(y[1]=++y[1]%12)||++*y)y[2]-=g();y[1]++;}

Try it online!

Un-golfed:

// De No Oc Se Au Jl Jn Ma Ap Mr Fe Ja
// 31 30 31 30 31 31 30 31 30 31 28 31 = Month length
// 11 10 11 10 11 11 10 11 10 11 00 11 = Offset (2-bit representation)
//   E     E     F     B     B     3   = Hex representation

int m=0xEEFBB3; // Month lengths-28 in reverse order, stored as 2 bits/month
int *y; // Pointer to the output date, shared as a global between calls

// Regenerate month length and add leap day
int days_month(void) { 
  return 28+(m>>y[1]*2&3)+!(y[1]-1)*(!(*y%4)&&(*y%100)||!(*y%400));
}

int calendar_day(int day) { return day>days_month()?days_month():day; }

void truncate_date(void) {
  *y+=y[1]/12; y[1]%=12;
  y[2]=calendar_day(y[2]);
}

void f(int *a) {
  int z;
  y=a+6;
  for(z=0;z<3;z++)y[z]=a[z];y[1]--; // Convert month to 0-based
  truncate_date();
  *y+=a[3]; y[1]+=a[4]; truncate_date();
  y[2]+=a[5];
  for(;y[2]>calendar_day(y[2]);(y[1]=++y[1]%12)||++*y)
    y[2]-=days_month();
  y[1]++; // Return month to 1-based
}

Try it online!

\$\endgroup\$
1
1
\$\begingroup\$

perl -MDate::Calc=:all -E, 28 bytes

$,=$";say Add_Delta_YMD@ARGV

This takes 6 arguments: the input year, month and date (as separate arguments), and the number of years, months and days to add.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ This doesn't deal with the quirky "rule 4" of the task, so fails some of the test cases - for eg., perl -MDate::Calc=:all -E '$,=$";say Add_Delta_YMD@ARGV' -- 2000 2 29 1 0 0 returns 2001 3 1 instead of 2001 2 28 as the OP expects (test case 6). \$\endgroup\$
    – Sundar R
    Jul 21, 2018 at 10:34
1
\$\begingroup\$

R, 88 bytes

function(Y,M,D,y,m,d,o=M+m){while(is.na(x<-ISOdate(Y+y+o%/%12,o%%12,D)))D=D-1;x+864e2*d}

Try it online!

A function that takes 3 arguments (Y,M,D) for the date, and other 3 arguments (y,m,d) for the values to be added.

The output comes with prepended 12:00:00 GMT which is the default format for ISOdate's

\$\endgroup\$
1
\$\begingroup\$

Perl 6,  60 50 45  44 bytes

{Date.new($^a).later(:$:year).later(:$:month).later(:$:day)}

Test it (60)
Input is ( "2000-02-29", year => 1, month => 0, day => 0 )


{$^a.later(:$:year).later(:$:month).later(:$:day)}

Test it (50)
Input is ( Date.new("2000-02-29"), year => 1, month => 0, day => 0 )


{$/=$^a;$/.=later(|$_) for |[R,] $^b.sort;$/}

Test it (45)
Input is ( Date.new("2000-02-29"), %( year => 1 ) )
(No need to include keys with a value of 0)


{$/=$^a;$/.=later(|$_) for |[R,] %_.sort;$/}

Test it (44)
Input is ( Date.new("2000-02-29"), year => 1 )

Expanded:

{  # bare block lambda

  $/ = $^a; # store only positional param into a modifiable scalar
            # (params are readonly by default)


  # do a loop over the data to add

  $/ .= later(    # add using Date.later()
    |$_           # turn current iterated Pair into a named parameter
  )

    for

      |           # flatten so that `for` will iterate

        [R,]      # shorter than `reverse` (year=>1, month=>0, day=>0)

          %_.sort # sort the named arguments (day=>0, month=>0, year=>1)
  ;

  # return new Date
  $/
}
\$\endgroup\$
1
  • \$\begingroup\$ You can remove the space before the for \$\endgroup\$
    – Jo King
    Jul 27, 2018 at 4:08
1
\$\begingroup\$

C# (.NET Core), 48 bytes

(a,y,m,d)=>a.AddYears(y).AddMonths(m).AddDays(d)

Try it online!

\$\endgroup\$
1
\$\begingroup\$

Java 8, 51 bytes

(s,y,m,d)->s.plusYears(y).plusMonths(m).plusDays(d)

Input (s) and output are both java.time.LocalDate.

Try it online.

Explanation:

(s,y,m,d)->        // Method with LocalDate and 3 int parameters and LocalDate return-type
  s.plusYears(y)   //  Add the years to the input start-Date
   .plusMonths(m)  //  Add the months as well
   .plusDays(d)    //  And add the days as well
\$\endgroup\$
1
\$\begingroup\$

R, 65 bytes

function(x,y){require(lubridate)
x%m+%period(y,c("ye","mo","d"))}

Uses the lubridate package. The %m+% infix operator is sugar for the add_with_rollback function which essentially implements what the question asks for.

TIO doesn't have lubridate so you can Try It Here Instead with f <- prepended to the function above along with test cases:

f(as.Date("2018-01-01"),c(0,0,1))
f(as.Date("2018-01-01"),c(0,1,0))
f(as.Date("2018-01-01"),c(1,0,0))
f(as.Date("2018-01-30"),c(0,1,0))
f(as.Date("2018-01-30"),c(0,2,0))
f(as.Date("2000-02-29"),c(1,0,0))
f(as.Date("2000-02-29"),c(4,0,0))
f(as.Date("2000-01-30"),c(0,0,2))
f(as.Date("2018-01-01"),c(2,3,4))
f(as.Date("2018-01-01"),c(5,15,40))
\$\endgroup\$
1
  • \$\begingroup\$ You can save save two bytes with:function(x,y)x%m+%period(y,c("ye","mo","d")) require(lubridate) (require outisde of function) \$\endgroup\$
    – JayCe
    Jul 25, 2018 at 13:58
0
\$\begingroup\$

Bash, 150 149 bytes

a=$2+$5-1+b
y=$1+$4+a/12
m=1+a%12
d=date
$d -d@$[$($d +%s+$6*86400 -d$[y]-$[m]-$($d +$3%n%d -d@$[`b=1;$d +%s-86400 -d$[y]-$[m]-1`]|sort -n|head -1))]

Try it online!

Takes input via command line arguments in order: old year, old month, old day. year change, month change, day change. Outputs a string like Wed Feb 28 00:00:00 UTC 2018 to stdout.

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

PHP, 203 bytes

<?list(,$a,$y,$m,$d)=$argv;$b=new DateTime($a);$j=$b->format('j');$b->modify("+$y year +$m month");$j==$b->format('j')?:$b->modify('last day of last month');$b->modify("+$d day");echo$b->format('Y-m-d');

To run it:

php -n <filename> <date> <y> <m> <d>

Example:

php -n date_occurrences.php 2018-01-01 5 15 40

Or Try it online!

Tests: Try it online!

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

T-SQL, 53 Bytes

SELECT DATEADD(D,d,DATEADD(M,m,DATEADD(Y,y,a)))FROM t

I'm not sure that it matters, but I'm applying the Year adjustment, followed by the Month adjustment, followed by the Day. All test values check out.

Per our IO standards, input is taken from a pre-existing table t with date field a and integer fields y, m, and d.

Note interestingly that it isn't the capitalization that matters between the date type codes (D, M, and Y) and my input values (d, m, and y) its simply the order of parameters in the SQL DATEADD function.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Does this pass test case 6? Since it doesn't implement Rule 4, I think it'd give 2001 3 1 instead of 2001 2 28 for input 6. \$\endgroup\$
    – Sundar R
    Jul 27, 2018 at 20:42
  • \$\begingroup\$ @sundar Looks like you are correct; I thought I had passed all the test cases. I'll see if it can be fixed... \$\endgroup\$
    – BradC
    Jul 30, 2018 at 15:20

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.