Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Validator | Description
**isBtcAddress(str)** | check if the string is a valid BTC address.
**isByteLength(str [, options])** | check if the string's length (in UTF-8 bytes) falls in a range.<br/><br/>`options` is an object which defaults to `{ min: 0, max: undefined }`.
**isCreditCard(str [, options])** | check if the string is a credit card number.<br/><br/> `options` is an optional object that can be supplied with the following key(s): `provider` is an optional key whose value should be a string, and defines the company issuing the credit card. Valid values include `['amex', 'dinersclub', 'discover', 'jcb', 'mastercard', 'unionpay', 'visa']` or blank will check for any provider.
**isCron(str [, options])** | check if the string is a valid five-field Unix cron expression. Lists, ranges, positive step values, three-letter month and weekday names, and the standard `@reboot`, `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`, `@midnight`, and `@hourly` aliases are supported.<br/><br/>`options` defaults to `{ allow_seconds: false }`. If `allow_seconds` is `true`, six-field expressions with a leading seconds field are also accepted. Quartz-specific syntax such as `?`, `L`, `W`, and `#` is not supported.
**isCurrency(str [, options])** | check if the string is a valid currency amount.<br/><br/>`options` is an object which defaults to `{ symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_decimal: true, require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false }`.<br/>**Note:** The array `digits_after_decimal` is filled with the exact number of digits allowed not a range, for example a range 1 to 3 will be given as [1, 2, 3].
**isDataURI(str)** | check if the string is a [data uri format][Data URI Format].
**isDate(str [, options])** | check if the string is a valid date. e.g. [`2002-07-15`, new Date()].<br/><br/> `options` is an object which can contain the keys `format`, `strictMode` and/or `delimiters`.<br/><br/>`format` is a string and defaults to `YYYY/MM/DD`.<br/><br/>`strictMode` is a boolean and defaults to `false`. If `strictMode` is set to true, the validator will reject strings different from `format`.<br/><br/> `delimiters` is an array of allowed date delimiters and defaults to `['/', '-']`.
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ import isEthereumAddress from './lib/isEthereumAddress';

import isCurrency from './lib/isCurrency';

import isCron from './lib/isCron';

import isBtcAddress from './lib/isBtcAddress';

import { isISO6346, isFreightContainerID } from './lib/isISO6346';
Expand Down Expand Up @@ -207,6 +209,7 @@ const validator = {
isPostalCodeLocales,
isEthereumAddress,
isCurrency,
isCron,
isBtcAddress,
isISO6346,
isFreightContainerID,
Expand Down
129 changes: 129 additions & 0 deletions src/lib/isCron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import assertString from './util/assertString';

const aliases = [
'@reboot',
'@yearly',
'@annually',
'@monthly',
'@weekly',
'@daily',
'@midnight',
'@hourly',
];

const months = [
'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC',
];

const days = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];

const standardFields = [
{ min: 0, max: 59 },
{ min: 0, max: 23 },
{ min: 1, max: 31 },
{ min: 1, max: 12, names: months },
{ min: 0, max: 7, names: days },
];

const secondsField = { min: 0, max: 59 };
const digits = /^\d+$/;

function parseValue(value, field) {
if (digits.test(value)) {
const number = parseInt(value, 10);
return number >= field.min && number <= field.max ? number : null;
}

if (field.names) {
const index = field.names.indexOf(value.toUpperCase());
if (index !== -1) {
return index + field.min;
}
}

return null;
}

function isValidRange(value, field) {
const range = value.split('-');

if (range.length !== 2) {
return false;
}

const start = parseValue(range[0], field);
const end = parseValue(range[1], field);

return start !== null && end !== null && start <= end;
}

function isValidItem(value, field) {
const stepped = value.split('/');

if (stepped.length > 2 || !stepped[0]) {
return false;
}

if (stepped.length === 2 && (!digits.test(stepped[1]) || parseInt(stepped[1], 10) === 0)) {
return false;
}

if (stepped[0] === '*') {
return true;
}

if (stepped[0].indexOf('-') !== -1) {
return isValidRange(stepped[0], field);
}

return parseValue(stepped[0], field) !== null;
}

