@@ -1645,6 +1645,9 @@ func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error {
16451645// characters and default sheet name.
16461646func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) {
16471647 reference = strings.ReplaceAll(reference, "$", "")
1648+ if parts := split3DReference(reference); len(parts) == 3 {
1649+ return f.parse3DReference(ctx, parts)
1650+ }
16481651 ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New()
16491652 if len(ranges) > 1 {
16501653 var cr cellRange
@@ -1684,6 +1687,89 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul
16841687 return f.rangeResolver(ctx, cellRefs, cellRanges)
16851688}
16861689
1690+ // parse3DReference detects a 3D reference from a range reference and expands it
1691+ // across the workbook-order sheet range.
1692+ func (f *File) parse3DReference(ctx *calcContext, parts []string) (formulaArg, error) {
1693+ firstSheet, lastSheet, cellRef := parts[0], parts[1], parts[2]
1694+ sheets, err := f.expand3DSheetRange(firstSheet, lastSheet)
1695+ if err != nil {
1696+ return newErrorFormulaArg(formulaErrorREF, formulaErrorREF), err
1697+ }
1698+ var matrix [][]formulaArg
1699+ for _, sheet := range sheets {
1700+ result, err := f.parseReference(ctx, sheet, cellRef)
1701+ if err != nil {
1702+ return newErrorFormulaArg(formulaErrorNAME, formulaErrorNAME), err
1703+ }
1704+ switch result.Type {
1705+ case ArgMatrix:
1706+ matrix = append(matrix, result.Matrix...)
1707+ default:
1708+ matrix = append(matrix, []formulaArg{result})
1709+ }
1710+ }
1711+ return newMatrixFormulaArg(matrix), err
1712+ }
1713+
1714+ // split3DReference parses a reference string for a 3D reference of the form
1715+ // formula structure in FirstSheet:LastSheet!CellReference and returns the three
1716+ // components if valid, or an empty slice if not.
1717+ func split3DReference(reference string) []string {
1718+ var parts []string
1719+ idx := strings.Index(reference, "!")
1720+ if idx < 0 || idx == len(reference)-1 {
1721+ return parts
1722+ }
1723+ sheetsRef, cellRef := reference[:idx], reference[idx+1:]
1724+ firstSheet, rest, ok := readSheetToken(sheetsRef)
1725+ if !ok || len(rest) < 2 || rest[0] != ':' {
1726+ return parts
1727+ }
1728+ lastSheet, rest, ok := readSheetToken(rest[1:])
1729+ if !ok || rest != "" {
1730+ return parts
1731+ }
1732+ if firstSheet == "" || lastSheet == "" {
1733+ return parts
1734+ }
1735+ return []string{firstSheet, lastSheet, cellRef}
1736+ }
1737+
1738+ // readSheetToken reads an unquoted sheet name from the front of s and
1739+ // returns (name, remaining, ok). A `:` ends the name; a `!` or `'` is
1740+ // rejected as malformed for the 3D shape parser.
1741+ func readSheetToken(s string) (name, rest string, ok bool) {
1742+ if s == "" {
1743+ return "", "", false
1744+ }
1745+ for i, r := range s {
1746+ if r == ':' {
1747+ return s[:i], s[i:], true
1748+ }
1749+ if r == '!' || r == '\'' {
1750+ return "", "", false
1751+ }
1752+ }
1753+ return s, "", true
1754+ }
1755+
1756+ // expand3DSheetRange returns the workbook-order slice of sheet names
1757+ // from first sheet to last sheet inclusive.
1758+ func (f *File) expand3DSheetRange(sheet1, sheet2 string) ([]string, error) {
1759+ firstSheetIdx, err := f.GetSheetIndex(sheet1)
1760+ if err != nil || firstSheetIdx < 0 {
1761+ return nil, ErrSheetNotExist{sheet1}
1762+ }
1763+ lastSheetIdx, err := f.GetSheetIndex(sheet2)
1764+ if err != nil || lastSheetIdx < 0 {
1765+ return nil, ErrSheetNotExist{sheet2}
1766+ }
1767+ if firstSheetIdx > lastSheetIdx {
1768+ firstSheetIdx, lastSheetIdx = lastSheetIdx, firstSheetIdx
1769+ }
1770+ return f.GetSheetList()[firstSheetIdx : lastSheetIdx+1], err
1771+ }
1772+
16871773// prepareValueRange prepare value range.
16881774func prepareValueRange(cr cellRange, valueRange []int) {
16891775 if cr.From.Row < valueRange[0] || valueRange[0] == 0 {
0 commit comments