using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Radzen.Documents.Spreadsheet;
#nullable enable
///
/// Specifies the type of data validation to apply.
///
public enum DataValidationType
{
/// Whole number validation.
WholeNumber,
/// Decimal number validation.
Decimal,
/// List validation with predefined values.
List,
/// Date validation.
Date,
/// Time validation.
Time,
/// Text length validation.
TextLength,
/// Custom formula validation.
Custom
}
///
/// Specifies the comparison operator for data validation.
///
public enum DataValidationOperator
{
/// Value must be between two values.
Between,
/// Value must not be between two values.
NotBetween,
/// Value must be equal to a value.
Equal,
/// Value must not be equal to a value.
NotEqual,
/// Value must be greater than a value.
GreaterThan,
/// Value must be less than a value.
LessThan,
/// Value must be greater than or equal to a value.
GreaterThanOrEqual,
/// Value must be less than or equal to a value.
LessThanOrEqual
}
///
/// Specifies the error style for data validation.
///
public enum DataValidationErrorStyle
{
/// Stop - prevents entry of invalid data.
Stop,
/// Warning - warns the user but allows entry.
Warning,
/// Information - displays information but allows entry.
Information
}
///
/// Represents a data validation rule that can be applied to a range of cells.
///
public class DataValidationRule : ICellValidator
{
///
/// Gets or sets the type of data validation.
///
public DataValidationType Type { get; set; }
///
/// Gets or sets the comparison operator.
///
public DataValidationOperator Operator { get; set; }
///
/// Gets or sets the first formula or value for comparison.
///
public string? Formula1 { get; set; }
///
/// Gets or sets the second formula or value for comparison (used with Between/NotBetween).
///
public string? Formula2 { get; set; }
///
/// Gets or sets whether blank cells are allowed.
///
public bool AllowBlank { get; set; } = true;
///
/// Gets or sets the error dialog title.
///
public string? ErrorTitle { get; set; }
///
/// Gets or sets the error message.
///
public string Error { get; set; } = "The value you entered is not valid.";
///
/// Gets or sets the input message title.
///
public string? InputTitle { get; set; }
///
/// Gets or sets the input message.
///
public string? InputMessage { get; set; }
///
/// Gets or sets whether to show the input message.
///
public bool ShowInputMessage { get; set; }
///
/// Gets or sets whether to show the error message.
///
public bool ShowErrorMessage { get; set; } = true;
///
/// Gets or sets the error style.
///
public DataValidationErrorStyle ErrorStyle { get; set; } = DataValidationErrorStyle.Stop;
///
/// Gets the list items for List type validation, parsed from Formula1.
///
public IReadOnlyList ListItems => GetListItems(null);
///
/// Gets the list items for List type validation, resolving cell range references from the given sheet.
///
public IReadOnlyList GetListItems(Worksheet? sheet)
{
if (Type != DataValidationType.List || string.IsNullOrEmpty(Formula1))
{
return [];
}
if (Formula1.StartsWith('='))
{
return sheet is not null ? ResolveListFromRange(sheet) : [];
}
return Formula1.Split(',').Select(s => s.Trim().Trim('"')).ToList();
}
private IReadOnlyList ResolveListFromRange(Worksheet sheet)
{
try
{
var rangeText = Formula1![1..]; // strip leading '='
var range = RangeRef.Parse(rangeText);
var items = new List();
for (var row = range.Start.Row; row <= range.End.Row; row++)
{
for (var col = range.Start.Column; col <= range.End.Column; col++)
{
if (sheet.Cells.TryGet(row, col, out var cell) && cell.Value is not null)
{
items.Add(cell.GetValueAsString() ?? "");
}
else
{
items.Add("");
}
}
}
return items;
}
catch (ArgumentException)
{
return [];
}
catch (FormatException)
{
return [];
}
}
///
public bool Validate(Cell cell)
{
ArgumentNullException.ThrowIfNull(cell);
return Validate(cell, cell.Address);
}
///
/// Validates the specified cell, adjusting formula references relative to the given range start.
///
public bool Validate(Cell cell, CellRef rangeStart)
{
ArgumentNullException.ThrowIfNull(cell);
if (AllowBlank && (cell.Value is null || (cell.Value is string s && string.IsNullOrEmpty(s)) || cell.ValueType == CellDataType.Empty))
{
return true;
}
return Type switch
{
DataValidationType.WholeNumber => ValidateWholeNumber(cell),
DataValidationType.Decimal => ValidateDecimal(cell),
DataValidationType.List => ValidateList(cell),
DataValidationType.Date => ValidateDate(cell),
DataValidationType.Time => ValidateTime(cell),
DataValidationType.TextLength => ValidateTextLength(cell),
DataValidationType.Custom => ValidateCustom(cell, rangeStart),
_ => true
};
}
private bool ValidateWholeNumber(Cell cell)
{
if (cell.ValueType != CellDataType.Number)
{
return false;
}
var value = Convert.ToDouble(cell.Value, CultureInfo.InvariantCulture);
if (value != Math.Floor(value))
{
return false;
}
return CompareValue(value);
}
private bool ValidateDecimal(Cell cell)
{
if (cell.ValueType != CellDataType.Number)
{
return false;
}
var value = Convert.ToDouble(cell.Value, CultureInfo.InvariantCulture);
return CompareValue(value);
}
private bool ValidateList(Cell cell)
{
var items = GetListItems(cell.Worksheet);
// Must match the culture used to render the list items in GetListItems.
var cellValue = cell.GetValueAsString() ?? "";
return items.Any(item => string.Equals(item, cellValue, StringComparison.OrdinalIgnoreCase));
}
private bool ValidateDate(Cell cell)
{
if (cell.Value is DateTime dateValue)
{
var serialDate = dateValue.ToNumber();
return CompareValue(serialDate);
}
if (cell.ValueType == CellDataType.Number)
{
var value = Convert.ToDouble(cell.Value, CultureInfo.InvariantCulture);
return CompareValue(value);
}
return false;
}
private bool ValidateTime(Cell cell)
{
if (cell.ValueType == CellDataType.Number)
{
var value = Convert.ToDouble(cell.Value, CultureInfo.InvariantCulture);
return CompareValue(value);
}
return false;
}
private bool ValidateTextLength(Cell cell)
{
var text = cell.Value?.ToString() ?? "";
return CompareValue(text.Length);
}
private bool ValidateCustom(Cell cell, CellRef rangeStart)
{
if (string.IsNullOrEmpty(Formula1))
{
return true;
}
try
{
var formula = Formula1.StartsWith('=') ? Formula1 : "=" + Formula1;
var rowDelta = cell.Address.Row - rangeStart.Row;
var colDelta = cell.Address.Column - rangeStart.Column;
if (rowDelta != 0 || colDelta != 0)
{
formula = Worksheet.AdjustFormulaForCopy(formula, rowDelta, colDelta);
}
var tree = FormulaParser.Parse(formula);
if (tree.Errors.Count > 0)
{
return false;
}
var evaluator = new FormulaEvaluator(cell.Worksheet, cell);
var result = evaluator.Evaluate(tree.Root);
if (result.Value is bool boolValue)
{
return boolValue;
}
if (result.Type == CellDataType.Number)
{
return result.GetValueOrDefault() != 0;
}
return false;
}
catch (ArgumentException)
{
return false;
}
catch (FormatException)
{
return false;
}
catch (OverflowException)
{
return false;
}
catch (InvalidCastException)
{
return false;
}
catch (InvalidOperationException)
{
return false;
}
}
private bool CompareValue(double value)
{
var formula1Value = ParseDouble(Formula1);
var formula2Value = ParseDouble(Formula2);
return Operator switch
{
DataValidationOperator.Between => formula1Value.HasValue && formula2Value.HasValue && value >= formula1Value.Value && value <= formula2Value.Value,
DataValidationOperator.NotBetween => formula1Value.HasValue && formula2Value.HasValue && (value < formula1Value.Value || value > formula2Value.Value),
DataValidationOperator.Equal => formula1Value.HasValue && value == formula1Value.Value,
DataValidationOperator.NotEqual => formula1Value.HasValue && value != formula1Value.Value,
DataValidationOperator.GreaterThan => formula1Value.HasValue && value > formula1Value.Value,
DataValidationOperator.LessThan => formula1Value.HasValue && value < formula1Value.Value,
DataValidationOperator.GreaterThanOrEqual => formula1Value.HasValue && value >= formula1Value.Value,
DataValidationOperator.LessThanOrEqual => formula1Value.HasValue && value <= formula1Value.Value,
_ => true
};
}
private static double? ParseDouble(string? value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
{
return result;
}
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateResult))
{
return dateResult.ToNumber();
}
return null;
}
}