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

RuleSet and rrulestr doesn't work as intended #332

Open
7 tasks done
florianchevallier opened this issue Mar 20, 2019 · 4 comments
Open
7 tasks done

RuleSet and rrulestr doesn't work as intended #332

florianchevallier opened this issue Mar 20, 2019 · 4 comments

Comments

@florianchevallier
Copy link

florianchevallier commented Mar 20, 2019

Reporting an issue

Thank you for taking an interest in rrule! Please include the following in
your report:

  • Verify that you've looked through existing issues for duplicates before
    creating a new one
  • Code sample reproducing the issue. Be sure to include all input values you
    are using such as the exact RRule string and dates.
  • Expected output
  • Actual output
  • The version of rrule you are using
  • Your operating system
  • Your local timezone (run $ date from the command line
    of the machine showing the bug)

rrule version : ^2.6.0
on MacOS
date: Lun 18 mar 2019 16:47:35 CET


Hi,

I'm trying to create a rruleset from a string passed in the DB, and I have a different behavior when using two rruleset.rrule or just one.

I'm not really clear, so here is the code sample :

const { RRuleSet, rrulestr } = require('rrule');
const moment = require('moment');

const rruleset = new RRuleSet();
const rruleset2 = new RRuleSet();

// creating a first ruleset with two different rrules
rruleset.rrule(rrulestr('DTSTART;TZID=Europe/Paris:20190311T070000\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20190324T230000'));
rruleset.rrule(rrulestr('DTSTART;TZID=Europe/Paris:20190325T120000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20210325T120000Z'));
console.log(rruleset.toString());

console.log('---------');

// creating a rruleset from the first rruleset
rruleset2.rrule(rrulestr(rruleset.toString(), { forceset: true }));
console.log(rruleset2.toString());

console.log('---------');
console.log(rruleset.between(moment('2019-03-18 00:00').toDate(), moment('2019-04-01 00:00').toDate()));
console.log('--------');
console.log(rruleset2.between(moment('2019-03-18 00:00').toDate(), moment('2019-04-01 00:00').toDate()));

And here is the console.log associated, with the difference of the toString() of the two methods highlighted with a + and - :

DTSTART;TZID=Europe/Paris:20190311T070000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20190324T230000
- DTSTART;TZID=Europe/Paris:20190325T120000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20210325T120000
---------
DTSTART;TZID=Europe/Paris:20190311T070000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20190324T230000
+ DTSTART;TZID=Europe/Paris:20190311T070000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20210325T120000
---------
[ 2019-03-18T07:00:00.000Z,
  2019-03-19T07:00:00.000Z,
  2019-03-20T07:00:00.000Z,
  2019-03-21T07:00:00.000Z,
  2019-03-22T07:00:00.000Z,
  2019-03-25T12:00:00.000Z,
  2019-03-26T12:00:00.000Z,
  2019-03-27T12:00:00.000Z,
  2019-03-28T12:00:00.000Z,
  2019-03-29T12:00:00.000Z ]
--------
[ 2019-03-20T17:41:03.000Z ]

Obviously, the second result acts weirdly. I don't know if it's a bug, but it feels like one.

@eyalcohen4
Copy link

I've encountered something similar too, and I think it's the Z at the ISO date end. Weird

@ephys
Copy link

ephys commented Feb 9, 2021

From what I can tell, it's a bug in rrulestr where it reuses the DTSTART from the first RRule for subsequent rrules

@davidgoli If you're interested I can open a new PR for this once my current one is merged

@ephys
Copy link

ephys commented Mar 31, 2021

Until this gets fixed I implemented an alternative to rrulestr that should handle DTSTART as expected:

import { RRule, RRuleSet, rrulestr } from 'rrule';

/**
 * Parse a rrule string as a RRuleSet.
 *
 * This also fixes a bug in {@link rrulestr} where the dtstart of the rrules following the first one are lost.
 * {@link https://github.com/jakubroztocil/rrule/issues/332}
 *
 * Does not support EXRULE, RDATE, or EXDATE
 *
 * @throws Error if the rrule cannot be parsed into a rruleset.
 * @param {string} rruleStr
 * @returns {RRuleSet}
 */
export function parseRRuleSet(rruleStr: string): RRuleSet {
  const set = new RRuleSet();

  rruleStr = rruleStr.trim();

  let dtStart = '';
  for (let line of rruleStr.split('\n')) {
    line = line.trim();

    const { name, value, parms } = breakDownLine(line);

    if (name !== 'RRULE' && dtStart) {
      throw new Error('Incorrectly placed DTSTART found. Must be placed one line before RRULE');
    }

    switch (name) {
      case 'RDATE': {
        const dates = parseRDate(value, parms);

        for (const date of dates) {
          set.rdate(date);
        }

        break;
      }

      case 'DTSTART':
        dtStart = line;
        continue;

      case 'RRULE': {
        const rrule = parseRrule(`${dtStart}\n${line}`);
        set.rrule(rrule);
        dtStart = '';
        break;
      }

      default:
        throw new Error('parseRRuleSet only supports DTSTART, RDATE & RRULE for now');
    }
  }

  return set;
}

export function parseRrule(rruleStr: string): RRule {
  const rrule = rrulestr(rruleStr);

  if (!(rrule instanceof RRule)) {
    throw new Error('Cannot parse input as RRule. Is it an RRuleSet?');
  }

  return rrule;
}

function parseRDate(rdateval, parms): Date[] {
  validateDateParm(parms);

  return rdateval
    .split(',')
    .map(datestr => {
      return parseRRuleDate(datestr);
    });
}

function parseRRuleDate(until: string): Date {
  const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/;
  const bits = re.exec(until);
  if (!bits) {
    throw new Error(`Invalid RRule Date value: ${until}`);
  }

  const parseInt = Number.parseInt;

  return new Date(Date.UTC(
    parseInt(bits[1], 10),
    parseInt(bits[2], 10) - 1,
    parseInt(bits[3], 10),
    parseInt(bits[5], 10) || 0,
    parseInt(bits[6], 10) || 0,
    parseInt(bits[7], 10) || 0,
  ));
}

function validateDateParm(parms) {
  parms.forEach(parm => {
    if (!/(VALUE=DATE(-TIME)?)|(TZID=)/.test(parm)) {
      throw new Error(`unsupported RDATE/EXDATE parm: ${parm}`);
    }
  });
}

function breakDownLine(line) {
  const { name, value } = extractName(line);
  const parms = name.split(';');
  if (!parms) {
    throw new Error('empty property name');
  }

  return {
    name: parms[0].toUpperCase(),
    parms: parms.slice(1),
    value,
  };
}

function extractName(line) {
  if (line.indexOf(':') === -1) {
    return {
      name: 'RRULE',
      value: line,
    };
  }

  const [name, value] = pythonSplit(line, ':', 1);

  return {
    name,
    value,
  };
}

function pythonSplit(str: string, sep: string, splitCount?: number) {
  const splits = str.split(sep);

  return splitCount
    ? splits.slice(0, splitCount).concat([splits.slice(splitCount).join(sep)])
    : splits;
}

@ArnaudBan
Copy link

Thank you so mutch this works for me.

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

4 participants