Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

can a math macro ignore NaN attributes?

I've got a macro that adds a bunch of <input> values on my custom sheet. Let's call them @{item_mass_1} through @{item_mass_27}. Currently I'm doing the brute force method, lol, literally adding @{item_mass_1}+@{item_mass_2}+@{item_mass_3}+@{item_mass_4} ... all the way to @{item_mass_27} It's a lot of typing, but it's all I know how to do. I don't know java script or whatever other programming language you might suggest. The problem is that if a single one of those @{item_mass} values is not a number, the whole thing falls apart. So any blank space, any dash to represent that it is weightless, any "n/a" that a player might enter if it is something that has no mass, etc.... anything a player might put in there besides a number breaks the equation. I've temporarily solved it by giving every @{item_mass} attribute a starting value of zero. It works but it is ugly on the sheet and any player who changes the 0 to anything else that is not a number breaks the equation. I could make the <input> type = "number" but then I get those up and down arrows on each side. That might be my best solution if the answer to my question is "no, macros can't ignore a non-number attribute" I did want to use repeating items for this, but I struggled to make it work and gave up. So FIRST QUESTION - is there a simple-ish way to tell the macro to ignore attributes in the equation that are not numbers? BONUS QUESTION - is there no simpler way with HTML or with the macro to add together a range? (like in Excel/Sheets, you might use a colon to represent a range) Or maybe a very simple piece of code you could suggest that I can toss right into the HTML. GOLD STAR : if you are the super awesome kind of person that just writes a snippet and says "use this, should work.." , which I don't know how to write, the complete concept is that it will multiply @{item_mass_1} by @{item_qty_1}, do that for all 27 items, and total it. (@{item_mass_1}*@{item_qty_1}) + (@{item_mass_2}*@{item_qty_2}) + (@{item_mass_3}*@{item_qty_3}) + ad nauseum. Thanks so much for your help. 
1643562716

Edited 1643562885
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
The best way to handle this is going to be via javascript with repeating sections, but we'll get to that at the end. First Question With your current setup (which I'm assuming uses a disabled input to autocalculate the total), there isn't a way to handle any invalid value in the macro. You can make it handle blanks by putting 0  immediately before the attribute call, but dashes, "n/a", etc will still break it. Bonus Question Unfortunately, HTML just isn't a language that is built to handle iteration or complex actions; it's just a text markup language. Which brings us to: Gold Star As I said, the best way to do this is going to be via Javascript, and we'll even swap this to a repeating section while we're at it. This code is going to be slightly overkill so that writing the actual calculation and future calculations will be easier: <input type='number' name="attr_total_mass" value="0" readonly><!--This input is readonly which means only a sheetworker will be able to change it. Sheetworkers cannot interact with autocalc attributes (aka disabled="true")--> <fieldset class="repeating_item"><!-- Replace the contents of this repeating section with whatever your setup for your items is--> <input type="text" name="attr_name"> <input type="number" name="attr_mass"> <input type="number" name="attr_quantity"> </fieldset> <script type="text/worker"> const repeatingSectionDetails = [//Array that holds details of each of our repeating sections that we'll need to work with { section:'repeating_item',//The name of the repeating section fields:['mass','quantity']//The fields in the section } ]; //converts attribute values to numbers const value = function(val,def){ return (+val||def||0); }; //Iterates through a set of repeating sections to get the IDs of all the rows and creates an array of which repeating attributes to get. const getSections = function(sectionDetails,callback){ let queueClone = _.clone(sectionDetails); const worker = (queue,repeatAttrs=[],sections={})=>{ let detail = queue.shift(); getSectionIDs(detail.section,(IDs)=>{ sections[detail.section] = IDs; IDs.forEach((id)=>{ detail.fields.forEach((f)=>{ repeatAttrs.push(`${detail.section}_${id}_${f}`); }); }); repeatAttrs.push(`_reporder_${detail.section}`); if(queue.length){ worker(queue,repeatAttrs,sections); }else{ callback(repeatAttrs,sections); } }); }; if(!queueClone[0]){ callback([],{}); }else{ worker(queueClone); } }; //gets our repeating and non repeating attributes const getAllAttrs = function({props=baseGet,sectionDetails=repeatingSectionDetails,callback}){ getSections(sectionDetails,(repeats,sections)=>{ getAttrs([...props,...repeats],(values)=>{ callback(attributes,sections); }) }); }; const calculateTotalMass = function(){ getAllAttrs({ sectionDetails:repeatingSectionDetails, callback:(attributes,sections)=>{ const setObj = {};//This is going to accumulate our attribute changes //totals the mass of items multiplied by how many of them there are, all while iterating over each of the repeating rows. setObj.total_mass = sections.repeating_item.reduce((total,id)=> total += value(attributes[`repeating_item_${id}_mass`])*value(attributes[`repeating_item_${id}_quantity`]),0); setAttrs(setObj,{silent:true});//Apply our changes } }); }; //Our listener to actually trigger the calculation when a quantity or mass changes on("change:repeating_item:mass change:repeating_item:quantity",calculateTotalMass); </script> Because we've setup our utility functions, we can use those in future calculations or for other more complex actions. The utility functions are adapted from my K-scaffold for building character sheets .
1643562823

Edited 1643563563
The Aaron
Roll20 Production Team
API Scripter
The short answer is there isn't a way with a macro/formula to ignore NaN values that I know of. You could try putting them in a group like {@{attr},0}kh1 and see if that filters things out.  (Edit: <-- That didn't work) If you have control over the sheet, it would be easiest to handle with a sheet worker, written in javascript. I know you said you don't know that programming language, but there are lots of peeps here that could likely help with that. 
1643563216
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I'll also note that GiGs wrote a great snippet called repeatingSum that is available on the Wiki. It is a little more concise than the code I presented.
1643563470
Kraynic
Pro
Sheet Author
First Question:&nbsp; Not that I am aware of.&nbsp; There are a few things that could be done to minimize the potential for problems. In the html, you can specify a default value for an input.&nbsp; Then that value is created when the sheet is created and if someone deletes the value in the input, it will return to the default.&nbsp; So you just fill in whatever default you want things to start with by setting that value. If people don't need to be able to input text (for customizing built in rolls by plugging in attribute names), you can set the type of input to number.&nbsp; Then people can't input anything that isn't a number like your N/A example. Bonus Question:&nbsp; Looks like you need to look into sheetworkers (javascript) that is what sheets use for automation (sums, calculating modifiers based on core stats, etc.).&nbsp; The main wiki article on Building Character Sheets has a section mentioning this and a link to the main sheetworker page:&nbsp; <a href="https://wiki.roll20.net/Building_Character_Sheets#JavaScript_2" rel="nofollow">https://wiki.roll20.net/Building_Character_Sheets#JavaScript_2</a> Gold Star: Sheetworkers have that covered.&nbsp; If it is in a repeating section, there is a "Repeating Sum" sheetworker that is specifically for doing things like that:&nbsp; <a href="https://wiki.roll20.net/RepeatingSum" rel="nofollow">https://wiki.roll20.net/RepeatingSum</a> &nbsp; Other pages that may be of interest for sheetworkers would be these: <a href="https://wiki.roll20.net/Sheetworker_examples_for_Non-programmers" rel="nofollow">https://wiki.roll20.net/Sheetworker_examples_for_Non-programmers</a> <a href="https://wiki.roll20.net/Sheet_Worker_Snippets" rel="nofollow">https://wiki.roll20.net/Sheet_Worker_Snippets</a>
Thank you everyone for the replies. And Scott, in particular, thank you so much for putting so much effort into that. I won't get a chance to work with it for a couple of hours, but I wanted to thank you without delay. I do have access to the sheet, it's my first custom sheet. Everything is working fine so far, I just know that it is not "elegant" code. I'm like a toddler with a hammer! But I'm having fun hitting things and seeing what happens. Yes, I have been totaling the items into a disabled &lt;input&gt;. I set all those attributes (qty and mass) to be type="number" so that no one could enter something NaN, and set each input to a value of 0. My brute-force method works, it's just ugly and puts a lot of unnecessary Zeros on the sheet. I'll be really excited if I can clean it up and get the repeating items working. Even more excited if I actually understand how it worked! Thanks for trying to teach me. I have gotten really far with the wiki pages but the repeating items thing has been quite a tangle for me to unravel.
The ENC field and MAX fields autocalc from the strength attribute, and the TOTAL is my crazy long calc with 54 variables, lol I know it's not pretty. It'll work for the game this week and then I can make it prettier as we go along. Obviously I have room for three columns there, and being able to add just a row at a time would be awesome (I'm hoping that's in the code that Scott wrote). I'll def check out that repeatingSum snippet.
1643632893

