/Utilities/Collections/AutoSortList.cs
C# | 668 lines | 389 code | 39 blank | 240 comment | 23 complexity | 8144e08acce65d5d90d9d83938017d93 MD5 | raw file
1using System; 2using System.Collections; 3using System.Collections.Generic; 4using Delta.Utilities.Helpers; 5using NUnit.Framework; 6 7namespace Delta.Utilities.Collections 8{ 9 /// <summary> 10 /// AutoSortArrayList extends the ArrayList to manage a list that is 11 /// always sorted. 12 /// </summary> 13 /// <remarks> 14 /// Details: AutoSortArrayList is basically an ArrayList which sorts 15 /// itself whenever we add or remove items. The list does not use a key to 16 /// identify each instance. 17 /// The .NET framework offers similar classes, each with shortcomings. 18 /// * ArrayList - Can sort any type of object but cannot automatically add 19 /// instances into the correct sorted position. 20 /// * SortedList - Can keep any type of object in a sorted order. However, 21 /// it demands a key for every instance. If you don't manage your objects 22 /// by name, this won't work. 23 /// * StringsCollection - Can keep a list of strings without the requirement 24 /// of a key. However, it can't keep them sorted. 25 /// AutoSortArrayList is based on ArrayList and overrides any method that 26 /// dictates order, such as Add(), Insert(), and AddRange(). It overrides 27 /// methods that lookup data so that a binary search is used for speed. 28 /// It overrides methods that allow the user to control the location of 29 /// inserted data and throws InvalidOperationExceptions. 30 /// 31 /// AutoSortArrayList can be used with any class that you want to keep 32 /// ordered. It determines order with an object that implements the 33 /// IComparer interface. If you are collecting primative types like int 34 /// and double, the default IComparer methodology is used. If you are 35 /// collecting strings and need case insensitivity, .NET provides 36 /// System.Collections.CaseInsensitiveComparer. 37 /// 38 /// Here is an overview of the key properties of AutoSortArrayList: 39 /// * AutoSort - when true, Add() will insert in a sorted order. 40 /// Defaults to true. 41 /// * Comparer - use this to supply a custom IComparer. Defaults to null. 42 /// * AreDuplicatesAllowed - when true, Add() will permit duplicate instances 43 /// (as determined by BinarySearch() finding an exact match in the list. 44 /// Defaults to false. 45 /// There are several constructors, designed to override these property's 46 /// defaults. 47 /// </remarks> 48 /// <typeparam name="T">Data type</typeparam> 49 public class AutoSortList<T> : IList<T>, ICollection<T>, IEnumerable<T> 50 { 51 #region AutoSort (Public) 52 /// <summary> 53 /// AutoSort indicates if auto sorting is on. 54 /// When true, Add() and AddRange() will insert the object in the 55 /// position provided by BinarySearch. A number of methods will raise 56 /// InvalidOperationException when true: Insert(), InsertRange(), and 57 /// Reverse(). When false, all methods follow use their ancestor'str 58 /// functionality. If set to false and you add items, when you set it 59 /// back to true, Add() and AddRange() can incorrectly position data 60 /// as a binary search is used to position data. So if switching from 61 /// false to true, call Sort(). 62 /// </summary> 63 /// <typeparam name="T">T</typeparam> 64 public bool AutoSort 65 { 66 get 67 { 68 return autoSort; 69 } 70 } 71 #endregion 72 73 #region Comparer (Public) 74 /// <summary> 75 /// Comparer provides an IComparer interface for use with Sort() and 76 /// BinarySearch(). By default, these methods use the IComparer methods 77 /// of the objects you insert into the list. For primitive values, this 78 /// works well. For strings, you may consider System.Collections. 79 /// CaseInsensitiveComparer. But for custom objects, you'll need to help 80 /// with an IComparer. 81 /// Defaults to null. 82 /// </summary> 83 /// <typeparam name="T">T</typeparam> 84 public IComparer<T> Comparer 85 { 86 get 87 { 88 return comparer; 89 } 90 set 91 { 92 comparer = value; 93 Sort(); 94 } 95 } 96 #endregion 97 98 #region AreDuplicatesAllowed (Public) 99 /// <summary> 100 /// AreDuplicatesAllowed determines if a duplicate is allowed into the list. 101 /// This only applies with AutoSort is true. 102 /// If Add() detects a duplicate when this is false, an 103 /// InvalidOperationException is thrown. 104 /// Defaults to false. 105 /// </summary> 106 /// <typeparam name="T">T</typeparam> 107 public bool AreDuplicatesAllowed 108 { 109 get 110 { 111 return duplicatesAllowed; 112 } 113 } 114 #endregion 115 116 #region Item (Public) 117 /// <summary> 118 /// This 119 /// </summary> 120 /// <param name="index">Index</param> 121 /// <returns>T</returns> 122 public T this[int index] 123 { 124 get 125 { 126 return data[index]; 127 } 128 set 129 { 130 data[index] = value; 131 } 132 } 133 #endregion 134 135 #region Count (Public) 136 /// <summary> 137 /// Count 138 /// </summary> 139 /// <returns>Int</returns> 140 public int Count 141 { 142 get 143 { 144 return data.Count; 145 } 146 } 147 #endregion 148 149 #region IsReadOnly (Public) 150 /// <summary> 151 /// Is read only 152 /// </summary> 153 /// <returns> 154 /// True if the list is readonly. Will always return false, this feature is 155 /// not supported by AutoSortList). 156 /// </returns> 157 public bool IsReadOnly 158 { 159 get 160 { 161 return false; 162 } 163 } 164 #endregion 165 166 #region Protected 167 168 #region autoSort (Protected) 169 /// <summary> 170 /// Auto sorting on? 171 /// </summary> 172 /// <typeparam name="T">T</typeparam> 173 protected bool autoSort = true; 174 #endregion 175 176 #region comparer (Protected) 177 /// <summary> 178 /// Comparer for sort. 179 /// </summary> 180 /// <typeparam name="T">T</typeparam> 181 protected IComparer<T> comparer; 182 #endregion 183 184 #region duplicatesAllowed (Protected) 185 /// <summary> 186 /// Duplicates allowed? 187 /// </summary> 188 /// <typeparam name="T">T</typeparam> 189 protected bool duplicatesAllowed; 190 #endregion 191 192 #endregion 193 194 #region Private 195 196 #region data (Private) 197 /// <summary> 198 /// Using List<T>, we can't derive from it to override the methods. 199 /// </summary> 200 /// <typeparam name="T">T</typeparam> 201 private readonly List<T> data = new List<T>(); 202 #endregion 203 204 #endregion 205 206 #region Constructors 207 /// <summary> 208 /// Create auto sort list 209 /// </summary> 210 public AutoSortList() 211 { 212 } 213 214 /// <summary> 215 /// Create auto sort list 216 /// </summary> 217 /// <param name="setAutoSort">Auto sort</param> 218 public AutoSortList(bool setAutoSort) 219 { 220 autoSort = setAutoSort; 221 } 222 223 /// <summary> 224 /// Create auto sort list 225 /// </summary> 226 /// <param name="setAutoSort">Auto sort</param> 227 /// <param name="setComparer">P i comparer</param> 228 public AutoSortList(bool setAutoSort, IComparer<T> setComparer) 229 { 230 autoSort = setAutoSort; 231 comparer = setComparer; 232 } 233 234 /// <summary> 235 /// Create auto sort list 236 /// </summary> 237 /// <param name="setComparer">Comparer</param> 238 public AutoSortList(IComparer<T> setComparer) 239 { 240 comparer = setComparer; 241 } 242 243 /// <summary> 244 /// Create auto sort list 245 /// </summary> 246 /// <param name="setAutoSort">Auto sort</param> 247 /// <param name="setComparer">Comparer</param> 248 /// <param name="setDuplicatesAllowed">Duplicates allowed</param> 249 public AutoSortList( 250 bool setAutoSort, 251 IComparer<T> setComparer, 252 bool setDuplicatesAllowed) 253 { 254 autoSort = setAutoSort; 255 comparer = setComparer; 256 duplicatesAllowed = setDuplicatesAllowed; 257 } 258 #endregion 259 260 #region ICollection<T> Members 261 /// <summary> 262 /// Add inserts an item into the collection. 263 /// It adds to the end if AutoSort is false. Otherwise, it adds in 264 /// the sorted order as determined by BinarySearch(Comparer). 265 /// Returns the position inserted into the list. 266 /// Will throw the exception InvalidOperationException if called when 267 /// AreDuplicatesAllowed is false and you add a duplicate instance. 268 /// </summary> 269 /// <param name="item">Item to add</param> 270 public void Add(T item) 271 { 272 if (autoSort) 273 { 274 int index = -1; 275 if (data.Count == 0) 276 { 277 data.Add(item); 278 } 279 else 280 { 281 index = BinarySearch(item); 282 // not found. 283 // vIndex is the bitwise complement of the position to insert 284 if (index < 0) 285 { 286 index = ~index; 287 if (index >= Count) 288 { 289 data.Add(item); 290 } 291 else 292 { 293 data.Insert(index, item); 294 } 295 } 296 else if (duplicatesAllowed) // already have one 297 { 298 data.Insert(index, item); 299 } 300 else 301 { 302 throw new InvalidOperationException( 303 "The instance " + item + " is a duplicate of one entry " + 304 "already in the list."); 305 } 306 } 307 } 308 else 309 { 310 data.Add(item); 311 } 312 } 313 314 /// <summary> 315 /// Clear 316 /// </summary> 317 public void Clear() 318 { 319 data.Clear(); 320 } 321 322 /// <summary> 323 /// Contains is overridden to use BinarySearch when xAutoSort is true. 324 /// </summary> 325 /// <param name="item">Item to check</param> 326 /// <returns>True if the item was found, otherwise false</returns> 327 public bool Contains(T item) 328 { 329 if (autoSort) 330 { 331 return BinarySearch(item) >= 0; 332 } 333 else 334 { 335 return data.Contains(item); 336 } 337 } 338 339 /// <summary> 340 /// Copy to 341 /// </summary> 342 /// <param name="array">Array</param> 343 /// <param name="arrayIndex">Array index</param> 344 public void CopyTo(T[] array, int arrayIndex) 345 { 346 data.CopyTo(array, arrayIndex); 347 } 348 349 /// <summary> 350 /// Remove 351 /// </summary> 352 /// <param name="item">Item</param> 353 /// <returns>True if the item was removed, otherwise false</returns> 354 public bool Remove(T item) 355 { 356 return data.Remove(item); 357 } 358 #endregion 359 360 #region IEnumerable Members 361 /// <summary> 362 /// Get enumerator 363 /// </summary> 364 /// <returns>IEnumerator</returns> 365 IEnumerator IEnumerable.GetEnumerator() 366 { 367 return data.GetEnumerator(); 368 } 369 #endregion 370 371 #region IEnumerable<T> Members 372 /// <summary> 373 /// Get enumerator 374 /// </summary> 375 /// <returns>IEnumerator</returns> 376 public IEnumerator<T> GetEnumerator() 377 { 378 return data.GetEnumerator(); 379 } 380 #endregion 381 382 #region IList<T> Members 383 /// <summary> 384 /// IndexOf is overridden to optimize the search using BinarySearch when 385 /// AutoSort is true. 386 /// </summary> 387 /// <param name="item">Item to get index from</param> 388 /// <returns>Int</returns> 389 public int IndexOf(T item) 390 { 391 if (AutoSort) 392 { 393 int index = BinarySearch(item); 394 if (index >= 0) // exact match 395 { 396 return index; 397 } 398 else 399 { 400 return -1; 401 } 402 } 403 else 404 { 405 return data.IndexOf(item); 406 } 407 } 408 409 /// <summary> 410 /// Insert is overridden to prevent inserting when AutoSort is true. 411 /// You should use Add() instead. Will throw the exception 412 /// InvalidOperationException if called when AutoSort is true. 413 /// </summary> 414 /// <param name="index">Index</param> 415 /// <param name="item">Item</param> 416 public void Insert(int index, T item) 417 { 418 if (autoSort) 419 { 420 throw new InvalidOperationException( 421 "Cannot insert into a sorted AutoSortList. " + 422 "Use the Add method instead."); 423 } 424 else 425 { 426 data.Insert(index, item); 427 } 428 } 429 430 /// <summary> 431 /// Remove at 432 /// </summary> 433 /// <param name="index">Index</param> 434 public void RemoveAt(int index) 435 { 436 data.RemoveAt(index); 437 } 438 #endregion 439 440 #region AddRange (Public) 441 /// <summary> 442 /// AddRange is overridden to enforce sorting when AutoSort is true. 443 /// </summary> 444 /// <param name="collection">Collection</param> 445 public void AddRange(ICollection<T> collection) 446 { 447 if (autoSort) 448 { 449 // Following the documented rules of ArrayList.AddRange: 450 // If the new Count (the current Count plus the size of the 451 // collection) will be greater than Capacity, the capacity of the 452 // list is either doubled or increased to the new Count, whichever 453 // is greater. 454 if (Count + collection.Count > 455 data.Capacity) 456 { 457 if (data.Capacity * 2 > 458 Count + collection.Count) 459 { 460 data.Capacity = data.Capacity * 2; 461 } 462 else 463 { 464 data.Capacity = Count + collection.Count; 465 } 466 } 467 468 foreach (T value in collection) 469 { 470 Add(value); // this will keep it sorted 471 } 472 } 473 else 474 { 475 data.AddRange(collection); 476 } 477 } 478 #endregion 479 480 #region BinarySearch (Public) 481 /// <summary> 482 /// BinarySearch is overridden to enforce the comparer property. 483 /// </summary> 484 /// <param name="item">Item to check</param> 485 /// <returns>Item index if the item was found, otherwise -1</returns> 486 public int BinarySearch(T item) 487 { 488 if (autoSort) 489 { 490 return data.BinarySearch(item, comparer); 491 } 492 else 493 { 494 return data.BinarySearch(item); 495 } 496 } 497 #endregion 498 499 #region IndexOf (Public) 500 /// <summary> 501 /// IndexOf is overridden to optimize the search using BinarySearch when 502 /// AutoSort is true. 503 /// </summary> 504 /// <param name="item">Item</param> 505 /// <param name="startIndex">Start index</param> 506 /// <returns>Int</returns> 507 public int IndexOf(T item, int startIndex) 508 { 509 if (autoSort) 510 { 511 int index = data.BinarySearch(startIndex, 512 Count - startIndex, item, Comparer); 513 if (index >= 0) // exact match 514 { 515 return index; 516 } 517 else 518 { 519 return -1; 520 } 521 } 522 else 523 { 524 return data.IndexOf(item, startIndex); 525 } 526 } 527 528 /// <summary> 529 /// IndexOf is overridden to optimize the search using BinarySearch when 530 /// AutoSort is true. 531 /// </summary> 532 /// <param name="item">Item</param> 533 /// <param name="startIndex">Start index</param> 534 /// <param name="count">Count</param> 535 /// <returns>Int</returns> 536 public int IndexOf(T item, int startIndex, int count) 537 { 538 if (autoSort) 539 { 540 int index = data.BinarySearch( 541 startIndex, count, item, comparer); 542 if (index >= 0) // exact match 543 { 544 return index; 545 } 546 else 547 { 548 return -1; 549 } 550 } 551 else 552 { 553 return data.IndexOf(item, startIndex, count); 554 } 555 } 556 #endregion 557 558 #region InsertRange (Public) 559 /// <summary> 560 /// InsertRange is overridden to prevent inserting when AutoSort is true. 561 /// You should use AddRange instead. Will throw the exception 562 /// InvalidOperationException if called when AutoSort is true. 563 /// </summary> 564 /// <param name="index">Index</param> 565 /// <param name="collection">Collection</param> 566 public void InsertRange(int index, ICollection<T> collection) 567 { 568 if (autoSort) 569 { 570 throw new InvalidOperationException( 571 "Cannot insert into a sorted AutoSortList. " + 572 "Use the AddRange method instead."); 573 } 574 else 575 { 576 data.InsertRange(index, collection); 577 } 578 } 579 #endregion 580 581 #region Sort (Public) 582 /// <summary> 583 /// Sort is overridden to enforce the Comparer property 584 /// </summary> 585 public void Sort() 586 { 587 if (autoSort) 588 { 589 data.Sort(comparer); 590 } 591 else 592 { 593 data.Sort(); 594 } 595 } 596 #endregion 597 598 #region Reverse (Public) 599 /// <summary> 600 /// Reverse is overridden to prevent reversing when AutoSort is true. 601 /// You should supply an IComparer that reverses the sort and call Sort(). 602 /// Will throw the exception InvalidOperationException if called when 603 /// AutoSort is true. 604 /// </summary> 605 public void Reverse() 606 { 607 if (autoSort) 608 { 609 throw new InvalidOperationException( 610 "Cannot reverse into a sorted AutoSortList. " + 611 "Use an Comparer whose sort is reversed and call Sort()."); 612 } 613 else 614 { 615 data.Reverse(); 616 } 617 } 618 619 /// <summary> 620 /// Reverse is overridden to prevent reversing when AutoSort is true. 621 /// You should supply an IComparer that reverses the sort and call Sort(). 622 /// Will throw the exception InvalidOperationException if called when 623 /// AutoSort is true. 624 /// </summary> 625 /// <param name="index">Index</param> 626 /// <param name="count">Count</param> 627 public void Reverse(int index, int count) 628 { 629 if (autoSort) 630 { 631 throw new InvalidOperationException( 632 "Cannot reverse into a sorted AutoSortList. " + 633 "Use an Comparer whose sort is reversed and call Sort()."); 634 } 635 else 636 { 637 data.Reverse(index, count); 638 } 639 } 640 #endregion 641 } 642 643 /// <summary> 644 /// AutoSortList tests helper class, must be outside of the generic 645 /// AutoSortList class to be able to test it with TestDriven.net. Also must 646 /// be named uniquely for Resharpers run test feature. 647 /// </summary> 648 public class AutoSortListTests 649 { 650 #region TestAddingEntries 651 /// <summary> 652 /// Test adding entries 653 /// </summary> 654 [Test] 655 public void TestAddingEntries() 656 { 657 AutoSortList<int> testAutoSortList = new AutoSortList<int>(); 658 testAutoSortList.Add(1); 659 testAutoSortList.Add(10); 660 testAutoSortList.Add(5); 661 testAutoSortList.Add(3); 662 // Check if list is sorted 663 Assert.Equal("1, 3, 5, 10", 664 testAutoSortList.Write()); 665 } 666 #endregion 667 } 668}