Skip to content

Commit 71aaf20

Browse files
committed
feat: support for @ operator
1 parent 7d722c9 commit 71aaf20

File tree

5 files changed

+90
-65
lines changed

5 files changed

+90
-65
lines changed

ast.go

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,34 +27,35 @@ const (
2727
Ident TokenType = iota + 1
2828
Number
2929
String
30-
BoolLiteral // TRUE or FALSE
31-
EValue // Error value (e.g., #DIV/0!, #VALUE!, etc)
32-
Exclamation // !
33-
BraceOpen // {
34-
BraceClose // }
35-
BracketOpen // [
36-
BracketClose // ]
37-
ParenOpen // (
38-
ParenClose // )
39-
Comma // ,
40-
Semicolon // ;
41-
Percent // %
42-
Exponentiation // ^
43-
Multiply // *
44-
Divide // /
45-
Plus // +
46-
Colon // :
47-
Minus // -
48-
Concat // &
49-
Equal // =
50-
NotEqual // <>
51-
LessThan // <
52-
GreaterThan // >
53-
LessThanOrEqual // <=
54-
GreaterThanOrEqual // >=
55-
Cell // e.g., A1, B2, etc
56-
AbsoluteRow // Absolute row reference (e.g., $1, $2)
57-
AbsoluteColumn // Absolute column reference (e.g., $A, $B)
30+
BoolLiteral // TRUE or FALSE
31+
EValue // Error value (e.g., #DIV/0!, #VALUE!, etc)
32+
Exclamation // !
33+
BraceOpen // {
34+
BraceClose // }
35+
BracketOpen // [
36+
BracketClose // ]
37+
ParenOpen // (
38+
ParenClose // )
39+
Comma // ,
40+
Semicolon // ;
41+
ImplicitIntersection // @
42+
Percent // %
43+
Exponentiation // ^
44+
Multiply // *
45+
Divide // /
46+
Plus // +
47+
Colon // :
48+
Minus // -
49+
Concat // &
50+
Equal // =
51+
NotEqual // <>
52+
LessThan // <
53+
GreaterThan // >
54+
LessThanOrEqual // <=
55+
GreaterThanOrEqual // >=
56+
Cell // e.g., A1, B2, etc
57+
AbsoluteRow // Absolute row reference (e.g., $1, $2)
58+
AbsoluteColumn // Absolute column reference (e.g., $A, $B)
5859
)
5960

6061
type Pos struct {

lexer.go

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,67 +32,68 @@ func (l *lexer) next() (*Token, error) {
3232
for l.ch == ' ' || l.ch == '\n' || l.ch == '\t' {
3333
l.nextch()
3434
}
35+
var start = l.pos
3536
switch l.ch {
3637
case '"':
3738
return l.stringLiteral(l.ch)
3839
case '\'':
3940
return l.stringLiteral(l.ch)
4041
case '=':
4142
l.nextch()
42-
return newToken(l.pos, l.pos, Equal, "="), nil
43+
return newToken(start, start, Equal, "="), nil
4344
case '!':
4445
l.nextch()
45-
return newToken(l.pos, l.pos, Exclamation, "!"), nil
46+
return newToken(start, start, Exclamation, "!"), nil
4647
case ',':
4748
l.nextch()
48-
return newToken(l.pos, l.pos, Comma, ","), nil
49+
return newToken(start, start, Comma, ","), nil
4950
case '(':
5051
l.nextch()
51-
return newToken(l.pos, l.pos, ParenOpen, "("), nil
52+
return newToken(start, start, ParenOpen, "("), nil
5253
case ')':
5354
l.nextch()
54-
return newToken(l.pos, l.pos, ParenClose, ")"), nil
55+
return newToken(start, start, ParenClose, ")"), nil
5556
case '{':
5657
l.nextch()
57-
return newToken(l.pos, l.pos, BraceOpen, "{"), nil
58+
return newToken(start, start, BraceOpen, "{"), nil
5859
case '}':
5960
l.nextch()
60-
return newToken(l.pos, l.pos, BraceClose, "}"), nil
61+
return newToken(start, start, BraceClose, "}"), nil
6162
case '[':
6263
l.nextch()
63-
return newToken(l.pos, l.pos, BracketOpen, "["), nil
64+
return newToken(start, start, BracketOpen, "["), nil
6465
case ']':
6566
l.nextch()
66-
return newToken(l.pos, l.pos, BracketClose, "]"), nil
67+
return newToken(start, start, BracketClose, "]"), nil
6768
case ':':
6869
l.nextch()
69-
return newToken(l.pos, l.pos, Colon, ":"), nil
70+
return newToken(start, start, Colon, ":"), nil
7071
case ';':
7172
l.nextch()
72-
return newToken(l.pos, l.pos, Semicolon, ";"), nil
73+
return newToken(start, start, Semicolon, ";"), nil
7374
case '+':
7475
l.nextch()
75-
return newToken(l.pos, l.pos, Plus, "+"), nil
76+
return newToken(start, start, Plus, "+"), nil
7677
case '-':
7778
l.nextch()
78-
return newToken(l.pos, l.pos, Minus, "-"), nil
79+
return newToken(start, start, Minus, "-"), nil
7980
case '*':
8081
l.nextch()
81-
return newToken(l.pos, l.pos, Multiply, "*"), nil
82+
return newToken(start, start, Multiply, "*"), nil
8283
case '/':
8384
l.nextch()
84-
return newToken(l.pos, l.pos, Divide, "/"), nil
85+
return newToken(start, start, Divide, "/"), nil
8586
case '$':
8687
return l.absoluteReference()
8788
case '&':
8889
l.nextch()
89-
return newToken(l.pos, l.pos, Concat, "&"), nil
90+
return newToken(start, start, Concat, "&"), nil
9091
case '^':
9192
l.nextch()
92-
return newToken(l.pos, l.pos, Exponentiation, "^"), nil
93+
return newToken(start, start, Exponentiation, "^"), nil
9394
case '%':
9495
l.nextch()
95-
return newToken(l.pos, l.pos, Percent, "%"), nil
96+
return newToken(start, start, Percent, "%"), nil
9697
case '<':
9798
var start = l.pos
9899
l.nextch()
@@ -118,6 +119,9 @@ func (l *lexer) next() (*Token, error) {
118119
}
119120
case '#':
120121
return l.errValue()
122+
case '@':
123+
l.nextch()
124+
return newToken(start, start, ImplicitIntersection, "@"), nil
121125
default:
122126
if isDigit(l.ch) {
123127
return l.number()
@@ -224,16 +228,15 @@ func (l *lexer) errValue() (*Token, error) {
224228
"#N/A",
225229
}
226230
for _, value := range values {
231+
_len := utf8.RuneCountInString(value)
227232
// check length
228-
if l.offset-1+len(value) > len(l.src) {
233+
if l.offset-1+_len > len(l.src) {
229234
continue // Not enough characters left for this error value
230235
}
231-
if string(l.src[l.offset-1:l.offset-1+len(value)]) == value {
236+
if string(l.src[l.offset-1:l.offset-1+_len]) == value {
232237
var start = l.pos
233-
l.eat(len(value) - 1) // Consume the error value
238+
l.eat(_len) // Consume the error value
234239
var end = l.pos
235-
l.offset += len(value) - 1 // Adjust offset to skip the error value
236-
l.ch = l.peekChar() // Update current character
237240
return newToken(start, end, EValue, value), nil
238241
}
239242
}
@@ -244,25 +247,21 @@ func (l *lexer) stringLiteral(quote rune) (*Token, error) {
244247
var start = l.pos
245248
var end = start
246249
l.nextch() // Consume the opening quote
247-
for l.ch != quote && l.ch >= 0 {
250+
for l.ch >= 0 {
248251
if l.ch == '\n' {
249252
return nil, newLexError(l.pos, "newline in string literal")
250253
}
251-
if l.ch == '\\' {
252-
l.nextch()
253-
switch {
254-
case l.ch < 0:
255-
return nil, newLexError(l.pos, "unclosed string literal")
256-
case l.ch == quote || l.ch == '\\':
257-
// Consume the escaped character
258-
default:
259-
return nil, newLexError(l.pos, "invalid escape sequence in string literal")
254+
if l.ch == quote {
255+
if l.peekChar() == quote {
256+
l.nextch()
257+
} else {
258+
break
260259
}
261260
}
262261
end = l.pos
263262
l.nextch()
264263
}
265-
if l.ch < 0 {
264+
if l.ch != quote {
266265
return nil, newLexError(l.pos, "unclosed string literal")
267266
}
268267
end = l.pos // Update end to the position after the closing quote
@@ -315,7 +314,7 @@ func (l *lexer) ident() (*Token, error) {
315314
LOOP:
316315
for l.ch >= 0 {
317316
switch {
318-
case isASCIILetter(l.ch) || isDigit(l.ch):
317+
case isASCIILetter(l.ch) || isDigit(l.ch) || l.ch == '.':
319318
end = l.pos
320319
endOffset = l.offset
321320
l.nextch()

lexer_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ func TestLexer(t *testing.T) {
1010
src string
1111
expected TokenType
1212
}{
13+
{"ERROR.TYPE", Ident},
14+
{"@", ImplicitIntersection},
15+
{`"""str"""`, String},
1316
{"#DIV/0!", EValue},
1417
{"#VALUE!", EValue},
1518
{"#N/A", EValue},
@@ -27,8 +30,6 @@ func TestLexer(t *testing.T) {
2730
{"0.230000", Number},
2831
{`"hello"`, String},
2932
{`'world'`, String},
30-
{`'with quote\''`, String},
31-
{`"with quote\""`, String},
3233
{"=", Equal},
3334
{"!", Exclamation},
3435
{",", Comma},

parser.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,26 @@ func (p *Parser) negation() (Node, error) {
226226
if p.token != nil && p.token.Type == BraceOpen {
227227
return p.arrayExpr()
228228
}
229+
return p.implicitIntersection()
230+
}
231+
232+
func (p *Parser) implicitIntersection() (Node, error) {
233+
if p.token != nil && p.token.Type == ImplicitIntersection {
234+
var start = p.token.Start
235+
var op = p.token
236+
if err := p.advance(); err != nil { // consume the token
237+
return nil, err
238+
}
239+
operand, err := p.rangeExpr()
240+
if err != nil {
241+
return nil, err
242+
}
243+
return &UnaryExpr{
244+
baseNode: newBaseNode(start, operand.End()),
245+
Operator: op,
246+
Operand: operand,
247+
}, nil
248+
}
229249
return p.rangeExpr()
230250
}
231251

parser_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ func TestParse(t *testing.T) {
77
src string
88
expected string
99
}{
10+
{`=+ AName- (-+-+-2^6) = {"A","B"} + @SUM(A1) + (@ERROR.TYPE(#VALUE!) = 2)`, `BinaryExpr(Left: BinaryExpr(Left: UnaryExpr(Operator: +, Operand: IdentExpr(Name: AName)), Operator: -, Right: ParenthesizedExpr(Inner: BinaryExpr(Left: UnaryExpr(Operator: -, Operand: UnaryExpr(Operator: +, Operand: UnaryExpr(Operator: -, Operand: UnaryExpr(Operator: +, Operand: LiteralExpr(Value: -2))))), Operator: ^, Right: LiteralExpr(Value: 6)))), Operator: =, Right: BinaryExpr(Left: BinaryExpr(Left: ArrayExpr([LiteralExpr(Value: "A"), LiteralExpr(Value: "B")]), Operator: +, Right: UnaryExpr(Operator: @, Operand: FunCallExpr(Name: SUM, Arguments: [CellExpr(A1)]))), Operator: +, Right: ParenthesizedExpr(Inner: BinaryExpr(Left: UnaryExpr(Operator: @, Operand: FunCallExpr(Name: ERROR.TYPE, Arguments: [LiteralExpr(Value: #VALUE!)])), Operator: =, Right: LiteralExpr(Value: 2)))))`},
11+
{"=@A:A", "UnaryExpr(Operator: @, Operand: RangeExpr(CellExpr(A):CellExpr(A)))"},
12+
{`=IF("a"={"a","b";"c",#N/A;-1,TRUE}, "yes", "no") & " more ""test"" text"`, `BinaryExpr(Left: FunCallExpr(Name: IF, Arguments: [BinaryExpr(Left: LiteralExpr(Value: "a"), Operator: =, Right: ArrayExpr([LiteralExpr(Value: "a"), LiteralExpr(Value: "b")], [LiteralExpr(Value: "c"), LiteralExpr(Value: #N/A)], [LiteralExpr(Value: -1), LiteralExpr(Value: TRUE)])), LiteralExpr(Value: "yes"), LiteralExpr(Value: "no")]), Operator: &, Right: LiteralExpr(Value: " more ""test"" text"))`},
13+
{"={#N/A}", "ArrayExpr([LiteralExpr(Value: #N/A)])"},
1014
{"={1,2;3,4}", "ArrayExpr([LiteralExpr(Value: 1), LiteralExpr(Value: 2)], [LiteralExpr(Value: 3), LiteralExpr(Value: 4)])"},
1115
{"=-{-1}", "UnaryExpr(Operator: -, Operand: ArrayExpr([LiteralExpr(Value: -1)]))"},
1216
{"=--1", "UnaryExpr(Operator: -, Operand: LiteralExpr(Value: -1))"},

0 commit comments

Comments
 (0)