Skip to content

Commit 77be74f

Browse files
authored
Merge pull request #292 from anmcgrath/row-column-formula
Row column formula
2 parents c0c394a + 07fbbac commit 77be74f

26 files changed

Lines changed: 344 additions & 78 deletions

src/BlazorDatasheet.Core/FormulaEngine/FormulaEngine.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ internal void AddSheet(Sheet sheet)
4747
sheet.Cells.CellsChanged += SheetOnCellsChanged;
4848
sheet.Rows.Removed += RowColsOnRemoved;
4949
sheet.Columns.Removed += RowColsOnRemoved;
50+
sheet.Rows.Inserted += RowColsOnInserted;
51+
sheet.Columns.Inserted += RowColsOnInserted;
5052
}
5153

5254
internal void RemoveSheet(Sheet sheet)
@@ -57,6 +59,8 @@ internal void RemoveSheet(Sheet sheet)
5759
sheet.Cells.CellsChanged -= SheetOnCellsChanged;
5860
sheet.Rows.Removed -= RowColsOnRemoved;
5961
sheet.Columns.Removed -= RowColsOnRemoved;
62+
sheet.Rows.Inserted -= RowColsOnInserted;
63+
sheet.Columns.Inserted -= RowColsOnInserted;
6064
}
6165

6266
private void SheetOnCellsChanged(object? sender, CellDataChangedEventArgs e)
@@ -101,6 +105,11 @@ private void RowColsOnRemoved(object? sender, RowColRemovedEventArgs e)
101105
CalculateSheet(true);
102106
}
103107

108+
private void RowColsOnInserted(object? sender, RowColInsertedEventArgs e)
109+
{
110+
CalculateSheet(true);
111+
}
112+
104113
private void SheetOnBeforeCellEdit(object? sender, BeforeCellEditEventArgs e)
105114
{
106115
if (sender is not Editor editor)
@@ -213,33 +222,37 @@ private void EndCalculation(IEnumerable<Sheet> batchedSheets)
213222
private void EvaluateStronglyConnectedGroup(IList<FormulaVertex> stronglyConnectedGroup,
214223
FormulaExecutionContext executionContext)
215224
{
216-
var group = stronglyConnectedGroup;
217225
bool isCircularGroup = false;
218226

219-
executionContext.SetCurrentGroup(ref group);
227+
executionContext.SetCurrentGroup(stronglyConnectedGroup);
220228

221229
foreach (var vertex in stronglyConnectedGroup)
222230
{
223231
var formula = vertex.Formula;
224232
if (formula == null)
225233
continue;
226234

227-
var value = EvaluateFormulaInGroup(formula, executionContext, ref isCircularGroup);
235+
var value = EvaluateFormulaInGroup(vertex, executionContext, ref isCircularGroup);
228236
executionContext.ClearExecuting();
229237
ApplyVertexValue(vertex, value);
230238
}
231239
}
232240

233-
private CellValue EvaluateFormulaInGroup(CellFormula formula, FormulaExecutionContext executionContext,
241+
private CellValue EvaluateFormulaInGroup(FormulaVertex vertex, FormulaExecutionContext executionContext,
234242
ref bool isCircularGroup)
235243
{
236244
if (isCircularGroup)
237245
return CellValue.Error(ErrorType.Circular);
238246

247+
var formula = vertex.Formula!;
248+
239249
if (executionContext.TryGetExecutedValue(formula, out var cachedValue))
240250
return cachedValue;
241251

242-
var value = _evaluator.Evaluate(formula, executionContext);
252+
FormulaCallerInfo? caller = vertex.VertexType == VertexType.Cell
253+
? new FormulaCallerInfo(vertex.Row, vertex.Col, vertex.SheetName)
254+
: null;
255+
var value = _evaluator.Evaluate(formula, executionContext, caller: caller);
243256
executionContext.RecordExecuted(formula, value);
244257
if (value.Data is FormulaError formulaError && formulaError.ErrorType == ErrorType.Circular)
245258
isCircularGroup = true;
@@ -332,4 +345,4 @@ internal CellFormula CloneFormula(CellFormula formula)
332345
{
333346
return _environment.TryGetFunction(functionName, out var function) ? function : null;
334347
}
335-
}
348+
}

src/BlazorDatasheet.Core/FormulaEngine/WorkbookEnvironment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public CellValue GetVariable(string name)
2828
return _variables[name];
2929
}
3030

