Merge List Values Based on Duplicates

What I’m trying to do

I’m making a fairly elaborate DnD character sheet setup with Bases and using a series of formulas to aggregate different active sources of modifications to certain Skills.

My starting point for what I need to do is a list with entries in the following format: Skill | Modifier

Here’s a copy of the current list output from Skills Compiled shown in the screenshot.

["Acrobatics | ADV", "Acrobatics | DIS", "Arcana | Proficiency", "Athletics | Expertise", "Athletics | Proficiency", "Deception | DIS", "Deception | Expertise"]

Because they can all come in from different sources at any of those modifier values (ADV, DIS, Proficiency, and Expertise), it’s important for them to start separately, but eventually I need them consolidated all in one place.

You can see where I have multiple entries per Skill; I’d like to be able find a way to map/filter/reduce this list so that it merges the modifiers of Skills which share the same value before the bar. For instance, in the example above, I’m trying to get Acrobatics | ADV and Acrobatics | DIS to become Acrobatics | ADV, DIS and Deception | Expertise & Deception | DIS to become Deception | Expertise, DIS (the commas aren’t necessary per se, but they are readable).

The goal is to fold the values together per Skill so that some more logic can be applied (e.g. if the same value contains ADV & DIS, cancel them out from the final compilation; and if the same value contains “Proficiency” & “Expertise,” just show Expertise). I think I know how to handle that part of it all, however, once I get the entries into the proper format.

Bear in mind that in theory any given Skill value could contain any/all of those 4 values, and that some Skill names are multiple words long like Animal Handling or Sleight of Hand.

Things I have tried

I’ve been scratching my head at this for a while trying things, and the closest I could get was something like the following, but although it helps remove the duplicate Skill items and preserves the order…the result is just a disconnected list, not one grouped by Skill anymore:

this.formula["Skills Compiled"].map(value.split(" | ").slice(0,-1)+value.split(" | ").slice(-1)).flat().unique().join(", ")

Returns Acrobatics, ADV, DIS, Arcana, Proficiency, Athletics, Expertise, Deception …not sure if that’s helpful, but it shows I tried first?!

Much obliged!

1 Like

Does this have to happen in Obsidian? If you are willing to copy paste to Python for a moment, you can run this quick Claude Code’d script on your own machine or online like at PyNative after replacing the example object with your input.

#!/usr/bin/env python3

def consolidate_skills(skill_list):
    """
    Consolidates skill modifiers by grouping modifiers for the same skill.

    Args:
        skill_list: List of strings in format "Skill | Modifier"

    Returns:
        List of strings in format "Skill | Modifier1, Modifier2, ..."
    """
    skill_dict = {}

    # Group modifiers by skill
    for entry in skill_list:
        parts = entry.split(" | ")
        if len(parts) == 2:
            skill, modifier = parts[0].strip(), parts[1].strip()
            if skill not in skill_dict:
                skill_dict[skill] = []
            skill_dict[skill].append(modifier)

    # Combine back into list format
    result = []
    for skill, modifiers in skill_dict.items():
        combined = f"{skill} | {', '.join(modifiers)}"
        result.append(combined)

    return result


# Example usage
if __name__ == "__main__":
    example = [
        "Acrobatics | ADV",
        "Acrobatics | DIS",
        "Arcana | Proficiency",
        "Athletics | Expertise",
        "Athletics | Proficiency",
        "Deception | DIS",
        "Deception | Expertise"
    ]

    print("Input:")
    for item in example:
        print(f"  {item}")

    print("\nOutput:")
    result = consolidate_skills(example)
    for item in result:
        print(f"  {item}")

Appreciate the external consideration! Unfortunately yes, I’m really doggedly trying to push this through the more limited scope of Bases–the starting point list is itself the product of several other formulas. I’ve been really pleased with the plasticity of it otherwise, though. Able to do tons!

1 Like

Claude Code gave this:

Object.entries(                                                                                                                                   
    this.formula["Skills Compiled"].reduce((acc, entry) => {                                                                                        
      let [skill, modifier] = entry.split(" | ");                                                                                                   
      if (!acc[skill]) {                                                                                                                            
        acc[skill] = [];                                                                                                                            
      }                                                                                                                                             
      acc[skill].push(modifier);                                                                                                                    
      return acc;                                                                                                                                   
    }, {})                                                                                                                                          
  ).map(([skill, modifiers]) => skill + " | " + modifiers.join(", ")) 

With Bases being relatively new and so similar to Dataview, I think the AI might be getting its wires crossed. This happened all the time with Google’s AI section as well. Some of the terminology is correct, but the implementation still looks very Dataview compared to what I’ve seen.

I think the result does point in the right direction, however–it’s definitely a “for” loop type of construction. The functions documentation lists at least 3 list functions that perform operations on all items of a list (filter, map, reduce), and 2/3 of them are present. It’s got something to do with that and splitting the value at the bar to get at the pre-bar matches to compare which are the same.

There’s a conditional element to it in there as well. It’s almost like you need to compare the list item at one index with the item at the next index to see which match. Bringing it back together might then entail something where an entry gets compared to the next/prior entry and if it doesn’t have one of the Mod keywords e.g. !value.containsAny("ADV","DIS","Proficiency","Expertise") then the value is a skill.

Another limitation I’ve come across elsewhere, if it wards anyone away from it, is list-to-list substring comparisons. Imagine you’ve got a list of strings and you want to see if any entries contain values from another group of possible items. You’d think containsAny("thing1","thing2","thing3") would be able to support containsAny([list]) so that the criteria could be determined dynamically…but not so far as I’ve been able to manage it! I get substring-to-substring matches not being an option, but it does feel like containsAny() is just passing a list already.

I mention this because I was tempted to just throw every Skill name in a list and approach it that way…but adding in a list of like 20+ entries potentially multiple times seemed gratuitous.

Forgot to sneak this into the thoughts above before the edit timer ran out haha

This little snippet based off insights from this thread might be useful too since it is the method I’ve seen used to compile two lists values at the same index into a new combo list.

[" ".repeat(this.formula["Skills Compiled"].length).split("").map([ this.formula["List A"][index]]).map(this.formula["List A"][index]+[" (" + this.formula["List B"][index]+")"])]

I was only barely able to wrangle it for another purpose elsewhere and tried not to question it too much after it worked in that instance, but to a better trained eye, maybe it offers a springboard.

After a bunch more trial and error, while I did not find a direct solution to the issue as I’ve described it, I was able to achieve what I wanted by taking a step back and viewing the problem from a different angle. For others learning the ropes with Bases, the good news is if my art brain can muscle through this then probably so can you! I wanted to close the loop here by noting what I did in case it helps anyone down the line to see steps broken out.

The answer was…another View. I needed a per-Skill place to collect and consolidate the existing modifications, and what I realized was I already had that in the form of pages in my vault dedicated to their explanation in general. They were even in their own folder already, so all I had to do was create a new view and filter in that folder’s contents (and filter out any non-.md extension files)

file.folder == "Sidera/Dungeons & Dragons/Rules & Mechanics/Skills"

That gave me the Skill list I needed to search through, but made it accessible for .filter(value)

this.formula["Skills Compiled"].filter(value.contains(file.name))

This gave me the proof of concept I needed for mixing “file” specific properties with this and I expanded it to the following to remove the Skill name text from the Skills Compiled list item split(" | ")1 & slice(-1), remove duplicate entries unique() and then put the remainder into one string join(). The list() wrapping everything is important.

list(this.formula["Skills Compiled"].filter(value.contains(file.name)).map(value.split(" | ").slice(-1)).unique().join(" "))

From that single per-skill string, I needed to do 3 things: add the Skill’s Ability score bonus (another formula calculated off raw stat properties; the chain of if() conditionals), evaluate the entry for whether it contained “Proficiency” or “Expertise” (and choose the higher bonus value of the two in the event both are detected; that’s the funky reduce() function and I’ll admit I pasted it in from the function documentation without fully understanding how it would work…but it does so yay!), and lastly it needed to handle what to do in the event of ADV & DIS being present at the same time (neutralize).

Add in some display dressing with additional links to file properties, displaying ability scores based on an “Abilities” property on those pages, totaling the numerical bonuses and adding the (DIS/ADV) situation where applicable and you get…

list(this.formula["Skills Compiled"].filter(value.contains(file.name)).map(value.split(" | ").slice(-1)).unique().join(" ")).map([[link(file.name)]+["("+file.properties.Ability+") | "]+[if(file.properties.Ability=="STR",this.formula["STR Mod"],if(file.properties.Ability=="DEX",this.formula["DEX Mod"],if(file.properties.Ability=="CON",this.formula["CON Mod"],if(file.properties.Ability=="INT",this.formula["INT Mod"],if(file.properties.Ability=="WIS",this.formula["WIS Mod"],if(file.properties.Ability=="CHA",this.formula["CHA Mod"]))))))+ [if(value.contains("Expertise"),this.formula["Proficiency Bonus"]*2),if(value.contains("Proficiency"),this.formula["Proficiency Bonus"],0)].reduce(if(acc == null || value > acc, value, acc), null)].map(if(value>-1,"+"+value,value))+ [if(value.contains("ADV")&&value.contains("DIS"),"",if(value.contains("ADV")&&!value.contains("DIS"),"(ADV)",if(value.contains("DIS")&&!value.contains("ADV"),"(DIS)")))]])

I’ve kept the scale low while I test so the list is incomplete for now, but this’ll all populate dynamically as I add the remaining pages.

Thanks for the brainpower & chance to make myself think differently by having to articulate the problem!

And uh… tl;dr follow your dreams! coding is hard but not impossible!

1 Like