Best practice for Google Application Scripting in Tiller

I am wary about using GAS in Tiller itself, though I have some experience using it with other Google Sheets. My main concern is mucking up the Transaction table in some way.

My immediate use case is using ARRAYFORMULA to populate a custom function that does fuzzy lookup in a helper tab/sheet.

Anyone else using custom GAS formulas fearlessly? Or are there any summary of best practices for mucking in the Transactions sheet? Been searching through the help files, but nothing clear (except the excellent ARRAYFORMULA tip I found).

Update: I set up a test environment by copying over a subset of my transactions tab. I have the following substring matching custom function working. Heavy lifting around array processing and clever caching to speed things up was done by ChatGPT.

const LOOKUP_CACHE = {};

function SUB_LOOKUP(searchTerms, numbers) {
  const cacheKey = 'lookupData';
  if (!LOOKUP_CACHE[cacheKey]) {
    const lookupSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("URL Lookup");
    const range = lookupSheet.getRange("A:C");
    const lookupData = range.getValues();
    const richTextValues = range.getRichTextValues();

    LOOKUP_CACHE[cacheKey] = lookupData.map((row, i) => {
      const richText = richTextValues[i][2];
      const linkUrl = richText ? richText.getLinkUrl() : null;
      const visibleText = row[2] || "";
      return [row[0], row[1], visibleText, linkUrl];
    }).filter(row => row[0] !== "" && !isNaN(row[1]));
  }

  const lookupArray = LOOKUP_CACHE[cacheKey];

  if (Array.isArray(searchTerms) && Array.isArray(numbers)) {
    return searchTerms.map((row, i) => {
      const term = row[0];
      const num = parseFloat(numbers[i][0]);
      return [getApproximateMatchUrl(term, num, lookupArray)];
    });
  } else {
    return getApproximateMatchUrl(searchTerms, parseFloat(numbers), lookupArray);
  }
}


function getApproximateMatchUrl(text, num, lookupArray) {
  try {
    if (!text || isNaN(num)) return "No Input";

    const tolerance = 0.01;
    const cleanText = text.toString().toLowerCase().trim();

    for (const [substring, lookupNum, visibleText, linkUrl] of lookupArray) {
      const cleanSubstring = substring.toString().toLowerCase().trim();
      if (
        cleanText.includes(cleanSubstring) &&
        Math.abs(parseFloat(lookupNum) - num) <= tolerance
      ) {
        return linkUrl || visibleText || "No Link";
      }
    }
    return "";
  } catch (err) {
    Logger.log("Error: " + err);
    return "ERROR";
  }
}


This matches on amount and a substring match of the Description against the store name in the lookup table. With a properly setup lookup table, you can then use the following formula in the header row:

=ARRAYFORMULA(IF(isblank($C:$C),"",if($C:$C<>"Description", hyperlink(SUB_LOOKUP($C:$C, $F:$F)), "Receipt")))

This yields the following sample output:

The URLs are hoverable, to show a preview of the receipt, and with the right browser extensions should show the full file. Have not tested that bit yet.

For reference: the lookup table looks like this:

1 Like

@BryanS, Apps Script is my jam!! I have definitely added custom code to my Tiller spreadsheet. It’s ALWAYS fearless, because you can always use version history to rewind it.

Go to the File menu and chose to name the current version before you run your script. This makes it easy to put it back.

I have even put my sheet back weeks and then resync’d. Obviously I lose my categories I assigned, but Tiller is smart enough to know which transactions are not on your sheet and resyncs.

Have fun scripting!

-Alice
Tiller Evangelist

Bluesky, Instagram, Facebook, LinkedIn

1 Like

Thanks for the affirmation! I had not realized that about versioning and smart re-syncing. Very reassuring.

Agreed, I think as long as you dont change the core Tiller sheets, Transactions and Balance HIstory you shoudl be fine. everything else is fairgame.

2 Likes

I’m curious, why not just manually insert the receipt link on say the Description cell and not use any scripting?
Is the lookup table auto-generated maybe? :thinking:

The intent is to automate yes. I have the beginnings of that built out, but my approach uses AI to simplify the parsing. That may not work for everyone. Thanks for the interest!