diff --git a/goaway.go b/goaway.go index c026781..eef3e66 100644 --- a/goaway.go +++ b/goaway.go @@ -26,6 +26,7 @@ type ProfanityDetector struct { sanitizeLeetSpeak bool // Whether to replace characters with a non-' ' value in characterReplacements sanitizeAccents bool sanitizeSpaces bool + exactWord bool profanities []string falseNegatives []string @@ -41,6 +42,7 @@ func NewProfanityDetector() *ProfanityDetector { sanitizeLeetSpeak: true, sanitizeAccents: true, sanitizeSpaces: true, + exactWord: false, profanities: DefaultProfanities, falsePositives: DefaultFalsePositives, falseNegatives: DefaultFalseNegatives, @@ -109,6 +111,16 @@ func (g *ProfanityDetector) WithCustomCharacterReplacements(characterReplacement return g } +// WithExactWord allows configuring whether the profanity check process should require exact matches or not. +// Using this reduces false positives and winds up more permissive. +// +// Note: this entails also setting WithSanitizeSpaces(false), since without spaces present exact word matching +// does not make sense. +func (g *ProfanityDetector) WithExactWord(exactWord bool) *ProfanityDetector { + g.exactWord = exactWord + return g.WithSanitizeSpaces(false) +} + // IsProfane takes in a string (word or sentence) and look for profanities. // Returns a boolean func (g *ProfanityDetector) IsProfane(s string) bool { @@ -119,6 +131,7 @@ func (g *ProfanityDetector) IsProfane(s string) bool { // Returns the first profanity found, or an empty string if none are found func (g *ProfanityDetector) ExtractProfanity(s string) string { s, _ = g.sanitize(s, false) + // Check for false negatives for _, word := range g.falseNegatives { if match := strings.Contains(s, word); match { @@ -129,15 +142,34 @@ func (g *ProfanityDetector) ExtractProfanity(s string) string { for _, word := range g.falsePositives { s = strings.Replace(s, word, "", -1) } - // Check for profanities - for _, word := range g.profanities { - if match := strings.Contains(s, word); match { - return word + + if !g.exactWord { + // Check for profanities + for _, word := range g.profanities { + if match := strings.Contains(s, word); match { + return word + } + } + } else { + tokens := strings.Split(s, space) + for _, token := range tokens { + if sliceContains(g.profanities, token) { + return token + } } } return "" } +func sliceContains(words []string, s string) bool { + for _, word := range words { + if strings.EqualFold(s, word) { + return true + } + } + return false +} + func (g *ProfanityDetector) indexToRune(s string, index int) int { count := 0 for i := range s { diff --git a/goaway_test.go b/goaway_test.go index 210cd8a..4df76ef 100644 --- a/goaway_test.go +++ b/goaway_test.go @@ -548,6 +548,36 @@ func TestFalsePositives(t *testing.T) { } } +func TestExactWord(t *testing.T) { + acceptSentences := []string{ + "I'm an analyst", + } + rejectSentences := []string{"Go away, ass."} + tests := []struct { + name string + profanityDetector *ProfanityDetector + }{ + { + name: "With Empty FalsePositives", + profanityDetector: NewProfanityDetector().WithExactWord(true).WithSanitizeSpecialCharacters(true).WithCustomDictionary(DefaultProfanities, nil, nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, s := range acceptSentences { + if tt.profanityDetector.IsProfane(s) { + t.Error("Expected false, got true from:", s) + } + } + for _, s := range rejectSentences { + if !tt.profanityDetector.IsProfane(s) { + t.Error("Expected true, got false from:", s) + } + } + }) + } +} + func TestFalseNegatives(t *testing.T) { sentences := []string{ "dumb ass", // ass -> bASS (FP) -> dumBASS (FFP) @@ -564,6 +594,10 @@ func TestFalseNegatives(t *testing.T) { name: "With Custom Dictionary", profanityDetector: NewProfanityDetector().WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives), }, + { + name: "With Custom Dictionary", + profanityDetector: NewProfanityDetector().WithExactWord(true).WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {