10

I have been trying to create a regular expressions pattern that matches any reference in any Excel formula, including absolute, relative, and external references. I need to return the entire reference, including the worksheet and workbook name.

I haven't been able to find exhaustive documentation about Excel A1-notation, but with a lot of testing I have determined the following:

  • Formulas are preceded with an equal sign "="
  • Strings within formulas are enclosed in double quotes and need to be removed before looking for real references, otherwise =A1&"A1" would break regex
  • Worksheet names can be up to 31 characters long, excluding \ / ? * [ ] :
  • Worksheet names in external references must be succeeded with bang =Sheet1!A1
  • Workbook names in external references must be enclosed in square brackets =[Book1.xlsx]Sheet1!A1
  • Workbook paths, which Excel adds if a reference is to a range in a closed workbook, are always enclosed in single quotes and to the left of the brackets for the workbook name 'C:\[Book1.xlsx]Sheet1'!A1
  • Some characters (non-breaking space, for example) cause Excel to enclose the workbook and worksheet name in an external reference in single quotes, but I don't know specifically which characters ='[Book 1.xlsx]Sheet 1'!A1
  • Even if R1C1-notation is enabled, Range.Formula still returns references in A1-notation. Range.FormulaR1C1 returns references in R1C1 notation.
  • 3D reference style allows a range of sheet names on one workbook =SUM([Book5]Sheet1:Sheet3!A1)
  • Named ranges can be specified in formulas:
    • The first character of a name must be a letter, an underscore character (_), or a backslash (\). Remaining characters in the name can be letters, numbers, periods, and underscore characters.
    • You cannot use the uppercase and lowercase characters "C", "c", "R", or "r" as a defined name, because they are all used as a shorthand for selecting a row or column for the currently selected cell when you enter them in a Name or Go To text box.
    • Names cannot be the same as a cell reference, such as Z$100 or R1C1.
    • Spaces are not allowed as part of a name.
    • A name can be up to 255 characters in length.
    • Names can contain uppercase and lowercase letters. Excel does not distinguish between uppercase and lowercase characters in names.

Here is what I came up with wrapped in a VBA procedure for testing. I updated the code to handle names as well:

Sub ReturnFormulaReferences()

    Dim objRegExp As New VBScript_RegExp_55.RegExp
    Dim objCell As Range
    Dim objStringMatches As Object
    Dim objReferenceMatches As Object
    Dim objMatch As Object
    Dim intReferenceCount As Integer
    Dim intIndex As Integer
    Dim booIsReference As Boolean
    Dim objName As Name
    Dim booNameFound As Boolean

    With objRegExp
        .MultiLine = True
        .Global = True
        .IgnoreCase = True
    End With

    For Each objCell In Selection.Cells
        If Left(objCell.Formula, 1) = "=" Then

            objRegExp.Pattern = "\"".*\"""
            Set objStringMatches = objRegExp.Execute(objCell.Formula)

            objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
            & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
            & "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
            & "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
            & "|[a-z]{1,3}\:[a-z]{1,3}" _
            & "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
            & "|[0-9]{1,7}\:[0-9]{1,7}" _
            & "|[a-z_\\][a-z0-9_\.]{0,254})"
            Set objReferenceMatches = objRegExp.Execute(objCell.Formula)

            intReferenceCount = 0
            For Each objMatch In objReferenceMatches
                intReferenceCount = intReferenceCount + 1
            Next

            Debug.Print objCell.Formula
            For intIndex = intReferenceCount - 1 To 0 Step -1
                booIsReference = True
                For Each objMatch In objStringMatches
                    If objReferenceMatches(intIndex).FirstIndex > objMatch.FirstIndex _
                    And objReferenceMatches(intIndex).FirstIndex < objMatch.FirstIndex + objMatch.Length Then
                        booIsReference = False
                        Exit For
                    End If
                Next

                If booIsReference Then
                    objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                    & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
                    & "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
                    & "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
                    & "|[a-z]{1,3}\:[a-z]{1,3}" _
                    & "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
                    & "|[0-9]{1,7}\:[0-9]{1,7})"
                    If Not objRegExp.Test(objReferenceMatches(intIndex).Value) Then 'reference is not A1
                        objRegExp.Pattern = "^(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                        & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)" _
                        & "[a-z_\\][a-z0-9_\.]{0,254}$"
                        If Not objRegExp.Test(objReferenceMatches(intIndex).Value) Then 'name is not external
                            booNameFound = False
                            For Each objName In objCell.Worksheet.Parent.Names
                                If objReferenceMatches(intIndex).Value = objName.Name Then
                                    booNameFound = True
                                    Exit For
                                End If
                            Next
                            If Not booNameFound Then
                                objRegExp.Pattern = "^(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
                                & "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)"
                                For Each objName In objCell.Worksheet.Names
                                    If objReferenceMatches(intIndex).Value = objRegExp.Replace(objName.Name, "") Then
                                        booNameFound = True
                                        Exit For
                                    End If
                                Next
                            End If
                            booIsReference = booNameFound
                        End If
                    End If
                End If

                If booIsReference Then
                    Debug.Print "  " & objReferenceMatches(intIndex).Value _
                    & " (" & objReferenceMatches(intIndex).FirstIndex & ", " _
                    & objReferenceMatches(intIndex).Length & ")"
                End If
            Next intIndex
            Debug.Print

        End If
    Next

    Set objRegExp = Nothing
    Set objStringMatches = Nothing
    Set objReferenceMatches = Nothing
    Set objMatch = Nothing
    Set objCell = Nothing
    Set objName = Nothing

End Sub

Can anyone break or improve this? Without exhaustive documentation on Excel's formula syntax it is difficult to know if this is correct.

Thanks!

Ben McCormack
  • 32,086
  • 48
  • 148
  • 223
Kuyenda
  • 4,529
  • 11
  • 46
  • 64
  • 1
    My regex is rusty so I can't answer your direct question. However, don't forget that functions in formulas can return references, and of course defined names might refer to references. If this matters to you, obviously a regex approach is doomed. And actually parsing the formula might be easier in any case. You might find this (and the MSDN link within it) to be helpful: http://www.dailydoseofexcel.com/archives/2009/12/05/formula-tokenizer/ – jtolle Dec 13 '09 at 23:11
  • Yup, names matter, and they definitely break my code. I have, however, figured out how to isolate strings in any formula from other references. I might still be able to pull this off. I'll edit the question when I figure it out. Thanks! – Kuyenda Dec 14 '09 at 00:58
  • Good news. The syntax for names is specified in the help documentation. I'll add that to my question. – Kuyenda Dec 14 '09 at 01:01
  • jtoole, that link is extremely helpful. Thank you. FYI, I am only using RegExp.Execute to parse the formula. – Kuyenda Dec 14 '09 at 01:20
  • I don't think any regex is going to work for you, if you are doing this for some kind of auditing purpose (as in seeing which cells are used by which other cells). If that is the case, I highly recommend that you look at one of the free or commercial tools that exist for this. At the very least look at the methods on the Range object that tell you precedents and dependents. – jtolle Dec 14 '09 at 02:41
  • `Range.Precedents` and `Range.Dependents` returns the union of all the precedents or dependents as a range object. I'm not using regex alone. I am using it to parse out potential references for testing. The regex just makes the parsing easier. – Kuyenda Dec 14 '09 at 04:06
  • Updated code. Seems to be working correctly even with named ranges. – Kuyenda Dec 14 '09 at 05:23
  • Do you need to account for the use of `INDIRECT()`? For example, you could use `=INDIRECT(CONCATENATE("A",ROW(B2)))` which resolves to the same thing as `=A2`. I use that sometimes when working with the column identifier in something like a `VLOOKUP()`. – Ben McCormack Dec 14 '09 at 18:46
  • It sounds like you're trying to identify every subexpression in an Excel formula that refers to a range. I'm still not clear why you need to do that, but I wouldn't use regex for this regardless. To do this in general you need to *parse* the formula, and *evaluate* the subexpressions, not just identify text that matches some pattern. INDIRECT() isn't the only built-in function that returns references, and UDFs can be named anything and return references. Of course, Excel already parses and evaluates the formula... – jtolle Dec 14 '09 at 20:23
  • I am parsing the formula and evaluating anything that looks like a reference. I am using this method in a procedure that "relocates" formulas (formula moves but points to the same cells as before). For example, if a formula is being relocated to a new location within the same sheet, I don't need to do anything, if it is being moved to another worksheet, add the sheet name, if it is being moved to another workbook, add the book name. Is there a way to tap into Excel's native parsing? I haven't found one. – Kuyenda Dec 17 '09 at 15:40
  • In this specific case, I actually think Excel will do the right thing with literal references if you cut (not copy) and paste them. That's what I meant about Excel already parsing/evaluating formulas - not that you would get the parse tree from Excel, but that you could use the fact that Excel already knows what references are in a formula, and let it do the work for you if possible. If you can't set things up so that Excel's built-in stuff can handle them and decide you must parse formulas, I still wouldn't use regex for that purpose. You're better off building/generating a real parser. – jtolle Dec 17 '09 at 21:51
  • Did a little testing. That Cut + Paste looks like the functionality that I am trying to replicate. The problem is, I don't know if it helps me. I don't know how I would utilize cut programmaticly, because I have to leave the cells at the origin untouched. Thanks for the tip though! – Kuyenda Dec 17 '09 at 22:55
  • 1
    Record the formula text and location, do the cut-paste, and then restore the formula? Best of luck. – jtolle Dec 17 '09 at 23:35
  • Yup, that did it. I know it doesn't answer the regex question, but, whomever is looking for a regex solution will still find this post useful as a reference. Who knows, maybe some MS MVP will give up the secret recipe. – Kuyenda Dec 18 '09 at 00:25
  • Here's an even easier way to get at what you're wanting to do. 1) Do a find-replace on the cell in question to turn "=" into "#". 2) Copy the value of that cell (which is now just a string, not a formula) and paste it to wherever you want. 3) Do a find-replace on both the original cell and the destination cell to convert the #'s back into ='s. – Chris Stocking Jul 29 '15 at 02:05

4 Answers4

4

I'm a few years late here, but I was looking for something similar and so dug into this. The main pattern you use is this:

objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
& "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
& "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
& "|\$[a-z]{1,3}\:\$[a-z]{1,3}" _
& "|[a-z]{1,3}\:[a-z]{1,3}" _
& "|\$[0-9]{1,7}\:\$[0-9]{1,7}" _
& "|[0-9]{1,7}\:[0-9]{1,7}" _
& "|[a-z_\\][a-z0-9_\.]{0,254})"

Basically you have six alternatives for a range reference (lines 3-8), any of which will produce a match by itself, with two alternatives for an optional filename/sheet name prefix (lines 1-2).

For the two prefix alternatives, the only difference is that the first is wrapped in single quotes, with an extra dot star after the initial quote. These single quotes occur mainly when there is a space in a sheet name. The purpose of the dot star, matching unconstrained text after an initial single quote, is unclear and it appears to create problems. I'll discuss those problems below. Besides that the two alternative prefixes are the same, and I'll refer to them collectively as the Optional External Prefix (OEP).

The OEP has its own two optional prefixes (the same in either alternative). The first is for the workbook name, an open-ended dot star in brackets.

(\[.*\])? 

The second is for a "3D" cell reference, with two sheet names separated by a colon; it is the initial sheet name including the colon. The pattern here is a negated character class allowing up to 31 characters of anything except forward slash, back slash, question mark, asterisk, brackets, or colon, followed by a colon:

([^\:\\\/\?\*\[\]]{1,31}\:)?

Finally for the OEP is its only required part: a sheet name, same as the optional sheet name but with no colon. The effect is (if these all worked correctly) that the required sheet name will match if it can, and then only if there is a 3d reference or additional prior bracketed text will its optional prefixes also match.

Issues with the Workbook/Sheet name prefix: First, the dot star at the beginning of the first line is over-inclusive. Similarly, the negated character class for the sheet name appears to need additional characters including parens, comma, plus, minus, equals, and bang. Otherwise, extra material is interpreted as part of the sheet name. On my testing, this overinclusion happened with any of these:

=SUM(Sheet1!A1,Sheet2!A2)
=Sheet1!A1+Sheet2!A2
=Sheet1!A1-Sheet2!A2

Sheet names can include some of these characters, so accounting for that would require some additional measure. For instance, a sheet could be named "(Sheet1)", giving an odd formula like:

=SUM('(Sheet1)'!A1:A2)

You'd like to get the inner parens with the sheet name there, but not the outer paren. Excel puts the single quotes on that one, as it would with a space in the sheet name. You could then exclude parens in the non-single quote version since within the single quote it's ok. But then beware Excel seems to even allow single quotes in sheet names. Taking these naming quirks to the extreme, I just successfully named a sheet "Hi'Sheet1'SUM('Sheet2'!A1,A2)!". That's absurd but it points to what could happen. I learned in doing this that if I include a single quote in a sheet name, formulas escape the single quote with a second single quote. So a SUM(A1:A2) referring to the sheet I just created ends up looking like this:

=SUM('Hi''Sheet1''SUM(''Sheet2''!A1,A2)!'!A1:A2)

That actually does give some insight into the Excel parser itself. I suspect to adequately deal with this you may want separately (outside the regex) to compare the potential sheet names or workbook names to the actual sheet names, as you have done with the named ranges.

This leads to the six forms of cell references allowed in the regex (any one of which, if met, will produce a match):

1.) A one-cell or multi-cell range with rows and columns

"(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?"

The open paren here is closed at the end of the 6 options. Otherwise, this line allows a basic cell reference of the type "$A$1", "A1", "$A1", "A$1", or any combination of these in a multi-cell range ("$A1:A$2", etc.).

2.) A full-column or multi-column range with absolute references only

"|\$[a-z]{1,3}\:\$[a-z]{1,3}"

This one allows a cell reference of the type "$A:$B" with a dollar sign on both. Note a dollar sign on only one side will not match.

3.) A full-column or multi-column range with relative references only

"|[a-z]{1,3}\:[a-z]{1,3}"

This line is like the last, but matches only with no dollar signs. Note a dollar sign on only one side will not match here either.

4.) A full-row or multi-row range with absolute references only

"|\$[0-9]{1,7}\:\$[0-9]{1,7}"

This line allows a cell reference of the type "$1:$2" with a dollar sign on both.

5.) A full-row or multi-row range with relative references only

"|[0-9]{1,7}\:[0-9]{1,7}" 

This version is like the last, but matches only with no dollar signs.

6.) Other text that could be a named range

 "|[a-z_\\][a-z0-9_\.]{0,254})"

Finally, the sixth option allows text. This text is compared to actual named ranges later in sub.

The main omission that I see here is ranges that have both absolute and relative references, of the type "A:$A" or "1:$1". While $A:A is captured because it includes "A:A", "A:$A" is not captured. You could address this and simplify the regex by combining 2 and 3 and combining 4 and 5 with optional dollar signs:

objRegExp.Pattern = "(\'.*(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\'\!" _
& "|(\[.*\])?([^\:\\\/\?\*\[\]]{1,31}\:)?[^\:\\\/\?\*\[\]]{1,31}\!)?" _
& "(\$?[a-z]{1,3}\$?[0-9]{1,7}(\:\$?[a-z]{1,3}\$?[0-9]{1,7})?" _
& "|\$?[a-z]{1,3}\:\$?[a-z]{1,3}" _
& "|\$?[0-9]{1,7}\:\$?[0-9]{1,7}" _
& "|[a-z_\\][a-z0-9_\.]{0,254})"

Combining these further would seem to come up against the everything-is-optional problem.

One other issue is in the initial regex pattern for matching strings, which you use to expunge potential ranges that fall inside a quoted string: objRegExp.Pattern = "\"".*\""" When I test this on a formula with a string at the beginning and end of a formula, the greediness of the dot star captures everything from the initial quote to the final quote (in other words it interprets the entire formula as one big quoted string, even though there is non-string material in the middle). It appears you can fix this by making the dot star lazy (adding a question mark after it). That raises questions about quotes within quotes, but they may not be a problem. For instance, I tested this formula:

="John loves his A1 steak sauce, but said the ""good A1 steak sauce price"" is $" & A2+A3 & " less than the ""bad price"" of $" & A4 & "."

With cell values plugged in, this formula evaluates to:

John loves his A1 steak sauce, but said the "good A1 steak sauce price" is $5 less than the "bad price" of $8.

With the lazy modifier added to your string pattern, both versions of "A1" above were recognized as occurring within a string and so were expunged, while A2, A3 and A4 were recognized as cell references.

I'm sure there are some technical issues with some of my language above, but hopefully the analysis is still useful.

Mark E.
  • 373
  • 2
  • 10
3

jtolle steered me in the right direction. As far as I can tell, this is what I was trying to do. I've been testing and it seems to work.

stringOriginFormula = rangeOrigin.Formula
rangeOrigin.Cut rangeDestination
rangeOrigin.Formula = stringOriginFormula

Thanks jtolle!

Kuyenda
  • 4,529
  • 11
  • 46
  • 64
  • 1
    You're welcome. A nice example of reframing the problem leading to a (much) easier solution. You might want to edit the question title...maybe "How can I get literal references in formulas to update when they are moved?" or something? Technically that's now "not programming related", but I think our discussion in the comments justifies keeping the question around. – jtolle Dec 18 '09 at 00:56
  • Those are lines of code from my procedure. I am not doing this with a mouse, so we are safely in the programming related zone. – Kuyenda Dec 18 '09 at 01:07
  • A low-privilege user provided the following warning about this solution in a now-deleted answer: "A warning about using .Cut. If any there are any cell formulas that refer the cell(s) which are .Cut, those other formulas could get altered by the .Cut." I don't know if it holds up, but caveat user. – alexis Nov 21 '16 at 22:32
0

Thanks Ben (I'm new to post here, even though Stackoverflow has caught my attention for years for high quality technical stuff, so I'm not sure if I read this page correctly for the author J)

I tried the posted solutions (testing, testing updated, as well as the one using range.precendents (which as correctly pointed, does not cover references to other sheets or other workbooks) and found a minor flaw: the external sheet name is enclosed in 'single quotation marks' only if it is a number; if it contains space (and possibly other characters as Ben (?) listed in the orginal post. with a simple addition to the regEx (opening [) this can be corrected (added "[", see code below). In addition, for my own purpose I converted the sub to a function that will return a comma-separated list with duplicates removed (note, this removes just identical reference notation, not cells that are included in multiple ranges):

Public Function CellReflist(Optional r As Range)  ' single cell
Dim result As Object: Dim testExpression As String: Dim objRegEx As Object
If r Is Nothing Then Set r = ActiveCell ' Cells(1, 2)  ' INPUT THE CELL HERE , e.g.    RANGE("A1")
Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.IgnoreCase = True: objRegEx.Global = True: objRegEx.Pattern = """.*?"""    ' remove expressions
testExpression = CStr(r.Formula)
testExpression = objRegEx.Replace(testExpression, "")
'objRegEx.Pattern = "(([A-Z])+(\d)+)"  'grab the address

objRegEx.Pattern = "(['\[].*?['!])?([[A-Z0-9_]+[!])?(\$?[A-Z]+\$?(\d)+(:\$?[A-Z]+\$?(\d)+)?|\$?[A-Z]+:\$?[A-Z]+|(\$?[A-Z]+\$?(\d)+))"
If objRegEx.Test(testExpression) Then
    Set result = objRegEx.Execute(testExpression)
    If result.Count > 0 Then CellReflist = result(0).Value
    If result.Count > 1 Then
        For i = 1 To result.Count - 1 'Each Match In result
            dbl = False ' poistetaan tuplaesiintymiset
            For j = 0 To i - 1
                If result(i).Value = result(j).Value Then dbl = True
            Next j
            If Not dbl Then CellReflist = CellReflist & "," & result(i).Value 'Match.Value
        Next i 'Match
    End If
End If

End Function

0

I resolved a similar problem in Google Sheets.

The following adds/subtract row references from a formula. Because I just needed to update row references, rather than extracting the formula I just extracted and updated the row reference with this /((?<=[A-Za-z\$:\!])\d+(?![A-Za-z\(!]))|(\d+(?=[:]))/

String.prototype.replaceAt = function(index, replacement, diff = 0) {
    let end = this.substr(index + replacement.length + diff)
    if((this.length - 1) === index) end = ""
    return this.substr(0, index) + replacement + end;
}
// Ref: https://stackoverflow.com/a/1431113/2319414

/**
 * @param row - positive integer to add, negative to subtract rows.
 */
function updateRowReference(formula, row){

  let masked = formula
  const mask = "#"

  // masking double quotes in string literals
  let exp = /""/g
  let result;
  while((result = exp.exec(masked)) !== null){
    masked = masked.replaceAt(result.index, new Array(result[0].length).fill(mask).join(""))
  }
  // masking string literals
  exp = /\"([^\\\"]|\\.)*\"/g
  // Ref: https://stackoverflow.com/a/9260547
  while((result = exp.exec(masked)) !== null){
    masked = masked.replaceAt(result.index, new Array(result[0].length).fill(mask).join(""))
  }

  // updating row references
  const sRow = row.toString()
  // The magic is happening here
  // Just matching a number which is part of range address
  exp = /((?<=[A-Za-z\$:\!])\d+(?![A-Za-z\(!]))|(\d+(?=[:]))/g
  while((result = exp.exec(masked)) !== null){
    const oldRow = Number(result[0])
    // adding/subtracting rows
    const newRow = (row + oldRow).toString()
    
    // preserving formula string length integrity if number of digits of new row is different than old row
    const diff = result[0].length - newRow.length
    masked = masked.replaceAt(result.index, newRow, diff)
    formula = formula.replaceAt(result.index, newRow, diff)
    exp.lastIndex -= diff
  }

  let updated = masked;

  // revert mask
  const array = formula.split("")
  while((result = updated.search(mask)) !== -1){
    updated = updated.replaceAt(result, array[result])
  }

  return updated
}

function test(){
  const cases = [
    "=$A$1", 
    "=A1", 
    "=$A1", 
    "=A$1", 
    "=$A1:B$1", 
    "=1:1", 
    "=Sheet1!1:1", 
    "=Sheet1!$A1:B$1", 
    "=Sheet1!A$1",
    '=IF(AND($C6 <> ""; NOT(ISBLANK(B$6))); IF(SUM(FILTER($F$6:$F$7;$C$6:$C$7 = $C6)) < $G6; 1; IF($E6 = 0; 1; 0)); 0)',
    "=$A$111", "=A111", "=$A111", "=A$111", "=$A111:B$111", 
    "=111:111", 
    "=Sheet1!111:111", 
    "=Sheet1!$A111:B$111", 
    "=Sheet1!A$111",
    '=IF(AND($C111 <> ""; NOT(ISBLANK(B$111))); IF(SUM(FILTER($F$111:$F$112;$C$111:$C$112 = $C111)) < $G111; 1; IF($E111 = 0; 1; 0)); 0)',

    // if string literals have addresses they shouldn't be affected
    '=IF(AND($C111 <> "A1 $A1 $A1:B$1";$C111 <> "Sheet1!1:1";$C111 <> "Sheet1!$A1:B$1"); 1 , 0)'
  ]

  const expectedAdd = [
    '=$A$16',
    '=A16',
    '=$A16',
    '=A$16',
    '=$A16:B$16',
    '=16:16',
    '=Sheet1!16:16',
    '=Sheet1!$A16:B$16',
    '=Sheet1!A$16',
    '=IF(AND($C21 <> ""; NOT(ISBLANK(B$21))); IF(SUM(FILTER($F$21:$F$22;$C$21:$C$22 = $C21)) < $G21; 1; IF($E21 = 0; 1; 0)); 0)',
    '=$A$126',
    '=A126',
    '=$A126',
    '=A$126',
    '=$A126:B$126',
    '=126:126',
    '=Sheet1!126:126',
    '=Sheet1!$A126:B$126',
    '=Sheet1!A$126',
    '=IF(AND($C126 <> ""; NOT(ISBLANK(B$126))); IF(SUM(FILTER($F$126:$F$127;$C$126:$C$127 = $C126)) < $G126; 1; IF($E126 = 0; 1; 0)); 0)',
    '=IF(AND($C126 <> "A1 $A1 $A1:B$1";$C126 <> "Sheet1!1:1";$C126 <> "Sheet1!$A1:B$1"); 1 , 0)' 
    ]

  let results = cases.map(_case => updateRowReference(_case, 15))

  console.log('Test Add')
  console.log(results.every((result, i) => result === expectedAdd[i]))

  console.log('Test Subtract')
  results = results.map(_case => updateRowReference(_case, -15))
  console.log(results.every((result, i) => result === cases[i]))
}

test()

'INDIRECT' function with addresses as strings will not be updated

S.aad
  • 518
  • 1
  • 5
  • 22