Skip to content

Commit

Permalink
feat: Ajouter le support du "~" (aléatoire).
Browse files Browse the repository at this point in the history
  • Loading branch information
regseb committed Jun 30, 2023
1 parent ef2c431 commit 444958d
Show file tree
Hide file tree
Showing 7 changed files with 1,188 additions and 1,221 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ Pour chaque élément, des compositions sont possibles :
- `*` : couvrir toutes les unités (`0`, `1`, `2`, …) ;
- `-` : définir un intervalle (`1-3` corresponds aux unités `1`, `2` et `3`) ;
- `/` : indiquer le pas (`2-6/2` corresponds aux unités `2`, `4` et `6`) ;
- `,` : créer une liste (`4,8` corresponds aux unités `4` et `8`).
- `,` : créer une liste (`4,8` corresponds aux unités `4` et `8`) ;
- `?` : affecter l'unité courante à la création (pour une expression _cron_
créée à 13h37, la valeur `13` sera utilisée pour les heures et `37` pour les
minutes) ;
- `~` : générer un nombre aléatoire.

Il existe aussi des chaines spéciales :

Expand Down
200 changes: 7 additions & 193 deletions src/cronexp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,120 +5,7 @@
*/

import Field from "./field.js";

/**
* Les chaines spéciales avec leur équivalent.
*
* @type {Object<string, string>}
*/
const NICKNAMES = {
"@yearly": "0 0 1 1 *",
"@annually": "0 0 1 1 *",
"@monthly": "0 0 1 * *",
"@weekly": "0 0 * * 0",
"@daily": "0 0 * * *",
"@midnight": "0 0 * * *",
"@hourly": "0 * * * *",
};

/**
* Les formes littérales des mois et des jours de la semaine avec leur
* équivalent numérique. Les autres champs (minutes, heures et jour du mois)
* n'en ont pas.
*
* @type {Object<string, number>[]}
*/
const NAMES = [
// Minutes.
{},
// Heures.
{},
// Jour du mois.
{},
// Mois.
{
jan: 1,
feb: 2,
mar: 3,
apr: 4,
may: 5,
jun: 6,
jul: 7,
aug: 8,
sep: 9,
oct: 10,
nov: 11,
dec: 12,
},
// Jour de la semaine.
{
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6,
},
];

/**
* Les formes littérales des mois et des jours de la semaine avec leur
* équivalent numérique dans le cas où ils sont utilisés dans la valeur maximale
* d'un intervalle.
*
* @type {Object<string, number>[]}
*/
const NAMES_MAX = NAMES.map((n) => ("sun" in n ? { ...n, sun: 7 } : { ...n }));

/**
* Les expressions rationnelles pour découper un intervalle.
*
* @type {RegExp[]}
*/
const RANGE_REGEXES = NAMES.map(
(n) =>
new RegExp(
`^(?<min>\\d{1,2}|${Object.keys(n).join("|")})` +
`(?:-(?<max>\\d{1,2}|${Object.keys(n).join("|")})` +
"(?:/(?<step>\\d+))?)?$",
"u",
),
);

/**
* Les valeurs minimales et maximales (incluses) saisissables dans les cinq
* champs (pour des valeurs simples ou des intervalles).
*
* @type {Object<string, number>[]}
*/
const LIMITS = [
// Minutes.
{ min: 0, max: 59 },
// Heures.
{ min: 0, max: 23 },
// Jour du mois.
{ min: 1, max: 31 },
// Mois.
{ min: 1, max: 12 },
// Jour de la semaine.
{ min: 0, max: 7 },
];

/**
* Le nombre maximum de jours pour chaque mois.
*
* @type {number[]}
*/
const MAX_DAYS_IN_MONTHS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

/**
* Le message d'erreur (qui sera suivi de l'expression <em>cron</em>) renvoyé
* dans l'exception.
*
* @type {string}
*/
const ERROR = "Syntax error, unrecognized expression: ";
import parse from "./parse.js";

