The code for this post can be found in this github project.
Occasionally you may present an interface which allows the user to select a subset of specific items. You may have a setting which allows the user to configure for example a set of plugins, turning on or off certain plugins or features.
At the same time it may be desirable to present an abbreviated notation for those items. As an example, if you were presenting a selection from alphabetic characters, you may want to present them as a series of ranges; if you had A,B,C,D,E, and Q selected, for example, you may want to show it as “A-E,Q”.
The first step, then, would be to define a range. We can then take appropriate inputs, generate a list of ranges, and then convert that list of ranges into a string expression to provide the output we are looking for.
For flexibility we would like many aspects to be adjustable, in particular, it would be nice to be able to adjust the formatting of each range based on other information, so rather than hard-coding an appropriate ToString() routine, we’ll have it call a custom function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class SelectionRange<T> { private delegate String FormatRangeFunc(T StartRange, T EndRange); private FormatRangeFunc RangeFormatter = DefaultRangeFormat; private static String DefaultRangeFormat(T StartRange,T EndRange) { if (StartRange.Equals(EndRange)) return StartRange.ToString(); return StartRange + "-" + EndRange; } public SelectionRange(T pStartRange, T pEndRange,Func<T,T, String> pRangeFormatter) { StartRange = pStartRange; EndRange = pEndRange; if (pRangeFormatter != null) RangeFormatter = (range, endRange) => pRangeFormatter(range, endRange); else RangeFormatter = DefaultRangeFormat; } public SelectionRange(T pStartRange,T pEndRange):this(pStartRange,pEndRange,DefaultRangeFormat) { } public T StartRange { get; private set; } public T EndRange { get; private set; } public override string ToString() { return RangeFormatter(StartRange, EndRange); } } |
Pretty straightforward- a starting point, an ending point, and some string formatting. Now, one might wonder about the lack of an IComparable constraint on the type parameter. That would make sense for certain types of data being collated but in some cases the “data” doesn’t have a type-specific succession.
Now, we need to write a routine that will return an enumeration of these ranges given a list of all the items and a list of the selected items. This, too, is relatively straightforward. Instead of a routine this could also be encapsulated as a separate class with member variables to customize the formatted output. As with any programming problem, there are many ways to do things, and the trick is finding the right balance, and in some cases a structured approach to a problem can be suitable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/// <summary> /// Given a list of selected entries, and a full listing of those entries, retrieves a series of selection ranges representing the specified selection within that full listing. /// </summary> /// <param name="SelectedItems">Items that are selected</param> /// <param name="FullListing">Full list of selectable items.</param> /// <returns></returns> public static IEnumerable<SelectionRange<T>> GetSelectionRanges<T>(IList<T> pSelectedItems, IList<T> pFullListing, Func<T, T, String> pRangeFormatter = null) { //make copies of both listings, sorting them. List<T> SelectedItems = (from sel in pSelectedItems orderby sel select sel).ToList(); List<T> FullListing = (from sel in pFullListing orderby sel select sel).ToList(); //Validation: Make sure SelectedItems is a subset of FullListing. foreach (var verifySelection in SelectedItems) { if (!FullListing.Contains(verifySelection)) throw new ArgumentException("Selection List contains entries not present in full listing.", nameof(SelectedItems)); } //iterate through every element in SelectedItems. List<SelectionRange<T>> ChosenRanges = new List<SelectionRange<T>>(); for (int i = 0; i < SelectedItems.Count; i++) { T StartItem = SelectedItems[i]; //find the index of this item in the full listing. int StartFullIndex = FullListing.IndexOf(StartItem); int offset = 0; while (i + offset < (SelectedItems.Count - 1) && SelectedItems[i + offset].Equals(FullListing[StartFullIndex + offset])) { offset++; } if (offset > 0) offset--; T EndItem = SelectedItems[i + offset]; if (offset == 1) { ChosenRanges.Add(new SelectionRange<T>(StartItem, StartItem, pRangeFormatter)); ChosenRanges.Add(new SelectionRange<T>(EndItem, EndItem, pRangeFormatter)); } else { SelectionRange<T> BuildRange = new SelectionRange<T>(StartItem, EndItem); ChosenRanges.Add(BuildRange); } i += offset; } return ChosenRanges; } |
Sometimes you might not have a full list of the items in question, but you might be able to indicate what an item is followed by. For this, I constructed a separate routine with a similar structure which instead uses a function to callback and determine the item that follows another. For integer types we can just add 1, for example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public static IEnumerable<SelectionRange<T>> GetSelectionRanges<T>(IList<T> pSelectedItems, Succ<T> Successor, Func<T, T, String> pRangeFormatter = null) { List<T> SelectedItems = (from sel in pSelectedItems orderby sel select sel).ToList(); List<SelectionRange<T>> ChosenRanges = new List<SelectionRange<T>>(); for (int i = 0; i < SelectedItems.Count; i++) { T StartItem = SelectedItems[i]; T CurrentItem = StartItem; int offset = 0; while ((i + offset < (SelectedItems.Count - 1)) && (Successor(CurrentItem).Equals(SelectedItems[i + offset]))) { CurrentItem = Successor(CurrentItem); offset++; } if (offset > 0) offset--; T EndItem = SelectedItems[i + offset]; if (offset == 1) { ChosenRanges.Add(new SelectionRange<T>(StartItem, StartItem)); ChosenRanges.Add(new SelectionRange<T>(EndItem, EndItem)); } else { SelectionRange<T> BuildRange = new SelectionRange<T>(StartItem, EndItem,pRangeFormatter); ChosenRanges.Add(BuildRange); } i += offset; } return ChosenRanges; } |
This is expanded in the github project I linked above, which also features a number of other helper routines for a few primitive types as well as example usage. In particular, the most useful “helper” is the routine that simply joins the results of these functions into a resulting string:
1 2 3 4 5 6 7 8 |
public static String FormatSelection<T>(String pSeparator,IList<T> pSelectedItems,IList<T> pFullListing,Func<T,T,String> pRangeFormatter=null) { return String.Join(pSeparator, GetSelectionRanges<T>(pSelectedItems, pFullListing, pRangeFormatter)); } public static String FormatSelection<T>(String pSeparator,IList<T> pSelectedItems,Succ<T> Successor,Func<T,T,String> pRangeFormatter = null) { return String.Join(",", GetSelectionRanges(pSelectedItems, Successor, pRangeFormatter)); } |
Have something to say about this post? Comment!