Skip to main content
Known Participant
December 27, 2023
해결됨

I wanna order my questions and answers sort to the “key” paragraph style, from smallest to largest

  • December 27, 2023
  • 5 답변들
  • 2205 조회

Hi,
I have a document with hundreds of questions and answers. I want to order my questions and answers sort to the “key” paragraph style, from smallest to largest. There can be any order for those with the same number.

 

I have attached the indesign copy document. Thanks advice. 

이 주제는 답변이 닫혔습니다.
최고의 답변: m1b

Hi @Peter Kahrel, thank you for this, that is a very efficient approach and helped me a lot.

 

With your help I have updated my script. It will not be as fast as yours because it is careful about which paragraphs to move (eg. it won't damage non-question paragraphs in the same story if they occur before or after the questions — see screen shot below), but it will be faster than my first attempt. On my computer here, using your demo document but with 1000 questions, and the "safe" paragraphs before and after, it completed in 15 seconds.

 

@uniq1, if you don't mind trying this, I would like to know if it is now fast enough for real use in your large document. I have an idea for a further optimization if it is still too slow.

- Mark

/**
 * Sort 'questions' by 'key number', where
 * questions are paragraph groupings delimited
 * by the paragraph style "question". The key
 * number is derived from the paragraph with
 * style "key" using a regexp.
 *
 * Usage: select a text frame or put cursor in
 * text of a story and run script.
 *
 * NOTE: cannot currently sort multiple stories.
 *
 * @author m1b - with much help from Peter Kahrel
 * @discussion https://community.adobe.com/t5/indesign-discussions/i-wanna-order-my-questions-and-answers-sort-to-the-key-paragraph-style-from-smallest-to-largest/m-p/14320542
 */

var settings = {

    doc: app.activeDocument,

    // match the key number
    keyGrep: /^Key\s*(\d+)/,

    // key paragraph contains the key number
    keyParagraphsStyleName: 'key',

    // answer is the last paragraph in the set
    answerParagraphStyleName: 'answer',

    // the question paragraph style
    questionParagraphStyle: (
        app.activeDocument.paragraphStyleGroups.itemByName('quiz')
            .paragraphStyles.itemByName('question')
    ),

};

function main() {

    var target = settings.doc.selection[0] || settings.doc;

    if ('function' !== typeof target.findGrep)
        return alert('Please select a text object, or nothing, and try again.')

    if (!settings.questionParagraphStyle.isValid)
        return alert('Error: no valid `key` paragraph style found.');

    if ('TextFrame' === target.constructor.name)
        target = target.parentStory;

    else if (
        target.hasOwnProperty('parent')
        && 'Story' === target.parent.constructor.name
    )
        target = target.parent;

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;
    app.findGrepPreferences.appliedParagraphStyle = settings.questionParagraphStyle;

    // this will be a collection of texts
    var found = target.findGrep();

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;

    var questions = [],
        story,
        question,
        styles,
        start,
        end,
        match;

    foundLoop:
    for (var i = 0; i < found.length; i++) {

        question = {};
        questions.push(question);

        if (undefined != story) {
            if (found[i].parent !== story)
                return alert('Abort: There are muliple stories to sort. Please do them one at a time.');
        }
        else {
            story = found[i].parent;

            if (story.characters[-1].contents != '\r')
                // makes things easier if we have a CR here
                story.insertionPoints[-1].contents = '\r';

        }

        start = found[i].index;
        end = i === found.length - 1 ? story.characters.length - 1 : found[i + 1].index - 1;

        question.characters = story.characters.itemByRange(start, end);

        // use the styles to understand the question set
        styles = question.characters.paragraphs.everyItem().appliedParagraphStyle;

        for (var j = 0; j < styles.length; j++) {

            if (
                settings.keyParagraphsStyleName === styles[j].name
                && (match = question.characters.paragraphs[j].contents[0].match(settings.keyGrep))
                && 2 === match.length
            )
                // add the key number
                question.keyNumber = Number(match[1]);

            else if (
                settings.answerParagraphStyleName == styles[j].name
                && end !== question.characters.paragraphs[j].characters[-1].index[0]
            ) {
                // double-check the question set ends with the answer, and not some other text
                end = question.characters.paragraphs[j].characters[-1].index[0];
                question.characters = story.characters.itemByRange(start, end);
            }

        }

    }

    if (0 === questions.length)
        return alert('No questions found.');

    // we'll use these to remove the original questions
    start = questions[0].characters.characters[0].index[0];
    end = questions[questions.length - 1].characters.characters[-1].index[0];

    // sort the questions by key number
    questions.sort(function (a, b) { return a.keyNumber - b.keyNumber });

    // create temporary frame
    var tempFrame = app.documents[0].textFrames.add();

    for (i = 0; i < questions.length; i++)
        questions[i].characters.duplicate(LocationOptions.AT_END, tempFrame.parentStory);

    // remove the original questions
    story.characters.itemByRange(start, end).remove();

    // clean up
    tempFrame.parentStory.move(LocationOptions.AFTER, story.insertionPoints[start]);
    tempFrame.remove();

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Sort Answers');

 

5 답변

Peter Kahrel
Community Expert
Community Expert
December 29, 2023

Try this one.

Select one of the text frames that your exam story sits in, then run the script.

 

 

(function () {
  var i, par, temp;
  var blocks = [];
  var story = app.selection[0].parentStory;
  
  if (story.characters[-1].contents != '\r') {
    story.insertionPoints[-1].contents = '\r';
  }

  par = story.paragraphs.everyItem().getElements();
  for (i = 0; i < par.length-6; i = i + 7) {
    blocks.push ({
      key: Number(par[i+5].contents.match(/\d+/)[0]),
      text: story.paragraphs.itemByRange(i,i+6)
    });
  }

  blocks.sort (function (a, b) {
    return a.key - b.key;
  });

  temp = app.documents[0].textFrames.add ({
    geometricBounds: [0,0,'100mm','100mm'],
  });

  for (i = 0; i < blocks.length; i++) {
    blocks[i].text.duplicate (LocationOptions.AT_END, temp.parentStory);
  }

  story.contents = '';
  temp.parentStory.move (LocationOptions.AFTER, story.insertionPoints[0]);
  temp.remove();
}());

 

 

m1b
Community Expert
m1bCommunity Expert답변
Community Expert
December 29, 2023

Hi @Peter Kahrel, thank you for this, that is a very efficient approach and helped me a lot.

 

With your help I have updated my script. It will not be as fast as yours because it is careful about which paragraphs to move (eg. it won't damage non-question paragraphs in the same story if they occur before or after the questions — see screen shot below), but it will be faster than my first attempt. On my computer here, using your demo document but with 1000 questions, and the "safe" paragraphs before and after, it completed in 15 seconds.

 

@uniq1, if you don't mind trying this, I would like to know if it is now fast enough for real use in your large document. I have an idea for a further optimization if it is still too slow.

- Mark

/**
 * Sort 'questions' by 'key number', where
 * questions are paragraph groupings delimited
 * by the paragraph style "question". The key
 * number is derived from the paragraph with
 * style "key" using a regexp.
 *
 * Usage: select a text frame or put cursor in
 * text of a story and run script.
 *
 * NOTE: cannot currently sort multiple stories.
 *
 * @author m1b - with much help from Peter Kahrel
 * @discussion https://community.adobe.com/t5/indesign-discussions/i-wanna-order-my-questions-and-answers-sort-to-the-key-paragraph-style-from-smallest-to-largest/m-p/14320542
 */

var settings = {

    doc: app.activeDocument,

    // match the key number
    keyGrep: /^Key\s*(\d+)/,

    // key paragraph contains the key number
    keyParagraphsStyleName: 'key',

    // answer is the last paragraph in the set
    answerParagraphStyleName: 'answer',

    // the question paragraph style
    questionParagraphStyle: (
        app.activeDocument.paragraphStyleGroups.itemByName('quiz')
            .paragraphStyles.itemByName('question')
    ),

};

function main() {

    var target = settings.doc.selection[0] || settings.doc;

    if ('function' !== typeof target.findGrep)
        return alert('Please select a text object, or nothing, and try again.')

    if (!settings.questionParagraphStyle.isValid)
        return alert('Error: no valid `key` paragraph style found.');

    if ('TextFrame' === target.constructor.name)
        target = target.parentStory;

    else if (
        target.hasOwnProperty('parent')
        && 'Story' === target.parent.constructor.name
    )
        target = target.parent;

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;
    app.findGrepPreferences.appliedParagraphStyle = settings.questionParagraphStyle;

    // this will be a collection of texts
    var found = target.findGrep();

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;

    var questions = [],
        story,
        question,
        styles,
        start,
        end,
        match;

    foundLoop:
    for (var i = 0; i < found.length; i++) {

        question = {};
        questions.push(question);

        if (undefined != story) {
            if (found[i].parent !== story)
                return alert('Abort: There are muliple stories to sort. Please do them one at a time.');
        }
        else {
            story = found[i].parent;

            if (story.characters[-1].contents != '\r')
                // makes things easier if we have a CR here
                story.insertionPoints[-1].contents = '\r';

        }

        start = found[i].index;
        end = i === found.length - 1 ? story.characters.length - 1 : found[i + 1].index - 1;

        question.characters = story.characters.itemByRange(start, end);

        // use the styles to understand the question set
        styles = question.characters.paragraphs.everyItem().appliedParagraphStyle;

        for (var j = 0; j < styles.length; j++) {

            if (
                settings.keyParagraphsStyleName === styles[j].name
                && (match = question.characters.paragraphs[j].contents[0].match(settings.keyGrep))
                && 2 === match.length
            )
                // add the key number
                question.keyNumber = Number(match[1]);

            else if (
                settings.answerParagraphStyleName == styles[j].name
                && end !== question.characters.paragraphs[j].characters[-1].index[0]
            ) {
                // double-check the question set ends with the answer, and not some other text
                end = question.characters.paragraphs[j].characters[-1].index[0];
                question.characters = story.characters.itemByRange(start, end);
            }

        }

    }

    if (0 === questions.length)
        return alert('No questions found.');

    // we'll use these to remove the original questions
    start = questions[0].characters.characters[0].index[0];
    end = questions[questions.length - 1].characters.characters[-1].index[0];

    // sort the questions by key number
    questions.sort(function (a, b) { return a.keyNumber - b.keyNumber });

    // create temporary frame
    var tempFrame = app.documents[0].textFrames.add();

    for (i = 0; i < questions.length; i++)
        questions[i].characters.duplicate(LocationOptions.AT_END, tempFrame.parentStory);

    // remove the original questions
    story.characters.itemByRange(start, end).remove();

    // clean up
    tempFrame.parentStory.move(LocationOptions.AFTER, story.insertionPoints[start]);
    tempFrame.remove();

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Sort Answers');

 

uniq1작성자
Known Participant
December 30, 2023

Hi @m1b , 

Definitely this script is very fast. He ordered 400 questions in 15 seconds. Thank you very much. 

 

I found another solution as follows...
I created a temporary "key para style". I solved every question. I used Marc Autret's "smart sort" script. I sorted all the questions. Then I removed a Temporary "key para style".

uniq1작성자
Known Participant
December 29, 2023

Hi everyone,

150 questions take 3 hours. This is quite tiring.

I guess I have to look for another method...

 

Peter Kahrel
Community Expert
Community Expert
December 28, 2023

@m1b 

Mark --

 

It sounds like you are getting much more robust object references than I am!

 

In this script (https://creativepro.com/files/kahrel/indesign/sort.html) I do something similar to what Robert mentioned: after sorting the keys, move the paragrapgraphs to be sorted one by one to a separate text frame, then move them back to the original story. Originally I used a method more or less like yours, in situ sorting, and that often caused problems. Moving them elsewhere (end of story, like Robert does, other frame, like I do) solves all those problems and is a lot quicker to boot.

m1b
Community Expert
Community Expert
December 28, 2023

Ah I see! That makes perfect sense. That will help me next time. Thanks Peter. (And Robert!)

uniq1작성자
Known Participant
December 27, 2023

Hi @m1b,

Worked perfectly Mark. Thank you very much.

I will have to work hard to understand the logic of this script.

m1b
Community Expert
Community Expert
December 27, 2023

Great to hear!

 

The logic is this:

1. find the questions paragraphs.

2. collect and store all the paragraphs from the question onwards, and stop when we reach another question.

3. one of those will be the key number—collect that and store it.

4. sort the questions based on the key number.

5. duplicate all the collected questions to the end of the story (in the new order)

6. remove the original questions (in the wrong order)

 

I am sorry there are some... strange... bits in the script. I had to do a lot of resolve(  .toSpecifier()) because when we are moving things around in a document, we have to be careful that the references are still pointing to what we expect them to be. You can pretty much ignore those as they are a technique requirement, nothing to do with the logic I mentioned above. Actually, now that I've thought about it, it will be cleaner if, I put those in a little helper function at the end. See updated script if you like.

- Mark

Robert at ID-Tasker
Legend
December 27, 2023

@m1b - great piece of code - as always.

 

But if I may - why do you have to remove sorted "pieces of text" during sorting - why not duplicate them at the end - and then remove the whole original block as the last / cleanup step?

 

m1b
Community Expert
Community Expert
December 27, 2023

Hi @uniq1, here's my attempt. Let me know if it works for you.

- Mark

 

 

/**
 * Sort 'questions' by 'key number', where
 * questions are paragraph groupings delimited
 * by the paragraph style "question". The key
 * number is derived from the paragraph with
 * style "key" using a regexp.
 *
 * Usage: select a text frame or put cursor in
 * text of a story and run script.
 *
 * NOTE: cannot currently sort multiple stories.
 *
 * @author m1b
 * @discussion https://community.adobe.com/t5/indesign-discussions/i-wanna-order-my-questions-and-answers-sort-to-the-key-paragraph-style-from-smallest-to-largest/m-p/14320542
 */

var settings = {

    doc: app.activeDocument,

    // match the key number
    keyGrep: /^Key\s*(\d+)/,

    // the key paragraph style name
    keyParagraphStyleName: 'key',

    // the question paragraph style
    questionParagraphStyle: (
        app.activeDocument.paragraphStyleGroups.itemByName('quiz')
            .paragraphStyles.itemByName('question')
    ),

};

function main() {

    var target = settings.doc.selection[0] || settings.doc;

    if ('function' !== typeof target.findGrep)
        return alert('Please select a text object, or nothing, and try again.')

    if (!settings.questionParagraphStyle.isValid)
        return alert('Error: no valid `key` paragraph style found.');

    if ('TextFrame' === target.constructor.name)
        target = target.parentStory;

    else if (
        target.hasOwnProperty('parent')
        && 'Story' === target.parent.constructor.name
    )
        target = target.parent;

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;
    app.findGrepPreferences.appliedParagraphStyle = settings.questionParagraphStyle;

    // this will be a collection of texts
    var found = target.findGrep();

    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;

    var questions = [],
        story,
        paras,
        para,
        match;

    foundLoop:
    for (var i = 0; i < found.length; i++) {

        if (undefined != story) {
            if (found[i].parent !== story)
                return alert('Abort: There are muliple stories to sort. Please do them one at a time.');
        }
        else {
            story = found[i].parent;
        }

        paras = [];
        para = found[i].paragraphs[0];
        questions.push(paras);

        // the first para is the question
        paras.push(update(para));

        while (para.isValid) {

            // get the next paragraph
            para = found[i].parent.paragraphs.nextItem(para);

            if (!para.isValid)
                // probably end of text
                break;

            if (settings.questionParagraphStyle === para.appliedParagraphStyle)
                // this is the next question!
                continue foundLoop;

            if (para.contents.slice(-1) != '\r')
                // makes things easier if we have a CR here
                para.insertionPoints[-1].contents = '\r';

            // add the paragraph
            paras.push(update(para));

            if (
                settings.keyParagraphStyleName === para.appliedParagraphStyle.name
                && (match = para.contents.match(settings.keyGrep))
                && 2 === match.length
            )
                // this is the key paragraph
                paras.keyNumber = Number(match[1]);

        }

    }

    if (0 === questions.length)
        return alert('No questions found.');

    // get the last insertion point of the last para of the last question
    var afterPoint = update(questions[questions.length - 1][questions[questions.length - 1].length - 1].insertionPoints[-1]);

    // sort the questions by key number
    questions.sort(function (a, b) { return a.keyNumber - b.keyNumber });

    // duplicate paragraphs in sorted order *after* the last found insertion point
    for (var i = 0; i < questions.length; i++)
        for (var j = 0; j < questions[i].length; j++)
            update(questions[i][j]).duplicate(LocationOptions.AFTER, afterPoint);

    // remove the unsorted paras
    afterPoint.parent.characters.itemByRange(afterPoint.parent.insertionPoints[0], afterPoint).remove()

    // helper function to clean up the code a bit
    function update(obj) { return resolve(obj.toSpecifier()) };

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Sort Answers');

Edit 2023-12-27: minor change to put the resolve/toSpecifier calls into a little helper function. Trying to make code more readable for people.

 

uniq1작성자
Known Participant
December 28, 2023

Hi @m1b 

It works well, but its only drawback is that it can list 150 questions in 2 hours.