ironcalc_base 0.7.1

Open source spreadsheet engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
#![allow(clippy::unwrap_used)]

/// Here we add tests that cannot be done in Excel
/// Either because Excel does not have that feature (i.e. wrong number of arguments)
/// or because we differ from Excel throwing #NUM! on invalid dates
/// We can also enter examples that illustrate/document a part of the function
use crate::{cell::CellValue, test::util::new_empty_model};

// Excel uses a serial date system where Jan 1, 1900 = 1 (though it treats 1900 as a leap year)
// Most test dates are documented inline, but we define boundary values here:
const EXCEL_MAX_DATE: f64 = 2958465.0; // Dec 31, 9999 - used in boundary tests
const EXCEL_INVALID_DATE: f64 = 2958466.0; // One day past max - used in error tests

#[test]
fn test_fn_date_arguments() {
    let mut model = new_empty_model();

    // Wrong number of arguments produce #ERROR!
    // NB: Excel does not have this error, but does not let you enter wrong number of arguments in the UI
    model._set("A1", "=DATE()");
    model._set("A2", "=DATE(1975)");
    model._set("A3", "=DATE(1975, 2)");
    model._set("A4", "=DATE(1975, 2, 10, 3)");

    // Arguments are out of rage. This is against Excel
    // Excel will actually compute a date by continuing to the next month, year...
    // We throw #NUM!
    model._set("A5", "=DATE(1975, -2, 10)");
    model._set("A6", "=DATE(1975, 2, -10)");
    model._set("A7", "=DATE(1975, 14, 10)");
    // February doesn't have 30 days
    model._set("A8", "=DATE(1975, 2, 30)");

    // 1975, a great year, wasn't a leap year
    model._set("A9", "=DATE(1975, 2, 29)");
    // 1976 was
    model._set("A10", "=DATE(1976, 2, 29)");
    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#ERROR!");
    assert_eq!(model._get_text("A2"), *"#ERROR!");
    assert_eq!(model._get_text("A3"), *"#ERROR!");
    assert_eq!(model._get_text("A4"), *"#ERROR!");

    assert_eq!(model._get_text("A5"), *"10/10/1974");
    assert_eq!(model._get_text("A6"), *"1/21/1975");
    assert_eq!(model._get_text("A7"), *"2/10/1976");
    assert_eq!(model._get_text("A8"), *"3/2/1975");

    assert_eq!(model._get_text("A9"), *"3/1/1975");
    assert_eq!(model._get_text("A10"), *"2/29/1976");
    assert_eq!(
        model.get_cell_value_by_ref("Sheet1!A10"),
        Ok(CellValue::Number(27819.0))
    );
}

