Skip to content

Commit

Permalink
More clean up and better caching.
Browse files Browse the repository at this point in the history
  • Loading branch information
IEvangelist committed Jan 30, 2024
1 parent 550354b commit 555eb0b
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 90 deletions.
87 changes: 24 additions & 63 deletions src/ProfanityFilter.Services/DefaultProfaneContentCensorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ internal sealed class DefaultProfaneContentCensorService(IMemoryCache cache) : I
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation that
/// returns a readonly dictionary of all profane words.</returns>
private async Task<IEnumerable<KeyValuePair<string, FrozenSet<string>>>> ReadAllProfaneWordsAsync()
private async Task<Dictionary<string, ProfaneFilter>> 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);
Expand Down Expand Up @@ -49,27 +50,15 @@ await Parallel.ForEachAsync(fileNames,
})
.ConfigureAwait(false);
return allWords.Select(
static kvp =>
new KeyValuePair<string, FrozenSet<string>>(kvp.Key, kvp.Value.ToFrozenSet()));
}) ?? [];
}

/// <inheritdoc />
async ValueTask<bool> 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()));
}) ?? [];
}

/// <inheritdoc />
Expand All @@ -84,27 +73,15 @@ async ValueTask<CensorResult> 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
Expand All @@ -116,7 +93,7 @@ async ValueTask<CensorResult> 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)
{
Expand All @@ -134,34 +111,18 @@ async ValueTask<CensorResult> IProfaneContentCensorService.CensorProfanityAsync(
return result;
}

private async ValueTask<string?> 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<string> 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,
};
}
}
10 changes: 0 additions & 10 deletions src/ProfanityFilter.Services/IProfaneContentCensorService.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using ProfanityFilter.Services.Results;

namespace ProfanityFilter.Services;

/// <summary>
/// Defines methods for checking and censoring profane content.
/// </summary>
public interface IProfaneContentCensorService
{
/// <summary>
/// Determines whether the specified content contains profanity.
/// </summary>
/// <param name="content">The content to check for profanity.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> representing the asynchronous
/// operation, containing a <see cref="bool"/> indicating whether the content contains profanity.</returns>
ValueTask<bool> ContainsProfanityAsync(string content);

/// <summary>
/// Censors any profanity in the specified content.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/ProfanityFilter.Services/Internals/ProfaneFilter.cs
Original file line number Diff line number Diff line change
@@ -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<string> ProfaneWords)
{
public string RegexPattern { get; } =
$"\\b({string.Join('|', ProfaneWords)})\\b";
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,6 @@ public class DefaultProfaneContentCensorServiceTests
public DefaultProfaneContentCensorServiceTests() => _sut = new DefaultProfaneContentCensorService(
new MemoryCache(Options.Create<MemoryCacheOptions>(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("", "")]
Expand Down

0 comments on commit 555eb0b

Please sign in to comment.