function isValidField(value, field) {
const items = value.split(',');
return items.length > 0 && items.every(item => isValidItem(item, field));
}

function trimSpacesAndTabs(value) {
let start = 0;
let end = value.length;

while (start < end) {
const character = value.charAt(start);
if (character !== ' ' && character !== '\t') {
break;
}
start += 1;
}

while (end > start) {
const character = value.charAt(end - 1);
if (character !== ' ' && character !== '\t') {
break;
}
end -= 1;
}

return value.slice(start, end);
}

export default function isCron(str, options = {}) {
assertString(str);

const expression = trimSpacesAndTabs(str);

if (aliases.indexOf(expression) !== -1) {
return true;
}

const fields = expression.split(/[ \t]+/);
let fieldDefinitions = standardFields;

if (fields.length === 6 && options && options.allow_seconds === true) {
fieldDefinitions = [secondsField].concat(standardFields);
}

return fields.length === fieldDefinitions.length &&
fields.every((field, index) => isValidField(field, fieldDefinitions[index]));
}
119 changes: 119 additions & 0 deletions test/validators/isCron.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import assert from 'assert';
import validator from '../../src/index';
import test from '../testFunctions';

describe('isCron', () => {
it('should validate common five-field cron expressions', () => {
test({
validator: 'isCron',
valid: [
'* * * * *',
'0 0 1 1 *',
'*/15 0 1,15 * 1-5',
'0-59/2 0,12 1-31 JAN-MAR MON-FRI',
'5 4 * * sun',
'0 0 * jan,feb mon,wed,fri',
'00 04 01 01 0',
'0 0 31 2 *',
'0 0 * * 7',
'0 0 * * SUN-SAT/2',
'0/35 * * * *',
' 0\t0 * * * ',
],
invalid: [
'',
' ',
'* * * *',
'* * * * * *',
'60 * * * *',
'* 24 * * *',
'* * 0 * *',
'* * 32 * *',
'* * * 0 *',
'* * * 13 *',
'* * * * 8',
'* * * * MONDAY',
'* * * FOO *',
'* * * DEC-JAN *',
'* * * * FRI-MON',
'* * * * -1',
'1.5 * * * *',
'1foo * * * *',
'*/0 * * * *',
'*/ * * * *',
'*//2 * * * *',
'1-2-3 * * * *',
'1, * * * *',
'1,,2 * * * *',
'0 0\n* * *',
'0\u00a00 * * *',
'? * * * *',
'* * L * *',
'* * * * 3#2',
'@daily command',
],
});
});

it('should validate standard cron aliases', () => {
test({
validator: 'isCron',
valid: [
'@reboot',
'@yearly',
'@annually',
'@monthly',
'@weekly',
'@daily',
'@midnight',
'@hourly',
' @daily ',
],
invalid: [
'@secondly',
'@DAILY',
'@daily extra',
],
});
});

it('should optionally validate expressions with a leading seconds field', () => {
test({
validator: 'isCron',
args: [{ allow_seconds: true }],
valid: [
'* * * * *',
'* * * * * *',
'*/10 0 0 1 JAN MON',
'0-59/15 0,30 8-17 * * MON-FRI',
],
invalid: [
'60 * * * * *',
'* * * * * * *',
'0 0 0 ? * MON',
],
});

test({
validator: 'isCron',
args: [null],
valid: ['* * * * *'],
invalid: ['* * * * * *'],
});
});

it('should reject non-string inputs', () => {
test({
validator: 'isCron',
error: [null, undefined, 0, {}, []],
});
});

it('should process long invalid expressions without disproportionate delay', () => {
const expression = `x${'\t'.repeat(20000)}x`;
const start = Date.now();

assert.strictEqual(validator.isCron(expression), false);
assert.ok(Date.now() - start < 100);
});
Comment thread
rubiin marked this conversation as resolved.
Outdated
});
Loading