#[test]
fn test_date_out_of_range() {
    let mut model = new_empty_model();

    // month
    model._set("A1", "=DATE(2022, 0, 10)");
    model._set("A2", "=DATE(2022, 13, 10)");

    // day
    model._set("B1", "=DATE(2042, 5, 0)");
    model._set("B2", "=DATE(2025, 5, 32)");

    // year (actually years < 1900 don't really make sense)
    model._set("C1", "=DATE(-1, 5, 5)");
    // excel is not compatible with years past 9999
    model._set("C2", "=DATE(10000, 5, 5)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"12/10/2021");
    assert_eq!(model._get_text("A2"), *"1/10/2023");
    assert_eq!(model._get_text("B1"), *"4/30/2042");
    assert_eq!(model._get_text("B2"), *"6/1/2025");

    assert_eq!(model._get_text("C1"), *"#NUM!");
    assert_eq!(model._get_text("C2"), *"#NUM!");
}

#[test]
fn test_year_arguments() {
    let mut model = new_empty_model();
    model._set("A1", "=YEAR()");
    model._set("A2", "=YEAR(27819)");
    model._set("A3", "=YEAR(27819, 3)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#ERROR!");
    assert_eq!(model._get_text("A2"), *"1976");
    assert_eq!(model._get_text("A3"), *"#ERROR!");
}

#[test]
fn test_month_arguments() {
    let mut model = new_empty_model();
    model._set("A1", "=MONTH()");
    model._set("A2", "=MONTH(27819)");
    model._set("A3", "=MONTH(27819, 3)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#ERROR!");
    assert_eq!(model._get_text("A2"), *"2");
    assert_eq!(model._get_text("A3"), *"#ERROR!");
}

#[test]
fn test_day_arguments() {
    let mut model = new_empty_model();
    model._set("A1", "=DAY()");
    model._set("A2", "=DAY(27819)");
    model._set("A3", "=DAY(27819, 3)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#ERROR!");
    assert_eq!(model._get_text("A2"), *"29");
    assert_eq!(model._get_text("A3"), *"#ERROR!");
}

#[test]
fn test_day_small_serial() {
    let mut model = new_empty_model();
    model._set("A1", "=DAY(-1)");
    model._set("A2", "=DAY(0)");
    model._set("A3", "=DAY(60)");

    model._set("A4", "=DAY(61)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#NUM!");
    assert_eq!(model._get_text("A2"), *"#NUM!");
    // Excel thinks is Feb 29, 1900
    assert_eq!(model._get_text("A3"), *"28");

    // From now on everyone agrees
    assert_eq!(model._get_text("A4"), *"1");
}

#[test]
fn test_month_small_serial() {
    let mut model = new_empty_model();
    model._set("A1", "=MONTH(-1)");
    model._set("A2", "=MONTH(0)");
    model._set("A3", "=MONTH(60)");

    model._set("A4", "=MONTH(61)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#NUM!");
    assert_eq!(model._get_text("A2"), *"#NUM!");
    // We agree with Excel here (We are both in Feb)
    assert_eq!(model._get_text("A3"), *"2");

    // Same as Excel
    assert_eq!(model._get_text("A4"), *"3");
}

#[test]
fn test_year_small_serial() {
    let mut model = new_empty_model();
    model._set("A1", "=YEAR(-1)");
    model._set("A2", "=YEAR(0)");
    model._set("A3", "=YEAR(60)");

    model._set("A4", "=YEAR(61)");

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"#NUM!");
    assert_eq!(model._get_text("A2"), *"#NUM!");

    assert_eq!(model._get_text("A3"), *"1900");

    // Same as Excel
    assert_eq!(model._get_text("A4"), *"1900");
}

#[test]
fn test_date_early_dates() {
    let mut model = new_empty_model();
    model._set("A1", "=DATE(1900, 1, 1)");
    model._set("A2", "=DATE(1900, 2, 28)");
    model._set("B2", "=DATE(1900, 2, 29)");
    model._set("A3", "=DATE(1900, 3, 1)");

    model.evaluate();

    // This is 1 in Excel, we agree with Google Docs
    assert_eq!(model._get_text("A1"), *"1/1/1900");
    assert_eq!(
        model.get_cell_value_by_ref("Sheet1!A1"),
        Ok(CellValue::Number(2.0))
    );

    // 1900 was not a leap year, this is a bug in EXCEL
    // This would be 60 in Excel
    assert_eq!(model._get_text("A2"), *"2/28/1900");
    assert_eq!(
        model.get_cell_value_by_ref("Sheet1!A2"),
        Ok(CellValue::Number(60.0))
    );

    // This does not agree with Excel, instead of mistakenly allowing
    // for Feb 29, it will auto-wrap to the next day after Feb 28.
    assert_eq!(model._get_text("B2"), *"3/1/1900");

    // This agrees with Excel from he onward
    assert_eq!(model._get_text("A3"), *"3/1/1900");
    assert_eq!(
        model.get_cell_value_by_ref("Sheet1!A3"),
        Ok(CellValue::Number(61.0))
    );
}
#[test]
fn test_days_function() {
    let mut model = new_empty_model();

    // Basic functionality
    model._set("A1", "=DAYS(44570,44561)");
    model._set("A2", "=DAYS(44561,44570)"); // Reversed order
    model._set("A3", "=DAYS(44561,44561)");

    // Edge cases
    model._set("A4", "=DAYS(1,2)"); // Early dates
    model._set(
        "A5",
        &format!("=DAYS({},{})", EXCEL_MAX_DATE, EXCEL_MAX_DATE - 1.0),
    ); // Near max date

    // Error cases - wrong argument count
    model._set("A6", "=DAYS()");
    model._set("A7", "=DAYS(44561)");
    model._set("A8", "=DAYS(44561,44570,1)");

    // Error cases - invalid dates
    model._set("A9", "=DAYS(-1,44561)");
    model._set("A10", &format!("=DAYS(44561,{EXCEL_INVALID_DATE})"));

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"9");
    assert_eq!(model._get_text("A2"), *"-9");
    assert_eq!(model._get_text("A3"), *"0");
    assert_eq!(model._get_text("A4"), *"-1"); // DAYS(1,2) = 1-2 = -1
    assert_eq!(model._get_text("A5"), *"1");
    assert_eq!(model._get_text("A6"), *"#ERROR!");
    assert_eq!(model._get_text("A7"), *"#ERROR!");
    assert_eq!(model._get_text("A8"), *"#ERROR!");
    assert_eq!(model._get_text("A9"), *"#NUM!");
    assert_eq!(model._get_text("A10"), *"#NUM!");
}