31+
public bool TryGetVariable(string name, out CellValue value)
32+
{
33+
return _variables.TryGetValue(name, out value);
34+
}
35+
3136
public void SetVariable(string name, object value)
3237
{
3338
SetVariable(name, new CellValue(value));
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
namespace BlazorDatasheet.Formula.Core;
22

3-
public class FunctionCallMetaData
3+
public readonly record struct FunctionCallMetaData(
4+
int? CallingRowIndex = null,
5+
int? CallingColumnIndex = null,
6+
string? CallingSheetName = null)
47
{
5-
internal FunctionCallMetaData(ParameterDefinition[] parameterDefinitions)
6-
{
7-
ParameterDefinitions = parameterDefinitions;
8-
}
9-
10-
public ParameterDefinition[] ParameterDefinitions { get; }
11-
}
8+
}

src/BlazorDatasheet.Formula.Core/FunctionDescriptor.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
namespace BlazorDatasheet.Formula.Core;
22

3+
public delegate CellValue FunctionInvoker(ReadOnlySpan<CellValue> args, FunctionCallMetaData callMetaData);
4+
35
public sealed class FunctionDescriptor
46
{
5-
private readonly Func<CellValue[], FunctionCallMetaData, CellValue> _invoker;
6-
private readonly FunctionCallMetaData _callMetaData;
7+
private readonly FunctionInvoker _invoker;
78

89
public string Name { get; }
910
public ParameterDefinition[] ParameterDefinitions { get; }
@@ -16,7 +17,7 @@ public sealed class FunctionDescriptor
1617
public FunctionDescriptor(
1718
string name,
1819
ParameterDefinition[] parameterDefinitions,
19-
Func<CellValue[], FunctionCallMetaData, CellValue> invoker,
20+
FunctionInvoker invoker,
2021
bool acceptsErrors = false,
2122
bool isVolatile = false,
2223
ReturnShape returnShape = ReturnShape.Scalar)
@@ -33,15 +34,14 @@ public FunctionDescriptor(
3334

3435
var validator = new FunctionParameterValidator();
3536
validator.ValidateOrThrow(ParameterDefinitions);
36-
_callMetaData = new FunctionCallMetaData(ParameterDefinitions);
3737

3838
MinArity = ComputeMinArity(ParameterDefinitions);
3939
MaxArity = ComputeMaxArity(ParameterDefinitions);
4040
}
4141

42-
public CellValue Invoke(CellValue[] args)
42+
public CellValue Invoke(ReadOnlySpan<CellValue> args, FunctionCallMetaData callMetaData)
4343
{
44-
return _invoker(args, _callMetaData);
44+
return _invoker(args, callMetaData);
4545
}
4646

4747
private static int ComputeMinArity(ParameterDefinition[] parameterDefinitions)

src/BlazorDatasheet.Formula.Core/IEnvironment.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ public interface IEnvironment : IFunctionProvider
1919
public CellValue[][] GetRangeValues(Reference reference);
2020
bool VariableExists(string variableIdentifier);
2121
CellValue GetVariable(string variableIdentifier);
22+
bool TryGetVariable(string variableIdentifier, out CellValue value);
2223
void SetVariable(string name, CellValue value);
2324
public IEnumerable<CellValue> GetNonEmptyInRange(Reference reference);
2425
void SetCellValue(int row, int col, string sheetName, CellValue value);
2526
void ClearVariable(string varName);
2627
IEnumerable<string> GetVariableNames();
27-
}
28+
}

src/BlazorDatasheet.Formula.Core/Interpreter/Evaluation/Evaluator.cs

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Buffers;
12
using BlazorDatasheet.Formula.Core.Interpreter.Parsing;
23
using BlazorDatasheet.Formula.Core.Interpreter.References;
34
using SyntaxTree = BlazorDatasheet.Formula.Core.Interpreter.Parsing.SyntaxTree;
@@ -13,7 +14,8 @@ public class Evaluator
1314

1415
private readonly record struct EvalContext(
1516
FormulaEvaluationOptions Options,
16-
FormulaExecutionContext ExecutionContext);
17+
FormulaExecutionContext ExecutionContext,
18+
FormulaCallerInfo? Caller);
1719

1820
public Evaluator(IEnvironment environment)
1921
{
@@ -25,11 +27,13 @@ public Evaluator(IEnvironment environment)
2527
}
2628

2729
public CellValue Evaluate(CellFormula cellFormula, FormulaExecutionContext? executionContext = null,
28-
FormulaEvaluationOptions? options = null)
30+
FormulaEvaluationOptions? options = null,
31+
FormulaCallerInfo? caller = null)
2932
{
3033
var ctx = new EvalContext(
3134
options ?? FormulaEvaluationOptions.Default,
32-
executionContext ?? new FormulaExecutionContext());
35+
executionContext ?? new FormulaExecutionContext(),
36+
caller);
3337
return DoEvaluate(cellFormula, ctx);
3438
}
3539