Edited 1643633076
Joel
Pro
Scott C. said: Thank you for doing all that Scott. The idea works great, but for some reason it is not actually totaling the values. The total mass stays zero. I've been reading it over and over trying to figure out if there might be a typo or if I did something wrong. In order to test it by itself I just copied and pasted right into the HTML section of a brand new custom sheet, no other HTML was added, and no CSS (for testing). Any idea why it might not be adding up?
1643638833
Finderski
Plus
Sheet Author
Compendium Curator
Do you have an input with the name attr_total_mass? If not, you'll need to add one (outside of the repeating section, btw). &nbsp;If you have it, is it disabled? If it's disabled make readonly instead of disabled. A sheetworker can't update a disabled field.
1643643890

Edited 1643643919
Joel
Pro
Finderski. Thanks, I'll look into that later today (I'm away from my editing software) This is really helping me understand sheetworkers in general. I didn't want you to think I'm just sitting back and waiting for others to do the work for me, lol. So far I have gotten all of this other stuff to work great (see below), I just couldn't figure out what I was missing in that one result. (I'm sure I could condense all those similar functions into one smaller one, but you know, baby steps. I tried condensing it and messed something up, so I'm back to what works.) &lt;script type="text/worker"&gt; // calculate strength and punch score on("change:str_base change:str_mod change:str_xp sheet:opened", function() { getAttrs(["str_base","str_mod","str_xp"], function(values) { let foo_str_base = parseInt(values.str_base)||0; let foo_str_mod = parseInt(values.str_mod)||0; let foo_str_xp = parseInt (values.str_xp)||0; let foo_str = foo_str_base + foo_str_mod + foo_str_xp; setAttrs({ str: foo_str, ps: Math.floor(foo_str / 10) }); }); }); // calculate stamnina on("change:sta_base change:sta_mod change:sta_xp sheet:opened", function() { getAttrs(["sta_base","sta_mod","sta_xp"], function(values) { let foo_sta_base = parseInt(values.sta_base)||0; let foo_sta_mod = parseInt(values.sta_mod)||0; let foo_sta_xp = parseInt (values.sta_xp)||0; let foo_sta = foo_sta_base + foo_sta_mod + foo_sta_xp; setAttrs({ sta: foo_sta }); }); }); // calculate dexterity on("change:dex_base change:dex_mod change:dex_xp sheet:opened", function() { getAttrs(["dex_base","dex_mod","dex_xp"], function(values) { let foo_dex_base = parseInt(values.dex_base)||0; let foo_dex_mod = parseInt(values.dex_mod)||0; let foo_dex_xp = parseInt (values.dex_xp)||0; let foo_dex = foo_dex_base + foo_dex_mod + foo_dex_xp; setAttrs({ dex: foo_dex }); }); }); // calculate reaction speed and initiative modifier on("change:rs_base change:rs_mod change:rs_xp sheet:opened", function() { getAttrs(["rs_base","rs_mod","rs_xp"], function(values) { let foo_rs_base = parseInt(values.rs_base)||0; let foo_rs_mod = parseInt(values.rs_mod)||0; let foo_rs_xp = parseInt (values.rs_xp)||0; let foo_rs = foo_rs_base + foo_rs_mod + foo_rs_xp; setAttrs({ rs: foo_rs, im: Math.ceil(foo_rs / 10) }); }); }); // calculate intuition on("change:int_base change:int_mod change:int_xp sheet:opened", function() { getAttrs(["int_base","int_mod","int_xp"], function(values) { let foo_int_base = parseInt(values.int_base)||0; let foo_int_mod = parseInt(values.int_mod)||0; let foo_int_xp = parseInt (values.int_xp)||0; let foo_int = foo_int_base + foo_int_mod + foo_int_xp; setAttrs({ int: foo_int }); }); }); // calculate logic on("change:log_base change:log_mod change:log_xp sheet:opened", function() { getAttrs(["log_base","log_mod","log_xp"], function(values) { let foo_log_base = parseInt(values.log_base)||0; let foo_log_mod = parseInt(values.log_mod)||0; let foo_log_xp = parseInt (values.log_xp)||0; let foo_log = foo_log_base + foo_log_mod + foo_log_xp; setAttrs({ log: foo_log }); }); }); // calculate personality on("change:per_base change:per_mod change:per_xp sheet:opened", function() { getAttrs(["per_base","per_mod","per_xp"], function(values) { let foo_per_base = parseInt(values.per_base)||0; let foo_per_mod = parseInt(values.per_mod)||0; let foo_per_xp = parseInt (values.per_xp)||0; let foo_per = foo_per_base + foo_per_mod + foo_per_xp; setAttrs({ per: foo_per }); }); }); // calculate leadership on("change:ldr_base change:ldr_mod change:ldr_xp sheet:opened", function() { getAttrs(["ldr_base","ldr_mod","ldr_xp"], function(values) { let foo_ldr_base = parseInt(values.ldr_base)||0; let foo_ldr_mod = parseInt(values.ldr_mod)||0; let foo_ldr_xp = parseInt (values.ldr_xp)||0; let foo_ldr = foo_ldr_base + foo_ldr_mod + foo_ldr_xp; setAttrs({ ldr: foo_ldr }); }); }); &lt;/script&gt;
1643814885

