[Modified]
A little help here can do.
I ran into a problem with for loop. I got one for loop nested in another. The outer loop has just level of elements, while the nested contains only 27 items.
The problem is that the setup can only run through 22 items of the nested array before exiting.
The setup is running on the server side, done in node.js. The Client is React which communicates with the server through websocket. From the console report, node.js runs the above code 3x; and in all those times, only 22 items are looped out of the expected 27.
Join at Repl for the full Node.js dir:
https://replit.com/join/uabhesdiif-emexrevolarter
The code below. I appreciate any help. Thank you.
VerifyResult.js
let data = {};
let error = [];
let feedback = [];
if(typeof dataJson == 'string') {
data = JSON.parse(dataJson)
} else {
data = dataJson;
}
try {
// get variables
const getAvatarState = data.settings.isAvatar;
const getIdState = data.settings.isAdminId;
const getStudents = data.students[0]['Students Data'];
const getClassInfo = data.students[0]['Class Info'][0];
const getAvatars = data.images;
const getLogo = data.logo;
const getFilenames = data.filenames;
const getFirstTermState = data.settings.isFirstTerm;
const getSecondTermState = data.settings.isSecondTerm;
const getThirdTermState = data.settings.isThirdTerm;
const getSubjectList = data.subjects;
let getFirstTerm = [];
let getSecondTerm =[];
let getThirdTerm = [];
// verify #1: if avatars are permitted & same number of images included
if(getAvatarState == '1') {
if(getStudents.length > getAvatars.length) {
feedback.push(`${getAvatars.length} Images supplied for Avatar is lesser than the number of ${getStudents.length} students`);
feedback.push('Some Results may not include Avatar')
}
if(getStudents.length < getAvatars.length) {
feedback.push(`${getAvatars.length} Images supplied for Avatar is more than the number of ${getStudents.length} students`);
feedback.push('This may not pose any problem except possible mismatch')
}
} else {
feedback.push('Avatar will not be included in any result')
}
// verify #2: if logo is included
if(getLogo == null) {
feedback.push('No image was added as logo, and hence, will not be included')
}
// verify #3: if empty value for Admin Numbers, Names, in Students file
let idList = [];
let countItem = 0;
let innerCount = 0;
for(let k = 0; k < getStudents.length; k++) {
let d = getStudents[k];
countItem = k + 1;
let fName, lName;
if(getIdState == '1') {
if(d.Admin_Number != undefined && d.Admin_Number != '') {
idList.push(d.Admin_Number);
} else {
error.push(`Admin Number for Student at row "${countItem}" is empty, in Students workbook`)
}
}
if(d.Firstname != undefined && d.Firstname != '') {
fName = d.Firstname
} else {
error.push(`Firstname for Student at row "${countItem}" is empty, in Students workbook`)
}
if(d.Surname != undefined && d.Surname != '') {
lName = d.Surname
} else {
error.push(`Surname for Student at row "${countItem}" is empty, in Students workbook`)
}
if(fName != undefined && lName != undefined) {
(getIdState == '0') && idList.push(`${lName}/${fName}`);
}
};
// verify #4: if IDs for Students file is unique
if(!IsArrayUnique(idList)) {
(getIdState == '1') && error.push(`One or Student Admin Numbers are not unique, in Students file`);
(getIdState == '0') && error.push(`One or more Student names are not unique, in Students file`);
}
// verify #5: if empty value for Class Info, in Students file
if(getClassInfo.Class_Name == undefined) {
error.push('Class Name of Class Info worksheet is empty, in Students file')
}
if(getClassInfo.School == undefined) {
error.push('School Name of Class Info worksheet is empty, in Students file')
}
if(getClassInfo.Session == undefined) {
error.push('Session of Class Info worksheet is empty, in Students file')
}
if(getClassInfo.Times_School_Opened == undefined) {
error.push('No of Times School Opened of Class Info worksheet is empty, in Students file')
}
countItem = 0;
for(let i = 0; i < getSubjectList.length; i++) {
let sub = getSubjectList[i];
let gFile = getFilenames[i];
countItem = i + 1;
let innError = false;
let list = [];
let subj, fName, lName, sName, termName;
if(getFirstTermState == '1') {
// verify #6: if empty value in Subject files for First Term
innError = false;
termName = 'First Term';
subj = sub['First Term'];
if(sub['Class Info'][0].Class_Name == undefined) {
innError = true;
error.push(`Class Name in ${gFile} is empty`)
}
if(sub['Class Info'][0].Subject == undefined) {
innError = true;
error.push(`Subject Name in ${gFile} is empty`)
}
if(subj < 1) {
innError = true;
error.push(`Subject scores for ${termName} cannot be found in ${gFile}`)
}
if(!innError) {
console.log('==== STUDENTS TOTAL: ', subj.length);
innerCount = 0;
for(let j = 0; j < subj.length; j++) {
let item = subj[j];
innerCount = j + 1;
if(item.Surname == undefined) {
innError = true;
error.push(`Surname for a Student at row ${innerCount} is empty, for ${termName} in ${gFile}`)
}
if(item.Firstname == undefined) {
innError = true;
error.push(`Firstname for a Student at row ${innerCount} is empty, for ${termName} in ${gFile}`)
}
if(item.Admin_Number == undefined && getIdState == '1') {
innError = true;
error.push(`Admin Number for a Student at row ${innerCount} is empty, for ${termName} in ${gFile}`)
}
if(!innError) {
fName = item.Firstname;
lName = item.Surname;
if(item.CA == undefined && !innError) {
innError = true;
error.push(`CA for "${lName} ${fName}" at row ${innerCount} is empty, for ${termName} in ${gFile}`)
}
if(item.Exam == undefined && !innError) {
innError = true;
error.push(`Exam for "${lName} ${fName}" at row ${innerCount} is empty, for ${termName} in ${gFile}`)
}
}
// verify #7: if both CA & Exam are numbers for First Term
if(!innError) {
console.log('====== TOTAL READ: ', innerCount + ' | ' + countItem);
console.log('====== IS CA A NUMBER?: ', item.CA + ' | ' + IsNumber(item.CA));
console.log('====== IS Exam A NUMBER?: ', item.Exam + ' | ' + IsNumber(item.Exam));
if(!IsNumber(item.CA)) {
innError = true;
error.push(`CA for "${lName} ${fName}" at row ${innerCount} is not a number (${item.CA}), for ${termName} in ${gFile}`)
}
if(!IsNumber(item.Exam)) {
innError = true;
error.push(`Exam for "${lName} ${fName}" at row ${innerCount} is not a number (${item.Exam}), for ${termName} in ${gFile}`)
}
}
// verify #8: if Total is > 45 & < 50
if(!innError) {
const itemTotal = Number(item.CA) + Number(item.Exam);
if(itemTotal > 45 && itemTotal < 50) {
innError = true;
error.push(`The CA (CA: ${item.CA}; Exam: ${item.Exam}; Total: ${itemTotal}) for "${lName} ${fName}" at row ${innerCount}, should be upgraded to 50, for ${termName} in ${gFile}`)
}
}
if(!innError) {
if(getIdState == '1'){
sName = item.Admin_Number;
list.push(sName);
} else {
sName = lName + '/' + fName;
list.push(`${sName}`);
}
// verify #9: match IDs as Admin Number or Names for First Term
if(idList.indexOf(sName) === -1) {
innError = true;
(getIdState == '1') && error.push(`Student Admin Number "${sName}" is mismatched, for ${termName} in ${gFile}`);
(getIdState == '0') && error.push(`Student name "${lName} ${fName}" is mismatched, for ${termName} in ${gFile}`);
}
}
}
// verify #10: if unique IDs for First Term
if(!innError) {
if(idList.length == list.length) {
if(!IsArrayUnique(list)){
innError = true;
(getIdState == '1') && error.push(`One or more Student Admin Numbers are not unique, for ${termName} in ${gFile}`)
(getIdState == '0') && error.push(`One or more Student names are not unique, for ${termName} in ${gFile}`);
}
} else {
innError = true;
error.push(`Found ${list.length} Students against expected ${idList.length} number of Students, for ${termName} in ${gFile}`)
}
}
}
}
}
// verify #3: match admin Id is set as active, else match students names
} catch (err) {
error.push(err)
}
const res = {
data: data,
error: error,
feedback: feedback
}
data.json
{"settings":{"isAdminId":"1","isFirstTerm":"1","isSecondTerm":"0","isThirdTerm":"0","isAvatar":"1","isVerify":"1"},"subjects":[{"First Term":[{"Surname":"ADEOLA","Firstname":"TOLUWALASE","CA":38,"Exam":56,"Admin_Number":"AND/J/0271"},{"Surname":"ADEMOLA","Firstname":"SAMUEL","CA":35,"Exam":55,"Admin_Number":"ADM/J/0273"},{"Surname":"AGBONIRO","Firstname":"JESSE","CA":40,"Exam":54,"Admin_Number":"ADM/J/0284"},{"Surname":"AJAYI","Firstname":"ABIGAIL","CA":23,"Exam":33,"Admin_Number":"ADM/J/0269"},{"Surname":"AKANDE","Firstname":"MICHAEL","CA":29,"Exam":41,"Admin_Number":"ADM/J/0267"},{"Surname":"AKINTOKUN","Firstname":"MOFE","CA":27,"Exam":47,"Admin_Number":"ADM/J/0272"},{"Surname":"ARTHUR","Firstname":"DESTINY","CA":33,"Exam":55,"Admin_Number":"ADM/J/0266"},{"Surname":"AYANWENU","Firstname":"AKOREDE","CA":22,"Exam":35,"Admin_Number":"ADM/J/0270"},{"Surname":"BETHEL","Firstname":"MOYINULUWA","CA":40,"Exam":58,"Admin_Number":"ADM/J/0274"},{"Surname":"CHIJIOKE","Firstname":"JOY","CA":26,"Exam":44,"Admin_Number":"ADM/J/0276"},{"Surname":"CHINWUBA","Firstname":"CHARLES","CA":33,"Exam":40,"Admin_Number":"ADM/J/0275"},{"Surname":"DAVID","Firstname":"BEST","CA":40,"Exam":59,"Admin_Number":"ADM/J/0277"},{"Surname":"DUYILE","Firstname":"IFEOLUWA","CA":29,"Exam":41,"Admin_Number":"ADM/J/0256"},{"Surname":"EZEADILI","Firstname":"CHIBUNDU","CA":35,"Exam":49,"Admin_Number":"ADM/J/0278"},{"Surname":"FAGITE","Firstname":"TOBILOBA","CA":40,"Exam":57,"Admin_Number":"ADM/J/0279"},{"Surname":"IDOGEN","Firstname":"EHIZOFUA","CA":31,"Exam":51,"Admin_Number":"ADM/J/0282"},{"Surname":"IFADA","Firstname":"HOSSONA","CA":33,"Exam":50,"Admin_Number":"ADM/J/0255"},{"Surname":"IHEDIOHA","Firstname":"DELIGHT","CA":34,"Exam":53,"Admin_Number":"ADM/J/0280"},{"Surname":"JOSIAH","Firstname":"MATTHEW","CA":29,"Exam":49,"Admin_Number":"ADM/J/0283"},{"Surname":"MBONU","Firstname":"MMESOMA","CA":32,"Exam":45,"Admin_Number":"ADM/J/0285"},{"Surname":"NWONU","Firstname":"DAVID","CA":27,"Exam":41,"Admin_Number":"ADM/J/0260"},{"Surname":"OBI","Firstname":"LUCIA","CA":20,"Exam":26,"Admin_Number":"ADM/J/0289"},{"Surname":"ODOZOR","Firstname":"AUSTIN","CA":19,"Exam":29,"Admin_Number":"ADM/J/0249"},{"Surname":"OLAWALE","Firstname":"MOYINOLUWA","CA":29,"Admin_Number":"ADM/J/0290"},{"Surname":"OKERENITE","Firstname":"SPLENDID","Exam":53,"Admin_Number":"ADM/J/0287"},{"Surname":"OKUSHI","Firstname":"DANIEL","CA":32,"Exam":"x","Admin_Number":"ADM/J/0288"},{"Surname":"DABRINZE","Firstname":"POSSIBLE","CA":"y","Exam":40,"Admin_Number":"ADM/J/0250"}],"Second Term":[],"Third Term":[],"Class Info":[{"Class_Name":"JS2 Goodness","Subject":"BST (Information Tech.)"}],"ID":[{"Id":"Subjects","Start_Row":3}]}],"filenames":["subject_ict.xlsx"],"students":[{"Students Data":[{"Surname":"ADEOLA","Firstname":"TOLUWALASE","House":"Green","Admin_Number":"AND/J/0271","DOB":"26 FEB 2010","Sex":"M","Times_Present":126,"Override_Teacher_Comment":"very naughty","Override_Skills_Musical":3,"Override_Skills_Painting":4,"Override_Skills_Craft":3,"Override_Skills_Tools":3,"Override_Skills_Fluency":4,"Override_Sports_Indoor":4,"Override_Sports_Ball":4,"Override_Sports_Combative":3,"Override_Sports_Track":3,"Override_Sports_Gymnastics":4,"Override_Curricular_Jets":5,"Override_Curricular_Farmers":3,"Override_Curricular_Debating":4,"Override_Curricular_Homemaker":5,"Override_Curricular_Drama":5,"Override_Curricular_Voluntary":4,"Override_Curricular_Others":3,"Override_Behaviour_Reliability":5,"Override_Behaviour_Neatness":5,"Override_Behaviour_Politeness":5,"Override_Behaviour_Honesty":5,"Override_Behaviour_Creativity":4,"Override_Behaviour_Leadership":5,"Override_Behaviour_Spirituality":5,"Override_Behaviour_Cooporation":5},{"Surname":"ADEMOLA","Firstname":"SAMUEL","House":"Green","Admin_Number":"ADM/J/0273","DOB":"27 MAY 2009","Sex":"M","Times_Present":126},{"Surname":"AGBONIRO","Firstname":"JESSE","House":"Red","Admin_Number":"ADM/J/0284","DOB":"9 SEPT 2010","Sex":"M","Times_Present":126},{"Surname":"AJAYI","Firstname":"ABIGAIL","House":"Green","Admin_Number":"ADM/J/0269","DOB":"JUNE 2010","Sex":"F","Times_Present":120},{"Surname":"AKANDE","Firstname":"MICHAEL","House":"Red","Admin_Number":"ADM/J/0267","DOB":"29 DEC 2010","Sex":"M","Times_Present":126},{"Surname":"AKINTOKUN","Firstname":"MOFE","House":"Yellow","Admin_Number":"ADM/J/0272","DOB":"21 JULY 2010","Sex":"M","Times_Present":126},{"Surname":"ARTHUR","Firstname":"DESTINY","House":"Green","Admin_Number":"ADM/J/0266","DOB":"23 AUG 2009","Sex":"M","Times_Present":126},{"Surname":"AYANWENU","Firstname":"AKOREDE","House":"Blue","Admin_Number":"ADM/J/0270","DOB":"17 MAY 2010","Sex":"F","Times_Present":126},{"Surname":"BETHEL","Firstname":"MOYINULUWA","House":"Blue","Admin_Number":"ADM/J/0274","DOB":"13 MAR 2010","Sex":"F","Times_Present":126},{"Surname":"CHIJIOKE","Firstname":"JOY","House":"Yellow","Admin_Number":"ADM/J/0276","DOB":"27 FEB 2010","Sex":"F","Times_Present":126},{"Surname":"CHINWUBA","Firstname":"CHARLES","House":"Yellow","Admin_Number":"ADM/J/0275","DOB":"15 MAY 2010","Sex":"M","Times_Present":122},{"Surname":"DAVID","Firstname":"BEST","House":"Yellow","Admin_Number":"ADM/J/0277","DOB":"3 MAR 2010","Sex":"F","Times_Present":126},{"Surname":"DUYILE","Firstname":"IFEOLUWA","House":"Green","Admin_Number":"ADM/J/0256","DOB":"8 JAN 2010","Sex":"M","Times_Present":126},{"Surname":"EZEADILI","Firstname":"CHIBUNDU","House":"Yellow","Admin_Number":"ADM/J/0278","DOB":"14 FEB 2010","Sex":"M","Times_Present":126},{"Surname":"FAGITE","Firstname":"TOBILOBA","House":"Blue","Admin_Number":"ADM/J/0279","DOB":"26 AUG 2010","Sex":"M","Times_Present":126},{"Surname":"IDOGEN","Firstname":"EHIZOFUA","House":"Blue","Admin_Number":"ADM/J/0282","DOB":"6 JUNE 2010","Sex":"F","Times_Present":126},{"Surname":"IFADA","Firstname":"HOSSONA","House":"Red","Admin_Number":"ADM/J/0255","DOB":"12 OCT 2010","Sex":"M","Times_Present":126},{"Surname":"IHEDIOHA","Firstname":"DELIGHT","House":"Green","Admin_Number":"ADM/J/0280","DOB":"4 NOV 2010","Sex":"M","Times_Present":126},{"Surname":"JOSIAH","Firstname":"MATTHEW","House":"Red","Admin_Number":"ADM/J/0283","DOB":"13 AUG 2010","Sex":"F","Times_Present":126},{"Surname":"MBONU","Firstname":"MMESOMA","House":"Green","Admin_Number":"ADM/J/0285","DOB":"17 MAY 2009","Sex":"F","Times_Present":126},{"Surname":"NWONU","Firstname":"DAVID","House":"Yellow","Admin_Number":"ADM/J/0260","DOB":"29 AUG 2010","Sex":"M","Times_Present":124},{"Surname":"OBI","Firstname":"LUCIA","House":"Red","Admin_Number":"ADM/J/0289","DOB":"29 DEC 2010","Sex":"F","Times_Present":126},{"Surname":"ODOZOR","Firstname":"AUSTIN","House":"Green","Admin_Number":"ADM/J/0249","DOB":"9 AUG 2008","Sex":"M","Times_Present":126},{"Surname":"OLAWALE","Firstname":"MOYINOLUWA","House":"Red","Admin_Number":"ADM/J/0290","DOB":"19 SEP 2010","Sex":"F","Times_Present":108},{"Surname":"OKERENITE","Firstname":"SPLENDID","House":"Green","Admin_Number":"ADM/J/0287","DOB":"14 DEC 2009","Sex":"M","Times_Present":126},{"Surname":"OKUSHI","Firstname":"DANIEL","House":"Blue","Admin_Number":"ADM/J/0288","DOB":"29 SEP 2010","Sex":"M","Times_Present":126},{"Surname":"DABRINZE","Firstname":"POSSIBLE","House":"Yellow","Admin_Number":"ADM/J/0250","DOB":"20 JUN 2010","Sex":"M","Times_Present":126}],"Class Info":[{"Class_Name":"JS2 Goodness","School":"Junior Secondary","Session":"2021/2022","Times_School_Opened":126,"Next_Term_Begins":"4th January, 2022"}],"Settings":[{"Id_By_Admin_No":"Yes","First_Term_Active":"Yes","Second_Term_Active":"No","Third_Term_Active":"No","Students_Photos":"Yes","Verify":"Yes"}],"ID":[{"Id":"Students","Start_Row":3}]}],"keys":[{"Junior Grade Keys":[{"Grade_Name":"A","Min_Value":70,"Max_Value":100},{"Grade_Name":"B","Min_Value":60,"Max_Value":69.9},{"Grade_Name":"C","Min_Value":50,"Max_Value":59.9},{"Grade_Name":"P","Min_Value":40,"Max_Value":49.9},{"Grade_Name":"F","Min_Value":0,"Max_Value":39.9}],"Senior Grade Keys":[{"Grade_Name":"A","Min_Value":75,"Max_Value":100},{"Grade_Name":"B2","Min_Value":70,"Max_Value":74.9},{"Grade_Name":"B3","Min_Value":65,"Max_Value":69.9},{"Grade_Name":"C4","Min_Value":60,"Max_Value":64.9},{"Grade_Name":"C5","Min_Value":55,"Max_Value":59.9},{"Grade_Name":"C6","Min_Value":50,"Max_Value":54.9},{"Grade_Name":"D7","Min_Value":45,"Max_Value":49.9},{"Grade_Name":"E8","Min_Value":40,"Max_Value":44.9},{"Grade_Name":"F9","Min_Value":0,"Max_Value":39.9}],"Rating Keys":[{"Rating_Name":"Skill","Min_Value":2,"Max_Value":5},{"Rating_Name":"Sports","Min_Value":2,"Max_Value":5},{"Rating_Name":"Curricular","Min_Value":2,"Max_Value":5},{"Rating_Name":"Behaviour","Min_Value":2,"Max_Value":5}],"School Info":[{"School_Name":"SAMUEL ADEGBITE ANGLICAN COLLEGE","Extra_Info":"(A Day-Co-educational Institution Of The Anglican Diocese Of Lagos West)","Address":"St. Peters Anglican Church, Ikotun Road, Idumu, Lagos.","Title":"SECONDARY SCHOOL STUDENT'S INTERNAL ACADEMIC REPORT SHEET","Watermark":"SAMUEL ADEGBITE ANGLICAN COLLEGE"},{"Address":null,"Title":null},{"Address":null,"Title":null},{"Address":null,"Title":null}],"ID":[{"Id":"DON'T CHANGE!!!","Start_Row":null},{"Id":"File ID","Start_Row":"Start Row"},{"Id":"Keys","Start_Row":1},{"Id":null}]}],"pool":[{"Principal Comment":[{"Pool":"Good performance, keep it up"},{"Pool":"A wonderful performance, keep it up"},{"Pool":"A highly impressive result, keep it up"},{"Pool":"Good result but you need to improve in your weak area(s)"},{"Pool":"Average result, there is still room for improvement"},{"Pool":"Average result, you can still do better"},{"Pool":"A fair result, you really need to improve in your weak subject(s)"},{"Pool":"Weak performance, you really need to improve in your weak subjects"},{"Pool":"A very good performance, keep it up"},{"Pool":"Beautiful performance, keep the flag flying"},{"Pool":"Weak result, you need to concentrate more on your studies"},{"Pool":"Excellent performance, keep it up"},{"Pool":"A brilliant performance, keep it up"},{"Pool":"Good result, there is room for improvement"},{"Pool":"An average result, you really need to study harder"},{"Pool":"A very good result, weldone."},{"Pool":"A beautiful result, keep it up"},{"Pool":"An impressive result, keep it up"},{"Pool":"A very good performance, keep the flag flying"},{"Pool":"An average result, there is still room for improvement,"},{"Pool":"A fair result, you can still do better."},{"Pool":"A weak result, there is room for you to work harder."},{"Pool":"A very weak performance, you seriously need to work harder."},{"Pool":"Poor performance, your need to work on your weak subjects"},{"Pool":"Excellent performance, keep it up."},{"Pool":null}],"Teacher Comment":[{"Pool":"Always willing to take up responsiblities"},{"Pool":"Very friendly and humorous"},{"Pool":"A highly talented student"},{"Pool":"Very hardworking and conscioustious"},{"Pool":"Friendly but needs to put in more efforts"},{"Pool":"Easy-going and very neat"},{"Pool":"Always punctual to class and all other activities"},{"Pool":"Highly dependable and trustworthy"},{"Pool":"Very reliable and hardworking"},{"Pool":"Active and always willing to take up responsibilities"},{"Pool":"Friendly but needs to pay more attention to his/her studies"},{"Pool":"Friendly but needs to settle down for serious work"},{"Pool":"Easy-going but needs to be more punctual"},{"Pool":"Very studious and always attentive in class"},{"Pool":"Very reliable and willing to assist others"},{"Pool":"Highly cooperative and dependable"},{"Pool":"Friendly but needs to be less playful"},{"Pool":"Friendly but restless, needs to always observe siesta"},{"Pool":"Highly organized and conscientious"},{"Pool":"Friendly and highly willing to learn"},{"Pool":"A consistent and handworking student"},{"Pool":"Very reliable and ready to take to correction"},{"Pool":null},{"Pool":null},{"Pool":null}],"Skills":[{"Pool":"Musical Instrument"},{"Pool":"Drawing and Painting"},{"Pool":"Craft"},{"Pool":"Handling of Workshop Tools"},{"Pool":"Handwriting/Fluency"}],"Sports":[{"Pool":"Indoor Games"},{"Pool":"Ball Games"},{"Pool":"Combative Games"},{"Pool":"Track/Throws"},{"Pool":"Gymnastics"}],"Curricular":[{"Pool":"JETS"},{"Pool":"Young Farmers Club"},{"Pool":"Literary & Debating"},{"Pool":"Home Maker"},{"Pool":"Drama/Culture/Music/Craft"},{"Pool":"Voluntary Service Org."},{"Pool":"Others"}],"Behaviour":[{"Pool":"Reliability"},{"Pool":"Neatness"},{"Pool":"Politeness"},{"Pool":"Honesty"},{"Pool":"Initiative/Creativity"},{"Pool":"Leadership Role"},{"Pool":"Level of Spiritual Dev.","Override":3},{"Pool":"Spirit of Co-operation"}],"ID":[{"Id":"DON'T CHANGE!!!","Start_Row":null},{"Id":"File ID","Start_Row":"Start Row"},{"Id":"Pool","Start_Row":1}]}],"logo":null,"images":["stunt toy car.jpg"],"errors":[]}
I tried your code with your data and it is -- as expected -- iterating over all 27 items. But because some condition isn't met, you are setting innError = true for the 22nd record and never setting it back to false.
So all items after that will be treated like they have an error too (which probably makes you believe they are not iterated). Just try adding a console.log(j) as first statement in your inner loop. And you will see, it will printout all values from 0 to 26 ...
I have a cron "time definition"
1 * * * * (every hour at xx:01)
2 5 * * * (every day at 05:02)
0 4 3 * * (every third day of the month at 04:00)
* 2 * * 5 (every minute between 02:00 and 02:59 on fridays)
And I have an unix timestamp.
Is there an obvious way to find (calculate) the next time (after that given timestamp) the job is due to be executed?
I'm using PHP, but the problem should be fairly language-agnostic.
[Update]
The class "PHP Cron Parser" (suggested by Ray) calculates the LAST time the CRON job was supposed to be executed, not the next time.
To make it easier: In my case the cron time parameters are only absolute, single numbers or "*". There are no time-ranges and no "*/5" intervals.
Here's a PHP project that is based on dlamblin's psuedo code.
It can calculate the next run date of a CRON expression, the previous run date of a CRON expression, and determine if a CRON expression matches a given time. You can skip This CRON expression parser fully implements CRON:
Increments of ranges (e.g. */12, 3-59/15)
Intervals (e.g. 1-4, MON-FRI, JAN-MAR )
Lists (e.g. 1,2,3 | JAN,MAR,DEC)
Last day of a month (e.g. L)
Last given weekday of a month (e.g. 5L)
Nth given weekday of a month (e.g. 3#2, 1#1, MON#4)
Closest weekday to a given day of the month (e.g. 15W, 1W, 30W)
https://github.com/mtdowling/cron-expression
Usage (PHP 5.3+):
<?php
// Works with predefined scheduling definitions
$cron = Cron\CronExpression::factory('#daily');
$cron->isDue();
$cron->getNextRunDate();
$cron->getPreviousRunDate();
// Works with complex expressions
$cron = Cron\CronExpression::factory('15 2,6-12 */15 1 2-5');
$cron->getNextRunDate();
This is basically doing the reverse of checking if the current time fits the conditions. so something like:
//Totaly made up language
next = getTimeNow();
next.addMinutes(1) //so that next is never now
done = false;
while (!done) {
if (cron.minute != '*' && next.minute != cron.minute) {
if (next.minute > cron.minute) {
next.addHours(1);
}
next.minute = cron.minute;
}
if (cron.hour != '*' && next.hour != cron.hour) {
if (next.hour > cron.hour) {
next.hour = cron.hour;
next.addDays(1);
next.minute = 0;
continue;
}
next.hour = cron.hour;
next.minute = 0;
continue;
}
if (cron.weekday != '*' && next.weekday != cron.weekday) {
deltaDays = cron.weekday - next.weekday //assume weekday is 0=sun, 1 ... 6=sat
if (deltaDays < 0) { deltaDays+=7; }
next.addDays(deltaDays);
next.hour = 0;
next.minute = 0;
continue;
}
if (cron.day != '*' && next.day != cron.day) {
if (next.day > cron.day || !next.month.hasDay(cron.day)) {
next.addMonths(1);
next.day = 1; //assume days 1..31
next.hour = 0;
next.minute = 0;
continue;
}
next.day = cron.day
next.hour = 0;
next.minute = 0;
continue;
}
if (cron.month != '*' && next.month != cron.month) {
if (next.month > cron.month) {
next.addMonths(12-next.month+cron.month)
next.day = 1; //assume days 1..31
next.hour = 0;
next.minute = 0;
continue;
}
next.month = cron.month;
next.day = 1;
next.hour = 0;
next.minute = 0;
continue;
}
done = true;
}
I might have written that a bit backwards. Also it can be a lot shorter if in every main if instead of doing the greater than check you merely increment the current time grade by one and set the lesser time grades to 0 then continue; however then you'll be looping a lot more. Like so:
//Shorter more loopy version
next = getTimeNow().addMinutes(1);
while (true) {
if (cron.month != '*' && next.month != cron.month) {
next.addMonths(1);
next.day = 1;
next.hour = 0;
next.minute = 0;
continue;
}
if (cron.day != '*' && next.day != cron.day) {
next.addDays(1);
next.hour = 0;
next.minute = 0;
continue;
}
if (cron.weekday != '*' && next.weekday != cron.weekday) {
next.addDays(1);
next.hour = 0;
next.minute = 0;
continue;
}
if (cron.hour != '*' && next.hour != cron.hour) {
next.addHours(1);
next.minute = 0;
continue;
}
if (cron.minute != '*' && next.minute != cron.minute) {
next.addMinutes(1);
continue;
}
break;
}
For anyone interested, here's my final PHP implementation, which pretty much equals dlamblin pseudo code:
class myMiniDate {
var $myTimestamp;
static private $dateComponent = array(
'second' => 's',
'minute' => 'i',
'hour' => 'G',
'day' => 'j',
'month' => 'n',
'year' => 'Y',
'dow' => 'w',
'timestamp' => 'U'
);
static private $weekday = array(
1 => 'monday',
2 => 'tuesday',
3 => 'wednesday',
4 => 'thursday',
5 => 'friday',
6 => 'saturday',
0 => 'sunday'
);
function __construct($ts = NULL) { $this->myTimestamp = is_null($ts)?time():$ts; }
function __set($var, $value) {
list($c['second'], $c['minute'], $c['hour'], $c['day'], $c['month'], $c['year'], $c['dow']) = explode(' ', date('s i G j n Y w', $this->myTimestamp));
switch ($var) {
case 'dow':
$this->myTimestamp = strtotime(self::$weekday[$value], $this->myTimestamp);
break;
case 'timestamp':
$this->myTimestamp = $value;
break;
default:
$c[$var] = $value;
$this->myTimestamp = mktime($c['hour'], $c['minute'], $c['second'], $c['month'], $c['day'], $c['year']);
}
}
function __get($var) {
return date(self::$dateComponent[$var], $this->myTimestamp);
}
function modify($how) { return $this->myTimestamp = strtotime($how, $this->myTimestamp); }
}
$cron = new myMiniDate(time() + 60);
$cron->second = 0;
$done = 0;
echo date('Y-m-d H:i:s') . '<hr>' . date('Y-m-d H:i:s', $cron->timestamp) . '<hr>';
$Job = array(
'Minute' => 5,
'Hour' => 3,
'Day' => 13,
'Month' => null,
'DOW' => 5,
);
while ($done < 100) {
if (!is_null($Job['Minute']) && ($cron->minute != $Job['Minute'])) {
if ($cron->minute > $Job['Minute']) {
$cron->modify('+1 hour');
}
$cron->minute = $Job['Minute'];
}
if (!is_null($Job['Hour']) && ($cron->hour != $Job['Hour'])) {
if ($cron->hour > $Job['Hour']) {
$cron->modify('+1 day');
}
$cron->hour = $Job['Hour'];
$cron->minute = 0;
}
if (!is_null($Job['DOW']) && ($cron->dow != $Job['DOW'])) {
$cron->dow = $Job['DOW'];
$cron->hour = 0;
$cron->minute = 0;
}
if (!is_null($Job['Day']) && ($cron->day != $Job['Day'])) {
if ($cron->day > $Job['Day']) {
$cron->modify('+1 month');
}
$cron->day = $Job['Day'];
$cron->hour = 0;
$cron->minute = 0;
}
if (!is_null($Job['Month']) && ($cron->month != $Job['Month'])) {
if ($cron->month > $Job['Month']) {
$cron->modify('+1 year');
}
$cron->month = $Job['Month'];
$cron->day = 1;
$cron->hour = 0;
$cron->minute = 0;
}
$done = (is_null($Job['Minute']) || $Job['Minute'] == $cron->minute) &&
(is_null($Job['Hour']) || $Job['Hour'] == $cron->hour) &&
(is_null($Job['Day']) || $Job['Day'] == $cron->day) &&
(is_null($Job['Month']) || $Job['Month'] == $cron->month) &&
(is_null($Job['DOW']) || $Job['DOW'] == $cron->dow)?100:($done+1);
}
echo date('Y-m-d H:i:s', $cron->timestamp) . '<hr>';
Use this function:
function parse_crontab($time, $crontab)
{$time=explode(' ', date('i G j n w', strtotime($time)));
$crontab=explode(' ', $crontab);
foreach ($crontab as $k=>&$v)
{$v=explode(',', $v);
foreach ($v as &$v1)
{$v1=preg_replace(array('/^\*$/', '/^\d+$/', '/^(\d+)\-(\d+)$/', '/^\*\/(\d+)$/'),
array('true', '"'.$time[$k].'"==="\0"', '(\1<='.$time[$k].' and '.$time[$k].'<=\2)', $time[$k].'%\1===0'),
$v1
);
}
$v='('.implode(' or ', $v).')';
}
$crontab=implode(' and ', $crontab);
return eval('return '.$crontab.';');
}
var_export(parse_crontab('2011-05-04 02:08:03', '*/2,3-5,9 2 3-5 */2 *'));
var_export(parse_crontab('2011-05-04 02:08:03', '*/8 */2 */4 */5 *'));
Edit Maybe this is more readable:
<?php
function parse_crontab($frequency='* * * * *', $time=false) {
$time = is_string($time) ? strtotime($time) : time();
$time = explode(' ', date('i G j n w', $time));
$crontab = explode(' ', $frequency);
foreach ($crontab as $k => &$v) {
$v = explode(',', $v);
$regexps = array(
'/^\*$/', # every
'/^\d+$/', # digit
'/^(\d+)\-(\d+)$/', # range
'/^\*\/(\d+)$/' # every digit
);
$content = array(
"true", # every
"{$time[$k]} === 0", # digit
"($1 <= {$time[$k]} && {$time[$k]} <= $2)", # range
"{$time[$k]} % $1 === 0" # every digit
);
foreach ($v as &$v1)
$v1 = preg_replace($regexps, $content, $v1);
$v = '('.implode(' || ', $v).')';
}
$crontab = implode(' && ', $crontab);
return eval("return {$crontab};");
}
Usage:
<?php
if (parse_crontab('*/5 2 * * *')) {
// should run cron
} else {
// should not run cron
}
Created javascript API for calculating next run time based on #dlamblin idea. Supports seconds and years. Have not managed to test it fully yet so expect bugs but let me know if find any.
Repository link: https://bitbucket.org/nevity/cronner
Check this out:
It can calculate the next time a scheduled job is supposed to be run based on the given cron definitions.
Thanks for posting this code. It definitely helped me out, even 6 years later.
Trying to implement I found a small bug.
date('i G j n w', $time) returns a 0 padded integer for the minutes.
Later in the code, it does a modulus on that 0 padded integer. PHP doesn't seem to handle this as expected.
$ php
<?php
print 8 % 5 . "\n";
print 08 % 5 . "\n";
?>
3
0
As you can see, 08 % 5 returns 0, whereas 8 % 5 returns the expected 3. I couldn't find a non padded option for the date command. I tried fiddling with the {$time[$k]} % $1 === 0 line (like changing {$time[$k]} to ({$time[$k]}+0), but couldn't get it to drop the 0 padding during the modulus.
So, I ended up just changing the original value returned by the date function and removed the 0 by running $time[0] = $time[0] + 0;.
Here is my test.
<?php
function parse_crontab($frequency='* * * * *', $time=false) {
$time = is_string($time) ? strtotime($time) : time();
$time = explode(' ', date('i G j n w', $time));
$time[0] = $time[0] + 0;
$crontab = explode(' ', $frequency);
foreach ($crontab as $k => &$v) {
$v = explode(',', $v);
$regexps = array(
'/^\*$/', # every
'/^\d+$/', # digit
'/^(\d+)\-(\d+)$/', # range
'/^\*\/(\d+)$/' # every digit
);
$content = array(
"true", # every
"{$time[$k]} === $0", # digit
"($1 <= {$time[$k]} && {$time[$k]} <= $2)", # range
"{$time[$k]} % $1 === 0" # every digit
);
foreach ($v as &$v1)
$v1 = preg_replace($regexps, $content, $v1);
$v = '('.implode(' || ', $v).')';
}
$crontab = implode(' && ', $crontab);
return eval("return {$crontab};");
}
for($i=0; $i<24; $i++) {
for($j=0; $j<60; $j++) {
$date=sprintf("%d:%02d",$i,$j);
if (parse_crontab('*/5 * * * *',$date)) {
print "$date yes\n";
} else {
print "$date no\n";
}
}
}
?>
My answer is not unique. Just a replica of #BlaM answer written in java because PHP's date and time is a bit different from Java.
This program assumes that the CRON expression is simple. It can only contain digits or *.
Minute = 0-60
Hour = 0-23
Day = 1-31
MONTH = 1-12 where 1 = January.
WEEKDAY = 1-7 where 1 = Sunday.
Code:
package main;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CronPredict
{
public static void main(String[] args)
{
String cronExpression = "5 3 27 3 3 ls -la > a.txt";
CronPredict cronPredict = new CronPredict();
String[] parsed = cronPredict.parseCronExpression(cronExpression);
System.out.println(cronPredict.getNextExecution(parsed).getTime().toString());
}
//This method takes a cron string and separates entities like minutes, hours, etc.
public String[] parseCronExpression(String cronExpression)
{
String[] parsedExpression = null;
String cronPattern = "^([0-9]|[1-5][0-9]|\\*)\\s([0-9]|1[0-9]|2[0-3]|\\*)\\s"
+ "([1-9]|[1-2][0-9]|3[0-1]|\\*)\\s([1-9]|1[0-2]|\\*)\\s"
+ "([1-7]|\\*)\\s(.*)$";
Pattern cronRegex = Pattern.compile(cronPattern);
Matcher matcher = cronRegex.matcher(cronExpression);
if(matcher.matches())
{
String minute = matcher.group(1);
String hour = matcher.group(2);
String day = matcher.group(3);
String month = matcher.group(4);
String weekday = matcher.group(5);
String command = matcher.group(6);
parsedExpression = new String[6];
parsedExpression[0] = minute;
parsedExpression[1] = hour;
parsedExpression[2] = day;
//since java's month start's from 0 as opposed to PHP which starts from 1.
parsedExpression[3] = month.equals("*") ? month : (Integer.parseInt(month) - 1) + "";
parsedExpression[4] = weekday;
parsedExpression[5] = command;
}
return parsedExpression;
}
public Calendar getNextExecution(String[] job)
{
Calendar cron = Calendar.getInstance();
cron.add(Calendar.MINUTE, 1);
cron.set(Calendar.MILLISECOND, 0);
cron.set(Calendar.SECOND, 0);
int done = 0;
//Loop because some dates are not valid.
//e.g. March 29 which is a Friday may never come for atleast next 1000 years.
//We do not want to keep looping. Also it protects against invalid dates such as feb 30.
while(done < 100)
{
if(!job[0].equals("*") && cron.get(Calendar.MINUTE) != Integer.parseInt(job[0]))
{
if(cron.get(Calendar.MINUTE) > Integer.parseInt(job[0]))
{
cron.add(Calendar.HOUR_OF_DAY, 1);
}
cron.set(Calendar.MINUTE, Integer.parseInt(job[0]));
}
if(!job[1].equals("*") && cron.get(Calendar.HOUR_OF_DAY) != Integer.parseInt(job[1]))
{
if(cron.get(Calendar.HOUR_OF_DAY) > Integer.parseInt(job[1]))
{
cron.add(Calendar.DAY_OF_MONTH, 1);
}
cron.set(Calendar.HOUR_OF_DAY, Integer.parseInt(job[1]));
cron.set(Calendar.MINUTE, 0);
}
if(!job[4].equals("*") && cron.get(Calendar.DAY_OF_WEEK) != Integer.parseInt(job[4]))
{
Date previousDate = cron.getTime();
cron.set(Calendar.DAY_OF_WEEK, Integer.parseInt(job[4]));
Date newDate = cron.getTime();
if(newDate.before(previousDate))
{
cron.add(Calendar.WEEK_OF_MONTH, 1);
}
cron.set(Calendar.HOUR_OF_DAY, 0);
cron.set(Calendar.MINUTE, 0);
}
if(!job[2].equals("*") && cron.get(Calendar.DAY_OF_MONTH) != Integer.parseInt(job[2]))
{
if(cron.get(Calendar.DAY_OF_MONTH) > Integer.parseInt(job[2]))
{
cron.add(Calendar.MONTH, 1);
}
cron.set(Calendar.DAY_OF_MONTH, Integer.parseInt(job[2]));
cron.set(Calendar.HOUR_OF_DAY, 0);
cron.set(Calendar.MINUTE, 0);
}
if(!job[3].equals("*") && cron.get(Calendar.MONTH) != Integer.parseInt(job[3]))
{
if(cron.get(Calendar.MONTH) > Integer.parseInt(job[3]))
{
cron.add(Calendar.YEAR, 1);
}
cron.set(Calendar.MONTH, Integer.parseInt(job[3]));
cron.set(Calendar.DAY_OF_MONTH, 1);
cron.set(Calendar.HOUR_OF_DAY, 0);
cron.set(Calendar.MINUTE, 0);
}
done = (job[0].equals("*") || cron.get(Calendar.MINUTE) == Integer.parseInt(job[0])) &&
(job[1].equals("*") || cron.get(Calendar.HOUR_OF_DAY) == Integer.parseInt(job[1])) &&
(job[2].equals("*") || cron.get(Calendar.DAY_OF_MONTH) == Integer.parseInt(job[2])) &&
(job[3].equals("*") || cron.get(Calendar.MONTH) == Integer.parseInt(job[3])) &&
(job[4].equals("*") || cron.get(Calendar.DAY_OF_WEEK) == Integer.parseInt(job[4])) ? 100 : (done + 1);
}
return cron;
}
}