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