#[test]
fn test_days360_function() {
    let mut model = new_empty_model();

    // Basic functionality with different basis values
    model._set("A1", "=DAYS360(44196,44560)"); // Default basis (US 30/360)
    model._set("A2", "=DAYS360(44196,44560,FALSE)"); // US 30/360 explicitly
    model._set("A3", "=DAYS360(44196,44560,TRUE)"); // European 30/360

    // Same date
    model._set("A4", "=DAYS360(44561,44561)");
    model._set("A5", "=DAYS360(44561,44561,TRUE)");

    // Reverse order (negative result)
    model._set("A6", "=DAYS360(44560,44196)");
    model._set("A7", "=DAYS360(44560,44196,TRUE)");

    // Edge cases
    model._set("A8", "=DAYS360(1,2)");
    model._set("A9", "=DAYS360(1,2,FALSE)");

    // Error cases - wrong argument count
    model._set("A10", "=DAYS360()");
    model._set("A11", "=DAYS360(44561)");
    model._set("A12", "=DAYS360(44561,44570,TRUE,1)");

    // Error cases - invalid dates
    model._set("A13", "=DAYS360(-1,44561)");
    model._set("A14", &format!("=DAYS360(44561,{EXCEL_INVALID_DATE})"));

    model.evaluate();

    assert_eq!(model._get_text("A1"), *"360");
    assert_eq!(model._get_text("A2"), *"360");
    assert_eq!(model._get_text("A3"), *"360");
    assert_eq!(model._get_text("A4"), *"0");
    assert_eq!(model._get_text("A5"), *"0");
    assert_eq!(model._get_text("A6"), *"-360");
    assert_eq!(model._get_text("A7"), *"-360");
    assert_eq!(model._get_text("A8"), *"1");
    assert_eq!(model._get_text("A9"), *"1");
    assert_eq!(model._get_text("A10"), *"#ERROR!");
    assert_eq!(model._get_text("A11"), *"#ERROR!");
    assert_eq!(model._get_text("A12"), *"#ERROR!");
    assert_eq!(model._get_text("A13"), *"#NUM!");
    assert_eq!(model._get_text("A14"), *"#NUM!");
}

#[test]
fn test_weekday_function() {
    let mut model = new_empty_model();

    // Test return_type parameter variations with one known date (Friday 44561)
    model._set("A1", "=WEEKDAY(44561)"); // Default: Sun=1, Fri=6
    model._set("A2", "=WEEKDAY(44561,2)"); // Mon=1, Fri=5
    model._set("A3", "=WEEKDAY(44561,3)"); // Mon=0, Fri=4

    // Test boundary days (Sun/Mon) to verify return_type logic
    model._set("A4", "=WEEKDAY(44556,1)"); // Sunday: should be 1
    model._set("A5", "=WEEKDAY(44556,2)"); // Sunday: should be 7
    model._set("A6", "=WEEKDAY(44557,2)"); // Monday: should be 1

    // Error cases
    model._set("A7", "=WEEKDAY()"); // Wrong arg count
    model._set("A8", "=WEEKDAY(44561,0)"); // Invalid return_type
    model._set("A9", "=WEEKDAY(-1)"); // Invalid date

    model.evaluate();

    // Core functionality
    assert_eq!(model._get_text("A1"), *"6"); // Friday default
    assert_eq!(model._get_text("A2"), *"5"); // Friday Mon=1
    assert_eq!(model._get_text("A3"), *"4"); // Friday Mon=0

    // Boundary verification
    assert_eq!(model._get_text("A4"), *"1"); // Sunday Sun=1
    assert_eq!(model._get_text("A5"), *"7"); // Sunday Mon=1
    assert_eq!(model._get_text("A6"), *"1"); // Monday Mon=1

    // Error cases
    assert_eq!(model._get_text("A7"), *"#ERROR!");
    assert_eq!(model._get_text("A8"), *"#VALUE!");
    assert_eq!(model._get_text("A9"), *"#NUM!");
}

