Copy link to clipboard
Copied
Dear forum,
I have a long table which is threaded among several text frames (pages). I want to get the reference to the first body row in each frame so that to check if it contains a product name. (I can do this by checking if all the cells in the row have been merged.)
Since a table looks like a single character for script, I don’t see a straightforward way to achieve my goal.
So far, I solved it in a sloppy way: I check the baseline of the 1st insertion point of the 1st cell in the row.
else if (RoundString(row.cells[0].insertionPoints[0].baseline, 1) == firstRowBaseline && mainRow != null) {
Is there a more elegant solution?
Thank you in advance!
Regards,
Kasyan
1 Correct answer
Copy link to clipboard
Copied
Copy link to clipboard
Copied
Hi Trevor,
Thank you very much for pointing me in the right direction!
I used the approach you used in your second script.
Here's the code in case someone is interested:
main();
function main() {
var doc = app.activeDocument,
table = doc.stories[1].tables[0];
tableTest(table);
}
function tableTest(table) {
var textFrame, previousTextFrame, currentTextFrame,
rows = table.rows;
previousTextFrame = rows[0].cells[0].insertionPoints[0].parentTextFrames[0];
for (var i = 0; i < rows.length; i++) {
row = rows;
if (row.rowType != RowTypes.BODY_ROW) continue; // skip headers & footers
currentTextFrame = row.cells[0].insertionPoints[0].parentTextFrames[0];
// the first body row in the first frame, or text frame has changed
if ((i - table.headerRowCount == 0 && currentTextFrame == previousTextFrame) || currentTextFrame != previousTextFrame) {
row.fillColor = "Yellow";
}
else {
row.fillColor = "Magenta";
}
previousTextFrame = currentTextFrame;
}
}
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
also prepare for the case where a row has no cells at all.
So test for:
row.cells.length
Could be that the value of length is: 0
How could that happen?
1. Copy a range R of rows from table A
2. Paste the range to a new text frame => that will become table B
3. Add a column at the right edge of table B
4. Merge the cells in table B columnwise, but not the new added column
5. Select and copy the merged cells of table B only
6. Select the first cell in R of table A
7. Paste the copied cells
Now table A has rows where the length of cells is zero.
Here a screenshot from a sample where I set the contents of each cell to its name:
The Table panel is showing 10 rows and is obviously following: table.rows.length
The Story Editor panel is showing 7 rows. That's the ones with cells that can be edited.
If you run a snippet like that on the table where I loop through all the rows of the table:
// Select textFrame and run this script:
var rows = app.selection[0].tables[0].rows.everyItem().getElements();
var rowsLength = rows.length;
for(var n=0;n<rowsLength;n++)
{
$.writeln("row"+"\t"+n+"\t"+"cells.length:"+"\t"+rows
.cells.length); };
The result of cells.length is that:
/*
row 0 cells.length: 6
row 1 cells.length: 6
row 2 cells.length: 0
row 3 cells.length: 0
row 4 cells.length: 0
row 5 cells.length: 6
row 6 cells.length: 6
row 7 cells.length: 6
row 8 cells.length: 6
row 9 cells.length: 6
*/
Regards,
Uwe
Copy link to clipboard
Copied
If I'm looking for the rowType of every row of such a table:
// Select textFrame and run this script:
var rows = app.selection[0].tables[0].rows.everyItem().getElements();
var rowsLength = rows.length;
for(var n=0;n<rowsLength;n++)
{
// Write the rowType of every row to the JavaScript Console of the ESTK:
$.writeln("row-index"+"\t"+n+"\t"+"rows
.rowType.toString():"+"\t"+rows .rowType.toString() ); };
The result is this:
/*
row-index 0 rows
row-index 1 rows
row-index 2 rows
row-index 3 rows
row-index 4 rows
row-index 5 rows
row-index 6 rows
row-index 7 rows
row-index 8 rows
row-index 9 rows
*/
Regards,
Uwe
Copy link to clipboard
Copied
Here the thing in detail:
1. Copy some rows (yellow range) to the clipboard
2. Paste to the page (or to an empty text frame) and add a column to the new table
3. Merge the yellow cells columnwise.
4. Select the merged cells and copy
5. Select the first cell of the range in the old table
6. Paste the copied merged cells:
7. Run a script to get the actual names of each cell of the table as contents:Result:
Bottom line of this exercise:
If you get customer's documents and running scripts on tables it could well be that some rows of a table contain no cells at all.
Regards,
Uwe
Copy link to clipboard
Copied
And the same can be said for columns.
It is possible, that some columns of a table contain no cells at all.
Here an example where columns[2].cells.length is 0 :
( 5 columns are visible, still the Tables panel is saying that 6 columns are in the table. )
Regards,
Uwe
Copy link to clipboard
Copied
Hi Uwe,
Thank you for your reply! I didn't know that such situation is possible.
However, I'm writing a script testing it in a document provided by my client and it doesn't happen to me. The main purpose of the script is to duplicate the 'product name' at the top of each page and apply alternative fills using the product's color (tinted, say, by 12%). If some rows are added/removed, the script updates them. It's a sort of the 2-nd level of header rows.
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
to catch the first cells of every row that contains any cells can be done reliably with:
var firstCells = table.rows.everyItem().cells[0].getElements();
You now could loop the firstCells array to look after firstCells
Regards,
Uwe
Copy link to clipboard
Copied
Even an empty cell can be overset, e.g. if its height is fixed and the point size of the first insertion point is too high so that the insertion point cannot be shown in the cell:
Note: InDesign's preflight will not recognize this problem.
The Story Editor window does.
Regards,
Uwe
Copy link to clipboard
Copied
Hi Uwe,
Thank you for your tips. I just started working on the script so haven't got to dealing with overset cells and tables.
I tried using
var rows = table.rows.everyItem().getElements();
instead of
var rows = table.rows;
thinking it would make things run faster, but it resulted in a total mess. I guess 'static array' doesn't work in this case (unlike 'alive collection')
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
I thought something like this could work:
var doc = app.documents[0];
var table = doc.stories[0].tables[0];
var color = doc.colors.itemByName("Yellow");
var firstCellsArray = table.rows.everyItem().cells[0].getElements();
var firstCellsArrayLength = firstCellsArray.length;
/*
Add some code here to get the first row after your first header rows.
*/
for(var n=1;n<firstCellsArrayLength;n++)
{
// In case a footer row is also there:
if(firstCellsArray
.rowType == RowTypes.FOOTER_ROW){continue};
var textFrameIDbefore = firstCellsArray[n-1].insertionPoints[0].parentTextFrames[0].id;
var currentTextFrameID = firstCellsArray
.insertionPoints[0].parentTextFrames[0].id;
if(currentTextFrameID != textFrameIDbefore)
{
var rowAfterHeader = firstCellsArray
.parentRow; doSomethingWithRow(rowAfterHeader);
}
};
function doSomethingWithRow(tableRow)
{
tableRow.fillColor = color ;
};
I tested with a table with 2 header rows and 1 footer row with CS6 on OSX.
Also with a table with 2 header rows and no footer row.
Regards,
Uwe
Copy link to clipboard
Copied
Here a variant of the above that is looking for first rows in text frames, that contain only one cell.
That should fit your criteria on merged cells that I can see in your screenshot of the table.
Important note: Do not test with your original table first, because:
1. The first table of the first story in your document might not be your target table.
2. The snippet adds new contents to a qualifying row.
3. Will color that row "Yellow"
However, I implemented a global undo.
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
app.doScript
(
checkFirstTableRowInTextFrame,
ScriptLanguage.JAVASCRIPT,
[],
UndoModes.ENTIRE_SCRIPT,
"Check first table row in every text frame that has one cell only."
);
function checkFirstTableRowInTextFrame()
{
if(app.documents.length == 0){return};
if(app.documents[0].stories.length == 0){return};
if(app.documents[0].stories[0].tables.length == 0){return};
var doc = app.documents[0];
//Adapt the target to your needs:
var table = doc.stories[0].tables[0];
var color = doc.colors.itemByName("Yellow");
var firstCellsArray = table.rows.everyItem().cells[0].getElements();
var firstCellsArrayLength = firstCellsArray.length;
for(var n=1;n<firstCellsArrayLength;n++)
{
if(firstCellsArray
.rowType == RowTypes.FOOTER_ROW){continue};
var textFrameIDbefore = firstCellsArray[n-1].insertionPoints[0].parentTextFrames[0].id;
var currentTextFrameID = firstCellsArray
.insertionPoints[0].parentTextFrames[0].id;
if(currentTextFrameID != textFrameIDbefore)
{
var rowAfterHeader = firstCellsArray
.parentRow; doSomethingWithRow( rowAfterHeader , color );
}
};
function doSomethingWithRow(tableRow , color )
{
var cellLength = tableRow.cells.everyItem().getElements().length;
$.writeln(cellLength);
if(cellLength == 1)
{
tableRow.contents = ["Header"];
tableRow.fillColor = color;
}
};
};
Tested on several variants of a table, that is running through several text frames and some (not all) of its first rows are merged to one cell.
Variants were: 0 footer rows, 1 footer row, 2 footer rows, 2 header rows.
Regards,
Uwe
Copy link to clipboard
Copied
In fact basically we could change the loop a bit:
var table = app.selection[0].tables[0];
var colorFirstBodyCell= app.documents[0].colors.itemByName("Yellow");
var firstCellsArray = table.rows.everyItem().cells[0].getElements();
var firstCellsArrayLength = firstCellsArray.length;
var numberOfHeaderRows = table.headerRowCount;
var numberOfFooterRows = table.footerRowCount;
for(var n=numberOfHeaderRows;n<firstCellsArrayLength-numberOfFooterRows;n++)
{
var textFrameIDbefore = firstCellsArray[n-1].insertionPoints[0].parentTextFrames[0].id;
var currentTextFrameID = firstCellsArray
.insertionPoints[0].parentTextFrames[0].id;
if(currentTextFrameID != textFrameIDbefore)
{
var rowAfterHeader = firstCellsArray
.parentRow; rowAfterHeader.fillColor = colorFirstBodyCell;
}
};
Because of the following situation that makes the role of header and footer rows in the index of rows a bit more clear.
The contents of all first cells are their names that reflect the position of indexOfColumn : indexOfRow in the table.
Below the table is the list of rowTypes in order of the index of the rows:
Regards,
Uwe
Copy link to clipboard
Copied
Hi Uwe,
You tips are very interesting to me and I'd like to post them on my site if you don't mind.
From the outset I must be wasn't clear enough about what I'm trying to achieve and where I've got so far.
Here I posted the before & after test files (CC 2017) and the current version of the script.
At start I have this.
A long table -- catalog -- with two header rows (gray) and one footer row (empty).
The task is to duplicate the product name (e.g. Suzuki on the screenshot) at the top on the next page, and apply alternative fills using the product's color tinted by XX%, like so:
However, the table can be edited: more models (rows) added or removed so the script should update it. For example, I added 5 rows so the 'Suzuki' row on the right page moved downwards:
I run the script again and it fixed the problem
By the way, at first I used
app.doScript(PreCheck, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "\"" + scriptName + "\" Script");
to trigger the script so the user could Undo-Redo it, but it ruined the reference to the table on the 2nd run, for some reason, so I uncommented it.
Here's the whole script:
var scriptName = "Test tables",
doc, table;
PreCheck();
//~ app.doScript(PreCheck, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "\"" + scriptName + "\" Script");
//===================================== FUNCTIONS ======================================
function Main() {
var row, newRow, textFrame, previousTextFrame, currentTextFrame;
var startTime = new Date();
var mainRow = null,
previousMainRowContents = null,
fillTint = 50,
countRowsInRange = 0;
var whiteSwatch = MakeColor("Blanc", ColorSpace.CMYK, ColorModel.process, [0, 0, 0, 0]);
var rows = table.rows;
previousTextFrame = rows[0].cells[0].insertionPoints[0].parentTextFrames[0];
for (var i = 0; i < rows.length; i++) {
row = rows;
if (row.rowType != RowTypes.BODY_ROW) {
continue; // skip headers & footers
}
currentTextFrame = row.cells[0].insertionPoints[0].parentTextFrames[0];
if (row.cells.length == 1) { // main row
mainRow = row;
countRowsInRange = 0; // reset alternative fills counter
if (previousMainRowContents == null) {
previousMainRowContents = mainRow.contents;
}
}
else if (currentTextFrame != previousTextFrame) {
newRow = rows.add(LocationOptions.BEFORE, row);
countRowsInRange = 0; // reset alternative fills counter
newRow.cells[0].merge(newRow);
newRow.cells[0].label = "duplicated";
newRow.cells[0].appliedCellStyle = mainRow.cells[0].appliedCellStyle;
newRow.cells[0].fillColor = mainRow.cells[0].fillColor;
newRow.cells[0].fillTint = mainRow.cells[0].fillTint;
mainRow.cells[0].texts[0].duplicate(LocationOptions.AT_BEGINNING, newRow.cells[0]);
}
else if (countRowsInRange % 2 == 0) {
row.fillColor = mainRow.fillColor;
row.fillTint = fillTint;
}
else {
row.fillColor = whiteSwatch;
}
previousTextFrame = currentTextFrame;
countRowsInRange++;
}
var endTime = new Date();
var duration = GetDuration(startTime, endTime);
//alert("Finished.", scriptName, false);
$.writeln("DONE: time elapsed: " + duration);
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function CleanUp() {
var row,
rows = table.rows;
for (var i = rows.length - 1; i >= 0; i--) {
row = rows;
if (row.rowType != RowTypes.BODY_ROW) continue; // skip headers & footers
if (row.cells.length == 1 && row.cells[0].label == "duplicated") { // main row
try {
row.remove();
}
catch(err) {
$.writeln(err.message + ", line: " + err.line);
}
}
else if (row.cells.length > 1) {
row.fillColor = doc.swatches.itemByName("None");
}
}
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function FindTable(obj) {
if (app.selection[0].constructor.name == "TextFrame" && app.selection[0].tables.length == 1) { // a text frame is selected
obj = app.selection[0].tables[0];
}
else {
while (obj.constructor.name != "Table") {
obj = obj.parent;
if (obj.constructor.name == "Application") {
ErrorExit("Can't get the table.", true);
}
}
}
return obj;
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function PreCheck() {
if (app.documents.length == 0) ErrorExit("Please open a document and try again.", true);
doc = app.activeDocument;
if (doc.converted) ErrorExit("The current document has been modified by being converted from older version of InDesign. Please save the document and try again.", true);
if (!doc.saved) ErrorExit("The current document has not been saved since it was created. Please save the document and try again.", true);
if (app.selection.length == 0 || app.selection.length > 1) ErrorExit("One text frame containing the table, or something in the table should be selected, or the cursor should be inserted into the table.", true);
table = FindTable(app.selection[0]);
if (table.constructor.name != "Table") ErrorExit("Can't get the table.", true);
CleanUp();
Main();
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function GetDuration(startTime, endTime) {
var str;
var duration = (endTime - startTime)/1000;
duration = Math.round(duration);
if (duration >= 60) {
var minutes = Math.floor(duration/60);
var seconds = duration - (minutes * 60);
str = minutes + ((minutes != 1) ? " minutes, " : " minute, ") + seconds + ((seconds != 1) ? " seconds" : " second");
if (minutes >= 60) {
var hours = Math.floor(minutes/60);
minutes = minutes - (hours * 60);
str = hours + ((hours != 1) ? " hours, " : " hour, ") + minutes + ((minutes != 1) ? " minutes, " : " minute, ") + seconds + ((seconds != 1) ? " seconds" : " second");
}
}
else {
str = duration + ((duration != 1) ? " seconds" : " second");
}
return str;
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function ErrorExit(error, icon) {
alert(error, scriptName, icon);
exit();
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
function MakeColor(colorName, colorSpace, colorModel, colorValue) {
var color = doc.colors.item(colorName);
if (!color.isValid) {
color = doc.colors.add({name: colorName, space: colorSpace, model: colorModel, colorValue: colorValue});
}
return color;
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
go ahead and publish my tips.
Now it's clear to me what you like to achieve.
Glad, it's working now…
Hm. Did not test your code yet. So I'm not sure why the doScript() approach does not work in this case.
Maybe you should wrap all your code into one function and call that function with doScript()?
I think you could make your code a bit faster by using the approach with getElements(). Less access to actual unresolved DOM objects after writing all relevant first cells of all rows to an array.
You also could avoid the test for rowType if you change the for loop a bit. Now that we know that index numbers for header rows are always at the start of the index of rows (that's obvious) and the index numbers of footer rows are always at the end of the index of rows (not so obvious; see my answer 13).
Regards,
Uwe
Copy link to clipboard
Copied
Hi Uwe,
Hm. Did not test your code yet. So I'm not sure why the doScript() approach does not work in this case.
Maybe you should wrap all your code into one function and call that function with doScript()?
Wrapping the whole code into one function or calling a chain of functions work exactly in the same way with doScript() method. I tested this before and re-tested it now. The latter is more convenient for me from the standpoint of readability.
Here's a brief description of what goes wrong:
- I select the table: e.g. by selecting a text frame
- Run the script for the first time -- everything goes as expected
- Then I Undo the whole script (Ctrl + Z). The frame is still selected.
- Finally I run the script for the 2nd time and it throws the 'Object is invalid' error while trying to get reference to the table's rows
In 'Call stack' I switch to the PreCheck function where the table variable is defined -- it's invalid.
In the FindTable function the obj variable is also invalid.
If I close and reopen the document, the above-mentioned variables become valid again. I don't know why this happens: haven't dug into this issue too deep so far.
I think you could make your code a bit faster by using the approach with getElements(). Less access to actual unresolved DOM objects after writing all relevant first cells of all rows to an array.
I tried this approach thinking it would make the script run faster, but it didn't work for me.
Namely I changed a couple of lines:
var rows = table.rows;
to
var rows = table.rows.everyItem().getElements();
and
newRow = rows.add(LocationOptions.BEFORE, row);
to
newRow = table.rows.add(LocationOptions.BEFORE, row);
which resulted in a mess
As I already said in my previous post, I think this happened because I used static array instead of live collection. I have to use the latter because the script is constantly adding new rows during execution and the rows collection is changing for that reason.
You also could avoid the test for rowType if you change the for loop a bit. Now that we know that index numbers for header rows are always at the start of the index of rows (that's obvious) and the index numbers of footer rows are always at the end of the index of rows (not so obvious; see my answer 13).
I understand your idea. But do you think it would make the script to run faster?
Timing in ESTK shows me it doesn't take long to check if the row isn't body row. A few nano-secs doesn't matter, I think; anyway, it is not a bottleneck in the script.
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
a brief comment on the getElements() approach.
The first time we gather all rows in one array by using getElements()—you call it static and it is static in the sense that it captures the rows at a given time—we have to work with that array from back to forth if we want to add rows, because the index of rows will then be effected downstream only, so to say.
In my examples I did not add (or remove) rows, just colored some rows and changed the contents of some cells. So I did not run into trouble looping forward. Maybe getElements() is not applicable to your script structure here, but generally I think it should be possible to work with getElements() when adding rows. But that will mean a total rewrite of your script.
Your other problem with the undo:
Currently I have no other idea than to:
1. Save the document before running the script with as Save As and a version number.
2. Running the script.
3. Save the document with a version number.
Regards,
Uwe
Copy link to clipboard
Copied
Hi Uwe,
In practice, using .everyItem().getElements() is not always faster than using collections.
I tested the script against the Test-Before.indd (the link to both is in a previous post) on my home PC a few times and it takes to complete:
4 secs with dynamic collection
5 secs with static array
Adding the 'Undo-Redo' feature is not a client's requirement; it's just a habit of mine. In this particular case, it didn't work for some reason. For pure academic interest, I'd like to know why.
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan,
Try .everyItem().getElements().slice()
Adding the slice can make a significant difference with large collections.
I'm not saying it will. Also it avoids the [too many elements] error.
I just did a search on it and, regarding the performance it could be that it's only with older versions.
See Re: Get the index of the current page?
See also Re: Big performance issue while removing tabs by indents
Worth a go for the extra 8 characters.
Trevor
Copy link to clipboard
Copied
Hi Trevor,
Thanks for the links. I'll look into this.
As far as I remember, the '.everyItem().getElements().slice()' tip originates from Kris Coppieters: he used this trick in the first version of InDesign and said it helped him somehow to avoid some problems but couldn't explain how exactly it worked and which problems it solved; he simply used it out of habit. But again: it relates to an antique version of InDesign; I don't think anybody uses it nowadays.
Anyway, I don't think I can use static array -- .everyItem().getElements() -- in my script because it constantly adds new rows so I have to use a live collection which is dynamically updated. I mentioned this in my previous posts #10 and #16.
Regards,
Kasyan
Copy link to clipboard
Copied
Kasyan Servetsky wrote:
I don't think anybody uses it nowadays.
Definitely wrong about that, I use it.
But doing a quick search on the forum for '.everyItem().getElements().slice' does feature by far mostly me, so I see your point.
I had a script on CS5 that was unworkable without the slice(), I'm pretty sure about that.
I did get a "too many elements" in InDesign CC2017 without using the slice which when I added the slice didn't get but I haven't been able to duplicate this.
Copy link to clipboard
Copied
Hi Trevor,
I made a test using the script and file (with a table of 240 rows) in my post #14 (following your tips here).
To slice, or not to slice? That is the question.
In both cases:
var rows = table.rows.everyItem().getElements().slice(0);
and
var rows = table.rows.everyItem().getElements();
I get exactly the same result: it takes 4-5 secs to complete the script.
Disabling redraw
app.scriptPreferences.enableRedraw = false;
makes the script one sec faster.
doScript
- UndoModes.FAST_ENTIRE_SCRIPT
- UndoModes.ENTIRE_SCRIPT
- without doScript
All the three options produce the same timing
while or for loop
In the above mentioned post you also suggested to use
l = paras.length;
while (l--)
{
p = paras
; ......
}
paras = null;
instead of
p = paras.pop()
Do you think
var myarray = [],
i = myarray.length;
while (i--) {
// do something with myarray
}
may be faster then
for (var i = myArray.length - 1; i >= 0; i--) {
// do something with mAarray
}
I don't know: never tested it. I personally have a habit of using 'for-loop'. I guess (but don't know for sure) they're both equivalent.
So far I'm working with a simple file: prefer to move from the simple to complex as I was taught in an art school. Later I'm going to test it with larger tables and probably I'll get more precise results.
Regards,
Kasyan
Copy link to clipboard
Copied
Hi Kasyan
1) Slice is not going to make any real difference for such an amount of items. The collections I were referring to were in the 1000s
2) pop is a function
The are cases when one would want to use pop.
3) Yes -- is quicker again over 2 times so for your 240 items you might save another 1/2 nanosecond
I find the while format quicker to type, might be just psychological but I'm a bit of a psyco so that's fine with me.
I have a nice script for testing function speeds, I just use an include to test the functions but here a full example.
There's another well know hi-res timer but it's no good as it highly weighs in favor of the 1st function were as my one doesn't, also for various reasons the high res is a waist of time.
Imagine timing how long it takes for a leaf to fall from the top of the Eiffel tower in milliseconds. Pointless, the next leaf is going to be a different size, the wind might be different, one of the spectators might sneeze or otherwise exchange gasses and effect the timing etc.
Anyways
function compare(snippets, /* a function or array of functions to time */
runs, /* [Amount of times to call the functions] */
functionArguments, /* [Amount of loop] */
digits /* [round results to n decimal points] */) {
var l, a = [], f, r, i, c, result = [], results = [], ranking = [], functionNames = [],
testsStarted, testsTook, start, end, t, n, snip,
quickest, slowest, quicker,slower, looser, winner, speed;
var DEFAULT_RUNS = 10; // Avoiding use of const for wider compatibility
var DEFAULT_DIGITS = 3;
var STARS = '\n**********************************************************************\n';
if (!snippets) {
throw "The first argument of the compare function must be a function or an array of functions";
}
if (!(snippets instanceof Array)) {
snippets = [snippets];
}
l = snippets.length;
// if runs is not a positive integer then assign it a default value
runs = ~~runs; // converts runs into an integer
if (runs < 1) {
runs = DEFAULT_RUNS;
}
// if digits is not a positive integer then assign it a default value
if (digits === undefined) {
digits = DEFAULT_DIGITS;
}
digits = ~~digits; // converts digits into an integer
/** If only one function is to be tested then we'll just do a simple timing test */
if (snippets.length === 1) {
n = runs;
t = 0;
while (n--) {
/* See below note on the use of the Date function */
start = new Date();
// call the function
snippets[0](functionArguments);
end = new Date();
t += end - start;
}
// Average out the times
t /= runs;
return 'Average execution time: ' + toDigits(t, digits) + 'ms';
}
/** For multiple funbctions */
// There is sometimes a bias towards the first tested function
// to "help" reduce this we shall randomly shuffle the execution order of the functions
// create an array of the snippet indexes so it can be shuffled
for (f = 0; f < l; f++) {
for (r = 0; r < runs; r++) {
a.push(f);
}
}
function shuffle(arr, preserve) { // This is my (Trevor) implementation of Fisher-Yates shuffle
var a, i, r, v;
// duplicate the array if one wants to preserve the order of the original array
a = (preserve) ? arr.slice() : arr;
i = a.length;
while (i--) {
// generate a random integer for an array index
r = ((1 + i) * Math.random()) | 0;
v = a
; // get the value of that index // swap the values;
a
= a; a = v;
}
return a;
}
shuffle(a);
n = a.length;
// record test start time
testsStarted = new Date();
/** execute the snippets in the random order **/
while (n--) {
i = a
; /* There is an opinion that one should use more accurate time measuring methods than the Date method
While it is certainly true that when the clock is changed in the middle of executing the comparison
the results will be inaccurate however the "noise" and external factors that cause the discrepancy
in the result and even the time it takes to call even an empty function or to loop through a loop
in my opinion render a higher resolution recording pointless */
start = new Date();
// call the function
snippets(functionArguments);
end = new Date();
t = end - start;
// add the execution time to the results,
// if the result for that index is currently undefined set the result to the execution time
results = (results + t) || t;
}
testsTook = new Date() - testsStarted;
// rank the results
for (c = 0; c < l; c++) {
results
/= runs; // average out the results ranking
= {snippet: c, rank: results , name: snippets .name}; }
ranking.sort(function(a, b) {return +(a.rank > b.rank) || -(a.rank !== b.rank)});
// For reporting the ranked function names
for (c = 0; c < l; c++) {
functionNames
= ranking .name }
function toDigits(x, n) {
if (n === undefined) {
return x;
}
return (~~(Math.pow(10, n) * x)) / Math.pow(10, n);
}
quickest = ranking[0].rank;
slowest = ranking[l - 1].rank;
looser = l - 1;
winner = 0;
result.push(['Test Started: ' + testsStarted,
'Functions Tested: (Listed in order of performance) "' + functionNames.join('", "') + '"',
'Arguments: ' + functionArguments,
'Runs: ' + runs,
'Test Took: ' + testsTook + 'ms'].join('\n'));
for (c = 0; c < l; c++) {
snip = ranking
; speed = snip.rank
quicker = slowest / speed;
slower = speed / quickest;
result.push(['Function ' + (snip.snippet + 1) + ') "' + snip.name + '"',
'Average execution time: ' + (speed) + 'ms',
'Rank: ' + (c + 1)+ ' (1 is best)',
(looser === c) ? '** SLOWEST **' : toDigits(quicker, digits) + ' times (' + toDigits(quicker * 100 - 100, digits)+ '%) quicker than slowest function',
(winner === c) ? '** QUICKEST **' : toDigits(slower, digits) + ' times (' + toDigits(100 - 100 / slower, digits)+ '%) slower than quickest function',
].join('\n'));
}
return STARS + result.join(STARS) + STARS;
}
/************************************************************************************************************************************************************
**************************************************************************** Sample use ****************************************************************
************************************************************************************************************************************************************/
function While(n){
var n;
while (n--);
}
function For(n){
for (; n>=0; n--);
}
$.writeln(compare ([ For, While], 10, 1e6, 3));
Sample output
**********************************************************************
Test Started: Wed Jan 11 2017 01:00:27 GMT+0200
Functions Tested: (Listed in order of performance) "While", "For"
Arguments: 1000000
Runs: 10
Test Took: 1158ms
**********************************************************************
Function 2) "While"
Average execution time: 36.6ms
Rank: 1 (1 is best)
2.163 times (116.393%) quicker than slowest function
** QUICKEST **
**********************************************************************
Function 1) "For"
Average execution time: 79.2ms
Rank: 2 (1 is best)
** SLOWEST **
2.163 times (53.787%) slower than quickest function
**********************************************************************
Copy link to clipboard
Copied
Hi Trevor,
Thank you very much for your answers and the script! I wish I had it before.
However, it's not clear to me what the 3rd argument does: functionArguments, /* [Amount of loop] */
In your example you set it to '1e6' (in HEX). Its DEC equivalent is 486 -- according to my Calculator app -- but the script reports
Arguments: 1000000
in the console.
In other words, which value should I use here?
Regards,
Kasyan


-
- 1
- 2