Skip to content

Commit

Permalink
Merge pull request #28 from anderssonjohan/#22
Browse files Browse the repository at this point in the history
Add support for seconds with human-readable format
  • Loading branch information
anderssonjohan committed Feb 8, 2017
2 parents 608e154 + c532bd3 commit 3d109f5
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 41 deletions.
190 changes: 161 additions & 29 deletions prettycron.js
Expand Up @@ -42,14 +42,83 @@ if ((!moment || !later) && (typeof require !== 'undefined')) {
return numbers.join(', ') + ' and ' + moment()._locale.ordinal(last_val);
};

var stepSize = function(numbers) {
if( !numbers || numbers.length <= 1 ) return 0;

var expectedStep = numbers[1] - numbers[0];
if( numbers.length == 2 ) return expectedStep;

// Check that every number is the previous number + the first number
return numbers.slice(1).every(function(n,i,a){
return (i === 0 ? n : n-a[i-1]) === expectedStep;
}) ? expectedStep : 0;
};

var isEveryOther = function(stepsize, numbers) {
return numbers.length === 30 && stepsize === 2;
};
var isTwicePerHour = function(stepsize, numbers) {
return numbers.length === 2 && stepsize === 30;
};
var isOnTheHour = function(numbers) {
return numbers.length === 1 && numbers[0] === 0;
};
var isStepValue = function(stepsize, numbers) {
// Value with slash (https://en.wikipedia.org/wiki/Cron#Non-Standard_Characters)
return numbers.length > 2 && stepsize > 0;
};
/*
* For an array of numbers of seconds, return a string
* listing all the values unless they represent a frequency divisible by 60:
* /2, /3, /4, /5, /6, /10, /12, /15, /20 and /30
*/
var getMinutesTextParts = function(numbers) {
var stepsize = stepSize(numbers);
if(!numbers) {
return { beginning: 'minute', text: '' };
}

var minutes = { beginning: '', text: '' };
if( isOnTheHour( numbers ) ) {
minutes.text = 'hour, on the hour';
} else if( isEveryOther( stepsize, numbers ) ) {
minutes.beginning = 'other minute';
} else if( isStepValue( stepsize, numbers ) ) {
minutes.text = stepsize + ' minutes';
} else if( isTwicePerHour( stepsize, numbers ) ) {
minutes.text = 'first and 30th minute';
} else {
minutes.text = numberList(numbers) + ' minute';
}
return minutes;
};
/*
* For an array of numbers of seconds, return a string
* listing all the values unless they represent a frequency divisible by 60:
* /2, /3, /4, /5, /6, /10, /12, /15, /20 and /30
*/
var getSecondsTextParts = function(numbers) {
var stepsize = stepSize(numbers);
if( !numbers ) {
return { beginning: 'second', text: '' };
}
if( isEveryOther( stepsize, numbers ) ) {
return { beginning: '', text: 'other second' };
} else if( isStepValue( stepsize, numbers ) ) {
return { beginning: '', text: stepsize + ' seconds' };
} else {
return { beginning: 'minute', text: 'starting on the ' + (numbers.length === 2 && stepsize === 30 ? 'first and 30th second' : numberList(numbers) + ' second') };
}
};

