Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for reverse or negative occurrences? #57

Closed
mattdeluco opened this issue Jul 18, 2014 · 11 comments
Closed

Support for reverse or negative occurrences? #57

mattdeluco opened this issue Jul 18, 2014 · 11 comments

Comments

@mattdeluco
Copy link

Is it possible to specify a recurrence with negative values?

For example, the 2nd last Tuesday of the month?

Or something like recur().on(-5).dayOfMonth() for the fifth last day of the month, recur().on(-101).dayOfYear(), or recur().on(-3).weekOfYear()?

@bunkat
Copy link
Owner

bunkat commented Jul 18, 2014

Negative values are not supported. You can specify constraints using last such as

later.parse.recur().on(5).hour().last().dayOfMonth();

to signify the last day of a month (which could be the 28th, 29th, 30th, or 31st). Otherwise you need to specify a positive value. If you really need negative values, you could create a custom modifier (see http://bunkat.github.io/later/modifiers.html#custom) that would convert your negative values to positive values and pass those onto the standard constraints.

Here is the example of a custom modifier to support a negative values. I haven't tested this at all, but this should give you the basic idea of what you would need to do. This is also assuming that the rest of the code works with negative numbers which it may or may not since I've never tried.

later.modifier.negative = later.modifier.n = function(period, values) {
    return {
      name:     'negative' + period.name,
      range:    period.range,
      val:      function(d) { return period.val(d); },
      isValid:  function(d, val) { return period.isValid(d, val < 0 ? period.extent(d)[1] + val : val); },
      extent:   function(d) { return [-1 * period.extent(d)[1],period.extent(d)[1]] ; },
      start:    period.start,
      end:      period.end,
      next:     function(d, val) { return period.next(d, val < 0 ? period.extent(d)[1] + val : val); },
      prev:     function(d, val) { return period.prev(d, val < 0 ? period.extent(d)[1] + val : val); }
    };
  };

The idea is to modify the extent such that both negative and positive numbers are supported, modify the isValid function to check against the correct value (which is max - supplied), and modify the next and prev functions in the same manner as the isValid function.

@mattdeluco
Copy link
Author

I'm doing some testing with your suggestion, and so far so good, except for a little detail around timezones that I don't understand.

Why would the second last day of January be reported as "Thu Jan 30 2014 19:00:00 GMT-0500 (EST)"? If I specify later.date.localTime() I get "Thu Jan 30 2014 00:00:00 GMT-0500 (EST)".

But I don't understand the effect of timezone here - regardless of what timezone I'm in, if I ask for the second last day of January, should it not always be the 30th?

@bunkat
Copy link
Owner

bunkat commented Jul 19, 2014

It should definitely be giving you Jan 30th at midnight in your desired timezone. If it is giving you Jan 31st, then I would guess the value being passed into period.next isn't correct. Unfortunately there is no easy way to determine where the problem is, you'll just need to trace the values through your modifier to make sure everything is as expected.

@mattdeluco
Copy link
Author

Here's what I've done with the code you provided, followed by a test case:

later.modifier.negative = later.modifier.n = function(period, values) {

    if (['day', 'day of year', 'week of year'].indexOf(period.name) < 0) {
        throw new Error('Negative modifier only intended for periods day, ' +
            'day of year, and week of year.');
    }

    return {
        name: 'negative ' + period.name,
        range: period.range,
        val: function(d) {
            return period.val(d);
        },
        isValid: function(d, val) {
            return period.isValid(d, period.extent(d)[1] + val + 1);
        },
        extent: function(d) {
            return [-1 * period.extent(d)[1], -1 * period.extent(d)[0]];
        },
        start: period.start,
        end: period.end,
        next: function(d, val) {
            return period.next(d, period.extent(d)[1] + val + 1);
        },
        prev: function(d, val) {
            return period.prev(d, period.extent(d)[1] + val + 1);
        }
    };
};
    describe('next', function () {

        var d = new Date('2014-01-01T00:00:00Z');

        it('should return the last day of the month', function () {
            later.date.UTC();
            var neg = later.modifier.negative(later.day);
            console.log(neg.next(d, -1));
            neg.next(d, -1).should.eql(new Date('2014-01-31T00:00:00Z'));
        });
    });

This test will pass, however the output of console.log(neg.next(d, -1)); is this:

Thu Jan 30 2014 19:00:00 GMT-0500 (EST)

Do I just have a misunderstanding of how a Date with timezone works? For example, if the test is passing, then the correct date is being returned. However, that correct date (Jan. 31st at midnight), displayed in my timezone would be Jan 30th at 7pm. I guess I just don't understand why console.log() is converting to my timezone - just a javascript behaviour?

@bunkat
Copy link
Owner

bunkat commented Jul 19, 2014

Yes, console.log() shows dates in your local timezone. Looks like everything is working as expected.

@mattdeluco
Copy link
Author

Could you suggest a behaviour for next() and prev() if the absolute value of the given value is greater than the number of days in a month?

For example, in later.day.next(), if the given value is greater than the number of days in the following month, it returns the first day of the following month. (I'm curious why you chose to do that, too, rather than returning the next month where value is actually valid. Like in January, skipping February for March when val = 31.)

I'm still not entirely familiar with the expected behaviour of later - can you give me an idea of what you might expect from the following: later.modifier.negative(later.day).next(new Date('2008-01-01T00:00:00Z'), -31);.

I would think return the first day of the following month, it's just that I'm getting failures with this configuration in the test runner:

        {
            date: new Date(2008, 0, 1),
            val: -31,
            extent: [-31,-1],
            start: new Date(2008, 0, 1),
            end: new Date(2008, 0, 1, 23, 59, 59)
        }

I realize it may not be possible to use the test runner since this modifier creates some unique scenarios.

@bunkat
Copy link
Owner

bunkat commented Jul 25, 2014

I would suggest that you return the last second of the last day of the previous month if the value provided is greater than the number of days in the month. This is for the same reason that later returns the first day of the following month if the given value is greater than the number of the days in the month - to ensure that no valid occurrences are skipped.

Imagine if you had a constraint that said days 5, 15, 20, 25, 30 of every month are valid. Now the 27 of February rolls around and you run the scheduler to find the next valid occurrence. The next largest value after 27 is 30 and so it starts looking for a valid day with value 30. There is no such day in February so what should we do?

If we skipped to the next month where 30 was valid, we would end up at March 30th. However, according to the constraints, days 5, 15, 20, and 25 were also valid. We've missed an occurrence on March 5th. To avoid this problem, the constraint returns the first day of the following month (the first time a valid occurrence could possibly occur) and the search for the next valid occurrence begins from there.

If your constraint only said days 30 of every month are valid it would in fact return only the months with 30 days.

@ciekawy
Copy link

ciekawy commented Dec 2, 2014

I have similar question. Is it possible to specify last weekday of each month? Do I have to use modifier as well or is there any easier way?

@bunkat
Copy link
Owner

bunkat commented Dec 2, 2014

Sure, you just need to think at about it a little creatively. If you want the last weekday of each month, it basically means the following:

* Use the last day of the month if it is a Monday, Tuesday, Wednesday, Thursday, or Friday
* Use the last Friday of the month if the last day is a Saturday or Sunday

The second is a little bit tricky to encode, but the way I thought about it was to just always take the last Friday of the month if it occurs at the very end of the month. For example, the last Friday of March 2015 is the 27th, but since Tuesday the 31st is the last weekday, we don't want to include that occurrence. Basically for months with 31 days, we only want Friday if it is either the 30th (the 31st was a Saturday) or the 29th (the 31st was a Sunday). We can then do something similar with months with 30 days (we only want the 28 or 29th).

February is similar, but of course, leap year messes everything up so you would need to create additional exceptions to handle the leap years if that's important to you.

later.date.localTime()
var s = {schedules:
    [ {D:[0], dw: [2,3,4,5,6]},     // last day of month if it is a weekday
      {dc:[0], dw: [6], D:[29,30], M:[1,3,5,7,8,10,12]},  // last Friday for 31 day months
      {dc:[0], dw: [6], D:[28,29], M:[4,6,9,11]},  // last Friday for 30 day months
      {dc:[0], dw: [6], D:[26,27], M:[2]}] }  // last Friday for Feb (non-leap year)
later.schedule(s).next(10)

There might be a smarter way to do it, this is just the first way that I thought of.

@ciekawy
Copy link

ciekawy commented Dec 2, 2014

I am asking since I am new to this very interesting api. I was wondering about the concept of incrementally narrowing data set. Eg

  • for given month last() would return last day of month.
  • for given month weekDay() would return all week days than last() would return last of those week days?
    I am just not sure if there is a chance last() could work (or be developed :) this way...

@bunkat
Copy link
Owner

bunkat commented Dec 4, 2014

That's not how Later works. With Later, all of the constraints are completely independent from one another. The Weekday constraint specifies which day of the week you want. Last in that case means Saturday since that is the last day of the week. It doesn't know anything about days of the month or weeks of the month, so Last can't mean anything else and still make sense.

You could create a custom time period that simply calculates the last weekday of a month and uses just a flag (1 for last weekday, 0 for don't care). Assume you create this time period and called it 'lwdm' for last weekday in a month or something, then you could just do:

var s =  {schedules: [{lwdm: [1]}]};
later.schedules(s).next(10);

If you haven't seen it yet, I show an example at http://bunkat.github.io/later/time-periods.html#custom that walks through splitting up a day. The one to calculate the last weekday of the month would be similar.

@bunkat bunkat closed this as completed Sep 2, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants