[
  {
    "name": "Replace last comma with \"and\" (Oxford comma)",
    "type": "Formula",
    "tags": [
      "Lookups",
      "Regex",
      "Strings"
    ],
    "code": "SUBSTITUTE(\n  IF(\n    FIND(\n      \",\",\n      ARRAYJOIN({Array})\n    ),\n    REGEX_REPLACE(\n      ARRAYJOIN({Array}),\n      \",([^,]+)$\",\n      \" and \"\n    ) & \n    REGEX_EXTRACT(\n      ARRAYJOIN({Array}),\n      \",([^,]+)$\"\n    ),\n    ARRAYJOIN({Array})\n  ),\n  \",\",\n  \", \"\n)",
    "notes": ""
  },
  {
    "name": "Extract numbers and consonants from string",
    "type": "Formula",
    "tags": [
      "Regex",
      "Strings"
    ],
    "code": "REGEX_REPLACE(\n  UPPER({Name),\n  \"[^0-9BCDFGHJKLMNPQRSTVWXYZ]\",\n  \"\"\n)",
    "notes": ""
  },
  {
    "name": "Date range status (Active/Inactive)",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "IF(\n  AND(\n    {Start date}, \n    {End date}\n  ),\n  IF(\n    OR(\n      IS_BEFORE({End date}, TODAY()), \n      IS_AFTER({Start date}, TODAY())\n    ),\n    \"Inactive\",\n    \"Active\"\n  )\n)",
    "notes": ""
  },
  {
    "name": "Check if date is weekday",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "AND(\n\tWEEKDAY({Date}) >= 1, \n\tWEEKDAY({Date}) <= 5\n)",
    "notes": ""
  },
  {
    "name": "Convert decimal to hh:mm duration string",
    "type": "Formula",
    "tags": [
      "Dates & times",
      "Strings",
      "Duration",
      "Conversion"
    ],
    "code": "IF(\n  {Decimal Hours},\n  FLOOR({Decimal Hours}) & \":\" &\n  RIGHT(\"0\" & ROUND(60 * MOD({Decimal Hours}, 1)), 2)\n)",
    "notes": ""
  },
  {
    "name": "Convert duration to HH:MM string",
    "type": "Formula",
    "tags": [
      "Dates & times",
      "Conversion",
      "Strings"
    ],
    "code": "IF(\n  {Start time},\n  RIGHT(\"0\" & FLOOR({Start time} / 3600), 2) & \":\" &\n  RIGHT(\"0\" & ROUNDDOWN(MOD({Start time}, 3600) / 60, 0), 2)\n)",
    "notes": ""
  },
  {
    "name": "Previous record IDs before this record ID",
    "type": "Formula",
    "tags": [
      "Linked records",
      "Strings",
      "Arrays",
      "Sort",
      "Primary keys"
    ],
    "code": "IF(\n  {All record IDs},\n  IF(\n    AND(\n      \"/* Has one ID before it */\",\n      FIND({Record ID}, {All record IDs}) >= LEN({Record ID}) + 1\n    ),\n    LEFT({All record IDs}, FIND({Record ID}, {All record IDs}) - 3)\n  )\n)",
    "notes": ""
  },
  {
    "name": "Extract text between characters (emoji)",
    "type": "Formula",
    "tags": [
      "Strings",
      "Linked records",
      "Automation triggers & utilities",
      "Regex"
    ],
    "code": "IF(\n    FIND(\n      \"👩‍🍳\",\n      {Name}\n    ),\n    TRIM(REGEX_EXTRACT({Name}, '👩‍🍳(.*?)👩‍🍳'))\n )",
    "notes": ""
  },
  {
    "name": "Remove leading and trailing commas \", \" from string",
    "type": "Formula",
    "tags": [
      "Regex",
      "Strings"
    ],
    "code": "REGEX_REPLACE(\n REGEX_REPLACE({Text}, \"^(,\\\\\\\\s*)+\", \"\"),\n \"(,\\\\\\\\s*)+$\", \"\"\n)",
    "notes": ""
  },
  {
    "name": "Count unique (include blanks)",
    "type": "Formula",
    "tags": [
      "Rollups"
    ],
    "code": "COUNTALL(ARRAYUNIQUE(values))",
    "notes": ""
  },
  {
    "name": "Return largest of dates from fields",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "DATETIME_PARSE(\nMAX(\n  IF({Date 1}, VALUE(DATETIME_FORMAT({Date 1}, \"X\")), 0),\n  IF({Date 2}, VALUE(DATETIME_FORMAT({Date 2}, \"X\")), 0),\n  IF({Date 3}, VALUE(DATETIME_FORMAT({Date 3}, \"X\")), 0)\n), \"X\")",
    "notes": ""
  },
  {
    "name": "Delete a single record",
    "type": "Script",
    "tags": [
      "Delete"
    ],
    "code": "let inputConfig = input.config();\n\nlet table = base.getTable(\"Table name\");\n\nawait table.deleteRecordAsync(inputConfig.recordID);",
    "notes": "Use Input configuration for your recordID\n"
  },
  {
    "name": "Padded Autonumber",
    "type": "Formula",
    "tags": [
      "Regex",
      "Strings",
      "Numbers",
      "IDs",
      "Primary keys"
    ],
    "code": "IF(\n  LEN(Autonumber & \"\") < 5,\n  REPT(\"0\", 5 - LEN(Autonumber & \"\")) & Autonumber,\n  Autonumber\n)",
    "notes": "Padded to min 5 numbers, \ne.g. \n231 -> 00231\n123456 -> 123456\n"
  },
  {
    "name": "Delay automation by X seconds",
    "type": "Script",
    "tags": [
      "Utility"
    ],
    "code": "function delay(seconds) {\n    const startTime = Date.now()\n    while (Date.now() - startTime < seconds * 1000)\n        continue\n}\n\ndelay(5)",
    "notes": ""
  },
  {
    "name": "Check if string has trailing space",
    "type": "Formula",
    "tags": [
      "Strings"
    ],
    "code": "IF(\n  RIGHT(Name, 1) = \" \",\n  \"Yes\"\n)",
    "notes": ""
  },
  {
    "name": "Fuzzy compare two strings ignoring spaces and commas",
    "type": "Formula",
    "tags": [
      "Strings"
    ],
    "code": "REGEX_REPLACE({STRING A} & \"\", \"[,\\\\\\\\s]+\", \"\") != REGEX_REPLACE({= STRING B} & \"\", \"[,\\\\\\\\s]+\", \"\"),",
    "notes": ""
  },
  {
    "name": "Date range status (Completed/Current/Future)",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n AND({Start Date}, {End Date}),\n IF(\n  IS_BEFORE({End Date}, TODAY()), \n  \"Completed\",\n  IF(\n    IS_AFTER({Start Date}, TODAY()),\n   \"Future\",\n   \"Current\"\n  )\n )\n)",
    "notes": ""
  },
  {
    "name": "Currency string (£10.00)",
    "type": "Formula",
    "tags": [
      "Currency"
    ],
    "code": "IF(\nLEFT({Total}&\"\",1,1)='-',\n'-',\n'')&'$'&\nIF(\nABS(VALUE({Total}&\"\"))>=1000000,\nINT(ABS(VALUE({Total}&\"\"))/1000000)&','&\nIF(\nINT(MOD(VALUE({Total}&\"\"),1000000)/1000)<100,\nREPT('0',3-LEN(INT(MOD(VALUE({Total}&\"\"),1000000)/1000)&\"\")),\n'')&INT(MOD(VALUE({Total}&\"\"),1000000)/1000)&','&\nIF(\nINT(MOD(VALUE({Total}&\"\"),1000))<100,\nREPT('0',3-LEN(INT(MOD(VALUE({Total}&\"\"),1000))&\"\")),\n'')&INT(MOD(VALUE({Total}&\"\"),1000)),\nIF(\nABS(VALUE({Total}&\"\"))>=1000,\nINT(ABS(VALUE({Total}&\"\"))/1000)&','&\nIF(\nINT(MOD(VALUE({Total}&\"\"),1000))<100,\nREPT('0',3-LEN(INT(MOD(VALUE({Total}&\"\"),1000))&\"\")),\n'')&INT(MOD(VALUE({Total}&\"\"),1000)),\nINT(ABS(VALUE({Total}&\"\")))))&'.'&\nIF(\nLEN(ROUND(MOD(VALUE({Total}&\"\"),1)*100,0)&\"\")<2,\n'0',\n''\n)&\nROUND(MOD(VALUE({Total}&\"\"),1)*100)",
    "notes": ""
  },
  {
    "name": "Create a project in Harvest",
    "type": "Script",
    "tags": [
      "Harvest",
      "API",
      "Fetch",
      "POST"
    ],
    "code": "let inputConfig = input.config();\n\nconst accountId = '123123123'; // Replace with your Harvest account ID\nconst accessToken = '123123123123'; // Replace with your Harvest access token\n\nconst apiUrl = 'https://api.harvestapp.com/v2/projects'; // Updated to projects endpoint\n\n// New project data\nconst newProjectData = {\n    client_id: inputConfig.projectClientHarvestID, // Replace with the Harvest ID of the client\n    name: inputConfig.projectName,\n    code: inputConfig.projectJobCode,\n    is_billable: true,\n    bill_by: \"Tasks\",\n    budget_by: \"project_cost\",\n    starts_on: new Date()\n    // Add other project properties as needed\n};\n\nlet createdProjectId;\n\n// Create a new project in Harvest\ntry {\n  const response = await fetch(apiUrl, {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${accessToken}`,\n      'Harvest-Account-ID': accountId,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify(newProjectData),\n  });\n\n  if (!response.ok) {\n    throw new Error(`Error creating new project in Harvest: ${response.statusText}`);\n  }\n\n  const createdProject = await response.json();\n\n  createdProjectId = createdProject.id;\n\n  console.log('New Project created in Harvest:', createdProject);\n\n} catch (error) {\n  console.error('Error creating new project in Harvest:', error);\n}",
    "notes": ""
  },
  {
    "name": "Send webhook to Make",
    "type": "Script",
    "tags": [
      "Make",
      "Webhook",
      "Fetch"
    ],
    "code": "let inputConfig = input.config();\nlet webhook = 'https://hook.eu2.make.com/…';\nlet call = await fetch(`${webhook}?recordID=${inputConfig.recordID}`);\nconsole.log(call);",
    "notes": ""
  },
  {
    "name": "Unencode a URL encoded string",
    "type": "Formula",
    "tags": [
      "Strings",
      "Utility"
    ],
    "code": "IF(\n  {Encoded},\n  SUBSTITUTE(\n    SUBSTITUTE(\n      SUBSTITUTE(\n        SUBSTITUTE(\n          SUBSTITUTE(\n            SUBSTITUTE(\n              SUBSTITUTE(\n                SUBSTITUTE(\n                  SUBSTITUTE(\n                    SUBSTITUTE(\n                      SUBSTITUTE(\n                        SUBSTITUTE(\n                          SUBSTITUTE(\n                            SUBSTITUTE(\n                              SUBSTITUTE(\n                                SUBSTITUTE(\n                                  SUBSTITUTE(\n                                    {Encoded},\n                                    \"%20\", \" \"\n                                  ),\n                                  \"%2C\", \",\"\n                                ),\n                                \"%3A\", \":\"\n                              ),\n                              \"%2F\", \"/\"\n                            ),\n                            \"%40\", \"@\"\n                          ),\n                          \"%22\", \"\\\\\"\"\n                        ),\n                        \"%7B\", \"{\"\n                      ),\n                      \"%7D\", \"}\"\n                    ),\n                    \"%25\", \"%\"\n                  ),\n                  \"%5B\", \"[\"\n                ),\n                \"%5D\", \"]\"\n              ),\n              \"%26\", \"&\"\n            ),\n            \"%3D\", \"=\"\n          ),\n          \"%3F\", \"?\"\n        ),\n        \"%0A\", \"\\n\"\n      ),\n      \"%28\", \"(\"\n    ),\n    \"%29\", \")\"\n  )\n)",
    "notes": "Helpful when you are building a URL in a formula but want to preview the output as you work on it.\n"
  },
  {
    "name": "Calculate # of overlapping days",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n    AND(\n        {Range 1 (Start date)} <= {Range 2 (Start date)},\n        {Range 1 (End date)} >= {Range 2 (End date)}\n    ),\n    {# days (from Month)},\n    IF(\n        AND(\n            {Range 2 (Start date)} <= {Range 1 (Start date)},\n            {Range 2 (End date)} >= {Range 1 (End date)}\n        ),\n        {# days (from Project periods)},\n        IF(\n            AND(\n                {Range 2 (Start date)} <= {Range 1 (Start date)},\n                {Range 2 (End date)} <= {Range 1 (End date)}\n            ),\n            DATETIME_DIFF({Range 2 (End date)}, {Range 1 (Start date)}, 'days') + 1,\n            IF(\n                AND(\n                    {Range 2 (End date)} >= {Range 1 (End date)},\n                    {Range 2 (Start date)} <= {Range 1 (End date)}\n                ),\n                DATETIME_DIFF({Range 1 (End date)}, {Range 2 (Start date)}, 'days') + 1\n            )\n        )\n    )\n)",
    "notes": ""
  },
  {
    "name": "Compare comma-separated string to linked record",
    "type": "Formula",
    "tags": [
      "Strings",
      "Linked records",
      "Regex"
    ],
    "code": "IF(\n  REGEX_REPLACE({Buckets}, \"[, ]\", \"\") != REGEX_REPLACE({= Buckets}, \"[, ]\", \"\"),\n\"Yes\"\n)",
    "notes": ""
  },
  {
    "name": "Link or unlink to junction",
    "type": "Formula",
    "tags": [
      "Automation triggers & utilities"
    ],
    "code": "IF(\n  OR(\n    {Field: String or select etc} != {Field: Linked record to junction table}\n  ),\n  \"Yes\"\n)",
    "notes": ""
  },
  {
    "name": "Date status (Past/Future/Today)",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "IF(\n  {Date},\n  IF(\n    IS_BEFORE({Date}, TODAY()),\n    \"Past\",\n    IF(\n      IS_AFTER({Date}, TODAY()),\n      \"Future\",\n      \"Today\"\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Previous single record ID from this record in array",
    "type": "Formula",
    "tags": [
      "Lookups"
    ],
    "code": "IF(\n  {All record IDs from a parent},\n  IF(\n    AND(\n      \"/* Has one ID before it */\",\n      FIND({This record ID}, {All record IDs from a parent}) >= 18\n    ),\n    MID({All record IDs from a parent}, FIND({This record ID}, {All record IDs from a parent}) - 19, 17)\n  )\n)",
    "notes": "Assumes that {All record IDs from a parent} is comma-separated with a space, e.g. \nrec5Elj0JybN4NvFh, recMFGrwGr7LfUkDo\n\nIf not, use -17 instead of -19. \n"
  },
  {
    "name": "Financial year (UK) as string",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n    VALUE(DATETIME_FORMAT({Date}, \"MMDD\")) >= 406,\n    DATETIME_FORMAT({Date}, \"YYYY\") & \"/\" & (VALUE(DATETIME_FORMAT({Date}, \"YYYY\")) + 1),\n    (VALUE(DATETIME_FORMAT({Date}, \"YYYY\")) - 1) & \"/\" & DATETIME_FORMAT({Date}, \"YYYY\")\n)",
    "notes": ""
  },
  {
    "name": "Merge arrays and return unique values",
    "type": "Formula",
    "tags": [
      "Arrays",
      "Rollups"
    ],
    "code": "ARRAYUNIQUE(ARRAYFLATTEN(values))",
    "notes": "Must be a rollup of an **array** (lookup or rollup).\n"
  },
  {
    "name": "% through dates",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "SWITCH(\n  {Date status},\n  \"Completed\",\n  1,\n  \"Current\",\n  DAY(TODAY()) / {# days},\n  \"Future\",\n  0\n)",
    "notes": ""
  },
  {
    "name": "Count unique (don't include blanks)",
    "type": "Formula",
    "tags": [
      "Rollups"
    ],
    "code": "COUNTA(ARRAYUNIQUE(values))",
    "notes": ""
  },
  {
    "name": "Sort and slice records",
    "type": "Script",
    "tags": [
      "Arrays",
      "Filter",
      "Sort",
      "Select records"
    ],
    "code": "// Input config\nlet config = input.config();\nlet tableName = config.tableName || \"Freedcamp projects\";\nlet sortField = config.sortField || \"Last refreshed timelogs\";\n\n// Get table\nlet table = base.getTable(tableName);\nlet view = table.getView(\"🔖 Refresh timelogs\");\n\n// Select all records with the target field\nlet query = await view.selectRecordsAsync({\n    fields: [sortField]\n});\n\n// Convert records into array with field values\nlet records = query.records.map(record => {\n    return {\n        id: record.id,\n        sortValue: record.getCellValue(sortField) // could be null/blank or a date\n    };\n});\n\n// Custom sort: \n// 1) null/blank values first\n// 2) then by oldest date\nrecords.sort((a, b) => {\n    if (a.sortValue === null && b.sortValue === null) return 0;\n    if (a.sortValue === null) return -1;\n    if (b.sortValue === null) return 1;\n    return new Date(a.sortValue) - new Date(b.sortValue); // earlier date first\n});\n\n// Slice top 300\nlet topRecords = records.slice(0, 300);\n\n// Collect record IDs\nlet recordIds = topRecords.map(r => r.id);\n\nconsole.log(\"Collected record IDs:\", recordIds);\noutput.set(\"projectRecordIDs\", recordIds);",
    "notes": ""
  },
  {
    "name": "Date range as string",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "IF(\n  AND(\n    {Date 1} != \"\",\n    {Date 2} != \"\"\n  ),\n  DATETIME_FORMAT({Date 1}, \"DD/M/YYYY\") & \n  \" → \" & DATETIME_FORMAT({Date 2}, \"DD/M/YYYY\"),\n  IF(\n    {Date 1} != \"\",\n    DATETIME_FORMAT({Date 1}, \"DD/M/YYYY\"),\n    IF(\n      {Date 2} != \"\",\n      DATETIME_FORMAT({Date 2}, \"DD/M/YYYY\")\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Weeks from this week",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "ROUND((DATETIME_FORMAT({Start date}, 'X') - DATETIME_FORMAT(DATEADD(TODAY(), 1-IF(WEEKDAY(TODAY())=0, 7, WEEKDAY(TODAY())), 'day'), 'X')) / 60 / 60 / 24 / 7)",
    "notes": ""
  },
  {
    "name": "UNIX timestamp as days (Xd)",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "ROUNDDOWN(VALUE(DATETIME_FORMAT({Datetime}, 'X')) / 60 / 60 / 24, 0)",
    "notes": "Very helpful for using an automations' Find Records conditions to check if dates are >= or <= other dates.\n"
  },
  {
    "name": "Compare strings with different types",
    "type": "Formula",
    "tags": [
      "Strings",
      "Regex"
    ],
    "code": "REGEX_REPLACE(SUBSTITUTE({Field 1}, ' ', ''), '[^a-zA-Z0-9]', '') != REGEX_REPLACE(SUBSTITUTE({Field 2}, ' ', ''), '[^a-zA-Z0-9]', '')",
    "notes": ""
  },
  {
    "name": "Currency string (£10)",
    "type": "Formula",
    "tags": [
      "Currency"
    ],
    "code": "\"£\" & IF(LEN(ROUND({Value}, 0) & \"\")<=3,ROUND({Value}, 0) & \"\",IF(LEN(ROUND({Value}, 0) & \"\")<=6,CONCATENATE(LEFT(ROUND({Value}, 0) & \"\",LEN(ROUND({Value}, 0) & \"\")-3),\",\",MID(ROUND({Value}, 0) & \"\",LEN(ROUND({Value}, 0) & \"\")-2,3)),CONCATENATE(LEFT(ROUND({Value}, 0) & \"\",LEN(ROUND({Value}, 0) & \"\")-6),\",\",MID(ROUND({Value}, 0) & \"\",LEN(ROUND({Value}, 0) & \"\")-5,3),\",\",MID(ROUND({Value}, 0) & \"\",LEN(ROUND({Value}, 0) & \"\")-2,3))))",
    "notes": ""
  },
  {
    "name": "Fuzzy compare strings by a-z09",
    "type": "Formula",
    "tags": [
      "Strings"
    ],
    "code": "**Field A#**\n\"#\" & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"a\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"b\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"c\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"d\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"e\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"f\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"g\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"h\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"i\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"j\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"k\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"l\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"m\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"n\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"o\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"p\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"q\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"r\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"s\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"t\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"u\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"v\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"w\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"x\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"y\", \"\"))) & \n(LEN(LOWER({A})) - LEN(SUBSTITUTE(LOWER({A}), \"z\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"0\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"1\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"2\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"3\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"4\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"5\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"6\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"7\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"8\", \"\"))) & \n(LEN({A}) - LEN(SUBSTITUTE({A}, \"9\", \"\")))\n\n**Field B#**\n\"#\" & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"a\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"b\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"c\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"d\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"e\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"f\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"g\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"h\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"i\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"j\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"k\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"l\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"m\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"n\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"o\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"p\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"q\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"r\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"s\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"t\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"u\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"v\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"w\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"x\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"y\", \"\"))) & \n(LEN(LOWER({B})) - LEN(SUBSTITUTE(LOWER({B}), \"z\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"0\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"1\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"2\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"3\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"4\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"5\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"6\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"7\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"8\", \"\"))) & \n(LEN({B}) - LEN(SUBSTITUTE({B}, \"9\", \"\")))\n\n**Compare Fields**\nIF(\n  {A #} != {B #},\n  \"Mismatch\",\n  \"Match\"\n)",
    "notes": "Used as an approximation to determine if two strings are the same with an alphabet comparison, e.g. two sentences, addresses etc.\n"
  },
  {
    "name": "Get all projects from Harvest",
    "type": "Script",
    "tags": [
      "Harvest",
      "API",
      "Fetch",
      "Loop",
      "GET"
    ],
    "code": "const accountId = '123123'; // Replace with your Harvest account ID\nconst accessToken = '123123123123'; // Replace with your Harvest access token\n\nconst apiUrl = 'https://api.harvestapp.com/v2/projects';\n\nlet harvestProjects;\n\n// Fetch all projects from Harvest\nfetch(apiUrl, {\n  method: 'GET',\n  headers: {\n    'Authorization': `Bearer ${accessToken}`,\n    'Harvest-Account-ID': accountId,\n    'Content-Type': 'application/json',\n  },\n})\n  .then(response => {\n    if (!response.ok) {\n      throw new Error(`Error fetching projects from Harvest: ${response.statusText}`);\n    }\n    return response.json();\n  })\n  .then(data => {\n    console.log('List of Projects:', data.projects);\n    harvestProjects = data.projects;\n  })\n  .catch(error => {\n    console.error('Error fetching projects from Harvest:', error);\n  });",
    "notes": ""
  },
  {
    "name": "Count occurrences of string in list of strings",
    "type": "Formula",
    "tags": [
      "Linked records"
    ],
    "code": "((FIND({Invoice record ID}, {Client invoice record IDs list} & \"\") - 1)/ LEN({Invoice record ID})) + 1",
    "notes": ""
  },
  {
    "name": "Batch delete records",
    "type": "Script",
    "tags": [
      "Delete",
      "Batch",
      "Arrays"
    ],
    "code": "let inputConfig = input.config(); \nlet table = base.getTable(\"Table\"); \n\nwhile (inputConfig.recordIDs.length > 0) { \n\tawait table.deleteRecordsAsync(inputConfig.recordIDs.slice(0, 50)); \n\tinputConfig.recordIDs = inputConfig.recordIDs.slice(50); \n}",
    "notes": ""
  },
  {
    "name": "Filter selectRecordsAsync()",
    "type": "Script",
    "tags": [
      "Filter",
      "Select records"
    ],
    "code": "let periodsQuery = await periodsViews.selectRecordsAsync({\n    fields: [\"Weeks from This Week\"]\n});\n\nconst rangeOfPeriodsIDs = periodsQuery.records\n    .filter(record =>  \n        record.getCellValue('Weeks from This Week') >= inputConfig.StartDateWeeksFromThisWeek &&\n        record.getCellValue('Weeks from This Week') <= inputConfig.EndDateWeeksFromThisWeek\n        // console.log(record.getCellValue('Weeks from This Week'))\n        // return \"\"\n    )\n    .map(record => \n        // get cell value of Period column\n        record.id\n    );\n\n\n**Example 2**\n\nconst existingPeriodsIDs = scheduledQuery.records\n    .filter(record => \n        record.getCellValueAsString('Project') == inputConfig.ProjectName.toString() && \n        record.getCellValueAsString('Member') == inputConfig.MemberName.toString()\n    )\n    .map(record => \n        // get cell value of Period column\n        record.getCellValue('Period')[0].id\n    );",
    "notes": ""
  },
  {
    "name": "# days from today (Single date)",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n  {Date},\n  DATETIME_DIFF({Date}, TODAY(), 'days')\n)",
    "notes": ""
  },
  {
    "name": "First workday of this month",
    "type": "Formula",
    "tags": [
      "Dates & times",
      "Utility"
    ],
    "code": "SWITCH(\n  WEEKDAY(DATETIME_PARSE(DATETIME_FORMAT(TODAY(), \"YYYY-MM\") & \"-01\")),\n  0, DATEADD(DATETIME_PARSE(DATETIME_FORMAT(TODAY(), \"YYYY-MM\") & \"-01\"), 1, 'days'),\n  6, DATEADD(DATETIME_PARSE(DATETIME_FORMAT(TODAY(), \"YYYY-MM\") & \"-01\"), 2, 'days'),\n  DATETIME_PARSE(DATETIME_FORMAT(TODAY(), \"YYYY-MM\") & \"-01\")\n)",
    "notes": ""
  },
  {
    "name": "Random 4 digit number from record ID",
    "type": "Formula",
    "tags": [
      "Strings",
      "Numbers",
      "Placeholder",
      "Random"
    ],
    "code": "VALUE(\n  CONCATENATE(\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 3, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 2, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 1, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()), 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Extract value from key in JSON",
    "type": "Formula",
    "tags": [
      "Strings",
      "Utility"
    ],
    "code": "MID(\n  {🤖 Data},\n  FIND('\"jsonKey\":\"', {🤖 Data}) + LEN('\"jsonKey\":\"'),\n  FIND('\"', {🤖 Data}, FIND('\"jsonKey\":\"', {🤖 Data}) + LEN('\"jsonKey\":\"')) - (FIND('\"jsonKey\":\"', {🤖 Data}) + LEN('\"jsonKey\":\"'))\n)",
    "notes": "e.g. \n{\"name\":\"Alice\",\"email\":\"alice@example.com\"}\nreturns\nalice@example.com\n"
  },
  {
    "name": "Regex subtract one list from another ",
    "type": "Formula",
    "tags": [
      "Strings",
      "Regex"
    ],
    "code": "IF(\n  {Include},\n  IF(\n    {Exclude} = \"\",\n    {Include},\n    REGEX_REPLACE(\n      {Include},\n      \"(?:\\\\\\\\b\" & SUBSTITUTE({Exclude}, \", \", \"\\\\\\\\b|\\\\\\\\b\") & \"\\\\\\\\b)\",\n      \"\"\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Merge or Union two comma-separated arrays",
    "type": "Formula",
    "tags": [
      "Strings",
      "Arrays",
      "Regex"
    ],
    "code": "IF( {Linked Category years}, IF( {Backfilled category years}, {Linked Category years} & IF( TRIM( REGEX_REPLACE( {Backfilled category years}, \"(?:\\\\\\\\b\" & SUBSTITUTE({Linked Category years}, \", \", \"\\\\\\\\b|\\\\\\\\b\") & \"\\\\\\\\b(?:, )?)\", \"\" ) ) = \"\", \"\", \", \" & TRIM( REGEX_REPLACE( {Backfilled category years}, \"(?:\\\\\\\\b\" & SUBSTITUTE({Linked Category years}, \", \", \"\\\\\\\\b|\\\\\\\\b\") & \"\\\\\\\\b(?:, )?)\", \"\" ) ) ), {Linked Category years} ), {Backfilled category years} )",
    "notes": ""
  },
  {
    "name": "Update record with all of another record's field values",
    "type": "Script",
    "tags": [
      "Update",
      "Duplicate"
    ],
    "code": "let inputConfig = input.config();\n\n// Initialize the table object\nlet table = base.getTable(\"Law versions\");\n\n// Retrieve the specific record by its ID using selectRecordAsync\nlet previousRecord = await table.selectRecordAsync(inputConfig.previousRecordID);\n\n// Ensure the record exists\nif (!previousRecord) {\n    throw new Error(`Record with ID ${inputConfig.previousRecordID} not found.`);\n}\n\n// Initialize an object to hold the fields to copy\nlet fieldsToCopy = {};\n\n// Loop over the table's fields\nfor (let field of table.fields) {\n    let fieldName = field.name;\n    // Check if the field name does not start with '🔴' and is not a computed field\n    if (!fieldName.startsWith('🔴') && !field.isComputed) {\n        // Use getCellValue to fetch the value from the previousRecord for this field\n        let fieldValue = previousRecord.getCellValue(fieldName);\n        // Check if the field value is not undefined or null before adding it to fieldsToCopy\n        if (fieldValue !== undefined && fieldValue !== null) {\n            fieldsToCopy[fieldName] = fieldValue;\n        }\n    }\n}\n\nconsole.log(fieldsToCopy);\n\n// Update the existing record with the fields to copy\nawait table.updateRecordAsync(inputConfig.newRecordID, fieldsToCopy);",
    "notes": ""
  },
  {
    "name": "Extract letters from string",
    "type": "Formula",
    "tags": [
      "Regex"
    ],
    "code": "REGEX_REPLACE({Your Field}, \"[^A-Za-z]\", \"\")",
    "notes": ""
  },
  {
    "name": "Sanitise email addresses",
    "type": "Formula",
    "tags": [
      "Strings"
    ],
    "code": "LOWER(REGEX_REPLACE(TRIM(Email), \"[^a-zA-Z0-9@._%+-]\", \"\"))",
    "notes": ""
  },
  {
    "name": "Get all clients from Harvest",
    "type": "Script",
    "tags": [
      "Harvest",
      "API",
      "Fetch",
      "GET"
    ],
    "code": "const accountId = '712104'; // Replace with your Harvest account ID\nconst accessToken = 'APIKEY'; // Replace with your Harvest access token\n\nconst apiUrl = `https://api.harvestapp.com/v2/clients`;\n\n// Fetch Harvest clients\nlet harvestClients;\ntry {\n  const response = await fetch(apiUrl, {\n    method: 'GET',\n    headers: {\n      'Authorization': `Bearer ${accessToken}`,\n      'Harvest-Account-ID': accountId,\n      'Content-Type': 'application/json',\n    },\n  });\n\n  if (!response.ok) {\n    throw new Error(`Error fetching Harvest clients: ${response.statusText}`);\n  }\n\n  const data = await response.json();\n  harvestClients = data.clients;\n  console.log('List of Clients:', harvestClients);\n} catch (error) {\n  console.error('Error fetching clients:', error);\n}\n\n// Fetch Airtable clients\nlet clientsTable = base.getTable(\"Clients\");\nlet allClientsRecords = await clientsTable.selectRecordsAsync({\n  fields: [\"Name\", \"Harvest ID\", \"Harvest name\", \"Address\"]\n});\n\n// Convert Airtable records to an array\nlet allClients = allClientsRecords.records.map(record => ({\n  id: record.id,\n  name: record.getCellValueAsString(\"Name\").toLowerCase(),\n  harvestID: record.getCellValueAsString(\"Harvest ID\"),\n  harvestName: record.getCellValueAsString(\"Harvest name\"),\n  address: record.getCellValueAsString(\"Address\"),\n}));\n\nlet recordsToUpdate = [];\n\n// Loop through the Harvest clients\nharvestClients.forEach(harvestClient => {\n  const harvestClientName = harvestClient.name;\n  \n  // Find a matching Airtable client by name\n  const matchingAirtableClient = allClients.find(airtableClient => {\n    // Use a case-insensitive comparison for names\n    return airtableClient.name === harvestClientName.toLowerCase();\n  });\n\n  if (matchingAirtableClient) {\n\n    recordsToUpdate.push({\n        id: matchingAirtableClient.id,\n        fields: {\n          \"Harvest ID\": harvestClient.id.toString(), // Assuming 'id' is a string in Airtable\n          \"Harvest name\": harvestClientName,\n          \"Address\": harvestClient.address\n        }\n    });\n\n  }\n});\n\nconsole.log(recordsToUpdate);\n\n// Update records with Hourly rate = 0\nwhile (recordsToUpdate.length > 0) {\n    await clientsTable.updateRecordsAsync(recordsToUpdate.slice(0, 50));\n    recordsToUpdate = recordsToUpdate.slice(50);\n}",
    "notes": ""
  },
  {
    "name": "Update custom field in Asana",
    "type": "Script",
    "tags": [
      "Update",
      "API",
      "Asana"
    ],
    "code": "const key = \"a\";\n\nlet inputConfig = input.config();\nlet asanaFinanceTaskID = inputConfig.asanaFinanceTaskID.toString();\nlet teamNameString = inputConfig.teamNameString.toString();\n\n// Update custom field on the Asana task\nconst updateAsanaTaskCustomField = (asanaGid) => {\n  const options = {\n    method: 'PUT',\n    headers: {\n      accept: 'application/json',\n      'content-type': 'application/json',\n      authorization: 'Bearer ' + key,\n    },\n    body: JSON.stringify({\n      data: {\n        custom_fields: {\n          '1206877936413068': teamNameString, // Asana finance URL\n        },\n      },\n    }),\n  };\n\n  return fetch('https://app.asana.com/api/1.0/tasks/' + asanaGid, options)\n    .then(response => response.json())\n    .then(response => {\n      console.log(response);\n      return asanaGid; // Return the GID\n    })\n    .catch(err => {\n      console.error(err);\n      return null;\n    });\n};\n\n// Execute the update\nupdateAsanaTaskCustomField(asanaFinanceTaskID)\n  .then(asanaGid => {\n    if (asanaGid) {\n      console.log('Asana task updated successfully:', asanaGid);\n    } else {\n      throw new Error('Failed to update the Asana task');\n    }\n  })\n  .catch(err => console.error(err));",
    "notes": ""
  },
  {
    "name": "Bulk assign users to Harvest",
    "type": "Script",
    "tags": [
      "Harvest",
      "API",
      "Fetch",
      "POST"
    ],
    "code": "let inputConfig = input.config();\n\nif(\n  inputConfig.activeUserIDs.length > 0\n)\n\n  {\n\n  // Assuming projectID is the ID of the project created in Script 1\n  const projectID = inputConfig.projectID;\n\n  // Assuming activeUserIDs is the array of active user IDs from Script 2\n  // const activeUserIDs = ['user_id_1', 'user_id_2', 'user_id_3', /* ... */];\n  const activeUserIDs = inputConfig.activeUserIDs;\n\n  // Harvest API endpoint for assigning a user to a project\n  const assignUserEndpoint = 'https://api.harvestapp.com/v2/projects/' + inputConfig.projectID + '/user_assignments';\n\n  // Harvest API access toke\n  const accountId = 'ID'; // Replace with your Harvest account ID\n  const accessToken = 'KEY'; // Replace with your Harvest access token\n\n  // Function to assign a user to the project\n  function assignUserToProject(userID) {\n    const payload = {\n      method: 'post',\n      contentType: 'application/json',\n      headers: {\n        'Authorization': `Bearer ${accessToken}`,\n        'Harvest-Account-ID': accountId,\n        'Content-Type': 'application/json',\n        'Accept': 'application/json',\n      },\n      body: JSON.stringify({\n        project_id: projectID,\n        user_id: userID\n      }),\n    };\n\n    return fetch(assignUserEndpoint, payload)\n      .then(response => response.json())\n      .then(responseData => {\n        console.log(`User ${userID} assigned to project. Response:`, responseData);\n      })\n      .catch(error => {\n        console.error(`Error assigning user ${userID} to project:`, error.message);\n      });\n  }\n\n  function assignAllUsersToProject() {\n    // Map each user assignment promise and wait for all to complete\n    Promise.all(activeUserIDs.map(userID => assignUserToProject(userID)))\n      .then(() => {\n        console.log('All user assignments completed.');\n      })\n      .catch(error => {\n        console.error('Error in user assignments:', error.message);\n      });\n  }\n\n  assignAllUsersToProject();\n\n}",
    "notes": ""
  },
  {
    "name": "Is today?",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n  Date,\n  IF(\n    DATETIME_FORMAT({Date}, \"DD-MM-YYYY\") = DATETIME_FORMAT(TODAY(), \"DD-MM-YYYY\"),\n    \"Yes\"\n  )\n)",
    "notes": ""
  },
  {
    "name": "Merge arrays and return unique count",
    "type": "Formula",
    "tags": [
      "Arrays",
      "Rollups"
    ],
    "code": "COUNTALL(ARRAYUNIQUE(ARRAYFLATTEN(values)), \", \")",
    "notes": "Must be a rollup of an **array** (lookup or rollup).\n"
  },
  {
    "name": "Select records by set of IDs",
    "type": "Script",
    "tags": [
      "Loop",
      "Select records"
    ],
    "code": "let table = base.getTable(\"Table\");\nlet records = await budgetItemsTable.selectRecordsAsync(\n    {\n        fields: [\n            \"Title\"\n        ],\n        recordIds: inputConfig.recordIds\n    }\n);",
    "notes": ""
  },
  {
    "name": "Random 1 digit number",
    "type": "Formula",
    "tags": [
      "Random",
      "Numbers",
      "Strings",
      "Placeholder"
    ],
    "code": "VALUE(\n  CONCATENATE(\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 1, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Combine records IDs from one or more Find record actions and set as output",
    "type": "Script",
    "tags": [
      "Arrays"
    ],
    "code": "let inputConfig = input.config();\n\nlet firstRecordIDs = inputConfig.firstRecordIDs || [];\nlet secondRecordIDs = inputConfig.secondRecordIDs || [];\n\n// Merge and filter unique values\nlet mergedRecordIDs = [...new Set([...firstRecordIDs, ...secondRecordIDs])];\n\n// Log for debugging\nconsole.log(\"Merged Unique Record IDs:\", mergedRecordIDs);\n\n// Set as output\noutput.set(\"mergedRecordIDs\", mergedRecordIDs);",
    "notes": "Great for setting an output for use in Airtable's Repeating Groups.\n"
  },
  {
    "name": "Batch 50 max",
    "type": "Script",
    "tags": [
      "Batch",
      "Create",
      "Update",
      "Arrays"
    ],
    "code": "while (newPSMs.length > 0) {\n  await projectSpecialismMonthsTable.createRecordsAsync(newPSMs.slice(0, 50));\n  newPSMs = newPSMs.slice(50);\n}",
    "notes": ""
  },
  {
    "name": "Years from this year",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "VALUE(DATETIME_FORMAT({Date}, \"YYYY\")) - VALUE(DATETIME_FORMAT(TODAY(), \"YYYY\"))",
    "notes": ""
  },
  {
    "name": "TOTODAY()",
    "type": "Formula",
    "tags": [
      "Dates"
    ],
    "code": "IF(\n  {Date},\n  IF(\n    DATETIME_DIFF(TODAY(), {Date}, 'days') = 0,\n    \"Today\",\n    IF(\n      DATETIME_DIFF(TODAY(), {Date}, 'days') = 1,\n      \"1 day ago\",\n      IF(\n        DATETIME_DIFF(TODAY(), {Date}, 'days') < 7,\n        DATETIME_DIFF(TODAY(), {Date}, 'days') & \" days ago\",\n        IF(\n          DATETIME_DIFF(TODAY(), {Date}, 'weeks') < 5,\n          DATETIME_DIFF(TODAY(), {Date}, 'weeks') & \" week\" & IF(DATETIME_DIFF(TODAY(), {Date}, 'weeks') > 1, \"s\", \"\") & \" ago\",\n          DATETIME_DIFF(TODAY(), {Date}, 'months') & \" month\" & IF(DATETIME_DIFF(TODAY(), {Date}, 'months') > 1, \"s\", \"\") & \" ago\"\n        )\n      )\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Random 3 digit number from record ID",
    "type": "Formula",
    "tags": [
      "Strings",
      "Numbers",
      "Placeholder",
      "Random"
    ],
    "code": "VALUE(\n  CONCATENATE(\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 2, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 1, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()), 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    )\n  )\n)",
    "notes": ""
  },
  {
    "name": "Extract numbers from string",
    "type": "Formula",
    "tags": [
      "Numbers",
      "Strings"
    ],
    "code": "IF(\n{Text},\nVALUE(REGEX_EXTRACT({Text}, \"\\\\\\\\d+\"))\n)",
    "notes": ""
  },
  {
    "name": "Format date as month (from Months sync)",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n{Start date}, \nDATETIME_FORMAT({Start date}, \"YY-MM MMM\")\n)",
    "notes": ""
  },
  {
    "name": "Find record ID position in array of record IDs",
    "type": "Formula",
    "tags": [
      "Linked records",
      "IDs"
    ],
    "code": "IF(\n  {Parent's list of record IDs},\n  ((FIND(\n    RECORD_ID(),\n    {Parent's list of record IDs}\n  ) - 1) / 18) + 1\n)",
    "notes": "### Purpose\nThis formula finds the position (item number) of the current record's ID within a comma-separated list of record IDs stored in a field called`{Parent's list of record IDs}`.\n### How It Works\n1. `IF({Parent's list of record IDs}, ...)`\n2. First checks if the `{Parent's list of record IDs}` field has a value. If it's empty, the formula returns nothing.\n3. `RECORD_ID()`\n4. Gets the unique ID of the current record (e.g., `recABC123xyz4567`).\n5. `FIND(RECORD_ID(), {Parent's list of record IDs})`\n6. Finds the character position where the current record's ID starts within the parent's list string.\n7. `(...) - 1) / 18) + 1`\n    - This arithmetic converts the character position into an item number:Subtracts 1 to make the calculation zero-indexed\n    - Divides by 18 (the length of a record ID, which is 17 characters, plus 1 for the comma separator)\n    - Adds 1 to convert back to a 1-indexed position\n### Example\nIf `{Parent's list of record IDs}` contains:\n`recABC123xyz4567,recDEF456uvw7890,recGHI789rst0123`\n- The 1st record ID starts at character 1 → returns 1\n- The 2nd record ID starts at character 19 → returns 2\n- The 3rd record ID starts at character 37 → returns 3\n### Important Note\nThis formula assumes record IDs are separated by commas (with no spaces).\n"
  },
  {
    "name": "Remove trailing \", \" comma and space at end of string",
    "type": "Formula",
    "tags": [
      "Strings",
      "Regex"
    ],
    "code": "REGEX_REPLACE({Text}, \"(, )$| $\", \"\")",
    "notes": ""
  },
  {
    "name": "Extract text after last comma in string",
    "type": "Formula",
    "tags": [
      "Strings"
    ],
    "code": "IF(\n FIND(\",\", {Field} & \"\"),\n TRIM(\n  REGEX_EXTRACT(\n   {Field} & \"\",\n   \",([^,]+)$\"\n  )\n )\n)",
    "notes": "One, Two, Three\n=\nThree\n"
  },
  {
    "name": "Months from this month",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "(12 * (DATETIME_FORMAT({Start Date}, 'YY') - DATETIME_FORMAT(TODAY(), 'YY'))) - (DATETIME_FORMAT(TODAY(), 'MM') - DATETIME_FORMAT({Start Date}, 'MM'))",
    "notes": ""
  },
  {
    "name": "Diff in minutes",
    "type": "Formula",
    "tags": [
      "Dates & times"
    ],
    "code": "IF(\n  AND(\n    {Start (datetime)},\n    {End (datetime)}\n  ),\n  MAX(\n    DATETIME_DIFF({End (datetime)}, {Start (datetime)}, 'minutes'),\n    0\n  )\n)",
    "notes": ""
  },
  {
    "name": "Random 2 digit number from record ID",
    "type": "Formula",
    "tags": [
      "Strings",
      "Numbers",
      "Placeholder",
      "Random"
    ],
    "code": "VALUE(\n  CONCATENATE(\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()) - 1, 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    ),\n    SWITCH(\n      MID(UPPER(RECORD_ID()), LEN(RECORD_ID()), 1),\n      \"A\", 1, \"B\", 1,\n      \"C\", 2, \"D\", 2,\n      \"E\", 3, \"F\", 3,\n      \"G\", 4, \"H\", 4,\n      \"I\", 5, \"J\", 5,\n      \"K\", 6, \"L\", 6,\n      \"M\", 7, \"N\", 7,\n      \"O\", 8, \"P\", 8,\n      \"Q\", 9, \"R\", 9,\n      \"S\", 1, \"T\", 1,\n      \"U\", 2, \"V\", 2,\n      \"W\", 3, \"X\", 3,\n      \"Y\", 4, \"Z\", 4,\n      \"1\"\n    )\n  )\n)",
    "notes": ""
  }
]