From 9e4462127725436eb76878fc0ed9378164bb9abd Mon Sep 17 00:00:00 2001 From: "Brian-J. Ebeling" Date: Sun, 17 Mar 2024 04:37:22 +0100 Subject: [PATCH] refactor(Paginator): extract filtering of text input into function Adds a new constructor parameter that accepts a function for filtering text input (i.e. how text is filtered when typing into a Select or MultiSelect Prompt). This allows developers to override the default behavior (.Contains(keyword)) with custom behavior such as fuzzy matching. This helps with long inputs, where you may have multiple items in your strings but are unsure about the order or exact input. --- Sharprompt.Example/Program.cs | 2 ++ Sharprompt.Tests/PaginatorTests.cs | 17 ++++++++++------- Sharprompt/Forms/MultiSelectForm.cs | 2 +- Sharprompt/Forms/SelectForm.cs | 2 +- Sharprompt/Internal/Paginator.cs | 8 +++++--- Sharprompt/MultiSelectOptions.cs | 2 ++ Sharprompt/Prompt.Basic.cs | 16 ++++++++++++++-- Sharprompt/SelectOptions.cs | 2 ++ 8 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Sharprompt.Example/Program.cs b/Sharprompt.Example/Program.cs index b1bc839..323d366 100644 --- a/Sharprompt.Example/Program.cs +++ b/Sharprompt.Example/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text; using System.Text.Json; @@ -79,6 +80,7 @@ private static void RunSelectSample() private static void RunMultiSelectSample() { var options = Prompt.MultiSelect("Which cities would you like to visit?", new[] { "Seattle", "London", "Tokyo", "New York", "Singapore", "Shanghai" }, pageSize: 3, defaultValues: new[] { "Tokyo" }); + Console.WriteLine($"You picked {string.Join(", ", options)}"); } diff --git a/Sharprompt.Tests/PaginatorTests.cs b/Sharprompt.Tests/PaginatorTests.cs index 3d9b8c9..308f48d 100644 --- a/Sharprompt.Tests/PaginatorTests.cs +++ b/Sharprompt.Tests/PaginatorTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Sharprompt.Internal; @@ -8,10 +9,12 @@ namespace Sharprompt.Tests; public class PaginatorTests { + private readonly Func _containsTextInputFilter = (item, keyword) => item.ToString().Contains(keyword, StringComparison.OrdinalIgnoreCase); + [Fact] public void Basic() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); var currentItems1 = paginator.CurrentItems; @@ -29,7 +32,7 @@ public void Basic() [Fact] public void Filter_NotEmpty() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); paginator.UpdateFilter("0"); @@ -42,7 +45,7 @@ public void Filter_NotEmpty() [Fact] public void Filter_Empty() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); paginator.UpdateFilter("x"); @@ -54,7 +57,7 @@ public void Filter_Empty() [Fact] public void SelectedItem() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); paginator.NextPage(); paginator.NextItem(); @@ -68,7 +71,7 @@ public void SelectedItem() [Fact] public void SelectedItem_NotSelected() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); var selected = paginator.TryGetSelectedItem(out _); @@ -78,7 +81,7 @@ public void SelectedItem_NotSelected() [Fact] public void SelectedItem_EmptyList() { - var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), _containsTextInputFilter); paginator.UpdateFilter("x"); paginator.NextItem(); diff --git a/Sharprompt/Forms/MultiSelectForm.cs b/Sharprompt/Forms/MultiSelectForm.cs index 0b477a1..c55872f 100644 --- a/Sharprompt/Forms/MultiSelectForm.cs +++ b/Sharprompt/Forms/MultiSelectForm.cs @@ -15,7 +15,7 @@ public MultiSelectForm(MultiSelectOptions options) options.EnsureOptions(); _options = options; - _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Empty, options.TextSelector) + _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Empty, options.TextSelector, options.TextInputFilter) { LoopingSelection = options.LoopingSelection }; diff --git a/Sharprompt/Forms/SelectForm.cs b/Sharprompt/Forms/SelectForm.cs index 241db25..9475af1 100644 --- a/Sharprompt/Forms/SelectForm.cs +++ b/Sharprompt/Forms/SelectForm.cs @@ -14,7 +14,7 @@ public SelectForm(SelectOptions options) options.EnsureOptions(); _options = options; - _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Create(options.DefaultValue), options.TextSelector) + _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Create(options.DefaultValue), options.TextSelector, options.TextInputFilter) { LoopingSelection = options.LoopingSelection }; diff --git a/Sharprompt/Internal/Paginator.cs b/Sharprompt/Internal/Paginator.cs index e1e5680..c367522 100644 --- a/Sharprompt/Internal/Paginator.cs +++ b/Sharprompt/Internal/Paginator.cs @@ -8,17 +8,19 @@ namespace Sharprompt.Internal; internal class Paginator : IEnumerable where T : notnull { - public Paginator(IEnumerable items, int pageSize, Optional defaultValue, Func textSelector) + public Paginator(IEnumerable items, int pageSize, Optional defaultValue, Func textSelector, Func textInputFilter) { _items = items.ToArray(); _pageSize = pageSize <= 0 ? _items.Length : Math.Min(pageSize, _items.Length); _textSelector = textSelector; + _filterFunc = textInputFilter; InitializeDefaults(defaultValue); } private readonly T[] _items; private readonly Func _textSelector; + private readonly Func _filterFunc; private int _pageSize; private T[] _filteredItems = Array.Empty(); @@ -144,8 +146,8 @@ public void UpdatePageSize(int newPageSize) private void UpdateFilteredItems() { - _filteredItems = _items.Where(x => _textSelector(x).IndexOf(FilterKeyword, StringComparison.OrdinalIgnoreCase) != -1) - .ToArray(); + _filteredItems = _items.Where(x => _filterFunc(x, FilterKeyword)) + .ToArray(); PageCount = (_filteredItems.Length - 1) / _pageSize + 1; diff --git a/Sharprompt/MultiSelectOptions.cs b/Sharprompt/MultiSelectOptions.cs index 02e636a..948e47c 100644 --- a/Sharprompt/MultiSelectOptions.cs +++ b/Sharprompt/MultiSelectOptions.cs @@ -32,6 +32,8 @@ public MultiSelectOptions() public Func TextSelector { get; set; } = x => x.ToString()!; + public Func TextInputFilter { get; set; } = (item, keyword) => item.ToString().Contains(keyword, StringComparison.OrdinalIgnoreCase); + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); public bool LoopingSelection { get; set; } = true; diff --git a/Sharprompt/Prompt.Basic.cs b/Sharprompt/Prompt.Basic.cs index 2a12620..d8a3ee3 100644 --- a/Sharprompt/Prompt.Basic.cs +++ b/Sharprompt/Prompt.Basic.cs @@ -106,7 +106,7 @@ public static T Select(Action> configure) where T : notnull return Select(options); } - public static T Select(string message, IEnumerable? items = default, int pageSize = int.MaxValue, object? defaultValue = default, Func? textSelector = default) where T : notnull + public static T Select(string message, IEnumerable? items = default, int pageSize = int.MaxValue, object? defaultValue = default, Func? textSelector = default, Func? textInputFilter = default) where T : notnull { return Select(options => { @@ -124,6 +124,11 @@ public static T Select(string message, IEnumerable? items = default, int p { options.TextSelector = textSelector; } + + if (textInputFilter is not null) + { + options.TextInputFilter = textInputFilter; + } }); } @@ -143,7 +148,9 @@ public static IEnumerable MultiSelect(Action> config return MultiSelect(options); } - public static IEnumerable MultiSelect(string message, IEnumerable? items = null, int pageSize = int.MaxValue, int minimum = 1, int maximum = int.MaxValue, IEnumerable? defaultValues = default, Func? textSelector = default) where T : notnull + public static IEnumerable MultiSelect(string message, + IEnumerable? items = null, int pageSize = int.MaxValue, int minimum = 1, int maximum = int.MaxValue, + IEnumerable? defaultValues = default, Func? textSelector = default, Func? textInputFilter = default) where T : notnull { return MultiSelect(options => { @@ -167,6 +174,11 @@ public static IEnumerable MultiSelect(string message, IEnumerable? item { options.TextSelector = textSelector; } + + if (textInputFilter is not null) + { + options.TextInputFilter = textInputFilter; + } }); } diff --git a/Sharprompt/SelectOptions.cs b/Sharprompt/SelectOptions.cs index e3c2e15..6e28790 100644 --- a/Sharprompt/SelectOptions.cs +++ b/Sharprompt/SelectOptions.cs @@ -27,6 +27,8 @@ public SelectOptions() public Func TextSelector { get; set; } = x => x.ToString()!; + public Func TextInputFilter { get; set; } = (item, keyword) => item.ToString().Contains(keyword, StringComparison.OrdinalIgnoreCase); + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); public bool LoopingSelection { get; set; } = true;