/**
* La classe d'une expression <em>cron</em>.
Expand Down Expand Up @@ -178,85 +65,12 @@ export default class CronExp {
* de caractères.
*/
constructor(pattern) {
// Remplacer l'éventuelle chaine spéciale par son équivalent et séparer
// les cinq champs (minutes, heures, jour du mois, mois et jour de la
// semaine).
const fields = (NICKNAMES[pattern] ?? pattern)
.toLowerCase()
.split(/\s+/u);
if (5 !== fields.length) {
throw new Error(ERROR + pattern);
}

// Parcourir les cinq champs.
const conds = fields.map((field, index) => {
if ("*" === field) {
return Field.all(LIMITS[index].min, LIMITS[index].max);
}

// Gérer les motifs "*/x".
const result = /^\*\/(?<step>\d+)$/u.exec(field);
if (undefined !== result?.groups) {
const step = Number(result.groups.step);
if (0 === step) {
throw new RangeError(ERROR + pattern);
}

return Field.range(LIMITS[index].min, LIMITS[index].max, step);
}

// Gérer les listes.
return Field.flat(
field.split(",").map((range) => {
const subresult = RANGE_REGEXES[index].exec(range);
if (undefined === subresult?.groups) {
throw new Error(ERROR + pattern);
}

const min =
NAMES[index][subresult.groups.min] ??
Number(subresult.groups.min);
const max =
undefined === subresult.groups.max
? min
: NAMES_MAX[index][subresult.groups.max] ??
Number(subresult.groups.max);
const step =
undefined === subresult.groups.step
? 1
: Number(subresult.groups.step);

// Vérifier que les valeurs sont dans les intervalles.
if (
min < LIMITS[index].min ||
LIMITS[index].max < max ||
max < min ||
0 === step
) {
throw new RangeError(ERROR + pattern);
}

return Field.range(min, max, step);
}),
);
});

this.#minutes = conds[0];
this.#hours = conds[1];
this.#date = conds[2];
// Faire commencer les mois à zéro.
this.#month = conds[3].map((v) => v - 1);
// Toujours utiliser zéro pour le dimanche.
this.#day = conds[4].map((v) => (7 === v ? 0 : v));

// Récupérer le nombre maximum de jours du mois le plus long parmi tous
// les mois autorisés.
const max = Math.max(
...this.#month.values().map((m) => MAX_DAYS_IN_MONTHS[m]),
);
if (max < this.#date.min) {
throw new RangeError(ERROR + pattern);
}
const fields = parse(pattern);
this.#minutes = fields.minutes;
this.#hours = fields.hours;
this.#date = fields.date;
this.#month = fields.month;
this.#day = fields.day;
}

/**
Expand Down
75 changes: 43 additions & 32 deletions src/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,65 @@
* @author Sébastien Règne
*/

/**
* Génère un nombre aléatoire entre le minimum et le maximum.
*
* @param {number} min La valeur minimale (incluse).
* @param {number} max La valeur maximale (incluse).
* @returns {number} Le nombre aléatoire généré.
*/
const random = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};

/**
* La classe d'un champ d'une expression <em>cron</em>.
*
* @class
*/
export default class Field {
/**
* Crée un champ d'une expression <em>cron</em> avec toutes les valeurs
* autorisées (sans restriction).
*
* @param {number} min La valeur minimale (incluse) autorisée.
* @param {number} max La valeur maximale (incluse) autorisée.
* @returns {Field} Le champ avec toutes les valeurs (sans restriction).
*/
static all(min, max) {
const values = [];
for (let value = min; value <= max; ++value) {
values.push(value);
}
return new Field(values, false);
}

/**
* Crée un champ d'une expression <em>cron</em> avec un intervalle de
* valeurs autorisées.
*
* @param {number} min La valeur minimale (incluse) autorisée.
* @param {number} max La valeur maximale (incluse) autorisée.
* @param {number} step Le pas entre les valeurs.
* @returns {Field} Le champ avec l'intervalle de valeurs.
* @param {number} min La valeur minimale (incluse) autorisée.
* @param {number} max La valeur maximale (incluse) autorisée.
* @param {number} step Le pas entre les valeurs.
* @param {Object} extra Les informations supplémentaires.
* @param {boolean} extra.restricted <code>true</code> pour un champ qui
* était différent de <code>"*"</code> ;
* sinon <code>false</code>.
* @param {boolean} extra.random <code>false</code> pour générer un
* nombre aléatoire pour le minimum.
* @returns {Field} Le champ avec les valeurs de l'intervalle.
*/
static range(min, max, step) {
static range(min, max, step, extra) {
const values = [];
for (let value = min; value <= max; value += step) {
for (
let value = extra.random
? random(min, Math.min(min + step - 1, max))
: min;
value <= max;
value += step
) {
values.push(value);
}
return new Field(values);
return new Field(values, extra.restricted);
}

/**
* Regroupe les valeurs de plusieurs champs.
*
* @param {Field[]} fields La liste des champs qui seront regroupés.
* @returns {Field} Le champ avec toutes les valeurs.
* @returns {Field} Le champ avec la fusion des valeurs.
*/
static flat(fields) {
return new Field(fields.flatMap((f) => f.#values));
return 1 === fields.length
? fields[0]
: new Field(
fields.flatMap((f) => f.#values),
true,
);
}

/**
Expand All @@ -71,14 +83,13 @@ export default class Field {
/**
* Crée un champ d'une expression <em>cron</em>.
*
* @param {number[]} values La liste des valeurs autorisées pour le
* champ.
* @param {boolean} [restricted] <code>true</code> (par défaut) pour un
* champ qui était différent de
* <code>"*"</code> ; sinon
* <code>false</code>.
* @param {number[]} values La liste des valeurs autorisées pour le
* champ.
* @param {boolean} restricted <code>true</code> pour un champ qui était
* différent de <code>"*"</code> ; sinon
* <code>false</code>.
*/
constructor(values, restricted = true) {
constructor(values, restricted) {
// Enlever les doublons et trier les valeurs pour faciliter les
// algorithmes.
this.#values = values
Expand Down
Loading

0 comments on commit 444958d

Please sign in to comment.