#[test]
fn test_weeknum_function() {
    let mut model = new_empty_model();

    // Test different return_type values (1=week starts Sunday, 2=week starts Monday)
    model._set("A1", "=WEEKNUM(44561)"); // Default return_type=1
    model._set("A2", "=WEEKNUM(44561,1)"); // Sunday start
    model._set("A3", "=WEEKNUM(44561,2)"); // Monday start

    // Test year boundaries
    model._set("A4", "=WEEKNUM(43831,1)"); // Jan 1, 2020 (Wednesday)
    model._set("A5", "=WEEKNUM(43831,2)"); // Jan 1, 2020 (Wednesday)
    model._set("A6", "=WEEKNUM(44196,1)"); // Dec 31, 2020 (Thursday)
    model._set("A7", "=WEEKNUM(44196,2)"); // Dec 31, 2020 (Thursday)

    // Test first and last weeks of year
    model._set("A8", "=WEEKNUM(44197,1)"); // Jan 1, 2021 (Friday)
    model._set("A9", "=WEEKNUM(44197,2)"); // Jan 1, 2021 (Friday)
    model._set("A10", "=WEEKNUM(44561,1)"); // Dec 31, 2021 (Friday)
    model._set("A11", "=WEEKNUM(44561,2)"); // Dec 31, 2021 (Friday)

    // Error cases - wrong argument count
    model._set("A12", "=WEEKNUM()");
    model._set("A13", "=WEEKNUM(44561,1,1)");

    // Error cases - invalid return_type
    model._set("A14", "=WEEKNUM(44561,0)");
    model._set("A15", "=WEEKNUM(44561,3)");
    model._set("A16", "=WEEKNUM(44561,-1)");

    // Error cases - invalid dates
    model._set("A17", "=WEEKNUM(-1)");
    model._set("A18", &format!("=WEEKNUM({EXCEL_INVALID_DATE})"));

    model.evaluate();

    // Basic functionality
    assert_eq!(model._get_text("A1"), *"53"); // Week 53
    assert_eq!(model._get_text("A2"), *"53"); // Week 53 (Sunday start)
    assert_eq!(model._get_text("A3"), *"53"); // Week 53 (Monday start)

    // Year boundary tests
    assert_eq!(model._get_text("A4"), *"1"); // Jan 1, 2020 (Sunday start)
    assert_eq!(model._get_text("A5"), *"1"); // Jan 1, 2020 (Monday start)
    assert_eq!(model._get_text("A6"), *"53"); // Dec 31, 2020 (Sunday start)
    assert_eq!(model._get_text("A7"), *"53"); // Dec 31, 2020 (Monday start)

    // 2021 tests
    assert_eq!(model._get_text("A8"), *"1"); // Jan 1, 2021 (Sunday start)
    assert_eq!(model._get_text("A9"), *"1"); // Jan 1, 2021 (Monday start)
    assert_eq!(model._get_text("A10"), *"53"); // Dec 31, 2021 (Sunday start)
    assert_eq!(model._get_text("A11"), *"53"); // Dec 31, 2021 (Monday start)

    // Error cases
    assert_eq!(model._get_text("A12"), *"#ERROR!");
    assert_eq!(model._get_text("A13"), *"#ERROR!");
    assert_eq!(model._get_text("A14"), *"#VALUE!");
    assert_eq!(model._get_text("A15"), *"#VALUE!");
    assert_eq!(model._get_text("A16"), *"#VALUE!");
    assert_eq!(model._get_text("A17"), *"#NUM!");
    assert_eq!(model._get_text("A18"), *"#NUM!");
}

