Compare commits

..

271 Commits

Author SHA1 Message Date
Atanas Korchev
ed0f8892a8 Fix nullability errors. 2026-01-19 11:30:49 +02:00
Atanas Korchev
452e3f046c WEEKNUM function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
797fcefbc8 Extract common code. 2026-01-19 11:30:49 +02:00
Atanas Korchev
9677545c6c WEEKDAY function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
00f37c4562 HOUR, MINUTE, SECOND functions. 2026-01-19 11:30:49 +02:00
Atanas Korchev
69dfd2a672 YEAR function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
8f35d495f2 MONTH function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
e6e91e96d7 DAY function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
7b0e05069e NOW function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
425b36db7f TODAY function and date arithmetic with numbers. 2026-01-19 11:30:49 +02:00
Atanas Korchev
7e28102477 REPT function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
722ae13c00 VALUE function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
11863e2f91 LOWER function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
0b5e629f78 UPPER function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
1639325741 PROPER function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
665372b397 SUBSTITUTE function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
0d218e174f REPLACE function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
c88a4c19fe FIND function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
bb1b810c72 Use common code for wildcards. 2026-01-19 11:30:49 +02:00
Atanas Korchev
d098a6c0f1 SEARCH function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
f0edbce9c6 Refactor common parameter parsing. 2026-01-19 11:30:49 +02:00
Atanas Korchev
112f7d1b70 MID function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
cd6eba4882 RIGHT function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
b1aae0904c Add boolean literal parsing tests. 2026-01-19 11:30:49 +02:00
Atanas Korchev
019977d7e7 Unary parsing. 2026-01-19 11:30:49 +02:00
Atanas Korchev
c556e0de1a LEFT function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
8fdf1ba312 Boolean token support (TRUE, FALSE). TEXTJOIN function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
7fe4f0f96d CONCAT function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
c70efee29a TRIM function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
b1c1687c48 LEN function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
082894054b ROWS and COLUMNS function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
da19e7404b COLUMN function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
3cb7106c4f ROW function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
716a75872d CHOOSE function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
ae1c0d8ee1 INDEX function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
463db22a2e Refactor common code. 2026-01-19 11:30:49 +02:00
Atanas Korchev
f104d939a2 Add RAND and RANDBETWEEN functions. 2026-01-19 11:30:49 +02:00
Atanas Korchev
9da74fb81d Refactor common code. 2026-01-19 11:30:49 +02:00
Atanas Korchev
b694daa52e AGGREGATE function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
4fd3e3c910 SUBTOTAL function. 2026-01-19 11:30:49 +02:00
Atanas Korchev
4c619fa5c9 INT and TRUNC functions. 2026-01-19 11:30:49 +02:00
Atanas Korchev
83bf1ae2cd ROUND, ROUNDUP and ROUNDDOWN functions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
f75ed5734c LARGE and SMALL functions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
09f229602d MIN, MINA, MAX and MAXA functions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
c8225a71bb HLOOKUP, VLOOKUP and XLOOKUP functions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
1e881ddd6e Support sheet names in references. 2026-01-19 11:30:48 +02:00
Atanas Korchev
095c5d2059 Conditional formatting. 2026-01-19 11:30:48 +02:00
Atanas Korchev
019f367466 Parse unknown tokens. 2026-01-19 11:30:48 +02:00
Atanas Korchev
28918e792b Move absolute flags to CellRef. 2026-01-19 11:30:48 +02:00
Atanas Korchev
57d832549d Address aware copy and cut. 2026-01-19 11:30:48 +02:00
Atanas Korchev
7c0768fc7a Insert before the first selected item or after the last selected item. 2026-01-19 11:30:48 +02:00
Atanas Korchev
fd69705fd9 Delete multiple rows and columns. 2026-01-19 11:30:48 +02:00
Atanas Korchev
55b58a744c Add row and column command tests. 2026-01-19 11:30:48 +02:00
Atanas Korchev
e264d654c6 Column insert and delete tools. 2026-01-19 11:30:48 +02:00
Atanas Korchev
d0f00bd7f6 Add Insert Row Before tool. Refactor row tools. 2026-01-19 11:30:48 +02:00
Atanas Korchev
1a62deaf35 Add InsertRowAfter tool. 2026-01-19 11:30:48 +02:00
Atanas Korchev
7fcf4047a1 Insert row and column and tests. 2026-01-19 11:30:48 +02:00
yordanov
ac4fd8d5b3 Update premium themes 2026-01-19 11:30:48 +02:00
Atanas Korchev
48d1c87908 Improved formula handling when a cell is deleted. 2026-01-19 11:30:48 +02:00
Atanas Korchev
dd7a13f3cf DeleteRow tool. 2026-01-19 11:30:48 +02:00
Atanas Korchev
db3c81388a Delete rows and columns. Absolute references. 2026-01-19 11:30:48 +02:00
Atanas Korchev
1e6dd77759 Extract highlight component. 2026-01-19 11:30:48 +02:00
Atanas Korchev
8a81491c29 Extract FunctionHint component. 2026-01-19 11:30:48 +02:00
Atanas Korchev
86245ae162 Refactor formula parsing to accumulate errors. 2026-01-19 11:30:48 +02:00
Atanas Korchev
05b957a8d3 Display function suggestions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
21828c6ae7 Formula highlight. 2026-01-19 11:30:48 +02:00
Atanas Korchev
89e91e5c0e Rename ContentEditable to SheetEditor. 2026-01-19 11:30:48 +02:00
Atanas Korchev
06c4d41273 Move matching to CellData. 2026-01-19 11:30:48 +02:00
Atanas Korchev
60cb216fbd Rename parameters. 2026-01-19 11:30:48 +02:00
Atanas Korchev
f4d916a0de Add support for SUMIF. Describe function parameters. 2026-01-19 11:30:48 +02:00
Atanas Korchev
eb54d808cf Move function tests in separate files. 2026-01-19 11:30:48 +02:00
Atanas Korchev
0565451a97 Introduce CellData. Use it for formula calculation. Unify formula behavior with Excel. 2026-01-19 11:30:48 +02:00
Atanas Korchev
8174ac2725 Remove expressions from formulas. 2026-01-19 11:30:48 +02:00
Atanas Korchev
493bd69a1e Add support for IFERROR. Support functions that accept error arguments. 2026-01-19 11:30:48 +02:00
Atanas Korchev
794d8255cf Add COUNTA function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
edadfe493b Add COUNT function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
c7bda57838 Add Average function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
947a23a4cb Add support for the NOT function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
bf30dabe4d Add Or function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
c5b6b8da95 Add And function. 2026-01-19 11:30:48 +02:00
Atanas Korchev
e171252ecc Add function registry. 2026-01-19 11:30:48 +02:00
Atanas Korchev
7d090a4147 Extract function base class. 2026-01-19 11:30:48 +02:00
Atanas Korchev
bbd2cd777b IF expressions in formulas. 2026-01-19 11:30:48 +02:00
Atanas Korchev
d7bc875d83 Add keyboard shortcuts for undo and redo. 2026-01-19 11:30:48 +02:00
Atanas Korchev
ae997416a2 Double-clicking edits the right merged cell. 2026-01-19 11:30:48 +02:00
Atanas Korchev
97d94b2512 Render the menu toggle only for the last cell of a merged region. 2026-01-19 11:30:48 +02:00
Atanas Korchev
14ec0a8e4e Do not reset the editor value. 2026-01-19 11:30:48 +02:00
Atanas Korchev
985a802314 Use DialogService.Alert to display the validation message. 2026-01-19 11:30:48 +02:00
Atanas Korchev
a3b4526c33 Do not trigger unnecessary undo commands. Do not trigger blur when changing between cell and formula editor. 2026-01-19 11:30:48 +02:00
Atanas Korchev
42e595ed85 Add cell value type class. 2026-01-19 11:30:48 +02:00
Atanas Korchev
cc62ceae3b Persist column width only if different from the default. 2026-01-19 11:30:48 +02:00
Atanas Korchev
1d0e748c91 Save the row height only if different from the default. 2026-01-19 11:30:48 +02:00
Atanas Korchev
b70a6cba99 Save the worksheets to a worksheets directory. 2026-01-19 11:30:48 +02:00
Atanas Korchev
cb469e97c5 Refactor LoadFromStream. 2026-01-19 11:30:48 +02:00
Atanas Korchev
0ebc789de0 Split SaveToStream in multiple methods. 2026-01-19 11:30:48 +02:00
Atanas Korchev
299f565d66 Export and import filters. 2026-01-19 11:30:48 +02:00
Atanas Korchev
8dd6aba59c Refactor the filter criterions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
ebf0e4fadf Restore the spreadsheet filter dialog from the current filter. 2026-01-19 11:30:48 +02:00
Atanas Korchev
2e1aeffbf8 Filter dialog. 2026-01-19 11:30:48 +02:00
Atanas Korchev
0a83fee55d Move the filter tool to Data tab. 2026-01-19 11:30:48 +02:00
Atanas Korchev
5a89117c6f Remove filter menu item. 2026-01-19 11:30:48 +02:00
Atanas Korchev
eb0e96ea42 Do not render merged cells that are hidden. 2026-01-19 11:30:48 +02:00
Atanas Korchev
a9f38fa9e9 Mask position fixes. 2026-01-19 11:30:48 +02:00
Atanas Korchev
b680633b5c Add filtering tests. 2026-01-19 11:30:48 +02:00
Atanas Korchev
ff5ce9298a Add numeric filter criterions. 2026-01-19 11:30:48 +02:00
Atanas Korchev
2b4bf2aef4 Rename DataTable to Table. 2026-01-19 11:30:48 +02:00
Atanas Korchev
fd59a1a1be Do not display the formula in the filter menu. 2026-01-19 11:30:48 +02:00
Atanas Korchev
3ff93dd0e6 Sort auto filter. 2026-01-19 11:30:48 +02:00
Atanas Korchev
b8edfad166 Sorting is undoable. 2026-01-19 11:30:48 +02:00
Atanas Korchev
6aaf42d883 CellMenu selected value state is no longer stale. 2026-01-19 11:30:48 +02:00
Atanas Korchev
9306c6f6d7 Undo filtering. 2026-01-19 11:30:48 +02:00
Atanas Korchev
1ddc854da9 RadzenSpreadsheet does the filtering. 2026-01-19 11:30:48 +02:00
Atanas Korchev
ae5c48f5e5 Use the AutoFilter range to get the filter values. 2026-01-19 11:30:48 +02:00
Atanas Korchev
e47848bf5a Add filter tool. 2026-01-19 11:30:48 +02:00
Atanas Korchev
e09002fa43 Refactor tables. Add AutoFilter. 2026-01-19 11:30:48 +02:00
Atanas Korchev
eb817de8e4 Simplify spreadsheet configuration. 2026-01-19 11:30:48 +02:00
Atanas Korchev
b2bc210eca Close the menu after sorting. 2026-01-19 11:30:48 +02:00
Atanas Korchev
da230e0cbe Styling improvements of the cell menu. 2026-01-19 11:30:48 +02:00
Atanas Korchev
ffae640c3f Disable the apply button when no value is selected. 2026-01-19 11:30:48 +02:00
Atanas Korchev
9f3e0d6098 Add show all and blank options. 2026-01-19 11:30:48 +02:00
Atanas Korchev
4078927d58 Filtering. 2026-01-19 11:30:48 +02:00
Atanas Korchev
c7994e0479 Add InList and Null filter. 2026-01-19 11:30:48 +02:00
Atanas Korchev
26c87de08c Bring back text align and vertical align tools. 2026-01-19 11:30:48 +02:00
Atanas Korchev
0b938dd51c Render cell popup chevron. 2026-01-19 11:30:48 +02:00
Atanas Korchev
8427d8688b Export cell sizes. 2026-01-19 11:30:48 +02:00
Atanas Korchev
9f198cbd23 Add more freeze options. 2026-01-19 11:30:47 +02:00
Atanas Korchev
a97e378800 Persist and load frozen state to XLSX. 2026-01-19 11:30:47 +02:00
Atanas Korchev
790f13ef2b Freeze tool. 2026-01-19 11:30:47 +02:00
Atanas Korchev
179d59c195 Use pointer events. 2026-01-19 11:30:47 +02:00
Atanas Korchev
87bdc2f859 Row resize. 2026-01-19 11:30:47 +02:00
Atanas Korchev
d75cf817c4 Column resizing. 2026-01-19 11:30:47 +02:00
Atanas Korchev
ca0806d5b1 Vertical align tool. 2026-01-19 11:30:47 +02:00
Atanas Korchev
3318386fe6 Remove justify align as Excel does not support it. 2026-01-19 11:30:47 +02:00
Atanas Korchev
44a835cfe8 Add text align tool. 2026-01-19 11:30:47 +02:00
Atanas Korchev
81b28b9602 Export bold, italic and underline. 2026-01-19 11:30:47 +02:00
Atanas Korchev
dc8ea54c91 Add Bold, Italic and Underline. 2026-01-19 11:30:47 +02:00
Atanas Korchev
1d40582cbd Move the tools in their own directory. 2026-01-19 11:30:47 +02:00
Atanas Korchev
e4cbcfdf74 Use command for applying the format. 2026-01-19 11:30:47 +02:00
Atanas Korchev
7ab385e552 Add background color tool. 2026-01-19 11:30:47 +02:00
Atanas Korchev
9b713a7f38 Add color button. 2026-01-19 11:30:47 +02:00
Atanas Korchev
11400e3690 Add RedoButton. 2026-01-19 11:30:47 +02:00
Atanas Korchev
fc31bd6fc6 Add undo button. 2026-01-19 11:30:47 +02:00
Atanas Korchev
d1d9a254d7 Add save button. 2026-01-19 11:30:47 +02:00
Atanas Korchev
d79f3a887d Add built-in upload button. 2026-01-19 11:30:47 +02:00
Atanas Korchev
849ec9c91a Move spreadsheet implementation. 2026-01-19 11:30:47 +02:00
Vladimir Enchev
13afd0fddb Version updated 2026-01-19 10:43:13 +02:00
Vladimir Enchev
fa7120571d DataGrid, DataFilter and PivotDataGrid numeric filter cannot be cleared 2026-01-19 10:42:35 +02:00
Vladimir Enchev
4d4e882dcc Upload API example fixed 2026-01-19 09:39:50 +02:00
Vladimir Enchev
af8c7179d1 Pivot Grid Sorting: SortOrder="SortOrder.Descending" can break sort controls
Fix #2425
2026-01-19 09:31:31 +02:00
Vladimir Enchev
c743054184 RadzenAIChat sometimes first character not detected in chat input field
Fix #2424
2026-01-19 09:17:31 +02:00
Vladimir Enchev
935d850296 Version updated 2026-01-16 11:46:46 +02:00
Vladimir Enchev
ad6b6886f1 ToFilterString() and ToODataString() fixed for non string values 2026-01-16 09:33:21 +02:00
Maxim Becker
d5d08c89de Fix filtering in RadzenAutoComplete if Data is simple list of strings (#2422) 2026-01-16 09:22:46 +02:00
Vladimir Enchev
5a246f732e Version updated 2026-01-15 18:28:58 +02:00
Vladimir Enchev
669ad421d9 Chart 0 at beginning and end of x-axis 2026-01-15 18:28:39 +02:00
Vladimir Enchev
98e8393d14 DataGrid column UniqueID initialization fixed 2026-01-15 18:08:38 +02:00
tharreck
de8dfb7e93 Fix issue where selectedItem is not updated when value is set to null in dropdown. (#2420)
Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2026-01-15 17:14:16 +02:00
Vladimir Enchev
61b32d6490 RadzenDropDown with AllowFiltering and single selection should close on ENTER 2026-01-15 09:21:08 +02:00
Vladimir Enchev
5057fb1cc8 PivotDataGrid row/col groups should respect sorting
Fix #2413
2026-01-14 16:07:14 +02:00
Vladimir Enchev
ed49dfc2fa DatePicke invalid cast with nullable DateTimeOffset 2026-01-14 13:52:29 +02:00
Vladimir Enchev
839018795c version updated 2026-01-13 14:28:29 +02:00
yordanov
c541f9d3e5 Style borders demos 2026-01-13 14:11:02 +02:00
yordanov
5e5957e4c4 Add video to borders' demos 2026-01-13 14:05:46 +02:00
yordanov
84a759267e Style Barcode demo 2026-01-13 13:48:58 +02:00
Vladimir Enchev
eb1e6fab0d Barcode added (#2416)
demo improved

various fixes

more fixes
2026-01-13 10:44:09 +02:00
Vladimir Enchev
6094953009 DataGrid drag to group or reorder restricted over grid only 2026-01-13 10:41:01 +02:00
Vladimir Enchev
b845c73ad6 DataGrid cannot group by column in some cases 2026-01-13 10:00:58 +02:00
Vladimir Enchev
2788606105 Version updated 2026-01-12 17:09:07 +02:00
Vladimir Enchev
aebf267320 Scheduler Planner/Year view: appointments missing when start month is January
Fix #2415
2026-01-12 17:08:37 +02:00
Vladimir Enchev
33896ea8db RadzenChat user is typing option added
Fix #2376
2026-01-12 10:51:11 +02:00
Vladimir Enchev
cb8699f315 RadzenTabs aria-selected accessibility issue 2026-01-12 09:50:09 +02:00
Vladimir Enchev
2a43db6560 RadzenChat always eats first key press after sending first message 2026-01-12 09:37:35 +02:00
Pavlo Iatsiuk
0ad1200870 #2406 - Allow to define Http method for Upload component (#2407)
* #2406 - Allow to define Http method for Upload component

* #2406 - Allow to define Http method for Upload component and stream raw file data

* #2406 - Allow to define Http method for Upload component
Example how to stream file

---------

Co-authored-by: Pavlo Iatsiuk <pavlo.iatsiuk@nems.eco>
2026-01-12 09:22:15 +02:00
yordanov
2debdbfd38 Update copyright year 2026-01-11 13:20:15 +02:00
tharreck
07c96bd4bd fix strange oninput behavior on textbox and textarea (server rendering) (#2410)
* use bind:get and bind:set to fix  strange behavior on input while using server rendering

* only update the Immediate textarea and textbox

---------

Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2026-01-09 14:29:03 +02:00
Vladimir Enchev
704c6abe7c Version updated 2026-01-09 08:09:02 +02:00
Vladimir Enchev
e0e0e608e2 DataGrid ToFilterString() fixed
Fix #2411
2026-01-09 08:07:17 +02:00
Vladimir Enchev
40e15a0720 Symbols package fixed 2026-01-08 10:48:34 +02:00
Vladimir Enchev
d6235fd147 Version updated 2026-01-08 10:24:26 +02:00
Vladimir Enchev
c5a3a911e0 DataGrid filtering by IsNull/IsNotNull fixed 2026-01-08 10:08:54 +02:00
Vladimir Enchev
4e7a037bc5 RadzenPickList selection by ValueProperty fixed 2026-01-08 10:08:29 +02:00
Vladimir Enchev
f1c3f46ad7 RadzenPickList ValueProperty added 2026-01-08 09:22:07 +02:00
Vladimir Enchev
6c0e39da20 DataGrid filter operators should check if the property is nullable 2026-01-07 13:48:59 +02:00
Vladimir Enchev
2a9d638acf null item selection restored 2026-01-07 13:23:04 +02:00
Vladimir Enchev
426e1ba8e8 DropDownBase/ListBox/DataGridHeaderCell selection of null item fixed 2026-01-07 13:03:07 +02:00
Vladimir Enchev
1bb36ef207 demo fixed 2026-01-07 10:06:45 +02:00
Vladimir Enchev
67cde0fd59 RadzenDropDownDataGrid paging fixed 2026-01-07 09:50:50 +02:00
Vladimir Enchev
0696cd20d5 Various warnings resolved and TreatWarningsAsErrors enabled (#2409) 2026-01-07 09:29:58 +02:00
Vladimir Enchev
34fce5188f DataGrid should not call Count() more than once 2026-01-07 09:11:33 +02:00
Vladimir Enchev
273ba0381f Version updated 2026-01-06 10:45:51 +02:00
Vladimir Enchev
63a05d86e8 Nested DataGrid ContextMenu not working properly 2026-01-06 10:41:52 +02:00
Vladimir Enchev
283e115d0a Upload file cannot be reselected after removed 2026-01-06 10:08:40 +02:00
Atanas Korchev
b513ebba8e Allow HEAD requests to routable URLs. 2026-01-05 11:41:22 +02:00
Vladimir Enchev
e324cea7b9 DropDown will select item on ENTER if there is only one item 2025-12-27 16:47:01 +02:00
Vladimir Enchev
c3cda91a2d DataGrid columns visibility not saved in settings when changing PageSize 2025-12-23 10:35:49 +02:00
Vladimir Enchev
bb9f611629 ClearSearchAfterSelection does not reset filtering after reopening DropDown
Fix #2405
2025-12-23 09:57:24 +02:00
Atanas Korchev
6cc8e62b15 Update the bug report issue template to include playground snippet instructions. 2025-12-22 14:46:27 +02:00
Atanas Korchev
5a44b76adb Use unique image name to produce a new deploy every time. 2025-12-22 12:45:07 +02:00
Atanas Korchev
109d6bc617 Update deploy.yml 2025-12-22 12:22:06 +02:00
Vladimir Enchev
7249ff6107 Version updated 2025-12-22 11:38:43 +02:00
Vladimir Enchev
406830fa13 Numeric fails to accept pasted input when Min is specified
Fix #2401
2025-12-22 10:25:17 +02:00
Vladimir Enchev
a6a3278443 Single selection filtered dropdown popup does not close when selecting an item with enter key
Fix #2398
2025-12-22 09:43:17 +02:00
Atanas Korchev
9fd8531529 Using ___ or *** in markdown content causes exception in RadzenMarkdown. Fixes #2402. 2025-12-22 08:29:07 +02:00
Atanas Korchev
c687976796 Update deploy.yml 2025-12-20 06:49:59 +02:00
Atanas Korchev
ae511929f7 Keep only one container version (the latest) 2025-12-20 06:30:41 +02:00
Atanas Korchev
e78e756206 Workaround for https://github.com/dotnet/aspnetcore/issues/64693. 2025-12-19 20:46:55 +02:00
Atanas Korchev
2e9e0dac1a Update deploy.yml 2025-12-19 19:44:36 +02:00
Atanas Korchev
bff97712e2 Update deploy.yml 2025-12-19 19:29:28 +02:00
Atanas Korchev
78bc25cce0 Update deploy.yml 2025-12-19 19:17:34 +02:00
Atanas Korchev
a2e829417d Update deploy.yml 2025-12-19 18:55:44 +02:00
Atanas Korchev
9902413daf Logging. 2025-12-19 18:54:05 +02:00
Atanas Korchev
06a35f4a73 Create deploy.yml 2025-12-19 18:26:49 +02:00
Atanas Korchev
e2a46157b9 Playground (#2400)
* Playground page.

* Persist the changes.

* Add copy button.

* Style Playground

* Update Playground button in CodeViewer

* Add AntiForgery support.

* Update Save alert

* Extract common code.

* Loading tweaks.

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-19 17:36:32 +02:00
Atanas Korchev
c4913a94c4 Remove duplicate anchors from the chart series demo. 2025-12-19 11:29:47 +02:00
Vladimir Enchev
d6e04c3ae8 DropDownDataGrid ContextMenuDataGrid added 2025-12-18 16:04:02 +02:00
Vladimir Enchev
5b17ef5217 Version updated 2025-12-17 16:19:05 +02:00
Anspitzen
8edeedc32c Rework paste handler and add same function to new drop handler (#2394)
* Rework paste handler and add same function to new drop handler

* Remove eventlistener on destroy

* Try restore formating of Radzen.Balzor.js

* Fix missing delegate flag on create editor from restoring formating

* Handle drop and paste with same c# EventCallback OnPaste/Paste
2025-12-17 11:34:04 +02:00
Atanas Korchev
543dbc9e50 Serve uploaded images as static assets. 2025-12-17 11:18:30 +02:00
tharreck
3710f4297b Add ExpandAll/CollapseAll button in hierarchy datagrid (#2391)
* Add ExpandAll/CollapseAll button in hierarchy datagrid

* update icons of ExpandAll/CollapseAll button

* Update naming and use default values for ExpandAllTitle and CollapseAllTitle

---------

Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2025-12-17 08:44:30 +02:00
Paul Ruston
91d6ba4da8 Add another condition for overlapping appointments (#2392) 2025-12-16 13:56:18 +02:00
Atanas Korchev
fbfa3c1abe Update README.md 2025-12-16 13:50:54 +02:00
Atanas Korchev
d13d7bef0c RadzenMarkdown allows unsafe protocols in attributes - javascript: etc. 2025-12-16 13:45:54 +02:00
joriverm
510f5d7190 RadzenDropdown: correctly close popup on item selection, ignoring OpenOnFocus (#2389)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-12-15 16:08:29 +02:00
Vladimir Enchev
595b98d4b7 DropDownDataGrid row not focused on select 2025-12-15 11:01:29 +02:00
Atanas Korchev
c696107aeb Build does not finish. 2025-12-11 18:18:16 +02:00
Atanas Korchev
632722cd7b Llms.txt fixes. 2025-12-11 17:54:58 +02:00
Atanas Korchev
60c264ea49 Remove UseStaticAssets to restore compression. 2025-12-11 17:10:19 +02:00
Vladimir Enchev
10ce92264d Version updated 2025-12-11 10:35:03 +02:00
Vladimir Enchev
2e5c7aa6bc Mask and Numeric Immediate property added
Fix #2384, #2386
2025-12-11 10:15:24 +02:00
yordanov
1e7e2c5b51 Add video to RadzenIcon demos 2025-12-11 10:08:29 +02:00
Atanas Korchev
46790ce3fe Html editor dropdown aria attributes. Closes #2385. (#2387)
* Add ARIA attributes to HtmlEditorDropdown.

* Fix spacing and font size of rz-html-editor-dropdown

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-10 11:31:33 +02:00
yordanov
7bd891f8f0 Add video to RadzenText demos 2025-12-10 09:50:27 +02:00
Vladimir Enchev
1ae16acbac Fixed possible DropDown NullReferenceException
Fix #2380
2025-12-10 08:20:33 +02:00
Vladimir Enchev
2281fb7f61 DataGrid expand/collapse of items using right/left keys added 2025-12-10 08:12:10 +02:00
Vladimir Enchev
89e8db6c6e Version updated 2025-12-09 10:24:30 +02:00
Vladimir Enchev
11300692e5 DropDownBase search by key enabled when bound without TextProperty 2025-12-09 10:21:38 +02:00
Barry
a31161c6d8 Fix: TreeItem Template click does not toggle Checkbox (#2374)
* add click handler for treeviewitem when Template used with checkboxes

* change approach to expose a toggle option

* remove unused method and add select to toggle checkbox selection

* rename to toggle checked

---------

Co-authored-by: Barry Wright <ace90210ace@msn.com>
2025-12-09 09:02:38 +02:00
xsainteer
edd85f8ec0 added UseDisplayName parameter to DataGridColumn component (#2378) 2025-12-08 08:48:18 +02:00
Vladimir Enchev
0d420604dd code fixed 2025-12-05 12:03:43 +02:00
Vladimir Enchev
777f5666e2 Docker improved to avoid running demos browse problems 2025-12-05 11:59:42 +02:00
Vladimir Enchev
e7bded641f Version updated 2025-12-05 11:16:11 +02:00
Vladimir Enchev
6f0dfbe038 Fixed cannot use ?? unparenthesized within || and && expressions 2025-12-05 11:15:57 +02:00
Vladimir Enchev
0e92eeb50e UseStaticFiles() added to enable llms.txt visibility 2025-12-05 11:06:56 +02:00
Vladimir Enchev
50b6cb879a Version updated 2025-12-05 10:14:58 +02:00
Vladimir Enchev
739ebde6a8 Llmtxt generation added (#2379)
* RadzenBlazorDemos.Tools added

* GenerateLlmsTxt Condition update to Release

* GenerateLlmsTxt updated again

* code fixed

* content generation cleaned

* razor/html tags removed from example descriptions and the code is now inside codeblock

* more fixes
2025-12-05 10:14:15 +02:00
Vladimir Enchev
24ef74f16d Upload remove file throws cannot read property 'find' exception 2025-12-05 10:10:13 +02:00
Vladimir Enchev
9e0a57ca5b RadzenDropDownDataGrid ColumnResized, ColumnReordering and ColumnReordered added 2025-12-04 11:00:07 +02:00
Vladimir Enchev
feaebb6f0f Version updated 2025-12-04 10:52:15 +02:00
yordanov
9a45759414 Add rz-default-scrollbars CSS class to base themes 2025-12-04 10:49:10 +02:00
yordanov
14c5a0447b Remove Black Friday promo banner 2025-12-04 10:37:47 +02:00
yordanov
7793e03d82 Update premium themes 2025-12-03 10:27:13 +02:00
Martin Hans
f422573fcc Add resizable option to side dialog. (#2331)
* Add resizable option to side dialog.

* Fix resize bar CSS class typo and make sideDialogResizeHandleJsModule nullable

* Add tests for resizable side dialog.

* Rename element references.

* Enable nullable refernce types for RadzenDialg.razor.cs

* Resolve requested change

Move code back to RadzenDialog.razor.

* Use sideDialog.classList to gather position rather than data-dir attribute.

* Rework initSideDialogResize function.

- Take position from options.
- Take width and height from sideDialogs clientWidth and clientHeight.
- Take minWidth and minHeight from options.
- Remove superflous pointer capturing.
- Remove superflous 'dragging' class.

* Replace title and aria label string constants by properties.

* Treat resizableMinWidth and resizableMinHeight as present.

* Reformat

* Use options.resizableMinWidth/Height only

* No need to set min-width neither min-height on side dialog.

* Rename ResizableMinWidth => MinWidth, ResizableMinHeight => MinHeight.

* Rename initSideDialogResize => createSideDialogResizer.

* Add dialog resize bar css variables

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-03 10:22:33 +02:00
tharreck
516109ecd3 Correctly select RadzenTocItem based on scroll direction in RadzenToc (#2370)
* Select toc item based on scroll direction in TOC

* Select correct RadzenTocItem when reloading a page with anchor
cleanup code

* add debounce back into scrollhandler

---------

Co-authored-by: Torrelle Steven <steven.torrelle@uzgent.be>
2025-12-02 15:45:10 +02:00
Mason Voxland
31d32b16a5 Clicking SelectAllText in CheckBoxList will do change (#2377) 2025-12-02 11:13:16 +02:00
yordanov
435c1bd6e2 Update Black Friday promo 2025-12-01 10:56:10 +02:00
Vladimir Enchev
6d5ebf3f58 Version updated 2025-12-01 10:55:13 +02:00
Vladimir Enchev
b97036b447 Support large strings added to QRCode
Fix #2373
2025-12-01 10:51:55 +02:00
Vladimir Enchev
d7ef3cb896 Upload file not removed properly when on file delete from UI 2025-11-27 14:10:49 +02:00
Robyn Choi
2218eefa67 RadzenPivotDataGrid AddPivotAggregate exposed (#2371) 2025-11-27 08:14:10 +02:00
Vladimir Enchev
666d33d767 Version updated 2025-11-26 14:00:45 +02:00
Vladimir Enchev
3f4f83d354 PivotDataGrid filter icon popup exception after adding more fields 2025-11-26 08:35:43 +02:00
Atanas Korchev
3dea547559 Enable CSS minification for the embedded themes. 2025-11-25 18:33:46 +02:00
rklfss
0e9c6acb84 Allow Specifying the interactive SortOrder Sequence on Data Grids (#2366) 2025-11-25 15:46:23 +02:00
Vladimir Enchev
1b6881673e AI assistant messages are sent as ‘user’ instead of ‘assistant’ role. ChatMessage Role property added
Fix #2365
2025-11-25 10:17:15 +02:00
yordanov
f069b33b60 Add videos to UI fundamentals demos 2025-11-25 10:03:54 +02:00
Vladimir Enchev
c0a86e31da Version updated 2025-11-24 10:05:17 +02:00
Vladimir Enchev
7b95778efe PivotDataGrid ColumnsCollection, RowsCollection and AggregatesCollection added 2025-11-24 09:45:02 +02:00
Vladimir Enchev
ef8a102d0a DataGrid OData column Type Guid exception on set filter
Fix #2363
2025-11-24 09:18:55 +02:00
Vladimir Enchev
95448de3ed DataGrid column CloseFilter() does not work when FilterProperty has a value 2025-11-21 10:14:08 +02:00
Vladimir Enchev
0a574762c7 RadzenTocItem inherits RadzenComponentWithChildren 2025-11-18 14:10:07 +02:00
joriverm
59b1440990 RadzenTocItem: take in attributes to pass on to the list item element (#2360)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-11-18 13:56:36 +02:00
820 changed files with 40984 additions and 5156 deletions

View File

@@ -11,10 +11,10 @@ assignees: ''
IMPORTANT: Read this first!!!
1. If you own a Radzen Blazor subscription you can report your issue or ask us a question via email at info@radzen.com. Radzen staff will reply within 24 hours (Pro) or 16 hours (Team)
1. If you own a Radzen Blazor Pro or Team subscription you can also report your issue or ask us a question via email at info@radzen.com. Radzen staff will reply within 24 hours (Pro) or 16 hours (Team)
2. The Radzen staff guarantees a response to issues in this repo only to paid subscribers.
3. If you have a HOW TO question start a new forum thread in the Radzen Community forum: https://forum.radzen.com. Radzen staff will close issues that are HOWTO questions.
4. Please adhere to the issue template. Specify all the steps required to reproduce the issue or link a project which reproduces it easily (without requiring extra steps such as restoring a database).
4. Please adhere to the issue template. Specify all the steps required to reproduce the issue.
-->
**Describe the bug**
@@ -27,7 +27,12 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
Alternatively link your repo with a sample project that can be run.
Alternatively make a new [playground](https://blazor.radzen.com/playground) snippet and paste its URL.
1. Go to any live demo at https://blazor.radzen.com
2. Click the **Edit Source** tab.
3. Then click **Open in Playground**.
4. Reproduce the problem and save the snippet.
5. Copy the snippet URL and provide it in the issue description.
**Expected behavior**
A clear and concise description of what you expected to happen.

66
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Deploy to blazor.radzen.com
on:
workflow_dispatch:
concurrency:
group: blazor-radzen-prod
cancel-in-progress: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Compute image tag (timestamp)
id: meta
run: |
TAG=$(date -u +"%Y%m%d%H%M%S")
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.image }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Push to dokku
uses: dokku/github-action@master
with:
git_remote_url: ${{ secrets.DOKKU_REPO }}
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
deploy_docker_image: ${{ steps.meta.outputs.image }}
- name: Prune GHCR versions (keep 1)
uses: actions/delete-package-versions@v5
with:
package-name: radzen-blazor
package-type: container
min-versions-to-keep: 1
delete-only-pre-release-versions: false

85
Directory.Build.props Normal file
View File

@@ -0,0 +1,85 @@
<Project>
<!--
Common build properties for all projects in the Radzen.Blazor solution.
To use this file:
1. Rename to Directory.Build.props (remove .sample extension)
2. Adjust settings based on your needs
3. Review the analyzer settings in .editorconfig
This file will be automatically imported by all projects in subdirectories.
-->
<PropertyGroup Label="Language Configuration">
<!-- Use latest C# language features -->
<LangVersion>latest</LangVersion>
<!-- Do NOT enable implicit usings - explicit imports preferred for library code -->
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>CA2007</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Code Analysis Configuration">
<!-- Enable .NET code analyzers -->
<AnalysisLevel>latest</AnalysisLevel>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<!-- Run analyzers during build and in IDE -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
<!-- Don't enforce code style in build (yet) - just show warnings -->
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<!-- Don't treat warnings as errors (yet) - too many to fix immediately -->
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<!-- Report all analyzer diagnostics -->
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<PropertyGroup Label="Build Quality">
<!-- Enable deterministic builds for reproducibility -->
<Deterministic>true</Deterministic>
<!-- Enable deterministic builds in CI/CD -->
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<!-- Embed source files for better debugging -->
<EmbedAllSources>true</EmbedAllSources>
<!--
IMPORTANT:
- NuGet symbol packages (.snupkg) require portable PDB files.
- If DebugType=embedded, there are no standalone PDBs, so the .snupkg ends up effectively empty.
Use portable PDBs when symbols are enabled; otherwise use embedded for local debugging convenience.
-->
<!--
NOTE: Directory.Build.props is imported before project files, so properties like IncludeSymbols
set in a .csproj may not be available yet for Conditions here.
IsPacking *is* set by `dotnet pack`, so use that to switch DebugType for symbol packages.
-->
<DebugType Condition="'$(IsPacking)' == 'true'">portable</DebugType>
<DebugType Condition="'$(IsPacking)' != 'true'">embedded</DebugType>
</PropertyGroup>
<PropertyGroup Label="Demos and Tests Project Configuration" Condition="$(MSBuildProjectName.Contains('Demos')) OR $(MSBuildProjectName.Contains('Tests'))">
<!-- Demo projects and Tests should not be packable -->
<IsPackable>false</IsPackable>
<!-- DISABLE ALL ANALYZERS FOR DEMO PROJECTS AND TESTS -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
</PropertyGroup>
<PropertyGroup Label="Performance">
<!-- Optimize startup time -->
<TieredCompilation>true</TieredCompilation>
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
</PropertyGroup>
</Project>

View File

@@ -1,23 +1,48 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0
# =============================
# BUILD STAGE
# =============================
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY Radzen.Blazor /app/Radzen.Blazor
COPY Radzen.DocFX /app/Radzen.DocFX
COPY RadzenBlazorDemos /app/RadzenBlazorDemos
COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
# Copy project files first for better caching
COPY Radzen.Blazor/*.csproj Radzen.Blazor/
COPY RadzenBlazorDemos/*.csproj RadzenBlazorDemos/
COPY RadzenBlazorDemos.Host/*.csproj RadzenBlazorDemos.Host/
WORKDIR /app
# Radzen.DocFX usually has no csproj → copy full folder
COPY Radzen.DocFX/ Radzen.DocFX/
# Restore dependencies
RUN dotnet restore RadzenBlazorDemos.Host/RadzenBlazorDemos.Host.csproj
# Copy full source after restore layer
COPY . .
# Install docfx (build stage only)
RUN dotnet tool install -g docfx
ENV PATH="$PATH:/root/.dotnet/tools"
RUN wget https://dot.net/v1/dotnet-install.sh \
&& bash dotnet-install.sh --channel 8.0 --runtime dotnet --install-dir /usr/share/dotnet
# Build shared project (keep net8.0 if required)
RUN dotnet build -c Release Radzen.Blazor/Radzen.Blazor.csproj -f net8.0
# Generate documentation
RUN docfx Radzen.DocFX/docfx.json
WORKDIR /app/RadzenBlazorDemos.Host
RUN dotnet publish -c Release -o out
# Publish the Blazor host app
WORKDIR /src/RadzenBlazorDemos.Host
RUN dotnet publish -c Release -o /app/out
ENV ASPNETCORE_URLS=http://*:5000
WORKDIR /app/RadzenBlazorDemos.Host/out
# =============================
# RUNTIME STAGE
# =============================
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
# Copy only published output
COPY --from=build /app/out ./
# Set runtime URL
ENV ASPNETCORE_URLS=http://+:5000
ENTRYPOINT ["dotnet", "RadzenBlazorDemos.Host.dll"]

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018-2025 Radzen Ltd
Copyright (c) 2018-2026 Radzen Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -54,4 +54,4 @@ Check the [getting started](https://blazor.radzen.com/get-started) instructions
## Run demos locally
Use Radzen.Server.sln to open and run demos as Blazor server application or Radzen.WebAssembly.sln to open and run demos as Blazor WebAssembly application. Radzen.sln has reference to all projects including tests.
Use **Radzen.Server.sln** to open and run demos as Blazor server application or **Radzen.WebAssembly.sln** to open and run demos as Blazor WebAssembly application. The demos require the .NET 10 SDK and should preferably be opened in VS2026.

View File

@@ -1,6 +1,9 @@
using Bunit;
using System.Collections;
using Bunit;
using Xunit;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Radzen.Blazor.Tests
{
@@ -135,5 +138,30 @@ namespace Radzen.Blazor.Tests
Assert.Equal("additional-name", AutoCompleteType.MiddleName.GetAutoCompleteValue());
Assert.Equal("family-name", AutoCompleteType.LastName.GetAutoCompleteValue());
}
[Fact]
public void AutoComplete_Filters_StringList()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var data = new List<string> { "Apple", "Banana", "Cherry" };
var component = ctx.RenderComponent<AutoCompleteWithAccessibleView>(parameters =>
{
parameters
.Add(p => p.Data, data)
.Add(p => p.SearchText, "Ban")
.Add(p => p.OpenOnFocus, true);
});
Assert.Contains("Banana", component.Instance.CurrentView.OfType<string>());
Assert.DoesNotContain("Apple", component.Instance.CurrentView.OfType<string>());
Assert.DoesNotContain("Cherry", component.Instance.CurrentView.OfType<string>());
}
private sealed class AutoCompleteWithAccessibleView : RadzenAutoComplete
{
public IEnumerable CurrentView => View;
}
}
}

View File

@@ -1728,6 +1728,7 @@ namespace Radzen.Blazor.Tests
{
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
builder.AddAttribute(1, "Property", "Name");
builder.AddAttribute(2, "Type", typeof(string));
builder.CloseComponent();
});
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
@@ -1751,6 +1752,7 @@ namespace Radzen.Blazor.Tests
{
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
builder.AddAttribute(1, "Property", "Name");
builder.AddAttribute(1, "Type", typeof(string));
builder.CloseComponent();
});
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
@@ -2804,6 +2806,12 @@ namespace Radzen.Blazor.Tests
builder.AddAttribute(1, "Property", "Id");
builder.AddAttribute(2, "Title", "Id");
builder.CloseComponent();
builder.OpenComponent(3, typeof(RadzenDataGridColumn<dynamic>));
builder.AddAttribute(4, "Property", "Tags");
builder.AddAttribute(5, "Title", "Tags");
builder.AddAttribute(6, "Type", typeof(object[]));
builder.CloseComponent();
});
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
parameterBuilder.Add<FilterMode>(p => p.FilterMode, FilterMode.SimpleWithMenu);
@@ -2834,6 +2842,12 @@ namespace Radzen.Blazor.Tests
builder.AddAttribute(1, "Property", "Id");
builder.AddAttribute(2, "Title", "Id");
builder.CloseComponent();
builder.OpenComponent(3, typeof(RadzenDataGridColumn<dynamic>));
builder.AddAttribute(4, "Property", "Tags");
builder.AddAttribute(5, "Title", "Tags");
builder.AddAttribute(6, "Type", typeof(object[]));
builder.CloseComponent();
});
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
parameterBuilder.Add<FilterMode>(p => p.FilterMode, FilterMode.SimpleWithMenu);

View File

@@ -1,11 +1,15 @@
using System;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class DialogServiceTests
public class DialogServiceTests : ComponentBase
{
public class OpenDialogTests
{
@@ -124,13 +128,81 @@ namespace Radzen.Blazor.Tests
var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []);
dialogService.Close();
await openTask;
// Assert
Assert.Equal("Dynamic Open", resultingTitle);
Assert.Equal(typeof(RadzenButton), resultingType);
}
}
public class OpenSideDialogTests
{
[Fact(DisplayName = "SideDialogOptions resizable option is retained after OpenSideDialog call")]
public void SideDialogOptions_Resizable_AreRetained_AfterOpenSideDialogCall()
{
// Arrange
var options = new SideDialogOptions { Resizable = true };
SideDialogOptions resultingOptions = null;
var dialogService = new DialogService(null, null);
dialogService.OnSideOpen += (_, _, sideOptions) => resultingOptions = sideOptions;
// Act
dialogService.OpenSide<DialogServiceTests>("Test", [], options);
// Assert
Assert.NotNull(resultingOptions);
Assert.Same(options, resultingOptions);
Assert.True(resultingOptions.Resizable);
}
[Fact(DisplayName = "Side dialog shows resize bar when Resizable is true")]
public void SideDialog_Resizable_ShowsResizeBar()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddScoped<DialogService>();
// Render the dialog host
var cut = ctx.RenderComponent<RadzenDialog>();
// Open a side dialog with Resizable=true
var dialogService = ctx.Services.GetRequiredService<DialogService>();
cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton),
new Dictionary<string, object>(), new SideDialogOptions { Resizable = true }));
// Assert: the resize bar element is present
cut.WaitForAssertion(() =>
{
var markup = cut.Markup;
Assert.Contains("rz-dialog-resize-bar", markup);
// Optionally ensure the inner handle exists too
Assert.Contains("rz-resize", markup);
});
}
[Fact(DisplayName = "Side dialog hides resize bar when Resizable is false")]
public void SideDialog_NonResizable_HidesResizeBar()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddScoped<DialogService>();
// Render the dialog host
var cut = ctx.RenderComponent<RadzenDialog>();
// Open a side dialog with Resizable=false
var dialogService = ctx.Services.GetRequiredService<DialogService>();
cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton),
new Dictionary<string, object>(), new SideDialogOptions()));
// Assert: the resize bar element is not present
cut.WaitForAssertion(() =>
{
var markup = cut.Markup;
Assert.DoesNotContain("rz-dialog-resize-bar", markup);
});
}
}
public class ConfirmTests
{
[Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")]

View File

@@ -22,7 +22,7 @@ namespace Radzen.Blazor.Tests
public Guid Id { get; set; }
public TimeOnly StartTime { get; set; }
public DateOnly BirthDate { get; set; }
public List<int> Scores { get; set; }
public IEnumerable<int> Scores { get; set; }
public List<string> Tags { get; set; }
public List<TestEntity> Children { get; set; }
public Address Address { get; set; }

View File

@@ -141,4 +141,28 @@ foo <!---> foo -->
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData("***foo bar***", @"<document>
<paragraph>
<emph>
<strong>
<text>foo bar</text>
</strong>
</emph>
</paragraph>
</document>")]
[InlineData("___foo bar___", @"<document>
<paragraph>
<emph>
<strong>
<text>foo bar</text>
</strong>
</emph>
</paragraph>
</document>")]
public void Parse_BoldAndItalic_ShouldNotThrowException(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -1,6 +1,7 @@
using Bunit;
using Xunit;
using System.Collections.Generic;
using System.Linq;
namespace Radzen.Blazor.Tests
{
@@ -156,6 +157,64 @@ namespace Radzen.Blazor.Tests
Assert.Contains("disabled", component.Markup);
}
[Fact]
public void PickList_GetSelectedSources_Respects_ValueProperty_Single()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var data = new List<Item>
{
new Item { Id = 1, Name = "A" },
new Item { Id = 2, Name = "B" }
};
var component = ctx.RenderComponent<RadzenPickList<Item>>(parameters =>
{
parameters.Add(p => p.Source, data);
parameters.Add(p => p.TextProperty, "Name");
parameters.Add(p => p.ValueProperty, "Id");
parameters.Add(p => p.Multiple, false);
});
// Simulate ListBox selection when ValueProperty is set: selectedSourceItems becomes the value (Id)
var field = typeof(RadzenPickList<Item>).GetField("selectedSourceItems", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
field.SetValue(component.Instance, 2);
var selected = component.Instance.GetSelectedSources();
Assert.Single(selected);
Assert.Equal(2, selected.First().Id);
}
[Fact]
public void PickList_GetSelectedSources_Respects_ValueProperty_Multiple()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var data = new List<Item>
{
new Item { Id = 1, Name = "A" },
new Item { Id = 2, Name = "B" },
new Item { Id = 3, Name = "C" }
};
var component = ctx.RenderComponent<RadzenPickList<Item>>(parameters =>
{
parameters.Add(p => p.Source, data);
parameters.Add(p => p.TextProperty, "Name");
parameters.Add(p => p.ValueProperty, "Id");
parameters.Add(p => p.Multiple, true);
});
// Simulate ListBox selection when ValueProperty is set: selectedSourceItems becomes IEnumerable of values (Ids)
var field = typeof(RadzenPickList<Item>).GetField("selectedSourceItems", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
field.SetValue(component.Instance, new[] { 1, 3 });
var selected = component.Instance.GetSelectedSources().Select(i => i.Id).OrderBy(i => i).ToArray();
Assert.Equal(new[] { 1, 3 }, selected);
}
}
}

View File

@@ -605,7 +605,7 @@ namespace Radzen.Blazor.Tests
Assert.True(grid.AllowFieldsPicking);
}
private static IRenderedComponent<RadzenPivotDataGrid<SalesData>> RenderPivotDataGrid(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenPivotDataGrid<SalesData>>>? configure = null)
private static IRenderedComponent<RadzenPivotDataGrid<SalesData>> RenderPivotDataGrid(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenPivotDataGrid<SalesData>>> configure = null)
{
return ctx.RenderComponent<RadzenPivotDataGrid<SalesData>>(parameters =>
{

View File

@@ -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);
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>disable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@@ -0,0 +1,63 @@
using Bunit;
using Microsoft.JSInterop;
using Microsoft.Extensions.DependencyInjection;
using Radzen.Blazor;
using Radzen.Blazor.Rendering;
using Radzen;
using System;
using System.Collections.Generic;
using System.Globalization;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class SchedulerYearRangeTests
{
class Appointment
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public string Text { get; set; } = "";
}
[Fact]
public void YearView_StartMonthJanuary_IncludesLastDaysOfYear_WhenYearStartsOnFirstDayOfWeek()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddScoped<DialogService>();
ctx.JSInterop.Setup<Rect>("Radzen.createScheduler", _ => true)
.SetResult(new Rect { Left = 0, Top = 0, Width = 200, Height = 200 });
// Make the first day of week Monday and use a year where Jan 1 is Monday (2024-01-01).
var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone();
culture.DateTimeFormat.FirstDayOfWeek = DayOfWeek.Monday;
var appointments = new List<Appointment>
{
new() { Start = new DateTime(2024, 12, 31), End = new DateTime(2025, 1, 1), Text = "Year end" }
};
var cut = ctx.RenderComponent<RadzenScheduler<Appointment>>(p =>
{
p.Add(x => x.Culture, culture);
p.Add(x => x.Date, new DateTime(2024, 6, 1));
p.Add(x => x.Data, appointments);
p.Add(x => x.StartProperty, nameof(Appointment.Start));
p.Add(x => x.EndProperty, nameof(Appointment.End));
p.Add(x => x.TextProperty, nameof(Appointment.Text));
p.AddChildContent<RadzenYearView>(v => v.Add(x => x.StartMonth, Radzen.Month.January));
});
var view = Assert.IsType<RadzenYearView>(cut.Instance.SelectedView);
// View should start on 2023-12-25 (one extra week above since 2024-01-01 is Monday).
Assert.Equal(new DateTime(2023, 12, 25), view.StartDate.Date);
// View end must include 2024-12-31 (it should extend to end-of-week containing the real year end).
Assert.True(view.EndDate.Date >= new DateTime(2024, 12, 31), $"EndDate was {view.EndDate:yyyy-MM-dd}");
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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));
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

File diff suppressed because it is too large Load Diff

View 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>());
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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>());
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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));
}
}

View 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);
}
}

View 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>());
}
}

View 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>());
}
}

View 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>());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>());
}
}

View 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>());
}
}

View 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);
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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)
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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>());
}
}

View 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>());
}
}

View 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);
}
}

View 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>());
}
}

View File

@@ -0,0 +1,27 @@
using Bunit;
using System.Collections.Generic;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class TocTests
{
[Fact]
public void TocItem_Renders_With_Attributes()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTocItem>(parameters =>
{
parameters.Add(p => p.Attributes, new Dictionary<string, object>
{
{ "data-enhance-nav", "false" },
{ "aria-label", "Table of Contents Item" }
});
});
Assert.Contains("data-enhance-nav=\"false\"", component.Markup);
Assert.Contains("aria-label=\"Table of Contents Item\"", component.Markup);
}
}
}

Some files were not shown because too many files have changed in this diff Show More