Repeating Section Function Library

The original Function Library showcases some functions and code snippets that are generally useful in sheet workers. This post will do the same for Repeating Sections, specifically.

Each function is listed with an accompanying explanation, explaining what is happening. Then at the end of the post, all the functions are listed together without the explanations, to make it easy to to copy abd past them into the start of your script block. You can then use them in any sheet worker after them. They don’t do anything unless they are called, so you can sdafely include them in any script block.

This is probably the most widely and easily used function. It has been used extensively in posts on this site.

const section_name = (section, id, field) => 
   `${section.startsWith('repeating_') ? section : 
       `repeating_${section}`}_${id}_${field}`;Code language: JavaScript (javascript)

This is a confusing looking function but it is used very easily. You often need to type an attribute name like:

const field = 'repeating_example_-fghw47gh5hw_field'

With this function, you can instead type:

const field = section_name('example', '-fghw47gh5hw', 'field');

That’s not a huge difference. The real advantage comes when you are working with variables. The row id is often variable, so that might be:

const field = section_name('example', id, 'field');

And it gets more potent. You might be filling all the sections with variables, like

const section = 'gear';
getSectionIDs(`repeating_${section}`, ids => {
   const fields = ['weight', 'quantity', 'container'];
   /* loop through each row id than each field and do something with them */
   ids.forEach(id => {
      const weight_properties = {};
      fields.forEach(field => {
          weight_properties[field] = values[section_name(section, id, field);
      });
   });Code language: JavaScript (javascript)

Obviously this is into a full or complete code block. It’s just to show the potential. Think things through for your need.

A repeating name is made of three parts, the repeating section name, row id, and actual attribute (which I call the field). Each is sperated by an underscore, and it starts with repeating_ which also ends with an underscore.

Javascript has a split() function, to split a string into an array. So you can do this:

const full_name = 'repeating_example_-345hgyw7whs_field_name';
const name_parts = full_name.split('_'); Code language: JavaScript (javascript)

This will split it into an array, like this:

name_parts = [
   'repeating', 'example', '-345hgyw7whs', 'field', 'name '
];Code language: JavaScript (javascript)

And so, if you just want the repeating section name or the row id, you can use the split method, like so:

const section_name = name_parts[1];
const row_id = name_parts[2];Code language: JavaScript (javascript)

But you have a problem with the final, field name. If it contains an underscore, as it does here, it’ll be split into more than one part so you can’t get it the same way.

People cleverer than I will point to regex as a way to sole this problem. You can look that up if you like.

Another way to solve this is using destructuring and the rest special variable. Destructuring works on an array, and automatically puts everything variables in order, like this:

const [stub, section, id, ...field] = name_parts.split('_');
const field_name = field.join('_');Code language: JavaScript (javascript)

This will give you variables named stub, section, id, and field. Because a name always starts ‘repeating_’ you need a name for that first entry (‘repeating’) even if you never use it.

The operator takes all remaining elemets of the array and puts them in an array. So in this case, we’ll need to convert it from an array, which is what the join command does.

We can go another route, and set up a function so we never need to think about that. For example:

const section_parts = (full_name, part='section') => {
   let [stub, section, row, ...field] = full_name.split('_');
   if(part === 'section'|| part === 1) return section;
   else if(part === 'row' || part === 'id' || part === 2) return row;
   else return field.join('_');
}
/* 
   with the above at the start of the script block,
   we can extract the parts like this
*/

const section = section_parts(full_name, 1);
const id = section_parts(full_name, 2);
const field = section_parts(full_name, 3);

/* or like this */

const section = section_parts(full_name, 'section');
const id = section_parts(full_name, 'id');
const field = section_parts(full_name, 'field');Code language: JavaScript (javascript)

A function like this makes things a lot simpler.

You can use destructuring with a Javascript Object. You can learn more about it here and many other places.

In the previous Function Library, we saw how to build the event line of a sheet worker very easily. It saved a lot of typing if you had a lot of attributes to watch. But it didn’t handle repeating sections. We’ll expand that here.

The original function:

const build_changes = (stats, sheet_open = false) => 
   stats.reduce((all,stat) => 
   `${all} change:${stat.toLowerCase()}`, sheet_open ? 'sheet:opened' : '');Code language: JavaScript (javascript)

This will work for attributes like this:

let attributes = ['strength', 'dexterity', 'constitution', 'repeating_example'];

But might hit problems if you use attributes like:

let attributes = ['repeating_example:test', 'repeating_example:another_test'];

To be clear, these attributes will work. But you often want to insert that array into getAttrs, and that expects attributes to be named like either of these:

let attributes = ['repeating_example_test', 'repeating_example_another_test'];
let attributes = ['repeating_example_-fger6h7u_test', 'repeating_example_-fger69xa_test'];

The problem is, you cannot use the same array for the event line and for getAttrs – you need two different arrays. That is certainly doable, but it would be nice to use the same array for both.

Section Changes

The method I’d recommend involves creating an object whose format you’ll remember from the Custom Function post. The reason for the similar format will become apparent.

const my_section = {
   example: ['first', 'second', 'third']
};Code language: JavaScript (javascript)

This is an object where you name the object, then put the section at the start of the next line, followed by a colon, then an array containing the attributes inside the repeating section you want to watch.

The advantage of this approach is you can create a globals list (attributes that aren’t in a srepeating section), and can watch multiple sectioons at the same time, like this:

const my_fields = {
   globals: ['strength', 'dexterity', 'constitution'],
   buffs: ['mod', 'cost'],
   gear: ['mod#, 'quantity']
};Code language: JavaScript (javascript)

The names above don’t really matter- it’s just an example of what is possible. Then you need a function to turn the above into a “watch changes” function.

const section_changes = section_object => {
   let output = '';
   Object.keys(section_object).forEach((key, index) => {
     if(index) section += ' ';
     section_object[key].forEach(field => {
       output += key === 'globals' ? field : `repeating_${key}:${field}`;
     });
   });
   return output;
};Code language: JavaScript (javascript)

This is the function to add to your script block. Then you’d use it like this:

const my_fields = {
   globals: ['strength', 'dexterity', 'constitution'],
   buffs: ['mod', 'cost'],
   gear: ['mod', 'quantity']
};
on(section_changes(my_fields), () => {
   getSectionObject(my_fields, data => {
       getAttrs(data.fields, values => {Code language: JavaScript (javascript)

See how the same object is used for both section_changes and getSectionObject.

For the record, the section_changes string would look like this:

'change:strength change:dexterity change:constitution change:repeating_buffs:mod change:repeating_buffs:cost change:repeating_gear:mod change:repeating_gear:cost'

The section_changes function is simpler than writing all that out. And at this point you have a data object that looks like this:

const data = {
   fields: [ / * all attributes, both inside repeatig sections and globals */],
   buffs: [ /* all row IDs for the buffs section */ ],
   gear: [ /* all row IDs for the gear section */]
};Code language: JavaScript (javascript)

You sheet worker can then operate on any stats you need to. You can use the original object to find the fieldnames in any section, and you can identify which attributes in fields aren’t in a repeating section (they don’t stat with ‘repeating_’.

The original Function Library demonstrated how you could sum across a whole sheet worker. You can do that with a repeating section, too.

const sum_section = (section, ids, field, int = false, fallback = 0) => 
   ids.reduce((all, one) => all + (int ? 
      int(section_name(section,id,field), fallback) : 
         num(section_name(section,id,field), fallback)), 0);Code language: JavaScript (javascript)

The above function can be used to all rows of a specific attribute together. That would look like:

const total = sum_section ('gear', data.gear, 'weight');Code language: JavaScript (javascript)

If you want to sum two different fields you would simply add them together like

const total = sum_section ('gear', data.gear, 'weight') + sum_section ('gear', data.gear, 'cost');Code language: JavaScript (javascript)

In practice, you probably want to multiple two or more fields together, like the weight and quanity of items carried. Here’s a better function that covers one or more fields, assuming all fields mentioned on the same row are multiplied together.

const sum_section = (section, ids, fields) => {
    if (!Array.isArray(fields)) fields = [fields.replace(/\s/g, '').split(',')];
    let total = 0;
    const getValue = (section, id, field) => v[`repeating_${section}_${id}_${field}`] === 'on' ? 1 : parseFloat(v[`repeating_${section}_${id}_${field}`]) || 0;
    ids.forEach(id => {
        let subtotal = 1;
        fields.forEach(field => subtotal *= getValue(section, id, field));
        total += subtotal;
    });
    return total;
};Code language: JavaScript (javascript)

This is the same as the basic repeatingSum function, but without needing a destination attribute. You’d use it like this (assuming data.gear contains all the row ids – you could put it inside getSectionIDs and get the id-0array that way):

const total = product_section ('gear', data.gear, ['weight', 'quantity']);Code language: JavaScript (javascript)

This function is amply described in an earlier post. The traditional way to get multiple sections at a time is to nest multiple copies of getSectionIDs inside each other. That is very clunky. Here’s s a function that streamlines it.

const getSectionObject = (sections, callback) => {
    const fields_object = {};
    fields_object.fields = [];
    let keys = Object.keys(sections);
    const burndown = () => {
        let section = keys.shift();
        getSectionIDs(`repeating_${section}`, ids => {
            ids.forEach(id => 
               sections[section].forEach(field => 
                  fields_object.fields.push(`repeating_${section}_${id}_${field}`)));
            fields_object[section] = ids;
            if(keys.length) {
                burndown();
            } else {
                callback(fields_object);
            }
        });
    };
    burndown();
};Code language: JavaScript (javascript)

Insert that function and call it like this:

on('change:repeating_weapons:cost change:repeating_weapons:weight change:repeating_gear:cost change:repeating_gear:weight remove:repeating_weapons remove:repeating_gear', () => {
/* create an object to hold data for all sections */
   const my_sections = {
      weapons: ['weight', 'cost'],
      gear: ['weight', 'cost']
   };
/* then use the function to extract the data obkect */
   getSectionObject(my_sections, data => {
       getAttrs(data.fields, values => {

          /* your working goes here */

       })
   });
});Code language: JavaScript (javascript)

The data obect contains a list of ids for each section, and all the full attributes in a date.fields array.

data = {
   fields = [ /* all the full attribute names */ ],
   weapons: [ /* an array of all the row ids for this section */ ],
   gear: [ /* an array of all the row ids for this section */ ],
}

You can use this function with single sections in place of getSectionIDs. There’s even a version for returning the section data in sorted order.


Combined Code Block

The functions listed here are examples of code. Copy this into your Script Block to use all the functions listed here. Just delete any you don’t need, or use this listing as inspiration for creating your own.

/* combine elements to create a full section attribute name *.
const section_name = (section, id, field) => 
   `${section.startsWith('repeating_') ? section : 
       `repeating_${section}`}_${id}_${field}`;

/* break a full repeating section attribute name into its parts */
const section_parts = (full_name, part='section') => {
   let [stub, section, row, ...field] = full_name.split('_');
   if(part === 'section'|| part === 1) return section;
   else if(part === 'row' || part === 'id' || part === 2) return row;
   else return field.join('_');
}

/* watch changes to build an event changes string */
const section_changes = section_object => {
   let output = '';
   Object.keys(section_object).forEach((key, index) => {
     if(index) section += ' ';
     section_object[key].forEach(field => {
       output += key === 'globals' ? field : `repeating_${key}:${field}`;
     });
   });
   return output;
};

/* add together the sum of multiple columns (can be just one) in section */
const sum_section = (section, ids, fields) => {
    if (!Array.isArray(fields)) fields = [fields.replace(/\s/g, '').split(',')];
    let total = 0;
    const getValue = (section, id, field) => v[`repeating_${section}_${id}_${field}`] === 'on' ? 1 : parseFloat(v[`repeating_${section}_${id}_${field}`]) || 0;
    ids.forEach(id => {
        let subtotal = 1;
        fields.forEach(field => subtotal *= getValue(section, id, field));
        total += subtotal;
    });
    return total;
};

/* operate on multiple repeating sections at a time, getting a data object
   which contains data.fields and data.each_section */
const getSectionObject = (sections, callback) => {
    const fields_object = {};
    fields_object.fields = [];
    let keys = Object.keys(sections);
    const burndown = () => {
        let section = keys.shift();
        getSectionIDs(`repeating_${section}`, ids => {
            ids.forEach(id => 
               sections[section].forEach(field => 
                  fields_object.fields.push(`repeating_${section}_${id}_${field}`)));
            fields_object[section] = ids;
            if(keys.length) {
                burndown();
            } else {
                callback(fields_object);
            }
        });
    };
    burndown();
};Code language: JavaScript (javascript)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.