@@ -46,7 +50,7 @@ private CellValue DoEvaluate(CellFormula formula, EvalContext ctx)
4650
/// Evaluates a syntax tree
4751
/// </summary>
4852
internal CellValue Evaluate(SyntaxTree tree) =>
49-
EvaluateTree(tree, new EvalContext(FormulaEvaluationOptions.Default, new FormulaExecutionContext()));
53+
EvaluateTree(tree, new EvalContext(FormulaEvaluationOptions.Default, new FormulaExecutionContext(), null));
5054

5155
private CellValue EvaluateTree(SyntaxTree tree, EvalContext ctx)
5256
{
@@ -99,9 +103,9 @@ private CellValue EvaluateExpression(Expression expression, EvalContext ctx)
99103

100104
private CellValue EvaluateNamedExpression(VariableExpression expression)
101105
{
102-
if (!_environment.VariableExists(expression.NameToken.Value))
106+
if (!_environment.TryGetVariable(expression.NameToken.Value, out var value))
103107
return CellValue.Error(ErrorType.Ref);
104-
return _environment.GetVariable(expression.NameToken.Value);
108+
return value;
105109
}
106110

107111
private CellValue EvaluateArrayConstantExpression(ArrayConstantExpression expression)
@@ -157,7 +161,10 @@ private CellValue EvaluateCellReference(CellReference cellReference, EvalContext
157161
if (ctx.ExecutionContext.TryGetExecutedValue(formula, out var result))
158162
return result;
159163

160-
return DoEvaluate(formula, ctx);
164+
return DoEvaluate(formula, new EvalContext(
165+
ctx.Options,
166+
ctx.ExecutionContext,
167+
new FormulaCallerInfo(cellReference.RowIndex, cellReference.ColIndex, cellReference.SheetName)));
161168
}
162169

163170
private CellValue EvaluateRangeReference(RangeReference reference)
@@ -181,30 +188,45 @@ private CellValue EvaluateFunctionCall(FunctionExpression node, EvalContext ctx)
181188
return CellValue.Error(ErrorType.Na, "Incorrect number of function arguments");
182189
}
183190

184-
int paramIndex = 0;
185-
int argIndex = 0;
191+
var acceptsErrors = func.AcceptsErrors;
192+
var callMetaData = new FunctionCallMetaData(
193+
ctx.Caller?.RowIndex,
194+
ctx.Caller?.ColIndex,
195+
ctx.Caller?.SheetName);
196+
CellValue[]? rentedArgs = nArgsProvided == 0 ? null : ArrayPool<CellValue>.Shared.Rent(nArgsProvided);
197+
Span<CellValue> convertedArgs = nArgsProvided == 0
198+
? Span<CellValue>.Empty
199+
: rentedArgs.AsSpan(0, nArgsProvided);
200+
201+
try
202+
{
203+
int paramIndex = 0;
204+
int argIndex = 0;
186205

187-
CellValue[] convertedArgs = new CellValue[nArgsProvided];
206+
while (paramIndex < paramDefinitions.Length &&
207+
argIndex < nArgsProvided)
208+
{
209+
var paramDefinition = paramDefinitions[paramIndex];
210+
var arg = EvaluateExpression(node.Args[argIndex], ctx);
188211

189-
while (paramIndex < paramDefinitions.Length &&
190-
argIndex < nArgsProvided)
191-
{
192-
var paramDefinition = paramDefinitions[paramIndex];
193-
var arg = EvaluateExpression(node.Args[argIndex], ctx);
212+
convertedArgs[argIndex] = _parameterConverter.ConvertVal(arg, paramDefinition.Type);
194213

195-
convertedArgs[argIndex] = _parameterConverter.ConvertVal(arg, paramDefinition.Type);
214+
if (convertedArgs[argIndex].IsError() && !acceptsErrors)
215+
return convertedArgs[argIndex];
196216

197-
if (convertedArgs[argIndex].IsError() && !func.AcceptsErrors)
198-
return convertedArgs[argIndex];
217+
if (IsConsumable(paramDefinition))
218+
paramIndex++;
199219

200-
if (IsConsumable(paramDefinition))
201-
paramIndex++;
220+
argIndex++;
221+
}
202222

203-
argIndex++;
223+
return func.Invoke(convertedArgs[..nArgsProvided], callMetaData);
224+
}
225+
finally
226+
{
227+
if (rentedArgs != null)
228+
ArrayPool<CellValue>.Shared.Return(rentedArgs, clearArray: true);
204229
}
205-
206-
var funcResult = func.Invoke(convertedArgs);
207-
return funcResult;
208230
}
209231