/*
* Parse a number into day of week, or a month name;
* used in dateList below.
*/
var numberToDateName = function(value, type) {
if (type == 'dow') {
if (type === 'dow') {
return moment().day(value - 1).format('ddd');
} else if (type == 'mon') {
} else if (type === 'mon') {
return moment().month(value - 1).format('MMM');
}
};
Expand Down Expand Up @@ -83,82 +152,145 @@ if ((!moment || !later) && (typeof require !== 'undefined')) {
return (x < 10) ? '0' + x : x;
};

var removeFromSchedule = function( schedule, member, length ) {
if( schedule[member] && schedule[member].length === length ) {
delete schedule[member];
}
}

//----------------

/*
* Given a schedule from later.js (i.e. after parsing the cronspec),
* generate a friendly sentence description.
*/
var scheduleToSentence = function(schedule) {
var output_text = 'Every ';
var scheduleToSentence = function(schedule, useSeconds) {
var textParts = [];

// A later.js schedules contains no member for time units where an asterisk is used,
// but schedules that means the same (e.g 0/1 is essentially the same as *) are
// returned with populated members.
// Remove all members that are fully populated to reduce complexity of code
removeFromSchedule( schedule, 'M', 12 );
removeFromSchedule( schedule, 'D', 31 );
removeFromSchedule( schedule, 'd', 7 );
removeFromSchedule( schedule, 'h', 24 );
removeFromSchedule( schedule, 'm', 60 );
removeFromSchedule( schedule, 's', 60 );

if (schedule['h'] && schedule['m'] && schedule['h'].length <= 2 && schedule['m'].length <= 2) {
var everySecond = useSeconds && schedule['s'] === undefined,
everyMinute = schedule['m'] === undefined,
everyHour = schedule['h'] === undefined
everyWeekday = schedule['d'] === undefined
everyDayInMonth = schedule['D'] === undefined,
everyMonth = schedule['M'] === undefined;

var oneOrTwoSecondsPerMinute = schedule['s'] && schedule['s'].length <= 2;
var oneOrTwoMinutesPerHour = schedule['m'] && schedule['m'].length <= 2;
var oneOrTwoHoursPerDay = schedule['h'] && schedule['h'].length <= 2;
var onlySpecificDaysOfMonth = schedule['D'] && schedule['D'].length !== 31;
if ( oneOrTwoHoursPerDay && oneOrTwoMinutesPerHour && oneOrTwoSecondsPerMinute ) {
// If there are only one or two specified values for
// hour or minute, print them in HH:MM format
// hour or minute, print them in HH:MM format, or HH:MM:ss if seconds are used
// If seconds are not used, later.js returns one element for the seconds (set to zero)

var hm = [];
var m = moment();
for (var i=0; i < schedule['h'].length; i++) {
for (var j=0; j < schedule['m'].length; j++) {
hm.push(zeroPad(schedule['h'][i]) + ':' + zeroPad(schedule['m'][j]));
for (var k=0; k < schedule['s'].length; k++) {
m.hour(schedule['h'][i]);
m.minute(schedule['m'][j]);
m.second(schedule['s'][k]);
hm.push(m.format( useSeconds ? 'HH:mm:ss' : 'HH:mm'));
}
}
}
if (hm.length < 2) {
output_text = hm[0];
textParts.push( hm[0] );
} else {
var last_val = hm.pop();
output_text = hm.join(', ') + ' and ' + last_val;
textParts.push( hm.join(', ') + ' and ' + last_val );
}
if (!schedule['d'] && !schedule['D']) {
output_text += ' every day';
if (everyWeekday && everyDayInMonth) {
textParts.push('every day');
}

} else {
var seconds = getSecondsTextParts(schedule['s']);
var minutes = getMinutesTextParts(schedule['m']);
var beginning = '';
var end = '';

textParts.push('Every');

// Otherwise, list out every specified hour/minute value.
var hasSpecificSeconds = schedule['s'] && (
schedule['s'].length > 1 && schedule['s'].length < 60 ||
schedule['s'].length === 1 && schedule['s'][0] !== 0 );
if(hasSpecificSeconds) {
beginning = seconds.beginning;
end = seconds.text;
}

if(schedule['h']) { // runs only at specific hours
if( hasSpecificSeconds ) {
end += ' on the ';
}
if (schedule['m']) { // and only at specific minutes
output_text += numberList(schedule['m']) + ' minute past the ' + numberList(schedule['h']) + ' hour';
var hours = numberList(schedule['h']) + ' hour';
if( !hasSpecificSeconds && isOnTheHour(schedule['m']) ) {
textParts = [ 'On the' ];
end += hours;
} else {
beginning = minutes.beginning;
end += minutes.text + ' past the ' + hours;
}
} else { // specific hours, but every minute
output_text += 'minute of ' + numberList(schedule['h']) + ' hour';
end += 'minute of ' + numberList(schedule['h']) + ' hour';
}
} else if(schedule['m']) { // every hour, but specific minutes
if (schedule['m'].length == 1 && schedule['m'][0] == 0) {
output_text += 'hour, on the hour';
} else {
output_text += numberList(schedule['m']) + ' minute past every hour';
beginning = minutes.beginning;
end += minutes.text;
if( !isOnTheHour(schedule['m']) && ( onlySpecificDaysOfMonth || schedule['d'] || schedule['M'] ) ) {
end += ' past every hour';
}
} else { // cronspec has "*" for both hour and minute
output_text += 'minute';
} else if( !schedule['s'] && !schedule['m'] ) {
beginning = seconds.beginning;
} else if( !useSeconds || !hasSpecificSeconds) { // cronspec has "*" for both hour and minute
beginning += minutes.beginning;
}
textParts.push(beginning);
textParts.push(end);
}

if (schedule['D'] && schedule['D'].length !== 31) { // runs only on specific day(s) of month
output_text += ' on the ' + numberList(schedule['D']);
if (onlySpecificDaysOfMonth) { // runs only on specific day(s) of month
textParts.push('on the ' + numberList(schedule['D']));
if (!schedule['M']) {
output_text += ' of every month';
textParts.push('of every month');
}
}

if (schedule['d']) { // runs only on specific day(s) of week
if (schedule['D']) {
// if both day fields are specified, cron uses both; superuser.com/a/348372
output_text += ' and every ';
textParts.push('and every');
} else {
output_text += ' on ';
textParts.push('on');
}
output_text += dateList(schedule['d'], 'dow');
textParts.push(dateList(schedule['d'], 'dow'));
}

if (schedule['M']) {
if( schedule['M'].length === 12 ) {
output_text += ' day of every month'
textParts.push('day of every month');
} else {
// runs only in specific months; put this output last
output_text += ' in ' + dateList(schedule['M'], 'mon');
textParts.push('in ' + dateList(schedule['M'], 'mon'));
}
}

return output_text;
return textParts.filter(function(p) { return p; }).join(' ');
};

//----------------
Expand All @@ -168,7 +300,7 @@ if ((!moment || !later) && (typeof require !== 'undefined')) {
*/
var toString = function(cronspec, sixth) {
var schedule = later.parse.cron(cronspec, sixth);
return scheduleToSentence(schedule['schedules'][0]);
return scheduleToSentence(schedule['schedules'][0], sixth);
};

/*
Expand Down
52 changes: 40 additions & 12 deletions test/homepage-examples.js
@@ -1,7 +1,12 @@
var assert = require('assert');

var prettyCron = require('../');

function assertReadableOutput(item) {
test(item.cron + ' == ' + item.readable, function() {
var readable_output = prettyCron.toString(item.cron, !!item.sixth );
assert.equal(readable_output, item.readable);
} );
}
suite('homepage examples', function() {
suite('human-readable output', function() {

Expand All @@ -16,20 +21,43 @@ suite('homepage examples', function() {
{ cron: '15 * * * 1,2', readable: 'Every 15th minute past every hour on Mon and Tue' },
{ cron: '* 8,10,12,14,16,18,20 * * *', readable: 'Every minute of 8, 10, 12, 14, 16, 18 and 20th hour' },
{ cron: '0 12 15,16 1 3', readable: '12:00 on the 15 and 16th and every Wed in Jan' },
{ cron: '0 4,8,12,4 * * 4,5,6', readable: 'Every 0th minute past the 4, 8 and 12th hour on Thu, Fri and Sat' },
{ cron: '0 4,8,12,4 * * 4,5,6', readable: 'On the 4, 8 and 12th hour on Thu, Fri and Sat' },
{ cron: '0 2,16 1,8,15,22 * 1,2', readable: '02:00 and 16:00 on the 1, 8, 15 and 22nd of every month and every Mon and Tue' },
{ cron: '15 3,8,10,12,14,16,18 16 * *', readable: 'Every 15th minute past the 3, 8, 10, 12, 14, 16 and 18th hour on the 16th of every month' },
{ cron: '2 8,10,12,14,16,18 * 8 0,3', readable: 'Every 2nd minute past the 8, 10, 12, 14, 16 and 18th hour on Sun and Wed in Aug' },
{ cron: '0 0 18 1/1 * ?', readable: '00:00 on the 18th day of every month' },
{ cron: '0 0 18 1/1 * ? *', readable: '18:00', sixth: true },
{ cron: '30 10 * * 0', readable: '10:30 on Sun' }
].forEach(function(item) {
test(item.cron, function() {
var readable_output = prettyCron.toString(item.cron, !!item.sixth );
assert.equal(readable_output, item.readable);
});
});

{ cron: '0 0 18 1/1 * ?', readable: '00:00 on the 18th of every month' },
{ cron: '30 10 * * 0', readable: '10:30 on Sun' },
{ cron: '* * * * *', readable: 'Every minute' },
{ cron: '*/2 * * * *', readable: 'Every other minute' },
].forEach(assertReadableOutput);
});
});

suite('extended format with seconds', function() {
suite('human-readable output', function() {
[
{ cron: '0 0 18 1/1 * ? *', readable: '18:00:00 every day', sixth: true },
{ cron: '* * * * * *', readable: 'Every second', sixth: true },
{ cron: '0/1 0/1 0/1 0/1 0/1 0/1', readable: 'Every second', sixth: true },
{ cron: '*/4 2 4 * * *', readable: 'Every 4 seconds on the 2nd minute past the 4th hour', sixth: true },
{ cron: '30 15 9 * * *', readable: '09:15:30 every day', sixth: true },
{ cron: '*/30 15 9 * * *', readable: '09:15:00 and 09:15:30 every day', sixth: true },
{ cron: '*/2 * * * * *', readable: 'Every other second', sixth: true },
{ cron: '*/3 * * * * *', readable: 'Every 3 seconds', sixth: true },
{ cron: '*/4 * * * * *', readable: 'Every 4 seconds', sixth: true },
{ cron: '*/5 * * * * *', readable: 'Every 5 seconds', sixth: true },
{ cron: '*/6 * * * * *', readable: 'Every 6 seconds', sixth: true },
{ cron: '*/10 * * * * *', readable: 'Every 10 seconds', sixth: true },
{ cron: '*/12 * * * * *', readable: 'Every 12 seconds', sixth: true },
{ cron: '*/15 * * * * *', readable: 'Every 15 seconds', sixth: true },
{ cron: '*/20 * * * * *', readable: 'Every 20 seconds', sixth: true },
{ cron: '*/30 * * * * *', readable: 'Every minute starting on the first and 30th second', sixth: true },
{ cron: '5 * * * * *', readable: 'Every minute starting on the 5th second', sixth: true },
{ cron: '5 */2 * * * *', readable: 'Every other minute starting on the 5th second', sixth: true },
{ cron: '30 * * * * *', readable: 'Every minute starting on the 30th second', sixth: true },
{ cron: '0,2,4,20 * * * * *', readable: 'Every minute starting on the 0, 2, 4 and 20th second', sixth: true },
{ cron: '5,10/30 * * * 1,3 8', readable: 'Every minute starting on the 5, 10 and 40th second on Sat in Jan and Mar', sixth: true },
{ cron: '15-17 * * * * *', readable: 'Every minute starting on the 15, 16 and 17th second', sixth: true },
].forEach(assertReadableOutput);
});
});

0 comments on commit 3d109f5

Please sign in to comment.