Edited 1643815922
Joel
Pro
Scott C. said: Gold Star As I said, the best way to do this is going to be via Javascript, and we'll even swap this to a repeating section while we're at it. This code is going to be slightly overkill so that writing the actual calculation and future calculations will be easier: Hey Scott, I'm sorry to revisit this, but I still can't figure out why the code you wrote isn't working. To test it outside my own sheet, I've simply copied and pasted your code directly into a new custom sheet in a new game. The "Add" and "Modify" buttons work as expected but the total_mass still does not update. Finderski said to make sure I had an input with the name attr_total_mass, and that it was readonly instead of disabled. I think that's what the first line of your code does, right? And to make sure it is outside of the repeating section, which it does appear to be outside. It seems to be set up like all my other working sheetworkers stuff. I looked for any obvious typo (like a simple misspelling) but didn't find anything obvious and that's as much as I know how to do. I went through the other snippets everyone talked about but still can't figure out why the code&nbsp; you wrote isn't working, although it seems perfect for my purpose.&nbsp; Any chance someone could look it over again really quickly or test it at home? Maybe I'm missing something soul-crushingly obvious.
1643818995
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hey Joel, I'll need to see the code you've got to troubleshoot.
1643821284

Edited 1643821321
Joel
Pro
I'm just using your code right now, from your earlier post in this thread, nothing added to it at all. I created a new game, chose Custom Character sheet, and the only code in that sheet right now is what you wrote. &lt;input type='number' name="attr_total_mass" value="0" readonly&gt;&lt;!--This input is readonly which means only a sheetworker will be able to change it. Sheetworkers cannot interact with autocalc attributes (aka disabled="true")--&gt; &lt;fieldset class="repeating_item"&gt;&lt;!-- Replace the contents of this repeating section with whatever your setup for your items is--&gt; &lt;input type="text" name="attr_name"&gt; &lt;input type="number" name="attr_mass"&gt; &lt;input type="number" name="attr_quantity"&gt; &lt;/fieldset&gt; &lt;script type="text/worker"&gt; const repeatingSectionDetails = [//Array that holds details of each of our repeating sections that we'll need to work with { section:'repeating_item',//The name of the repeating section fields:['mass','quantity']//The fields in the section } ]; //converts attribute values to numbers const value = function(val,def){ return (+val||def||0); }; //Iterates through a set of repeating sections to get the IDs of all the rows and creates an array of which repeating attributes to get. const getSections = function(sectionDetails,callback){ let queueClone = _.clone(sectionDetails); const worker = (queue,repeatAttrs=[],sections={})=&gt;{ let detail = queue.shift(); getSectionIDs(detail.section,(IDs)=&gt;{ sections[detail.section] = IDs; IDs.forEach((id)=&gt;{ detail.fields.forEach((f)=&gt;{ repeatAttrs.push(`${detail.section}_${id}_${f}`); }); }); repeatAttrs.push(`_reporder_${detail.section}`); if(queue.length){ worker(queue,repeatAttrs,sections); }else{ callback(repeatAttrs,sections); } }); }; if(!queueClone[0]){ callback([],{}); }else{ worker(queueClone); } }; //gets our repeating and non repeating attributes const getAllAttrs = function({props=baseGet,sectionDetails=repeatingSectionDetails,callback}){ getSections(sectionDetails,(repeats,sections)=&gt;{ getAttrs([...props,...repeats],(values)=&gt;{ callback(attributes,sections); }) }); }; const calculateTotalMass = function(){ getAllAttrs({ sectionDetails:repeatingSectionDetails, callback:(attributes,sections)=&gt;{ const setObj = {};//This is going to accumulate our attribute changes //totals the mass of items multiplied by how many of them there are, all while iterating over each of the repeating rows. setObj.total_mass = sections.repeating_item.reduce((total,id)=&gt; total += value(attributes[`repeating_item_${id}_mass`])*value(attributes[`repeating_item_${id}_quantity`]),0); setAttrs(setObj,{silent:true});//Apply our changes } }); }; //Our listener to actually trigger the calculation when a quantity or mass changes on("change:repeating_item:mass change:repeating_item:quantity",calculateTotalMass); &lt;/script&gt;
I was only working with your code and nothing else so that I could get it formatted the way I want without a lot of extra crap to get in the way. Once I get it formatted the way I want, I'll drop it into my actual character sheet. Maybe that's the problem, but this method works fine with the repeatingSum snippets. Yours just seemed better suited to my purpose, if only the total_mass would show up.
1643822578
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah, the problem is that I apparently didn't sanitize out all the K-scaffold specific stuff when I extracted those functions, so it was throwing referenceErrors. Here's the corrected code: &lt;input type='number' name="attr_total_mass" value="0" readonly&gt;&lt;!--This input is readonly which means only a sheetworker will be able to change it. Sheetworkers cannot interact with autocalc attributes (aka disabled="true")--&gt; &lt;fieldset class="repeating_item"&gt;&lt;!-- Replace the contents of this repeating section with whatever your setup for your items is--&gt; &lt;input type="text" name="attr_name"&gt; &lt;input type="number" name="attr_mass"&gt; &lt;input type="number" name="attr_quantity"&gt; &lt;/fieldset&gt; &lt;script type="text/worker"&gt; const repeatingSectionDetails = [//Array that holds details of each of our repeating sections that we'll need to work with { section:'repeating_item',//The name of the repeating section fields:['mass','quantity']//The fields in the section } ]; //converts attribute values to numbers const value = function(val,def){ return (+val||def||0); }; //Iterates through a set of repeating sections to get the IDs of all the rows and creates an array of which repeating attributes to get. const getSections = function(sectionDetails,callback){ let queueClone = _.clone(sectionDetails); const worker = (queue,repeatAttrs=[],sections={})=&gt;{ let detail = queue.shift(); getSectionIDs(detail.section,(IDs)=&gt;{ sections[detail.section] = IDs; IDs.forEach((id)=&gt;{ detail.fields.forEach((f)=&gt;{ repeatAttrs.push(`${detail.section}_${id}_${f}`); }); }); repeatAttrs.push(`_reporder_${detail.section}`); if(queue.length){ worker(queue,repeatAttrs,sections); }else{ callback(repeatAttrs,sections); } }); }; if(!queueClone[0]){ callback([],{}); }else{ worker(queueClone); } }; //gets our repeating and non repeating attributes const getAllAttrs = function({props,sectionDetails=repeatingSectionDetails,callback}){ getSections(sectionDetails,(repeats,sections)=&gt;{ getAttrs([...props,...repeats],(attributes)=&gt;{ callback(attributes,sections); }) }); }; const calculateTotalMass = function(){ getAllAttrs({ props: ['total_mass'], sectionDetails:repeatingSectionDetails, callback:(attributes,sections)=&gt;{ const setObj = {};//This is going to accumulate our attribute changes //totals the mass of items multiplied by how many of them there are, all while iterating over each of the repeating rows. setObj.total_mass = sections.repeating_item.reduce((total,id)=&gt; total += value(attributes[`repeating_item_${id}_mass`])*value(attributes[`repeating_item_${id}_quantity`]),0); setAttrs(setObj,{silent:true});//Apply our changes } }); }; //Our listener to actually trigger the calculation when a quantity or mass changes on("change:repeating_item:mass change:repeating_item:quantity",calculateTotalMass); &lt;/script&gt;
Perfect, thanks so much. I really appreciate it.
1644946427

Edited 1644946558
Joel
Pro
Scott C. said: Ah, the problem is that I apparently didn't sanitize out all the K-scaffold specific stuff when I extracted those functions, so it was throwing referenceErrors. Here's the corrected code: Hey Scott (or anyone else who can answer this)... I'm using the code from two posts up (Scott's corrected code) to sum all the 'mass' entries in a repeating section, and it works great when I have only one repeating section of items that needs to be totaled. Let's call that section repeating_equipment. The fields in it are 'quantity', 'name' (which does not need to be calculated and is therefore missing from the sheetworkers code), and 'mass'. The code above works perfectly to total the 'repeating_equipment' mass. But in another part of the character sheet, I have another section of items that needs to be independently totaled as well. It is called repeating_kits. The fields in repeating_kits are identical to those in repeating_equipment. They are 'quantity', 'name' (which does not need to be calculated), and 'mass'. The only difference between the two repeating sections are their names, repeating_equipment and repeating_kits. I need to total the 'repeating_kits' mass using the same technique. I copied and pasted a second version the original code, changing its repeating section name to the new section, 'repeating_kits', and hoped that the two versions could coexist.&nbsp; Either one of these sections will work fine if I delete the code for the other. But when both exist at the same time, it all locks up. My initial attempt to fix it was to change the attribute name of the sum in 'repeating_kits'. Originally called 'total_mass' (just like in 'repeating_equipment'), I changed the 'kits' version to 'subtotal_mass', both in the HTML and in the 'repeating_kits' script. But that did not resolve the issue. I've guessed that I need to change some of the other names in the code to make them unique, perhaps the two versions are competing with each other or overwriting each other. It's my understanding that the attribute names from each of the repeating sections can be the same ('quantity', 'name', 'mass') as long as there is not a "normal" non-repeating version of that attribute name somewhere else in the sheet. So instead I've been adding the word 'Kits' to various other parts of the second version of the script, parts that look like maybe they could use a unique name, but it has been trial and error. Mostly error. I have no idea which parts need unique names and which are fine to leave alone. After hours of failed attempts, I'm hoping someone here can help me. How can I use two versions of that code to independently calculate the total 'mass' from two different repeating sections? Thanks so much!
1644949066
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
When you say that you copied the code , did you copy everything? The code will throw syntax errors if that's the case cause then you're trying to define two instances of the same constant. Constants can only be defined once and then are immutable. You need to either define a totally new function, or edit the calcTotalMass function to work for both. My recommendation would be to edit the function to work for both as then you have less code to maintain. Something like: const parseTriggerName = function(string){//Add this function to the ones already present in the code above. let match = string.replace(/^clicked:/,'').match(/(?:(repeating_[^_]+)_([^_]+)_)?(.+)/); match.shift(); return match; }; const calculateTotalMass = function(event){//Replace calculateTotalMass with this version let [section,id,field] = parseTriggerName(event.sourceAttribute); let massAttributes = { repeating_equipment: 'total_mass', repeating_kit: 'total_kit_mass' }; let massAttr = massAttributes[section]; if(!massAttr){ return;//Protection for if something triggers this that shouldn't have } getAllAttrs({ props: [massAttr], sectionDetails:[{section,fields:['mass','quantity']}], callback:(attributes,sections)=&gt;{ const setObj = {};//This is going to accumulate our attribute changes //totals the mass of items multiplied by how many of them there are, all while iterating over each of the repeating rows. setObj[massAttr] = sections[section].reduce((total,id)=&gt; total += value(attributes[`${section}_${id}_mass`])*value(attributes[`${section}_${id}_quantity`]),0); setAttrs(setObj,{silent:true});//Apply our changes } }); }; //Replace the listener in the original code with the below //Our listener to actually trigger the calculation when a quantity or mass changes ['repeating_item','repeating_kit'].forEach((section)=&gt;{ ['mass','quantity'].forEach((field)=&gt;{ on(`change:${section}:${field}`,calculateTotalMass) }); });
ok, thank you. Yes I did copy the entire thing. I had a feeling those constants were the problem, but my hacky attempts to resolve it were not working. Thanks so much.
Unfortunately those changes did not work. Could be operator error, for sure. I also wondered if it might have K-scaffold stuff in it? That's what was wrong last time. Here's what I'm working with. It is supposed sum 'quantity' * 'mass' for the two repeating sections individually (_equipment and _kits), and then I would like to sum those two results so that I have a grand total. &lt;div&gt; &lt;b&gt;Equipment&lt;/b&gt; &lt;fieldset class="repeating_equipment"&gt; &lt;input name="attr_quantity" type="text" style="width:50px;" placeholder="Qty"&gt; &lt;input name="attr_mass" type="text" style="width:50px;" placeholder="Mass"&gt; &lt;/fieldset&gt; &lt;/div&gt; &lt;div&gt; 'equipmentmass' = &lt;input name="attr_equipmentmass" type="text" readonly="readonly" style="width:50px;"&gt; (sum all equipment, qty x mass) &lt;/div&gt; &lt;hr&gt; &lt;div&gt; &lt;b&gt;Kits&lt;/b&gt; &lt;fieldset class="repeating_kits"&gt; &lt;input name="attr_quantity" type="text" style="width:50px;" placeholder="Qty"&gt; &lt;input name="attr_mass" type="text" style="width:50px;" placeholder="Mass"&gt; &lt;/fieldset&gt; &lt;/div&gt; &lt;div&gt; 'kitsmass' = &lt;input name="attr_kitsmass" type="text" readonly="readonly" style="width:50px;"&gt; (sum all kits, qty x mass) &lt;/div&gt; &lt;hr&gt; &lt;div&gt; 'totalmass' = &lt;input name="attr_totalmass" type="text" readonly="readonly" style="width:50px;"&gt; (sum of 'equipmentmass' + 'kitsmass') &lt;/div&gt; &lt;hr&gt; &lt;div&gt; &lt;ul&gt; &lt;li&gt;I added the calculation for 'totalmass' to the script. Might want to check it, but the script fails even without that.&lt;/li&gt; &lt;li&gt;I would like to add remove: and sheet:opened to the listener.&lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;script type="text/worker"&gt; const parseTriggerName = function(string){ let match = string.replace(/^clicked:/,'').match(/(?:(repeating_[^_]+)_([^_]+)_)?(.+)/); match.shift(); return match; }; const repeatingSectionDetails = [ { section:['repeating_equipment','repeating_kits'] fields:['mass','quantity'] } ]; const value = function(val,def){ return (+val||def||0); }; const getSections = function(sectionDetails,callback){ let queueClone = _.clone(sectionDetails); const worker = (queue,repeatAttrs=[],sections={})=&gt;{ let detail = queue.shift(); getSectionIDs(detail.section,(IDs)=&gt;{ sections[detail.section] = IDs; IDs.forEach((id)=&gt;{ detail.fields.forEach((f)=&gt;{ repeatAttrs.push(`${detail.section}_${id}_${f}`); }); }); repeatAttrs.push(`_reporder_${detail.section}`); if(queue.length){ worker(queue,repeatAttrs,sections); }else{ callback(repeatAttrs,sections); } }); }; if(!queueClone[0]){ callback([],{}); }else{ worker(queueClone); } }; const calculateTotalMass = function(event){ let [section,id,field] = parseTriggerName(event.sourceAttribute); let massAttributes = { repeating_equipment: 'equipmentmass', repeating_kits: 'kitsmass' }; let massAttr = massAttributes[section]; if(!massAttr){ return; } getAllAttrs({ props: [massAttr], sectionDetails:[{section,fields:['mass','quantity']}], callback:(attributes,sections)=&gt;{ const setObj = {}; setObj[massAttr] = sections[section].reduce((total,id)=&gt; total += value(attributes[`${section}_${id}_mass`])*value(attributes[`${section}_${id}_quantity`]),0); setAttrs(setObj,{silent:true}); setAttrs({totalmass: equipmentmass + kitsmass});//Joel added this line to sum the two sections together } }); }; ['repeating_equipment','repeating_kits'].forEach((section)=&gt;{ ['mass','quantity'].forEach((field)=&gt;{ on(`change:${section}:${field}`,calculateTotalMass) }); }); //Joel would like to add remove: and sheet:open to the listener but not sure how to do that &lt;/script&gt;
1645016699

Edited 1645017358
GiGs
Pro
Sheet Author
API Scripter
You might be able to do this easier by copying the repeatingSum code from the wiki (as suggested by Scott earlier in the thread). You'd copy the code from the wiki to the start of your script block: const repeatingSum = ( destinations , section , fields ) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (! Array . isArray ( destinations )) destinations = [ destinations . replace ( /\s/ g , '' ). split ( ',' )]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (! Array . isArray ( fields )) fields = [ fields . replace ( /\s/ g , '' ). split ( ',' )]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getSectionIDs ( `repeating_ ${ section } ` , idArray =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const attrArray = idArray . reduce (( m , id ) =&gt; [... m , ...( fields . map ( field =&gt; `repeating_ ${ section } _ ${ id } _ ${ field } ` ))], []); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getAttrs ([... attrArray ], v =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const getValue = ( section , id , field ) =&gt; v [ `repeating_ ${ section } _ ${ id } _ ${ field } ` ] === 'on' ? 1 : parseFloat ( v [ `repeating_ ${ section } _ ${ id } _ ${ field } ` ]) || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const commonMultipliers = ( fields . length &lt;= destinations . length ) ? [] : fields . splice ( destinations . length , fields . length - destinations . length ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const output = {}; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; destinations . forEach (( destination , index ) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output [ destination ] = idArray . reduce (( total , id ) =&gt; total + getValue ( section , id , fields [ index ]) * commonMultipliers . reduce (( subtotal , mult ) =&gt; subtotal * getValue ( section , id , mult ), 1 ), 0 ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setAttrs ( output ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; }; You do not touch that code at all, just paste it into the start of your script block. Then to sum up each repeating section, create a worker following the instructions on the page. For your fieldsets above, that would look like: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; on ( 'change:repeating_equipment:quantity change:repeating_equipment:mass remove:repeating_equipment' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; repeatingSum ( "equipmentmass" , "equipment" ,[ "quantity" , "mass" ]); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; on ( 'change:repeating_kits:quantity change:repeating_equipment:mass remove:repeating_kits' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; repeatingSum ( "equipmentmass" , "kits" ,[ "quantity" , "mass" ]); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; Notice on the event line, this follows standard roll20 example syntax - include a change event for each relevant attribute, and a remove for the repeating section so it detects when the user deletes a row. Then call the function you have just added - first supply the attribute you want to save the total into, then the repeating section name (without the repeating_ part), and finally an array of the attributes inside the section you want to multiply together. You can repeat this process for every repeating section you want to sum up. I wrote this function because at the time I was having to sum up a lot of repeating sections, and I wanted a really simple way to do it over and over. Now I just plonk that function at the start of a code block, ignore it, and then create those 3 line workers when I need them. Finally if using multiple repeating sections, you need to add one more worker to add the totals together. There's lots of ways to do this, but following (almost) the roll20 example syntax, here's one: &nbsp;&nbsp;&nbsp;&nbsp; on ( 'change:kitsmass change:equipmentmass' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getAttrs ([ 'kitsmass' , 'equipmentmass' ], function ( v ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const kits = + v . kitsmass || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const equip = + v . equipmentmass || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const total = kits + equip ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setAttrs ({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; totalmass : total &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; }); Now this is less efficient than Scott's method, and relies on cascading events (one setattrs creating a change, which is then picked up by the change event in another worker), but if you dont go overboard with such things there's no reason this should be noticeable by the user. Putting it all together, your entire script block would look like: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const repeatingSum = ( destinations , section , fields ) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (! Array . isArray ( destinations )) destinations = [ destinations . replace ( /\s/ g , '' ). split ( ',' )]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (! Array . isArray ( fields )) fields = [ fields . replace ( /\s/ g , '' ). split ( ',' )]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getSectionIDs ( `repeating_ ${ section } ` , idArray =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const attrArray = idArray . reduce (( m , id ) =&gt; [... m , ...( fields . map ( field =&gt; `repeating_ ${ section } _ ${ id } _ ${ field } ` ))], []); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getAttrs ([... attrArray ], v =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const getValue = ( section , id , field ) =&gt; v [ `repeating_ ${ section } _ ${ id } _ ${ field } ` ] === 'on' ? 1 : parseFloat ( v [ `repeating_ ${ section } _ ${ id } _ ${ field } ` ]) || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const commonMultipliers = ( fields . length &lt;= destinations . length ) ? [] : fields . splice ( destinations . length , fields . length - destinations . length ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const output = {}; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; destinations . forEach (( destination , index ) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output [ destination ] = idArray . reduce (( total , id ) =&gt; total + getValue ( section , id , fields [ index ]) * commonMultipliers . reduce (( subtotal , mult ) =&gt; subtotal * getValue ( section , id , mult ), 1 ), 0 ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setAttrs ( output ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; }; &nbsp; &nbsp; &nbsp; &nbsp; on ( 'change:repeating_equipment:quantity change:repeating_equipment:mass remove:repeating_equipment' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; repeatingSum ( "equipmentmass" , "equipment" ,[ "quantity" , "mass" ]); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; on ( 'change:repeating_kits:quantity change:repeating_equipment:mass remove:repeating_kits' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; repeatingSum ( "kitsmass" , "kits" ,[ "quantity" , "mass" ]); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; on ( 'change:kitsmass change:equipmentmass' , function () { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getAttrs ([ 'kitsmass' , 'equipmentmass' ], function ( v ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const kits = + v . kitsmass || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const equip = + v . equipmentmass || 0 ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const total = kits + equip ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setAttrs ({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; totalmass : total &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; }); (Dont forget the starting and ending script lines.)
Thank you so much. Since Scott's original code was working well when there was just one repeating section, I hadn't really thought about scrapping the whole thing and starting over with one of the other scripts. It seems like I'm just one little detail away from this working. ARG! I will certainly give that wiki snippet a shot. Thanks for the detailed explanation. I do like the appeal of Scott's "elegant" or efficient code, but that's if I can get it to work. But honestly, like you said, if I go with this less efficient method, maybe it will go easier for me and the user will never know.
That works perfectly. Thank you so much. Really appreciate it.
1645031614
GiGs
Pro
Sheet Author
API Scripter
Great :)