#[test]
fn test_workday_function() {
    let mut model = new_empty_model();

    // Basic functionality
    model._set("A1", "=WORKDAY(44560,1)");
    model._set("A2", "=WORKDAY(44561,-1)");
    model._set("A3", "=WORKDAY(44561,0)");
    model._set("A4", "=WORKDAY(44560,5)");

    // Test with holidays
    model._set("B1", "44561");
    model._set("A5", "=WORKDAY(44560,1,B1)"); // Should skip the holiday
    model._set("B2", "44562");
    model._set("B3", "44563");
    model._set("A6", "=WORKDAY(44560,3,B1:B3)"); // Multiple holidays

    // Test starting on weekend
    model._set("A7", "=WORKDAY(44562,1)"); // Saturday start
    model._set("A8", "=WORKDAY(44563,1)"); // Sunday start

    // Test negative workdays
    model._set("A9", "=WORKDAY(44565,-3)"); // Go backwards 3 days
    model._set("A10", "=WORKDAY(44565,-5,B1:B3)"); // Backwards with holidays

    // Edge cases
    model._set("A11", "=WORKDAY(1,1)"); // Early date
    model._set("A12", "=WORKDAY(100000,10)"); // Large numbers

    // Error cases - wrong argument count
    model._set("A13", "=WORKDAY()");
    model._set("A14", "=WORKDAY(44560)");
    model._set("A15", "=WORKDAY(44560,1,B1,B2)");

    // Error cases - invalid dates
    model._set("A16", "=WORKDAY(-1,1)");
    model._set("A17", &format!("=WORKDAY({EXCEL_INVALID_DATE},1)"));

    // Error cases - invalid holiday dates
    model._set("B4", "-1");
    model._set("A18", "=WORKDAY(44560,1,B4)");

    model.evaluate();

    // Basic functionality
    assert_eq!(model._get_text("A1"), *"44561"); // 1 day forward
    assert_eq!(model._get_text("A2"), *"44560"); // 1 day backward
    assert_eq!(model._get_text("A3"), *"44561"); // 0 days
    assert_eq!(model._get_text("A4"), *"44567"); // 5 days forward

    // With holidays
    assert_eq!(model._get_text("A5"), *"44564"); // Skip holiday, go to Monday
    assert_eq!(model._get_text("A6"), *"44566"); // Skip multiple holidays

    // Weekend starts
    assert_eq!(model._get_text("A7"), *"44564"); // From Saturday
    assert_eq!(model._get_text("A8"), *"44564"); // From Sunday

    // Negative workdays
    assert_eq!(model._get_text("A9"), *"44560"); // 3 days back
    assert_eq!(model._get_text("A10"), *"44557"); // 5 days back with holidays

    // Edge cases
    assert_eq!(model._get_text("A11"), *"2"); // Early date
    assert_eq!(model._get_text("A12"), *"100014"); // Large numbers

    // Error cases
    assert_eq!(model._get_text("A13"), *"#ERROR!");
    assert_eq!(model._get_text("A14"), *"#ERROR!");
    assert_eq!(model._get_text("A15"), *"#ERROR!");
    assert_eq!(model._get_text("A16"), *"#NUM!");
    assert_eq!(model._get_text("A17"), *"#NUM!");
    assert_eq!(model._get_text("A18"), *"#NUM!"); // Invalid holiday
}

