1
votes

I'm running into an issue with adding an amount of seconds to a moment date in my Rails app. I use Stimulus for JS but that shouldn't relevant to the cause of the problem.

I've got a select form input that I've generated using the Rails form builder using these options_for_select:

[["1 day", 1.day], ["3 days", 3.days], ["7 days", 1.week],
["14 days", 2.weeks], ["1 month", 1.month],
["3 months", 3.months], ["6 months", 6.months], ["1 year", 1.year]]

That results in a select that looks like this:

<select class="form-control" data-admin--reports-form-target="sendFrequencySelect"
<option value="86400">1 day</option>
<option value="259200">3 days</option>
<option value="604800">7 days</option>
<option value="1209600">14 days</option>
<option value="2629746">1 month</option>
<option value="7889238">3 months</option>
<option selected="selected" value="15778476">6 months</option>
<option value="31556952">1 year</option>
</select>

I have some JS that takes the value of the selected option and adds that amount of seconds to a moment date, and then sets the value of a different input to the new moment date that results from that calculation:

let finalRunAt = moment(this.lastGeneratedTimestampInputTarget.value, "YYYY-MM-DD HH:mm");
let secondsToAdd = parseInt(this.sendFrequencySelectTarget.value);
finalRunAt.add(secondsToAdd, "seconds");
this.finalRunTimestampInputTarget.value = finalRunAt.format("YYYY-MM-DD HH:mm");

This works correctly... until you get to 1 month or larger. Here's how the calculations come out for a start date of 2021-02-25 12:00:

Select option Result of adding seconds to start date
1 day 2021-02-26 12:00
3 days 2021-02-28 12:00
7 days 2021-03-04 12:00
14 days 2021-03-11 12:00
1 month 2021-03-27 23:29
3 months 2021-05-27 20:27
6 months 2021-08-27 03:54
1 year 2022-02-25 17:49

I've been trying to reverse-engineer the weird (bolded) results. What's causing these results? Is it some discrepancy between what Rails considers 1 month and what moment considers 1 month?

1
"What's causing these results?" - Your numbers. 2629746 seconds are 30.436875 days and that's what moment is adding to the timestamp - 30 days and ~10.48 hoursAndreas
Also months don't all have the same number of seconds. That's really the wrong way to manipulate dates in JavaScript. The Date prototype has methods that correctly and consistently increment by time units.Pointy
That's what I was thinking, that the Rails 1-month-in-seconds is not actually 1-month-in-seconds on the JS end. I guess I'm mostly curious why the Rails-provided value works as a 1 month interval in Rails, then. Why does Rails use 2629746 seconds for 1 month, y'know?David Gay

1 Answers

0
votes

Though I'm unsure about the nuances of why Rails uses, say 2629746 as a representation of 1.month in seconds, I've implemented a workaround for this scenario.

I wanted to keep the select input generated as-is by Rails, since the values it generates with (listed in the question) work great on the Rails end. That is, I can use that select seamlessly to select an interval to be used as an ActiveSupport::Duration. For this reason, I didn't want to change the select input.

Instead, I wrote a simple (albeit a bit hacky) function to do the correct moment operation depending on the value of the select:

  addRailsSecondsStringToMoment(seconds_string, moment) {
    switch (seconds_string) {
      case "86400":
        moment.add(1, "days");
        break;
      case "259200":
        moment.add(3, "days");
        break;
      case "604800":
        moment.add(7, "days");
        break;
      case "1209600":
        moment.add(14, "days");
        break;
      case "2629746":
        moment.add(1, "months");
        break;
      case "7889238":
        moment.add(3, "months");
        break;
      case "15778476":
        moment.add(6, "months");
        break;
      case "31556952":
        moment.add(1, "years");
        break;
    }
  }

Then I can just call it like so:

this.addRailsSecondsStringToMoment(this.sendFrequencySelectTarget.value, finalRunAt);

... where finalRunAt is a moment object. Could probably be a bit prettier, but it works. Happy to accept a sexier solution if someone posts one, especially if it provides some information on why Rails uses the seconds values it does.