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