Skip to content

Commit 9406a24

Browse files
committed
Json-like syntax for arrays/dictionaries #56
1 parent 4c13d4a commit 9406a24

File tree

3 files changed

+118
-55
lines changed

3 files changed

+118
-55
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ NuGet | Windows x64 | Linux
1010
* supports arithmetic operations (+, -, *, /, %), comparisons (==, !=, >, <, >=, <=), conditionals including (ternary) operator ( boolVal ? whenTrue : whenFalse )
1111
* access object properties, call methods and indexers, invoke delegates
1212
* dynamic typed variables: performs automatic type conversions to match method signature or arithmetic operations
13-
* create arrays and dictionaries with simplified syntax: `new dictionary{ {"a", 1}, {"b", 2} }` , `new []{ 1, 2, 3}`
13+
* create arrays with C#-like or JSON syntax: `new []{ 1, 2, 3}` or `new [1, 2, 3]` or just `[1, 2, 3]`
14+
* create dictionaries (key-value map) with C#-like or JSON syntax: `new dictionary{ {"a", 1}, {"b", 2} }` or `new {"a":1}` or `{"a":1, "b":2}`
1415
* local variables that may go before main expression: `var a = 5; var b = contextVar/total*100;` (disabled by default, to enable use `LambdaParser.AllowVars` property)
1516

1617
Nuget package: [NReco.LambdaParser](https://www.nuget.org/packages/NReco.LambdaParser/)
@@ -55,6 +56,6 @@ lambdaParser.UseCache = false;
5556
NReco.LambdaParser is in production use at [SeekTable.com](https://www.seektable.com/) and [PivotData microservice](https://www.nrecosite.com/pivotdata_service.aspx) (used for user-defined calculated cube members: formulas, custom formatting).
5657

5758
## License
58-
Copyright 2016-2024 Vitaliy Fedorchenko and contributors
59+
Copyright 2016-2025 Vitaliy Fedorchenko and contributors
5960

6061
Distributed under the MIT license

src/NReco.LambdaParser.Tests/LambdaParserTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,27 @@ public void Eval() {
9191

9292
Assert.Equal(true, lambdaParser.Eval(" new[]{ one } == new[] { 1 } ", varContext));
9393

94+
Assert.Equal("1", (lambdaParser.Eval(" new[ \"1\" ] ", varContext) as IList)[0] );
95+
Assert.Equal(3, (lambdaParser.Eval("new[1,2,3]", varContext) as IList).Count);
96+
Assert.Throws<LambdaParserException>( ()=>lambdaParser.Eval("new[1 + 5", varContext));
97+
98+
Assert.Equal("1", (lambdaParser.Eval("[\"1\"] ", varContext) as IList)[0]);
99+
Assert.Equal(3, (lambdaParser.Eval("[1,2,3]", varContext) as IList).Count);
100+
Assert.Equal(0, (lambdaParser.Eval("[]", varContext) as IList).Count);
101+
Assert.Equal("1,2,3", lambdaParser.Eval("testObj.Format(\"{0},{1},{2}\", [1,2,3])", varContext) as string);
102+
94103
Assert.Equal(3, lambdaParser.Eval(" new dictionary{ {\"a\", 1}, {\"b\", 2}, {\"c\", 3} }.Count ", varContext));
104+
Assert.Equal(3, lambdaParser.Eval(" new { {\"a\", 1}, {\"b\", 2}, {\"c\", 3} }.Count ", varContext));
105+
Assert.Equal(3, lambdaParser.Eval(" { \"a\": 1, \"b\": 2, \"c\": 3 }.Count ", varContext));
106+
Assert.Equal(0, lambdaParser.Eval(" { }.Count ", varContext));
107+
Assert.Throws<LambdaParserException>(() => lambdaParser.Eval("{ \"a\": 1, \"b\": 2, }", varContext));
108+
Assert.Throws<LambdaParserException>(() => lambdaParser.Eval("{ \"a\" }", varContext));
109+
Assert.Throws<LambdaParserException>(() => lambdaParser.Eval("{ \"a\": }", varContext));
110+
Assert.Throws<LambdaParserException>(() => lambdaParser.Eval("testObj.Format(\"A\", { \"a\": 1, \"b\": 2 )", varContext));
95111

96112
Assert.Equal(2M, lambdaParser.Eval(" new dictionary{ {\"a\", 1}, {\"b\", 2}, {\"c\", 3} }[\"b\"] ", varContext));
113+
Assert.Equal(2M, lambdaParser.Eval(" { \"a\": 1, \"b\": 2, \"c\": 3 }[\"b\"] ", varContext));
114+
Assert.Equal(11M, lambdaParser.Eval(" { \"a\": {\"b\": [10,11] } }[\"a\"][\"b\"][1] ", varContext));
97115

98116
var arr = ((Array)lambdaParser.Eval(" new []{ new dictionary{{\"test\",2}}, new[] { one } }", varContext) );
99117
Assert.Equal(2M, ((IDictionary)arr.GetValue(0) )["test"] );

src/NReco.LambdaParser/Linq/LambdaParser.cs

Lines changed: 97 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -614,19 +614,16 @@ protected int ReadCallArguments(string expr, int start, string endLexem, List<Ex
614614
return lexem.End;
615615
} else if (lexem.GetValue() == ",") {
616616
if (args.Count == 0) {
617-
throw new LambdaParserException(expr, lexem.Start, "Expected method call parameter");
617+
throw new LambdaParserException(expr, lexem.Start, "Value missed");
618618
}
619619
end = lexem.End;
620+
} else {
621+
throw new LambdaParserException(expr, lexem.Start, String.Format("Expected '{0}' or ','", endLexem));
620622
}
621623
}
622624
// read parameter
623625
var paramExpr = ParseConditional(expr, end, vars);
624-
var argExpr = paramExpr.Expr;
625-
if (!(argExpr is ConstantExpression constExpr && constExpr.Value is LambdaParameterWrapper)) {
626-
// result may be a primitive type like bool
627-
argExpr = Expression.Convert(argExpr, typeof(object));
628-
}
629-
args.Add(argExpr);
626+
args.Add(ExprConvertToObjectIfNeeded(paramExpr.Expr));
630627
end = paramExpr.End;
631628
} while (true);
632629
}
@@ -644,23 +641,25 @@ protected ParseResult ParseValue(string expr, int start, Variables vars) {
644641
} else if (lexem.Type == LexemType.NumberConstant) {
645642
decimal numConst;
646643
if (!Decimal.TryParse(lexem.GetValue(), NumberStyles.Any, CultureInfo.InvariantCulture, out numConst)) {
647-
throw new Exception(String.Format("Invalid number: {0}", lexem.GetValue()));
644+
throw new Exception(String.Format("Invalid number: {0}", lexem.GetValue()));
648645
}
649-
return new ParseResult() {
650-
End = lexem.End,
651-
Expr = Expression.Constant(new LambdaParameterWrapper( numConst, LambdaParamCtx) ) };
646+
return new ParseResult() {
647+
End = lexem.End,
648+
Expr = Expression.Constant(new LambdaParameterWrapper(numConst, LambdaParamCtx))
649+
};
652650
} else if (lexem.Type == LexemType.StringConstant) {
653-
return new ParseResult() {
654-
End = lexem.End,
655-
Expr = Expression.Constant( new LambdaParameterWrapper( lexem.GetValue(), LambdaParamCtx) ) };
651+
return new ParseResult() {
652+
End = lexem.End,
653+
Expr = Expression.Constant(new LambdaParameterWrapper(lexem.GetValue(), LambdaParamCtx))
654+
};
656655
} else if (lexem.Type == LexemType.Name) {
657656
// check for predefined constants
658657
var val = lexem.GetValue();
659658
switch (val) {
660659
case "true":
661-
return new ParseResult() { End = lexem.End, Expr = Expression.Constant(new LambdaParameterWrapper(true, LambdaParamCtx) ) };
660+
return new ParseResult() { End = lexem.End, Expr = Expression.Constant(new LambdaParameterWrapper(true, LambdaParamCtx)) };
662661
case "false":
663-
return new ParseResult() { End = lexem.End, Expr = Expression.Constant(new LambdaParameterWrapper(false, LambdaParamCtx) ) };
662+
return new ParseResult() { End = lexem.End, Expr = Expression.Constant(new LambdaParameterWrapper(false, LambdaParamCtx)) };
664663
case "null":
665664
return new ParseResult() { End = lexem.End, Expr = Expression.Constant(new LambdaParameterWrapper(null, LambdaParamCtx)) };
666665
case "new":
@@ -669,9 +668,13 @@ protected ParseResult ParseValue(string expr, int start, Variables vars) {
669668

670669
// todo
671670
var localVarExpr = vars.Get(val);
672-
return new ParseResult() {
673-
End = lexem.End,
674-
Expr = localVarExpr!=null ? localVarExpr : Expression.Parameter(typeof(LambdaParameterWrapper), val) };
671+
return new ParseResult() {
672+
End = lexem.End,
673+
Expr = localVarExpr!=null ? localVarExpr : Expression.Parameter(typeof(LambdaParameterWrapper), val)
674+
};
675+
} else if (lexem.Type == LexemType.Delimiter && (lexem.GetValue() == "[" || lexem.GetValue() == "{")) {
676+
// json-like array or object (dictionary) syntax
677+
return ReadNewInstance(expr, lexem.Start, vars);
675678
}
676679
throw new LambdaParserException(expr, start, "Expected value");
677680
}
@@ -680,51 +683,84 @@ protected ParseResult ReadNewInstance(string expr, int start, Variables vars) {
680683
var nextLexem = ReadLexem(expr, start);
681684
if (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "[") {
682685
nextLexem = ReadLexem(expr, nextLexem.End);
683-
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "]"))
684-
throw new LambdaParserException(expr, nextLexem.Start, "Expected ']'");
685-
686-
nextLexem = ReadLexem(expr, nextLexem.End);
687-
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{"))
688-
throw new LambdaParserException(expr, nextLexem.Start, "Expected '{'");
689-
690686
var arrayArgs = new List<Expression>();
691-
var end = ReadCallArguments(expr, nextLexem.End, "}", arrayArgs, vars);
692-
var newArrExpr = Expression.NewArrayInit(typeof(object), arrayArgs );
693-
return new ParseResult() {
687+
int end;
688+
if (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "]") {
689+
nextLexem = ReadLexem(expr, nextLexem.End);
690+
if (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{")
691+
// this is 'new [] { val1, val2 }'
692+
end = ReadCallArguments(expr, nextLexem.End, "}", arrayArgs, vars);
693+
else {
694+
// just [] (empty array)
695+
end = nextLexem.Start;
696+
}
697+
} else {
698+
// this is '[ val1, val2 ]' syntax
699+
end = ReadCallArguments(expr, nextLexem.Start, "]", arrayArgs, vars);
700+
}
701+
702+
var newArrExpr = Expression.NewArrayInit(typeof(object), arrayArgs);
703+
return new ParseResult() {
694704
End = end,
695-
Expr = Expression.New(LambdaParameterWrapperConstructor,
705+
Expr = Expression.New(LambdaParameterWrapperConstructor,
696706
Expression.Convert(newArrExpr, typeof(object)),
697707
Expression.Constant(LambdaParamCtx, typeof(LambdaParameterWrapperContext)))
698708
};
699709
}
700710
if (nextLexem.Type == LexemType.Name && nextLexem.GetValue().ToLower() == "dictionary") {
711+
// just read it, this is old syntax 'new dictionary { }'
701712
nextLexem = ReadLexem(expr, nextLexem.End);
702-
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{"))
703-
throw new LambdaParserException(expr, nextLexem.Start, "Expected '{'");
704-
713+
}
714+
if (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{") {
705715
var dictionaryKeys = new List<Expression>();
706716
var dictionaryValues = new List<Expression>();
707-
do {
708-
nextLexem = ReadLexem(expr, nextLexem.End);
709-
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{"))
710-
throw new LambdaParserException(expr, nextLexem.Start, "Expected '{'");
711-
var entryArgs = new List<Expression>();
712-
var end = ReadCallArguments(expr, nextLexem.End, "}", entryArgs, vars);
713-
if (entryArgs.Count!=2)
714-
throw new LambdaParserException(expr, nextLexem.Start, "Dictionary entry should have exactly 2 arguments");
715-
716-
dictionaryKeys.Add( entryArgs[0] );
717-
dictionaryValues.Add( entryArgs[1] );
718-
719-
nextLexem = ReadLexem(expr, end);
720-
} while (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == ",");
721-
722-
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "}"))
723-
throw new LambdaParserException(expr, nextLexem.Start, "Expected '}'");
724-
725-
var newKeysArrExpr = Expression.NewArrayInit(typeof(object), dictionaryKeys );
726-
var newValuesArrExpr = Expression.NewArrayInit(typeof(object), dictionaryValues );
717+
var startLexem = ReadLexem(expr, nextLexem.End);
718+
if (startLexem.Type == LexemType.Delimiter && startLexem.GetValue() == "{") {
719+
// this is C#-like syntax: '{ {"key1", "val1"}, {"key2", "val2"} }
720+
do {
721+
nextLexem = ReadLexem(expr, nextLexem.End);
722+
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "{"))
723+
throw new LambdaParserException(expr, nextLexem.Start, "Expected '{'");
724+
var entryArgs = new List<Expression>();
725+
var end = ReadCallArguments(expr, nextLexem.End, "}", entryArgs, vars);
726+
if (entryArgs.Count!=2)
727+
throw new LambdaParserException(expr, nextLexem.Start, "Dictionary entry should have exactly 2 arguments");
728+
729+
dictionaryKeys.Add(entryArgs[0]);
730+
dictionaryValues.Add(entryArgs[1]);
731+
732+
nextLexem = ReadLexem(expr, end);
733+
} while (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == ",");
734+
735+
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == "}"))
736+
throw new LambdaParserException(expr, nextLexem.Start, "Expected '}'");
737+
} else if (startLexem.Type == LexemType.Delimiter && startLexem.GetValue() == "}") {
738+
// this is '{}' - empty object (dictionary)
739+
nextLexem = startLexem;
740+
} else {
741+
// this is JSON-like syntax: { 'key1' : 'val1', 'key2': 'val2' }
742+
do {
743+
// read key
744+
var keyExpr = ParseConditional(expr, nextLexem.End, vars);
745+
dictionaryKeys.Add(ExprConvertToObjectIfNeeded(keyExpr.Expr));
746+
747+
nextLexem = ReadLexem(expr, keyExpr.End);
748+
if (!(nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == ":"))
749+
throw new LambdaParserException(expr, nextLexem.Start, "Expected ':'");
750+
751+
// read value
752+
var valExpr = ParseConditional(expr, nextLexem.End, vars);
753+
dictionaryValues.Add(ExprConvertToObjectIfNeeded(valExpr.Expr));
754+
755+
nextLexem = ReadLexem(expr, valExpr.End);
756+
if (nextLexem.Type == LexemType.Delimiter && (nextLexem.GetValue()=="}" || nextLexem.GetValue()==","))
757+
continue;
758+
throw new LambdaParserException(expr, nextLexem.Start, "Expected ',' or '}'");
759+
} while (nextLexem.Type == LexemType.Delimiter && nextLexem.GetValue() == ",");
760+
}
727761

762+
var newKeysArrExpr = Expression.NewArrayInit(typeof(object), dictionaryKeys);
763+
var newValuesArrExpr = Expression.NewArrayInit(typeof(object), dictionaryValues );
728764
return new ParseResult() {
729765
End = nextLexem.End,
730766
Expr = Expression.Call(
@@ -736,6 +772,14 @@ protected ParseResult ReadNewInstance(string expr, int start, Variables vars) {
736772
throw new LambdaParserException(expr, start, "Unknown new instance initializer");
737773
}
738774

775+
Expression ExprConvertToObjectIfNeeded(Expression expr) {
776+
if (!(expr is ConstantExpression constExpr && constExpr.Value is LambdaParameterWrapper)) {
777+
// result may be a primitive type like bool
778+
return Expression.Convert(expr, typeof(object));
779+
}
780+
return expr;
781+
}
782+
739783
protected enum LexemType {
740784
Unknown,
741785
Name,

0 commit comments

Comments
 (0)