This is a submission for the over engineered category. It takes the challenge one step further by:
1. accepting pasted paragraphs of text or manually inputed text;
2. strips text of punctuation and other unwanted characters to split out the words;
3. finds the duplicates;
4. then to give the user feedback, works back through the text, using word recognition, applies colour-coded highlighting to both the text and the repeated words List.
Colour-coding helps the user quickly visualise. Here is a
screenshot[
^] to demonstrate this. All this is done in real time as the text changes.
Normally I would do this as a MVVM project, however, to keep it tight for this challenge, I've kept it all in the code-behind.
It is using the
RepeatedWords
extension method from my Solution 4.
Here is the code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfFindRepeats
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
highlightColors = (typeof(Colors)).GetPropertyBag()
.Where(x =>
{
var c = (Color)x.Value; return (c.R > 0x50 && c.R < 0xEF) &&
(c.G > 0x50 && c.G < 0xEF) &&
(c.B > 0x50 && c.B < 0xEF);
})
.Select(x => (Color)x.Value)
.OrderBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.R)
.ToList();
InitializeComponent();
DataContext = this;
}
private Brush highlightForeground
= new SolidColorBrush(SystemColors.ControlTextColor);
private List<Color> highlightColors;
private string userText;
public string UserText
{
get { return userText; }
set
{
ProcessText(value);
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(UserText)));
}
}
private int matchCount;
public int MatchCount
{
get { return matchCount; }
set
{
matchCount = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(MatchCount)));
}
}
private int repeatedCount;
public int RepeatedCount
{
get { return repeatedCount; }
set
{
repeatedCount = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(RepeatedCount)));
}
}
private void ProcessText(string text)
{
MatchCount = 0;
if (text == userText) return;
if (string.IsNullOrEmpty(text))
{
FormattedText.Inlines.Clear();
return;
}
userText = text;
var repeats = text.StripPuncuation().StripCarriageReturn(" ").ToLower()
.Split(new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries)
.GetRepeats().OrderByDescending(x => x).ToList();
if (repeats != null)
{
RepeatedCount = repeats.Count;
var repeatedWords = "[ " +
string.Join(", ", repeats.OrderBy(x => x)) + " ]";
Highlight(repeatedWords, repeats,
repeatedWords.FindMatches(repeats), RepeatedWords);
MatchCount = Highlight(text, repeats,
text.FindMatches(repeats), FormattedText);
}
}
private int Highlight
(string text, List<string> repeats,
IEnumerable<Tuple<int, int, int>> matches, TextBlock textControl)
{
int ndx = 0, c = 0;
textControl.Inlines.Clear();
foreach (var match in text.NextMatchOf(repeats.ToList(), matches))
{
if(match.Item2 > ndx)
textControl.Inlines
.Add(GetRunForText(text.Substring(ndx,
match.Item2 - ndx), false, -1));
textControl.Inlines
.Add(GetRunForText(text.Substring(match.Item2,
match.Item3 - match.Item2), true, match.Item1));
ndx = match.Item3;
c++;
}
if (ndx < text.Length)
textControl.Inlines
.Add(GetRunForText(text.Substring(ndx),
false, -1));
return c;
}
private Run GetRunForText(string text, bool isHighlighted, int indexColor)
=> new Run(text)
{
Foreground = isHighlighted ? highlightForeground : Foreground,
Background = isHighlighted
? new SolidColorBrush(highlightColors[indexColor
% highlightColors.Count])
: Background
};
public event PropertyChangedEventHandler PropertyChanged;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
UserText = @"Coding challenge: find the repeated items in a collection of elements.
Today's coding challenge is pretty loose in terms of how you approach it and how you interpret the problem.
Given a collection of items (integers, strings, objects - whatever) determine the set of subitems in that collection that are repeated.
For example
{1,2,3,3,4,5,5,6} => {3,5}
Points are awarded for elegance, speed, and excessive use of complicated logic. Over engineering the solution will gain you favours.
Last week's winner was Peter Leow, mainly because Graeme_Grant is killing it and I wanted to award a new player. Graeme's Delphi solution brought a tear to my eye. Peter: contact Sean for a trinket.";
}
}
public static class HelperExtension
{
public static IEnumerable<T> GetRepeats<T>(this IList<T> items)
=> items?.Intersect(
items.Where(x => items.Where(y => Equals(x, y)).Count() > 1));
public static IEnumerable<Tuple<int, int, int>> NextMatchOf
(this string text, List<string> words,
IEnumerable<Tuple<int, int, int>> matches)
{
int f = -1;
foreach (var match in matches.OrderBy(x => x.Item2)
.ThenByDescending(x => x.Item3 - x.Item2))
{
if (match.Item2 > f && text.IsWord(words[match.Item1], match.Item2))
{
yield return match;
f = match.Item3;
}
}
}
public static IEnumerable<Tuple<int, int, int>> FindMatches
(this string text, IEnumerable<string> repeats)
{
int e, f, p, l = e = f = p = 0;
foreach (var key in repeats)
{
l = key.Length; e = f = 0;
for (;;)
{
f = text.IndexOf(key, e,
StringComparison.InvariantCultureIgnoreCase);
if (f == -1) break;
e = f + l;
yield return new Tuple<int, int, int>(p, f, e);
}
p++;
}
}
public static string StripPuncuation(this string input)
{
var sb = new StringBuilder();
foreach (var c in input)
sb.Append(char.IsPunctuation(c) ? ' ' : c);
return sb.ToString();
}
public static string StripCarriageReturn(this string text,
string replaceWith = "")
=> !string.IsNullOrEmpty(text)
? text.Replace(oldValue: "\r\n", newValue: replaceWith)
.Replace(oldValue: "\r", newValue: replaceWith)
.Replace(oldValue: "\n", newValue: replaceWith)
: text;
public static bool IsWord(this string text, string key, int index)
{
bool b = true;
if (index > 0)
{
var ptr = index - 1;
b = IsWordBoundaryChar(text[ptr]);
}
var c = index + key.Length;
return (c < text.Length ? IsWordBoundaryChar(text[c]) : true) && b;
}
public static bool IsWordBoundaryChar(this char c)
=> char.IsPunctuation(c) || c == ' ' || c == '\r' || c == '\n';
public static Dictionary<string, object> GetPropertyBag(this Type t)
{
const BindingFlags flags
= BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
var map = new Dictionary<string, object>();
foreach (var prop in t.GetProperties(flags))
map[prop.Name] = prop.GetValue(null, null);
return map;
}
}
}
And Here is the Xaml page:
<Window
x:Class="WpfFindRepeats.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfFindRepeats"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Loaded="Window_Loaded" Height="600" Width="800"
Title="Code Project Weekly Challenge: Find Repeats">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Find Repeated Items in a Collection of Elements"
HorizontalAlignment="Center"
FontSize="16" Grid.ColumnSpan="2" Margin="0 10"/>
<TextBlock Text="Input:" Grid.Row="1" VerticalAlignment="Top" Margin="10"/>
<TextBox x:Name="UserInput"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
TextWrapping="Wrap"
Text="{Binding UserText, Delay=200,
UpdateSourceTrigger=PropertyChanged}"
Grid.Row="1" Grid.Column="1" Margin="10"
VerticalAlignment="Stretch" AcceptsReturn="True"/>
<TextBlock Text="Output:" Grid.Row="2" VerticalAlignment="Top" Margin="10"/>
<Border Grid.Row="2" Grid.Column="1" Margin="10"
BorderThickness="{Binding ElementName=UserInput,
Path=BorderThickness}"
BorderBrush="{Binding ElementName=UserInput, Path=BorderBrush}">
<ScrollViewer Padding="4" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="FormattedText" TextWrapping="Wrap"/>
</ScrollViewer>
</Border>
<TextBlock Text="Repeated Words:" Grid.Row="3" Margin="10"/>
<StackPanel Grid.Row="3" Grid.Column="1" Margin="10">
<ScrollViewer MaxHeight="70" Padding="4"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="RepeatedWords" TextWrapping="Wrap"/>
</ScrollViewer>
<TextBlock Margin="0 10 0 0">
<Run FontWeight="Bold" Text="Keys:"/>
<Run Text="{Binding RepeatedCount}"/>
<Run FontWeight="Bold" Text="... Total Matches:"/>
<Run Text="{Binding MatchCount}"/>
</TextBlock>
</StackPanel>
</Grid>
</Window>
You can download the
project[
^] and try it out for yourself.
Update: I found some time to refactor this WPF solution into a MVVM solution. I will only highlight key parts but you can download the
project[
^] and try it.
1. Code Project Extension
using System.Collections.Generic;
using System.Linq;
namespace WpfFindRepeats.Mvvm.Extensions
{
public static class CodeProjectExtension
{
public static IEnumerable<T> GetRepeats<T>(this IList<T> items)
=> items?.Intersect(
items.Where(x => items.Where(y => Equals(x, y)).Count() > 1));
}
}
2. MainViewModel
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Media;
using WpfFindRepeats.Mvvm.Extensions;
namespace WpfFindRepeats.Mvvm.ViewModels
{
class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
HighlightColors = (typeof(Colors)).GetPropertyBag()
.Where(x =>
{
var c = (Color)x.Value; return (c.R > 0x50 && c.R < 0xEF) &&
(c.G > 0x50 && c.G < 0xEF) &&
(c.B > 0x50 && c.B < 0xEF);
})
.Select(x => (Color)x.Value)
.OrderBy(x => x.G).ThenBy(x => x.B).ThenBy(x => x.R)
.ToList();
}
public List<Color> HighlightColors { get; set; }
private string userText;
public string UserText
{
get { return userText; }
set
{
ProcessText(value);
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(UserText)));
}
}
private int matchCount;
public int MatchCount
{
get { return matchCount; }
set
{
matchCount = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(MatchCount)));
}
}
private int repeatedCount;
public int RepeatedCount
{
get { return repeatedCount; }
set
{
repeatedCount = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(RepeatedCount)));
}
}
private string repeatedWords;
public string RepeatedWords
{
get { return repeatedWords; }
set
{
if (repeatedWords != value)
{
repeatedWords = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(RepeatedWords)));
}
}
}
public ObservableCollection<string> Repeats { get; set; } =
new ObservableCollection<string>();
private void ProcessText(string text)
{
if (text == userText) return;
if (string.IsNullOrEmpty(text))
return;
userText = text;
Repeats.Clear();
Repeats.AddRange(
text.StripPuncuation().StripCarriageReturn(" ").ToLower()
.Split(new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries)
.GetRepeats().OrderByDescending(x => x).ToList());
if (Repeats != null)
{
RepeatedCount = Repeats.Count;
RepeatedWords = "[ " +
string.Join(", ", Repeats.OrderBy(x => x)) + " ]";
}
}
public void InitText()
{
UserText = @"Coding challenge: find the repeated items in a collection of elements.
Today's coding challenge is pretty loose in terms of how you approach it and how you interpret the problem.
Given a collection of items (integers, strings, objects - whatever) determine the set of subitems in that collection that are repeated.
For example
{1,2,3,3,4,5,5,6} => {3,5}
Points are awarded for elegance, speed, and excessive use of complicated logic. Over engineering the solution will gain you favours.
Last week's winner was Peter Leow, mainly because Graeme_Grant is killing it and I wanted to award a new player. Graeme's Delphi solution brought a tear to my eye. Peter: contact Sean for a trinket.";
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
3.
HighlightingTextBlock
control:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfFindRepeats.Mvvm.Controls
{
[TemplatePart(Name = HighlightTextBlockName, Type = typeof(TextBlock))]
public class HighlightingTextBlock : Control
{
private const string HighlightTextBlockName = "PART_HighlightTextblock";
public static readonly DependencyProperty MatchCountProperty =
DependencyProperty.Register("MatchCount", typeof(int), typeof(HighlightingTextBlock),
new PropertyMetadata(0, null));
public static readonly DependencyProperty HighlightSourceProperty =
DependencyProperty.Register("HighlightSource", typeof(ObservableCollection<string>), typeof(HighlightingTextBlock),
new PropertyMetadata(null, OnHighlightSourcePropertyChanged));
public static readonly DependencyProperty TextProperty = TextBlock.TextProperty.AddOwner(
typeof(HighlightingTextBlock),
new PropertyMetadata(string.Empty, OnTextPropertyChanged));
public static readonly DependencyProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(
typeof(HighlightingTextBlock),
new PropertyMetadata(TextWrapping.NoWrap));
public static readonly DependencyProperty TextTrimmingProperty = TextBlock.TextTrimmingProperty.AddOwner(
typeof(HighlightingTextBlock),
new PropertyMetadata(TextTrimming.None));
public static readonly DependencyProperty HighlightForegroundProperty =
DependencyProperty.Register("HighlightForeground", typeof(Brush),
typeof(HighlightingTextBlock),
new PropertyMetadata(Brushes.Black));
public static readonly DependencyProperty HighlightColorsProperty =
DependencyProperty.Register("HighlightColors", typeof(List<Color>),
typeof(HighlightingTextBlock),
new PropertyMetadata(new List<Color> { Colors.Yellow }));
private TextBlock highlightTextBlock;
static HighlightingTextBlock()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(HighlightingTextBlock),
new FrameworkPropertyMetadata(typeof(HighlightingTextBlock)));
}
public int MatchCount
{
get { return (int)GetValue(MatchCountProperty); }
set { SetValue(MatchCountProperty, value); }
}
public List<Color> HighlightColors
{
get { return (List<Color>)GetValue(HighlightColorsProperty); }
set { SetValue(HighlightColorsProperty, value); }
}
public Brush HighlightForeground
{
get { return (Brush)GetValue(HighlightForegroundProperty); }
set { SetValue(HighlightForegroundProperty, value); }
}
public ObservableCollection<string> HighlightSource
{
get { return (ObservableCollection<string>)GetValue(HighlightSourceProperty); }
set { SetValue(HighlightSourceProperty, value); }
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public TextWrapping TextWrapping
{
get { return (TextWrapping)GetValue(TextWrappingProperty); }
set { SetValue(TextWrappingProperty, value); }
}
public TextTrimming TextTrimming
{
get { return (TextTrimming)GetValue(TextTrimmingProperty); }
set { SetValue(TextTrimmingProperty, value); }
}
private static void OnHighlightSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textblock = (HighlightingTextBlock)d;
textblock.ProcessTextChanged(textblock.Text, e.NewValue as ObservableCollection<string>);
}
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textblock = (HighlightingTextBlock)d;
textblock.ProcessTextChanged(e.NewValue as string, textblock.HighlightSource);
}
private void ProcessTextChanged(string text, ObservableCollection<string> repeats)
{
if (highlightTextBlock == null)
return;
highlightTextBlock.Inlines.Clear();
MatchCount = 0;
if (highlightTextBlock == null || string.IsNullOrWhiteSpace(text))
return;
if (repeats == null || repeats.Count == 0)
{
highlightTextBlock.Inlines.Add(new Run(text));
return;
}
int ndx = 0, c = 0;
foreach (var match in text.NextMatchOf(repeats, text.FindMatches(repeats)))
{
if (match.Item2 > ndx)
highlightTextBlock.Inlines
.Add(GetRunForText(text.Substring(ndx,
match.Item2 - ndx), false, -1));
highlightTextBlock.Inlines
.Add(GetRunForText(text.Substring(match.Item2,
match.Item3 - match.Item2), true, match.Item1));
ndx = match.Item3;
c++;
}
if (ndx < text.Length)
highlightTextBlock.Inlines
.Add(GetRunForText(text.Substring(ndx),
false, -1));
MatchCount = c;
}
private Run GetRunForText(string text, bool isHighlighted, int indexColor)
=> new Run(text)
{
Foreground = isHighlighted ? HighlightForeground : Foreground,
Background = isHighlighted
? new SolidColorBrush(HighlightColors[indexColor
% HighlightColors.Count])
: Background
};
public override void OnApplyTemplate()
{
highlightTextBlock = GetTemplateChild(HighlightTextBlockName) as TextBlock;
if (highlightTextBlock == null) return;
ProcessTextChanged(Text, HighlightSource);
}
}
}
4. The
HighlightingTextBlock
control
DefaultTemplate
:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:WpfFindRepeats.Mvvm.Controls">
<Style TargetType="{x:Type c:HighlightingTextBlock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type c:HighlightingTextBlock}">
<Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<TextBlock x:Name="PART_HighlightTextblock"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"
FontFamily="{TemplateBinding FontFamily}"
FontStretch="{TemplateBinding FontStretch}"
FontStyle="{TemplateBinding FontStyle}"
Margin="{TemplateBinding Margin}"
Padding="{TemplateBinding Padding}"
TextWrapping="{TemplateBinding TextWrapping}"
TextTrimming="{TemplateBinding TextTrimming}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
5. And Here is the Xaml page (no code-behind):
<Window
x:Class="WpfFindRepeats.Mvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:WpfFindRepeats.Mvvm.ViewModels"
xmlns:ex="clr-namespace:WpfFindRepeats.Mvvm.Extensions"
xmlns:c="clr-namespace:WpfFindRepeats.Mvvm.Controls"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Height="600" Width="800" Loaded="{ex:MethodBinding InitText}"
Title="Code Project Weekly Challenge: Find Repeats">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Find Repeated Items in a Collection of Elements"
HorizontalAlignment="Center"
FontSize="16" Grid.ColumnSpan="2" Margin="0 10"/>
<TextBlock Text="Input:" Grid.Row="1" VerticalAlignment="Top" Margin="10"/>
<TextBox x:Name="UserInput"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
TextWrapping="Wrap"
Text="{Binding UserText, Delay=200,
UpdateSourceTrigger=PropertyChanged}"
Grid.Row="1" Grid.Column="1" Margin="10"
VerticalAlignment="Stretch" AcceptsReturn="True"/>
<TextBlock Text="Output:" Grid.Row="2" VerticalAlignment="Top" Margin="10"/>
<Border Grid.Row="2" Grid.Column="1" Margin="10"
BorderThickness="{Binding ElementName=UserInput,
Path=BorderThickness}"
BorderBrush="{Binding ElementName=UserInput, Path=BorderBrush}">
<ScrollViewer Padding="4" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<c:HighlightingTextBlock x:Name="OutText"
Text="{Binding UserText}"
HighlightSource="{Binding Repeats}"
TextWrapping="Wrap"
HighlightForeground="Black"
HighlightColors="{Binding HighlightColors}"
MatchCount="{Binding MatchCount, Mode=TwoWay}"/>
</ScrollViewer>
</Border>
<TextBlock Text="Repeated Words:" Grid.Row="3" Margin="10"/>
<StackPanel Grid.Row="3" Grid.Column="1" Margin="10">
<ScrollViewer MaxHeight="70" Padding="4"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<c:HighlightingTextBlock Text="{Binding RepeatedWords}"
HighlightSource="{Binding Repeats}"
TextWrapping="Wrap"
HighlightForeground="Black"
HighlightColors="{Binding HighlightColors}"/>
</ScrollViewer>
<TextBlock Margin="0 10 0 0">
<Run FontWeight="Bold" Text="Keys:"/>
<Run Text="{Binding RepeatedCount}"/>
<Run FontWeight="Bold" Text="... Total Matches:"/>
<Run Text="{Binding MatchCount}"/>
</TextBlock>
</StackPanel>
</Grid>
</Window>