#[test]
fn test_workday_intl_function() {
    let mut model = new_empty_model();

    // Test key weekend mask types
    model._set("A1", "=WORKDAY.INTL(44560,1,1)"); // Numeric: standard (Sat-Sun)
    model._set("A2", "=WORKDAY.INTL(44560,1,2)"); // Numeric: Sun-Mon
    model._set("A3", "=WORKDAY.INTL(44560,1,\"0000001\")"); // String: Sunday only
    model._set("A4", "=WORKDAY.INTL(44560,1,\"1100000\")"); // String: Mon-Tue

    // Test with holidays
    model._set("B1", "44561");
    model._set("A5", "=WORKDAY.INTL(44560,2,1,B1)"); // Standard + holiday
    model._set("A6", "=WORKDAY.INTL(44560,2,7,B1)"); // Fri-Sat + holiday

    // Basic edge cases
    model._set("A7", "=WORKDAY.INTL(44561,0,1)"); // Zero days
    model._set("A8", "=WORKDAY.INTL(44565,-1,1)"); // Negative days

    // Error cases
    model._set("A9", "=WORKDAY.INTL()"); // Wrong arg count
    model._set("A10", "=WORKDAY.INTL(44560,1,0)"); // Invalid weekend mask
    model._set("A11", "=WORKDAY.INTL(44560,1,\"123\")"); // Invalid string mask
    model._set("A12", "=WORKDAY.INTL(-1,1,1)"); // Invalid date

    model.evaluate();

    // Weekend mask functionality
    assert_eq!(model._get_text("A1"), *"44561"); // Standard weekend
    assert_eq!(model._get_text("A2"), *"44561"); // Sun-Mon weekend
    assert_eq!(model._get_text("A3"), *"44561"); // Sunday only
    assert_eq!(model._get_text("A4"), *"44561"); // Mon-Tue weekend

    // With holidays
    assert_eq!(model._get_text("A5"), *"44565"); // Skip holiday + standard weekend
    assert_eq!(model._get_text("A6"), *"44564"); // Skip holiday + Fri-Sat weekend

    // Edge cases
    assert_eq!(model._get_text("A7"), *"44561"); // Zero days
    assert_eq!(model._get_text("A8"), *"44564"); // Negative days

    // Error cases
    assert_eq!(model._get_text("A9"), *"#ERROR!");
    assert_eq!(model._get_text("A10"), *"#NUM!");
    assert_eq!(model._get_text("A11"), *"#VALUE!");
    assert_eq!(model._get_text("A12"), *"#NUM!");
}

#[test]
fn test_yearfrac_function() {
    let mut model = new_empty_model();

    // Test key basis values (not exhaustive - just verify parameter works)
    model._set("A1", "=YEARFRAC(44561,44926)"); // Default (30/360)
    model._set("A2", "=YEARFRAC(44561,44926,1)"); // Actual/actual
    model._set("A3", "=YEARFRAC(44561,44926,4)"); // European 30/360

    // Edge cases
    model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
    model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)

    // Error cases
    model._set("A7", "=YEARFRAC()"); // Wrong arg count
    model._set("A8", "=YEARFRAC(44561,44926,5)"); // Invalid basis
    model._set("A9", "=YEARFRAC(-1,44926,1)"); // Invalid date

    model.evaluate();

    // Basic functionality (approximate values expected)
    assert_eq!(model._get_text("A1"), *"1"); // About 1 year
    assert_eq!(model._get_text("A2"), *"1"); // About 1 year
    assert_eq!(model._get_text("A3"), *"1"); // About 1 year

    // Edge cases
    assert_eq!(model._get_text("A4"), *"0"); // Same date
    assert_eq!(model._get_text("A6"), *"1"); // Exact year

    // Error cases
    assert_eq!(model._get_text("A7"), *"#ERROR!");
    assert_eq!(model._get_text("A8"), *"#NUM!"); // Invalid basis should return #NUM!
    assert_eq!(model._get_text("A9"), *"#NUM!");
}

#[test]
fn test_isoweeknum_function() {
    let mut model = new_empty_model();

    // Basic functionality
    model._set("A1", "=ISOWEEKNUM(44563)"); // Mid-week date
    model._set("A2", "=ISOWEEKNUM(44561)"); // Year-end date

    // Key ISO week boundaries (just critical cases)
    model._set("A3", "=ISOWEEKNUM(44197)"); // Jan 1, 2021 (Fri) -> Week 53 of 2020
    model._set("A4", "=ISOWEEKNUM(44200)"); // Jan 4, 2021 (Mon) -> Week 1 of 2021
    model._set("A5", "=ISOWEEKNUM(44564)"); // Jan 3, 2022 (Mon) -> Week 1 of 2022

    // Error cases
    model._set("A6", "=ISOWEEKNUM()"); // Wrong arg count
    model._set("A7", "=ISOWEEKNUM(-1)"); // Invalid date

    model.evaluate();

    // Basic functionality
    assert_eq!(model._get_text("A1"), *"52");
    assert_eq!(model._get_text("A2"), *"52");

    // ISO week boundaries
    assert_eq!(model._get_text("A3"), *"53"); // Week 53 of previous year
    assert_eq!(model._get_text("A4"), *"1"); // Week 1 of current year
    assert_eq!(model._get_text("A5"), *"1"); // Week 1 of next year

    // Error cases
    assert_eq!(model._get_text("A6"), *"#ERROR!");
    assert_eq!(model._get_text("A7"), *"#NUM!");
}