From 555eb0b316485c0ab6a4688120d3dfcc06333f2e Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 30 Jan 2024 07:38:04 -0600 Subject: [PATCH] More clean up and better caching. --- .../DefaultProfaneContentCensorService.cs | 87 +++++-------------- .../IProfaneContentCensorService.cs | 10 --- .../Internals/ProfaneFilter.cs | 12 +++ ...DefaultProfaneContentCensorServiceTests.cs | 17 ---- 4 files changed, 36 insertions(+), 90 deletions(-) create mode 100644 src/ProfanityFilter.Services/Internals/ProfaneFilter.cs diff --git a/src/ProfanityFilter.Services/DefaultProfaneContentCensorService.cs b/src/ProfanityFilter.Services/DefaultProfaneContentCensorService.cs index 1f4afbe..7abcfdb 100644 --- a/src/ProfanityFilter.Services/DefaultProfaneContentCensorService.cs +++ b/src/ProfanityFilter.Services/DefaultProfaneContentCensorService.cs @@ -12,13 +12,14 @@ internal sealed class DefaultProfaneContentCensorService(IMemoryCache cache) : I /// /// A representing the asynchronous operation that /// returns a readonly dictionary of all profane words. - private async Task>>> ReadAllProfaneWordsAsync() + private async Task> ReadAllProfaneWordsAsync() { - return await cache.GetOrCreateAsync(ProfaneListKey, static async entry => + return await cache.GetOrCreateAsync(ProfaneListKey, async entry => { var fileNames = ProfaneContentReader.GetFileNames(); Console.WriteLine("Source word list for profane content:"); + foreach (var fileName in fileNames) { Console.WriteLine(fileName); @@ -49,27 +50,15 @@ await Parallel.ForEachAsync(fileNames, }) .ConfigureAwait(false); - return allWords.Select( - static kvp => - new KeyValuePair>(kvp.Key, kvp.Value.ToFrozenSet())); - }) ?? []; - } - - /// - async ValueTask IProfaneContentCensorService.ContainsProfanityAsync(string content) - { - if (string.IsNullOrWhiteSpace(content)) - { - return false; - } - - var pattern = await GetProfaneWordListRegexPatternAsync(); + foreach (var (source, set) in allWords) + { + cache.Set(source, set.ToFrozenSet()); + } - return pattern switch - { - null => false, - _ => Regex.IsMatch(content, pattern, RegexOptions.IgnoreCase) - }; + return allWords.ToDictionary( + static kvp => kvp.Key, + static kvp => new ProfaneFilter(kvp.Key, kvp.Value.ToFrozenSet())); + }) ?? []; } /// @@ -84,27 +73,15 @@ async ValueTask IProfaneContentCensorService.CensorProfanityAsync( return result; } - var evaluator = replacementStrategy switch - { - ReplacementStrategy.Asterisk => MatchEvaluators.AsteriskEvaluator, - ReplacementStrategy.RandomAsterisk => MatchEvaluators.RandomAsteriskEvaluator, - ReplacementStrategy.MiddleAsterisk => MatchEvaluators.MiddleAsteriskEvaluator, - ReplacementStrategy.MiddleSwearEmoji => MatchEvaluators.MiddleSwearEmojiEvaluator, - ReplacementStrategy.VowelAsterisk => MatchEvaluators.VowelAsteriskEvaluator, - ReplacementStrategy.AngerEmoji => MatchEvaluators.AngerEmojiEvaluator, - - _ => MatchEvaluators.EmojiEvaluator, - }; + var evaluator = ToMatchEvaluator(replacementStrategy); var wordList = await ReadAllProfaneWordsAsync().ConfigureAwait(false); var stepContent = content; - foreach (var (source, set) in wordList) + foreach (var (source, filter) in wordList) { - var pattern = ToWordListRegexPattern(set); - if (result is { Steps: null } or { Steps.Count: 0 }) { result = result with @@ -116,7 +93,7 @@ async ValueTask IProfaneContentCensorService.CensorProfanityAsync( CensorStep step = new(stepContent, source); var potentiallyReplacedContent = - Regex.Replace(stepContent, pattern, evaluator, options: RegexOptions.IgnoreCase); + Regex.Replace(stepContent, filter.RegexPattern, evaluator, options: RegexOptions.IgnoreCase); if (stepContent != potentiallyReplacedContent) { @@ -134,34 +111,18 @@ async ValueTask IProfaneContentCensorService.CensorProfanityAsync( return result; } - private async ValueTask GetProfaneWordListRegexPatternAsync() + private static MatchEvaluator ToMatchEvaluator(ReplacementStrategy replacementStrategy) { - var wordList = - await ReadAllProfaneWordsAsync().ConfigureAwait(false); - - var set = wordList.SelectMany( - static kvp => kvp.Value - ) - .Distinct() - .ToFrozenSet(); - - return ToWordListRegexPattern(set); - } - - private static string ToWordListRegexPattern(FrozenSet set) - { - if (set.Count is 0) - { - Console.WriteLine("Unable to read profane word lists."); - return ""; - } - else + return replacementStrategy switch { - Console.WriteLine($"Found {set.Count:#,#} profane source words."); - } - - var pattern = $"\\b({string.Join('|', set)})\\b"; + ReplacementStrategy.Asterisk => MatchEvaluators.AsteriskEvaluator, + ReplacementStrategy.RandomAsterisk => MatchEvaluators.RandomAsteriskEvaluator, + ReplacementStrategy.MiddleAsterisk => MatchEvaluators.MiddleAsteriskEvaluator, + ReplacementStrategy.MiddleSwearEmoji => MatchEvaluators.MiddleSwearEmojiEvaluator, + ReplacementStrategy.VowelAsterisk => MatchEvaluators.VowelAsteriskEvaluator, + ReplacementStrategy.AngerEmoji => MatchEvaluators.AngerEmojiEvaluator, - return pattern; + _ => MatchEvaluators.EmojiEvaluator, + }; } } diff --git a/src/ProfanityFilter.Services/IProfaneContentCensorService.cs b/src/ProfanityFilter.Services/IProfaneContentCensorService.cs index 9ab41d4..20b0f42 100644 --- a/src/ProfanityFilter.Services/IProfaneContentCensorService.cs +++ b/src/ProfanityFilter.Services/IProfaneContentCensorService.cs @@ -1,8 +1,6 @@ // Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. -using ProfanityFilter.Services.Results; - namespace ProfanityFilter.Services; /// @@ -10,14 +8,6 @@ namespace ProfanityFilter.Services; /// public interface IProfaneContentCensorService { - /// - /// Determines whether the specified content contains profanity. - /// - /// The content to check for profanity. - /// A representing the asynchronous - /// operation, containing a indicating whether the content contains profanity. - ValueTask ContainsProfanityAsync(string content); - /// /// Censors any profanity in the specified content. /// diff --git a/src/ProfanityFilter.Services/Internals/ProfaneFilter.cs b/src/ProfanityFilter.Services/Internals/ProfaneFilter.cs new file mode 100644 index 0000000..76b69ff --- /dev/null +++ b/src/ProfanityFilter.Services/Internals/ProfaneFilter.cs @@ -0,0 +1,12 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace ProfanityFilter.Services; + +internal record class ProfaneFilter( + string SourceName, + FrozenSet ProfaneWords) +{ + public string RegexPattern { get; } = + $"\\b({string.Join('|', ProfaneWords)})\\b"; +} diff --git a/tests/ProfanityFilter.Services.Tests/DefaultProfaneContentCensorServiceTests.cs b/tests/ProfanityFilter.Services.Tests/DefaultProfaneContentCensorServiceTests.cs index fc530a5..3c64e3f 100644 --- a/tests/ProfanityFilter.Services.Tests/DefaultProfaneContentCensorServiceTests.cs +++ b/tests/ProfanityFilter.Services.Tests/DefaultProfaneContentCensorServiceTests.cs @@ -10,23 +10,6 @@ public class DefaultProfaneContentCensorServiceTests public DefaultProfaneContentCensorServiceTests() => _sut = new DefaultProfaneContentCensorService( new MemoryCache(Options.Create(new()))); - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("This is a sentence with the word crap.", true)] - [InlineData("This is a sentence with the word CrAp.", true)] - [InlineData("This is a sentence with the word crap and shit.", true)] - [InlineData("This is a sentence with the word crap and shit and fuck.", true)] - [InlineData("This is a sentence with the word crap and shit and fuck and ass.", true)] - public async Task ContainsProfanityAsync_Returns_Expected_Result(string? input, bool expectedResult) - { - // Act - var result = await _sut.ContainsProfanityAsync(input!); - - // Assert - Assert.Equal(expectedResult, result); - } - [Theory] [InlineData(null, null)] [InlineData("", "")]