mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-04 13:45:20 +00:00
Compare commits
145 Commits
accessibil
...
spreadshee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0f8892a8 | ||
|
|
452e3f046c | ||
|
|
797fcefbc8 | ||
|
|
9677545c6c | ||
|
|
00f37c4562 | ||
|
|
69dfd2a672 | ||
|
|
8f35d495f2 | ||
|
|
e6e91e96d7 | ||
|
|
7b0e05069e | ||
|
|
425b36db7f | ||
|
|
7e28102477 | ||
|
|
722ae13c00 | ||
|
|
11863e2f91 | ||
|
|
0b5e629f78 | ||
|
|
1639325741 | ||
|
|
665372b397 | ||
|
|
0d218e174f | ||
|
|
c88a4c19fe | ||
|
|
bb1b810c72 | ||
|
|
d098a6c0f1 | ||
|
|
f0edbce9c6 | ||
|
|
112f7d1b70 | ||
|
|
cd6eba4882 | ||
|
|
b1aae0904c | ||
|
|
019977d7e7 | ||
|
|
c556e0de1a | ||
|
|
8fdf1ba312 | ||
|
|
7fe4f0f96d | ||
|
|
c70efee29a | ||
|
|
b1c1687c48 | ||
|
|
082894054b | ||
|
|
da19e7404b | ||
|
|
3cb7106c4f | ||
|
|
716a75872d | ||
|
|
ae1c0d8ee1 | ||
|
|
463db22a2e | ||
|
|
f104d939a2 | ||
|
|
9da74fb81d | ||
|
|
b694daa52e | ||
|
|
4fd3e3c910 | ||
|
|
4c619fa5c9 | ||
|
|
83bf1ae2cd | ||
|
|
f75ed5734c | ||
|
|
09f229602d | ||
|
|
c8225a71bb | ||
|
|
1e881ddd6e | ||
|
|
095c5d2059 | ||
|
|
019f367466 | ||
|
|
28918e792b | ||
|
|
57d832549d | ||
|
|
7c0768fc7a | ||
|
|
fd69705fd9 | ||
|
|
55b58a744c | ||
|
|
e264d654c6 | ||
|
|
d0f00bd7f6 | ||
|
|
1a62deaf35 | ||
|
|
7fcf4047a1 | ||
|
|
ac4fd8d5b3 | ||
|
|
48d1c87908 | ||
|
|
dd7a13f3cf | ||
|
|
db3c81388a | ||
|
|
1e6dd77759 | ||
|
|
8a81491c29 | ||
|
|
86245ae162 | ||
|
|
05b957a8d3 | ||
|
|
21828c6ae7 | ||
|
|
89e91e5c0e | ||
|
|
06c4d41273 | ||
|
|
60cb216fbd | ||
|
|
f4d916a0de | ||
|
|
eb54d808cf | ||
|
|
0565451a97 | ||
|
|
8174ac2725 | ||
|
|
493bd69a1e | ||
|
|
794d8255cf | ||
|
|
edadfe493b | ||
|
|
c7bda57838 | ||
|
|
947a23a4cb | ||
|
|
bf30dabe4d | ||
|
|
c5b6b8da95 | ||
|
|
e171252ecc | ||
|
|
7d090a4147 | ||
|
|
bbd2cd777b | ||
|
|
d7bc875d83 | ||
|
|
ae997416a2 | ||
|
|
97d94b2512 | ||
|
|
14ec0a8e4e | ||
|
|
985a802314 | ||
|
|
a3b4526c33 | ||
|
|
42e595ed85 | ||
|
|
cc62ceae3b | ||
|
|
1d0e748c91 | ||
|
|
b70a6cba99 | ||
|
|
cb469e97c5 | ||
|
|
0ebc789de0 | ||
|
|
299f565d66 | ||
|
|
8dd6aba59c | ||
|
|
ebf0e4fadf | ||
|
|
2e1aeffbf8 | ||
|
|
0a83fee55d | ||
|
|
5a89117c6f | ||
|
|
eb0e96ea42 | ||
|
|
a9f38fa9e9 | ||
|
|
b680633b5c | ||
|
|
ff5ce9298a | ||
|
|
2b4bf2aef4 | ||
|
|
fd59a1a1be | ||
|
|
3ff93dd0e6 | ||
|
|
b8edfad166 | ||
|
|
6aaf42d883 | ||
|
|
9306c6f6d7 | ||
|
|
1ddc854da9 | ||
|
|
ae5c48f5e5 | ||
|
|
e47848bf5a | ||
|
|
e09002fa43 | ||
|
|
eb817de8e4 | ||
|
|
b2bc210eca | ||
|
|
da230e0cbe | ||
|
|
ffae640c3f | ||
|
|
9f3e0d6098 | ||
|
|
4078927d58 | ||
|
|
c7994e0479 | ||
|
|
26c87de08c | ||
|
|
0b938dd51c | ||
|
|
8427d8688b | ||
|
|
9f198cbd23 | ||
|
|
a97e378800 | ||
|
|
790f13ef2b | ||
|
|
179d59c195 | ||
|
|
87bdc2f859 | ||
|
|
d75cf817c4 | ||
|
|
ca0806d5b1 | ||
|
|
3318386fe6 | ||
|
|
44a835cfe8 | ||
|
|
81b28b9602 | ||
|
|
dc8ea54c91 | ||
|
|
1d40582cbd | ||
|
|
e4cbcfdf74 | ||
|
|
7ab385e552 | ||
|
|
9b713a7f38 | ||
|
|
11400e3690 | ||
|
|
fc31bd6fc6 | ||
|
|
d1d9a254d7 | ||
|
|
d79f3a887d | ||
|
|
849ec9c91a |
@@ -328,7 +328,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
var result = data.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
Assert.Equal(1, result.Count);
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Eve", result[0].Name);
|
||||
}
|
||||
|
||||
|
||||
49
Radzen.Blazor.Tests/Spreadsheet/AggregateFunctionTests.cs
Normal file
49
Radzen.Blazor.Tests/Spreadsheet/AggregateFunctionTests.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class AggregateFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(15, 5);
|
||||
|
||||
void SeedWithErrors()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2/0"; // #DIV/0!
|
||||
sheet.Cells["A2"].Value = 82;
|
||||
sheet.Cells["A3"].Value = 72;
|
||||
sheet.Cells["A4"].Value = 65;
|
||||
sheet.Cells["A5"].Value = 30;
|
||||
sheet.Cells["A6"].Value = 95;
|
||||
sheet.Cells["A7"].Formula = "=0/0"; // #DIV/0!
|
||||
sheet.Cells["A8"].Value = 63;
|
||||
sheet.Cells["A9"].Value = 31;
|
||||
sheet.Cells["A10"].Value = 53;
|
||||
sheet.Cells["A11"].Value = 96;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldComputeMaxIgnoringErrors()
|
||||
{
|
||||
SeedWithErrors();
|
||||
sheet.Cells["B1"].Formula = "=AGGREGATE(4,6,A1:A11)"; // MAX ignoring errors
|
||||
Assert.Equal(96d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldComputeLargeIgnoringErrors()
|
||||
{
|
||||
SeedWithErrors();
|
||||
sheet.Cells["B1"].Formula = "=AGGREGATE(14,6,A1:A11,3)"; // LARGE k=3 ignoring errors
|
||||
Assert.Equal(82d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorWhenKMissingForSmall()
|
||||
{
|
||||
SeedWithErrors();
|
||||
sheet.Cells["B1"].Formula = "=AGGREGATE(15,6,A1:A11)"; // SMALL requires k
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
160
Radzen.Blazor.Tests/Spreadsheet/AndFunctionTests.cs
Normal file
160
Radzen.Blazor.Tests/Spreadsheet/AndFunctionTests.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class AndFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithAllTrueValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithOneFalseValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithAllFalseValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithNumericValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1>1,A2<100)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithZeroAsFalse()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithNonZeroAsTrue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithStringValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "test";
|
||||
sheet.Cells["A2"].Value = "hello";
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithEmptyStringAsFalse()
|
||||
{
|
||||
sheet.Cells["A2"].Value = "hello";
|
||||
sheet.Cells["A3"].Formula = "=AND(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Formula = "=AND(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithOneFalseInMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Formula = "=AND(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForEmptyAndFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=AND()";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithRangeExpression()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Formula = "=AND(A1:A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionWithMixedTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = "3";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Formula = "=AND(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionInIfStatement()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=IF(AND(A1>1,A2<100),A1,\"Out of range\")";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionInIfStatementWithFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 150;
|
||||
sheet.Cells["A3"].Formula = "=IF(AND(A1>1,A2<100),A1,\"Out of range\")";
|
||||
|
||||
Assert.Equal("Out of range", sheet.Cells["A3"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
Radzen.Blazor.Tests/Spreadsheet/AutoFilterTests.cs
Normal file
105
Radzen.Blazor.Tests/Spreadsheet/AutoFilterTests.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class AutoFilterTests
|
||||
{
|
||||
private readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void Should_ToggleSheetAutoFilter()
|
||||
{
|
||||
// Initially no auto filter
|
||||
Assert.Null(sheet.AutoFilter);
|
||||
|
||||
// Apply auto filter to range A1:C5
|
||||
var range = RangeRef.Parse("A1:C5");
|
||||
var command = new SheetAutoFilterCommand(sheet, range);
|
||||
command.Execute();
|
||||
|
||||
// Auto filter should be applied
|
||||
Assert.NotNull(sheet.AutoFilter);
|
||||
Assert.Equal(range, sheet.AutoFilter.Range);
|
||||
|
||||
// Undo the command
|
||||
command.Unexecute();
|
||||
|
||||
// Auto filter should be removed
|
||||
Assert.Null(sheet.AutoFilter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_ToggleDataTableFilterButton()
|
||||
{
|
||||
// Add a data table
|
||||
var range = RangeRef.Parse("A1:C5");
|
||||
sheet.AddTable(range);
|
||||
|
||||
var table = sheet.Tables[0];
|
||||
|
||||
// Initially ShowFilterButton should be true
|
||||
Assert.True(table.ShowFilterButton);
|
||||
|
||||
// Toggle filter button off
|
||||
var command = new TableFilterCommand(sheet, 0);
|
||||
command.Execute();
|
||||
|
||||
// ShowFilterButton should be false
|
||||
Assert.False(table.ShowFilterButton);
|
||||
|
||||
// Undo the command
|
||||
command.Unexecute();
|
||||
|
||||
// ShowFilterButton should be true again
|
||||
Assert.True(table.ShowFilterButton);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_HandleMultipleDataTables()
|
||||
{
|
||||
// Add two data tables
|
||||
sheet.AddTable(RangeRef.Parse("A1:C5"));
|
||||
sheet.AddTable(RangeRef.Parse("E1:G5"));
|
||||
|
||||
var table1 = sheet.Tables[0];
|
||||
var table2 = sheet.Tables[1];
|
||||
|
||||
// Initially both should have ShowFilterButton = true
|
||||
Assert.True(table1.ShowFilterButton);
|
||||
Assert.True(table2.ShowFilterButton);
|
||||
|
||||
// Toggle filter button for first data table
|
||||
var command1 = new TableFilterCommand(sheet, 0);
|
||||
command1.Execute();
|
||||
|
||||
// Only first data table should be affected
|
||||
Assert.False(table1.ShowFilterButton);
|
||||
Assert.True(table2.ShowFilterButton);
|
||||
|
||||
// Toggle filter button for second data table
|
||||
var command2 = new TableFilterCommand(sheet, 1);
|
||||
command2.Execute();
|
||||
|
||||
// Both should be affected
|
||||
Assert.False(table1.ShowFilterButton);
|
||||
Assert.False(table2.ShowFilterButton);
|
||||
|
||||
// Undo second command
|
||||
command2.Unexecute();
|
||||
|
||||
// Only second data table should be restored
|
||||
Assert.False(table1.ShowFilterButton);
|
||||
Assert.True(table2.ShowFilterButton);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_HandleInvalidDataTableIndex()
|
||||
{
|
||||
// Try to toggle filter button for non-existent data table
|
||||
var command = new TableFilterCommand(sheet, 0);
|
||||
|
||||
// Should not throw exception
|
||||
var result = command.Execute();
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
||||
114
Radzen.Blazor.Tests/Spreadsheet/AverageFunctionTests.cs
Normal file
114
Radzen.Blazor.Tests/Spreadsheet/AverageFunctionTests.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class AverageFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionWithTwoArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=AVERAGE(A1,A2)";
|
||||
|
||||
Assert.Equal(12.5, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionWithEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=AVERAGE(A1,A2)";
|
||||
|
||||
Assert.Equal(10.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=AVERAGE(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(15.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnDiv0ErrorForEmptyAverageFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=AVERAGE()";
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnDiv0ErrorForAverageFunctionWithNoNumericValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "text";
|
||||
sheet.Cells["A2"].Value = "";
|
||||
sheet.Cells["A3"].Formula = "=AVERAGE(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionWithRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=AVERAGE(A1:A2)";
|
||||
|
||||
Assert.Equal(12.5, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionWithMixedTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15.5;
|
||||
sheet.Cells["A3"].Formula = "=AVERAGE(A1,A2)";
|
||||
|
||||
Assert.Equal(12.75, sheet.Cells["A3"].Value);
|
||||
|
||||
sheet.Cells["A4"].Value = 2.5;
|
||||
sheet.Cells["A5"].Formula = "=AVERAGE(A4,A1)";
|
||||
|
||||
Assert.Equal(6.25, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionIgnoringTextAndLogicalValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Value = 20;
|
||||
sheet.Cells["A5"].Formula = "=AVERAGE(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(15.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAverageFunctionIncludingZeroValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=AVERAGE(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(10.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenAverageRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=AVERAGE(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
261
Radzen.Blazor.Tests/Spreadsheet/CellSelectionTests.cs
Normal file
261
Radzen.Blazor.Tests/Spreadsheet/CellSelectionTests.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using Bunit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class CellSelectionTests : TestContext
|
||||
{
|
||||
private readonly Sheet sheet = new (4,4);
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_RendersWithCorrectClasses()
|
||||
{
|
||||
// Arrange
|
||||
var cell = new CellRef(0, 0);
|
||||
sheet.Selection.Select(new RangeRef(cell, cell));
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, cell)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-cell");
|
||||
Assert.NotNull(element);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell", element.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", element.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", element.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", element.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", element.ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_AppliesFrozenColumnClass()
|
||||
{
|
||||
// Arrange
|
||||
var cell = new CellRef(0, 0);
|
||||
sheet.Columns.Frozen = 1;
|
||||
sheet.Selection.Select(new RangeRef(cell, cell));
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, cell)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-cell");
|
||||
Assert.Contains("rz-spreadsheet-frozen-column", element.ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_AppliesFrozenRowClass()
|
||||
{
|
||||
// Arrange
|
||||
var cell = new CellRef(0, 0);
|
||||
var context = new MockVirtualGridContext();
|
||||
sheet.Rows.Frozen = 1;
|
||||
sheet.Selection.Select(new RangeRef(cell, cell));
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, cell)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-cell");
|
||||
Assert.Contains("rz-spreadsheet-frozen-row", element.ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_CalculatesStyle()
|
||||
{
|
||||
// Arrange
|
||||
var cell = new CellRef(0, 0);
|
||||
sheet.Selection.Select(new RangeRef(cell, cell));
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, cell)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-cell");
|
||||
Assert.Equal("transform: translate(0px, 0px); width: 100px; height: 24px", element.GetAttribute("style"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_SplitsMergedCell_WhenIntersectingFrozenRow()
|
||||
{
|
||||
// Arrange
|
||||
sheet.Rows.Frozen = 1;
|
||||
var range = new RangeRef(new CellRef(0, 0), new CellRef(2, 0));
|
||||
sheet.MergedCells.Add(range);
|
||||
sheet.Selection.Select(range);
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, new CellRef(0, 0))
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var elements = cut.FindAll(".rz-spreadsheet-selection-cell");
|
||||
Assert.Equal(2, elements.Count);
|
||||
|
||||
var frozen = cut.Find(".rz-spreadsheet-frozen-row");
|
||||
|
||||
Assert.NotNull(frozen);
|
||||
|
||||
// First element (frozen)
|
||||
Assert.Equal("transform: translate(0px, 0px); width: 100px; height: 24px", frozen.GetAttribute("style"));
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", frozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", frozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", frozen.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-bottom", frozen.ClassName);
|
||||
|
||||
var unfrozen = elements.Where(e => e != frozen).FirstOrDefault();
|
||||
|
||||
Assert.NotNull(unfrozen);
|
||||
|
||||
// Second element (non-frozen)
|
||||
Assert.Equal("transform: translate(0px, 24px); width: 100px; height: 48px", unfrozen.GetAttribute("style"));
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-top", unfrozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", unfrozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", unfrozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", unfrozen.ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_SplitsMergedCell_WhenIntersectingFrozenColumn()
|
||||
{
|
||||
// Arrange
|
||||
sheet.Columns.Frozen = 1;
|
||||
var range = new RangeRef(new CellRef(0, 0), new CellRef(0, 2));
|
||||
sheet.MergedCells.Add(range);
|
||||
sheet.Selection.Select(range);
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, new CellRef(0, 0))
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var elements = cut.FindAll(".rz-spreadsheet-selection-cell");
|
||||
Assert.Equal(2, elements.Count);
|
||||
|
||||
var frozen = cut.Find(".rz-spreadsheet-frozen-column");
|
||||
|
||||
Assert.NotNull(frozen);
|
||||
|
||||
// First element (frozen)
|
||||
Assert.Contains("rz-spreadsheet-frozen-column", frozen.ClassName);
|
||||
Assert.Equal("transform: translate(0px, 0px); width: 100px; height: 24px", frozen.GetAttribute("style"));
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", frozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", frozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", frozen.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-right", frozen.ClassName);
|
||||
|
||||
var unfrozen = elements.Where(e => e != frozen).FirstOrDefault();
|
||||
|
||||
Assert.NotNull(unfrozen);
|
||||
// Second element (non-frozen)
|
||||
Assert.Equal("transform: translate(100px, 0px); width: 200px; height: 24px", unfrozen.GetAttribute("style"));
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", unfrozen.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-left", unfrozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", unfrozen.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", unfrozen.ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellSelection_SplitsMergedCell_WhenIntersectingBothFrozen()
|
||||
{
|
||||
// Arrange
|
||||
sheet.Rows.Frozen = 1;
|
||||
sheet.Columns.Frozen = 1;
|
||||
var range = new RangeRef(new CellRef(0, 0), new CellRef(2, 2));
|
||||
sheet.MergedCells.Add(range);
|
||||
sheet.Selection.Select(range);
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<CellSelection>(parameters => parameters
|
||||
.Add(p => p.Cell, new CellRef(0, 0))
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var elements = cut.FindAll(".rz-spreadsheet-selection-cell");
|
||||
Assert.Equal(4, elements.Count);
|
||||
|
||||
// Top-left element (both frozen)
|
||||
var both = cut.Find(".rz-spreadsheet-frozen-row.rz-spreadsheet-frozen-column");
|
||||
Assert.NotNull(both);
|
||||
Assert.Contains("rz-spreadsheet-frozen-row", both.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-frozen-column", both.ClassName);
|
||||
Assert.Equal("transform: translate(0px, 0px); width: 100px; height: 24px", both.GetAttribute("style"));
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", both.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", both.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-bottom", both.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-right", both.ClassName);
|
||||
|
||||
// Bottom-left element (column frozen)
|
||||
var frozenColumn = cut.Find(".rz-spreadsheet-frozen-column:not(.rz-spreadsheet-frozen-row)");
|
||||
Assert.NotNull(frozenColumn);
|
||||
Assert.DoesNotContain("rz-spreadsheet-frozen-row", frozenColumn.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-frozen-column", frozenColumn.ClassName);
|
||||
Assert.Equal("transform: translate(0px, 24px); width: 100px; height: 48px", frozenColumn.GetAttribute("style"));
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-top", frozenColumn.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-left", frozenColumn.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", frozenColumn.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-right", frozenColumn.ClassName);
|
||||
|
||||
// Top-right element (row frozen)
|
||||
var frozenRow = cut.Find(".rz-spreadsheet-frozen-row:not(.rz-spreadsheet-frozen-column)");
|
||||
Assert.NotNull(frozenRow);
|
||||
Assert.Contains("rz-spreadsheet-frozen-row", frozenRow.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-frozen-column", frozenRow.ClassName);
|
||||
Assert.Equal("transform: translate(100px, 0px); width: 200px; height: 24px", frozenRow.GetAttribute("style"));
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-top", frozenRow.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-left", frozenRow.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-bottom", frozenRow.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", frozenRow.ClassName);
|
||||
|
||||
// Bottom-right element (neither frozen)
|
||||
var neither = elements.FirstOrDefault(e => e != both && e != frozenColumn && e != frozenRow);
|
||||
Assert.NotNull(neither);
|
||||
Assert.DoesNotContain("rz-spreadsheet-frozen-row", neither.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-frozen-column", neither.ClassName);
|
||||
Assert.Equal("transform: translate(100px, 24px); width: 200px; height: 48px", neither.GetAttribute("style"));
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-top", neither.ClassName);
|
||||
Assert.DoesNotContain("rz-spreadsheet-selection-cell-left", neither.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-bottom", neither.ClassName);
|
||||
Assert.Contains("rz-spreadsheet-selection-cell-right", neither.ClassName);
|
||||
}
|
||||
}
|
||||
|
||||
public class MockVirtualGridContext : IVirtualGridContext
|
||||
{
|
||||
private readonly Dictionary<(int Row, int Column), PixelRectangle> rectangle = [];
|
||||
|
||||
public void SetupRectangle(int row, int column, PixelRectangle rectangle)
|
||||
{
|
||||
this.rectangle[(row, column)] = rectangle;
|
||||
}
|
||||
|
||||
public PixelRectangle GetRectangle(int row, int column) => throw new NotImplementedException();
|
||||
|
||||
public PixelRectangle GetRectangle(int top, int left, int bottom, int right) => new(new (left * 100, (right + 1) * 100), new (top*24, (bottom + 1)*24));
|
||||
}
|
||||
69
Radzen.Blazor.Tests/Spreadsheet/CellStoreTests.cs
Normal file
69
Radzen.Blazor.Tests/Spreadsheet/CellStoreTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class CellStoreTests
|
||||
{
|
||||
readonly CellStore cellStore = new(new Sheet(5, 5));
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldReturnNewCell_WhenCellDoesNotExist()
|
||||
{
|
||||
var cell = cellStore[0, 0];
|
||||
|
||||
Assert.NotNull(cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldThrowArgumentOutOfRangeException_WhenRowExceedsMax()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => cellStore[5, 0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldThrowArgumentOutOfRangeException_WhenColumnExceedsMax()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => cellStore[0, 5]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldReturnExistingCell_WhenCellExists()
|
||||
{
|
||||
var expectedCell = new Cell(cellStore.Sheet, new CellRef(0, 0));
|
||||
cellStore[0, 0] = expectedCell;
|
||||
var cell = cellStore[0, 0];
|
||||
Assert.Same(expectedCell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldReturnExistingCell_ViaA1Notation()
|
||||
{
|
||||
var expectedCell = new Cell(cellStore.Sheet, new CellRef(0, 0));
|
||||
|
||||
cellStore[0, 0] = expectedCell;
|
||||
|
||||
var cell = cellStore["A1"];
|
||||
|
||||
Assert.Same(expectedCell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldThrowException_WhenInvalidA1Notation()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => cellStore["Invalid"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellStore_ShouldSupport_MultipleLettersInA1Notation()
|
||||
{
|
||||
var cellStore = new CellStore(new Sheet(5, 30));
|
||||
var expectedCell = new Cell(cellStore.Sheet, new CellRef(0, 26));
|
||||
|
||||
cellStore[0, 26] = expectedCell;
|
||||
|
||||
var cell = cellStore["AA1"];
|
||||
|
||||
Assert.Same(expectedCell, cell);
|
||||
}
|
||||
}
|
||||
34
Radzen.Blazor.Tests/Spreadsheet/ChooseFunctionTests.cs
Normal file
34
Radzen.Blazor.Tests/Spreadsheet/ChooseFunctionTests.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ChooseFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldPickScalarByIndex()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=CHOOSE(3,\"Wide\",115,\"world\",8)";
|
||||
Assert.Equal("world", sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPickCellReferenceByIndex()
|
||||
{
|
||||
sheet.Cells["A2"].Value = "1st";
|
||||
sheet.Cells["A3"].Value = "2nd";
|
||||
sheet.Cells["A4"].Value = "3rd";
|
||||
sheet.Cells["A5"].Value = "Finished";
|
||||
|
||||
sheet.Cells["B1"].Formula = "=CHOOSE(2,A2,A3,A4,A5)";
|
||||
Assert.Equal("2nd", sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorWhenIndexOutOfRange()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=CHOOSE(5,1,2,3)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
40
Radzen.Blazor.Tests/Spreadsheet/ColumnFunctionTests.cs
Normal file
40
Radzen.Blazor.Tests/Spreadsheet/ColumnFunctionTests.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ColumnFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Column_OmittedReference_ReturnsCurrentColumn()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["C10"].Formula = "=COLUMN()";
|
||||
Assert.Equal(3d, sheet.Cells["C10"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Column_SingleCellReference_ReturnsThatColumn()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["A1"].Formula = "=COLUMN(C10)";
|
||||
Assert.Equal(3d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Column_RangeReference_SingleRow_ReturnsLeftmostColumn()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["B2"].Formula = "=COLUMN(C10:E10)";
|
||||
Assert.Equal(3d, sheet.Cells["B2"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Column_RangeReference_MultiRowAndColumn_IsError()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["B2"].Formula = "=COLUMN(C10:D20)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B2"].Data.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
Radzen.Blazor.Tests/Spreadsheet/ColumnsFunctionTests.cs
Normal file
30
Radzen.Blazor.Tests/Spreadsheet/ColumnsFunctionTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ColumnsFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Columns_Range_ReturnsColumnCount()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=COLUMNS(C1:E4)";
|
||||
Assert.Equal(3d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_SingleCell_ReturnsOne()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=COLUMNS(C10)";
|
||||
Assert.Equal(1d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_SingleColumnRange_ReturnsOne()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=COLUMNS(C10:C20)";
|
||||
Assert.Equal(1d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
}
|
||||
44
Radzen.Blazor.Tests/Spreadsheet/ConcatFunctionTests.cs
Normal file
44
Radzen.Blazor.Tests/Spreadsheet/ConcatFunctionTests.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ConcatFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Concat_Literals_Works()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["A1"].Formula = "=CONCAT(\"The\",\" \",\"sun\",\" \",\"will\",\" \",\"come\",\" \",\"up\",\" \",\"tomorrow.\")";
|
||||
Assert.Equal("The sun will come up tomorrow.", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concat_SingleRange_LinearizesRowMajor()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["B2"].Value = "a1";
|
||||
sheet.Cells["C2"].Value = "b1";
|
||||
sheet.Cells["B3"].Value = "a2";
|
||||
sheet.Cells["C3"].Value = "b2";
|
||||
sheet.Cells["B4"].Value = "a4";
|
||||
sheet.Cells["C4"].Value = "b4";
|
||||
sheet.Cells["B5"].Value = "a5";
|
||||
sheet.Cells["C5"].Value = "b5";
|
||||
sheet.Cells["B6"].Value = "a6";
|
||||
sheet.Cells["C6"].Value = "b6";
|
||||
sheet.Cells["B7"].Value = "a7";
|
||||
sheet.Cells["C7"].Value = "b7";
|
||||
sheet.Cells["A1"].Formula = "=CONCAT(B2:C7)";
|
||||
Assert.Equal("a1b1a2b2a4b4a5b5a6b6a7b7", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concat_MixedArgs_RangeAndLiterals()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["B2"].Value = "Andreas";
|
||||
sheet.Cells["C2"].Value = "Hauser";
|
||||
sheet.Cells["A1"].Formula = "=CONCAT(B2,\" \",C2)";
|
||||
Assert.Equal("Andreas Hauser", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
}
|
||||
150
Radzen.Blazor.Tests/Spreadsheet/CountAllFunctionTests.cs
Normal file
150
Radzen.Blazor.Tests/Spreadsheet/CountAllFunctionTests.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class CountAllFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithTwoArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=COUNTA(A1,A2)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=COUNTA(A1,A2)";
|
||||
|
||||
Assert.Equal(1.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=COUNTA(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroForEmptyCountaFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNTA()";
|
||||
|
||||
Assert.Equal(0.0, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=COUNTA(A1:A2)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionIncludingTextValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=COUNTA(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionIncludingLogicalValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Value = false;
|
||||
sheet.Cells["A4"].Value = 20;
|
||||
sheet.Cells["A5"].Formula = "=COUNTA(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(4.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionIncludingEmptyStrings()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "";
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=COUNTA(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionIgnoringTrulyEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A4"].Value = 20;
|
||||
sheet.Cells["A5"].Formula = "=COUNTA(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithAllNumericValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["A4"].Formula = "=COUNTA(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldShowDifferenceBetweenCountAndCounta()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Value = "";
|
||||
sheet.Cells["A5"].Value = 20;
|
||||
sheet.Cells["B1"].Formula = "=COUNT(A1,A2,A3,A4,A5)";
|
||||
sheet.Cells["B2"].Formula = "=COUNTA(A1,A2,A3,A4,A5)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["B1"].Value);
|
||||
Assert.Equal(5.0, sheet.Cells["B2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountaFunctionWithMixedTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Value = "";
|
||||
sheet.Cells["A5"].Value = 3.14;
|
||||
sheet.Cells["B1"].Formula = "=COUNTA(A1,A2,A3,A4,A5)";
|
||||
|
||||
Assert.Equal(5.0, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenCountaRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNTA(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
139
Radzen.Blazor.Tests/Spreadsheet/CountFunctionTests.cs
Normal file
139
Radzen.Blazor.Tests/Spreadsheet/CountFunctionTests.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class CountFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithTwoArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=COUNT(A1,A2)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=COUNT(A1,A2)";
|
||||
|
||||
Assert.Equal(1.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=COUNT(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroForEmptyCountFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNT()";
|
||||
|
||||
Assert.Equal(0.0, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15;
|
||||
sheet.Cells["A3"].Formula = "=COUNT(A1:A2)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithMixedTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 15.5;
|
||||
sheet.Cells["A3"].Formula = "=COUNT(A1,A2)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A3"].Value);
|
||||
|
||||
sheet.Cells["A4"].Value = 2.5;
|
||||
sheet.Cells["A5"].Formula = "=COUNT(A4,A1)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionIncludingLogicalValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Value = false;
|
||||
sheet.Cells["A4"].Value = 20;
|
||||
sheet.Cells["A5"].Formula = "=COUNT(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(4.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionIncludingTextRepresentationsOfNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "15";
|
||||
sheet.Cells["A3"].Value = "text";
|
||||
sheet.Cells["A4"].Value = "3.14";
|
||||
sheet.Cells["A5"].Formula = "=COUNT(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionIgnoringTextAndEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = "";
|
||||
sheet.Cells["A4"].Value = 20;
|
||||
sheet.Cells["A5"].Formula = "=COUNT(A1,A2,A3,A4)";
|
||||
|
||||
Assert.Equal(2.0, sheet.Cells["A5"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionIncludingZeroValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Value = 20;
|
||||
sheet.Cells["A4"].Formula = "=COUNT(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateCountFunctionWithAllNumericValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["A4"].Formula = "=COUNT(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(3.0, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenCountRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNT(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
Radzen.Blazor.Tests/Spreadsheet/DayFunctionTests.cs
Normal file
32
Radzen.Blazor.Tests/Spreadsheet/DayFunctionTests.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class DayFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Day_FromDateSerial_ReturnsDay()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
// Using DATEVALUE via VALUE on a date string to get a serial
|
||||
sheet.Cells["A1"].Formula = "=DAY(VALUE(\"2011-04-15\"))";
|
||||
Assert.Equal(15, sheet.Cells["A1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Day_FromDateValue_ReturnsDay()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2011, 4, 15));
|
||||
sheet.Cells["B1"].Formula = "=DAY(A1)";
|
||||
Assert.Equal(15, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Day_InvalidText_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=DAY(\"abc\")";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
81
Radzen.Blazor.Tests/Spreadsheet/DeleteRowColumnTests.cs
Normal file
81
Radzen.Blazor.Tests/Spreadsheet/DeleteRowColumnTests.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class DeleteRowColumnTests
|
||||
{
|
||||
[Fact]
|
||||
public void DeleteColumn_ShiftsDataAndDecreasesColumnCount()
|
||||
{
|
||||
var sheet = new Sheet(3, 4);
|
||||
sheet.Cells[0, 0].Value = "A";
|
||||
sheet.Cells[0, 1].Value = "B";
|
||||
sheet.Cells[0, 2].Value = "C";
|
||||
sheet.Cells[0, 3].Value = "D";
|
||||
|
||||
sheet.DeleteColumn(1); // delete column B
|
||||
|
||||
Assert.Equal(3, sheet.ColumnCount);
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("C", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("D", sheet.Cells[0, 2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteRow_ShiftsDataAndDecreasesRowCount()
|
||||
{
|
||||
var sheet = new Sheet(4, 2);
|
||||
sheet.Cells[0, 0].Value = "R1";
|
||||
sheet.Cells[1, 0].Value = "R2";
|
||||
sheet.Cells[2, 0].Value = "R3";
|
||||
sheet.Cells[3, 0].Value = "R4";
|
||||
|
||||
sheet.DeleteRow(1); // delete row 2
|
||||
|
||||
Assert.Equal(3, sheet.RowCount);
|
||||
Assert.Equal("R1", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("R3", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("R4", sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteColumn_DoesNotAdjustFormulas_RefsBecomeError()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.Cells[0, 0].Value = 1; // A1
|
||||
sheet.Cells[0, 1].Value = 2; // B1
|
||||
sheet.Cells[0, 2].Value = 3; // C1
|
||||
|
||||
// Formula in B2 references A1 and C1
|
||||
sheet.Cells[1, 1].Formula = "=A1+C1";
|
||||
Assert.Equal(4d, sheet.Cells[1, 1].Value);
|
||||
|
||||
// Delete referenced column A -> A1 becomes invalid => #REF!
|
||||
sheet.DeleteColumn(0);
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=#REF!+C1", sheet.Cells[1, 0].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteRow_DoesNotAdjustFormulas_RefsBecomeError()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.Cells[0, 0].Value = 1; // A1
|
||||
sheet.Cells[1, 0].Value = 2; // A2
|
||||
sheet.Cells[2, 0].Value = 3; // A3
|
||||
|
||||
// Formula in B2 references A1 and A3
|
||||
sheet.Cells[1, 1].Formula = "=A1+A3";
|
||||
Assert.Equal(4d, sheet.Cells[1, 1].Value);
|
||||
|
||||
// Delete referenced row 1 -> A1 becomes invalid => #REF!
|
||||
sheet.DeleteRow(0);
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("=#REF!+A3", sheet.Cells[0, 1].Formula);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
146
Radzen.Blazor.Tests/Spreadsheet/FilterCommandTests.cs
Normal file
146
Radzen.Blazor.Tests/Spreadsheet/FilterCommandTests.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class FilterCommandTests
|
||||
{
|
||||
private readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void Should_AddFilterWithCommand()
|
||||
{
|
||||
// Initially no filters
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Create a filter
|
||||
var filter = new SheetFilter(
|
||||
new EqualToCriterion { Column = 0, Value = "Test" },
|
||||
RangeRef.Parse("A1:A5")
|
||||
);
|
||||
|
||||
// Execute the command
|
||||
var command = new FilterCommand(sheet, filter);
|
||||
var result = command.Execute();
|
||||
|
||||
// Command should succeed
|
||||
Assert.True(result);
|
||||
|
||||
// Filter should be added
|
||||
Assert.Single(sheet.Filters);
|
||||
Assert.Contains(filter, sheet.Filters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_UndoFilterCommand()
|
||||
{
|
||||
// Initially no filters
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Create a filter
|
||||
var filter = new SheetFilter(
|
||||
new EqualToCriterion { Column = 0, Value = "Test" },
|
||||
RangeRef.Parse("A1:A5")
|
||||
);
|
||||
|
||||
// Execute the command
|
||||
var command = new FilterCommand(sheet, filter);
|
||||
command.Execute();
|
||||
|
||||
// Filter should be added
|
||||
Assert.Single(sheet.Filters);
|
||||
|
||||
// Undo the command
|
||||
command.Unexecute();
|
||||
|
||||
// Filter should be removed
|
||||
Assert.Empty(sheet.Filters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_WorkWithUndoRedoStack()
|
||||
{
|
||||
// Initially no filters
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Create a filter
|
||||
var filter = new SheetFilter(
|
||||
new EqualToCriterion { Column = 0, Value = "Test" },
|
||||
RangeRef.Parse("A1:A5")
|
||||
);
|
||||
|
||||
// Execute the command through the undo/redo stack
|
||||
var command = new FilterCommand(sheet, filter);
|
||||
var result = sheet.Commands.Execute(command);
|
||||
|
||||
// Command should succeed
|
||||
Assert.True(result);
|
||||
|
||||
// Filter should be added
|
||||
Assert.Single(sheet.Filters);
|
||||
|
||||
// Undo should be available
|
||||
Assert.True(sheet.Commands.CanUndo);
|
||||
|
||||
// Undo the command
|
||||
sheet.Commands.Undo();
|
||||
|
||||
// Filter should be removed
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Redo should be available
|
||||
Assert.True(sheet.Commands.CanRedo);
|
||||
|
||||
// Redo the command
|
||||
sheet.Commands.Redo();
|
||||
|
||||
// Filter should be added again
|
||||
Assert.Single(sheet.Filters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_HandleMultipleFilters()
|
||||
{
|
||||
// Initially no filters
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Create multiple filters
|
||||
var filter1 = new SheetFilter(
|
||||
new EqualToCriterion { Column = 0, Value = "Test1" },
|
||||
RangeRef.Parse("A1:A5")
|
||||
);
|
||||
|
||||
var filter2 = new SheetFilter(
|
||||
new EqualToCriterion { Column = 1, Value = "Test2" },
|
||||
RangeRef.Parse("B1:B5")
|
||||
);
|
||||
|
||||
// Execute commands through the undo/redo stack
|
||||
var command1 = new FilterCommand(sheet, filter1);
|
||||
var command2 = new FilterCommand(sheet, filter2);
|
||||
|
||||
sheet.Commands.Execute(command1);
|
||||
sheet.Commands.Execute(command2);
|
||||
|
||||
// Both filters should be added
|
||||
Assert.Equal(2, sheet.Filters.Count);
|
||||
Assert.Contains(filter1, sheet.Filters);
|
||||
Assert.Contains(filter2, sheet.Filters);
|
||||
|
||||
// Undo both commands
|
||||
sheet.Commands.Undo(); // Undo filter2
|
||||
sheet.Commands.Undo(); // Undo filter1
|
||||
|
||||
// No filters should remain
|
||||
Assert.Empty(sheet.Filters);
|
||||
|
||||
// Redo both commands
|
||||
sheet.Commands.Redo(); // Redo filter1
|
||||
sheet.Commands.Redo(); // Redo filter2
|
||||
|
||||
// Both filters should be back
|
||||
Assert.Equal(2, sheet.Filters.Count);
|
||||
Assert.Contains(filter1, sheet.Filters);
|
||||
Assert.Contains(filter2, sheet.Filters);
|
||||
}
|
||||
}
|
||||
1105
Radzen.Blazor.Tests/Spreadsheet/FilteringTests.cs
Normal file
1105
Radzen.Blazor.Tests/Spreadsheet/FilteringTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
51
Radzen.Blazor.Tests/Spreadsheet/FindFunctionTests.cs
Normal file
51
Radzen.Blazor.Tests/Spreadsheet/FindFunctionTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class FindFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Find_CaseSensitive_MatchesUppercase()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Miriam McGovern";
|
||||
sheet.Cells["B1"].Formula = "=FIND(\"M\",A2)";
|
||||
Assert.Equal(1d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Find_CaseSensitive_MatchesLowercase()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Miriam McGovern";
|
||||
sheet.Cells["B1"].Formula = "=FIND(\"m\",A2)";
|
||||
Assert.Equal(6d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Find_WithStartNum()
|
||||
{
|
||||
var sheet = new Sheet(10, 30);
|
||||
sheet.Cells["A1"].Value = "AYF0093.YoungMensApparel";
|
||||
sheet.Cells["B1"].Formula = "=FIND(\"Y\",A1,8)";
|
||||
Assert.Equal(9d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Find_EmptyFindText_ReturnsStart()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "abc";
|
||||
sheet.Cells["B1"].Formula = "=FIND(\"\",A1,2)";
|
||||
Assert.Equal(2d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Find_NotFound_ReturnsValue()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "abc";
|
||||
sheet.Cells["B1"].Formula = "=FIND(\"z\",A1)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
298
Radzen.Blazor.Tests/Spreadsheet/FormulaEvaluatorTests.cs
Normal file
298
Radzen.Blazor.Tests/Spreadsheet/FormulaEvaluatorTests.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class FormulaEvaluationTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateFormulaAfterSettingIt()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Formula = "=A1+1";
|
||||
|
||||
Assert.Equal(2d, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateFormulaAfterSettingValue()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
|
||||
Assert.Equal(2d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldNotEvaluateFormulaIfEditing()
|
||||
{
|
||||
sheet.BeginUpdate();
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
|
||||
Assert.Null(sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateFormulaAfterEndingEdit()
|
||||
{
|
||||
sheet.BeginUpdate();
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
sheet.EndUpdate();
|
||||
|
||||
Assert.Equal(2d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSetCellValueToErrorValueIfStringIsUsedInBinaryOperation()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Value = "test";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSetCellValueToErrorNameIfInvalidFunctionIsUsedInFormula()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=INVALID_FUNCTION()";
|
||||
sheet.Cells["A2"].Value = "test";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("=SUM(")]
|
||||
[InlineData("=SUM(A2,")]
|
||||
[InlineData("=SUM(A2:A2")]
|
||||
public void ShouldSetCellValueToErrorNameIfIncompleteFunctionIsUsedInFormula(string formula)
|
||||
{
|
||||
sheet.Cells["A1"].Formula = formula;
|
||||
sheet.Cells["A2"].Value = "test";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSetCellValueToEqualsIfOnlyEqualsIsSetAsFormula()
|
||||
{
|
||||
sheet.Cells["A1"].SetValue("=");
|
||||
|
||||
Assert.Equal("=", sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateFormulaWhenDependencyIsChanged()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Formula = "=A3+1";
|
||||
sheet.Cells["A3"].Value = 1;
|
||||
|
||||
Assert.Equal(3d, sheet.Cells["A1"].Value);
|
||||
Assert.Equal(2d, sheet.Cells["A2"].Value);
|
||||
Assert.Equal(1d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateFormulaWhenDependencyIsChangedAndEndEditIsCalled()
|
||||
{
|
||||
sheet.BeginUpdate();
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Formula = "=A3+1";
|
||||
sheet.Cells["A3"].Value = 1;
|
||||
sheet.EndUpdate();
|
||||
|
||||
Assert.Equal(3d, sheet.Cells["A1"].Value);
|
||||
Assert.Equal(2d, sheet.Cells["A2"].Value);
|
||||
Assert.Equal(1d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTreatEmptyValueAsZeroInFormula()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
|
||||
Assert.Equal(1d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHandleSelfReferencingFormulas()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A1+1";
|
||||
|
||||
// Setting a value should not cause infinite recursion
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
|
||||
// The value should be stable and not cause infinite recursion
|
||||
Assert.NotNull(sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSetDiv0ErrorWhenDividingByZero()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2/A3";
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
sheet.Cells["A3"].Value = 0;
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSetErrorToCircularWhenCellFormulasReferenceEachOther()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A2+1";
|
||||
sheet.Cells["A2"].Formula = "=A1+1";
|
||||
|
||||
// The value should be an error
|
||||
Assert.Equal(CellError.Circular, sheet.Cells["A1"].Value);
|
||||
Assert.Equal(CellError.Circular, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNameErrorForUnknownFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=UNKNOWN()";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=A6";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=SUM(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenCountRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNT(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateRefErrorWhenCountaRangeOutOfBounds()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=COUNTA(A2:A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithDecimalValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0.5m;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal("True", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNameErrorForUnknownFunctionUppercase()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=UNKNOWNFUNCTION(1,2,3)";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNameErrorForUnknownFunctionWithMixedCase()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=UnknownFunction(1,2,3)";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNameErrorForUnknownFunctionWithLowercase()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=unknownfunction(1,2,3)";
|
||||
|
||||
Assert.Equal(CellError.Name, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateAndFunctionInIfStatementWithFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 150;
|
||||
sheet.Cells["A3"].Formula = "=IF(AND(A1>1,A2<100),A1,\"Out of range\")";
|
||||
|
||||
Assert.Equal("Out of range", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithProvidedExample3()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 75;
|
||||
sheet.Cells["A3"].Formula = "=IF(OR(A2<0,A2>50),A2,\"The value is out of range\")";
|
||||
|
||||
Assert.Equal(75d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithOrFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=NOT(OR(A1,A2))";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
// IFERROR function tests are in IfErrorFunctionTests.cs
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSimpleDivisionByZero()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Formula = "=A1/0";
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluator_ShouldResolveCrossSheetCellReference()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var s1 = wb.AddSheet("Sheet1", 5, 5);
|
||||
var s2 = wb.AddSheet("Sheet2", 5, 5);
|
||||
|
||||
s2.Cells[0, 2].Value = 42; // C1 on Sheet2
|
||||
|
||||
s1.Cells[0, 0].Formula = "=Sheet2!C1"; // A1 on Sheet1 refers to Sheet2!C1
|
||||
|
||||
Assert.Equal(42d, s1.Cells[0, 0].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluator_ShouldResolveCrossSheetRangeInFunction()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var s1 = wb.AddSheet("Sheet1", 5, 5);
|
||||
var s2 = wb.AddSheet("Sheet2", 5, 5);
|
||||
|
||||
s2.Cells[0, 0].Value = 1; // A1
|
||||
s2.Cells[0, 1].Value = 2; // B1
|
||||
s2.Cells[1, 0].Value = 3; // A2
|
||||
s2.Cells[1, 1].Value = 4; // B2
|
||||
|
||||
s1.Cells[0, 0].Formula = "=SUM(Sheet2!A1:Sheet2!B2)";
|
||||
|
||||
Assert.Equal(10d, s1.Cells[0, 0].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
}
|
||||
143
Radzen.Blazor.Tests/Spreadsheet/FormulaLexerTests.cs
Normal file
143
Radzen.Blazor.Tests/Spreadsheet/FormulaLexerTests.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
public class FormulaLexerTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormulaLexer_ShouldParseCellIdentifier()
|
||||
{
|
||||
var tokens = FormulaLexer.Scan("=A1");
|
||||
Assert.Equal(FormulaTokenType.Equals, tokens[0].Type);
|
||||
Assert.Equal(0, tokens[0].Start);
|
||||
Assert.Equal(1, tokens[0].End);
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[1].Type);
|
||||
Assert.Equal("A1", tokens[1].Address.ToString());
|
||||
Assert.Equal(1, tokens[1].Start);
|
||||
Assert.Equal(3, tokens[1].End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaLexer_ShouldParseSimpleFormula()
|
||||
{
|
||||
var tokens = FormulaLexer.Scan("=A1+b2");
|
||||
Assert.Equal(FormulaTokenType.Equals, tokens[0].Type);
|
||||
Assert.Equal(0, tokens[0].Start);
|
||||
Assert.Equal(1, tokens[0].End);
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[1].Type);
|
||||
Assert.Equal("A1", tokens[1].Value);
|
||||
Assert.Equal(1, tokens[1].Start);
|
||||
Assert.Equal(3, tokens[1].End);
|
||||
Assert.Equal(FormulaTokenType.Plus, tokens[2].Type);
|
||||
Assert.Equal(3, tokens[2].Start);
|
||||
Assert.Equal(4, tokens[2].End);
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[3].Type);
|
||||
Assert.Equal("b2", tokens[3].Value);
|
||||
Assert.Equal(4, tokens[3].Start);
|
||||
Assert.Equal(6, tokens[3].End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaLexer_ShouldPreserveWhitespaceAsTrivia()
|
||||
{
|
||||
var tokens = FormulaLexer.Scan("= A1 + b2 ");
|
||||
|
||||
// Check that whitespace is preserved as trivia
|
||||
Assert.Equal(FormulaTokenType.Equals, tokens[0].Type);
|
||||
Assert.Empty(tokens[0].LeadingTrivia);
|
||||
Assert.Single(tokens[0].TrailingTrivia);
|
||||
Assert.Equal(FormulaTokenTriviaKind.Whitespace, tokens[0].TrailingTrivia[0].Kind);
|
||||
Assert.Equal(" ", tokens[0].TrailingTrivia[0].Text);
|
||||
Assert.Equal(0, tokens[0].Start);
|
||||
Assert.Equal(2, tokens[0].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[1].Type);
|
||||
Assert.Empty(tokens[1].LeadingTrivia);
|
||||
Assert.Single(tokens[1].TrailingTrivia);
|
||||
Assert.Equal(FormulaTokenTriviaKind.Whitespace, tokens[1].TrailingTrivia[0].Kind);
|
||||
Assert.Equal(" ", tokens[1].TrailingTrivia[0].Text);
|
||||
Assert.Equal(2, tokens[1].Start);
|
||||
Assert.Equal(5, tokens[1].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.Plus, tokens[2].Type);
|
||||
Assert.Empty(tokens[2].LeadingTrivia);
|
||||
Assert.Single(tokens[2].TrailingTrivia);
|
||||
Assert.Equal(FormulaTokenTriviaKind.Whitespace, tokens[2].TrailingTrivia[0].Kind);
|
||||
Assert.Equal(" ", tokens[2].TrailingTrivia[0].Text);
|
||||
Assert.Equal(5, tokens[2].Start);
|
||||
Assert.Equal(7, tokens[2].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[3].Type);
|
||||
Assert.Empty(tokens[3].LeadingTrivia);
|
||||
Assert.Single(tokens[3].TrailingTrivia);
|
||||
Assert.Equal(FormulaTokenTriviaKind.Whitespace, tokens[3].TrailingTrivia[0].Kind);
|
||||
Assert.Equal(" ", tokens[3].TrailingTrivia[0].Text);
|
||||
Assert.Equal(7, tokens[3].Start);
|
||||
Assert.Equal(10, tokens[3].End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaLexer_ShouldPreserveMultipleWhitespaceAsTrivia()
|
||||
{
|
||||
var tokens = FormulaLexer.Scan("= A1 + b2 ");
|
||||
|
||||
// Check that multiple whitespace characters are preserved
|
||||
Assert.Equal(FormulaTokenType.Equals, tokens[0].Type);
|
||||
Assert.Single(tokens[0].TrailingTrivia);
|
||||
Assert.Equal(" ", tokens[0].TrailingTrivia[0].Text);
|
||||
Assert.Equal(0, tokens[0].Start);
|
||||
Assert.Equal(3, tokens[0].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[1].Type);
|
||||
Assert.Single(tokens[1].TrailingTrivia);
|
||||
Assert.Equal(" ", tokens[1].TrailingTrivia[0].Text);
|
||||
Assert.Equal(3, tokens[1].Start);
|
||||
Assert.Equal(7, tokens[1].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.Plus, tokens[2].Type);
|
||||
Assert.Single(tokens[2].TrailingTrivia);
|
||||
Assert.Equal(" ", tokens[2].TrailingTrivia[0].Text);
|
||||
Assert.Equal(7, tokens[2].Start);
|
||||
Assert.Equal(10, tokens[2].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[3].Type);
|
||||
Assert.Single(tokens[3].TrailingTrivia);
|
||||
Assert.Equal(" ", tokens[3].TrailingTrivia[0].Text);
|
||||
Assert.Equal(10, tokens[3].Start);
|
||||
Assert.Equal(14, tokens[3].End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaLexer_ShouldPreserveLineEndingsAsTrivia()
|
||||
{
|
||||
var tokens = FormulaLexer.Scan("=A1\n+b2");
|
||||
|
||||
// Check that line endings are preserved as trivia
|
||||
Assert.Equal(FormulaTokenType.Equals, tokens[0].Type);
|
||||
Assert.Empty(tokens[0].LeadingTrivia);
|
||||
Assert.Empty(tokens[0].TrailingTrivia);
|
||||
Assert.Equal(0, tokens[0].Start);
|
||||
Assert.Equal(1, tokens[0].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.CellIdentifier, tokens[1].Type);
|
||||
Assert.Empty(tokens[1].LeadingTrivia);
|
||||
Assert.Single(tokens[1].TrailingTrivia);
|
||||
Assert.Equal(FormulaTokenTriviaKind.EndOfLine, tokens[1].TrailingTrivia[0].Kind);
|
||||
Assert.Equal("\n", tokens[1].TrailingTrivia[0].Text);
|
||||
Assert.Equal(1, tokens[1].Start);
|
||||
Assert.Equal(4, tokens[1].End);
|
||||
|
||||
Assert.Equal(FormulaTokenType.Plus, tokens[2].Type);
|
||||
Assert.Empty(tokens[2].LeadingTrivia);
|
||||
Assert.Empty(tokens[2].TrailingTrivia);
|
||||
Assert.Equal(4, tokens[2].Start);
|
||||
Assert.Equal(5, tokens[2].End);
|
||||
|
||||
Assert.Equal(expected: FormulaTokenType.CellIdentifier, tokens[3].Type);
|
||||
Assert.Empty(tokens[3].LeadingTrivia);
|
||||
Assert.Empty(tokens[3].TrailingTrivia);
|
||||
Assert.Equal(5, tokens[3].Start);
|
||||
Assert.Equal(7, tokens[3].End);
|
||||
}
|
||||
}
|
||||
644
Radzen.Blazor.Tests/Spreadsheet/FormulaParserTests.cs
Normal file
644
Radzen.Blazor.Tests/Spreadsheet/FormulaParserTests.cs
Normal file
@@ -0,0 +1,644 @@
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
public class FormulaParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldRequireEqualsAtStart()
|
||||
{
|
||||
var formula = "A1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.Contains("Unexpected token", syntaxTree.Errors[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseNumberLiteral()
|
||||
{
|
||||
var formula = "=123";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(syntaxTree.Root);
|
||||
var numberNode = (NumberLiteralSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal(123, numberNode.Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseAdditionOfTwoNumberLiterals()
|
||||
{
|
||||
var formula = "=123+456";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(syntaxTree.Root);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal(BinaryOperator.Plus, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)binaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseAdditionOfMultipleNumberLiterals()
|
||||
{
|
||||
var formula = "=123+456+789";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Plus, binaryNode.Operator);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(binaryNode.Left);
|
||||
var leftBinaryNode = (BinaryExpressionSyntaxNode)binaryNode.Left;
|
||||
Assert.Equal(BinaryOperator.Plus, leftBinaryNode.Operator);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)leftBinaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)leftBinaryNode.Right).Token.IntValue);
|
||||
Assert.Equal(789, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseSubtractionOfTwoNumberLiterals()
|
||||
{
|
||||
var formula = "=123-456";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Minus, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)binaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseUnaryNegativeNumber()
|
||||
{
|
||||
var formula = "=-123";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(syntaxTree.Root);
|
||||
var unary = (UnaryExpressionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal(UnaryOperator.Negate, unary.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(unary.Operand);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)unary.Operand).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseUnaryPlusNumber()
|
||||
{
|
||||
var formula = "=+123";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(syntaxTree.Root);
|
||||
var unary = (UnaryExpressionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal(UnaryOperator.Plus, unary.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(unary.Operand);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)unary.Operand).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseUnaryPlusInFunctionArgument()
|
||||
{
|
||||
var formula = "=LEFT(A1,+1)";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<FunctionSyntaxNode>(syntaxTree.Root);
|
||||
var fn = (FunctionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("LEFT", fn.Name);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(fn.Arguments[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseMultipleUnaryOperators()
|
||||
{
|
||||
var formula = "=-+-+3";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
// Expect nested unary nodes: - ( + ( - ( + 3 ) ) )
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(node);
|
||||
var u1 = (UnaryExpressionSyntaxNode)node; // '-'
|
||||
Assert.Equal(UnaryOperator.Negate, u1.Operator);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(u1.Operand);
|
||||
var u2 = (UnaryExpressionSyntaxNode)u1.Operand; // '+'
|
||||
Assert.Equal(UnaryOperator.Plus, u2.Operator);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(u2.Operand);
|
||||
var u3 = (UnaryExpressionSyntaxNode)u2.Operand; // '-'
|
||||
Assert.Equal(UnaryOperator.Negate, u3.Operator);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(u3.Operand);
|
||||
var u4 = (UnaryExpressionSyntaxNode)u3.Operand; // '+'
|
||||
Assert.Equal(UnaryOperator.Plus, u4.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(u4.Operand);
|
||||
Assert.Equal(3, ((NumberLiteralSyntaxNode)u4.Operand).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseUnaryNegativeInFunctionArgument()
|
||||
{
|
||||
var formula = "=LEFT(A1,-1)";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<FunctionSyntaxNode>(syntaxTree.Root);
|
||||
var fn = (FunctionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("LEFT", fn.Name);
|
||||
Assert.Equal(2, fn.Arguments.Count);
|
||||
Assert.IsType<CellSyntaxNode>(fn.Arguments[0]);
|
||||
Assert.IsType<UnaryExpressionSyntaxNode>(fn.Arguments[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseSubtractionOfMultipleNumberLiterals()
|
||||
{
|
||||
var formula = "=123-456-789";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Minus, binaryNode.Operator);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(binaryNode.Left);
|
||||
var leftBinaryNode = (BinaryExpressionSyntaxNode)binaryNode.Left;
|
||||
Assert.Equal(BinaryOperator.Minus, leftBinaryNode.Operator);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)leftBinaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)leftBinaryNode.Right).Token.IntValue);
|
||||
Assert.Equal(789, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseMultiplicationOfTwoNumberLiterals()
|
||||
{
|
||||
var formula = "=123*456";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Multiply, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)binaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParse_MultiplicationPrecedence()
|
||||
{
|
||||
var formula = "=123+456*789";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Plus, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(binaryNode.Right);
|
||||
var rightBinaryNode = (BinaryExpressionSyntaxNode)binaryNode.Right;
|
||||
Assert.Equal(BinaryOperator.Multiply, rightBinaryNode.Operator);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)rightBinaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(789, ((NumberLiteralSyntaxNode)rightBinaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseDivisionOfTwoNumberLiterals()
|
||||
{
|
||||
var formula = "=123/456";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Divide, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)binaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseParentheses()
|
||||
{
|
||||
var formula = "=(123+456)*789";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Multiply, binaryNode.Operator);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
var leftBinaryNode = (BinaryExpressionSyntaxNode)binaryNode.Left;
|
||||
Assert.Equal(789, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
Assert.Equal(BinaryOperator.Plus, leftBinaryNode.Operator);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)leftBinaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)leftBinaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseNestedParentheses()
|
||||
{
|
||||
var formula = "=((123+456)*789)/101112";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)node;
|
||||
Assert.Equal(BinaryOperator.Divide, binaryNode.Operator);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Right);
|
||||
var leftBinaryNode = (BinaryExpressionSyntaxNode)binaryNode.Left;
|
||||
Assert.Equal(101112, ((NumberLiteralSyntaxNode)binaryNode.Right).Token.IntValue);
|
||||
Assert.Equal(BinaryOperator.Multiply, leftBinaryNode.Operator);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(leftBinaryNode.Left);
|
||||
var leftLeftBinaryNode = (BinaryExpressionSyntaxNode)leftBinaryNode.Left;
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)leftLeftBinaryNode.Left).Token.IntValue);
|
||||
Assert.Equal(456, ((NumberLiteralSyntaxNode)leftLeftBinaryNode.Right).Token.IntValue);
|
||||
Assert.Equal(789, ((NumberLiteralSyntaxNode)leftBinaryNode.Right).Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseCellIndentifer()
|
||||
{
|
||||
var formula = "=A1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<CellSyntaxNode>(node);
|
||||
var cellIdentifierNode = (CellSyntaxNode)node;
|
||||
Assert.Equal("A1", cellIdentifierNode.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseSheetQualifiedCellIdentifier()
|
||||
{
|
||||
var formula = "=Sheet2!C1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<CellSyntaxNode>(node);
|
||||
var cellIdentifierNode = (CellSyntaxNode)node;
|
||||
Assert.Equal("C1", cellIdentifierNode.Token.Address.ToString());
|
||||
Assert.Equal("Sheet2", cellIdentifierNode.Token.Address.Sheet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseFunction()
|
||||
{
|
||||
var formula = "=SUM(A1,1)";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<FunctionSyntaxNode>(node);
|
||||
var functionNode = (FunctionSyntaxNode)node;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.Equal(2, functionNode.Arguments.Count);
|
||||
Assert.IsType<CellSyntaxNode>(functionNode.Arguments[0]);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(functionNode.Arguments[1]);
|
||||
|
||||
var cellIdentifierNode = (CellSyntaxNode)functionNode.Arguments[0];
|
||||
Assert.Equal("A1", cellIdentifierNode.Token.Address.ToString());
|
||||
var numberLiteralNode = (NumberLiteralSyntaxNode)functionNode.Arguments[1];
|
||||
Assert.Equal(1, numberLiteralNode.Token.IntValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseNestedFunctions()
|
||||
{
|
||||
var formula = "=SUM(A1,MAX(B1,C1))";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<FunctionSyntaxNode>(node);
|
||||
var functionNode = (FunctionSyntaxNode)node;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.Equal(2, functionNode.Arguments.Count);
|
||||
Assert.IsType<CellSyntaxNode>(functionNode.Arguments[0]);
|
||||
Assert.IsType<FunctionSyntaxNode>(functionNode.Arguments[1]);
|
||||
|
||||
var cellIdentifierNode = (CellSyntaxNode)functionNode.Arguments[0];
|
||||
Assert.Equal("A1", cellIdentifierNode.Token.Address.ToString());
|
||||
|
||||
var nestedFunctionNode = (FunctionSyntaxNode)functionNode.Arguments[1];
|
||||
Assert.Equal("MAX", nestedFunctionNode.Name);
|
||||
Assert.Equal(2, nestedFunctionNode.Arguments.Count);
|
||||
Assert.IsType<CellSyntaxNode>(nestedFunctionNode.Arguments[0]);
|
||||
Assert.IsType<CellSyntaxNode>(nestedFunctionNode.Arguments[1]);
|
||||
|
||||
var firstCellIdentifierNode = (CellSyntaxNode)nestedFunctionNode.Arguments[0];
|
||||
var secondCellIdentifierNode = (CellSyntaxNode)nestedFunctionNode.Arguments[1];
|
||||
Assert.Equal("B1", firstCellIdentifierNode.Token.Address.ToString());
|
||||
Assert.Equal("C1", secondCellIdentifierNode.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseFunctionWithNoArguments()
|
||||
{
|
||||
var formula = "=SUM()";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<FunctionSyntaxNode>(node);
|
||||
var functionNode = (FunctionSyntaxNode)node;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.Empty(functionNode.Arguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseCellRange()
|
||||
{
|
||||
var formula = "=A1:A2";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("A2", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseSheetQualifiedRange()
|
||||
{
|
||||
var formula = "=Sheet2!A1:Sheet2!B2";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("B2", rangeNode.End.Token.Address.ToString());
|
||||
Assert.Equal("Sheet2", rangeNode.Start.Token.Address.Sheet);
|
||||
Assert.Equal("Sheet2", rangeNode.End.Token.Address.Sheet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseCellRangeInFunction()
|
||||
{
|
||||
var formula = "=SUM(A1:A2)";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<FunctionSyntaxNode>(node);
|
||||
var functionNode = (FunctionSyntaxNode)node;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.Single(functionNode.Arguments);
|
||||
Assert.IsType<RangeSyntaxNode>(functionNode.Arguments[0]);
|
||||
var rangeNode = (RangeSyntaxNode)functionNode.Arguments[0];
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("A2", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleInvalidRange()
|
||||
{
|
||||
var formula = "=A2:A1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A2", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("A1", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleSingleCellRange()
|
||||
{
|
||||
var formula = "=A1:A1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("A1", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleMultiColumnRange()
|
||||
{
|
||||
var formula = "=A1:B1";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("B1", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleMultiRowRange()
|
||||
{
|
||||
var formula = "=A1:A2";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("A2", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleMultiCellRange()
|
||||
{
|
||||
var formula = "=A1:B2";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.IsType<RangeSyntaxNode>(node);
|
||||
var rangeNode = (RangeSyntaxNode)node;
|
||||
Assert.Equal("A1", rangeNode.Start.Token.Address.ToString());
|
||||
Assert.Equal("B2", rangeNode.End.Token.Address.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldAddErrorOnInvalidFormula()
|
||||
{
|
||||
var formula = "A1"; // Missing equals sign
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.Contains("Unexpected token", syntaxTree.Errors[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldReturnPartialExpressionOnIncompleteExpression()
|
||||
{
|
||||
var formula = "=123+"; // Incomplete expression
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(syntaxTree.Root);
|
||||
if (syntaxTree.Root is BinaryExpressionSyntaxNode binaryNode)
|
||||
{
|
||||
Assert.Equal(BinaryOperator.Plus, binaryNode.Operator);
|
||||
Assert.IsType<NumberLiteralSyntaxNode>(binaryNode.Left);
|
||||
Assert.Equal(123, ((NumberLiteralSyntaxNode)binaryNode.Left).Token.IntValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldAddErrorOnIncompleteExpression()
|
||||
{
|
||||
var formula = "=123+"; // Incomplete expression
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldReturnPartialFunctionOnMissingCloseParen()
|
||||
{
|
||||
var formula = "=SUM(A1"; // Missing closing parenthesis
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.IsType<FunctionSyntaxNode>(syntaxTree.Root);
|
||||
var functionNode = (FunctionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.Single(functionNode.Arguments);
|
||||
Assert.IsType<CellSyntaxNode>(functionNode.Arguments[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldAddErrorOnInvalidFunctionSyntax()
|
||||
{
|
||||
var formula = "=SUM(A1"; // Missing closing parenthesis
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseGroupedExpression()
|
||||
{
|
||||
var formula = "=(A1)"; // Parentheses without function name should parse as grouped expression
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors); // This should actually succeed as it's a valid grouped expression
|
||||
Assert.IsType<CellSyntaxNode>(syntaxTree.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldReturnPartialRangeOnIncompleteRange()
|
||||
{
|
||||
var formula = "=A1:"; // Incomplete range
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.NotNull(syntaxTree.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldAddErrorOnInvalidRange()
|
||||
{
|
||||
var formula = "=A1:"; // Incomplete range
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleUnterminatedString()
|
||||
{
|
||||
var formula = "=\"hello"; // Unterminated string literal
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors); // Should succeed as lexer handles unterminated strings
|
||||
Assert.IsType<StringLiteralSyntaxNode>(syntaxTree.Root);
|
||||
var stringNode = (StringLiteralSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("hello", stringNode.Token.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldHandleMissingOperand()
|
||||
{
|
||||
var formula = "=*5"; // Missing left operand
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors); // Should have errors
|
||||
Assert.NotNull(syntaxTree.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldReturnPartialExpressionOnUnbalancedParentheses()
|
||||
{
|
||||
var formula = "=(A1+B1"; // Missing closing parenthesis
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(syntaxTree.Root); // Should return the binary expression inside
|
||||
var binaryNode = (BinaryExpressionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal(BinaryOperator.Plus, binaryNode.Operator);
|
||||
Assert.IsType<CellSyntaxNode>(binaryNode.Left);
|
||||
Assert.IsType<CellSyntaxNode>(binaryNode.Right);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldAddErrorOnUnbalancedParentheses()
|
||||
{
|
||||
var formula = "=(A1+B1"; // Missing closing parenthesis
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldReturnPartialFunctionOnIncompleteArguments()
|
||||
{
|
||||
var formula = "=SUM(A1,"; // Incomplete function arguments
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
Assert.IsType<FunctionSyntaxNode>(syntaxTree.Root);
|
||||
var functionNode = (FunctionSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("SUM", functionNode.Name);
|
||||
Assert.True(functionNode.Arguments.Count >= 1); // Should have at least the first argument
|
||||
Assert.IsType<CellSyntaxNode>(functionNode.Arguments[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_DefaultBehavior_ShouldStillWork()
|
||||
{
|
||||
// Test that default behavior still works as before
|
||||
var formula = "=123+456";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var node = syntaxTree.Root;
|
||||
Assert.NotNull(node);
|
||||
Assert.IsType<BinaryExpressionSyntaxNode>(node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseBooleanTrue()
|
||||
{
|
||||
var formula = "=TRUE";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<BooleanLiteralSyntaxNode>(syntaxTree.Root);
|
||||
var boolNode = (BooleanLiteralSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("TRUE", boolNode.Token.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseBooleanFalse_Lowercase()
|
||||
{
|
||||
var formula = "=false";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
Assert.IsType<BooleanLiteralSyntaxNode>(syntaxTree.Root);
|
||||
var boolNode = (BooleanLiteralSyntaxNode)syntaxTree.Root;
|
||||
Assert.Equal("false", boolNode.Token.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParseBooleanInFunction()
|
||||
{
|
||||
var formula = "=TEXTJOIN(\", \", TRUE, A1:A2)";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.Empty(syntaxTree.Errors);
|
||||
var fn = Assert.IsType<FunctionSyntaxNode>(syntaxTree.Root);
|
||||
Assert.Equal("TEXTJOIN", fn.Name);
|
||||
Assert.IsType<BooleanLiteralSyntaxNode>(fn.Arguments[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaParser_ShouldParse_Percent()
|
||||
{
|
||||
var formula = "=$%";
|
||||
var syntaxTree = FormulaParser.Parse(formula);
|
||||
Assert.NotEmpty(syntaxTree.Errors);
|
||||
}
|
||||
}
|
||||
74
Radzen.Blazor.Tests/Spreadsheet/FunctionRegistryTests.cs
Normal file
74
Radzen.Blazor.Tests/Spreadsheet/FunctionRegistryTests.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class FunctionRegistryTests
|
||||
{
|
||||
private readonly FunctionStore functionRegistry = new();
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, -1)]
|
||||
[InlineData(4, -1)]
|
||||
[InlineData(5, 0)]
|
||||
[InlineData(6, 0)]
|
||||
[InlineData(7, 1)]
|
||||
[InlineData(8, 1)]
|
||||
[InlineData(9, 2)]
|
||||
[InlineData(10, 2)]
|
||||
public void Basic_Function_Provides_Correct_Arg_Index(int cursorPosition, int expectedArgIndex)
|
||||
{
|
||||
var func = "=SUM(1,2,3)";
|
||||
var result = functionRegistry.CreateFunctionHint(func, cursorPosition);
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(expectedArgIndex, result.ArgumentIndex);
|
||||
Assert.IsType<SumFunction>(result.Function);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(5, 0, "=SUM(")]
|
||||
[InlineData(4, -1, "=SUM(")]
|
||||
public void Basic_Function_Provides_Correct_Arg_Index_With_IncompleteFormula(int cursorPosition, int expectedArgIndex, string formula)
|
||||
{
|
||||
var result = functionRegistry.CreateFunctionHint(formula, cursorPosition);
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(expectedArgIndex, result.ArgumentIndex);
|
||||
Assert.IsType<SumFunction>(result.Function);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Position_Outside_Of_Formula_Returns_null()
|
||||
{
|
||||
var func = "=1 + SUM(1,2, 3) + 2";
|
||||
var result = functionRegistry.CreateFunctionHint(func, 0);
|
||||
|
||||
Assert.Null(result);
|
||||
|
||||
result = functionRegistry.CreateFunctionHint(func, 5);
|
||||
Assert.Null(result);
|
||||
|
||||
result = functionRegistry.CreateFunctionHint(func, 16);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, 0, typeof(SumFunction))]
|
||||
[InlineData(7, 1, typeof(SumFunction))]
|
||||
[InlineData(8, -1, typeof(CountFunction))]
|
||||
[InlineData(13, 0, typeof(CountFunction))]
|
||||
[InlineData(15, 1, typeof(CountFunction))]
|
||||
[InlineData(17, 1, typeof(SumFunction))]
|
||||
public void Nested_Function_Produces_Correct_ArgIndex(int cursorPosition, int expectedArgIndex, Type expectedFunction)
|
||||
{
|
||||
var func = "=SUM(1,COUNT(1,2),3)";
|
||||
var result = functionRegistry.CreateFunctionHint(func, cursorPosition);
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(expectedArgIndex, result.ArgumentIndex);
|
||||
Assert.IsType(expectedFunction, result.Function);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class HorizontalLookupFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindExactMatchInTwoRowRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "Size";
|
||||
sheet.Cells["B1"].Value = "Color";
|
||||
sheet.Cells["A2"].Value = "M";
|
||||
sheet.Cells["B2"].Value = "Blue";
|
||||
|
||||
sheet.Cells["C1"].Formula = "=HLOOKUP(\"Color\",A1:B2,2,0)";
|
||||
|
||||
Assert.Equal("Blue", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNAWhenNoExactMatch()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "Size";
|
||||
sheet.Cells["A2"].Value = "M";
|
||||
|
||||
sheet.Cells["B1"].Formula = "=HLOOKUP(\"Color\",A1:A2,2,0)";
|
||||
|
||||
Assert.Equal(CellError.NA, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindApproximateMatchInSortedTopRow()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["B1"].Value = 20;
|
||||
sheet.Cells["C1"].Value = 30;
|
||||
|
||||
sheet.Cells["A2"].Value = "Low";
|
||||
sheet.Cells["B2"].Value = "Medium";
|
||||
sheet.Cells["C2"].Value = "High";
|
||||
|
||||
sheet.Cells["D1"].Formula = "=HLOOKUP(25,A1:C2,2,1)";
|
||||
|
||||
Assert.Equal("Medium", sheet.Cells["D1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldErrorWhenIndexOutOfRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "X";
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=HLOOKUP(\"X\",A1:A2,3,0)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["B1"].Value);
|
||||
}
|
||||
}
|
||||
33
Radzen.Blazor.Tests/Spreadsheet/HourFunctionTests.cs
Normal file
33
Radzen.Blazor.Tests/Spreadsheet/HourFunctionTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class HourFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hour_FromFraction_Returns18()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Data = CellData.FromNumber(0.75); // 18:00
|
||||
sheet.Cells["B2"].Formula = "=HOUR(A2)";
|
||||
Assert.Equal(18, sheet.Cells["B2"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hour_FromDateTimeValue_ReturnsHour()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Data = CellData.FromDate(new System.DateTime(2011, 7, 18, 7, 45, 0));
|
||||
sheet.Cells["B3"].Formula = "=HOUR(A3)";
|
||||
Assert.Equal(7, sheet.Cells["B3"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hour_FromDateOnly_ReturnsZero()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A4"].Data = CellData.FromDate(new System.DateTime(2012, 4, 21));
|
||||
sheet.Cells["B4"].Formula = "=HOUR(A4)";
|
||||
Assert.Equal(0, sheet.Cells["B4"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
}
|
||||
138
Radzen.Blazor.Tests/Spreadsheet/IfErrorFunctionTests.cs
Normal file
138
Radzen.Blazor.Tests/Spreadsheet/IfErrorFunctionTests.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class IfErrorFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithNoError()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, \"Error in calculation\")";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithDivisionByZero()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, \"Error in calculation\")";
|
||||
|
||||
Assert.Equal("Error in calculation", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithReferenceError()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IFERROR(A6, \"Error in calculation\")";
|
||||
|
||||
Assert.Equal("Error in calculation", sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithEmptyStringForError()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, \"\")";
|
||||
|
||||
Assert.Equal("", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithNumericErrorValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, 0)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithEmptyCellAsErrorValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, A4)";
|
||||
|
||||
Assert.Equal("", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithEmptyCellAsValue()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IFERROR(A2, \"Empty cell\")";
|
||||
|
||||
Assert.Equal("", sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithStringValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "Hello";
|
||||
sheet.Cells["A2"].Formula = "=IFERROR(A1, \"Error\")";
|
||||
|
||||
Assert.Equal("Hello", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithBooleanValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Formula = "=IFERROR(A1, \"Error\")";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForIfErrorTooFewArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IFERROR(A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForIfErrorTooManyArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IFERROR(A2, \"Error\", \"Extra\")";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithNestedFormula()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(A1/A2, IFERROR(A1/0, \"Nested Error\"))";
|
||||
|
||||
Assert.Equal("Nested Error", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithSumFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Formula = "=IFERROR(SUM(A1:A2), \"Error\")";
|
||||
|
||||
Assert.Equal(30d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfErrorFunctionWithSumFunctionError()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IFERROR(SUM(A6:A8), \"Error\")";
|
||||
|
||||
Assert.Equal("Error", sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
Radzen.Blazor.Tests/Spreadsheet/IfFunctionTests.cs
Normal file
178
Radzen.Blazor.Tests/Spreadsheet/IfFunctionTests.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class IfFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithTrueCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,\"Yes\",\"No\")";
|
||||
|
||||
Assert.Equal("Yes", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 2;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,\"Yes\",\"No\")";
|
||||
|
||||
Assert.Equal("No", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithNumericComparison()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 5;
|
||||
sheet.Cells["A3"].Formula = "=IF(A1>A2,\"Over Budget\",\"Within Budget\")";
|
||||
|
||||
Assert.Equal("Over Budget", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithNumericResult()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 5;
|
||||
sheet.Cells["A3"].Formula = "=IF(A1>A2,A1-A2,0)";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithTwoArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,\"True\")";
|
||||
|
||||
Assert.Equal("True", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithTwoArgumentsFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,\"True\")";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithZeroAsFalse()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal("False", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithNonZeroAsTrue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal("True", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateToErrorIfFunctionWithStringCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "test";
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"Not Empty\",\"Empty\")";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithEmptyStringCondition()
|
||||
{
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"Not Empty\",\"Empty\")";
|
||||
|
||||
Assert.Equal("Empty", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithNullCondition()
|
||||
{
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"Not Empty\",\"Empty\")";
|
||||
|
||||
Assert.Equal("Empty", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForTooFewArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IF(A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForTooManyArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IF(A2,\"True\",\"False\",\"Extra\")";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPropagateErrorFromCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=IF(A6,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPropagateErrorFromTrueValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,A6,\"False\")";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPropagateErrorFromFalseValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1=1,\"True\",A6)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNestedIfFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 85;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1>=90,\"A\",IF(A1>=80,\"B\",IF(A1>=70,\"C\",\"F\")))";
|
||||
|
||||
Assert.Equal("B", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithBooleanValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal("True", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateIfFunctionWithDecimalValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0.5m;
|
||||
sheet.Cells["A2"].Formula = "=IF(A1,\"True\",\"False\")";
|
||||
|
||||
Assert.Equal("True", sheet.Cells["A2"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
100
Radzen.Blazor.Tests/Spreadsheet/IndexFunctionTests.cs
Normal file
100
Radzen.Blazor.Tests/Spreadsheet/IndexFunctionTests.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class IndexFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
void Seed()
|
||||
{
|
||||
sheet.Cells["A2"].Value = "Apples";
|
||||
sheet.Cells["B2"].Value = "Lemons";
|
||||
sheet.Cells["A3"].Value = "Bananas";
|
||||
sheet.Cells["B3"].Value = "Pears";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnIntersectionValue()
|
||||
{
|
||||
Seed();
|
||||
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,2,2)";
|
||||
Assert.Equal("Pears", sheet.Cells["C1"].Value);
|
||||
|
||||
sheet.Cells["C2"].Formula = "=INDEX(A2:B3,2,1)";
|
||||
Assert.Equal("Bananas", sheet.Cells["C2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnRefErrorIfOutOfRange()
|
||||
{
|
||||
// numeric values just to ensure range exists
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["B1"].Value = 2;
|
||||
sheet.Cells["A2"].Value = 3;
|
||||
sheet.Cells["B2"].Value = 4;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A1:B2,3,1)"; // row 3 out of 2 rows
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldDefaultColumnToFirstWhenOmitted()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,2)"; // column omitted -> first column
|
||||
Assert.Equal("Bananas", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldUseAreaOneWhenSpecified()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,2,2,1)";
|
||||
Assert.Equal("Pears", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorWhenAreaGreaterThanOne()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,1,1,2)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnFirstOfEntireColumnWhenRowIsZero()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,0,2)";
|
||||
Assert.Equal("Lemons", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnFirstOfEntireRowWhenColumnIsZero()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,2,0)";
|
||||
Assert.Equal("Bananas", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorWhenBothRowAndColumnOmitted()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnRefErrorOnNegativeIndices()
|
||||
{
|
||||
Seed();
|
||||
sheet.Cells["C1"].Formula = "=INDEX(A2:B3,0-1,1)"; // -1
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["C1"].Value);
|
||||
|
||||
sheet.Cells["C2"].Formula = "=INDEX(A2:B3,1,0-1)"; // -1
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["C2"].Value);
|
||||
}
|
||||
}
|
||||
64
Radzen.Blazor.Tests/Spreadsheet/InsertRowColumnTests.cs
Normal file
64
Radzen.Blazor.Tests/Spreadsheet/InsertRowColumnTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class InsertRowColumnTests
|
||||
{
|
||||
[Fact]
|
||||
public void InsertColumn_ShiftsReferencesAndValues()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
|
||||
sheet.Cells[1, 0].Value = 1; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
Assert.Equal(11d, sheet.Cells[1, 1].Value);
|
||||
|
||||
// Insert a column before A (index 0)
|
||||
sheet.InsertColumn(0, 1);
|
||||
|
||||
// Values shift right
|
||||
Assert.Equal(1d, sheet.Cells[1, 1].Value); // A2 moved to B2
|
||||
|
||||
// Formula shifts position and updates referenced address
|
||||
Assert.Equal("=B2+10", sheet.Cells[1, 2].Formula); // original B2 moved to C2
|
||||
Assert.Equal(11d, sheet.Cells[1, 2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRow_ShiftsReferencesAndValues()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
|
||||
sheet.Cells[1, 0].Value = 1; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
Assert.Equal(11d, sheet.Cells[1, 1].Value);
|
||||
|
||||
// Insert a row before row 2 (index 1)
|
||||
sheet.InsertRow(1, 1);
|
||||
|
||||
// Values shift down
|
||||
Assert.Equal(1d, sheet.Cells[2, 0].Value); // A2 moved to A3
|
||||
|
||||
// Formula shifts position and updates referenced address
|
||||
Assert.Equal("=A3+10", sheet.Cells[2, 1].Formula); // original B2 moved to B3
|
||||
Assert.Equal(11d, sheet.Cells[2, 1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRow_IncreasesRowCount()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.InsertRow(2, 2);
|
||||
Assert.Equal(7, sheet.RowCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertColumn_IncreasesColumnCount()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.InsertColumn(3, 3);
|
||||
Assert.Equal(8, sheet.ColumnCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
Radzen.Blazor.Tests/Spreadsheet/IntFunctionTests.cs
Normal file
30
Radzen.Blazor.Tests/Spreadsheet/IntFunctionTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class IntFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownPositive()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=INT(8.9)";
|
||||
Assert.Equal(8d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownNegative()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=INT(0-8.9)";
|
||||
Assert.Equal(-9d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnDecimalPart()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 19.5;
|
||||
sheet.Cells["A1"].Formula = "=A2-INT(A2)";
|
||||
Assert.Equal(0.5, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
74
Radzen.Blazor.Tests/Spreadsheet/LargeFunctionTests.cs
Normal file
74
Radzen.Blazor.Tests/Spreadsheet/LargeFunctionTests.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class LargeFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnKthLargestAcrossRange()
|
||||
{
|
||||
// Populate A2:B6 as in example
|
||||
sheet.Cells["A2"].Value = 3;
|
||||
sheet.Cells["A3"].Value = 4;
|
||||
sheet.Cells["A4"].Value = 5;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
sheet.Cells["A6"].Value = 3;
|
||||
|
||||
sheet.Cells["B2"].Value = 4;
|
||||
sheet.Cells["B3"].Value = 5;
|
||||
sheet.Cells["B4"].Value = 6;
|
||||
sheet.Cells["B5"].Value = 4;
|
||||
sheet.Cells["B6"].Value = 7;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=LARGE(A2:B6,3)";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturn7thLargestAsFour()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 3;
|
||||
sheet.Cells["A3"].Value = 4;
|
||||
sheet.Cells["A4"].Value = 5;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
sheet.Cells["A6"].Value = 3;
|
||||
|
||||
sheet.Cells["B2"].Value = 4;
|
||||
sheet.Cells["B3"].Value = 5;
|
||||
sheet.Cells["B4"].Value = 6;
|
||||
sheet.Cells["B5"].Value = 4;
|
||||
sheet.Cells["B6"].Value = 7;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=LARGE(A2:B6,7)";
|
||||
|
||||
Assert.Equal(4d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNumErrorForInvalidK()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["A3"].Value = 3;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=LARGE(A1:A3,0)";
|
||||
Assert.Equal(CellError.Num, sheet.Cells["B1"].Value);
|
||||
|
||||
sheet.Cells["B2"].Formula = "=LARGE(A1:A3,5)";
|
||||
Assert.Equal(CellError.Num, sheet.Cells["B2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldIgnoreNonNumericCellsInArray()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "x"; // ignored
|
||||
sheet.Cells["A3"].Value = 7;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=LARGE(A1:A3,2)";
|
||||
Assert.Equal(7d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
}
|
||||
42
Radzen.Blazor.Tests/Spreadsheet/LeftFunctionTests.cs
Normal file
42
Radzen.Blazor.Tests/Spreadsheet/LeftFunctionTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class LeftFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Left_WithCount_ReturnsPrefix()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Sale Price";
|
||||
sheet.Cells["B1"].Formula = "=LEFT(A2,4)";
|
||||
Assert.Equal("Sale", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Left_OmittedCount_DefaultsToOne()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "Sweden";
|
||||
sheet.Cells["B1"].Formula = "=LEFT(A3)";
|
||||
Assert.Equal("S", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Left_CountExceedsLength_ReturnsWhole()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Hi";
|
||||
sheet.Cells["B1"].Formula = "=LEFT(A1,5)";
|
||||
Assert.Equal("Hi", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Left_NegativeCount_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Test";
|
||||
sheet.Cells["B1"].Formula = "=LEFT(A1,-1)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
59
Radzen.Blazor.Tests/Spreadsheet/LenFunctionTests.cs
Normal file
59
Radzen.Blazor.Tests/Spreadsheet/LenFunctionTests.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class LenFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Len_String_ReturnsLength()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Phoenix, AZ"; // 11 characters
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(11d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Len_BooleanCellTrue_ReturnsFour()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(4d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Len_BooleanCellFalse_ReturnsFive()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(5d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
[Fact]
|
||||
public void Len_Empty_ReturnsZero()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = null; // empty
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(0d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Len_String_WithSpaces_CountsSpaces()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = " One "; // 11 characters including spaces
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(11d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Len_Number_TreatsAsTextLength()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = 123.45; // "123.45" length 6
|
||||
sheet.Cells["B1"].Formula = "=LEN(A1)";
|
||||
Assert.Equal(6d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
24
Radzen.Blazor.Tests/Spreadsheet/LowerFunctionTests.cs
Normal file
24
Radzen.Blazor.Tests/Spreadsheet/LowerFunctionTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class LowerFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Lower_ConvertsToLowercase()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "E. E. Cummings";
|
||||
sheet.Cells["B1"].Formula = "=LOWER(A2)";
|
||||
Assert.Equal("e. e. cummings", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lower_IgnoresNonLetters()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "Apt. 2B";
|
||||
sheet.Cells["B1"].Formula = "=LOWER(A3)";
|
||||
Assert.Equal("apt. 2b", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
64
Radzen.Blazor.Tests/Spreadsheet/MaxAllFunctionTests.cs
Normal file
64
Radzen.Blazor.Tests/Spreadsheet/MaxAllFunctionTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MaxAllFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateLogicalValuesInRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true; // 1
|
||||
sheet.Cells["A2"].Value = false; // 0
|
||||
sheet.Cells["A3"].Value = 5; // 5
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAXA(A1:A3)";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTreatTextInRangeAsZeroAndNumericTextAsNumber()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "abc"; // -> 0
|
||||
sheet.Cells["A2"].Value = "15"; // -> 15
|
||||
sheet.Cells["A3"].Value = 10;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAXA(A1:A3)";
|
||||
|
||||
Assert.Equal(15d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCountDirectLogicalAndTextArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=MAXA(1=1, \"7\", 1=2)"; // TRUE, "7", FALSE -> 7
|
||||
Assert.Equal(7d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroWhenNoValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = null; // empty
|
||||
sheet.Cells["A2"].Value = ""; // empty string -> Empty
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAXA(A1:A2)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPropagateErrors()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=A1/A2"; // #DIV/0!
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAXA(A1:A3)";
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["B1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
69
Radzen.Blazor.Tests/Spreadsheet/MaxFunctionTests.cs
Normal file
69
Radzen.Blazor.Tests/Spreadsheet/MaxFunctionTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MaxFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnLargestValueFromNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 7;
|
||||
sheet.Cells["A3"].Value = 9;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAX(A1:A5)";
|
||||
|
||||
Assert.Equal(27d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnLargestValueFromRangeAndLiteral()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 7;
|
||||
sheet.Cells["A3"].Value = 9;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAX(A1:A5,30)";
|
||||
|
||||
Assert.Equal(30d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldIgnoreNonNumericInRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = null;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MAX(A1:A5)";
|
||||
|
||||
Assert.Equal(27d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTreatDirectLogicalAndNumericStringsAsNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=MAX(\"15\", 5, 10)";
|
||||
Assert.Equal(15d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroWhenNoNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "a";
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=MAX(A1:A2)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
Radzen.Blazor.Tests/Spreadsheet/MidFunctionTests.cs
Normal file
51
Radzen.Blazor.Tests/Spreadsheet/MidFunctionTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MidFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Mid_Start1_Take5()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Fluid Flow"; // length 10
|
||||
sheet.Cells["B1"].Formula = "=MID(A2,1,5)";
|
||||
Assert.Equal("Fluid", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mid_Start7_Take20_Clamped()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Fluid Flow"; // length 10
|
||||
sheet.Cells["B1"].Formula = "=MID(A2,7,20)";
|
||||
Assert.Equal("Flow", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mid_StartBeyondLength_ReturnsEmpty()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Fluid Flow"; // length 10
|
||||
sheet.Cells["B1"].Formula = "=MID(A2,20,5)";
|
||||
Assert.Equal(string.Empty, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mid_StartLessThan1_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Fluid Flow";
|
||||
sheet.Cells["B1"].Formula = "=MID(A2,0,5)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mid_NegativeNumChars_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Fluid Flow";
|
||||
sheet.Cells["B1"].Formula = "=MID(A2,1,-1)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
64
Radzen.Blazor.Tests/Spreadsheet/MinAllFunctionTests.cs
Normal file
64
Radzen.Blazor.Tests/Spreadsheet/MinAllFunctionTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MinAllFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateLogicalValuesInRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true; // 1
|
||||
sheet.Cells["A2"].Value = false; // 0
|
||||
sheet.Cells["A3"].Value = 5; // 5
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MINA(A1:A3)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTreatTextInRangeAsZeroAndNumericTextAsNumber()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "abc"; // -> 0
|
||||
sheet.Cells["A2"].Value = "15"; // -> 15
|
||||
sheet.Cells["A3"].Value = 10;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MINA(A1:A3)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCountDirectLogicalAndTextArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=MINA(1=1, \"7\", 1=2)"; // TRUE, "7", FALSE -> min is 0
|
||||
Assert.Equal(0d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroWhenNoValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = null; // empty
|
||||
sheet.Cells["A2"].Value = ""; // empty string -> Empty
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MINA(A1:A2)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldPropagateErrors()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=A1/A2"; // #DIV/0!
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MINA(A1:A3)";
|
||||
|
||||
Assert.Equal(CellError.Div0, sheet.Cells["B1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
69
Radzen.Blazor.Tests/Spreadsheet/MinFunctionTests.cs
Normal file
69
Radzen.Blazor.Tests/Spreadsheet/MinFunctionTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MinFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnSmallestValueFromNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 7;
|
||||
sheet.Cells["A3"].Value = 9;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MIN(A1:A5)";
|
||||
|
||||
Assert.Equal(2d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnSmallestValueFromRangeAndLiteral()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 7;
|
||||
sheet.Cells["A3"].Value = 9;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MIN(A1:A5,1)";
|
||||
|
||||
Assert.Equal(1d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldIgnoreNonNumericInRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = "text";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Value = 27;
|
||||
sheet.Cells["A5"].Value = null;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=MIN(A1:A5)";
|
||||
|
||||
Assert.Equal(10d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTreatDirectNumericStringsAsNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=MIN(\"15\", 5, 10)";
|
||||
Assert.Equal(5d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnZeroWhenNoNumbers()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "a";
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=MIN(A1:A2)";
|
||||
|
||||
Assert.Equal(0d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
Radzen.Blazor.Tests/Spreadsheet/MinuteFunctionTests.cs
Normal file
34
Radzen.Blazor.Tests/Spreadsheet/MinuteFunctionTests.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MinuteFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Minute_FromFraction_ReturnsMinutes()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
// 0.78125 = 18:45 -> minutes 45
|
||||
sheet.Cells["A1"].Data = CellData.FromNumber(0.78125);
|
||||
sheet.Cells["B1"].Formula = "=MINUTE(A1)";
|
||||
Assert.Equal(45, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Minute_FromDateTimeValue_ReturnsMinute()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Data = CellData.FromDate(new System.DateTime(2011, 7, 18, 7, 45, 0));
|
||||
sheet.Cells["B2"].Formula = "=MINUTE(A2)";
|
||||
Assert.Equal(45, sheet.Cells["B2"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Minute_FromDateOnly_ReturnsZero()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Data = CellData.FromDate(new System.DateTime(2012, 4, 21));
|
||||
sheet.Cells["B3"].Formula = "=MINUTE(A3)";
|
||||
Assert.Equal(0, sheet.Cells["B3"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
}
|
||||
31
Radzen.Blazor.Tests/Spreadsheet/MonthFunctionTests.cs
Normal file
31
Radzen.Blazor.Tests/Spreadsheet/MonthFunctionTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class MonthFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Month_FromDateSerial_ReturnsMonth()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=MONTH(VALUE(\"2011-04-15\"))";
|
||||
Assert.Equal(4, sheet.Cells["A1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Month_FromDateValue_ReturnsMonth()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2011, 4, 15));
|
||||
sheet.Cells["B1"].Formula = "=MONTH(A1)";
|
||||
Assert.Equal(4, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Month_InvalidText_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=MONTH(\"abc\")";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
200
Radzen.Blazor.Tests/Spreadsheet/NotFunctionTests.cs
Normal file
200
Radzen.Blazor.Tests/Spreadsheet/NotFunctionTests.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class NotFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithTrueValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithEmptyStringAsError()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "";
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithFalseValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithNumericValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithZeroValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithStringValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "test";
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithEmptyValue()
|
||||
{
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithComparison()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 50;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1>100)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithTrueComparison()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 150;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1>100)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForNotFunctionWithNoArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=NOT()";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForNotFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=NOT(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithRangeExpression()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1:A1)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithDecimalValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0.5m;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithNegativeValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = -5;
|
||||
sheet.Cells["A2"].Formula = "=NOT(A1)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionInIfStatement()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 50;
|
||||
sheet.Cells["A2"].Formula = "=IF(NOT(A1>100),\"Valid\",\"Invalid\")";
|
||||
|
||||
Assert.Equal("Valid", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionInIfStatementWithFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 150;
|
||||
sheet.Cells["A2"].Formula = "=IF(NOT(A1>100),\"Valid\",\"Invalid\")";
|
||||
|
||||
Assert.Equal("Invalid", sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithProvidedExample1()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 50;
|
||||
sheet.Cells["A3"].Formula = "=NOT(A2>100)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithProvidedExample2()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 50;
|
||||
sheet.Cells["A3"].Formula = "=IF(AND(NOT(A2>1),NOT(A2<100)),A2,\"The value is out of range\")";
|
||||
|
||||
Assert.Equal("The value is out of range", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithProvidedExample3()
|
||||
{
|
||||
sheet.Cells["A3"].Value = 100;
|
||||
sheet.Cells["A4"].Formula = "=IF(OR(NOT(A3<0),NOT(A3>50)),A3,\"The value is out of range\")";
|
||||
|
||||
Assert.Equal(100d, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithNestedLogicalFunctions()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=NOT(AND(A1,A2))";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateNotFunctionWithOrFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=NOT(OR(A1,A2))";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
Radzen.Blazor.Tests/Spreadsheet/NowFunctionTests.cs
Normal file
33
Radzen.Blazor.Tests/Spreadsheet/NowFunctionTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class NowFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Now_ReturnsCurrentDateTime()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=NOW()";
|
||||
var dt = sheet.Cells["A1"].Data.GetValueOrDefault<System.DateTime>();
|
||||
Assert.Equal(System.DateTime.Today, dt.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Now_MinusToday_IsFractionalDayBetween0And1()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=NOW()-TODAY()";
|
||||
var serial = sheet.Cells["A1"].Data.GetValueOrDefault<double>();
|
||||
Assert.True(serial >= 0 && serial < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Now_PlusSevenDays_MinusToday_IsBetweenSevenAndEight()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=NOW()+7 - TODAY()";
|
||||
var serial = sheet.Cells["A1"].Data.GetValueOrDefault<double>();
|
||||
Assert.True(serial >= 7 && serial < 8);
|
||||
}
|
||||
}
|
||||
216
Radzen.Blazor.Tests/Spreadsheet/OrFunctionTests.cs
Normal file
216
Radzen.Blazor.Tests/Spreadsheet/OrFunctionTests.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class OrFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithAllTrueValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithOneTrueValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = true;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithAllFalseValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithNumericValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1>1,A2<100)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithZeroAsFalse()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Value = 1;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithBothZeroAsFalse()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Value = 0;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithNonZeroAsTrue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithStringValues()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "test";
|
||||
sheet.Cells["A2"].Value = "hello";
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithStringValueAndNumber()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "2";
|
||||
sheet.Cells["A2"].Value = "hello";
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithEmptyStringAsFalse()
|
||||
{
|
||||
sheet.Cells["A2"].Value = "2";
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithBothEmptyStringsAsFalse()
|
||||
{
|
||||
sheet.Cells["A3"].Formula = "=OR(A1,A2)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Formula = "=OR(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithAllFalseInMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = false;
|
||||
sheet.Cells["A3"].Value = false;
|
||||
sheet.Cells["A4"].Formula = "=OR(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(false, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForEmptyOrFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=OR()";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithRangeExpression()
|
||||
{
|
||||
sheet.Cells["A1"].Value = false;
|
||||
sheet.Cells["A2"].Value = true;
|
||||
sheet.Cells["A3"].Formula = "=OR(A1:A2)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithMixedTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Value = "";
|
||||
sheet.Cells["A3"].Value = true;
|
||||
sheet.Cells["A4"].Formula = "=OR(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionInIfStatement()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 5;
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Formula = "=IF(OR(A1>1,A2<100),A1,\"Out of range\")";
|
||||
|
||||
Assert.Equal(5d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionInIfStatementWithFalseCondition()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 0;
|
||||
sheet.Cells["A2"].Value = 150;
|
||||
sheet.Cells["A3"].Formula = "=IF(OR(A1>1,A2<100),A1,\"Out of range\")";
|
||||
|
||||
Assert.Equal("Out of range", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithProvidedExample1()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 50;
|
||||
sheet.Cells["A3"].Formula = "=OR(A2>1,A2<100)";
|
||||
|
||||
Assert.Equal(true, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithProvidedExample2()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 5;
|
||||
sheet.Cells["A3"].Value = 25;
|
||||
sheet.Cells["A4"].Formula = "=IF(OR(A2>1,A2<100),A3,\"The value is out of range\")";
|
||||
|
||||
Assert.Equal(25d, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateOrFunctionWithProvidedExample3()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 75;
|
||||
sheet.Cells["A3"].Formula = "=IF(OR(A2<0,A2>50),A2,\"The value is out of range\")";
|
||||
|
||||
Assert.Equal(75d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
Radzen.Blazor.Tests/Spreadsheet/ProperFunctionTests.cs
Normal file
33
Radzen.Blazor.Tests/Spreadsheet/ProperFunctionTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ProperFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Proper_TitleCase_Simple()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "this is a TITLE";
|
||||
sheet.Cells["B1"].Formula = "=PROPER(A2)";
|
||||
Assert.Equal("This Is A Title", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Proper_KeepsHyphenation()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "2-way street";
|
||||
sheet.Cells["B1"].Formula = "=PROPER(A3)";
|
||||
Assert.Equal("2-Way Street", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Proper_AlnumBoundary()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A4"].Value = "76BudGet";
|
||||
sheet.Cells["B1"].Formula = "=PROPER(A4)";
|
||||
Assert.Equal("76Budget", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
31
Radzen.Blazor.Tests/Spreadsheet/RandBetweenFunctionTests.cs
Normal file
31
Radzen.Blazor.Tests/Spreadsheet/RandBetweenFunctionTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RandBetweenFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 100)]
|
||||
[InlineData(-1, 1)]
|
||||
[InlineData(0, 0)]
|
||||
public void RandBetween_ShouldReturnInclusiveRange(int bottom, int top)
|
||||
{
|
||||
static string Tok(int n) => n < 0 ? $"0{n}" : n.ToString();
|
||||
sheet.Cells["A1"].Formula = $"=RANDBETWEEN({Tok(bottom)},{Tok(top)})";
|
||||
var v = sheet.Cells["A1"].Value;
|
||||
Assert.IsType<double>(v); // numeric stored as double
|
||||
var d = (double)v;
|
||||
Assert.True(d >= bottom && d <= top);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RandBetween_ShouldReturnNumError_WhenBottomGreaterThanTop()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=RANDBETWEEN(5,1)";
|
||||
Assert.Equal(CellError.Num, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
Radzen.Blazor.Tests/Spreadsheet/RandFunctionTests.cs
Normal file
36
Radzen.Blazor.Tests/Spreadsheet/RandFunctionTests.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RandFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void Rand_ShouldReturnInRangeZeroToOneExclusiveOfOne()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=RAND()";
|
||||
var v = sheet.Cells["A1"].Value;
|
||||
Assert.IsType<double>(v);
|
||||
var d = (double)v;
|
||||
Assert.True(d >= 0d && d < 1d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rand_RecalculatesOnFormulaReassignment()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=RAND()";
|
||||
var d1 = (double)sheet.Cells["A1"].Value;
|
||||
sheet.Cells["A1"].Formula = "=RAND()"; // force recalc
|
||||
var d2 = (double)sheet.Cells["A1"].Value;
|
||||
// It's possible (though unlikely) to be equal; allow a retry window
|
||||
if (d1 == d2)
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=RAND()";
|
||||
d2 = (double)sheet.Cells["A1"].Value;
|
||||
}
|
||||
Assert.True(d1 != d2 || (d1 >= 0d && d1 < 1d));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
132
Radzen.Blazor.Tests/Spreadsheet/RangeSelectionItemTests.cs
Normal file
132
Radzen.Blazor.Tests/Spreadsheet/RangeSelectionItemTests.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Bunit;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RangeSelectionItemTests : TestContext
|
||||
{
|
||||
private readonly Sheet sheet = new(4, 4);
|
||||
|
||||
[Fact]
|
||||
public void RangeSelectionItem_ShouldCalculateCorrectMaskForMergedCells()
|
||||
{
|
||||
// Arrange
|
||||
var mergedRange = RangeRef.Parse("B1:C1");
|
||||
sheet.MergedCells.Add(mergedRange);
|
||||
|
||||
// Select a range that overlaps with the merged cell
|
||||
var selectionRange = RangeRef.Parse("A1:D1");
|
||||
sheet.Selection.Select(selectionRange);
|
||||
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<RangeSelectionItem>(parameters => parameters
|
||||
.Add(p => p.Range, selectionRange)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Cell, sheet.Selection.Cell) // This should be A1 (the active cell)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-range");
|
||||
Assert.NotNull(element);
|
||||
|
||||
// The style should include mask properties that account for the merged cell
|
||||
var style = element.GetAttribute("style");
|
||||
Assert.NotNull(style);
|
||||
Assert.Contains("mask-size", style);
|
||||
Assert.Contains("mask-position", style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RangeSelectionItem_ShouldHandleNonMergedCellsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var selectionRange = RangeRef.Parse("A1:B2");
|
||||
sheet.Selection.Select(selectionRange);
|
||||
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Act
|
||||
var cut = RenderComponent<RangeSelectionItem>(parameters => parameters
|
||||
.Add(p => p.Range, selectionRange)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Cell, sheet.Selection.Cell)
|
||||
.Add(p => p.Context, context));
|
||||
|
||||
// Assert
|
||||
var element = cut.Find(".rz-spreadsheet-selection-range");
|
||||
Assert.NotNull(element);
|
||||
|
||||
var style = element.GetAttribute("style");
|
||||
Assert.NotNull(style);
|
||||
Assert.Contains("transform", style);
|
||||
Assert.Contains("width", style);
|
||||
Assert.Contains("height", style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RangeSelectionItem_ShouldHandleMergedCellsAcrossFrozenColumnBoundary()
|
||||
{
|
||||
// Arrange
|
||||
sheet.Columns.Frozen = 1; // Freeze first column
|
||||
var mergedRange = RangeRef.Parse("A1:B1"); // Merged cell spans across frozen boundary
|
||||
sheet.MergedCells.Add(mergedRange);
|
||||
|
||||
// Select the merged cell
|
||||
sheet.Selection.Select(mergedRange);
|
||||
|
||||
var context = new MockVirtualGridContext();
|
||||
|
||||
// Get the split ranges (frozen and non-frozen parts)
|
||||
var ranges = sheet.GetRanges(mergedRange).ToList();
|
||||
Assert.Equal(2, ranges.Count); // Should be split into 2 parts
|
||||
|
||||
// Test the frozen part (A1:A1)
|
||||
var frozenRange = ranges.First(r => r.FrozenColumn);
|
||||
var frozenCut = RenderComponent<RangeSelectionItem>(parameters => parameters
|
||||
.Add(p => p.Range, frozenRange.Range)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Cell, sheet.Selection.Cell)
|
||||
.Add(p => p.Context, context)
|
||||
.Add(p => p.FrozenColumn, frozenRange.FrozenColumn)
|
||||
.Add(p => p.FrozenRow, frozenRange.FrozenRow)
|
||||
.Add(p => p.Top, frozenRange.Top)
|
||||
.Add(p => p.Left, frozenRange.Left)
|
||||
.Add(p => p.Bottom, frozenRange.Bottom)
|
||||
.Add(p => p.Right, frozenRange.Right));
|
||||
|
||||
var frozenElement = frozenCut.Find(".rz-spreadsheet-selection-range");
|
||||
Assert.NotNull(frozenElement);
|
||||
Assert.Contains("rz-spreadsheet-frozen-column", frozenElement.ClassName);
|
||||
|
||||
var frozenStyle = frozenElement.GetAttribute("style");
|
||||
Assert.NotNull(frozenStyle);
|
||||
Assert.Contains("mask-size", frozenStyle);
|
||||
Assert.Contains("mask-position", frozenStyle);
|
||||
|
||||
// Test the non-frozen part (B1:B1)
|
||||
var nonFrozenRange = ranges.First(r => !r.FrozenColumn);
|
||||
var nonFrozenCut = RenderComponent<RangeSelectionItem>(parameters => parameters
|
||||
.Add(p => p.Range, nonFrozenRange.Range)
|
||||
.Add(p => p.Sheet, sheet)
|
||||
.Add(p => p.Cell, sheet.Selection.Cell)
|
||||
.Add(p => p.Context, context)
|
||||
.Add(p => p.FrozenColumn, nonFrozenRange.FrozenColumn)
|
||||
.Add(p => p.FrozenRow, nonFrozenRange.FrozenRow)
|
||||
.Add(p => p.Top, nonFrozenRange.Top)
|
||||
.Add(p => p.Left, nonFrozenRange.Left)
|
||||
.Add(p => p.Bottom, nonFrozenRange.Bottom)
|
||||
.Add(p => p.Right, nonFrozenRange.Right));
|
||||
|
||||
var nonFrozenElement = nonFrozenCut.Find(".rz-spreadsheet-selection-range");
|
||||
Assert.NotNull(nonFrozenElement);
|
||||
Assert.DoesNotContain("rz-spreadsheet-frozen-column", nonFrozenElement.ClassName);
|
||||
|
||||
var nonFrozenStyle = nonFrozenElement.GetAttribute("style");
|
||||
Assert.NotNull(nonFrozenStyle);
|
||||
Assert.Contains("mask-size", nonFrozenStyle);
|
||||
Assert.Contains("mask-position", nonFrozenStyle);
|
||||
}
|
||||
}
|
||||
51
Radzen.Blazor.Tests/Spreadsheet/ReplaceFunctionTests.cs
Normal file
51
Radzen.Blazor.Tests/Spreadsheet/ReplaceFunctionTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ReplaceFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Replace_Middle_WithAsterisk()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "abcdefghijk";
|
||||
sheet.Cells["B1"].Formula = "=REPLACE(A2,6,5,\"*\")";
|
||||
Assert.Equal("abcde*k", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_LastTwoDigits_With10()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "2009";
|
||||
sheet.Cells["B1"].Formula = "=REPLACE(A3,3,2,\"10\")";
|
||||
Assert.Equal("2010", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_FirstThree_WithAt()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A4"].Value = "123456";
|
||||
sheet.Cells["B1"].Formula = "=REPLACE(A4,1,3,\"@\")";
|
||||
Assert.Equal("@456", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_StartBeyond_Appends()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "abc";
|
||||
sheet.Cells["B1"].Formula = "=REPLACE(A1,10,2,\"XYZ\")";
|
||||
Assert.Equal("abcXYZ", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_InvalidStartNum_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "abc";
|
||||
sheet.Cells["B1"].Formula = "=REPLACE(A1,0,2,\"X\")";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
47
Radzen.Blazor.Tests/Spreadsheet/ReptFunctionTests.cs
Normal file
47
Radzen.Blazor.Tests/Spreadsheet/ReptFunctionTests.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ReptFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Rept_Basic()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=REPT(\"*-\",3)";
|
||||
Assert.Equal("*-*-*-", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rept_DashesTen()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=REPT(\"-\",10)";
|
||||
Assert.Equal("----------", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rept_ZeroTimes_ReturnsEmpty()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=REPT(\"x\",0)";
|
||||
Assert.Equal(string.Empty, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rept_Negative_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=REPT(\"x\",-1)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rept_Overflow_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
// text length 2 * 20000 = 40000 > 32767
|
||||
sheet.Cells["A1"].Formula = "=REPT(\"ab\",20000)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
42
Radzen.Blazor.Tests/Spreadsheet/RightFunctionTests.cs
Normal file
42
Radzen.Blazor.Tests/Spreadsheet/RightFunctionTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RightFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Right_WithCount_ReturnsSuffix()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Sale Price";
|
||||
sheet.Cells["B1"].Formula = "=RIGHT(A2,5)";
|
||||
Assert.Equal("Price", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Right_OmittedCount_DefaultsToOne()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "Stock Number";
|
||||
sheet.Cells["B1"].Formula = "=RIGHT(A3)";
|
||||
Assert.Equal("r", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Right_CountExceedsLength_ReturnsWhole()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Hi";
|
||||
sheet.Cells["B1"].Formula = "=RIGHT(A1,5)";
|
||||
Assert.Equal("Hi", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Right_NegativeCount_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Test";
|
||||
sheet.Cells["B1"].Formula = "=RIGHT(A1,-1)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
39
Radzen.Blazor.Tests/Spreadsheet/RoundDownFunctionTests.cs
Normal file
39
Radzen.Blazor.Tests/Spreadsheet/RoundDownFunctionTests.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RoundDownFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownToZeroDecimalPlaces()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDDOWN(3.2,0)";
|
||||
Assert.Equal(3d, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Formula = "=ROUNDDOWN(76.9,0)";
|
||||
Assert.Equal(76d, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownToSpecifiedDecimalPlaces()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDDOWN(3.14159,3)";
|
||||
Assert.Equal(3.141, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownNegativeNumbersTowardZero()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDDOWN(0-3.14159,1)";
|
||||
Assert.Equal(-3.1, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundDownToLeftOfDecimalWhenNegativeDigits()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDDOWN(31415.92654,0-2)";
|
||||
Assert.Equal(31400d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
41
Radzen.Blazor.Tests/Spreadsheet/RoundFunctionTests.cs
Normal file
41
Radzen.Blazor.Tests/Spreadsheet/RoundFunctionTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RoundFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundToOneDecimalPlace()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUND(2.15,1)";
|
||||
Assert.Equal(2.2, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Formula = "=ROUND(2.149,1)";
|
||||
Assert.Equal(2.1, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundNegativeWithAwayFromZeroMidpoint()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUND(0-1.475,2)";
|
||||
Assert.Equal(-1.48, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundWithNegativeDigits()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUND(21.5,0-1)";
|
||||
Assert.Equal(20d, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Formula = "=ROUND(626.3,0-3)";
|
||||
Assert.Equal(1000d, sheet.Cells["A2"].Value);
|
||||
|
||||
sheet.Cells["A3"].Formula = "=ROUND(1.98,0-1)";
|
||||
Assert.Equal(0d, sheet.Cells["A3"].Value);
|
||||
|
||||
sheet.Cells["A4"].Formula = "=ROUND(0-50.55,0-2)";
|
||||
Assert.Equal(-100d, sheet.Cells["A4"].Value);
|
||||
}
|
||||
}
|
||||
39
Radzen.Blazor.Tests/Spreadsheet/RoundUpFunctionTests.cs
Normal file
39
Radzen.Blazor.Tests/Spreadsheet/RoundUpFunctionTests.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RoundUpFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundUpToZeroDecimalPlaces()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDUP(3.2,0)";
|
||||
Assert.Equal(4d, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Formula = "=ROUNDUP(76.9,0)";
|
||||
Assert.Equal(77d, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundUpToSpecifiedDecimalPlaces()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDUP(3.14159,3)";
|
||||
Assert.Equal(3.142, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundUpNegativeNumbersAwayFromZero()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDUP(0-3.14159,1)";
|
||||
Assert.Equal(-3.2, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRoundUpToLeftOfDecimalWhenNegativeDigits()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=ROUNDUP(31415.92654,0-2)";
|
||||
Assert.Equal(31500d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
301
Radzen.Blazor.Tests/Spreadsheet/RowColumnCommandTests.cs
Normal file
301
Radzen.Blazor.Tests/Spreadsheet/RowColumnCommandTests.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RowColumnCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public void DeleteRowsCommand_SingleRow_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(4, 3);
|
||||
sheet.Cells[0, 0].Value = "R1";
|
||||
sheet.Cells[1, 0].Value = "R2";
|
||||
sheet.Cells[2, 0].Value = "R3";
|
||||
sheet.Cells[3, 0].Value = "R4";
|
||||
|
||||
var cmd = new DeleteRowsCommand(sheet, 1, 1);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(3, sheet.RowCount);
|
||||
Assert.Equal("R1", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("R3", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("R4", sheet.Cells[2, 0].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(4, sheet.RowCount);
|
||||
Assert.Equal("R1", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("R2", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("R3", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal("R4", sheet.Cells[3, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteRowsCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(6, 3);
|
||||
sheet.Cells[0, 0].Value = "R1";
|
||||
sheet.Cells[1, 0].Value = "R2";
|
||||
sheet.Cells[2, 0].Value = "R3";
|
||||
sheet.Cells[3, 0].Value = "R4";
|
||||
sheet.Cells[4, 0].Value = "R5";
|
||||
sheet.Cells[5, 0].Value = "R6";
|
||||
|
||||
var cmd = new DeleteRowsCommand(sheet, 1, 3); // delete rows 2..4
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(3, sheet.RowCount);
|
||||
Assert.Equal("R1", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("R5", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("R6", sheet.Cells[2, 0].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
Assert.Equal(6, sheet.RowCount);
|
||||
Assert.Equal("R1", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("R2", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("R3", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal("R4", sheet.Cells[3, 0].Value);
|
||||
Assert.Equal("R5", sheet.Cells[4, 0].Value);
|
||||
Assert.Equal("R6", sheet.Cells[5, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRowAfterCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(4, 3);
|
||||
sheet.Cells[1, 0].Value = 1d; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
|
||||
var cmd = new InsertRowAfterCommand(sheet, 1); // after row 1 -> insert at index 2
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(5, sheet.RowCount);
|
||||
// Inserting after row 1 does not move row 1; A2 and B2 stay
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 1].Formula);
|
||||
Assert.Equal(11d, sheet.Cells[1, 1].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(4, sheet.RowCount);
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 1].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRowBeforeCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(4, 3);
|
||||
sheet.Cells[1, 0].Value = 1d; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
|
||||
var cmd = new InsertRowBeforeCommand(sheet, 1); // before row 1 -> insert at index 1
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(5, sheet.RowCount);
|
||||
// value shifted down (A2 becomes A3)
|
||||
Assert.Equal(1d, sheet.Cells[2, 0].Value);
|
||||
// formula shifted down and reference updated A2->A3
|
||||
Assert.Equal("=A3+10", sheet.Cells[2, 1].Formula);
|
||||
Assert.Equal(11d, sheet.Cells[2, 1].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(4, sheet.RowCount);
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 1].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRowAfterCommand_WithMultiSelection_InsertsAfterLastRow()
|
||||
{
|
||||
var sheet = new Sheet(6, 2);
|
||||
// Mark rows with their index in A
|
||||
for (int r = 0; r < 6; r++) sheet.Cells[r, 0].Value = r + 1;
|
||||
|
||||
// Simulate selection rows 1..3 (0-based), last is 3 -> insert after 3 at index 4
|
||||
var cmd = new InsertRowAfterCommand(sheet, 3);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(7, sheet.RowCount);
|
||||
// Inserted row at index 4 is empty
|
||||
Assert.Null(sheet.Cells[4, 0].Value);
|
||||
// Row 5 takes previous row 4 value (5)
|
||||
Assert.Equal(5d, sheet.Cells[5, 0].Value);
|
||||
// Row 6 takes previous row 5 value (6)
|
||||
Assert.Equal(6d, sheet.Cells[6, 0].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
Assert.Equal(6, sheet.RowCount);
|
||||
Assert.Equal(5d, sheet.Cells[4, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRowBeforeCommand_WithMultiSelection_InsertsBeforeFirstRow()
|
||||
{
|
||||
var sheet = new Sheet(6, 2);
|
||||
for (int r = 0; r < 6; r++) sheet.Cells[r, 0].Value = r + 1;
|
||||
|
||||
// Simulate selection rows 2..4 (first is 2) -> insert at index 2
|
||||
var cmd = new InsertRowBeforeCommand(sheet, 2);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(7, sheet.RowCount);
|
||||
// Inserted row at index 2 is empty
|
||||
Assert.Null(sheet.Cells[2, 0].Value);
|
||||
// Row 3 takes previous row 2 value (3)
|
||||
Assert.Equal(3d, sheet.Cells[3, 0].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
Assert.Equal(6, sheet.RowCount);
|
||||
Assert.Equal(3d, sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertColumnAfterCommand_WithMultiSelection_InsertsAfterLastColumn()
|
||||
{
|
||||
var sheet = new Sheet(2, 6);
|
||||
// Mark columns with their index in row 1
|
||||
for (int c = 0; c < 6; c++) sheet.Cells[0, c].Value = c + 1;
|
||||
|
||||
// Simulate selection columns 1..3 (last is 3) -> insert after col 3 at index 4
|
||||
var cmd = new InsertColumnAfterCommand(sheet, 3);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(7, sheet.ColumnCount);
|
||||
// Inserted column at index 4 is empty
|
||||
Assert.Null(sheet.Cells[0, 4].Value);
|
||||
// Column 5 takes previous column 4 value (5)
|
||||
Assert.Equal(5d, sheet.Cells[0, 5].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
Assert.Equal(6, sheet.ColumnCount);
|
||||
Assert.Equal(5d, sheet.Cells[0, 4].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertColumnBeforeCommand_WithMultiSelection_InsertsBeforeFirstColumn()
|
||||
{
|
||||
var sheet = new Sheet(2, 6);
|
||||
for (int c = 0; c < 6; c++) sheet.Cells[0, c].Value = c + 1;
|
||||
|
||||
// Simulate selection columns 2..4 (first is 2) -> insert at index 2
|
||||
var cmd = new InsertColumnBeforeCommand(sheet, 2);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(7, sheet.ColumnCount);
|
||||
// Inserted column at index 2 is empty
|
||||
Assert.Null(sheet.Cells[0, 2].Value);
|
||||
// Column 3 takes previous column 2 value (3)
|
||||
Assert.Equal(3d, sheet.Cells[0, 3].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
Assert.Equal(6, sheet.ColumnCount);
|
||||
Assert.Equal(3d, sheet.Cells[0, 2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteColumnsCommand_SingleColumn_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(3, 4);
|
||||
sheet.Cells[0, 0].Value = "A";
|
||||
sheet.Cells[0, 1].Value = "B";
|
||||
sheet.Cells[0, 2].Value = "C";
|
||||
sheet.Cells[0, 3].Value = "D";
|
||||
|
||||
var cmd = new DeleteColumnsCommand(sheet, 1, 1);
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(3, sheet.ColumnCount);
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("C", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("D", sheet.Cells[0, 2].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(4, sheet.ColumnCount);
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("C", sheet.Cells[0, 2].Value);
|
||||
Assert.Equal("D", sheet.Cells[0, 3].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteColumnsCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(3, 6);
|
||||
sheet.Cells[0, 0].Value = "A";
|
||||
sheet.Cells[0, 1].Value = "B";
|
||||
sheet.Cells[0, 2].Value = "C";
|
||||
sheet.Cells[0, 3].Value = "D";
|
||||
sheet.Cells[0, 4].Value = "E";
|
||||
sheet.Cells[0, 5].Value = "F";
|
||||
|
||||
var cmd = new DeleteColumnsCommand(sheet, 1, 3); // delete B..D
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(3, sheet.ColumnCount);
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("E", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("F", sheet.Cells[0, 2].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(6, sheet.ColumnCount);
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("C", sheet.Cells[0, 2].Value);
|
||||
Assert.Equal("D", sheet.Cells[0, 3].Value);
|
||||
Assert.Equal("E", sheet.Cells[0, 4].Value);
|
||||
Assert.Equal("F", sheet.Cells[0, 5].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertColumnBeforeCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.Cells[1, 0].Value = 1d; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
|
||||
var cmd = new InsertColumnBeforeCommand(sheet, 0); // before A
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(6, sheet.ColumnCount);
|
||||
// value shifted right (A2 becomes B2)
|
||||
Assert.Equal(1d, sheet.Cells[1, 1].Value);
|
||||
// formula shifted right and reference updated A2->B2
|
||||
Assert.Equal("=B2+10", sheet.Cells[1, 2].Formula); // original B2 moved to C2
|
||||
Assert.Equal(11d, sheet.Cells[1, 2].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(5, sheet.ColumnCount);
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 1].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertColumnAfterCommand_ExecuteAndUndo_RestoresState()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
sheet.Cells[1, 0].Value = 1d; // A2
|
||||
sheet.Cells[1, 1].Formula = "=A2+10"; // B2
|
||||
|
||||
var cmd = new InsertColumnAfterCommand(sheet, 0); // after A
|
||||
Assert.True(sheet.Commands.Execute(cmd));
|
||||
|
||||
Assert.Equal(6, sheet.ColumnCount);
|
||||
// value at A2 stays, but B2 moves to C2; formula references should update if referencing >= inserted column
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 2].Formula); // original B2 moved to C2
|
||||
Assert.Equal(11d, sheet.Cells[1, 2].Value);
|
||||
|
||||
sheet.Commands.Undo();
|
||||
|
||||
Assert.Equal(5, sheet.ColumnCount);
|
||||
Assert.Equal(1d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("=A2+10", sheet.Cells[1, 1].Formula);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
Radzen.Blazor.Tests/Spreadsheet/RowFunctionTests.cs
Normal file
38
Radzen.Blazor.Tests/Spreadsheet/RowFunctionTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RowFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Row_OmittedReference_ReturnsCurrentRow()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["C10"].Formula = "=ROW()";
|
||||
Assert.Equal(10d, sheet.Cells["C10"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_SingleCellReference_ReturnsThatRow()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["A1"].Formula = "=ROW(C10)";
|
||||
Assert.Equal(10d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_RangeReference_ReturnsTopLeftRow()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["B2"].Formula = "=ROW(C10:E10)";
|
||||
Assert.Equal(10d, sheet.Cells["B2"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_RangeReference_MultiRowAndColumn_IsError()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
sheet.Cells["B2"].Formula = "=ROW(C10:D20)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["B2"].Data.Value);
|
||||
}
|
||||
}
|
||||
30
Radzen.Blazor.Tests/Spreadsheet/RowsFunctionTests.cs
Normal file
30
Radzen.Blazor.Tests/Spreadsheet/RowsFunctionTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class RowsFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Rows_Range_ReturnsRowCount()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=ROWS(C1:E4)";
|
||||
Assert.Equal(4d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rows_SingleCell_ReturnsOne()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=ROWS(C10)";
|
||||
Assert.Equal(1d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rows_SingleRowRange_ReturnsOne()
|
||||
{
|
||||
var sheet = new Sheet(50, 20);
|
||||
sheet.Cells["A1"].Formula = "=ROWS(C10:E10)";
|
||||
Assert.Equal(1d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
}
|
||||
65
Radzen.Blazor.Tests/Spreadsheet/SearchFunctionTests.cs
Normal file
65
Radzen.Blazor.Tests/Spreadsheet/SearchFunctionTests.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SearchFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Search_SimpleCharacter()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=SEARCH(\"n\",\"printer\")";
|
||||
Assert.Equal(4d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_Substring()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=SEARCH(\"base\",\"database\")";
|
||||
Assert.Equal(5d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_StartNum_SkipsPrefix()
|
||||
{
|
||||
var sheet = new Sheet(10, 30);
|
||||
sheet.Cells["A1"].Value = "AYF0093.YoungMensApparel";
|
||||
sheet.Cells["B1"].Formula = "=SEARCH(\"Y\",A1,8)";
|
||||
Assert.Equal(9d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_Wildcards_QuestionAndAsterisk()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "Profit Margin";
|
||||
sheet.Cells["B1"].Formula = "=SEARCH(\"M*r?in\",A1)";
|
||||
Assert.Equal(8d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_TildeEscapesWildcards()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = "a*?c";
|
||||
sheet.Cells["B1"].Formula = "=SEARCH(\"~*~?\",A1)";
|
||||
Assert.Equal(2d, sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_StartNumInvalid_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=SEARCH(\"e\",\"printer\",0)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Search_NotFound_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=SEARCH(\"zzz\",\"printer\")";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
33
Radzen.Blazor.Tests/Spreadsheet/SecondFunctionTests.cs
Normal file
33
Radzen.Blazor.Tests/Spreadsheet/SecondFunctionTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SecondFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Second_FromDateTimeWithSeconds_ReturnsSeconds()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2020, 1, 1, 16, 48, 18));
|
||||
sheet.Cells["B1"].Formula = "=SECOND(A1)";
|
||||
Assert.Equal(18, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Second_FromDateTimeWithoutSeconds_ReturnsZero()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Data = CellData.FromDate(new System.DateTime(2020, 1, 1, 16, 48, 0));
|
||||
sheet.Cells["B2"].Formula = "=SECOND(A2)";
|
||||
Assert.Equal(0, sheet.Cells["B2"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Second_FromDateOnly_ReturnsZero()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Data = CellData.FromDate(new System.DateTime(2020, 1, 1));
|
||||
sheet.Cells["B3"].Formula = "=SECOND(A3)";
|
||||
Assert.Equal(0, sheet.Cells["B3"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
}
|
||||
287
Radzen.Blazor.Tests/Spreadsheet/SelectionCycleTests.cs
Normal file
287
Radzen.Blazor.Tests/Spreadsheet/SelectionCycleTests.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SelectionCycleTests
|
||||
{
|
||||
readonly Sheet sheet = new(4, 4);
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:C1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:C1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheFirstCellInTheRangeWhenAtLastColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("C1"), RangeRef.Parse("A1:C1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:C1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextRowInTheRangeWhenAtLastColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("C1"), RangeRef.Parse("A1:C2"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:C2"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextVerticalCell()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:A3"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A3"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheFirstCellInTheRangeWhenAtLastRow()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A3"), RangeRef.Parse("A1:A3"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A3"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextColumnInTheRangeWhenAtLastRow()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A3"), RangeRef.Parse("A1:B3"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:B3"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("B1:C1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("C1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("B1:C1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheLastCellInTheRangeWhenAtFirstColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"), RangeRef.Parse("A1:C1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("C1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:C1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousRowInTheRangeWhenAtFirstColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"), RangeRef.Parse("A1:C2"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("C2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:C2"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextHorizontalCellIfOnlyOneCellIsSelected()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("B1:B1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextVerticalCellIfOnlyOneCellIsSelected()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A2:A2"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousHorizontalCellIfOnlyOneCellIsSelected()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousVerticalCellIfOnlyOneCellIsSelected()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A2"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(-1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_StayInTheSameCellIfTheFirstCellIsSelectedAndMovingBackwards()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_StayInTheSameCellIfTheLastCellIsSelectedAndMovingForwards()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("D4"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("D4", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("D4:D4"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_StayInTheSameCellIfOnlyOneCellIsSelectedAndMovingUp()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(-1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_StayInTheSameCellIfOnlyOneCellIsSelectedAndMovingDown()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("D4"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("D4", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("D4:D4"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_GoToTheFirstCellInTheSheetIfNoSelection()
|
||||
{
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_GoToTheNextHorizontalMergedCellInTheSelectedRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:D1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:D1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_GoToThePreviousHorizontalMergedCellInTheSelectedRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"), RangeRef.Parse("A1:D1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, -1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:D1"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_GoToTheNextVerticalMergedCellInTheSelectedRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A2:A3"));
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:A4"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(1, 0);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A4"), sheet.Selection.Range);
|
||||
}
|
||||
[Fact]
|
||||
public void Should_GoToThePreviousVerticalMergedCellInTheSelectedRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A2:A3"));
|
||||
sheet.Selection.Select(CellRef.Parse("A4"), RangeRef.Parse("A1:A4"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(-1, 0);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("A1:A4"), sheet.Selection.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveFromMergedCellToNextCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
var cell = sheet.Selection.Cycle(0, 1);
|
||||
|
||||
Assert.Equal("D1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
Assert.Equal(RangeRef.Parse("D1:D1"), sheet.Selection.Range);
|
||||
}
|
||||
}
|
||||
445
Radzen.Blazor.Tests/Spreadsheet/SelectionExtensionTests.cs
Normal file
445
Radzen.Blazor.Tests/Spreadsheet/SelectionExtensionTests.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SelectionExtensionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAppendNextHorizontalCellToRange()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAppendNextVerticalCellToRange()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B1:B2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousHorizontalCellFromRange()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousVerticalCellFromRange()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("B1:B2"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAppendPreviousHorizontalCellToRange()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldFirstAppendAndThenSubtractNextHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldFirstAppendAndThenSubtractPreviousHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAppendPreviousVerticalCellToRange()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B2"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B2", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldFirstAppendAndThenSubtractVerticalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B2"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B2:B2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B2", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_ShouldSelectTheWholeMergedCellRangeWhenTheStartIsUsed()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Select_ShouldSelectTheWholeMergedCellRangeWhenTheEndIsUsed()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"));
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextHorizontalCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:D1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextHorizontalCellAfterMergedCellAgain()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:E1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextMergedCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("D1:E1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:E1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractNextMergedCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("D1:E1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, 1);
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousMergedCellBeforeMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("D1:E1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:E1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("D1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousMergedCellBeforeMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("D1:E1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("D1:E1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("D1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousHorizontalMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:D1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("D1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubractPreviousMergedCellFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:D1", sheet.Selection.Range.ToString());
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("D1:D1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("D1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractNextMergedCellFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:A1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousHorizontalCellBeforeMergedCellFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousHorizontalCellBeforeMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextVerticalCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B1:B3", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextVerticalCellAfterMergedCellAgain()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B1:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldHandleMergedCellsConsistentlyWhenMovingLeft()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("B1:D1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("D1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldHandleMergedCellsConsistentlyWhenMovingLeftFromStart()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldHandleMergedCellsConsistentlyWhenMovingLeftFromEnd()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldHandleMergedCellsConsistentlyWhenMovingLeftFromEndAndThenRight()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
sheet.Selection.Extend(0, 1);
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldHandleMergedCellsConsistentlyWhenMovingLeftFromMiddle()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:D1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"));
|
||||
sheet.Selection.Extend(0, -1);
|
||||
|
||||
Assert.Equal("A1:D1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddNextMergedRowAfterMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B3:B4"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B1:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractNextMergedRowAfterMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B3:B4"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Extend(1, 0);
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousMergedRowBeforeMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B3:B4"));
|
||||
sheet.Selection.Select(CellRef.Parse("B3"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B3", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousMergedRowBeforeMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:B2"));
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B3:B4"));
|
||||
sheet.Selection.Select(CellRef.Parse("B3"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B3:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B3", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousVerticalMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Select(CellRef.Parse("B4"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B2:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B4", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousMergedRowFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Select(CellRef.Parse("B4"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B2:B4", sheet.Selection.Range.ToString());
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B4:B4", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B4", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractNextMergedRowFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Select(RangeRef.Parse("B1:B3"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldSubtractPreviousVerticalCellBeforeMergedRowFromSelection()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Select(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
Assert.Equal("B1:B3", sheet.Selection.Range.ToString());
|
||||
|
||||
sheet.Selection.Extend(1, 0);
|
||||
|
||||
Assert.Equal("B2:B3", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B2", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extend_ShouldAddPreviousVerticalCellBeforeMergedRow()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B2:B3"));
|
||||
sheet.Selection.Select(CellRef.Parse("B2"));
|
||||
sheet.Selection.Extend(-1, 0);
|
||||
|
||||
Assert.Equal("B1:B3", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B2", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
}
|
||||
71
Radzen.Blazor.Tests/Spreadsheet/SelectionMergingTests.cs
Normal file
71
Radzen.Blazor.Tests/Spreadsheet/SelectionMergingTests.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SelectionMergingTests
|
||||
{
|
||||
readonly Sheet sheet = new(4, 4);
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectAllHorizontalCellsBetweenTheCurrentCellAndTheNewOne()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("C1"));
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectAllVerticalCellsBetweenTheCurrentCellAndTheNewOne()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("A3"));
|
||||
|
||||
Assert.Equal("A1:A3", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_ExtendRangeWhenMerginANewCell()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:C1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("D2"));
|
||||
|
||||
Assert.Equal("A1:D2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_AddTheEntireMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("B1"));
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_AddTheEntireMergedCellWhenSelectingIt()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("A1"));
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_ExpandSelectionToIncludeMergedCells()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
sheet.Selection.Merge(CellRef.Parse("B2"));
|
||||
|
||||
Assert.Equal("A1:C2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
}
|
||||
155
Radzen.Blazor.Tests/Spreadsheet/SelectionMovingTests.cs
Normal file
155
Radzen.Blazor.Tests/Spreadsheet/SelectionMovingTests.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SelectionMovingTests
|
||||
{
|
||||
readonly Sheet sheet = new(4, 4);
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, 1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_NotMoveToNextHorizontalCellWhenAlreadyAtLastColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("D1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, 1);
|
||||
|
||||
Assert.Equal("D1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToTheNextVerticalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(1, 0);
|
||||
|
||||
Assert.Equal("A2", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_NotMoveToNextVerticalCellWhenAlreadyAtLastRow()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A4"));
|
||||
|
||||
var cell = sheet.Selection.Move(1, 0);
|
||||
|
||||
Assert.Equal("A4", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousHorizontalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, -1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_NotMoveToPreviousHorizontalCellWhenAlreadyAtFirstColumn()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, -1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToThePreviousVerticalCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A2"));
|
||||
|
||||
var cell = sheet.Selection.Move(-1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_NotMoveToPreviousVerticalCellWhenAlreadyAtFirstRow()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(-1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToNextHorizontalCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A1:B1"));
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, 1);
|
||||
|
||||
Assert.Equal("C1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToNextVerticalCellAfterMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A1:A2"));
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
var cell = sheet.Selection.Move(1, 0);
|
||||
|
||||
Assert.Equal("A3", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToPreviousHorizontalCellBeforeMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, -1);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveToPreviousVerticalCellBeforeMergedCell()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A2:A3"));
|
||||
sheet.Selection.Select(CellRef.Parse("A2"));
|
||||
|
||||
var cell = sheet.Selection.Move(-1, 0);
|
||||
|
||||
Assert.Equal("A1", cell.ToString());
|
||||
Assert.Equal(sheet.Selection.Cell, cell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_MoveTheSelectedCellOnly()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:C1"));
|
||||
|
||||
var cell = sheet.Selection.Move(0, 1);
|
||||
|
||||
Assert.Equal("B1", cell.ToString());
|
||||
Assert.Equal("B1:B1", sheet.Selection.Range.ToString());
|
||||
}
|
||||
}
|
||||
75
Radzen.Blazor.Tests/Spreadsheet/SelectionTests.cs
Normal file
75
Radzen.Blazor.Tests/Spreadsheet/SelectionTests.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SelectionTests
|
||||
{
|
||||
readonly Sheet sheet = new(4, 4);
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectSingleCell()
|
||||
{
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
Assert.Equal("A1:A1", sheet.Selection.Range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectRangeOfCells()
|
||||
{
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:C3"));
|
||||
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Range.Start.ToString());
|
||||
Assert.Equal("C3", sheet.Selection.Range.End.ToString());
|
||||
Assert.Equal("A1:C3", sheet.Selection.Range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectTheWholeMergedCellAsPartOfRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:B1"));
|
||||
|
||||
Assert.Equal("A1:C1", sheet.Selection.Range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectTheWholeMergedCellByStart()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("B1"));
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectTheWholeMergedCellByEnd()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"));
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectTheWholeMergedCellByEndAndRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("B1:C1"));
|
||||
sheet.Selection.Select(CellRef.Parse("C1"), RangeRef.Parse("B1:C1"));
|
||||
|
||||
Assert.Equal("B1:C1", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("B1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SelectCompleteMergedCellRange()
|
||||
{
|
||||
sheet.MergedCells.Add(RangeRef.Parse("A1:D1"));
|
||||
sheet.Selection.Select(RangeRef.Parse("A1:B2"));
|
||||
|
||||
Assert.Equal("A1:D2", sheet.Selection.Range.ToString());
|
||||
Assert.Equal("A1", sheet.Selection.Cell.ToString());
|
||||
}
|
||||
}
|
||||
43
Radzen.Blazor.Tests/Spreadsheet/SheetRangeTests.cs
Normal file
43
Radzen.Blazor.Tests/Spreadsheet/SheetRangeTests.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SheetRangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Range_Parse_ShouldParseValidRange()
|
||||
{
|
||||
var range = RangeRef.Parse("A1:B2");
|
||||
|
||||
Assert.Equal(new CellRef(0, 0), range.Start);
|
||||
Assert.Equal(new CellRef(1, 1), range.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_Parse_ShouldThrowOnInvalidRange()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => RangeRef.Parse("A1:B2:C3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_GetCells_ShouldReturnAllCellsInRange()
|
||||
{
|
||||
var range = RangeRef.Parse("A1:B2");
|
||||
var cells = range.GetCells().ToList();
|
||||
|
||||
Assert.Equal(4, cells.Count);
|
||||
Assert.Equal(new CellRef(0, 0), cells[0]); // A1
|
||||
Assert.Equal(new CellRef(0, 1), cells[1]); // B1
|
||||
Assert.Equal(new CellRef(1, 0), cells[2]); // A2
|
||||
Assert.Equal(new CellRef(1, 1), cells[3]); // B2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_ToString_ShouldReturnA1Notation()
|
||||
{
|
||||
var range = new RangeRef(new CellRef(0, 0), new CellRef(1, 1));
|
||||
Assert.Equal("A1:B2", range.ToString());
|
||||
}
|
||||
}
|
||||
58
Radzen.Blazor.Tests/Spreadsheet/SmallFunctionTests.cs
Normal file
58
Radzen.Blazor.Tests/Spreadsheet/SmallFunctionTests.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SmallFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(12, 12);
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturn4thSmallestInFirstColumn()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 3;
|
||||
sheet.Cells["A3"].Value = 4;
|
||||
sheet.Cells["A4"].Value = 5;
|
||||
sheet.Cells["A5"].Value = 2;
|
||||
sheet.Cells["A6"].Value = 3;
|
||||
sheet.Cells["A7"].Value = 4;
|
||||
sheet.Cells["A8"].Value = 6;
|
||||
sheet.Cells["A9"].Value = 4;
|
||||
sheet.Cells["A10"].Value = 7;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SMALL(A2:A10,4)";
|
||||
|
||||
Assert.Equal(4d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturn2ndSmallestInSecondColumn()
|
||||
{
|
||||
sheet.Cells["B2"].Value = 1;
|
||||
sheet.Cells["B3"].Value = 4;
|
||||
sheet.Cells["B4"].Value = 8;
|
||||
sheet.Cells["B5"].Value = 3;
|
||||
sheet.Cells["B6"].Value = 7;
|
||||
sheet.Cells["B7"].Value = 12;
|
||||
sheet.Cells["B8"].Value = 54;
|
||||
sheet.Cells["B9"].Value = 8;
|
||||
sheet.Cells["B10"].Value = 23;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SMALL(B2:B10,2)";
|
||||
|
||||
Assert.Equal(3d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldErrorForEmptyArrayOrInvalidK()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=SMALL(A2:A2,1)"; // empty range
|
||||
Assert.Equal(CellError.Num, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Value = 5;
|
||||
sheet.Cells["A3"].Formula = "=SMALL(A2:A2,0)"; // k <= 0
|
||||
Assert.Equal(CellError.Num, sheet.Cells["A3"].Value);
|
||||
|
||||
sheet.Cells["A4"].Formula = "=SMALL(A2:A2,2)"; // k > count
|
||||
Assert.Equal(CellError.Num, sheet.Cells["A4"].Value);
|
||||
}
|
||||
}
|
||||
147
Radzen.Blazor.Tests/Spreadsheet/SortCommandTests.cs
Normal file
147
Radzen.Blazor.Tests/Spreadsheet/SortCommandTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SortCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public void SortCommand_ShouldSortDataInAscendingOrder()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var range = RangeRef.Parse("A1:A3");
|
||||
|
||||
// Set up test data
|
||||
sheet.Cells["A1"].Value = "Charlie";
|
||||
sheet.Cells["A2"].Value = "Alice";
|
||||
sheet.Cells["A3"].Value = "Bob";
|
||||
|
||||
var command = new SortCommand(sheet, range, SortOrder.Ascending, 0);
|
||||
|
||||
// Act
|
||||
var result = command.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal("Alice", sheet.Cells["A1"].Value);
|
||||
Assert.Equal("Bob", sheet.Cells["A2"].Value);
|
||||
Assert.Equal("Charlie", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortCommand_ShouldSortDataInDescendingOrder()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var range = RangeRef.Parse("A1:A3");
|
||||
|
||||
// Set up test data
|
||||
sheet.Cells["A1"].Value = "Alice";
|
||||
sheet.Cells["A2"].Value = "Bob";
|
||||
sheet.Cells["A3"].Value = "Charlie";
|
||||
|
||||
var command = new SortCommand(sheet, range, SortOrder.Descending, 0);
|
||||
|
||||
// Act
|
||||
var result = command.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal("Charlie", sheet.Cells["A1"].Value);
|
||||
Assert.Equal("Bob", sheet.Cells["A2"].Value);
|
||||
Assert.Equal("Alice", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortCommand_ShouldRestoreOriginalOrderWhenUndone()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var range = RangeRef.Parse("A1:A3");
|
||||
|
||||
// Set up test data
|
||||
sheet.Cells["A1"].Value = "Charlie";
|
||||
sheet.Cells["A2"].Value = "Alice";
|
||||
sheet.Cells["A3"].Value = "Bob";
|
||||
|
||||
var command = new SortCommand(sheet, range, SortOrder.Ascending, 0);
|
||||
|
||||
// Act
|
||||
command.Execute();
|
||||
command.Unexecute();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Charlie", sheet.Cells["A1"].Value);
|
||||
Assert.Equal("Alice", sheet.Cells["A2"].Value);
|
||||
Assert.Equal("Bob", sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortCommand_ShouldReturnFalseForInvalidRange()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var command = new SortCommand(sheet, RangeRef.Invalid, SortOrder.Ascending, 0);
|
||||
|
||||
// Act
|
||||
var result = command.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortCommand_ShouldPreserveCellFormatting()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var range = RangeRef.Parse("A1:A2");
|
||||
|
||||
// Set up test data with formatting
|
||||
sheet.Cells["A1"].Value = "Charlie";
|
||||
sheet.Cells["A1"].Format.Bold = true;
|
||||
sheet.Cells["A2"].Value = "Alice";
|
||||
sheet.Cells["A2"].Format.Italic = true;
|
||||
|
||||
var command = new SortCommand(sheet, range, SortOrder.Ascending, 0);
|
||||
|
||||
// Act
|
||||
command.Execute();
|
||||
command.Unexecute();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Charlie", sheet.Cells["A1"].Value);
|
||||
Assert.True(sheet.Cells["A1"].Format.Bold);
|
||||
Assert.Equal("Alice", sheet.Cells["A2"].Value);
|
||||
Assert.True(sheet.Cells["A2"].Format.Italic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortCommand_ShouldWorkWithAutoFilterRange()
|
||||
{
|
||||
// Arrange
|
||||
var sheet = new Sheet(5, 5);
|
||||
var range = RangeRef.Parse("A1:B3");
|
||||
|
||||
// Set up test data in a format similar to AutoFilter
|
||||
sheet.Cells["A1"].Value = "Name";
|
||||
sheet.Cells["B1"].Value = "Age";
|
||||
sheet.Cells["A2"].Value = "Charlie";
|
||||
sheet.Cells["B2"].Value = 30;
|
||||
sheet.Cells["A3"].Value = "Alice";
|
||||
sheet.Cells["B3"].Value = 25;
|
||||
|
||||
var command = new SortCommand(sheet, range, SortOrder.Ascending, 0, skipHeaderRow: true);
|
||||
|
||||
// Act
|
||||
var result = command.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal("Alice", sheet.Cells["A2"].Value);
|
||||
Assert.Equal(25.0, sheet.Cells["B2"].Value);
|
||||
Assert.Equal("Charlie", sheet.Cells["A3"].Value);
|
||||
Assert.Equal(30.0, sheet.Cells["B3"].Value);
|
||||
}
|
||||
}
|
||||
231
Radzen.Blazor.Tests/Spreadsheet/SortingTests.cs
Normal file
231
Radzen.Blazor.Tests/Spreadsheet/SortingTests.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SortingTests
|
||||
{
|
||||
private readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void Should_SortStringsInAscendingOrder()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "C";
|
||||
sheet.Cells[1, 0].Value = "A";
|
||||
sheet.Cells[2, 0].Value = "B";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:A3"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("C", sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortStringsInDescendingOrder()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "C";
|
||||
sheet.Cells[1, 0].Value = "A";
|
||||
sheet.Cells[2, 0].Value = "B";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:A3"), SortOrder.Descending);
|
||||
|
||||
Assert.Equal("C", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("A", sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortNumbersInDescendingOrder()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = 1;
|
||||
sheet.Cells[1, 0].Value = 3;
|
||||
sheet.Cells[2, 0].Value = 2;
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:A3"), SortOrder.Descending);
|
||||
|
||||
Assert.Equal(3d, sheet.Cells[0, 0].Value);
|
||||
Assert.Equal(2d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal(1d, sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortFullRowsByColumnA()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "B"; sheet.Cells[0, 1].Value = "B2";
|
||||
sheet.Cells[1, 0].Value = "A"; sheet.Cells[1, 1].Value = "A2";
|
||||
sheet.Cells[2, 0].Value = "C"; sheet.Cells[2, 1].Value = "C2";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:B3"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("A2", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("B", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("B2", sheet.Cells[1, 1].Value);
|
||||
Assert.Equal("C", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal("C2", sheet.Cells[2, 1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_IgnoreHeaderRowInSort()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "Header";
|
||||
sheet.Cells[1, 0].Value = "C";
|
||||
sheet.Cells[2, 0].Value = "A";
|
||||
sheet.Cells[3, 0].Value = "B";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A2:A4"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("Header", sheet.Cells[0, 0].Value); // header unchanged
|
||||
Assert.Equal("A", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal("C", sheet.Cells[3, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortOnlySelectedRange_NotWholeSheet()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "Z";
|
||||
sheet.Cells[1, 0].Value = "X";
|
||||
sheet.Cells[2, 0].Value = "Y";
|
||||
sheet.Cells[3, 0].Value = "A";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A2:A4"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("Z", sheet.Cells[0, 0].Value); // unchanged
|
||||
Assert.Equal("A", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal("X", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal("Y", sheet.Cells[3, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortBlanksLastInAscendingOrder()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "B";
|
||||
sheet.Cells[1, 0].Value = null; // blank
|
||||
sheet.Cells[2, 0].Value = "A";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:A3"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal("B", sheet.Cells[1, 0].Value);
|
||||
Assert.Null(sheet.Cells[2, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_NotBreakDataAlignmentAcrossColumns()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = "Charlie"; sheet.Cells[0, 1].Value = "30";
|
||||
sheet.Cells[1, 0].Value = "Alice"; sheet.Cells[1, 1].Value = "25";
|
||||
sheet.Cells[2, 0].Value = "Bob"; sheet.Cells[2, 1].Value = "20";
|
||||
|
||||
sheet.Sort(RangeRef.Parse("A1:B3"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal("Alice", sheet.Cells[0, 0].Value);
|
||||
Assert.Equal(25d, sheet.Cells[0, 1].Value);
|
||||
Assert.Equal("Bob", sheet.Cells[1, 0].Value);
|
||||
Assert.Equal(20d, sheet.Cells[1, 1].Value);
|
||||
Assert.Equal("Charlie", sheet.Cells[2, 0].Value);
|
||||
Assert.Equal(30d, sheet.Cells[2, 1].Value);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Should_SortByAbsoluteColumnIndex()
|
||||
{
|
||||
// Data layout now starts at column B (index 1):
|
||||
// | B (Name) | C (Age) | D (Dept) |
|
||||
// |----------|---------|----------|
|
||||
// | Charlie | 42 | Dev |
|
||||
// | Alice | 25 | HR |
|
||||
// | Bob | 30 | Sales |
|
||||
|
||||
sheet.Cells[0, 1].Value = "Charlie"; // B1
|
||||
sheet.Cells[0, 2].Value = 42; // C1
|
||||
sheet.Cells[0, 3].Value = "Dev"; // D1
|
||||
|
||||
sheet.Cells[1, 1].Value = "Alice"; // B2
|
||||
sheet.Cells[1, 2].Value = 25; // C2
|
||||
sheet.Cells[1, 3].Value = "HR"; // D2
|
||||
|
||||
sheet.Cells[2, 1].Value = "Bob"; // B3
|
||||
sheet.Cells[2, 2].Value = 30; // C3
|
||||
sheet.Cells[2, 3].Value = "Sales"; // D3
|
||||
|
||||
// Sort range B1:D3 by **column C** (absolute index 2, "Age")
|
||||
sheet.Sort(RangeRef.Parse("B1:D3"), SortOrder.Ascending, keyIndex: 2);
|
||||
|
||||
// Sorted order: Alice (25), Bob (30), Charlie (42)
|
||||
|
||||
Assert.Equal("Alice", sheet.Cells[0, 1].Value);
|
||||
Assert.Equal(25d, sheet.Cells[0, 2].Value);
|
||||
Assert.Equal("HR", sheet.Cells[0, 3].Value);
|
||||
|
||||
Assert.Equal("Bob", sheet.Cells[1, 1].Value);
|
||||
Assert.Equal(30d, sheet.Cells[1, 2].Value);
|
||||
Assert.Equal("Sales", sheet.Cells[1, 3].Value);
|
||||
|
||||
Assert.Equal("Charlie", sheet.Cells[2, 1].Value);
|
||||
Assert.Equal(42d, sheet.Cells[2, 2].Value);
|
||||
Assert.Equal("Dev", sheet.Cells[2, 3].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SortCellsWithRelativeFormulasCorrectly()
|
||||
{
|
||||
// Initial setup:
|
||||
// A1: 2
|
||||
// A2: =A1 + 1 (should be 3)
|
||||
// A3: 1
|
||||
|
||||
sheet.Cells[0, 0].Value = 2; // A1
|
||||
sheet.Cells[1, 0].Formula = "=A1 + 1"; // A2
|
||||
sheet.Cells[2, 0].Value = 1; // A3
|
||||
|
||||
// Confirm initial formula result
|
||||
Assert.Equal(3d, sheet.Cells[1, 0].Value);
|
||||
|
||||
// Now sort A1:A3 in ascending order
|
||||
sheet.Sort(RangeRef.Parse("A1:A3"), SortOrder.Ascending);
|
||||
|
||||
// The sorted order should be:
|
||||
// A1: 1 (was A3)
|
||||
// A2: 2 (was A1)
|
||||
// A3: =A1 + 1 (was A2)
|
||||
|
||||
Assert.Equal(1d, sheet.Cells[0, 0].Value); // A1
|
||||
Assert.Equal(2d, sheet.Cells[1, 0].Value); // A2
|
||||
Assert.Equal("=A1 + 1", sheet.Cells[2, 0].Formula); // A3
|
||||
Assert.Equal(2d, sheet.Cells[2, 0].Value); // A3 evaluated: =1 + 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_PlaceBlankValuesAtBottomInAscendingAndDescending()
|
||||
{
|
||||
sheet.Cells[0, 0].Value = 3;
|
||||
sheet.Cells[1, 0].Value = null; // blank
|
||||
sheet.Cells[2, 0].Value = 1;
|
||||
sheet.Cells[3, 0].Value = 2;
|
||||
|
||||
// Sort ascending
|
||||
sheet.Sort(RangeRef.Parse("A1:A4"), SortOrder.Ascending);
|
||||
|
||||
Assert.Equal(1d, sheet.Cells[0, 0].Value);
|
||||
Assert.Equal(2d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal(3d, sheet.Cells[2, 0].Value);
|
||||
Assert.Null(sheet.Cells[3, 0].Value);
|
||||
|
||||
// Re-set original order
|
||||
sheet.Cells[0, 0].Value = 3;
|
||||
sheet.Cells[1, 0].Value = null;
|
||||
sheet.Cells[2, 0].Value = 1;
|
||||
sheet.Cells[3, 0].Value = 2;
|
||||
|
||||
// Sort descending
|
||||
sheet.Sort(RangeRef.Parse("A1:A4"), SortOrder.Descending);
|
||||
|
||||
Assert.Equal(3d, sheet.Cells[0, 0].Value);
|
||||
Assert.Equal(2d, sheet.Cells[1, 0].Value);
|
||||
Assert.Equal(1d, sheet.Cells[2, 0].Value);
|
||||
Assert.Null(sheet.Cells[3, 0].Value);
|
||||
}
|
||||
}
|
||||
60
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs
Normal file
60
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SpreadsheetClipboardTests
|
||||
{
|
||||
[Fact]
|
||||
public void Copy_Down_AdjustsRelative_KeepsAbsolute()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells[0, 0].Formula = "=A1"; // A1
|
||||
sheet.PasteRange(sheet, RangeRef.Parse("A1"), CellRef.Parse("A2"), FormulaAdjustment.AdjustRelative);
|
||||
Assert.Equal("=A2", sheet.Cells[1, 0].Formula);
|
||||
|
||||
sheet.Cells[0, 1].Formula = "=$A$1"; // B1
|
||||
sheet.PasteRange(sheet, RangeRef.Parse("B1"), CellRef.Parse("B2"), FormulaAdjustment.AdjustRelative);
|
||||
Assert.Equal("=$A$1", sheet.Cells[1, 1].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_Right_AdjustsColumn_RetainsAbsoluteColumn()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells[0, 0].Formula = "=A1"; // A1
|
||||
sheet.PasteRange(sheet, RangeRef.Parse("A1"), CellRef.Parse("B1"), FormulaAdjustment.AdjustRelative);
|
||||
Assert.Equal("=B1", sheet.Cells[0, 1].Formula);
|
||||
|
||||
sheet.Cells[0, 1].Formula = "=$A1"; // B1 absolute column
|
||||
sheet.PasteRange(sheet, RangeRef.Parse("B1"), CellRef.Parse("C1"), FormulaAdjustment.AdjustRelative);
|
||||
Assert.Equal("=$A1", sheet.Cells[0, 2].Formula);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cut_DoesNotAdjustFormula()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells[0, 0].Formula = "=A1"; // A1
|
||||
sheet.Selection.Select(CellRef.Parse("A1"));
|
||||
var clipboard = new SpreadsheetClipboard();
|
||||
clipboard.Cut(sheet);
|
||||
clipboard.Paste(sheet, CellRef.Parse("B2"));
|
||||
Assert.Equal("=A1", sheet.Cells[1, 1].Formula); // not adjusted
|
||||
Assert.Null(sheet.Cells[0, 0].Formula); // source cleared
|
||||
Assert.Null(sheet.Cells[0, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_AcrossSheets_AdjustsRelativeReferences()
|
||||
{
|
||||
var source = new Sheet(10, 10);
|
||||
var target = new Sheet(10, 10);
|
||||
|
||||
source.Cells[0, 0].Formula = "=A1";
|
||||
|
||||
// Copy A1 from source to B2 in target
|
||||
target.PasteRange(source, RangeRef.Parse("A1"), CellRef.Parse("B2"), FormulaAdjustment.AdjustRelative);
|
||||
|
||||
Assert.Equal("=B2", target.Cells[1, 1].Formula);
|
||||
}
|
||||
}
|
||||
33
Radzen.Blazor.Tests/Spreadsheet/SubstituteFunctionTests.cs
Normal file
33
Radzen.Blazor.Tests/Spreadsheet/SubstituteFunctionTests.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SubstituteFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Substitute_AllOccurrences()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "Sales Data";
|
||||
sheet.Cells["B1"].Formula = "=SUBSTITUTE(A2, \"Sales\", \"Cost\")";
|
||||
Assert.Equal("Cost Data", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Substitute_FirstInstanceOnly()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "Quarter 1, 2008";
|
||||
sheet.Cells["B1"].Formula = "=SUBSTITUTE(A3, \"1\", \"2\", 1)";
|
||||
Assert.Equal("Quarter 2, 2008", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Substitute_ThirdInstanceOnly()
|
||||
{
|
||||
var sheet = new Sheet(10, 20);
|
||||
sheet.Cells["A4"].Value = "Quarter 1, 2011";
|
||||
sheet.Cells["B1"].Formula = "=SUBSTITUTE(A4, \"1\", \"2\", 3)";
|
||||
Assert.Equal("Quarter 1, 2012", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
94
Radzen.Blazor.Tests/Spreadsheet/SubtotalFunctionTests.cs
Normal file
94
Radzen.Blazor.Tests/Spreadsheet/SubtotalFunctionTests.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SubtotalFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldSumWithCode9()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 120;
|
||||
sheet.Cells["A3"].Value = 10;
|
||||
sheet.Cells["A4"].Value = 150;
|
||||
sheet.Cells["A5"].Value = 23;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(9,A2:A5)";
|
||||
|
||||
Assert.Equal(303d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldAverageWithCode1()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 120;
|
||||
sheet.Cells["A3"].Value = 10;
|
||||
sheet.Cells["A4"].Value = 150;
|
||||
sheet.Cells["A5"].Value = 23;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(1,A2:A5)";
|
||||
|
||||
Assert.Equal(75.75, sheet.Cells["B1"].Value);
|
||||
}
|
||||
[Fact]
|
||||
public void ShouldCountWithCode2()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 120;
|
||||
sheet.Cells["A3"].Value = "x"; // non-numeric, ignored by COUNT
|
||||
sheet.Cells["A4"].Value = 150;
|
||||
sheet.Cells["A5"].Value = null;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(2,A2:A5)";
|
||||
Assert.Equal(2d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCountAWithCode3()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 120;
|
||||
sheet.Cells["A3"].Value = "x";
|
||||
sheet.Cells["A4"].Value = null;
|
||||
sheet.Cells["A5"].Value = 23;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(3,A2:A5)";
|
||||
Assert.Equal(3d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldMaxWithCode4()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Value = 40;
|
||||
sheet.Cells["A4"].Value = 30;
|
||||
sheet.Cells["A5"].Value = 20;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(4,A2:A5)";
|
||||
Assert.Equal(40d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldMinWithCode5()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 10;
|
||||
sheet.Cells["A3"].Value = 40;
|
||||
sheet.Cells["A4"].Value = 30;
|
||||
sheet.Cells["A5"].Value = 20;
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(5,A2:A5)";
|
||||
Assert.Equal(10d, sheet.Cells["B1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldRespectHiddenRowsWith109()
|
||||
{
|
||||
sheet.Cells["A2"].Value = 120;
|
||||
sheet.Cells["A3"].Value = 10;
|
||||
sheet.Cells["A4"].Value = 150;
|
||||
sheet.Cells["A5"].Value = 20;
|
||||
sheet.Rows.Hide(2); // hide row 3 (A3)
|
||||
|
||||
sheet.Cells["B1"].Formula = "=SUBTOTAL(109,A2:A5)";
|
||||
Assert.Equal(290d, sheet.Cells["B1"].Value); // 120 + 150 + 20 (excludes hidden 10)
|
||||
}
|
||||
}
|
||||
73
Radzen.Blazor.Tests/Spreadsheet/SumFunctionTests.cs
Normal file
73
Radzen.Blazor.Tests/Spreadsheet/SumFunctionTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SumFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(5, 5);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["A3"].Formula = "=SUM(A1,A2)";
|
||||
|
||||
Assert.Equal(3d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumFunctionWithEmptyCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A3"].Formula = "=SUM(A1,A2)";
|
||||
|
||||
Assert.Equal(1d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumFunctionWithMultipleArguments()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["A3"].Value = 3;
|
||||
sheet.Cells["A4"].Formula = "=SUM(A1,A2,A3)";
|
||||
|
||||
Assert.Equal(6d, sheet.Cells["A4"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForEmptySumFunction()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=SUM()";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSumRangeOfCells()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["A3"].Formula = "=SUM(A1:A2)";
|
||||
|
||||
Assert.Equal(3d, sheet.Cells["A3"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSumNumbersOfDifferentTypes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2.5;
|
||||
sheet.Cells["A3"].Formula = "=SUM(A1,A2)";
|
||||
|
||||
Assert.Equal(3.5, sheet.Cells["A3"].Value);
|
||||
|
||||
sheet.Cells["A4"].Value = 2.5;
|
||||
sheet.Cells["A5"].Formula = "=SUM(A4,A1)";
|
||||
|
||||
Assert.Equal(3.5, sheet.Cells["A5"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
278
Radzen.Blazor.Tests/Spreadsheet/SumIfFunctionTests.cs
Normal file
278
Radzen.Blazor.Tests/Spreadsheet/SumIfFunctionTests.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SumIfFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithNumericCriteria()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = 100000;
|
||||
sheet.Cells["A2"].Value = 200000;
|
||||
sheet.Cells["A3"].Value = 300000;
|
||||
sheet.Cells["A4"].Value = 400000;
|
||||
|
||||
sheet.Cells["B1"].Value = 7000;
|
||||
sheet.Cells["B2"].Value = 14000;
|
||||
sheet.Cells["B3"].Value = 21000;
|
||||
sheet.Cells["B4"].Value = 28000;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\">160000\",B1:B4)";
|
||||
|
||||
Assert.Equal(63000d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithoutSumRange()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = 100000;
|
||||
sheet.Cells["A2"].Value = 200000;
|
||||
sheet.Cells["A3"].Value = 300000;
|
||||
sheet.Cells["A4"].Value = 400000;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\">160000\")";
|
||||
|
||||
Assert.Equal(900000d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithExactMatch()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = 100000;
|
||||
sheet.Cells["A2"].Value = 200000;
|
||||
sheet.Cells["A3"].Value = 300000;
|
||||
sheet.Cells["A4"].Value = 400000;
|
||||
|
||||
sheet.Cells["B1"].Value = 7000;
|
||||
sheet.Cells["B2"].Value = 14000;
|
||||
sheet.Cells["B3"].Value = 21000;
|
||||
sheet.Cells["B4"].Value = 28000;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,300000,B1:B4)";
|
||||
|
||||
Assert.Equal(21000d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithStringCriteria()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = "Vegetables";
|
||||
sheet.Cells["A2"].Value = "Vegetables";
|
||||
sheet.Cells["A3"].Value = "Fruits";
|
||||
sheet.Cells["A4"].Value = "";
|
||||
sheet.Cells["A5"].Value = "Vegetables";
|
||||
sheet.Cells["A6"].Value = "Fruits";
|
||||
|
||||
sheet.Cells["C1"].Value = 2300;
|
||||
sheet.Cells["C2"].Value = 5500;
|
||||
sheet.Cells["C3"].Value = 800;
|
||||
sheet.Cells["C4"].Value = 400;
|
||||
sheet.Cells["C5"].Value = 4200;
|
||||
sheet.Cells["C6"].Value = 1200;
|
||||
|
||||
sheet.Cells["D1"].Formula = "=SUMIF(A1:A6,\"Fruits\",C1:C6)";
|
||||
|
||||
Assert.Equal(2000d, sheet.Cells["D1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithWildcardPattern()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = "Vegetables";
|
||||
sheet.Cells["A2"].Value = "Vegetables";
|
||||
sheet.Cells["A3"].Value = "Fruits";
|
||||
sheet.Cells["A4"].Value = "";
|
||||
sheet.Cells["A5"].Value = "Vegetables";
|
||||
sheet.Cells["A6"].Value = "Fruits";
|
||||
|
||||
sheet.Cells["B1"].Value = "Tomatoes";
|
||||
sheet.Cells["B2"].Value = "Celery";
|
||||
sheet.Cells["B3"].Value = "Oranges";
|
||||
sheet.Cells["B4"].Value = "Butter";
|
||||
sheet.Cells["B5"].Value = "Carrots";
|
||||
sheet.Cells["B6"].Value = "Apples";
|
||||
|
||||
sheet.Cells["C1"].Value = 2300;
|
||||
sheet.Cells["C2"].Value = 5500;
|
||||
sheet.Cells["C3"].Value = 800;
|
||||
sheet.Cells["C4"].Value = 400;
|
||||
sheet.Cells["C5"].Value = 4200;
|
||||
sheet.Cells["C6"].Value = 1200;
|
||||
|
||||
sheet.Cells["D1"].Formula = "=SUMIF(B1:B6,\"*es\",C1:C6)";
|
||||
|
||||
Assert.Equal(4300d, sheet.Cells["D1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithEmptyCriteria()
|
||||
{
|
||||
// Test data from Excel example
|
||||
sheet.Cells["A1"].Value = "Vegetables";
|
||||
sheet.Cells["A2"].Value = "Vegetables";
|
||||
sheet.Cells["A3"].Value = "Fruits";
|
||||
sheet.Cells["A4"].Value = "";
|
||||
sheet.Cells["A5"].Value = "Vegetables";
|
||||
sheet.Cells["A6"].Value = "Fruits";
|
||||
|
||||
sheet.Cells["C1"].Value = 2300;
|
||||
sheet.Cells["C2"].Value = 5500;
|
||||
sheet.Cells["C3"].Value = 800;
|
||||
sheet.Cells["C4"].Value = 400;
|
||||
sheet.Cells["C5"].Value = 4200;
|
||||
sheet.Cells["C6"].Value = 1200;
|
||||
|
||||
sheet.Cells["D1"].Formula = "=SUMIF(A1:A6,\"\",C1:C6)";
|
||||
|
||||
Assert.Equal(400d, sheet.Cells["D1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithLessThanCriteria()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["A4"].Value = 40;
|
||||
|
||||
sheet.Cells["B1"].Value = 100;
|
||||
sheet.Cells["B2"].Value = 200;
|
||||
sheet.Cells["B3"].Value = 300;
|
||||
sheet.Cells["B4"].Value = 400;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\"<25\",B1:B4)";
|
||||
|
||||
Assert.Equal(300d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithGreaterThanOrEqualCriteria()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["A4"].Value = 40;
|
||||
|
||||
sheet.Cells["B1"].Value = 100;
|
||||
sheet.Cells["B2"].Value = 200;
|
||||
sheet.Cells["B3"].Value = 300;
|
||||
sheet.Cells["B4"].Value = 400;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\">=25\",B1:B4)";
|
||||
|
||||
Assert.Equal(700d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithNotEqualCriteria()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["A4"].Value = 40;
|
||||
|
||||
sheet.Cells["B1"].Value = 100;
|
||||
sheet.Cells["B2"].Value = 200;
|
||||
sheet.Cells["B3"].Value = 300;
|
||||
sheet.Cells["B4"].Value = 400;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\"<>20\",B1:B4)";
|
||||
|
||||
Assert.Equal(800d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithQuestionMarkWildcard()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "cat";
|
||||
sheet.Cells["A2"].Value = "bat";
|
||||
sheet.Cells["A3"].Value = "rat";
|
||||
sheet.Cells["A4"].Value = "goat";
|
||||
|
||||
sheet.Cells["B1"].Value = 10;
|
||||
sheet.Cells["B2"].Value = 20;
|
||||
sheet.Cells["B3"].Value = 30;
|
||||
sheet.Cells["B4"].Value = 40;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A4,\"?at\",B1:B4)";
|
||||
|
||||
Assert.Equal(60d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldEvaluateSumIfFunctionWithEscapedWildcards()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "*";
|
||||
sheet.Cells["A2"].Value = "?";
|
||||
sheet.Cells["A3"].Value = "test";
|
||||
|
||||
sheet.Cells["B1"].Value = 10;
|
||||
sheet.Cells["B2"].Value = 20;
|
||||
sheet.Cells["B3"].Value = 30;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A3,\"~*\",B1:B3)";
|
||||
|
||||
Assert.Equal(10d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForInvalidArgumentCount()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=SUMIF(A2)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Value);
|
||||
|
||||
sheet.Cells["A2"].Formula = "=SUMIF(A3,A4,A5,A6)";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A2"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnValueErrorForMismatchedRangeSizes()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 1;
|
||||
sheet.Cells["A2"].Value = 2;
|
||||
sheet.Cells["B1"].Value = 10;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A2,\">0\",B1)";
|
||||
|
||||
Assert.Equal(CellError.Value, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHandleEmptyCellsInSumRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
|
||||
// B2 is empty
|
||||
sheet.Cells["B1"].Value = 100;
|
||||
sheet.Cells["B3"].Value = 300;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A3,\">15\",B1:B3)";
|
||||
|
||||
Assert.Equal(300d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHandleNonNumericValuesInSumRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
|
||||
sheet.Cells["B1"].Value = 100;
|
||||
sheet.Cells["B2"].Value = "text"; // Non-numeric
|
||||
sheet.Cells["B3"].Value = 300;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=SUMIF(A1:A3,\">15\",B1:B3)";
|
||||
|
||||
Assert.Equal(300d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
}
|
||||
53
Radzen.Blazor.Tests/Spreadsheet/TextFunctionTests.cs
Normal file
53
Radzen.Blazor.Tests/Spreadsheet/TextFunctionTests.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class TextFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Text_Currency_Thousands_TwoDecimals()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = 1234.567;
|
||||
sheet.Cells["B1"].Formula = "=TEXT(A1,\"$#,##0.00\")";
|
||||
Assert.Equal("$1,234.57", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Text_Date_MMDDYY()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var dt = new System.DateTime(2012, 3, 14);
|
||||
sheet.Cells["A1"].Value = dt;
|
||||
sheet.Cells["B1"].Formula = "=TEXT(A1,\"MM/DD/YY\")";
|
||||
Assert.Equal("03/14/12", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Text_Time_12h_AMPM()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var dt = new System.DateTime(2020, 1, 1, 13, 29, 0);
|
||||
sheet.Cells["A1"].Value = dt;
|
||||
sheet.Cells["B1"].Formula = "=TEXT(A1,\"H:MM AM/PM\")";
|
||||
Assert.Equal("1:29 PM", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Text_Percent_OneDecimal()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = 0.285;
|
||||
sheet.Cells["B1"].Formula = "=TEXT(A1,\"0.0%\")";
|
||||
Assert.Equal("28.5%", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Text_LeadingZeros()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Value = 1234;
|
||||
sheet.Cells["B1"].Formula = "=TEXT(A1,\"0000000\")";
|
||||
Assert.Equal("0001234", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
57
Radzen.Blazor.Tests/Spreadsheet/TextJoinFunctionTests.cs
Normal file
57
Radzen.Blazor.Tests/Spreadsheet/TextJoinFunctionTests.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class TextJoinFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void TextJoin_Literals_IgnoreEmptyTrue()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=TEXTJOIN(\" \",TRUE,\"The\",\"sun\",\"will\",\"come\",\"up\",\"tomorrow.\")";
|
||||
Assert.Equal("The sun will come up tomorrow.", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextJoin_Range_CommaSpace_IgnoreEmptyTrue()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
// A2:A8 values
|
||||
sheet.Cells["A2"].Value = "US Dollar";
|
||||
sheet.Cells["A3"].Value = "Australian Dollar";
|
||||
sheet.Cells["A4"].Value = "Chinese Yuan";
|
||||
sheet.Cells["A5"].Value = "Hong Kong Dollar";
|
||||
sheet.Cells["A6"].Value = "Israeli Shekel";
|
||||
sheet.Cells["A7"].Value = "South Korean Won";
|
||||
sheet.Cells["A8"].Value = "Russian Ruble";
|
||||
sheet.Cells["B1"].Formula = "=TEXTJOIN(\", \", TRUE, A2:A8)";
|
||||
var result = sheet.Cells["B1"].Data.GetValueOrDefault<string>();
|
||||
Assert.Equal("US Dollar, Australian Dollar, Chinese Yuan, Hong Kong Dollar, Israeli Shekel, South Korean Won, Russian Ruble", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextJoin_Range2D_CommaSpace_IgnoreEmptyVariants()
|
||||
{
|
||||
var sheet = new Sheet(20, 10);
|
||||
// A2:B8 grid
|
||||
sheet.Cells["A2"].Value = "a1";
|
||||
sheet.Cells["B2"].Value = "b1";
|
||||
sheet.Cells["A3"].Value = "a2";
|
||||
sheet.Cells["B3"].Value = "b2";
|
||||
sheet.Cells["A4"].Value = string.Empty; // empty cell value
|
||||
sheet.Cells["B4"].Value = string.Empty;
|
||||
sheet.Cells["A5"].Value = "a5";
|
||||
sheet.Cells["B5"].Value = "b5";
|
||||
sheet.Cells["A6"].Value = "a6";
|
||||
sheet.Cells["B6"].Value = "b6";
|
||||
sheet.Cells["A7"].Value = "a7";
|
||||
sheet.Cells["B7"].Value = "b7";
|
||||
sheet.Cells["B1"].Formula = "=TEXTJOIN(\", \", TRUE, A2:B7)";
|
||||
Assert.Equal("a1, b1, a2, b2, a5, b5, a6, b6, a7, b7", sheet.Cells["B1"].Data.Value);
|
||||
|
||||
sheet.Cells["C1"].Formula = "=TEXTJOIN(\", \", FALSE, A2:B7)";
|
||||
Assert.Equal("a1, b1, a2, b2, , , a5, b5, a6, b6, a7, b7", sheet.Cells["C1"].Data.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
Radzen.Blazor.Tests/Spreadsheet/TodayFunctionTests.cs
Normal file
26
Radzen.Blazor.Tests/Spreadsheet/TodayFunctionTests.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Xunit;
|
||||
using Radzen.Blazor.Spreadsheet;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class TodayFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Today_ReturnsCurrentDate()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=TODAY()";
|
||||
var dt = sheet.Cells["A1"].Data.GetValueOrDefault<System.DateTime>();
|
||||
Assert.Equal(System.DateTime.Today.Date, dt.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Today_PlusFiveDays()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=TODAY()+5";
|
||||
var serial = sheet.Cells["A1"].Data.GetValueOrDefault<double>();
|
||||
var expected = System.DateTime.Today.AddDays(5).ToNumber();
|
||||
Assert.Equal(expected, serial);
|
||||
}
|
||||
}
|
||||
31
Radzen.Blazor.Tests/Spreadsheet/TrimFunctionTests.cs
Normal file
31
Radzen.Blazor.Tests/Spreadsheet/TrimFunctionTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class TrimFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Trim_RemovesLeadingTrailingAndCollapsesInternalSpaces()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=TRIM(\" First Quarter Earnings \")";
|
||||
Assert.Equal("First Quarter Earnings", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trim_Empty_ReturnsEmpty()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=TRIM(\" \")";
|
||||
Assert.Equal(string.Empty, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trim_TextCell_Works()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["B1"].Value = " Hello world ";
|
||||
sheet.Cells["A1"].Formula = "=TRIM(B1)";
|
||||
Assert.Equal("Hello world", sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
}
|
||||
31
Radzen.Blazor.Tests/Spreadsheet/TruncFunctionTests.cs
Normal file
31
Radzen.Blazor.Tests/Spreadsheet/TruncFunctionTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class TruncFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldTruncatePositive()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=TRUNC(8.9)";
|
||||
Assert.Equal(8d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTruncateNegative()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=TRUNC(0-8.9)";
|
||||
Assert.Equal(-8d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldTruncateLessThanOne()
|
||||
{
|
||||
sheet.Cells["A1"].Formula = "=TRUNC(0.45)";
|
||||
Assert.Equal(0d, sheet.Cells["A1"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
Radzen.Blazor.Tests/Spreadsheet/UpperFunctionTests.cs
Normal file
24
Radzen.Blazor.Tests/Spreadsheet/UpperFunctionTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class UpperFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Upper_ConvertsToUppercase()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A2"].Value = "total";
|
||||
sheet.Cells["B1"].Formula = "=UPPER(A2)";
|
||||
Assert.Equal("TOTAL", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Upper_AlreadyUppercase()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A3"].Value = "Yield";
|
||||
sheet.Cells["B1"].Formula = "=UPPER(A3)";
|
||||
Assert.Equal("YIELD", sheet.Cells["B1"].Data.Value);
|
||||
}
|
||||
}
|
||||
22
Radzen.Blazor.Tests/Spreadsheet/ValueFunctionTests.cs
Normal file
22
Radzen.Blazor.Tests/Spreadsheet/ValueFunctionTests.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class ValueFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Value_ParsesCurrency()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=VALUE(\"$1,000\")";
|
||||
Assert.Equal(1000d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Value_TimeDifferenceFractionOfDay()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=VALUE(\"16:48:00\")-VALUE(\"12:00:00\")";
|
||||
Assert.Equal(0.2d, sheet.Cells["A1"].Data.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class VerticalLookupFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(10, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindExactMatchInTwoColumnRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "T-Shirt";
|
||||
sheet.Cells["A2"].Value = "Jeans";
|
||||
sheet.Cells["B1"].Value = 19.99;
|
||||
sheet.Cells["B2"].Value = 29.99;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=VLOOKUP(\"T-Shirt\",A1:B2,2,0)";
|
||||
|
||||
Assert.Equal(19.99, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNAWhenNoExactMatch()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "Hat";
|
||||
sheet.Cells["B1"].Value = 9.99;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=VLOOKUP(\"Gloves\",A1:B1,2,0)";
|
||||
|
||||
Assert.Equal(CellError.NA, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindApproximateMatchInSortedFirstColumn()
|
||||
{
|
||||
// First column sorted ascending
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
|
||||
sheet.Cells["B1"].Value = "Low";
|
||||
sheet.Cells["B2"].Value = "Medium";
|
||||
sheet.Cells["B3"].Value = "High";
|
||||
|
||||
// search_key 25 -> should pick row with 20
|
||||
sheet.Cells["C1"].Formula = "=VLOOKUP(25,A1:B3,2,1)";
|
||||
|
||||
Assert.Equal("Medium", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldErrorWhenIndexOutOfRange()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "X";
|
||||
sheet.Cells["B1"].Value = 1;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=VLOOKUP(\"X\",A1:B1,3,0)";
|
||||
|
||||
Assert.Equal(CellError.Ref, sheet.Cells["C1"].Value);
|
||||
}
|
||||
}
|
||||
42
Radzen.Blazor.Tests/Spreadsheet/WeekdayFunctionTests.cs
Normal file
42
Radzen.Blazor.Tests/Spreadsheet/WeekdayFunctionTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class WeekdayFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Weekday_Default_SundayToSaturday()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2008, 2, 14)); // Thursday
|
||||
sheet.Cells["B1"].Formula = "=WEEKDAY(A1)"; // default 1: Sun=1..Sat=7
|
||||
Assert.Equal(5, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekday_Type2_MondayToSunday()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2008, 2, 14)); // Thursday
|
||||
sheet.Cells["B1"].Formula = "=WEEKDAY(A1, 2)"; // Mon=1..Sun=7
|
||||
Assert.Equal(4, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekday_Type3_MondayZero_SundaySix()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2008, 2, 14)); // Thursday
|
||||
sheet.Cells["B1"].Formula = "=WEEKDAY(A1, 3)"; // Mon=0..Sun=6
|
||||
Assert.Equal(3, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekday_InvalidReturnType_ReturnsNumError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2008, 2, 14));
|
||||
sheet.Cells["B1"].Formula = "=WEEKDAY(A1, 10)"; // invalid
|
||||
Assert.Equal(CellError.Num, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
42
Radzen.Blazor.Tests/Spreadsheet/WeeknumFunctionTests.cs
Normal file
42
Radzen.Blazor.Tests/Spreadsheet/WeeknumFunctionTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class WeeknumFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Weeknum_Default_SundayStart()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2012, 3, 9)); // Excel example
|
||||
sheet.Cells["B1"].Formula = "=WEEKNUM(A1)"; // default 1
|
||||
Assert.Equal(10, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weeknum_Type2_MondayStart()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2012, 3, 9));
|
||||
sheet.Cells["B1"].Formula = "=WEEKNUM(A1, 2)"; // Monday start, System 1
|
||||
Assert.Equal(11, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weeknum_Type21_ISO()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2012, 3, 9));
|
||||
sheet.Cells["B1"].Formula = "=WEEKNUM(A1, 21)"; // ISO
|
||||
Assert.Equal(10, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weeknum_InvalidReturnType_ReturnsNumError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2012, 3, 9));
|
||||
sheet.Cells["B1"].Formula = "=WEEKNUM(A1, 4)"; // invalid code
|
||||
Assert.Equal(CellError.Num, sheet.Cells["B1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
114
Radzen.Blazor.Tests/Spreadsheet/XLookupFunctionTests.cs
Normal file
114
Radzen.Blazor.Tests/Spreadsheet/XLookupFunctionTests.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class XLookupFunctionTests
|
||||
{
|
||||
readonly Sheet sheet = new(20, 10);
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindExactMatchAndReturnFromAnotherColumn()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "P1";
|
||||
sheet.Cells["A2"].Value = "P2";
|
||||
sheet.Cells["B1"].Value = 10;
|
||||
sheet.Cells["B2"].Value = 20;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(\"P2\",A1:A2,B1:B2)";
|
||||
|
||||
Assert.Equal(20d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnIfNotFoundValue()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "P1";
|
||||
sheet.Cells["B1"].Value = 10;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(\"P2\",A1:A1,B1:B1,\"Missing\")";
|
||||
|
||||
Assert.Equal("Missing", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSupportWildcardMatch()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "Item-100";
|
||||
sheet.Cells["A2"].Value = "Item-200";
|
||||
sheet.Cells["B1"].Value = "A";
|
||||
sheet.Cells["B2"].Value = "B";
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(\"Item-2*\",A1:A2,B1:B2,\"\",2)";
|
||||
|
||||
Assert.Equal("B", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindNextSmallerWhenNotFound()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["B1"].Value = "L";
|
||||
sheet.Cells["B2"].Value = "M";
|
||||
sheet.Cells["B3"].Value = "H";
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(25,A1:A3,B1:B3,\"\",0-1)";
|
||||
|
||||
Assert.Equal("M", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldFindNextLargerWhenNotFound()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["B1"].Value = "L";
|
||||
sheet.Cells["B2"].Value = "M";
|
||||
sheet.Cells["B3"].Value = "H";
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(25,A1:A3,B1:B3,\"\",1)";
|
||||
|
||||
Assert.Equal("H", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSupportReverseSearch()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "A";
|
||||
sheet.Cells["A2"].Value = "A";
|
||||
sheet.Cells["B1"].Value = 1;
|
||||
sheet.Cells["B2"].Value = 2;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(\"A\",A1:A2,B1:B2,\"\",0,0-1)";
|
||||
|
||||
Assert.Equal(2d, sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldSupportBinarySearchAscending()
|
||||
{
|
||||
sheet.Cells["A1"].Value = 10;
|
||||
sheet.Cells["A2"].Value = 20;
|
||||
sheet.Cells["A3"].Value = 30;
|
||||
sheet.Cells["B1"].Value = "L";
|
||||
sheet.Cells["B2"].Value = "M";
|
||||
sheet.Cells["B3"].Value = "H";
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(20,A1:A3,B1:B3,\"\",0,2)";
|
||||
|
||||
Assert.Equal("M", sheet.Cells["C1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldReturnNAWhenNotFoundAndNoIfNotFound()
|
||||
{
|
||||
sheet.Cells["A1"].Value = "A";
|
||||
sheet.Cells["B1"].Value = 1;
|
||||
|
||||
sheet.Cells["C1"].Formula = "=XLOOKUP(\"B\",A1:A1,B1:B1)";
|
||||
|
||||
Assert.Equal(CellError.NA, sheet.Cells["C1"].Value);
|
||||
}
|
||||
}
|
||||
31
Radzen.Blazor.Tests/Spreadsheet/YearFunctionTests.cs
Normal file
31
Radzen.Blazor.Tests/Spreadsheet/YearFunctionTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class YearFunctionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Year_FromDateSerial_ReturnsYear()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=YEAR(VALUE(\"2025-05-23\"))";
|
||||
Assert.Equal(2025, sheet.Cells["A1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Year_FromDateValue_ReturnsYear()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Data = CellData.FromDate(new System.DateTime(2023, 7, 5));
|
||||
sheet.Cells["B1"].Formula = "=YEAR(A1)";
|
||||
Assert.Equal(2023, sheet.Cells["B1"].Data.GetValueOrDefault<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Year_InvalidText_ReturnsValueError()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Cells["A1"].Formula = "=YEAR(\"abc\")";
|
||||
Assert.Equal(CellError.Value, sheet.Cells["A1"].Data.GetValueOrDefault<CellError>());
|
||||
}
|
||||
}
|
||||
110
Radzen.Blazor/RadzenSpreadsheet.razor
Normal file
110
Radzen.Blazor/RadzenSpreadsheet.razor
Normal file
@@ -0,0 +1,110 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Radzen.Blazor.Spreadsheet
|
||||
@using Radzen.Blazor.Spreadsheet.Tools
|
||||
@using Radzen.Blazor.Rendering
|
||||
@inject DialogService DialogService
|
||||
@inherits RadzenComponent
|
||||
|
||||
<div @ref=@Element class=@GetCssClass() style=@Style @attributes=@Attributes tabindex="0">
|
||||
<CascadingValue TValue="ISpreadsheet" Value=this IsFixed="true">
|
||||
<RadzenTabs SelectedIndex="1">
|
||||
<Tabs>
|
||||
<RadzenTabsItem Text="File">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<Open WorkbookChanged=@OnWorkbookChangedAsync />
|
||||
<Save Workbook=@workbook FileName=@ExportFileName />
|
||||
</RadzenStack>
|
||||
</RadzenTabsItem>
|
||||
<RadzenTabsItem Text="Home">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<Undo Commands=@Sheet?.Commands />
|
||||
<Redo Commands=@Sheet?.Commands />
|
||||
</RadzenStack>
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<Bold Sheet=@Sheet />
|
||||
<Italic Sheet=@Sheet />
|
||||
<Underline Sheet=@Sheet />
|
||||
<Color Sheet=@Sheet />
|
||||
<BackgroundColor Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<TextAlign Sheet=@Sheet />
|
||||
<VerticalAlign Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</RadzenTabsItem>
|
||||
<RadzenTabsItem Text="Insert">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<DeleteRow Sheet=@Sheet />
|
||||
<InsertRowAfter Sheet=@Sheet />
|
||||
<InsertRowBefore Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<DeleteColumn Sheet=@Sheet />
|
||||
<InsertColumnAfter Sheet=@Sheet />
|
||||
<InsertColumnBefore Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</RadzenTabsItem>
|
||||
<RadzenTabsItem Text="View">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<Freeze Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
</RadzenTabsItem>
|
||||
<RadzenTabsItem Text="Data">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<AutoFilter Sheet=@Sheet />
|
||||
</RadzenStack>
|
||||
</RadzenTabsItem>
|
||||
</Tabs>
|
||||
</RadzenTabs>
|
||||
@if (Sheet != null)
|
||||
{
|
||||
<FormulaEditor Sheet=@Sheet />
|
||||
<VirtualGrid @ref=@grid style="flex: 1" Rows=@Sheet.Rows Columns=@Sheet.Columns MergedCells=@Sheet.MergedCells>
|
||||
<Template>
|
||||
@switch (context)
|
||||
{
|
||||
case VirtualCorner corner:
|
||||
<CornerHeaderCell Rect=@context.Rect />
|
||||
break;
|
||||
case VirtualColumnHeader column:
|
||||
<ColumnHeader Sheet=@Sheet FrozenState=@column.FrozenState Column=@column.Column Rect=@context.Rect />
|
||||
break;
|
||||
case VirtualRowHeader row:
|
||||
<RowHeader Sheet=@Sheet FrozenState=@row.FrozenState Row=@row.Row Rect=@context.Rect />
|
||||
break;
|
||||
case VirtualDataItem item:
|
||||
<CellView Sheet=@Sheet Column=@item.Column Row=@item.Row FrozenState=@item.FrozenState Rect=@context.Rect Toggle=@OnCellToggleAsync />
|
||||
break;
|
||||
case VirtualHorizontalSplitter horizontalSplitter:
|
||||
<Splitter class="rz-spreadsheet-horizontal-splitter" Rect=@horizontalSplitter.Rect />
|
||||
break;
|
||||
case VirtualVerticalSplitter verticalSplitter:
|
||||
<Splitter class="rz-spreadsheet-vertical-splitter" Rect=@verticalSplitter.Rect />
|
||||
break;
|
||||
}
|
||||
</Template>
|
||||
<ChildContent>
|
||||
<SelectionOverlay Sheet=@Sheet Context=@context />
|
||||
<CellEditor Sheet=@Sheet Context=@context />
|
||||
@foreach (var table in Sheet.Tables)
|
||||
{
|
||||
<TableFrame Sheet=@Sheet Table=@table Context=@context />
|
||||
}
|
||||
</ChildContent>
|
||||
</VirtualGrid>
|
||||
<Popup @ref=@cellMenuPopup class="rz-spreadsheet-cell-popup rz-context-menu" Lazy="true">
|
||||
@if (cellMenuRow >= 0 && cellMenuColumn >= 0)
|
||||
{
|
||||
<CellMenu Sheet=@Sheet Row=@cellMenuRow Column=@cellMenuColumn Cancel=@OnCellMenuCancelAsync Apply=@OnCellMenuApplyAsync
|
||||
SortAscending=@OnCellMenuSortAscendingAsync SortDescending=@OnCellMenuSortDescendingAsync Clear=@OnCellMenuClearAsync
|
||||
CustomFilter=@OnCellMenuCustomFilterAsync />
|
||||
}
|
||||
</Popup>
|
||||
}
|
||||
</CascadingValue>
|
||||
</div>
|
||||
901
Radzen.Blazor/RadzenSpreadsheet.razor.cs
Normal file
901
Radzen.Blazor/RadzenSpreadsheet.razor.cs
Normal file
@@ -0,0 +1,901 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using Radzen.Blazor.Spreadsheet;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using Radzen;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen.Blazor;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Exposes some of the methods of the RadzenSpreadsheet component.
|
||||
/// </summary>
|
||||
public interface ISpreadsheet
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts the current edit in the spreadsheet.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<bool> AcceptAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A spreadsheet component that allows users to view and edit workbooks.
|
||||
/// It supports features like cell selection, editing, and keyboard shortcuts.
|
||||
/// The component can display a workbook with multiple sheets and allows users to navigate through cells using keyboard shortcuts.
|
||||
/// It also supports mouse interactions for selecting and editing cells, rows, and columns.
|
||||
/// </summary>
|
||||
public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpreadsheet
|
||||
{
|
||||
private Workbook? workbook;
|
||||
|
||||
/// <summary>
|
||||
/// The workbook to display in the spreadsheet.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Workbook? Workbook { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the file to export the workbook to when using the export functionality.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ExportFileName { get; set; } = "sheet.xlsx";
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when the workbook changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<Workbook?> WorkbookChanged { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string GetComponentCssClass() => "rz-spreadsheet";
|
||||
|
||||
private VirtualGrid? grid;
|
||||
private Popup? cellMenuPopup;
|
||||
private int cellMenuRow = -1;
|
||||
private int cellMenuColumn = -1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
var didWorkbookChange = parameters.DidParameterChange(nameof(Workbook), Workbook);
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
if (didWorkbookChange)
|
||||
{
|
||||
workbook = Workbook;
|
||||
}
|
||||
}
|
||||
|
||||
private const int sheetIndex = 0;
|
||||
|
||||
private Sheet? Sheet => workbook?.Sheets[sheetIndex];
|
||||
|
||||
private async Task OnWorkbookChangedAsync(Workbook? value)
|
||||
{
|
||||
workbook = value;
|
||||
|
||||
await WorkbookChanged.InvokeAsync(value);
|
||||
}
|
||||
|
||||
private async Task OnCellToggleAsync(CellMenuToggleEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
cellMenuRow = args.Row;
|
||||
cellMenuColumn = args.Column;
|
||||
await cellMenuPopup.ToggleAsync(args.Element);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuCancelAsync()
|
||||
{
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuApplyAsync(SheetFilter? filter)
|
||||
{
|
||||
if (filter != null && Sheet != null)
|
||||
{
|
||||
var command = new FilterCommand(Sheet, filter);
|
||||
Sheet.Commands.Execute(command);
|
||||
}
|
||||
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuSortAscendingAsync()
|
||||
{
|
||||
if (Sheet != null)
|
||||
{
|
||||
// Check if we're in a data table
|
||||
foreach (var table in Sheet.Tables)
|
||||
{
|
||||
if (table.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Sheet, table.Range, SortOrder.Ascending, cellMenuColumn);
|
||||
Sheet.Commands.Execute(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're in an auto filter
|
||||
if (Sheet.AutoFilter != null && Sheet.AutoFilter.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Sheet, Sheet.AutoFilter.Range, SortOrder.Ascending, cellMenuColumn, skipHeaderRow: true);
|
||||
Sheet.Commands.Execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuSortDescendingAsync()
|
||||
{
|
||||
if (Sheet != null)
|
||||
{
|
||||
// Check if we're in a data table
|
||||
foreach (var table in Sheet.Tables)
|
||||
{
|
||||
if (table.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Sheet, table.Range, SortOrder.Descending, cellMenuColumn);
|
||||
Sheet.Commands.Execute(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're in an auto filter
|
||||
if (Sheet.AutoFilter != null && Sheet.AutoFilter.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Sheet, Sheet.AutoFilter.Range, SortOrder.Descending, cellMenuColumn, skipHeaderRow: true);
|
||||
Sheet.Commands.Execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuClearAsync()
|
||||
{
|
||||
if (Sheet != null)
|
||||
{
|
||||
// Remove all filters that affect the current column
|
||||
var filtersToRemove = new List<SheetFilter>();
|
||||
|
||||
foreach (var filter in Sheet.Filters)
|
||||
{
|
||||
if (filter.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
filtersToRemove.Add(filter);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute remove commands for each filter
|
||||
foreach (var filter in filtersToRemove)
|
||||
{
|
||||
var command = new RemoveFilterCommand(Sheet, filter);
|
||||
Sheet.Commands.Execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCellMenuCustomFilterAsync()
|
||||
{
|
||||
if (cellMenuPopup != null)
|
||||
{
|
||||
await cellMenuPopup.CloseAsync();
|
||||
}
|
||||
|
||||
if (Sheet != null)
|
||||
{
|
||||
FilterCriterion? existingFilter = Sheet.Filters.FirstOrDefault(f => f.Range.Contains(cellMenuRow, cellMenuColumn))?.Criterion;
|
||||
|
||||
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(FilterDialog.Sheet), Sheet },
|
||||
{ nameof(FilterDialog.Column), cellMenuColumn },
|
||||
{ nameof(FilterDialog.Row), cellMenuRow }
|
||||
};
|
||||
|
||||
if (existingFilter != null)
|
||||
{
|
||||
parameters.Add(nameof(FilterDialog.Filter), existingFilter);
|
||||
}
|
||||
|
||||
var result = await DialogService.OpenAsync<FilterDialog>("Custom Filter", parameters, new DialogOptions
|
||||
{
|
||||
Width = "600px",
|
||||
});
|
||||
|
||||
if (result is SheetFilter filter)
|
||||
{
|
||||
var command = new FilterCommand(Sheet, filter);
|
||||
Sheet.Commands.Execute(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveSelectionAsync(int rowOffset, int columnOffset)
|
||||
{
|
||||
if (Sheet is not null)
|
||||
{
|
||||
var address = Sheet.Selection.Move(rowOffset, columnOffset);
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
|
||||
private bool isAccepting;
|
||||
|
||||
/// <summary>
|
||||
/// Accepts the current edit in the spreadsheet.
|
||||
/// </summary>
|
||||
public async Task<bool> AcceptAsync()
|
||||
{
|
||||
var result = true;
|
||||
|
||||
if (isAccepting)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
isAccepting = true;
|
||||
|
||||
if (Sheet is not null)
|
||||
{
|
||||
if (Sheet.Editor.HasChanges)
|
||||
{
|
||||
var command = new AcceptEditCommand(Sheet);
|
||||
|
||||
var valid = Sheet.Commands.Execute(command);
|
||||
|
||||
if (!valid && Sheet.Editor.Cell is not null)
|
||||
{
|
||||
var error = string.Join(Environment.NewLine, Sheet.Editor.Cell.ValidationErrors);
|
||||
|
||||
await DialogService.Alert(error, "Invalid Value");
|
||||
|
||||
command.Unexecute();
|
||||
|
||||
Sheet.Editor.Cancel();
|
||||
|
||||
result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Element.FocusAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Sheet.Editor.EndEdit();
|
||||
}
|
||||
}
|
||||
|
||||
isAccepting = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CycleSelectionAsync(int rowOffset, int columnOffset)
|
||||
{
|
||||
if (await AcceptAsync())
|
||||
{
|
||||
if (Sheet is not null)
|
||||
{
|
||||
var address = Sheet.Selection.Cycle(rowOffset, columnOffset);
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtendSelectionAsync(int rowOffset, int columnOffset)
|
||||
{
|
||||
if (Sheet is not null)
|
||||
{
|
||||
var address = Sheet.Selection.Extend(rowOffset, columnOffset);
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
|
||||
private Task CancelEditAsync()
|
||||
{
|
||||
if (Sheet?.Editor.Mode != EditMode.None)
|
||||
{
|
||||
Sheet?.Editor.Cancel();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Func<KeyboardEventArgs, Task>> shortcuts = [];
|
||||
private readonly SpreadsheetClipboard clipboard = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
workbook = Workbook;
|
||||
shortcuts.Add("Enter", _ => CycleSelectionAsync(1, 0));
|
||||
shortcuts.Add("Escape", _ => CancelEditAsync());
|
||||
shortcuts.Add("Tab", _ => CycleSelectionAsync(0, 1));
|
||||
shortcuts.Add("ArrowUp", _ => MoveSelectionAsync(-1, 0));
|
||||
shortcuts.Add("ArrowDown", _ => MoveSelectionAsync(1, 0));
|
||||
shortcuts.Add("ArrowLeft", _ => MoveSelectionAsync(0, -1));
|
||||
shortcuts.Add("ArrowRight", _ => MoveSelectionAsync(0, 1));
|
||||
shortcuts.Add("Shift+Tab", _ => CycleSelectionAsync(0, -1));
|
||||
shortcuts.Add("Shift+Enter", _ => CycleSelectionAsync(-1, 0));
|
||||
shortcuts.Add("Shift+ArrowUp", _ => ExtendSelectionAsync(-1, 0));
|
||||
shortcuts.Add("Shift+ArrowDown", _ => ExtendSelectionAsync(1, 0));
|
||||
shortcuts.Add("Shift+ArrowLeft", _ => ExtendSelectionAsync(0, -1));
|
||||
shortcuts.Add("Shift+ArrowRight", _ => ExtendSelectionAsync(0, 1));
|
||||
shortcuts.Add("Ctrl+C", _ => CopySelectionAsync());
|
||||
shortcuts.Add("Ctrl+Z", _ => UndoAsync());
|
||||
shortcuts.Add("Ctrl+X", _ => CutSelectionAsync());
|
||||
shortcuts.Add("Ctrl+Shift+Z", _ => RedoAsync());
|
||||
}
|
||||
|
||||
private Task UndoAsync()
|
||||
{
|
||||
Sheet?.Commands.Undo();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RedoAsync()
|
||||
{
|
||||
Sheet?.Commands.Redo();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CopySelectionAsync()
|
||||
{
|
||||
if (Sheet is not null && jsRef is not null)
|
||||
{
|
||||
var text = Sheet.GetDelimitedString(Sheet.Selection.Range);
|
||||
|
||||
await jsRef.InvokeVoidAsync("copyToClipboard", text);
|
||||
clipboard.Copy(Sheet);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CutSelectionAsync()
|
||||
{
|
||||
if (Sheet is not null && jsRef is not null)
|
||||
{
|
||||
var text = Sheet.GetDelimitedString(Sheet.Selection.Range);
|
||||
await jsRef.InvokeVoidAsync("copyToClipboard", text);
|
||||
clipboard.Cut(Sheet);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop to copy the current selection to the clipboard.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JSInvokable]
|
||||
public Task OnCopyAsync() => CopySelectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop to paste text from the clipboard into the current selection.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public Task OnPasteAsync(string text)
|
||||
{
|
||||
if (Sheet is not null)
|
||||
{
|
||||
clipboard.Paste(Sheet, Sheet.Selection.Cell, text);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private IJSObjectReference? jsRef;
|
||||
private DotNetObjectReference<RadzenSpreadsheet>? dotNetRef;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && JSRuntime != null)
|
||||
{
|
||||
dotNetRef = DotNetObjectReference.Create(this);
|
||||
jsRef = await JSRuntime.InvokeAsync<IJSObjectReference>("Radzen.createSpreadsheet", new { Element, dotNetRef, shortcuts = shortcuts.Keys });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when a cell is clicked with the pointer.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task<bool> OnCellPointerDownAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
var address = new CellRef(args.Row, args.Column);
|
||||
|
||||
if (args.Pointer.ShiftKey)
|
||||
{
|
||||
Sheet?.Selection.Merge(address);
|
||||
}
|
||||
else
|
||||
{
|
||||
Sheet?.Selection.Select(address);
|
||||
|
||||
}
|
||||
|
||||
if (grid is not null)
|
||||
{
|
||||
var capture = new PointerCapture
|
||||
{
|
||||
ScrollTop = grid.ScrollTop,
|
||||
ScrollLeft = grid.ScrollLeft,
|
||||
Row = args.Row,
|
||||
Column = args.Column,
|
||||
Pointer = args.Pointer
|
||||
};
|
||||
|
||||
onCellPointerMoveAsync = pointer => OnCellPointerMoveAsync(capture, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Func<PointerEventArgs, Task>? onCellPointerMoveAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the pointer moves over a cell.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnCellPointerMoveAsync(PointerEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (onCellPointerMoveAsync is not null)
|
||||
{
|
||||
await onCellPointerMoveAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
class PointerCapture
|
||||
{
|
||||
public double ScrollTop { get; set; }
|
||||
public double ScrollLeft { get; set; }
|
||||
public int Row { get; set; }
|
||||
public int Column { get; set; }
|
||||
public PointerEventArgs Pointer { get; set; } = default!;
|
||||
}
|
||||
|
||||
private async Task OnCellPointerMoveAsync(PointerCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
var address = GetDeltaCell(capture, pointer);
|
||||
|
||||
if (address != CellRef.Invalid)
|
||||
{
|
||||
Sheet?.Selection.Merge(address);
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
|
||||
private CellRef GetDeltaCell(PointerCapture capture, PointerEventArgs args)
|
||||
{
|
||||
if (grid is not null)
|
||||
{
|
||||
var deltaX = args.ClientX - capture.Pointer.ClientX + capture.Pointer.OffsetX;
|
||||
|
||||
deltaX += grid.ScrollLeft - capture.ScrollLeft;
|
||||
|
||||
var deltaY = args.ClientY - capture.Pointer.ClientY + capture.Pointer.OffsetY;
|
||||
|
||||
deltaY += grid.ScrollTop - capture.ScrollTop;
|
||||
|
||||
var columnPixelRange = grid.Columns.GetPixelRange(capture.Column, capture.Column);
|
||||
|
||||
var columnIndex = grid.Columns.GetIndexRange(columnPixelRange.Start + deltaX, columnPixelRange.Start + deltaX, true);
|
||||
|
||||
var rowPixelRange = grid.Rows.GetPixelRange(capture.Row, capture.Row);
|
||||
|
||||
var rowIndex = grid.Rows.GetIndexRange(rowPixelRange.Start + deltaY, rowPixelRange.Start + deltaY, true);
|
||||
|
||||
return new CellRef(rowIndex.Start, columnIndex.Start);
|
||||
}
|
||||
|
||||
return CellRef.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when a row header is clicked with the pointer.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task<bool> OnRowPointerDownAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
if (args.Pointer.ShiftKey)
|
||||
{
|
||||
Sheet?.Selection.Merge(new CellRef(args.Row, Sheet.ColumnCount - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
Sheet?.Selection.Select(new RowRef(args.Row));
|
||||
}
|
||||
|
||||
if (grid is not null)
|
||||
{
|
||||
var capture = new PointerCapture
|
||||
{
|
||||
ScrollTop = grid.ScrollTop,
|
||||
ScrollLeft = grid.ScrollLeft,
|
||||
Row = args.Row,
|
||||
Pointer = args.Pointer
|
||||
};
|
||||
|
||||
onRowPointerMoveAsync = pointer => OnRowPointerMoveAsync(capture, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Func<PointerEventArgs, Task>? onRowPointerMoveAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the pointer moves over a row header.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnRowPointerMoveAsync(PointerEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (onRowPointerMoveAsync is not null)
|
||||
{
|
||||
await onRowPointerMoveAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnRowPointerMoveAsync(PointerCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
var address = GetDeltaCell(capture, pointer);
|
||||
|
||||
if (address != CellRef.Invalid)
|
||||
{
|
||||
Sheet?.Selection.Merge(new CellRef(address.Row, Sheet.ColumnCount - 1));
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when a column header is clicked with the pointer.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task<bool> OnColumnPointerDownAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
if (args.Pointer.ShiftKey)
|
||||
{
|
||||
Sheet?.Selection.Merge(new CellRef(Sheet.RowCount - 1, args.Column));
|
||||
}
|
||||
else
|
||||
{
|
||||
Sheet?.Selection.Select(new ColumnRef(args.Column));
|
||||
}
|
||||
|
||||
if (grid is not null)
|
||||
{
|
||||
var capture = new PointerCapture
|
||||
{
|
||||
ScrollTop = grid.ScrollTop,
|
||||
ScrollLeft = grid.ScrollLeft,
|
||||
Column = args.Column,
|
||||
Pointer = args.Pointer
|
||||
};
|
||||
|
||||
onColumnPointerMoveAsync = pointer => OnColumnPointerMoveAsync(capture, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Func<PointerEventArgs, Task>? onColumnPointerMoveAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the pointer moves over a column header.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnColumnPointerMoveAsync(PointerEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (onColumnPointerMoveAsync is not null)
|
||||
{
|
||||
await onColumnPointerMoveAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnColumnPointerMoveAsync(PointerCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
var address = GetDeltaCell(capture, pointer);
|
||||
|
||||
if (address != CellRef.Invalid)
|
||||
{
|
||||
Sheet?.Selection.Merge(new CellRef(Sheet.RowCount - 1, address.Column));
|
||||
|
||||
await ScrollToAsync(address);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when a cell is double-clicked with the pointer.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnCellDoubleClickAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (Sheet != null)
|
||||
{
|
||||
var address = Sheet.MergedCells.GetMergedRangeStartOrSelf(new CellRef(args.Row, args.Column));
|
||||
|
||||
var cell = Sheet.Cells[address];
|
||||
|
||||
if (cell != null)
|
||||
{
|
||||
await ScrollToAsync(address);
|
||||
|
||||
Sheet.Editor.StartEdit(address, cell.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the spreadsheet to the specified cell address.
|
||||
/// </summary>
|
||||
public async Task ScrollToAsync(CellRef address)
|
||||
{
|
||||
if (grid is not null)
|
||||
{
|
||||
await grid.ScrollToAsync(address.Row, address.Column);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TranslateShortcut(KeyboardEventArgs args)
|
||||
{
|
||||
var key = new StringBuilder();
|
||||
|
||||
if (args.CtrlKey || args.MetaKey)
|
||||
{
|
||||
key.Append("Ctrl+");
|
||||
}
|
||||
|
||||
if (args.AltKey)
|
||||
{
|
||||
key.Append("Alt+");
|
||||
}
|
||||
|
||||
if (args.ShiftKey)
|
||||
{
|
||||
key.Append("Shift+");
|
||||
}
|
||||
|
||||
key.Append(args.Code
|
||||
.Replace("Key", "", StringComparison.Ordinal)
|
||||
.Replace("Digit", "", StringComparison.Ordinal)
|
||||
.Replace("Numpad", "", StringComparison.Ordinal));
|
||||
|
||||
return key.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when a key is pressed down.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnKeyDownAsync(KeyboardEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (shortcuts.TryGetValue(TranslateShortcut(args), out var action))
|
||||
{
|
||||
await action(args);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.CtrlKey || args.MetaKey || args.AltKey || args.Key == "Shift")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ch = args.Key == "Space" ? ' ' : args.Key[0];
|
||||
|
||||
if (char.IsLetterOrDigit(ch) || char.IsPunctuation(ch) || char.IsSymbol(ch) || char.IsSeparator(ch))
|
||||
{
|
||||
Sheet?.Editor.StartEdit(Sheet.Selection.Cell, ch.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private Func<PointerEventArgs, Task>? onColumnResizePointerMoveAsync;
|
||||
private Func<PointerEventArgs, Task>? onRowResizePointerMoveAsync;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the column resize handle is pressed.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task<bool> OnColumnResizePointerDownAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
if (grid is not null)
|
||||
{
|
||||
var capture = new ColumnResizeCapture
|
||||
{
|
||||
ScrollTop = grid.ScrollTop,
|
||||
ScrollLeft = grid.ScrollLeft,
|
||||
Column = args.Column,
|
||||
StartX = args.Pointer.ClientX,
|
||||
StartWidth = Sheet?.Columns[args.Column] ?? 100
|
||||
};
|
||||
|
||||
onColumnResizePointerMoveAsync = pointer => OnColumnResizePointerMoveAsync(capture, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the row resize handle is pressed.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task<bool> OnRowResizePointerDownAsync(CellEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
if (grid is not null)
|
||||
{
|
||||
var capture = new RowResizeCapture
|
||||
{
|
||||
ScrollTop = grid.ScrollTop,
|
||||
ScrollLeft = grid.ScrollLeft,
|
||||
Row = args.Row,
|
||||
StartY = args.Pointer.ClientY,
|
||||
StartHeight = Sheet?.Rows[args.Row] ?? 20
|
||||
};
|
||||
|
||||
onRowResizePointerMoveAsync = pointer => OnRowResizePointerMoveAsync(capture, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the pointer moves while resizing a column.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnColumnResizePointerMoveAsync(PointerEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (onColumnResizePointerMoveAsync is not null)
|
||||
{
|
||||
await onColumnResizePointerMoveAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by JS interop when the pointer moves while resizing a row.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnRowResizePointerMoveAsync(PointerEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
if (onRowResizePointerMoveAsync is not null)
|
||||
{
|
||||
await onRowResizePointerMoveAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnColumnResizePointerMoveAsync(ColumnResizeCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
if (Sheet != null && capture.Column >= 0 && capture.Column < Sheet.Columns.Count)
|
||||
{
|
||||
var delta = pointer.ClientX - capture.StartX;
|
||||
var newWidth = Math.Max(24, capture.StartWidth + delta);
|
||||
Sheet.Columns[capture.Column] = newWidth;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnRowResizePointerMoveAsync(RowResizeCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
if (Sheet != null && capture.Row >= 0 && capture.Row < Sheet.Rows.Count)
|
||||
{
|
||||
var delta = pointer.ClientY - capture.StartY;
|
||||
var newHeight = Math.Max(16, capture.StartHeight + delta);
|
||||
Sheet.Rows[capture.Row] = newHeight;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
class ColumnResizeCapture
|
||||
{
|
||||
public double ScrollTop { get; set; }
|
||||
public double ScrollLeft { get; set; }
|
||||
public int Column { get; set; }
|
||||
public double StartX { get; set; }
|
||||
public double StartWidth { get; set; }
|
||||
}
|
||||
|
||||
class RowResizeCapture
|
||||
{
|
||||
public double ScrollTop { get; set; }
|
||||
public double ScrollLeft { get; set; }
|
||||
public int Row { get; set; }
|
||||
public double StartY { get; set; }
|
||||
public double StartHeight { get; set; }
|
||||
}
|
||||
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
if (jsRef is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await jsRef.InvokeVoidAsync("dispose");
|
||||
await jsRef.DisposeAsync();
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
bool open;
|
||||
ElementReference target;
|
||||
|
||||
public bool IsOpen => open;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the popup content is rendered only when open.
|
||||
/// </summary>
|
||||
|
||||
428
Radzen.Blazor/Spreadsheet/Axis.cs
Normal file
428
Radzen.Blazor/Spreadsheet/Axis.cs
Normal file
@@ -0,0 +1,428 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a range of pixels in a spreadsheet.
|
||||
/// </summary>
|
||||
public readonly struct PixelRange(double start, double end)
|
||||
{
|
||||
/// <summary>
|
||||
/// The start of the pixel range.
|
||||
/// </summary>
|
||||
public double Start { get; } = start;
|
||||
/// <summary>
|
||||
/// The end of the pixel range.
|
||||
/// </summary>
|
||||
public double End { get; } = end;
|
||||
/// <summary>
|
||||
/// The length of the pixel range, calculated as End - Start.
|
||||
/// </summary>
|
||||
public double Length => End - Start;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PixelRange"/> struct.
|
||||
/// </summary>
|
||||
public PixelRange OffsetStart(double offset) => new(Start + offset, End);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PixelRange"/> struct.
|
||||
/// </summary>
|
||||
public PixelRange OffsetEnd(double offset) => new(Start, End + offset);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this pixel range contains another pixel range.
|
||||
/// </summary>
|
||||
public bool Contains(PixelRange other)
|
||||
{
|
||||
return Start <= other.Start && End >= other.End;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a rectangle in pixel coordinates, defined by two pixel ranges: one for the x-axis and one for the y-axis.
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
public readonly struct PixelRectangle(PixelRange x, PixelRange y)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PixelRectangle"/> struct with the specified pixel ranges.
|
||||
/// </summary>
|
||||
public PixelRectangle(double top, double left, double bottom, double right)
|
||||
: this(new PixelRange(left, right), new PixelRange(top, bottom))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the left coordinate of the rectangle.
|
||||
/// </summary>
|
||||
public double Left => x.Start;
|
||||
/// <summary>
|
||||
/// Returns the top coordinate of the rectangle.
|
||||
/// </summary>
|
||||
public double Top => y.Start;
|
||||
/// <summary>
|
||||
/// Returns the right coordinate of the rectangle.
|
||||
/// </summary>
|
||||
public double Right => x.End;
|
||||
/// <summary>
|
||||
/// Returns the bottom coordinate of the rectangle.
|
||||
/// </summary>
|
||||
public double Bottom => y.End;
|
||||
/// <summary>
|
||||
/// Returns the width of the rectangle.
|
||||
/// </summary>
|
||||
public double Width => x.Length;
|
||||
/// <summary>
|
||||
/// Returns the height of the rectangle.
|
||||
/// </summary>
|
||||
public double Height => y.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the intersection of this rectangle with another rectangle.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
public PixelRectangle Intersection(PixelRectangle other)
|
||||
{
|
||||
var left = Math.Max(Left, other.Left);
|
||||
var top = Math.Max(Top, other.Top);
|
||||
var right = Math.Min(Right, other.Right);
|
||||
var bottom = Math.Min(Bottom, other.Bottom);
|
||||
|
||||
if (left < right && top < bottom)
|
||||
{
|
||||
return new PixelRectangle(new PixelRange(left, right), new PixelRange(top, bottom));
|
||||
}
|
||||
|
||||
return new PixelRectangle(new PixelRange(0, 0), new PixelRange(0, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the rectangle in CSS style format.
|
||||
/// </summary>
|
||||
public void AppendStyle(StringBuilder sb)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sb);
|
||||
sb.Append("transform: translate(");
|
||||
sb.Append(Left.ToPx());
|
||||
sb.Append(',');
|
||||
sb.Append(Top.ToPx());
|
||||
sb.Append("); width: ");
|
||||
sb.Append(Width.ToPx());
|
||||
sb.Append("; height: ");
|
||||
sb.Append(Height.ToPx());
|
||||
sb.Append(';');
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a range of indices in a spreadsheet, including the start index, end index, and an offset.
|
||||
/// </summary>
|
||||
public readonly struct IndexRange(int start, int end, double offset)
|
||||
{
|
||||
/// <summary>
|
||||
/// The start index of the range, inclusive.
|
||||
/// </summary>
|
||||
public int Start { get; } = start;
|
||||
/// <summary>
|
||||
/// The end index of the range, inclusive.
|
||||
/// </summary>
|
||||
public int End { get; } = end;
|
||||
/// <summary>
|
||||
/// The offset of the range, which can be used to adjust the starting position of the range.
|
||||
/// </summary>
|
||||
public double Offset { get; } = offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an axis in a spreadsheet, which can be used to manage the layout of rows or columns.
|
||||
/// </summary>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="count"></param>
|
||||
public class Axis(double size, int count)
|
||||
{
|
||||
/// <summary>
|
||||
/// The default size of an item of the axis.
|
||||
/// </summary>
|
||||
public double Size => size;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of items along this axis.
|
||||
/// </summary>
|
||||
private int count = count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of items along this axis.
|
||||
/// Setting this property triggers a change event.
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
get => count;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Count cannot be negative.");
|
||||
}
|
||||
|
||||
count = value;
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when an axis property changes, such as when a row or column is hidden or shown, or when the size of a row or column changes.
|
||||
/// </summary>
|
||||
public event Action? Changed;
|
||||
|
||||
private readonly Dictionary<int, double> data = [];
|
||||
|
||||
private readonly HashSet<int> hidden = [];
|
||||
|
||||
private bool isUpdating;
|
||||
|
||||
/// <summary>
|
||||
/// Suspend updates to the axis. This is useful when making multiple changes at once to prevent unnecessary updates.
|
||||
/// </summary>
|
||||
public void BeginUpdate()
|
||||
{
|
||||
isUpdating = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resume updates to the axis and trigger a change event.
|
||||
/// </summary>
|
||||
public void EndUpdate()
|
||||
{
|
||||
isUpdating = false;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private void TriggerChange()
|
||||
{
|
||||
if (!isUpdating)
|
||||
{
|
||||
Changed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified index is hidden.
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsHidden(int index)
|
||||
{
|
||||
return hidden.Contains(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the specified index.
|
||||
///</summary>
|
||||
|
||||
public void Hide(int index)
|
||||
{
|
||||
if (!IsHidden(index))
|
||||
{
|
||||
hidden.Add(index);
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the specified index if it is hidden.
|
||||
/// </summary>
|
||||
public void Show(int index)
|
||||
{
|
||||
if (IsHidden(index))
|
||||
{
|
||||
hidden.Remove(index);
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows all hidden indices in the axis.
|
||||
/// </summary>
|
||||
public void ShowAll()
|
||||
{
|
||||
if (hidden.Count > 0)
|
||||
{
|
||||
hidden.Clear();
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the axis at the specified index.
|
||||
/// </summary>
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (data.TryGetValue(index, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
set
|
||||
{
|
||||
data[index] = value;
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
private int frozen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of frozen items in the axis.
|
||||
/// </summary>
|
||||
public int Frozen
|
||||
{
|
||||
get => frozen;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Frozen cannot be negative.");
|
||||
}
|
||||
|
||||
frozen = value;
|
||||
TriggerChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the offset of the axis. This is used to adjust the starting position of the axis, for example, to render headers.
|
||||
/// </summary>
|
||||
public double Offset { get; set; }
|
||||
|
||||
internal IndexRange GetIndexRange(double start, double end, bool includeFrozen = false)
|
||||
{
|
||||
var currentPosition = Offset;
|
||||
var startOffset = 0d;
|
||||
|
||||
if (!includeFrozen)
|
||||
{
|
||||
if (Frozen > 0)
|
||||
{
|
||||
// Calculate position after frozen items
|
||||
for (int index = 0; index < Frozen; index++)
|
||||
{
|
||||
if (!IsHidden(index))
|
||||
{
|
||||
currentPosition += this[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find start index - include items that start before the viewport end
|
||||
int startIndex = includeFrozen ? 0 : Frozen;
|
||||
|
||||
for (; startIndex < Count - 1; startIndex++)
|
||||
{
|
||||
if (IsHidden(startIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentSize = this[startIndex];
|
||||
|
||||
if (currentPosition + segmentSize > start)
|
||||
{
|
||||
startOffset = start - currentPosition;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPosition += segmentSize;
|
||||
}
|
||||
|
||||
// Find end index - include items that end after the viewport start
|
||||
int endIndex;
|
||||
for (endIndex = startIndex; endIndex < Count - 1; endIndex++)
|
||||
{
|
||||
if (IsHidden(endIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentSize = this[endIndex];
|
||||
currentPosition += segmentSize;
|
||||
|
||||
if (currentPosition >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new IndexRange(startIndex, endIndex, startOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total size of the axis, including all visible items, default values for hidden items, and the offset.
|
||||
/// </summary>
|
||||
public double Total
|
||||
{
|
||||
get
|
||||
{
|
||||
var total = 0d;
|
||||
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (!IsHidden(item.Key))
|
||||
{
|
||||
total += item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return total + size * (Count - data.Count - hidden.Count) + Offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for the specified start and end indices.
|
||||
/// </summary>
|
||||
/// <param name="startIndex"></param>
|
||||
/// <param name="endIndex"></param>
|
||||
public PixelRange GetPixelRange(int startIndex, int endIndex)
|
||||
{
|
||||
double start;
|
||||
double end;
|
||||
var currentPosition = Offset;
|
||||
|
||||
for (var index = 0; index < startIndex; index++)
|
||||
{
|
||||
if (!IsHidden(index))
|
||||
{
|
||||
var segmentSize = this[index];
|
||||
currentPosition += segmentSize;
|
||||
}
|
||||
}
|
||||
start = currentPosition;
|
||||
|
||||
for (var index = startIndex; index <= endIndex; index++)
|
||||
{
|
||||
if (!IsHidden(index))
|
||||
{
|
||||
var segmentSize = this[index];
|
||||
currentPosition += segmentSize;
|
||||
}
|
||||
}
|
||||
|
||||
end = currentPosition;
|
||||
|
||||
return new PixelRange(start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for a single index, which is equivalent to calling GetPixelRange with the same start and end index.
|
||||
/// </summary>
|
||||
public PixelRange GetPixelRange(int startIndex) => GetPixelRange(startIndex, startIndex);
|
||||
}
|
||||
226
Radzen.Blazor/Spreadsheet/Cell.cs
Normal file
226
Radzen.Blazor/Spreadsheet/Cell.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a cell in a spreadsheet.
|
||||
/// </summary>
|
||||
public class Cell
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the sheet that contains this cell.
|
||||
/// </summary>
|
||||
public Sheet Sheet { get; private set; }
|
||||
|
||||
private Format? format;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format of the cell.
|
||||
/// </summary>
|
||||
public Format Format
|
||||
{
|
||||
get
|
||||
{
|
||||
if (format == null)
|
||||
{
|
||||
format = new Format();
|
||||
format.Changed += OnFormatChanged;
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (format != null)
|
||||
{
|
||||
format.Changed -= OnFormatChanged;
|
||||
}
|
||||
|
||||
format = value;
|
||||
|
||||
if (format != null)
|
||||
{
|
||||
format.Changed += OnFormatChanged;
|
||||
}
|
||||
|
||||
OnFormatChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the cell, creating a new instance with the same properties.
|
||||
/// </summary>
|
||||
|
||||
public Cell Clone() => new(Sheet, Address)
|
||||
{
|
||||
Data = new CellData(Value),
|
||||
Formula = Formula
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Copies the properties from another cell to this cell.
|
||||
/// </summary>
|
||||
public void CopyFrom(Cell other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
Data = other.Data;
|
||||
Formula = other.Formula;
|
||||
format = other.format;
|
||||
|
||||
Changed?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void ApplyFormat(StringBuilder sb)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sb);
|
||||
var effectiveFormat = format;
|
||||
|
||||
var conditionalFormat = Sheet.ConditionalFormats.Calculate(this);
|
||||
|
||||
if (conditionalFormat != null)
|
||||
{
|
||||
effectiveFormat = format?.Merge(conditionalFormat) ?? conditionalFormat;
|
||||
}
|
||||
|
||||
effectiveFormat?.AppendStyle(sb);
|
||||
}
|
||||
|
||||
private void OnFormatChanged()
|
||||
{
|
||||
Changed?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value and its type as a CellData object.
|
||||
/// </summary>
|
||||
public CellData Data { get; internal set; } = new CellData(null);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the cell.
|
||||
/// </summary>
|
||||
public object? Value
|
||||
{
|
||||
get => Data.Value;
|
||||
set
|
||||
{
|
||||
Data = new CellData(value);
|
||||
|
||||
Sheet.OnCellValueChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the cell as a string, or the formula if it exists.
|
||||
/// </summary>
|
||||
public string? GetValue()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Formula))
|
||||
{
|
||||
return Formula;
|
||||
}
|
||||
|
||||
return Value switch
|
||||
{
|
||||
null => null,
|
||||
CellError error => error.ToString(),
|
||||
string str => str,
|
||||
_ => Value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the cell as a string.
|
||||
/// </summary>
|
||||
public string? GetValueAsString()
|
||||
{
|
||||
return Value switch
|
||||
{
|
||||
null => null,
|
||||
CellError error => error.ToString(),
|
||||
string str => str,
|
||||
_ => Value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the cell based on a string input.
|
||||
/// If the string starts with '=', it is treated as a formula.
|
||||
/// Otherwise, it attempts to parse the string as a number or keeps it as a string.
|
||||
/// </summary>
|
||||
public void SetValue(string? value)
|
||||
{
|
||||
if (value?.StartsWith('=') == true && value != "=")
|
||||
{
|
||||
Formula = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnChanged()
|
||||
{
|
||||
Changed?.Invoke(this);
|
||||
}
|
||||
|
||||
internal event Action<Cell>? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of value contained in the cell.
|
||||
/// </summary>
|
||||
public CellDataType ValueType => Data.Type;
|
||||
|
||||
internal FormulaSyntaxTree? FormulaSyntaxTree { get; private set; }
|
||||
|
||||
private string? formula;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formula of the cell.
|
||||
/// </summary>
|
||||
public string? Formula
|
||||
{
|
||||
get => formula;
|
||||
set
|
||||
{
|
||||
formula = value;
|
||||
FormulaSyntaxTree = value != null ? FormulaParser.Parse(value) : null;
|
||||
Sheet.OnCellFormulaChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the cell.
|
||||
/// </summary>
|
||||
|
||||
public CellRef Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the cell's value against the sheet's validation rules.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
ValidationErrors = Sheet.Validation.Validate(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the cell has validation errors.
|
||||
/// </summary>
|
||||
public bool HasValidationErrors => ValidationErrors.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation errors for the cell.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ValidationErrors { get; private set; } = [];
|
||||
|
||||
internal Cell(Sheet sheet, CellRef address)
|
||||
{
|
||||
Address = address;
|
||||
Sheet = sheet;
|
||||
}
|
||||
}
|
||||
45
Radzen.Blazor/Spreadsheet/CellBase.cs
Normal file
45
Radzen.Blazor/Spreadsheet/CellBase.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for spreadsheet cells view components.
|
||||
/// </summary>
|
||||
public abstract class CellBase : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cell rectangle in pixels.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public PixelRectangle Rect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cell frozen state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public FrozenState FrozenState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the style of the cell.
|
||||
/// </summary>
|
||||
protected virtual string Style => GetStyle();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the style string for the cell.
|
||||
/// </summary>
|
||||
protected virtual string GetStyle()
|
||||
{
|
||||
var sb = StringBuilderCache.Acquire();
|
||||
AppendStyle(sb);
|
||||
return StringBuilderCache.GetStringAndRelease(sb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the style properties to the StringBuilder.
|
||||
/// </summary>
|
||||
protected virtual void AppendStyle(StringBuilder sb)
|
||||
{
|
||||
Rect.AppendStyle(sb);
|
||||
}
|
||||
}
|
||||
630
Radzen.Blazor/Spreadsheet/CellData.cs
Normal file
630
Radzen.Blazor/Spreadsheet/CellData.cs
Normal file
@@ -0,0 +1,630 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Represents the type of value contained in a cell.
|
||||
/// </summary>
|
||||
public enum CellDataType
|
||||
{
|
||||
/// <summary>
|
||||
/// The cell contains a numeric value.
|
||||
/// </summary>
|
||||
Number,
|
||||
/// <summary>
|
||||
/// The cell contains a string value.
|
||||
/// </summary>
|
||||
String,
|
||||
/// <summary>
|
||||
/// The cell contains an error value.
|
||||
/// </summary>
|
||||
Error,
|
||||
/// <summary>
|
||||
/// The cell is empty.
|
||||
/// </summary>
|
||||
Empty,
|
||||
|
||||
/// <summary>
|
||||
/// The cell contains a date value.
|
||||
/// </summary>
|
||||
Date,
|
||||
/// <summary>
|
||||
/// The cell contains a boolean value.
|
||||
/// </summary>
|
||||
Boolean
|
||||
}
|
||||
|
||||
static class TypeExtensions
|
||||
{
|
||||
public static double ToNumber(this DateTime date)
|
||||
{
|
||||
var epoch = new DateTime(1900, 1, 1);
|
||||
var span = date - epoch;
|
||||
return span.TotalDays; // include fractional day for time
|
||||
}
|
||||
|
||||
public static DateTime ToDate(this double number)
|
||||
{
|
||||
var epoch = new DateTime(1900, 1, 1);
|
||||
return epoch.AddDays(number);
|
||||
}
|
||||
|
||||
public static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null;
|
||||
|
||||
public static bool IsNumeric(this Type type)
|
||||
{
|
||||
if (type.IsNullable())
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type)!;
|
||||
}
|
||||
|
||||
return type == typeof(int) || type == typeof(double) || type == typeof(decimal) || type == typeof(short) ||
|
||||
type == typeof(float) || type == typeof(long) || type == typeof(byte) || type == typeof(uint) ||
|
||||
type == typeof(ulong) || type == typeof(ushort) || type == typeof(sbyte);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a value of a spreadsheet cell along with its type.
|
||||
/// </summary>
|
||||
[SuppressMessage("Design", "CA1036:Override methods on comparable types", Justification = "Comparison operators are intentionally omitted; use explicit comparison helpers.")]
|
||||
public class CellData : IComparable, IComparable<CellData>
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the data contained in the cell.
|
||||
/// </summary>
|
||||
public object? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the type of value contained in the cell.
|
||||
/// </summary>
|
||||
public CellDataType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of CellData with the specified data.
|
||||
/// </summary>
|
||||
public CellData(object? data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
Value = null;
|
||||
Type = CellDataType.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var valType = data.GetType();
|
||||
var isNullable = valType.IsNullable();
|
||||
var nullableType = Nullable.GetUnderlyingType(valType);
|
||||
|
||||
if (valType == typeof(string) || (isNullable && nullableType == typeof(string)))
|
||||
{
|
||||
var converted = TryConvertFromString(data.ToString(), out var convertedData, out var valueType);
|
||||
if (converted)
|
||||
{
|
||||
Value = convertedData;
|
||||
Type = valueType!.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = data;
|
||||
Type = CellDataType.String;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Type = GetValueType(data, valType, isNullable, nullableType);
|
||||
Value = (Type == CellDataType.Number) ? Convert.ToDouble(data, CultureInfo.InvariantCulture) : data;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryConvertFromString(string? value, out object? converted, out CellDataType? valueType)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
converted = null;
|
||||
valueType = CellDataType.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (double.TryParse(value, out var valNum))
|
||||
{
|
||||
converted = valNum;
|
||||
valueType = CellDataType.Number;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(value, out var valDate))
|
||||
{
|
||||
valueType = CellDataType.Date;
|
||||
converted = valDate;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bool.TryParse(value, out var valBool))
|
||||
{
|
||||
valueType = CellDataType.Boolean;
|
||||
converted = valBool;
|
||||
return true;
|
||||
}
|
||||
|
||||
converted = null;
|
||||
valueType = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryGetInt(out int value, bool allowBooleans = true, bool nonNumericTextAsZero = false)
|
||||
{
|
||||
value = 0;
|
||||
if (!TryCoerceToNumber(out var number, allowBooleans, nonNumericTextAsZero))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = (int)Math.Truncate(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryCoerceToNumber(out double number, bool allowBooleans, bool nonNumericTextAsZero)
|
||||
{
|
||||
number = 0d;
|
||||
switch (Type)
|
||||
{
|
||||
case CellDataType.Number:
|
||||
number = GetValueOrDefault<double>();
|
||||
return true;
|
||||
case CellDataType.String:
|
||||
if (TryConvertFromString(GetValueOrDefault<string>(), out var converted, out var valueType))
|
||||
{
|
||||
if (valueType == CellDataType.Number)
|
||||
{
|
||||
number = (double)converted!;
|
||||
return true;
|
||||
}
|
||||
if (valueType == CellDataType.Boolean && allowBooleans)
|
||||
{
|
||||
number = (bool)converted! ? 1d : 0d;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (nonNumericTextAsZero)
|
||||
{
|
||||
number = 0d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case CellDataType.Boolean:
|
||||
if (allowBooleans)
|
||||
{
|
||||
number = GetValueOrDefault<bool>() ? 1d : 0d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryCoerceToDate(out DateTime date)
|
||||
{
|
||||
date = default;
|
||||
switch (Type)
|
||||
{
|
||||
case CellDataType.Date:
|
||||
date = GetValueOrDefault<DateTime>();
|
||||
return true;
|
||||
case CellDataType.Number:
|
||||
date = GetValueOrDefault<double>().ToDate();
|
||||
return true;
|
||||
case CellDataType.String:
|
||||
if (TryConvertFromString(GetValueOrDefault<string>(), out var converted, out var valueType))
|
||||
{
|
||||
if (valueType == CellDataType.Date)
|
||||
{
|
||||
date = (DateTime)converted!;
|
||||
return true;
|
||||
}
|
||||
if (valueType == CellDataType.Number)
|
||||
{
|
||||
date = ((double)converted!).ToDate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static CellDataType GetValueType(object? value, Type valType, bool isNullable, Type? nullableType)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return CellDataType.Empty;
|
||||
}
|
||||
|
||||
if (value is CellError)
|
||||
{
|
||||
return CellDataType.Error;
|
||||
}
|
||||
|
||||
if (valType == typeof(string) || (isNullable && nullableType == typeof(string)))
|
||||
{
|
||||
return CellDataType.String;
|
||||
}
|
||||
|
||||
if (valType.IsNumeric() || (isNullable && nullableType?.IsNumeric() == true))
|
||||
{
|
||||
return CellDataType.Number;
|
||||
}
|
||||
|
||||
if (valType == typeof(bool) || (isNullable && nullableType == typeof(bool)))
|
||||
{
|
||||
return CellDataType.Boolean;
|
||||
}
|
||||
|
||||
if (valType == typeof(DateTime) || (isNullable && nullableType == typeof(DateTime)))
|
||||
{
|
||||
return CellDataType.Date;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Unsupported cell value type: {valType}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Cell's Value and attempts to cast it to T. If the Value is null or cannot be converted, returns the default value of T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public T? GetValueOrDefault<T>()
|
||||
{
|
||||
var val = GetValue(typeof(T));
|
||||
|
||||
return val == null ? default : (T)val;
|
||||
}
|
||||
|
||||
private object? GetValue(Type type)
|
||||
{
|
||||
if (Value == null && Type == CellDataType.String)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (Value?.GetType() == type)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
var conversionType = type;
|
||||
|
||||
if (Nullable.GetUnderlyingType(type) != null)
|
||||
{
|
||||
conversionType = Nullable.GetUnderlyingType(type);
|
||||
}
|
||||
|
||||
if (conversionType == typeof(string))
|
||||
{
|
||||
return Value?.ToString();
|
||||
}
|
||||
|
||||
if (Value is IConvertible && conversionType != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.ChangeType(Value, conversionType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(CellData? other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch (Value)
|
||||
{
|
||||
case null when other.Value == null:
|
||||
return 0;
|
||||
case null:
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (other.Value == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (Type == CellDataType.Number && other.Type == CellDataType.Date)
|
||||
{
|
||||
return ((IComparable)Value).CompareTo(((DateTime)other.Value).ToNumber());
|
||||
}
|
||||
|
||||
if (Type == CellDataType.Date && other.Type == CellDataType.Number)
|
||||
{
|
||||
return ((DateTime)Value).ToNumber().CompareTo((IComparable)other.Value);
|
||||
}
|
||||
|
||||
if (Type == other.Type)
|
||||
{
|
||||
return ((IComparable)Value).CompareTo((IComparable)other.Value);
|
||||
}
|
||||
|
||||
return Type.CompareTo(other.Type);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(object? obj) => obj is CellData value ? CompareTo(value) : -1;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the cell is empty (null or empty string).
|
||||
/// </summary>
|
||||
public bool IsEmpty => Type == CellDataType.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the cell contains an error value.
|
||||
/// </summary>
|
||||
public bool IsError => Type == CellDataType.Error;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data is equal to another cell data.
|
||||
/// </summary>
|
||||
public bool IsEqualTo(CellData other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
// special handling of empty cell vs empty string.
|
||||
if (other.IsEmpty && string.IsNullOrEmpty(Value?.ToString()) || IsEmpty && string.IsNullOrEmpty(other.Value?.ToString()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Type != other.Type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Type == CellDataType.Empty || Type == CellDataType.Empty)
|
||||
{
|
||||
return other.Value == null && Value == null;
|
||||
}
|
||||
|
||||
return ((IComparable)Value!).CompareTo(other.Value) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data is less than another cell data.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
public bool IsLessThan(CellData other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
if (Value == null || other.Value == null)
|
||||
return false;
|
||||
|
||||
var compareResult = ((IComparable)Value).CompareTo(other.Value);
|
||||
return compareResult < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data is greater than another cell data.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
public bool IsGreaterThan(CellData other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
if (Value == null || other.Value == null)
|
||||
return false;
|
||||
|
||||
var compareResult = ((IComparable)Value).CompareTo(other.Value);
|
||||
return compareResult > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data is less than or equal to another cell data.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
public bool IsLessThanOrEqualTo(CellData other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
if (Value == null || other.Value == null)
|
||||
return false;
|
||||
|
||||
var compareResult = ((IComparable)Value).CompareTo(other.Value);
|
||||
return compareResult <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data is greater than or equal to another cell data.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
public bool IsGreaterThanOrEqualTo(CellData other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
if (Value == null || other.Value == null)
|
||||
return false;
|
||||
|
||||
var compareResult = ((IComparable)Value).CompareTo(other.Value);
|
||||
return compareResult >= 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value == null ? -1 : Value.GetHashCode();
|
||||
}
|
||||
|
||||
internal CellData(object value, CellDataType type)
|
||||
{
|
||||
Value = value;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CellData instance for a string value.
|
||||
/// </summary>
|
||||
public static CellData FromString(string value) => new(value, CellDataType.String);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CellData instance for a numeric value.
|
||||
/// </summary>
|
||||
public static CellData FromNumber(double value) => new(value, CellDataType.Number);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CellData instance for a boolean value.
|
||||
/// </summary>
|
||||
public static CellData FromBoolean(bool value) => new(value, CellDataType.Boolean);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CellData instance for a date value.
|
||||
/// </summary>
|
||||
public static CellData FromDate(DateTime value) => new(value, CellDataType.Date);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CellData instance for an error value.
|
||||
/// </summary>
|
||||
public static CellData FromError(CellError error) => new(error, CellDataType.Error);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this cell data matches the specified criteria.
|
||||
/// </summary>
|
||||
/// <param name="criteria">The criteria to match against</param>
|
||||
/// <returns>True if this cell matches the criteria, false otherwise</returns>
|
||||
public bool MatchesCriteria(CellData criteria)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(criteria);
|
||||
// Handle error criteria
|
||||
if (criteria.IsError)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle empty criteria - only matches empty cells
|
||||
if (criteria.IsEmpty)
|
||||
{
|
||||
return IsEmpty;
|
||||
}
|
||||
|
||||
// Handle string criteria with wildcards
|
||||
if (criteria.Type == CellDataType.String)
|
||||
{
|
||||
var criteriaString = criteria.GetValueOrDefault<string>() ?? "";
|
||||
var cellString = ToString() ?? "";
|
||||
|
||||
// Check for wildcard patterns
|
||||
if (criteriaString.Contains('*', StringComparison.Ordinal) ||
|
||||
criteriaString.Contains('?', StringComparison.Ordinal))
|
||||
{
|
||||
return Wildcard.IsFullMatch(cellString, criteriaString);
|
||||
}
|
||||
|
||||
// Check for comparison expressions
|
||||
if (IsComparisonExpression(criteriaString))
|
||||
{
|
||||
return EvaluateComparisonExpression(criteriaString);
|
||||
}
|
||||
}
|
||||
|
||||
// Default comparison
|
||||
return IsEqualTo(criteria);
|
||||
}
|
||||
|
||||
private static bool IsComparisonExpression(string criteria)
|
||||
{
|
||||
return criteria.StartsWith(">=", StringComparison.Ordinal) ||
|
||||
criteria.StartsWith("<=", StringComparison.Ordinal) ||
|
||||
criteria.StartsWith("<>", StringComparison.Ordinal) ||
|
||||
criteria.StartsWith("!=", StringComparison.Ordinal) ||
|
||||
criteria.StartsWith('>') ||
|
||||
criteria.StartsWith('<');
|
||||
}
|
||||
|
||||
private bool EvaluateComparisonExpression(string criteria)
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the operator and value
|
||||
string operatorStr;
|
||||
string valueStr;
|
||||
|
||||
if (criteria.StartsWith(">=", StringComparison.Ordinal))
|
||||
{
|
||||
operatorStr = ">=";
|
||||
valueStr = criteria[2..].Trim();
|
||||
}
|
||||
else if (criteria.StartsWith("<=", StringComparison.Ordinal))
|
||||
{
|
||||
operatorStr = "<=";
|
||||
valueStr = criteria[2..].Trim();
|
||||
}
|
||||
else if (criteria.StartsWith("<>", StringComparison.Ordinal) || criteria.StartsWith("!=", StringComparison.Ordinal))
|
||||
{
|
||||
operatorStr = "<>";
|
||||
valueStr = criteria[2..].Trim();
|
||||
}
|
||||
else if (criteria.StartsWith('>'))
|
||||
{
|
||||
operatorStr = ">";
|
||||
valueStr = criteria[1..].Trim();
|
||||
}
|
||||
else if (criteria.StartsWith('<'))
|
||||
{
|
||||
operatorStr = "<";
|
||||
valueStr = criteria[1..].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the value
|
||||
if (!double.TryParse(valueStr, out var numericValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert cell data to number for comparison
|
||||
double cellValue;
|
||||
if (Type == CellDataType.Number)
|
||||
{
|
||||
cellValue = GetValueOrDefault<double>();
|
||||
}
|
||||
else if (Type == CellDataType.Date)
|
||||
{
|
||||
cellValue = GetValueOrDefault<DateTime>().ToNumber();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform the comparison
|
||||
return operatorStr switch
|
||||
{
|
||||
">" => cellValue > numericValue,
|
||||
"<" => cellValue < numericValue,
|
||||
">=" => cellValue >= numericValue,
|
||||
"<=" => cellValue <= numericValue,
|
||||
"<>" or "!=" => cellValue != numericValue,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
164
Radzen.Blazor/Spreadsheet/CellDependencyGraph.cs
Normal file
164
Radzen.Blazor/Spreadsheet/CellDependencyGraph.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
internal class CellDependencyGraph
|
||||
{
|
||||
private readonly Dictionary<Cell, HashSet<Cell>> dependencies = [];
|
||||
private readonly Dictionary<Cell, HashSet<Cell>> dependents = [];
|
||||
|
||||
private IEnumerable<Cell> GetDependentCells(Cell cell)
|
||||
{
|
||||
if (dependents.TryGetValue(cell, out var cells))
|
||||
{
|
||||
return cells;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public IEnumerable<Cell> GetTopologicallySortedDependencies(Cell cell) => GetTopologicallySortedDependencies(GetDependentCells(cell));
|
||||
|
||||
public IEnumerable<Cell> GetTopologicallySortedDependencies() => GetTopologicallySortedDependencies(dependencies.Keys);
|
||||
|
||||
private List<Cell> GetTopologicallySortedDependencies(IEnumerable<Cell> cells)
|
||||
{
|
||||
var visited = new HashSet<Cell>();
|
||||
var result = new List<Cell>();
|
||||
|
||||
void Visit(Cell cell)
|
||||
{
|
||||
if (!visited.Add(cell))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dependentCell in GetDependentCells(cell))
|
||||
{
|
||||
Visit(dependentCell);
|
||||
}
|
||||
result.Add(cell);
|
||||
}
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (!visited.Contains(cell))
|
||||
{
|
||||
Visit(cell);
|
||||
}
|
||||
}
|
||||
|
||||
result.Reverse();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Add(Cell cell)
|
||||
{
|
||||
if (cell.Formula == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tree = FormulaParser.Parse(cell.Formula);
|
||||
var visitor = new DependencyVisitor(cell.Sheet);
|
||||
tree.Root.Accept(visitor);
|
||||
|
||||
if (dependencies.TryGetValue(cell, out var oldDependencies))
|
||||
{
|
||||
foreach (var dependency in oldDependencies)
|
||||
{
|
||||
if (dependents.TryGetValue(dependency, out var dependentCells))
|
||||
{
|
||||
dependentCells.Remove(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newDependencies = visitor.Dependencies;
|
||||
dependencies[cell] = newDependencies;
|
||||
|
||||
foreach (var dependency in newDependencies)
|
||||
{
|
||||
if (!dependents.TryGetValue(dependency, out var dependentCells))
|
||||
{
|
||||
dependentCells = [];
|
||||
dependents[dependency] = dependentCells;
|
||||
}
|
||||
dependentCells.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DependencyVisitor(Sheet sheet) : IFormulaSyntaxNodeVisitor
|
||||
{
|
||||
private readonly Sheet sheet = sheet;
|
||||
|
||||
public HashSet<Cell> Dependencies { get; } = [];
|
||||
|
||||
public void VisitNumberLiteral(NumberLiteralSyntaxNode numberLiteralSyntaxNode)
|
||||
{
|
||||
}
|
||||
|
||||
public void VisitStringLiteral(StringLiteralSyntaxNode stringLiteralSyntaxNode)
|
||||
{
|
||||
}
|
||||
|
||||
public void VisitBooleanLiteral(BooleanLiteralSyntaxNode booleanLiteralSyntaxNode)
|
||||
{
|
||||
}
|
||||
|
||||
public void VisitUnaryExpression(UnaryExpressionSyntaxNode unaryExpressionSyntaxNode)
|
||||
{
|
||||
unaryExpressionSyntaxNode.Operand.Accept(this);
|
||||
}
|
||||
|
||||
public void VisitErrorLiteral(ErrorLiteralSyntaxNode errorLiteralSyntaxNode)
|
||||
{
|
||||
}
|
||||
|
||||
public void VisitBinaryExpression(BinaryExpressionSyntaxNode binaryExpressionSyntaxNode)
|
||||
{
|
||||
binaryExpressionSyntaxNode.Left.Accept(this);
|
||||
binaryExpressionSyntaxNode.Right.Accept(this);
|
||||
}
|
||||
|
||||
public void VisitCell(CellSyntaxNode cellIdentifierSyntaxNode)
|
||||
{
|
||||
var address = cellIdentifierSyntaxNode.Token.Address;
|
||||
var targetSheet = sheet;
|
||||
|
||||
if (!string.IsNullOrEmpty(address.Sheet))
|
||||
{
|
||||
var wb = sheet.Workbook;
|
||||
targetSheet = wb.GetSheet(address.Sheet) ?? targetSheet;
|
||||
if (targetSheet.Name != address.Sheet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (address.Row >= targetSheet.RowCount || address.Column >= targetSheet.ColumnCount)
|
||||
{
|
||||
// Out of bounds, do not add dependency
|
||||
return;
|
||||
}
|
||||
var cell = targetSheet.Cells[address];
|
||||
Dependencies.Add(cell);
|
||||
}
|
||||
|
||||
public void VisitFunction(FunctionSyntaxNode functionSyntaxNode)
|
||||
{
|
||||
foreach (var argument in functionSyntaxNode.Arguments)
|
||||
{
|
||||
argument.Accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void VisitRange(RangeSyntaxNode rangeSyntaxNode)
|
||||
{
|
||||
rangeSyntaxNode.Start.Accept(this);
|
||||
rangeSyntaxNode.End.Accept(this);
|
||||
}
|
||||
}
|
||||
12
Radzen.Blazor/Spreadsheet/CellEditor.razor
Normal file
12
Radzen.Blazor/Spreadsheet/CellEditor.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Radzen.Blazor.Spreadsheet
|
||||
@inject Microsoft.JSInterop.IJSRuntime JSRuntime
|
||||
|
||||
@if (Sheet.Editor.Mode != EditMode.None)
|
||||
{
|
||||
<div class=@className style=@cellStyle>
|
||||
<SheetEditor @bind-Value=@Sheet.Editor.Value AutoFocus=@(Sheet.Editor.Mode == EditMode.Cell) Blur=@(this.AsNonRenderingEventHandler(OnBlurAsync))
|
||||
Sheet=@Sheet Focus=@(this.AsNonRenderingEventHandler(OnFocus)) />
|
||||
</div>
|
||||
}
|
||||
121
Radzen.Blazor/Spreadsheet/CellEditor.razor.cs
Normal file
121
Radzen.Blazor/Spreadsheet/CellEditor.razor.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
#nullable enable
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
/// <summary>
|
||||
/// Renders an inline cell editor for a spreadsheet.
|
||||
/// </summary>
|
||||
public partial class CellEditor : ComponentBase, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the sheet.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Sheet Sheet { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the virtual grid context.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IVirtualGridContext Context { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the spreadsheet instance that contains this cell editor.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public ISpreadsheet Spreadsheet { get; set; } = default!;
|
||||
|
||||
private string? cellStyle;
|
||||
private string? className;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
if (Sheet != null)
|
||||
{
|
||||
Sheet.Editor.Changed -= OnEditModeChanged;
|
||||
Sheet.Editor.ValueChanged -= OnEditorValueChanged;
|
||||
}
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
if (Sheet != null)
|
||||
{
|
||||
Sheet.Editor.Changed += OnEditModeChanged;
|
||||
Sheet.Editor.ValueChanged += OnEditorValueChanged;
|
||||
|
||||
Render();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnBlurAsync()
|
||||
{
|
||||
if (Sheet.Editor.Mode == EditMode.Cell)
|
||||
{
|
||||
await Spreadsheet.AcceptAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
if (Sheet.Selection.Cell != CellRef.Invalid)
|
||||
{
|
||||
var cell = Sheet.Cells[Sheet.Selection.Cell];
|
||||
Sheet.Editor.StartEdit(cell.Address, Sheet.Editor.Mode != EditMode.None ? Sheet.Editor.Value : cell.GetValue(), EditMode.Cell);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEditorValueChanged()
|
||||
{
|
||||
if (Sheet.Editor.Mode == EditMode.Formula)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
var address = Sheet.Selection.Cell;
|
||||
|
||||
var cell = Sheet.Cells[address];
|
||||
|
||||
if (address != CellRef.Invalid)
|
||||
{
|
||||
var rect = Context.GetRectangle(address.Row, address.Column);
|
||||
var sb = StringBuilderCache.Acquire();
|
||||
rect.AppendStyle(sb);
|
||||
|
||||
cell = Sheet.Cells[address];
|
||||
cell.ApplyFormat(sb);
|
||||
|
||||
cellStyle = StringBuilderCache.GetStringAndRelease(sb);
|
||||
}
|
||||
else
|
||||
{
|
||||
cellStyle = null;
|
||||
}
|
||||
|
||||
className = ClassList.Create("rz-spreadsheet-cell-editor")
|
||||
.Add("rz-spreadsheet-frozen-column", Sheet.Selection.Cell != CellRef.Invalid && Sheet.Selection.Cell.Column < Sheet.Columns.Frozen)
|
||||
.Add("rz-spreadsheet-frozen-row", Sheet.Selection.Cell != CellRef.Invalid && Sheet.Selection.Cell.Row < Sheet.Rows.Frozen)
|
||||
.Add($"rz-spreadsheet-cell-editor-{cell.ValueType.ToString().ToLowerInvariant()}", cell != null)
|
||||
.ToString();
|
||||
}
|
||||
|
||||
private void OnEditModeChanged()
|
||||
{
|
||||
Render();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Sheet.Editor.Changed -= OnEditModeChanged;
|
||||
Sheet.Editor.ValueChanged -= OnEditorValueChanged;
|
||||
}
|
||||
}
|
||||
32
Radzen.Blazor/Spreadsheet/CellMenu.razor
Normal file
32
Radzen.Blazor/Spreadsheet/CellMenu.razor
Normal file
@@ -0,0 +1,32 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Radzen.Blazor.Rendering
|
||||
<ul class="rz-menu rz-spreadsheet-cell-menu">
|
||||
<CellMenuItem Text="Sort ascending" Icon="sort" Click=@OnSortAscendingAsync />
|
||||
<CellMenuItem Text="Sort descending" Icon="sort" Click=@OnSortDescendingAsync />
|
||||
<CellMenuItem Text="Clear Filter" Icon="filter_alt_off" Click=@OnClearFilterAsync Disabled=@(!HasFilterApplied()) />
|
||||
<CellMenuItem Text="Custom filter..." Icon="filter_alt" Click=@OnCustomFilterAsync />
|
||||
<div class="rz-spreadsheet-cell-menu-filter">
|
||||
<CellMenuItem>
|
||||
<RadzenCheckBox TValue="bool?" Value=@IsSelectAllChecked() Change=@OnSelectAllChanged TriState="true" />
|
||||
<RadzenLabel Text="(Select All)" />
|
||||
</CellMenuItem>
|
||||
@foreach (var item in LoadAvailableValues())
|
||||
{
|
||||
<CellMenuItem>
|
||||
<RadzenCheckBox TValue="bool" Value=@IsValueSelected(item.Value) Change=@(isChecked => OnValueSelectionChanged(item.Value, isChecked)) />
|
||||
<RadzenLabel Text=@item.Text />
|
||||
</CellMenuItem>
|
||||
}
|
||||
@if (ShouldShowBlankOption())
|
||||
{
|
||||
<CellMenuItem>
|
||||
<RadzenCheckBox TValue="bool" Value=@IsBlankSelected() Change=@OnBlankSelectionChanged />
|
||||
<RadzenLabel Text="(Blank)" />
|
||||
</CellMenuItem>
|
||||
}
|
||||
</div>
|
||||
<RadzenStack Orientation="Orientation.Horizontal" Gap="10px" JustifyContent="JustifyContent.End">
|
||||
<RadzenButton Text="OK" Click=@OnApplyFilterAsync ButtonStyle="ButtonStyle.Primary" Size="ButtonSize.Small" Disabled=@(!CanApplyFilter()) />
|
||||
<RadzenButton Text="Cancel" Click=@OnCancelFilterAsync ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" />
|
||||
</RadzenStack>
|
||||
</ul>
|
||||
554
Radzen.Blazor/Spreadsheet/CellMenu.razor.cs
Normal file
554
Radzen.Blazor/Spreadsheet/CellMenu.razor.cs
Normal file
@@ -0,0 +1,554 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a cell menu in a spreadsheet.
|
||||
/// </summary>
|
||||
public partial class CellMenu : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the sheet containing the cell menu.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public Sheet Sheet { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the row index of the cell menu.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public int Row { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents the column index of the cell menu.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public int Column { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked the user clicks the cancel button in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback Cancel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked the user clicks the apply button in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<SheetFilter?> Apply { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the user clicks the sort ascending option in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback SortAscending { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the user clicks the sort descending option in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback SortDescending { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the user clicks the clear filter option in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback Clear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the user clicks the custom filter option in the cell menu.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback CustomFilter { get; set; }
|
||||
|
||||
private readonly HashSet<object?> selectedFilterValues = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
var didRowChange = parameters.TryGetValue<int>(nameof(Row), out var row) && Row != row;
|
||||
var didColumnChange = parameters.TryGetValue<int>(nameof(Column), out var column) && Column != column;
|
||||
var didSheetChange = parameters.TryGetValue<Sheet>(nameof(Sheet), out var sheet) && Sheet != sheet;
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
// Check if any of the key parameters have changed
|
||||
if (didRowChange || didColumnChange || didSheetChange)
|
||||
{
|
||||
// Reinitialize the selected filter values
|
||||
InitializeSelectedFilterValues();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSelectedFilterValues()
|
||||
{
|
||||
selectedFilterValues.Clear();
|
||||
|
||||
var visitor = new InListCriterionVisitor(Column);
|
||||
|
||||
foreach (var filter in Sheet.Filters)
|
||||
{
|
||||
filter.Criterion.Accept(visitor);
|
||||
}
|
||||
|
||||
if (visitor.FoundValues.Count != 0)
|
||||
{
|
||||
foreach (var value in visitor.FoundValues)
|
||||
{
|
||||
if (value is string str)
|
||||
{
|
||||
CellData.TryConvertFromString(str, out var parsedValue, out _);
|
||||
selectedFilterValues.Add(parsedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFilterValues.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var availableValues = LoadAvailableValues();
|
||||
|
||||
foreach (var (_, Value) in availableValues)
|
||||
{
|
||||
selectedFilterValues.Add(Value);
|
||||
}
|
||||
|
||||
if (ShouldShowBlankOption())
|
||||
{
|
||||
selectedFilterValues.Add(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InListCriterionVisitor(int column) : IFilterCriterionVisitor
|
||||
{
|
||||
private readonly int targetColumn = column;
|
||||
public HashSet<object?> FoundValues { get; } = [];
|
||||
|
||||
public void Visit(OrCriterion criterion)
|
||||
{
|
||||
foreach (var c in criterion.Criteria)
|
||||
{
|
||||
c.Accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(AndCriterion criterion)
|
||||
{
|
||||
foreach (var c in criterion.Criteria)
|
||||
{
|
||||
c.Accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(EqualToCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(GreaterThanCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(InListCriterion criterion)
|
||||
{
|
||||
if (criterion.Column == targetColumn)
|
||||
{
|
||||
foreach (var value in criterion.Values)
|
||||
{
|
||||
FoundValues.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(IsNullCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(LessThanCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(GreaterThanOrEqualCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(LessThanOrEqualCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(NotEqualToCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(StartsWithCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(DoesNotStartWithCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(EndsWithCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(DoesNotEndWithCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(ContainsCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
|
||||
public void Visit(DoesNotContainCriterion criterion)
|
||||
{
|
||||
// Not relevant for InListCriterion extraction
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSortAscendingAsync()
|
||||
{
|
||||
await SortAscending.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task OnSortDescendingAsync()
|
||||
{
|
||||
await SortDescending.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task OnClearFilterAsync()
|
||||
{
|
||||
await Clear.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task OnCancelFilterAsync()
|
||||
{
|
||||
await Cancel.InvokeAsync();
|
||||
}
|
||||
|
||||
private void OnValueSelectionChanged(object? value, bool isChecked)
|
||||
{
|
||||
if (isChecked)
|
||||
{
|
||||
selectedFilterValues.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFilterValues.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValueSelected(object? value)
|
||||
{
|
||||
return selectedFilterValues.Contains(value);
|
||||
}
|
||||
|
||||
private bool? IsSelectAllChecked()
|
||||
{
|
||||
var availableValues = LoadAvailableValues();
|
||||
var showBlank = ShouldShowBlankOption();
|
||||
|
||||
if (availableValues.Count == 0 && !showBlank)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var totalItems = availableValues.Count + (showBlank ? 1 : 0);
|
||||
var selectedCount = availableValues.Count(v => selectedFilterValues.Contains(v.Value));
|
||||
|
||||
// Add 1 to selected count if blank is selected
|
||||
if (showBlank && selectedFilterValues.Contains(null))
|
||||
{
|
||||
selectedCount++;
|
||||
}
|
||||
|
||||
return selectedCount switch
|
||||
{
|
||||
0 => false, // No items selected
|
||||
var count when count == totalItems => true, // All items selected
|
||||
_ => null // Indeterminate state
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSelectAllChanged(bool? isChecked)
|
||||
{
|
||||
var availableValues = LoadAvailableValues();
|
||||
var showBlank = ShouldShowBlankOption();
|
||||
|
||||
if (isChecked != false)
|
||||
{
|
||||
foreach (var (_, Value) in availableValues)
|
||||
{
|
||||
selectedFilterValues.Add(Value);
|
||||
}
|
||||
|
||||
if (showBlank)
|
||||
{
|
||||
selectedFilterValues.Add(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
foreach (var (_, Value) in availableValues)
|
||||
{
|
||||
selectedFilterValues.Remove(Value);
|
||||
}
|
||||
|
||||
selectedFilterValues.Remove(null);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldShowBlankOption()
|
||||
{
|
||||
var table = GetCurrentTable();
|
||||
var dataTable = GetCurrentTable();
|
||||
var autoFilter = GetCurrentAutoFilter();
|
||||
|
||||
// Determine the range to use for checking blank values
|
||||
RangeRef rangeToUse = RangeRef.Invalid;
|
||||
|
||||
if (dataTable != null)
|
||||
{
|
||||
// Use data table range if the cell is part of a data table
|
||||
rangeToUse = dataTable.Range;
|
||||
}
|
||||
else if (autoFilter != null)
|
||||
{
|
||||
// Use auto filter range if the cell is part of an auto filter
|
||||
rangeToUse = autoFilter.Range;
|
||||
}
|
||||
|
||||
if (rangeToUse == RangeRef.Invalid) return false;
|
||||
|
||||
// Excel treats the first row as a header and excludes it from blank checking
|
||||
// Check if any cell in the column (excluding header) has null or empty value
|
||||
for (int row = rangeToUse.Start.Row + 1; row <= rangeToUse.End.Row; row++)
|
||||
{
|
||||
var cell = Sheet.Cells[row, Column];
|
||||
var value = cell.Value;
|
||||
var text = cell.GetValue();
|
||||
|
||||
if (value == null || string.IsNullOrEmpty(text))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsBlankSelected()
|
||||
{
|
||||
return selectedFilterValues.Contains(null);
|
||||
}
|
||||
|
||||
private void OnBlankSelectionChanged(bool isChecked)
|
||||
{
|
||||
if (isChecked)
|
||||
{
|
||||
selectedFilterValues.Add(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFilterValues.Remove(null);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnApplyFilterAsync()
|
||||
{
|
||||
var availableValues = LoadAvailableValues();
|
||||
var showBlank = ShouldShowBlankOption();
|
||||
var totalItems = availableValues.Count + (showBlank ? 1 : 0);
|
||||
var selectedCount = availableValues.Count(v => selectedFilterValues.Contains(v.Value));
|
||||
|
||||
// Add 1 to selected count if blank is selected
|
||||
if (showBlank && selectedFilterValues.Contains(null))
|
||||
{
|
||||
selectedCount++;
|
||||
}
|
||||
|
||||
// If all items are selected, clear the filter instead
|
||||
if (selectedCount == totalItems && totalItems > 0)
|
||||
{
|
||||
await Clear.InvokeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
SheetFilter? filter = null;
|
||||
|
||||
if (selectedFilterValues.Count != 0)
|
||||
{
|
||||
var dataTable = GetCurrentTable();
|
||||
var autoFilter = GetCurrentAutoFilter();
|
||||
|
||||
// Determine the range to use for the filter
|
||||
RangeRef rangeToUse = RangeRef.Invalid;
|
||||
|
||||
if (dataTable != null)
|
||||
{
|
||||
// Use data table range if the cell is part of a data table
|
||||
rangeToUse = dataTable.Range;
|
||||
}
|
||||
else if (autoFilter != null)
|
||||
{
|
||||
// Use auto filter range if the cell is part of an auto filter
|
||||
rangeToUse = autoFilter.Range;
|
||||
}
|
||||
|
||||
if (rangeToUse != RangeRef.Invalid)
|
||||
{
|
||||
// Create a new range for the current column using the determined range
|
||||
var columnRange = new RangeRef(
|
||||
new CellRef(rangeToUse.Start.Row, Column),
|
||||
new CellRef(rangeToUse.End.Row, Column)
|
||||
);
|
||||
|
||||
filter = new SheetFilter(
|
||||
new InListCriterion
|
||||
{
|
||||
Column = Column,
|
||||
Values = [.. selectedFilterValues]
|
||||
},
|
||||
columnRange
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Apply.InvokeAsync(filter);
|
||||
}
|
||||
|
||||
private bool CanApplyFilter()
|
||||
{
|
||||
return selectedFilterValues.Count != 0;
|
||||
}
|
||||
|
||||
private bool HasFilterApplied()
|
||||
{
|
||||
foreach (var filter in Sheet.Filters)
|
||||
{
|
||||
if (filter.Range.Contains(Row, Column))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<(string Text, object? Value)> LoadAvailableValues()
|
||||
{
|
||||
var availableValues = new List<(string Text, object? Value)>();
|
||||
var dataTable = GetCurrentTable();
|
||||
var autoFilter = GetCurrentAutoFilter();
|
||||
|
||||
// Determine the range to use for loading values
|
||||
RangeRef rangeToUse = RangeRef.Invalid;
|
||||
|
||||
if (dataTable != null)
|
||||
{
|
||||
// Use data table range if the cell is part of a data table
|
||||
rangeToUse = dataTable.Range;
|
||||
}
|
||||
else if (autoFilter != null)
|
||||
{
|
||||
// Use auto filter range if the cell is part of an auto filter
|
||||
rangeToUse = autoFilter.Range;
|
||||
}
|
||||
|
||||
if (rangeToUse != RangeRef.Invalid)
|
||||
{
|
||||
var uniqueValues = new List<(string Text, object? Value)>();
|
||||
|
||||
// Excel treats the first row as a header and excludes it from the available values
|
||||
// Start from the second row (header row + 1)
|
||||
for (int row = rangeToUse.Start.Row + 1; row <= rangeToUse.End.Row; row++)
|
||||
{
|
||||
// Check if this row is hidden by a filter that affects the current column
|
||||
bool shouldSkipRow = false;
|
||||
|
||||
foreach (var filter in Sheet.Filters)
|
||||
{
|
||||
// If the filter affects the current column, we should include the value
|
||||
// regardless of whether the row is hidden
|
||||
if (filter.Range.Contains(row, Column))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the filter affects a different range and the row is hidden, skip it
|
||||
if (Sheet.Rows.IsHidden(row))
|
||||
{
|
||||
shouldSkipRow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkipRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cell = Sheet.Cells[row, Column];
|
||||
var value = cell.Value;
|
||||
var text = cell.GetValueAsString();
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
uniqueValues.Add((text, value));
|
||||
}
|
||||
}
|
||||
|
||||
availableValues = [.. uniqueValues
|
||||
.DistinctBy(x => x.Value)
|
||||
.OrderBy(x => x.Text)];
|
||||
}
|
||||
|
||||
return availableValues;
|
||||
}
|
||||
|
||||
private Table? GetCurrentTable()
|
||||
{
|
||||
foreach (var table in Sheet.Tables)
|
||||
{
|
||||
if (table.Range.Contains(Row, Column))
|
||||
{
|
||||
return table;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private AutoFilter? GetCurrentAutoFilter()
|
||||
{
|
||||
// Check if the sheet has an auto filter and if the current cell is within its range
|
||||
if (Sheet.AutoFilter != null && Sheet.AutoFilter.Range.Contains(Row, Column))
|
||||
{
|
||||
return Sheet.AutoFilter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task OnCustomFilterAsync()
|
||||
{
|
||||
await CustomFilter.InvokeAsync();
|
||||
}
|
||||
}
|
||||
43
Radzen.Blazor/Spreadsheet/CellMenuItem.razor
Normal file
43
Radzen.Blazor/Spreadsheet/CellMenuItem.razor
Normal file
@@ -0,0 +1,43 @@
|
||||
@using Radzen.Blazor.Rendering
|
||||
|
||||
<li class="@Class">
|
||||
@if (ChildContent != null)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rz-navigation-item-wrapper" @onclick=@OnClickAsync>
|
||||
<div class="rz-navigation-item-link">
|
||||
<i class="notranslate rzi rz-navigation-item-icon">@Icon</i>
|
||||
<span class="rz-navigation-item-text">@Text</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@code {
|
||||
[Parameter]
|
||||
public EventCallback Click { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Icon { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Text { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
private string Class => ClassList.Create("rz-navigation-item").AddDisabled(Disabled).ToString();
|
||||
|
||||
private async Task OnClickAsync()
|
||||
{
|
||||
if (!Disabled)
|
||||
{
|
||||
await Click.InvokeAsync(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
233
Radzen.Blazor/Spreadsheet/CellRef.cs
Normal file
233
Radzen.Blazor/Spreadsheet/CellRef.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference to a cell in A1 notation e.g. "A1", "B2", etc.
|
||||
/// The row and column are zero-based indices, so "A1" corresponds to (0, 0) and "B2" corresponds to (1, 1).
|
||||
/// This struct is immutable and provides methods for parsing, comparing, and converting to string representation.
|
||||
/// </summary>
|
||||
public readonly struct CellRef(int row, int column) : IEquatable<CellRef>
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an invalid cell reference with row and column set to -1.
|
||||
/// </summary>
|
||||
public static CellRef Invalid { get; } = new(-1, -1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the row index of the cell reference.
|
||||
/// The row index is zero-based, so the first row (A1) corresponds to 0.
|
||||
/// </summary>
|
||||
public int Row { get; } = row;
|
||||
/// <summary>
|
||||
/// Gets the column index of the cell reference.
|
||||
/// The column index is zero-based, so the first column (A1) corresponds to
|
||||
/// </summary>
|
||||
public int Column { get; } = column;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional sheet name qualifier for this reference.
|
||||
/// When null, the reference targets the current sheet.
|
||||
/// </summary>
|
||||
public string? Sheet { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the row reference is absolute (prefixed with '$' in A1 notation).
|
||||
/// </summary>
|
||||
public bool IsRowAbsolute { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the column reference is absolute (prefixed with '$' in A1 notation).
|
||||
/// </summary>
|
||||
public bool IsColumnAbsolute { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(CellRef other) => Row == other.Row && Column == other.Column;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is CellRef other && Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(Row, Column);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the cell reference is equal to another cell reference.
|
||||
/// </summary>
|
||||
public static bool operator ==(CellRef left, CellRef right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the cell reference is not equal to another cell reference.
|
||||
/// </summary>
|
||||
public static bool operator !=(CellRef left, CellRef right) => !left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the cell reference in A1 notation.
|
||||
/// For example, if the cell reference is (0, 0), it returns "A1";
|
||||
/// if the cell reference is (1, 1), it returns "B2".
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = StringBuilderCache.Acquire();
|
||||
|
||||
if (IsColumnAbsolute)
|
||||
{
|
||||
sb.Append('$');
|
||||
}
|
||||
|
||||
sb.Append(ColumnRef.ToString(Column));
|
||||
|
||||
if (IsRowAbsolute)
|
||||
{
|
||||
sb.Append('$');
|
||||
}
|
||||
|
||||
sb.Append(Row + 1);
|
||||
|
||||
return StringBuilderCache.GetStringAndRelease(sb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the cell reference to a range reference that includes the cell itself.
|
||||
/// </summary>
|
||||
public RangeRef ToRange() => new(this, this);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string in A1 notation (e.g., "A1", "B2") into a CellRef instance.
|
||||
/// If the string is not a valid A1 notation, it throws an ArgumentException.
|
||||
/// </summary>
|
||||
/// <param name="index">The A1 notation string to parse.</param>
|
||||
/// <returns>A CellRef instance representing the parsed cell reference.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the input string is not a valid A1 notation.</exception>
|
||||
public static CellRef Parse(string index)
|
||||
{
|
||||
if (!TryParse(index, out var result))
|
||||
{
|
||||
throw new ArgumentException($"Invalid A1 notation: {index}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps two CellRef instances if the first one is greater than the second one.
|
||||
/// This is useful for ensuring a consistent order when comparing or storing cell references.
|
||||
/// </summary>
|
||||
public static (CellRef, CellRef) Swap(CellRef a, CellRef b)
|
||||
{
|
||||
if (a.Row > b.Row || (a.Row == b.Row && a.Column > b.Column))
|
||||
{
|
||||
return (b, a);
|
||||
}
|
||||
|
||||
return (a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a string in A1 notation (optionally with '$' absolute markers) into a CellRef instance.
|
||||
/// Returns true if successful and outputs absolute flags for the column and row.
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="result"></param>
|
||||
public static bool TryParse(string index, out CellRef result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (string.IsNullOrEmpty(index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Support optional sheet name prefix: SheetName!A1
|
||||
string? sheetName = null;
|
||||
var bang = index.IndexOf('!', StringComparison.Ordinal);
|
||||
if (bang >= 0)
|
||||
{
|
||||
sheetName = index[..bang];
|
||||
|
||||
if (string.IsNullOrEmpty(sheetName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index = index[(bang + 1)..];
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var isColumnAbsolute = false;
|
||||
var isRowAbsolute = false;
|
||||
|
||||
|
||||
// Optional $ before column
|
||||
if (i < index.Length && index[i] == '$')
|
||||
{
|
||||
isColumnAbsolute = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Parse column letters
|
||||
var column = 0;
|
||||
var hasLetters = false;
|
||||
while (i < index.Length)
|
||||
{
|
||||
var ch = index[i];
|
||||
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
|
||||
{
|
||||
hasLetters = true;
|
||||
var upper = ch >= 'a' && ch <= 'z' ? (char)(ch - 'a' + 'A') : ch;
|
||||
column = column * 26 + (upper - 'A' + 1);
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLetters)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional $ before row digits
|
||||
if (i < index.Length && index[i] == '$')
|
||||
{
|
||||
isRowAbsolute = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Parse row digits
|
||||
var rowStart = i;
|
||||
while (i < index.Length && index[i] >= '0' && index[i] <= '9')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (rowStart == i)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(index[rowStart..i], out var row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must consume all characters
|
||||
if (i != index.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new CellRef(row - 1, column - 1)
|
||||
{
|
||||
IsColumnAbsolute = isColumnAbsolute,
|
||||
IsRowAbsolute = isRowAbsolute,
|
||||
Sheet = sheetName
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
5
Radzen.Blazor/Spreadsheet/CellSelection.razor
Normal file
5
Radzen.Blazor/Spreadsheet/CellSelection.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@using Radzen.Blazor.Rendering
|
||||
@foreach (var range in GetRanges())
|
||||
{
|
||||
<CellSelectionItem Context=@Context Range=@range.Range Sheet=@Sheet FrozenRow=@range.FrozenRow FrozenColumn=@range.FrozenColumn Top=@range.Top Left=@range.Left Bottom=@range.Bottom Right=@range.Right />
|
||||
}
|
||||
36
Radzen.Blazor/Spreadsheet/CellSelection.razor.cs
Normal file
36
Radzen.Blazor/Spreadsheet/CellSelection.razor.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a component that displays the selection of a cell in a spreadsheet.
|
||||
/// </summary>
|
||||
public partial class CellSelection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cell reference for which the selection is displayed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public CellRef Cell { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sheet that contains the cell for which the selection is displayed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Sheet Sheet { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the virtual grid context that provides information about the grid's state.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IVirtualGridContext Context { get; set; } = default!;
|
||||
|
||||
private IEnumerable<RangeInfo> GetRanges()
|
||||
{
|
||||
var mergedRange = Sheet.MergedCells.GetMergedRange(Cell);
|
||||
var range = mergedRange != RangeRef.Invalid ? mergedRange : new RangeRef(Cell, Cell);
|
||||
|
||||
return Sheet.GetRanges(range);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user