diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs index 77e3d5c62..e8f9b9efd 100644 --- a/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -1590,9 +1590,8 @@ public Boolean HasDataValidation /// The data validation rule applying to the current cell or null if there is no such rule. private IXLDataValidation GetDataValidation() { - return Worksheet - .DataValidations - .FirstOrDefault(dv => dv.Ranges.GetIntersectedRanges(this).Any()); + Worksheet.DataValidations.TryGet(AsRange().RangeAddress, out var dataValidation); + return dataValidation; } public IXLDataValidation SetDataValidation() @@ -2117,12 +2116,13 @@ private Boolean SetRange(Object rangeObject) rangesToMerge.ForEach(r => r.Merge(false)); var dataValidations = asRange.Worksheet.DataValidations - .Where(dv => dv.Ranges.GetIntersectedRanges(asRange.RangeAddress).Any()) + .GetAllInRange(asRange.RangeAddress) .ToList(); + foreach (var dataValidation in dataValidations) { XLDataValidation newDataValidation = null; - foreach (var dvRange in dataValidation.Ranges.GetIntersectedRanges(asRange.RangeAddress)) + foreach (var dvRange in dataValidation.Ranges.Where(r => r.Intersects(asRange))) { var dvTargetAddress = dvRange.RangeAddress.Relative(asRange.RangeAddress, targetRange.RangeAddress); var dvTargetRange = Worksheet.Range(dvTargetAddress); @@ -2132,7 +2132,7 @@ private Boolean SetRange(Object rangeObject) newDataValidation.CopyFrom(dataValidation); } else - newDataValidation.Ranges.Add(dvTargetRange); + newDataValidation.AddRange(dvTargetRange); } } diff --git a/ClosedXML/Excel/DataValidation/IXLDataValidation.cs b/ClosedXML/Excel/DataValidation/IXLDataValidation.cs index 1c7b9fd22..895bfa1b6 100644 --- a/ClosedXML/Excel/DataValidation/IXLDataValidation.cs +++ b/ClosedXML/Excel/DataValidation/IXLDataValidation.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace ClosedXML.Excel { @@ -7,7 +8,38 @@ public enum XLAllowedValues { AnyValue, WholeNumber, Decimal, Date, Time, TextLe public enum XLOperator { EqualTo, NotEqualTo, GreaterThan, LessThan, EqualOrGreaterThan, EqualOrLessThan, Between, NotBetween } public interface IXLDataValidation { - IXLRanges Ranges { get; set; } + /// + /// A collection of ranges the data validation rule applies too. + /// + IEnumerable Ranges { get; } + + /// + /// Add a range to the collection of ranges this rule applies to. + /// If the specified range does not belong to the worksheet of the data validation + /// rule it is transferred to the target worksheet. + /// + /// A range to add. + void AddRange(IXLRange range); + + /// + /// Add a collection of ranges to the collection of ranges this rule applies to. + /// Ranges that do not belong to the worksheet of the data validation + /// rule are transferred to the target worksheet. + /// + /// Ranges to add. + void AddRanges(IEnumerable ranges); + + /// + /// Detach data validation rule of all ranges it applies to. + /// + void ClearRanges(); + + /// + /// Remove the specified range from the collection of range this rule applies to. + /// + /// A range to remove. + bool RemoveRange(IXLRange range); + //void Delete(); //void CopyFrom(IXLDataValidation dataValidation); Boolean ShowInputMessage { get; set; } diff --git a/ClosedXML/Excel/DataValidation/IXLDataValidations.cs b/ClosedXML/Excel/DataValidation/IXLDataValidations.cs index 087297929..76b2e486c 100644 --- a/ClosedXML/Excel/DataValidation/IXLDataValidations.cs +++ b/ClosedXML/Excel/DataValidation/IXLDataValidations.cs @@ -5,10 +5,37 @@ namespace ClosedXML.Excel { public interface IXLDataValidations : IEnumerable { - void Add(IXLDataValidation dataValidation); + IXLWorksheet Worksheet { get; } + + /// + /// Add data validation rule to the collection. If the specified rule refers to another + /// worksheet than the collection, the copy will be created and its ranges will refer + /// to the worksheet of the collection. Otherwise the original instance will be placed + /// in the collection. + /// + /// A data validation rule to add. + /// The instance that has actually been added in the collection + /// (may be a copy of the specified one). + IXLDataValidation Add(IXLDataValidation dataValidation); Boolean ContainsSingle(IXLRange range); void Delete(Predicate predicate); + + /// + /// Get the data validation rule for the range with the specified address if it exists. + /// + /// A range address. + /// Data validation rule which ranges collection includes the specified + /// address. The specified range should be fully covered with the data validation rule. + /// For example, if the rule is applied to ranges A1:A3,C1:C3 then this method will + /// return True for ranges A1:A3, C1:C2, A2:A3, and False for ranges A1:C3, A1:C1, etc. + /// True is the data validation rule was found, false otherwise. + bool TryGet(IXLRangeAddress rangeAddress, out IXLDataValidation dataValidation); + + /// + /// Get all data validation rules applied to ranges that intersect the specified range. + /// + IEnumerable GetAllInRange(IXLRangeAddress rangeAddress); } } diff --git a/ClosedXML/Excel/DataValidation/XLDataValidation.cs b/ClosedXML/Excel/DataValidation/XLDataValidation.cs index 63bad1447..82b22ae83 100644 --- a/ClosedXML/Excel/DataValidation/XLDataValidation.cs +++ b/ClosedXML/Excel/DataValidation/XLDataValidation.cs @@ -1,28 +1,37 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace ClosedXML.Excel { internal class XLDataValidation : IXLDataValidation { - private XLDataValidation() + private readonly XLWorksheet _worksheet; + private readonly XLRanges _ranges; + + internal XLWorksheet Worksheet => _worksheet; + + + private XLDataValidation(XLWorksheet worksheet) { - Ranges = new XLRanges(); + _worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + _ranges = new XLRanges(); Initialize(); } public XLDataValidation(IXLRange range) - : this() + : this(range?.Worksheet as XLWorksheet) { - Ranges.Add(new XLRange(new XLRangeParameters((XLRangeAddress)range.RangeAddress, range.Worksheet.Style))); + if (range == null) throw new ArgumentNullException(nameof(range)); + + AddRange(range); } - public XLDataValidation(IXLRanges ranges) - : this() + public XLDataValidation(IXLDataValidation dataValidation, XLWorksheet worksheet) + : this(worksheet) { - ranges.ForEach(range => - { - Ranges.Add(new XLRange(new XLRangeParameters((XLRangeAddress)range.RangeAddress, range.Worksheet.Style))); - }); + _worksheet = worksheet; + CopyFrom(dataValidation); } private void Initialize() @@ -53,14 +62,76 @@ public Boolean IsDirty() (!String.IsNullOrWhiteSpace(ErrorTitle) || !String.IsNullOrWhiteSpace(ErrorMessage))); } - public XLDataValidation(IXLDataValidation dataValidation) + #region IXLDataValidation Members + + public IEnumerable Ranges => _ranges.AsEnumerable(); + + /// + /// Add a range to the collection of ranges this rule applies to. + /// If the specified range does not belong to the worksheet of the data validation + /// rule it is transferred to the target worksheet. + /// + /// A range to add. + public void AddRange(IXLRange range) { - CopyFrom(dataValidation); + if (range == null) throw new ArgumentNullException(nameof(range)); + + if (range.Worksheet != Worksheet) + range = Worksheet.Range(((XLRangeAddress) range.RangeAddress).WithoutWorksheet()); + + _ranges.Add(range); + + RangeAdded?.Invoke(this, new RangeEventArgs(range)); } - #region IXLDataValidation Members + /// + /// Add a collection of ranges to the collection of ranges this rule applies to. + /// Ranges that do not belong to the worksheet of the data validation + /// rule are transferred to the target worksheet. + /// + /// Ranges to add. + public void AddRanges(IEnumerable ranges) + { + ranges = ranges ?? Enumerable.Empty(); + + foreach (var range in ranges) + { + AddRange(range); + } + } + + /// + /// Detach data validation rule of all ranges it applies to. + /// + public void ClearRanges() + { + var allRanges = _ranges.ToList(); + _ranges.RemoveAll(); + + foreach (var range in allRanges) + { + RangeRemoved?.Invoke(this, new RangeEventArgs(range)); + } + } + + /// + /// Remove the specified range from the collection of range this rule applies to. + /// + /// A range to remove. + public bool RemoveRange(IXLRange range) + { + if (range == null) + return false; + + var res = _ranges.Remove(range); + + if (res) + { + RangeRemoved?.Invoke(this, new RangeEventArgs(range)); + } - public IXLRanges Ranges { get; set; } + return res; + } public Boolean IgnoreBlanks { get; set; } public Boolean InCellDropdown { get; set; } @@ -165,11 +236,8 @@ public void CopyFrom(IXLDataValidation dataValidation) { if (dataValidation == this) return; - if (Ranges == null && dataValidation.Ranges != null) - { - Ranges = new XLRanges(); - dataValidation.Ranges.ForEach(r => Ranges.Add(r)); - } + if (!_ranges.Any()) + AddRanges(dataValidation.Ranges); IgnoreBlanks = dataValidation.IgnoreBlanks; InCellDropdown = dataValidation.InCellDropdown; @@ -196,5 +264,31 @@ private void Validate(String value) if (value.Length > 255) throw new ArgumentOutOfRangeException(nameof(value), "The maximum allowed length of the value is 255 characters."); } + + internal event EventHandler RangeAdded; + + internal event EventHandler RangeRemoved; + + internal void SplitBy(IXLRangeAddress rangeAddress) + { + var rangesToSplit = _ranges.GetIntersectedRanges(rangeAddress).ToList(); + + foreach (var rangeToSplit in rangesToSplit) + { + var newRanges = (rangeToSplit as XLRange).Split(rangeAddress, includeIntersection: false); + RemoveRange(rangeToSplit); + newRanges.ForEach(AddRange); + } + } + } + + internal class RangeEventArgs : EventArgs + { + public RangeEventArgs(IXLRange range) + { + Range = range ?? throw new ArgumentNullException(nameof(range)); + } + + public IXLRange Range { get; } } } diff --git a/ClosedXML/Excel/DataValidation/XLDataValidations.cs b/ClosedXML/Excel/DataValidation/XLDataValidations.cs index 829a4c233..a3c5f9d3a 100644 --- a/ClosedXML/Excel/DataValidation/XLDataValidations.cs +++ b/ClosedXML/Excel/DataValidation/XLDataValidations.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ClosedXML.Excel.Ranges.Index; namespace ClosedXML.Excel { @@ -8,20 +9,105 @@ namespace ClosedXML.Excel internal class XLDataValidations : IXLDataValidations { + private readonly XLRangeIndex _dataValidationIndex; + + internal XLWorksheet Worksheet => _worksheet; + + private readonly XLWorksheet _worksheet; private readonly List _dataValidations = new List(); + public XLDataValidations(XLWorksheet worksheet) + { + _worksheet = worksheet ?? throw new ArgumentNullException(nameof(worksheet)); + _dataValidationIndex = new XLRangeIndex(_worksheet); + } + #region IXLDataValidations Members - public void Add(IXLDataValidation dataValidation) + IXLWorksheet IXLDataValidations.Worksheet => _worksheet; + + public IXLDataValidation Add(IXLDataValidation dataValidation) { - _dataValidations.Add(dataValidation); + return Add(dataValidation, skipIntersectionsCheck: false); + } + + internal IXLDataValidation Add(IXLDataValidation dataValidation, bool skipIntersectionsCheck) + { + if (dataValidation == null) throw new ArgumentNullException(nameof(dataValidation)); + + XLDataValidation xlDataValidation; + if (!(dataValidation is XLDataValidation) || + dataValidation.Ranges.Any(r => r.Worksheet != Worksheet)) + { + xlDataValidation = new XLDataValidation(dataValidation, Worksheet); + } + else + { + xlDataValidation = (XLDataValidation)dataValidation; + } + + xlDataValidation.RangeAdded += OnRangeAdded; + xlDataValidation.RangeRemoved += OnRangeRemoved; + + foreach (var range in xlDataValidation.Ranges) + { + ProcessRangeAdded(range, xlDataValidation, skipIntersectionsCheck); + } + + _dataValidations.Add(xlDataValidation); + + return xlDataValidation; } public void Delete(Predicate predicate) { - _dataValidations.RemoveAll(predicate); + var dataValidationsToRemove = _dataValidations.Where(dv => predicate(dv)) + .ToList(); + + dataValidationsToRemove.ForEach(Delete); + } + + /// + /// Get the data validation rule for the range with the specified address if it exists. + /// + /// A range address. + /// Data validation rule which ranges collection includes the specified + /// address. The specified range should be fully covered with the data validation rule. + /// For example, if the rule is applied to ranges A1:A3,C1:C3 then this method will + /// return True for ranges A1:A3, C1:C2, A2:A3, and False for ranges A1:C3, A1:C1, etc. + /// True is the data validation rule was found, false otherwise. + public bool TryGet(IXLRangeAddress rangeAddress, out IXLDataValidation dataValidation) + { + dataValidation = null; + if (rangeAddress == null || !rangeAddress.IsValid) + return false; + + var candidates = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .Where(c => c.RangeAddress.Contains(rangeAddress.FirstAddress) && + c.RangeAddress.Contains(rangeAddress.LastAddress)); + + if (!candidates.Any()) + return false; + + dataValidation = candidates.First().DataValidation; + + return true; } + /// + /// Get all data validation rules applied to ranges that intersect the specified range. + /// + public IEnumerable GetAllInRange(IXLRangeAddress rangeAddress) + { + if (rangeAddress == null || !rangeAddress.IsValid) + return Enumerable.Empty(); + + return _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .Select(indexEntry => indexEntry.DataValidation) + .Distinct(); + } + + public IEnumerator GetEnumerator() { return _dataValidations.GetEnumerator(); @@ -44,18 +130,34 @@ public Boolean ContainsSingle(IXLRange range) return count == 1; } - #endregion IXLDataValidations Members - public void Delete(IXLDataValidation dataValidation) { - _dataValidations.RemoveAll(dv => dv.Ranges.Equals(dataValidation.Ranges)); + if (!_dataValidations.Remove(dataValidation)) + return; + var xlDataValidation = dataValidation as XLDataValidation; + xlDataValidation.RangeAdded -= OnRangeAdded; + xlDataValidation.RangeRemoved -= OnRangeRemoved; + + foreach (var range in dataValidation.Ranges) + { + ProcessRangeRemoved(range); + } } public void Delete(IXLRange range) { - _dataValidations.RemoveAll(dv => dv.Ranges.Contains(range)); + if (range == null) throw new ArgumentNullException(nameof(range)); + + var dataValidationsToRemove = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) range.RangeAddress) + .Select(e => e.DataValidation) + .Distinct() + .ToList(); + + dataValidationsToRemove.ForEach(Delete); } + #endregion IXLDataValidations Members + public void Consolidate() { Func areEqual = (dv1, dv2) => @@ -73,11 +175,12 @@ public void Consolidate() dv1.AllowedValues == dv2.AllowedValues && dv1.Operator == dv2.Operator && dv1.MinValue == dv2.MinValue && - dv1.MaxValue == dv2.MaxValue; + dv1.MaxValue == dv2.MaxValue && + dv1.Value == dv2.Value; }; var rules = _dataValidations.ToList(); - _dataValidations.Clear(); + rules.ForEach(Delete); while (rules.Any()) { @@ -86,10 +189,90 @@ public void Consolidate() var consRule = similarRules.First(); var ranges = similarRules.SelectMany(dv => dv.Ranges).ToList(); - consRule.Ranges.RemoveAll(); - ranges.ForEach(r => consRule.Ranges.Add(r)); - consRule.Ranges = consRule.Ranges.Consolidate(); - _dataValidations.Add(consRule); + + IXLRanges consolidatedRanges = new XLRanges(); + ranges.ForEach(r => consolidatedRanges.Add(r)); + consolidatedRanges = consolidatedRanges.Consolidate(); + + consRule.ClearRanges(); + consRule.AddRanges(consolidatedRanges); + Add(consRule); + } + } + + + private void OnRangeAdded(object sender, RangeEventArgs e) + { + ProcessRangeAdded(e.Range, sender as XLDataValidation, skipIntersectionCheck: false); + } + private void OnRangeRemoved(object sender, RangeEventArgs e) + { + ProcessRangeRemoved(e.Range); + } + + private void ProcessRangeAdded(IXLRange range, XLDataValidation dataValidation, bool skipIntersectionCheck) + { + if (!skipIntersectionCheck) + { + SplitExistingRanges(range.RangeAddress); + } + + var indexEntry = new XLDataValidationIndexEntry(range.RangeAddress, dataValidation); + _dataValidationIndex.Add(indexEntry); + } + + /// + /// The flag used to avoid unnecessary check for splitting intersected ranges when we already + /// are performing the splitting. + /// + private bool _skipSplittingExistingRanges = false; + private void SplitExistingRanges(IXLRangeAddress rangeAddress) + { + if (_skipSplittingExistingRanges) return; + + try + { + _skipSplittingExistingRanges = true; + var entries = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) rangeAddress) + .ToList(); + + foreach (var entry in entries) + { + entry.DataValidation.SplitBy(rangeAddress); + } + } + finally + { + _skipSplittingExistingRanges = false; + } + + //TODO Remove empty data validations + } + + private void ProcessRangeRemoved(IXLRange range) + { + var entry = _dataValidationIndex.GetIntersectedRanges((XLRangeAddress) range.RangeAddress) + .SingleOrDefault(e => Equals(e.RangeAddress, range.RangeAddress)); + + _dataValidationIndex.Remove(entry.RangeAddress); + } + + /// + /// Class used for indexing data validation rules. + /// + private class XLDataValidationIndexEntry : IXLAddressable + { + /// + /// Gets an object with the boundaries of this range. + /// + public IXLRangeAddress RangeAddress { get; } + + public XLDataValidation DataValidation { get; } + + public XLDataValidationIndexEntry(IXLRangeAddress rangeAddress, XLDataValidation dataValidation) + { + RangeAddress = rangeAddress; + DataValidation = dataValidation; } } } diff --git a/ClosedXML/Excel/Patterns/Quadrant.cs b/ClosedXML/Excel/Patterns/Quadrant.cs index 76bdebfd1..5591ff517 100644 --- a/ClosedXML/Excel/Patterns/Quadrant.cs +++ b/ClosedXML/Excel/Patterns/Quadrant.cs @@ -7,10 +7,10 @@ namespace ClosedXML.Excel.Patterns /// /// Implementation of QuadTree adapted to Excel worksheet specifics. Differences with the classic implementation /// are that the topmost level is split to 128 square parts (2 columns of 64 blocks, each 8192*8192 cells) and that splitting - /// the quadrand onto 4 smaller quadrants does not depent on the number of items in this quadrant. When the range is added to the + /// the quadrant onto 4 smaller quadrants does not depend on the number of items in this quadrant. When the range is added to the /// QuadTree it is placed on the bottommost level where it fits to a single quadrant. That means, row-wide and column-wide ranges /// are always placed at the level 0, and the smaller the range is the deeper it goes down the tree. This approach eliminates - /// the need of trasferring ranges between levels. + /// the need of transferring ranges between levels. /// internal class Quadrant { @@ -32,7 +32,7 @@ internal class Quadrant public int MinimumColumn { get; } /// - /// Minimun row included in this quadrant. + /// Minimum row included in this quadrant. /// public int MinimumRow { get; } @@ -49,7 +49,7 @@ internal class Quadrant /// /// Collection of ranges belonging to this quadrant (does not include ranges from child quadrants). /// - public IEnumerable Ranges + public IEnumerable Ranges { get => _ranges?.Values.AsEnumerable(); } @@ -95,7 +95,7 @@ private Quadrant(byte level, short x, short y) /// Add a range to the quadrant or to one of the child quadrants (recursively). /// /// True, if range was successfully added, false if it has been added before. - public bool Add(IXLRangeBase range) + public bool Add(IXLAddressable range) { bool res = false; var children = Children ?? CreateChildren().ToList(); @@ -123,7 +123,7 @@ public bool Add(IXLRangeBase range) /// /// Get all ranges from the quadrant and all child quadrants (recursively). /// - public IEnumerable GetAll() + public IEnumerable GetAll() { if (Ranges != null) { @@ -145,7 +145,7 @@ public IEnumerable GetAll() /// /// Get all ranges from the quadrant and all child quadrants (recursively) that intersect the specified address. /// - public IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) + public IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) { if (Ranges != null) { @@ -173,7 +173,7 @@ public IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddre /// /// Get all ranges from the quadrant and all child quadrants (recursively) that cover the specified address. /// - public IEnumerable GetIntersectedRanges(IXLAddress address) + public IEnumerable GetIntersectedRanges(IXLAddress address) { if (Ranges != null) { @@ -211,7 +211,7 @@ public bool Remove(IXLRangeAddress rangeAddress) { foreach (var childQuadrant in Children) { - if (childQuadrant.Covers(in rangeAddress)) + if (childQuadrant.Covers(rangeAddress)) { res |= childQuadrant.Remove(rangeAddress); coveredByChild = true; @@ -225,14 +225,14 @@ public bool Remove(IXLRangeAddress rangeAddress) res = true; } - return res; ; + return res; } /// /// Remove all the ranges matching specified criteria from the quadrant and its child quadrants (recursively). /// Don't use it for searching intersections as it would be much less efficient than . /// - public IEnumerable RemoveAll(Predicate predicate) + public IEnumerable RemoveAll(Predicate predicate) { if (_ranges != null) { @@ -272,20 +272,20 @@ public IEnumerable RemoveAll(Predicate predicate) /// /// Collection of ranges belonging to the current quadrant (that cannot fit into child quadrants). /// - private Dictionary _ranges; + private Dictionary _ranges; #endregion Private Fields #region Private Methods /// - /// Add a range to the collection of quandrant's own ranges. + /// Add a range to the collection of quadrant's own ranges. /// - /// True if the range was succesfully added, false if it had been added before. - private bool AddInternal(IXLRangeBase range) + /// True if the range was successfully added, false if it had been added before. + private bool AddInternal(IXLAddressable range) { if (_ranges == null) - _ranges = new Dictionary(); + _ranges = new Dictionary(); if (_ranges.ContainsKey(range.RangeAddress)) return false; @@ -336,8 +336,8 @@ private IEnumerable CreateChildren() byte childLevel = (byte)(Level + 1); if (childLevel > MAX_LEVEL) yield break; - byte xCount = 2; // Always divide on halfs - byte yCount = (byte)((Level == 0) ? (XLHelper.MaxRowNumber / XLHelper.MaxColumnNumber) : 2); // Level 0 divide onto 64 parts, the rest - on halfs + byte xCount = 2; // Always divide on halves + byte yCount = (byte)((Level == 0) ? (XLHelper.MaxRowNumber / XLHelper.MaxColumnNumber) : 2); // Level 0 divide onto 64 parts, the rest - on halves for (byte dy = 0; dy < yCount; dy++) { @@ -350,4 +350,42 @@ private IEnumerable CreateChildren() #endregion Private Methods } + + /// + /// A generic version of + /// + internal class Quadrant : Quadrant + where T:IXLAddressable + { + public new IEnumerable Ranges => base.Ranges.Cast(); + + public bool Add(T range) + { + return base.Add(range); + } + + public new IEnumerable GetAll() + { + return base.GetAll().Cast(); + } + + public new IEnumerable GetIntersectedRanges(IXLRangeAddress rangeAddress) + { + return base.GetIntersectedRanges(rangeAddress).Cast(); + } + + public new IEnumerable GetIntersectedRanges(IXLAddress address) + { + return base.GetIntersectedRanges(address).Cast(); + } + + public bool Remove(T range) + { + return Remove(range.RangeAddress); + } + public IEnumerable RemoveAll(Predicate predicate) + { + return base.RemoveAll(r => predicate((T) r)).Cast(); + } + } } diff --git a/ClosedXML/Excel/Ranges/IXLAddressable.cs b/ClosedXML/Excel/Ranges/IXLAddressable.cs new file mode 100644 index 000000000..b2921b435 --- /dev/null +++ b/ClosedXML/Excel/Ranges/IXLAddressable.cs @@ -0,0 +1,15 @@ +namespace ClosedXML.Excel +{ + /// + /// A very lightweight interface for entities that have an address as + /// a rectangular range. + /// + public interface IXLAddressable + { + /// + /// Gets an object with the boundaries of this range. + /// + + IXLRangeAddress RangeAddress { get; } + } +} diff --git a/ClosedXML/Excel/Ranges/IXLRangeBase.cs b/ClosedXML/Excel/Ranges/IXLRangeBase.cs index 955ea3eff..3af95e5a5 100644 --- a/ClosedXML/Excel/Ranges/IXLRangeBase.cs +++ b/ClosedXML/Excel/Ranges/IXLRangeBase.cs @@ -9,15 +9,10 @@ public enum XLScope Worksheet } - public interface IXLRangeBase + public interface IXLRangeBase : IXLAddressable { IXLWorksheet Worksheet { get; } - /// - /// Gets an object with the boundaries of this range. - /// - IXLRangeAddress RangeAddress { get; } - /// /// Sets a value to every cell in this range. /// If the object is an IEnumerable ClosedXML will copy the collection's data into a table starting from each cell. diff --git a/ClosedXML/Excel/Ranges/IXLRanges.cs b/ClosedXML/Excel/Ranges/IXLRanges.cs index 6d55b9c88..16dfa72f2 100644 --- a/ClosedXML/Excel/Ranges/IXLRanges.cs +++ b/ClosedXML/Excel/Ranges/IXLRanges.cs @@ -17,7 +17,7 @@ public interface IXLRanges : IEnumerable /// Removes the specified range from this group. /// /// The range to remove from this group. - void Remove(IXLRange range); + bool Remove(IXLRange range); /// /// Removes ranges matching the criteria from the collection, optionally releasing their event handlers. diff --git a/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs b/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs index 107672b92..ecc0e88b7 100644 --- a/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs +++ b/ClosedXML/Excel/Ranges/Index/IXLRangeIndex.cs @@ -8,17 +8,17 @@ namespace ClosedXML.Excel.Ranges.Index /// internal interface IXLRangeIndex { - bool Add(IXLRangeBase range); + bool Add(IXLAddressable range); bool Remove(IXLRangeAddress rangeAddress); - int RemoveAll(Predicate predicate = null); + int RemoveAll(Predicate predicate = null); - IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress); + IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress); - IEnumerable GetIntersectedRanges(XLAddress address); + IEnumerable GetIntersectedRanges(XLAddress address); - IEnumerable GetAll(); + IEnumerable GetAll(); bool Intersects(in XLRangeAddress rangeAddress); @@ -28,7 +28,7 @@ internal interface IXLRangeIndex } internal interface IXLRangeIndex : IXLRangeIndex - where T : IXLRangeBase + where T : IXLAddressable { bool Add(T range); diff --git a/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs b/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs index c00df814b..fd920684b 100644 --- a/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs +++ b/ClosedXML/Excel/Ranges/Index/XLRangeIndex.cs @@ -15,7 +15,7 @@ internal abstract class XLRangeIndex : IXLRangeIndex public XLRangeIndex(IXLWorksheet worksheet) { _worksheet = worksheet; - _rangeList = new List(); + _rangeList = new List(); (_worksheet as XLWorksheet).RegisterRangeIndex(this); } @@ -25,7 +25,7 @@ public XLRangeIndex(IXLWorksheet worksheet) public abstract bool MatchesType(XLRangeType rangeType); - public bool Add(IXLRangeBase range) + public bool Add(IXLAddressable range) { if (range == null) throw new ArgumentNullException(nameof(range)); @@ -33,7 +33,7 @@ public bool Add(IXLRangeBase range) if (!range.RangeAddress.IsValid) throw new ArgumentException("Range is invalid"); - CheckWorksheet(range.Worksheet); + CheckWorksheet(range.RangeAddress.Worksheet); _count++; if (_count < MinimumCountForIndexing) @@ -64,7 +64,7 @@ public bool Contains(in XLAddress address) return _quadTree.GetIntersectedRanges(address).Any(); } - public IEnumerable GetAll() + public IEnumerable GetAll() { if (_quadTree == null) { @@ -74,7 +74,7 @@ public IEnumerable GetAll() return _quadTree.GetAll(); } - public IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress) + public IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddress) { CheckWorksheet(rangeAddress.Worksheet); @@ -86,7 +86,7 @@ public IEnumerable GetIntersectedRanges(XLRangeAddress rangeAddres return _quadTree.GetIntersectedRanges(rangeAddress); } - public IEnumerable GetIntersectedRanges(XLAddress address) + public IEnumerable GetIntersectedRanges(XLAddress address) { CheckWorksheet(address.Worksheet); @@ -126,7 +126,7 @@ public bool Remove(IXLRangeAddress rangeAddress) return _quadTree.Remove(rangeAddress); } - public int RemoveAll(Predicate predicate = null) + public int RemoveAll(Predicate predicate = null) { predicate = predicate ?? (_ => true); @@ -152,11 +152,11 @@ public int RemoveAll(Predicate predicate = null) /// A collection of ranges used before the QuadTree is initialized (until /// is reached. /// - private readonly List _rangeList; + protected readonly List _rangeList; private readonly IXLWorksheet _worksheet; private int _count = 0; - private Quadrant _quadTree; + protected Quadrant _quadTree; #endregion Private Fields @@ -170,11 +170,16 @@ private void CheckWorksheet(IXLWorksheet worksheet) private void InitializeTree() { - _quadTree = new Quadrant(); + _quadTree = CreateQuadTree(); _rangeList.ForEach(r => _quadTree.Add(r)); _rangeList.Clear(); } + protected virtual Quadrant CreateQuadTree() + { + return new Quadrant(); + } + #endregion Private Methods } @@ -182,7 +187,7 @@ private void InitializeTree() /// Generic version of . /// internal class XLRangeIndex : XLRangeIndex, IXLRangeIndex - where T : IXLRangeBase + where T : IXLAddressable { public XLRangeIndex(IXLWorksheet worksheet) : base(worksheet) { @@ -195,6 +200,8 @@ public bool Add(T range) public int RemoveAll(Predicate predicate) { + predicate = predicate ?? (_ => true); + return base.RemoveAll(r => predicate((T)r)); } @@ -255,5 +262,10 @@ public new IEnumerable GetAll() { return base.GetAll().Cast(); } + + protected override Quadrant CreateQuadTree() + { + return new Quadrant(); + } } } diff --git a/ClosedXML/Excel/Ranges/XLRange.cs b/ClosedXML/Excel/Ranges/XLRange.cs index 3276c04ed..57236baa4 100644 --- a/ClosedXML/Excel/Ranges/XLRange.cs +++ b/ClosedXML/Excel/Ranges/XLRange.cs @@ -749,6 +749,59 @@ public virtual XLRangeColumn Column(String columnLetter) return Column(XLHelper.GetColumnNumberFromLetter(columnLetter)); } + internal IEnumerable Split(IXLRangeAddress anotherRange, bool includeIntersection) + { + if (!RangeAddress.Intersects(anotherRange)) + { + yield return this; + yield break; + } + + var thisRow1 = RangeAddress.FirstAddress.RowNumber; + var thisRow2 = RangeAddress.LastAddress.RowNumber; + var thisColumn1 = RangeAddress.FirstAddress.ColumnNumber; + var thisColumn2 = RangeAddress.LastAddress.ColumnNumber; + + var otherRow1 = Math.Min(Math.Max(thisRow1, anotherRange.FirstAddress.RowNumber), thisRow2 + 1); + var otherRow2 = Math.Max(Math.Min(thisRow2, anotherRange.LastAddress.RowNumber), thisRow1 - 1); + var otherColumn1 = Math.Min(Math.Max(thisColumn1, anotherRange.FirstAddress.ColumnNumber), thisColumn2 + 1); + var otherColumn2 = Math.Max(Math.Min(thisColumn2, anotherRange.LastAddress.ColumnNumber), thisColumn1 - 1); + + var candidates = new[] + { + // to the top of the intersection + new XLRangeAddress( + new XLAddress(thisRow1,thisColumn1, false, false), + new XLAddress(otherRow1 - 1, thisColumn2, false, false)), + + // to the left of the intersection + new XLRangeAddress( + new XLAddress(otherRow1,thisColumn1, false, false), + new XLAddress(otherRow2, otherColumn1 - 1, false, false)), + + includeIntersection + ? new XLRangeAddress( + new XLAddress(otherRow1, otherColumn1, false, false), + new XLAddress(otherRow2, otherColumn2, false, false)) + : XLRangeAddress.Invalid, + + // to the right of the intersection + new XLRangeAddress( + new XLAddress(otherRow1,otherColumn2 + 1, false, false), + new XLAddress(otherRow2, thisColumn2, false, false)), + + // to the bottom of the intersection + new XLRangeAddress( + new XLAddress(otherRow2 + 1,thisColumn1, false, false), + new XLAddress(thisRow2, thisColumn2, false, false)), + }; + + foreach (var rangeAddress in candidates.Where(c => c.IsValid && c.IsNormalized)) + { + yield return Worksheet.Range(rangeAddress); + } + } + private void TransposeRange(int squareSide) { var cellsToInsert = new Dictionary(); diff --git a/ClosedXML/Excel/Ranges/XLRangeBase.cs b/ClosedXML/Excel/Ranges/XLRangeBase.cs index a440b9a39..d3cde68ee 100644 --- a/ClosedXML/Excel/Ranges/XLRangeBase.cs +++ b/ClosedXML/Excel/Ranges/XLRangeBase.cs @@ -69,13 +69,8 @@ public IXLDataValidation NewDataValidation { get { - var newRanges = new XLRanges { AsRange() }; - var dataValidation = DataValidation; - - if (dataValidation != null) - Worksheet.DataValidations.Delete(dataValidation); - - dataValidation = new XLDataValidation(newRanges); + var newRange = AsRange(); + var dataValidation = new XLDataValidation(newRange); Worksheet.DataValidations.Add(dataValidation); return dataValidation; } @@ -94,20 +89,13 @@ public IXLDataValidation DataValidation private IXLDataValidation GetDataValidation() { - foreach (var xlDataValidation in Worksheet.DataValidations) - { - foreach (var range in xlDataValidation.Ranges) - { - if (range.ToString() == ToString()) - return xlDataValidation; - } - } - return null; + Worksheet.DataValidations.TryGet(RangeAddress, out var existingDataValidation); + return existingDataValidation; } #region IXLRangeBase Members - IXLRangeAddress IXLRangeBase.RangeAddress + IXLRangeAddress IXLAddressable.RangeAddress { get { return RangeAddress; } } @@ -739,7 +727,8 @@ internal XLCell FirstCellUsed(XLCellsUsedOptions options, Func { cellsUsed = cellsUsed.Union( Worksheet.DataValidations - .SelectMany(dv => dv.Ranges.GetIntersectedRanges(RangeAddress)) + .GetAllInRange(RangeAddress) + .SelectMany(dv => dv.Ranges) .Select(r => r.FirstCell()) .Where(predicate) ); @@ -820,7 +809,8 @@ internal XLCell LastCellUsed(XLCellsUsedOptions options, Func { cellsUsed = cellsUsed.Union( Worksheet.DataValidations - .SelectMany(dv => dv.Ranges.GetIntersectedRanges(RangeAddress)) + .GetAllInRange(RangeAddress) + .SelectMany(dv => dv.Ranges) .Select(r => r.LastCell()) .Where(predicate) ); @@ -2145,70 +2135,14 @@ public XLRangeRow RowQuick(Int32 row) public IXLDataValidation SetDataValidation() { var existingValidation = GetDataValidation(); - if (existingValidation != null) return existingValidation; - - IXLDataValidation dataValidationToCopy = null; - var dvEmpty = new List(); - foreach (IXLDataValidation dv in Worksheet.DataValidations) - { - foreach (IXLRange dvRange in dv.Ranges.GetIntersectedRanges(RangeAddress).ToList()) - { - if (dataValidationToCopy == null) - dataValidationToCopy = dv; - - dv.Ranges.Remove(dvRange); - foreach (var column in dvRange.Columns()) - { - if (column.Intersects(this)) - { - Int32 dvStart = column.RangeAddress.FirstAddress.RowNumber; - Int32 dvEnd = column.RangeAddress.LastAddress.RowNumber; - Int32 thisStart = RangeAddress.FirstAddress.RowNumber; - Int32 thisEnd = RangeAddress.LastAddress.RowNumber; - - if (thisStart > dvStart && thisEnd < dvEnd) - { - dv.Ranges.Add(Worksheet.Column(column.ColumnNumber()).Column(dvStart, thisStart - 1)); - dv.Ranges.Add(Worksheet.Column(column.ColumnNumber()).Column(thisEnd + 1, dvEnd)); - } - else - { - Int32 coStart; - if (dvStart < thisStart) - coStart = dvStart; - else - coStart = thisEnd + 1; - - if (coStart <= dvEnd) - { - Int32 coEnd; - if (dvEnd > thisEnd) - coEnd = dvEnd; - else - coEnd = thisStart - 1; - - if (coEnd >= dvStart) - { - dv.Ranges.Add(Worksheet.Column(column.ColumnNumber()).Column(coStart, coEnd)); - } - } - } - } - else - { - dv.Ranges.Add(column); - } - } - - if (!dv.Ranges.Any()) - dvEmpty.Add(dv); - } - } + if (existingValidation != null && existingValidation.Ranges.Any(r => r == this)) + return existingValidation; - dvEmpty.ForEach(dv => Worksheet.DataValidations.Delete(dv)); + IXLDataValidation dataValidationToCopy = Worksheet.DataValidations.GetAllInRange(RangeAddress) + .FirstOrDefault(); - var newRanges = new XLRanges { AsRange() }; - var dataValidation = new XLDataValidation(newRanges); + var newRange = AsRange(); + var dataValidation = new XLDataValidation(newRange); if (dataValidationToCopy != null) dataValidation.CopyFrom(dataValidationToCopy); diff --git a/ClosedXML/Excel/Ranges/XLRanges.cs b/ClosedXML/Excel/Ranges/XLRanges.cs index 5f38c10cf..8cf9900a1 100644 --- a/ClosedXML/Excel/Ranges/XLRanges.cs +++ b/ClosedXML/Excel/Ranges/XLRanges.cs @@ -63,10 +63,15 @@ public void Add(IXLCell cell) Add(cell.AsRange()); } - public void Remove(IXLRange range) + public bool Remove(IXLRange range) { if (GetRangeIndex(range.Worksheet).Remove(range.RangeAddress)) + { Count--; + return true; + } + + return false; } /// @@ -286,23 +291,14 @@ public override int GetHashCode() public IXLDataValidation SetDataValidation() { - foreach (XLRange range in Ranges) + var firstRange = Ranges.First(); + var dataValidation = new XLDataValidation(firstRange); + foreach (var range in Ranges.Skip(1)) { - foreach (IXLDataValidation dv in range.Worksheet.DataValidations) - { - foreach (IXLRange dvRange in dv.Ranges.GetIntersectedRanges(range.RangeAddress)) - { - dv.Ranges.Remove(dvRange); - foreach (IXLCell c in dvRange.Cells().Where(c => !range.Contains(c.Address.ToString()))) - { - dv.Ranges.Add(c.AsRange()); - } - } - } + dataValidation.AddRange(range); } - var dataValidation = new XLDataValidation(this); - Ranges.First().Worksheet.DataValidations.Add(dataValidation); + firstRange.Worksheet.DataValidations.Add(dataValidation); return dataValidation; } diff --git a/ClosedXML/Excel/XLWorkbook_Load.cs b/ClosedXML/Excel/XLWorkbook_Load.cs index 8cc6f5887..2e72b1d1b 100644 --- a/ClosedXML/Excel/XLWorkbook_Load.cs +++ b/ClosedXML/Excel/XLWorkbook_Load.cs @@ -2502,7 +2502,8 @@ private static void LoadDataValidations(DataValidations dataValidations, XLWorks if (String.IsNullOrWhiteSpace(txt)) continue; foreach (var rangeAddress in txt.Split(' ')) { - var dvt = ws.Range(rangeAddress).SetDataValidation(); + var dvt = new XLDataValidation(ws.Range(rangeAddress)); + ws.DataValidations.Add(dvt, skipIntersectionsCheck: true); if (dvs.AllowBlank != null) dvt.IgnoreBlanks = dvs.AllowBlank; if (dvs.ShowDropDown != null) dvt.InCellDropdown = !dvs.ShowDropDown.Value; if (dvs.ShowErrorMessage != null) dvt.ShowErrorMessage = dvs.ShowErrorMessage; diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs index 921d13bae..3ad3d0af3 100644 --- a/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -63,7 +63,7 @@ public XLWorksheet(String sheetName, XLWorkbook workbook) SheetView = new XLSheetView(); Tables = new XLTables(); Hyperlinks = new XLHyperlinks(); - DataValidations = new XLDataValidations(); + DataValidations = new XLDataValidations(this); PivotTables = new XLPivotTables(this); Protection = new XLSheetProtection(); AutoFilter = new XLAutoFilter(); @@ -636,7 +636,7 @@ public IXLWorksheet CopyTo(XLWorkbook workbook, String newSheetName, Int32 posit Internals.ColumnsCollection.ForEach(kp => kp.Value.CopyTo(targetSheet.Column(kp.Key))); Internals.RowsCollection.ForEach(kp => kp.Value.CopyTo(targetSheet.Row(kp.Key))); Internals.CellsCollection.GetCells().ForEach(c => targetSheet.Cell(c.Address).CopyFrom(c, XLCellCopyOptions.Values | XLCellCopyOptions.Styles)); - DataValidations.ForEach(dv => targetSheet.DataValidations.Add(new XLDataValidation(dv))); + DataValidations.ForEach(dv => targetSheet.DataValidations.Add(new XLDataValidation(dv, this))); targetSheet.Visibility = Visibility; targetSheet.ColumnWidth = ColumnWidth; targetSheet.ColumnWidthChanged = ColumnWidthChanged; @@ -1278,7 +1278,7 @@ private void ShiftDataValidationColumns(XLRange range, int columnsShifted) foreach (var dv in DataValidations.ToList()) { var dvRanges = dv.Ranges.ToList(); - dv.Ranges.RemoveAll(); + dv.ClearRanges(); foreach (var dvRange in dvRanges) { @@ -1304,7 +1304,7 @@ private void ShiftDataValidationColumns(XLRange range, int columnsShifted) if (newRange.RangeAddress.IsValid && newRange.RangeAddress.FirstAddress.ColumnNumber <= newRange.RangeAddress.LastAddress.ColumnNumber) - dv.Ranges.Add(newRange); + dv.AddRange(newRange); } if (!dv.Ranges.Any()) @@ -1407,7 +1407,7 @@ private void ShiftDataValidationRows(XLRange range, int rowsShifted) foreach (var dv in DataValidations.ToList()) { var dvRanges = dv.Ranges.ToList(); - dv.Ranges.RemoveAll(); + dv.ClearRanges(); foreach (var dvRange in dvRanges) { @@ -1432,7 +1432,7 @@ private void ShiftDataValidationRows(XLRange range, int rowsShifted) if (newRange.RangeAddress.IsValid && newRange.RangeAddress.FirstAddress.RowNumber <= newRange.RangeAddress.LastAddress.RowNumber) - dv.Ranges.Add(newRange); + dv.AddRange(newRange); } if (!dv.Ranges.Any()) diff --git a/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs b/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs index 909b108f3..4c82a8d0e 100644 --- a/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs +++ b/ClosedXML_Tests/Excel/DataValidations/DataValidationTests.cs @@ -1,6 +1,7 @@ using ClosedXML.Excel; using NUnit.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace ClosedXML_Tests.Excel.DataValidations @@ -174,7 +175,7 @@ public void DataValidationShiftedOnRowInsert(string initialAddress, int rowNum, //Assert Assert.AreEqual(1, ws.DataValidations.Count()); - Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count); + Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count()); Assert.AreEqual(expectedAddress, ws.DataValidations.First().Ranges.First().RangeAddress.ToString()); } @@ -200,7 +201,7 @@ public void DataValidationShiftedOnColumnInsert(string initialAddress, int colum //Assert Assert.AreEqual(1, ws.DataValidations.Count()); - Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count); + Assert.AreEqual(1, ws.DataValidations.First().Ranges.Count()); Assert.AreEqual(expectedAddress, ws.DataValidations.First().Ranges.First().RangeAddress.ToString()); } @@ -268,5 +269,215 @@ public void ListLengthOverflow() }); } } + + [Test] + public void CannotCreateDataValidationWithoutRange() + { + Assert.Throws(() => new XLDataValidation(null)); + } + + [Test] + public void DataValidationHasWorksheetAndRangesWhenCreated() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range = ws.Range("A1:A3"); + + var dv = new XLDataValidation(range); + + Assert.AreSame(ws, dv.Worksheet); + Assert.AreSame(range, dv.Ranges.Single()); + } + } + + [Test] + public void CanAddRangeFromSameWorksheet() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var ranges3 = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + + dv.AddRange(range2); + dv.AddRanges(ranges3); + + Assert.IsTrue(dv.Ranges.Any(r => r == range1)); + Assert.IsTrue(dv.Ranges.Any(r => r == range2)); + Assert.IsTrue(dv.Ranges.Any(r => r == ranges3.First())); + Assert.IsTrue(dv.Ranges.Any(r => r == ranges3.Last())); + } + } + + [Test] + public void CanAddRangeFromAnotherWorksheet() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + var ws2 = wb.AddWorksheet(); + var range1 = ws1.Range("A1:A3"); + var range2 = ws2.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + dv.AddRange(range2); + + Assert.IsTrue(dv.Ranges.Any(r => r != range2 && r.RangeAddress.ToString() == range2.RangeAddress.ToString())); + } + } + + [Test] + public void CanClearRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var ranges3 = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + dv.AddRanges(ranges3); + + dv.ClearRanges(); + + Assert.IsEmpty(dv.Ranges); + } + } + + [Test] + public void CanRemoveExistingRange() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + + dv.RemoveRange(range1); + + Assert.AreSame(range2, dv.Ranges.Single()); + } + } + + [Test] + public void RemovingExistingRangeDoesNoFail() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + + var dv = new XLDataValidation(range1); + + dv.RemoveRange(range2); + dv.RemoveRange(null); + + Assert.AreSame(range1, dv.Ranges.Single()); + } + } + + [Test] + public void AddRangeFiresEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + IXLRange addedRange = null; + + dv.RangeAdded += (s, e) => addedRange = e.Range; + + dv.AddRange(range2); + + Assert.AreSame(range2, addedRange); + } + } + + [Test] + public void AddRangesFiresMultipleEvents() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var ranges = ws.Ranges("D1:D3,F1:F3"); + var dv = new XLDataValidation(range1); + + var addedRanges = new List(); + + dv.RangeAdded += (s, e) => addedRanges.Add(e.Range); + + dv.AddRanges(ranges); + + Assert.AreEqual(2, addedRanges.Count); + } + } + + [Test] + public void RemoveRangeFiresEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + IXLRange removedRange = null; + dv.RangeRemoved += (s, e) => removedRange = e.Range; + + dv.RemoveRange(range2); + + Assert.AreSame(range2, removedRange); + } + } + + [Test] + public void RemoveNonExistingRangeDoesNotFireEvent() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + + dv.RangeRemoved += (s, e) => Assert.Fail("Expected not to fire event"); + + dv.RemoveRange(range2); + } + } + + [Test] + public void ClearRangesFiresMultipleEvents() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var range1 = ws.Range("A1:A3"); + var range2 = ws.Range("C1:C3"); + var dv = new XLDataValidation(range1); + dv.AddRange(range2); + + var removedRanges = new List(); + + dv.RangeRemoved += (s, e) => removedRanges.Add(e.Range); + + dv.ClearRanges(); + + Assert.AreEqual(2, removedRanges.Count); + } + } } } diff --git a/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs b/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs new file mode 100644 index 000000000..70757b057 --- /dev/null +++ b/ClosedXML_Tests/Excel/DataValidations/XLDataValidationsTests.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.DataValidations +{ + public class XLDataValidationsTests + { + [Test] + public void CannotCreateWithoutWorksheet() + { + Assert.Throws(() => new XLDataValidations(null)); + } + + [Test] + public void AddedRangesAreTransferredToTargetSheet() + { + using (var wb = new XLWorkbook()) + { + var ws1 = wb.AddWorksheet(); + var ws2 = wb.AddWorksheet(); + + var dv1 = ws1.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + + var dv2 = ws2.DataValidations.Add(dv1); + + Assert.AreEqual(1, ws1.DataValidations.Count()); + Assert.AreEqual(1, ws2.DataValidations.Count()); + + Assert.AreNotSame(dv1, dv2); + + Assert.AreSame(ws1, dv1.Ranges.Single().Worksheet); + Assert.AreSame(ws2, dv2.Ranges.Single().Worksheet); + } + } + + [TestCase("A1:A1", true)] + [TestCase("A1:A3", true)] + [TestCase("A1:A4", false)] + [TestCase("C2:C2", true)] + [TestCase("C1:C3", true)] + [TestCase("A1:C3", false)] + public void CanFindDataValidationForRange(string searchAddress, bool expectedResult) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv = ws.Range("A1:A3").SetDataValidation(); + dv.MinValue = "100"; + dv.AddRange(ws.Range("C1:C3")); + + var address = new XLRangeAddress(ws as XLWorksheet, searchAddress); + + var actualResult = ws.DataValidations.TryGet(address, out var foundDv); + Assert.AreEqual(expectedResult, actualResult); + if (expectedResult) + Assert.AreSame(dv, foundDv); + else + Assert.IsNull(foundDv); + } + } + + + [TestCase("A1:A1", 1)] + [TestCase("A1:A3", 1)] + [TestCase("B1:B4", 0)] + [TestCase("A1:C3", 1)] + [TestCase("C2:C3", 1)] + [TestCase("C2:G6", 2)] + [TestCase("E2:E3", 0)] + public void CanGetAllDataValidationsForRange(string searchAddress, int expectedCount) + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + dv1.AddRange(ws.Range("C1:C3")); + + var dv2 = ws.Range("E4:G6").SetDataValidation(); + dv2.MinValue = "200"; + + var address = new XLRangeAddress(ws as XLWorksheet, searchAddress); + + var actualResult = ws.DataValidations.GetAllInRange(address); + + Assert.AreEqual(expectedCount, actualResult.Count()); + } + } + + [Test] + public void AddDataValidationSplitsExistingRanges() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Ranges("B2:G7,C11:C13").SetDataValidation(); + dv1.MinValue = "100"; + + var dv2 = ws.Range("E4:G6").SetDataValidation(); + dv2.MinValue = "100"; + + Assert.AreEqual(4, dv1.Ranges.Count()); + Assert.AreEqual("B2:G3,B4:D6,B7:G7,C11:C13", + string.Join(",", dv1.Ranges.Select(r => r.RangeAddress.ToString()))); + } + } + + [Test] + public void RemovedRangeExcludedFromIndex() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv = ws.Range("A1:A3").SetDataValidation(); + dv.MinValue = "100"; + var range = ws.Range("C1:C3"); + dv.AddRange(range); + + dv.RemoveRange(range); + + var actualResult = ws.DataValidations.TryGet(range.RangeAddress, out var foundDv); + Assert.IsFalse(actualResult); + Assert.IsNull(foundDv); + } + } + + [Test] + public void ConsolidatedDataValidationsAreUnsubscribed() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var dv1 = ws.Range("A1:A3").SetDataValidation(); + dv1.MinValue = "100"; + var dv2 = ws.Range("B1:B3").SetDataValidation(); + dv2.MinValue = "100"; + + (ws.DataValidations as XLDataValidations).Consolidate(); + dv1.AddRange(ws.Range("C1:C3")); + dv2.AddRange(ws.Range("D1:D3")); + + var consolidatedDv = ws.DataValidations.Single(); + Assert.AreSame(dv1, consolidatedDv); + Assert.True(ws.Cell("C1").HasDataValidation); + Assert.False(ws.Cell("D1").HasDataValidation); + } + } + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs b/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs index dd5e7e0f9..93826fa4c 100644 --- a/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs +++ b/ClosedXML_Tests/Excel/Ranges/RangeIndexTest.cs @@ -266,7 +266,7 @@ public void XLRangesCountChangesCorrectly() private IXLRangeIndex CreateRangeIndex(IXLWorksheet worksheet) { - return new XLRangeIndex((XLWorksheet)worksheet); + return new XLRangeIndex((XLWorksheet)worksheet); } private IXLRangeIndex FillIndexWithTestData(IXLWorksheet worksheet) diff --git a/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs b/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs index 03dd7717c..7bc919b97 100644 --- a/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs +++ b/ClosedXML_Tests/Excel/Ranges/XLRangeBaseTests.cs @@ -485,5 +485,36 @@ public void ClearRangeRemovesSparklines() Assert.IsTrue(ws.Cell("B3").HasSparkline); } + + [TestCase("B2:G7", "D4:E5", true, "B2:G3,B4:C5,D4:E5,F4:G5,B6:G7")] + [TestCase("B2:G7", "D4:E5", false, "B2:G3,B4:C5,F4:G5,B6:G7")] + [TestCase("B2:G7", "B2:G7", true, "B2:G7")] + [TestCase("B2:G7", "B2:G7", false, "")] + [TestCase("B2:G7", "A1:H8", true, "B2:G7")] + [TestCase("B2:G7", "A1:H8", false, "")] + [TestCase("B2:G7", "A1:B2", true, "B2:B2,C2:G2,B3:G7")] + [TestCase("B2:G7", "A1:B2", false, "C2:G2,B3:G7")] + [TestCase("B2:G7", "E4:J5", true, "B2:G3,B4:D5,E4:G5,B6:G7")] + [TestCase("B2:G7", "E4:J5", false, "B2:G3,B4:D5,B6:G7")] + [TestCase("B2:G7", "A11:H18", true, "B2:G7")] + [TestCase("B2:G7", "A11:H18", false, "B2:G7")] + [TestCase("B2:G7", "A1:H1", true, "B2:G7")] + [TestCase("B2:G7", "A1:A12", true, "B2:G7")] + [TestCase("B2:G7", "A8:H8", true, "B2:G7")] + [TestCase("B2:G7", "H1:H8", true, "B2:G7")] + public void CanSplitRange(string rangeAddress, string splitBy, bool includeIntersection, string expectedResult) + { + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet(); + var range = ws.Range(rangeAddress) as XLRange; + var splitter = ws.Range(splitBy); + + var result = range.Split(splitter.RangeAddress, includeIntersection); + + var actualAddresses = string.Join(",", result.Select(r => r.RangeAddress.ToString())); + + Assert.AreEqual(expectedResult, actualAddresses); + + } } } diff --git a/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs index fd30410af..f4f0c68e1 100644 --- a/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs +++ b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs @@ -668,7 +668,10 @@ public void CopyWorksheetPreservesDataValidation() var original = ws1.DataValidations.ElementAt(i); var copy = ws2.DataValidations.ElementAt(i); - Assert.AreEqual(original.Ranges.ToString(), copy.Ranges.ToString()); + var originalRanges = string.Join(",", original.Ranges.Select(r => r.RangeAddress.ToString())); + var copyRanges = string.Join(",", original.Ranges.Select(r => r.RangeAddress.ToString())); + + Assert.AreEqual(originalRanges, copyRanges); Assert.AreEqual(original.AllowedValues, copy.AllowedValues); Assert.AreEqual(original.Operator, copy.Operator); Assert.AreEqual(original.ErrorStyle, copy.ErrorStyle); diff --git a/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx index 11659ff14..1b92f8095 100644 Binary files a/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx and b/ClosedXML_Tests/Resource/Examples/Misc/DataValidation.xlsx differ