210232
private bool IsConsumable(ParameterDefinition param)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace BlazorDatasheet.Formula.Core.Interpreter.Evaluation;
2+
3+
public readonly record struct FormulaCallerInfo(int RowIndex, int ColIndex, string SheetName)
4+
{
5+
}

src/BlazorDatasheet.Formula.Core/Interpreter/Evaluation/FormulaExecutionContext.cs

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,43 @@ namespace BlazorDatasheet.Formula.Core.Interpreter.Evaluation;
77
public class FormulaExecutionContext
88
{
99
private readonly Dictionary<CellFormula, CellValue> _executedValues = new();
10-
private readonly Stack<CellFormula> _executing = new();
10+
private readonly HashSet<CellFormula> _executing = new();
1111
private IList<FormulaVertex>? _currentSccGroup;
12+
private HashSet<CellPosition>? _currentSccCells;
13+
private HashSet<string>? _currentSccNames;
1214

13-
public void SetCurrentGroup(ref IList<FormulaVertex> group)
15+
public void SetCurrentGroup(IList<FormulaVertex> group)
1416
{
1517
_currentSccGroup = group;
18+
_currentSccCells ??= new HashSet<CellPosition>();
19+
_currentSccCells.Clear();
20+
_currentSccNames ??= new HashSet<string>(StringComparer.Ordinal);
21+
_currentSccNames.Clear();
22+
foreach (var vertex in group)
23+
{
24+
if (vertex.Position != null)
25+
_currentSccCells.Add(new CellPosition(vertex.Row, vertex.Col));
26+
else
27+
_currentSccNames.Add(vertex.Key);
28+
}
1629
}
1730

1831
internal bool IsInSccGroup(Reference reference)
1932
{
2033
if (_currentSccGroup == null)
2134
return false;
22-
23-
if (reference is not NamedReference namedRef)
24-
return _currentSccGroup.Any(x => x.Position != null && reference.Region.Contains(x.Row, x.Col));
25-
26-
return _currentSccGroup.Any(x => x.Key == namedRef.Name);
35+
36+
if (reference is NamedReference namedRef)
37+
return _currentSccNames?.Contains(namedRef.Name) == true;
38+
39+
var region = reference.Region;
40+
foreach (var cell in _currentSccCells!)
41+
{
42+
if (region.Contains(cell.row, cell.col))
43+
return true;
44+
}
45+
46+
return false;
2747
}
2848

2949
internal bool IsExecuting(CellFormula formula)
@@ -36,12 +56,6 @@ public void RecordExecuted(CellFormula formula, CellValue value)
3656
_executedValues.TryAdd(formula, value);
3757
}
3858

39-
/// <summary>
40-
/// If the formula has been executed, returns true and sets <paramref name="value"/> to the executed value.
41-
/// </summary>
42-
/// <param name="formula"></param>
43-
/// <param name="value"></param>
44-
/// <returns></returns>
4559
public bool TryGetExecutedValue(CellFormula formula, out CellValue value)
4660
{
4761
value = CellValue.Empty;
@@ -56,23 +70,20 @@ public bool TryGetExecutedValue(CellFormula formula, out CellValue value)
5670

5771
internal void SetExecuting(CellFormula formula)
5872
{
59-
_executing.Push(formula);
73+
_executing.Add(formula);
6074
}
6175

62-
/// <summary>
63-
/// Clears the record of executing <see cref="CellFormula"/>
64-
/// </summary>
6576
public void ClearExecuting()
6677
{
6778
_executing.Clear();
6879
}
6980

70-
/// <summary>
71-
/// Clears the execution context
72-
/// </summary>
7381
public void Clear()
7482
{
7583
_executing.Clear();
7684
_executedValues.Clear();
85+
_currentSccGroup = null;
86+
_currentSccCells?.Clear();
87+
_currentSccNames?.Clear();
7788
}
78-
}
89+
}

src/BlazorDatasheet.Formula.Core/Interpreter/ParsingEnvironment.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public IEnumerable<FunctionDefinition> SearchForFunctions(string functionName)
2727
public CellValue[][] GetRangeValues(Reference reference) => throw NotSupported();
2828
public bool VariableExists(string variableIdentifier) => throw NotSupported();
2929
public CellValue GetVariable(string variableIdentifier) => throw NotSupported();
30+
public bool TryGetVariable(string variableIdentifier, out CellValue value) => throw NotSupported();
3031
public void SetVariable(string name, CellValue value) => throw NotSupported();
3132
public IEnumerable<CellValue> GetNonEmptyInRange(Reference reference) => throw NotSupported();
3233
public void SetCellValue(int row, int col, string sheetName, CellValue value) => throw NotSupported();

0 commit comments

Comments
 (0)