Skip to content

Instantly share code, notes, and snippets.

@Spotlightsrule
Last active June 28, 2020 09:15
Show Gist options
  • Save Spotlightsrule/c3d10ec983a03bfaef6b42d8db6b12ed to your computer and use it in GitHub Desktop.
Save Spotlightsrule/c3d10ec983a03bfaef6b42d8db6b12ed to your computer and use it in GitHub Desktop.
A small chat moderation tool used to detect the use excessive "caps", or capital letters in a message
//Import Java classes and dependencies
import java.util.Arrays;
/**
* A small chat moderation tool used to detect the
* use excessive "caps", or capital letters in a
* message. Has a default tolerance of 50% and
* minimum string length of 10, but these values
* can be tweaked. Also contains static string
* character analysis methods for aiding in the
* detection of caps
*
* @author Spotlightsrule
*/
class AntiCaps {
//Default class variables
/** Defines the default allowed ratio of caps to lowercase/other characters in a message **/
private static final double MAX_CAPS_RATIO = 0.67;
/** Defines the default allowed "acronym" length, or a message that won't be screened by the anti-caps if passed **/
private static final int MAX_ACRONYM = 10;
/** Defines the default setting as to whether or not the anti-caps screen should be based on ratios of caps to all non-whitespace characters (false) or caps to only lowercase characters (true) **/
private static final boolean LOWER_TO_CAPS_RATIO_ONLY = true;
/** Defines how much up or down the allowed caps ratio should "sway" when doing the caps ratio check by default **/
private static final double CAPS_RATIO_SWAY = 0;
/** Defines how much up or down the allowed acronym size should "sway" when doing the message length check by default **/
private static final int ACRONYM_LEN_SWAY = 0;
/** Defines the possible characters that could count as whitespace **/
private static final char[] WHITESPACE = {
'\u0009', //CHARACTER TABULATION
'\n', //LINE FEED (LF)
'\u000B', //LINE TABULATION
'\u000C', //FORM FEED (FF)
'\r', //CARRIAGE RETURN (CR)
'\u0020', //SPACE
'\u0085', //NEXT LINE (NEL)
'\u00A0', //NO-BREAK SPACE
'\u1680', //OGHAM SPACE MARK
'\u180E', //MONGOLIAN VOWEL SEPARATOR
'\u2000', //EN QUAD
'\u2001', //EM QUAD
'\u2002', //EN SPACE
'\u2003', //EM SPACE
'\u2004', //THREE-PER-EM SPACE
'\u2005', //FOUR-PER-EM SPACE
'\u2006', //SIX-PER-EM SPACE
'\u2007', //FIGURE SPACE
'\u2008', //PUNCTUATION SPACE
'\u2009', //THIN SPACE
'\u200A', //HAIR SPACE
'\u2028', //LINE SEPARATOR
'\u2029', //PARAGRAPH SEPARATOR
'\u202F', //NARROW NO-BREAK SPACE
'\u205F', //MEDIUM MATHEMATICAL SPACE
'\u3000', //IDEOGRAPHIC SPACE
};
//Class variables
/** Defines the allowed ratio of caps to lowercase/other characters in a message **/
private double maxCapsRatio;
/** Defines the allowed "acronym" length, or a message that won't be screened by the anti-caps if passed **/
private int maxAcronym;
/** Defines whether or not the anti-caps screen should be based on ratios of caps to all non-whitespace characters (false) or caps to only lowercase characters (true) **/
private boolean lowerToCapsRatioOnly;
/** Defines how much up or down the allowed caps ratio should "sway" when doing the caps ratio check (prevents the anti-caps detection from being as predictable and exploitable) **/
private double capsRatioSway;
/** Defines how much up or down the allowed acronym size should "sway" when doing the message length check (prevents the anti-caps detection from being as predictable and exploitable) **/
private int acronymLenSway;
//Constructors
/**
* Constructs a new instance of the {@code AntiCaps}
* class
* @param maxCapsRatio The maximum ratio of caps to lowercase/other characters in a message
* @param maxAcronym The maximum allowed "acronym" length, or a message that won't be screened by the anti-caps if passed
* @param lowerToCapsRatioOnly Whether or not whether or not the anti-caps screen should be based on ratios of caps to all non-whitespace characters (false) or caps to only lowercase characters (true)
*/
public AntiCaps(double maxCapsRatio, int maxAcronym, boolean lowerToCapsRatioOnly){
//Assign the class variables from the constructor's parameters using the class
setMaxCapsRatio(maxCapsRatio);
setMaxAcronym(maxAcronym);
setLowerToCapsRatioOnly(lowerToCapsRatioOnly);
//Initialize the sway ratios (these are opt-in only and are not initializable by the constructor, as these are considered advanced features)
this.capsRatioSway = CAPS_RATIO_SWAY;
this.acronymLenSway = ACRONYM_LEN_SWAY;
}
/**
* Constructs a new instance of the {@code AntiCaps}
* class
* @param maxCapsRatio The maximum ratio of caps to lowercase/other characters in a message
* @param maxAcronym The maximum allowed "acronym" length, or a message that won't be screened by the anti-caps if passed
*/
public AntiCaps(double maxCapsRatio, int maxAcronym){
//Redirect to the overloaded constructor, passing the default parameters instead
this(maxCapsRatio, maxAcronym, LOWER_TO_CAPS_RATIO_ONLY);
}
/**
* Constructs a new instance of the {@code AntiCaps}
* class
* @param maxCapsRatio The maximum ratio of caps to lowercase/other characters in a message
*/
public AntiCaps(double maxCapsRatio){
//Redirect to the overloaded constructor, passing the default parameters instead
this(maxCapsRatio, MAX_ACRONYM, LOWER_TO_CAPS_RATIO_ONLY);
}
/**
* Constructs a new instance of the {@code AntiCaps}
* class with all default options (66% allowed caps,
* 10 acronym length, and caps ratios only to lowercase
* characters)
*/
public AntiCaps(){
//Redirect to the overloaded constructor, passing the default parameters instead
this(MAX_CAPS_RATIO, MAX_ACRONYM, LOWER_TO_CAPS_RATIO_ONLY);
}
//Getters
/**
* Gets the allowed ratio of caps to lowercase/other
* characters in a message
* @return <b>double</b> The current value of the {@code maxCapsRatio} variable
*/
public double getMaxCapsRatio(){
return maxCapsRatio;
}
/**
* Gets the allowed "acronym" length, or a message
* that won't be screened by the anti-caps if passed
* @return <b>int</b> The current value of the {@code maxAcronym} variable
*/
public int getMaxAcronym(){
return maxAcronym;
}
/**
* Gets whether or not the anti-caps screen should
* be based on ratios of caps to all non-whitespace
* characters (false) or caps to only lowercase
* characters (true)
* @return <b>boolean</b> The current value of the {@code lowerToCapsRatioOnly} variable
*/
public boolean isLowerToCapsRatioOnly(){
return lowerToCapsRatioOnly;
}
/**
* Gets how much up or down the allowed caps
* ratio should "sway" when doing the caps check
* (prevents the anti-caps detection from being
* as predictable and exploitable)
* @return <b>double</b> The current value of the {@code capsRatioSway} variable
*/
public double getCapsRatioSway(){
return capsRatioSway;
}
/**
* Gets how much up or down the allowed acronym
* size should "sway" when doing the message
* length check (prevents the anti-caps detection
* from being as predictable and exploitable)
* @return <b>int</b> The current value of the {@code acronymLenSway} variable
*/
public int getAcronymLenSway(){
return acronymLenSway;
}
//Setters
/**
* Sets the allowed ratio of caps to lowercase/other
* characters in a message
* @param maxCapsRatio The value of the {@code maxCapsRatio} variable to set
*/
public void setMaxCapsRatio(double maxCapsRatio){
//Check if the max caps ratio is less than 0 or greater than 1.0
if(maxCapsRatio < 0 || maxCapsRatio > 1){
//Throw an IllegalArgumentException
throw new IllegalArgumentException("Maximum caps ratio cannot be less than 0 or greater than 1");
}
this.maxCapsRatio = maxCapsRatio;
}
/**
* Sets the allowed "acronym" length, or a message
* that won't be screened by the anti-caps if passed
* @param The value of the {@code maxAcronym} variable to set
*/
public void setMaxAcronym(int maxAcronym){
//Check if the max acronym length is less than 0
if(maxAcronym < 0){
//Throw an IllegalArgumentException
throw new IllegalArgumentException("Maximum acronym length cannot be less than 0");
}
this.maxAcronym = maxAcronym;
}
/**
* Sets whether or not the anti-caps screen should
* be based on ratios of caps to all non-whitespace
* characters (false) or caps to only lowercase
* characters (true)
* @param lowerToCapsRatioOnly The value of the {@code lowerToCapsRatioOnly} variable to set
*/
public void setLowerToCapsRatioOnly(boolean lowerToCapsRatioOnly){
this.lowerToCapsRatioOnly = lowerToCapsRatioOnly;
}
/**
* Sets how much up or down the allowed caps
* ratio should "sway" when doing the caps
* ratio check (prevents the anti-caps detection
* from being as predictable and exploitable)
* @param capsRatioSway The value of the {@code capsRatioSway} variable to set
*/
public void setCapsRatioSway(double capsRatioSway){
//Check if the ratio sway is less than 0 or greater than 0.5
if(capsRatioSway < 0 || capsRatioSway > 0.5){
//Throw an IllegalArgumentException
throw new IllegalArgumentException("Caps ratio sway cannot be less than 0 or greater than 0.5");
}
this.capsRatioSway = capsRatioSway;
}
/**
* Sets how much up or down the allowed acronym
* size should "sway" when doing the message
* length check (prevents the anti-caps detection
* from being as predictable and exploitable)
* @param acronymLenSway The value of the {@code acronymLenSway} variable to set
*/
public void setAcronymLenSway(int acronymLenSway){
//Check if the ratio sway is less than 0 or greater than the maximum acronym amount
if(acronymLenSway < 0 || acronymLenSway > maxAcronym){
//Throw an IllegalArgumentException
throw new IllegalArgumentException("Acronym length sway cannot be less than 0 or greater than " + maxAcronym);
}
this.acronymLenSway = acronymLenSway;
}
//Instance methods
/**
* Calculates how much of a given message is caps.
* Depends on the value of {@code lowerToCapsRatioOnly}
* @param strIn The string to get the caps ratio for
* @return <b>double</b> The ratio of caps to other characters or lowercase letters, depending on the value of {@code lowerToCapsRatioOnly}
*/
public double capsRatio(String strIn){
//Get the ratios of all the characters in the input string
int[] allRatios = (ratios(strIn));
//Get the number of capital letters in the input string
int capsCount = (allRatios[0]);
//Get the length of the entire string (without whitespace characters) to use as the divisor for the ratio calculation
int ratioTotal = (allRatios[5] - allRatios[3]);
//Test whether or not the total should include only lowercase letters
if(lowerToCapsRatioOnly){
//Set the ratio total to include only lowercase characters
ratioTotal = (allRatios[0] + allRatios[1]);
}
//Calculate the ratio of caps in the string (cap count divided by the total length) and return it
return ((double) capsCount / (double) ratioTotal);
}
/**
* Checks whether or not a given message contains
* an excessive amount of capital letters
* @param strIn The string to screen for caps
* @return <b>boolean</b> The status as to whether or not the input string is a caps violation
*/
public boolean isCapsViolation(String strIn){
//Get the caps ratio
double capsRatio = (capsRatio(strIn));
//Check if the "magic ratio" has been exceeded (introduce "sway" if necessary)
boolean isViolation = (capsRatio > ratioSway(maxCapsRatio, capsRatioSway, 0.0, 1.0));
//Check if the string contains any spaces using Java 8 streams
if(Arrays.stream(String.valueOf(WHITESPACE).split("")).parallel().anyMatch(strIn::contains)){
//Return the status of the caps violation boolean
return isViolation;
}
else {
//Return the status of the caps violation boolean if the length is greater than the max acronym length (with any "sway" accounted for), return false otherwise (accounts for acronyms)
return ((strIn.length() > ratioSway(maxAcronym, acronymLenSway, 0.0, strIn.length())) ? isViolation : false);
}
}
//Class methods
/**
* Gets the ratios of each significant character
* type in a given string and outputs an array
* with the ratios
* @param strIn The string to get the ratios for
* @return <b>int[]</b> An array containing the ratios of each character type in the following order: upper, lower, digits, whitespace, other, total
*/
public static int[] ratios(String strIn){
//Initialize the output array (6 slots: upper, lower, digits, whitespace, other, total)
int[] allRatios = new int[6];
//Initialize the counters
int upper = 0, lower = 0, digit = 0, whitespace = 0, other = 0;
//Iterate through each character of the input string
for(int i=0; i<strIn.length(); i++){
//Get the current character at index i
char curChar = (strIn.charAt(i));
//Switch over the current character
switch(Character.getType(curChar)){
//Character is an uppercase letter, so increment the uppercase counter
case Character.UPPERCASE_LETTER:{ upper++; break; }
//Character is a lowercase letter, so increment the lowercase counter
case Character.LOWERCASE_LETTER:{ lower++; break; }
//Character is a digit, so increment the digit counter
case Character.DECIMAL_DIGIT_NUMBER: case Character.OTHER_NUMBER:{ digit++; break; }
//Character is whitespace or a line seperator, so increment the whitespace counter
case Character.SPACE_SEPARATOR: case Character.LINE_SEPARATOR:{ whitespace++; break; }
//Increment the other character counter by default
default:{ other++; break; }
}
}
//Add each counter value to their positions in the array
allRatios[0] = upper;
allRatios[1] = lower;
allRatios[2] = digit;
allRatios[3] = whitespace;
allRatios[4] = other;
allRatios[5] = (strIn.length());
//Return the filled ratios array
return allRatios;
}
/**
* Skews a given ratio either up or down depending
* on the given maximum sway amount
* @param swayTarget The ratio to skew up or down
* @param swayAmount The maximum amount to skew the ratio (0-x)
* @param valMin The miniumum possible value that the input ratio can be
* @param valMax The maxiumum possible value that the input ratio can be
* @return <b>double</b> The resulting skewed ratio
*/
private static double ratioSway(double swayTarget, double swayAmount, double valMin, double valMax){
//Check if the sway amount is less than or equal to 0
if(swayAmount <= 0){
//Simply return the unmodified sway target, no need to do anything
return swayTarget;
}
//Check if the minimum value exceeds the maximum value
if(valMin > valMax){
//Throw an IllegalArgumentException
throw new IllegalArgumentException("Minimum value cannot exceed maximum value");
}
//Calculate the ratio offset by generating a random double from 0 to the desired sway amount and flip the sign at random
double ratioSway = (getRandomDouble(0, (getRandomBool() ? (swayAmount * -1) : swayAmount)));
//Add the calculated ratio sway to the sway target, casting each to an int if the original target is a whole number
double finalRatio = ((swayTarget == ((int) swayTarget)) ? (((int) swayTarget + (int) ratioSway)) : (swayTarget + ratioSway));
//Check if the final ratio is greater than the maximum value
if(finalRatio > valMax){
//Set the final ratio to be the maximum value
finalRatio = valMax;
}
//Check if the final ratio is less than the minimum value
if(finalRatio < valMin){
//Set the final ratio to be the minimum value
finalRatio = valMin;
}
//Return the swayed ratio
return finalRatio;
}
//CoffeeKit imports (makes the module standalone)
/**
* Get a random boolean value using
* {@code Math.random()}
* @return <b>boolean</b> A random boolean value
*/
static boolean getRandomBool(){
return (Math.random() >= 0.5);
}
/**
* Get a random double between a
* minimum and max number using
* {@code Math.random()}
* @param min The lowest number that can be selected
* @param max The highest number that can be selected
* @return <b>double</b> A random double value
*/
static double getRandomDouble(double min, double max){
return (Math.random() * (max - min) + min);
}
}
/**
* Test code for the AntiCaps class
*
* @author Spotlightsrule
*/
public class AntiCapsTest {
public static void main(String[] args){
//Create the screener object with default screening values (66% allowed caps, 10 maximum acronym size, ratio is caps to lowercase characters only)
//AntiCaps defaultScreen = new AntiCaps(0.67, 10, false);
AntiCaps defaultScreen = new AntiCaps();
//Tests with default screening options
System.out.println("--- DEFAULT SCREEN ---");
quickScreen(defaultScreen, "This sentence is not a caps violation"); // false
quickScreen(defaultScreen, "THIS SENTENCE IS A CAPS VIOLATION"); // true
quickScreen(defaultScreen, randomCase("This sentence has random case")); // false or true depending on the output of the method
quickScreen(defaultScreen, "ANTIFA"); // false
quickScreen(defaultScreen, "AREALLYLONGACRONYM"); // true
System.out.println();
//Create the screener object with very strict screening values (10% allowed caps, 5 maximum acronym size, ratio is caps to lowercase characters only)
AntiCaps aggressiveScreen = new AntiCaps(0.1, 5, true);
//Tests with aggressive screening options
System.out.println("--- AGGRESSIVE SCREEN ---");
quickScreen(aggressiveScreen, "This is not a caps violation, thank goodness"); // false
quickScreen(aggressiveScreen, "How Is This A Caps Violation?"); // true
quickScreen(aggressiveScreen, randomCase("This sentence has random case")); // false or true depending on the output of the method
quickScreen(aggressiveScreen, "NCAA"); // false
quickScreen(aggressiveScreen, "NAATCO"); // true
System.out.println();
//Create the screener object with very lenient screening values (90% allowed caps, 20 maximum acronym size, ratio is caps to all non-whitespace characters)
AntiCaps lenientScreen = new AntiCaps(0.9, 20, false);
//Tests with lenient screening options
System.out.println("--- LENIENT SCREEN ---");
quickScreen(lenientScreen, "HOW IS THIS not a CAPS VIOLATION?"); // false
quickScreen(lenientScreen, "THIS IS A CAPS VIOLATION"); // true
quickScreen(lenientScreen, randomCase("This sentence has random case")); // false or true depending on the output of the method
quickScreen(lenientScreen, "AREALLYLONGACRONYM"); // false
quickScreen(lenientScreen, "NIIOMTPLABOPARMBETZHELBETRABSBOMONIMONKONOTDTEKHSTROMONT"); // true (world record for the longest acronym)
System.out.println();
//Create the screener object with default screening values (66% allowed caps, 10 maximum acronym size, ratio is caps to lowercase characters only), but allow sway to occur with both the max caps ratio and the allowed acronym size
AntiCaps paranoidScreen = new AntiCaps();
paranoidScreen.setCapsRatioSway(0.1);
paranoidScreen.setAcronymLenSway(3);
//Tests with lenient screening options
System.out.println("--- PARANOID SCREEN ---");
quickScreen(paranoidScreen, "THIS IS not USUALLY A CAPS violation"); // false, but can vary
quickScreen(paranoidScreen, "THIS IS usually A CAPS VIOLATION"); // true, but can vary
quickScreen(paranoidScreen, randomCase("This sentence has random case")); // false or true depending on the output of the method, but can vary
quickScreen(paranoidScreen, "SMLACRO"); // false, but can vary
quickScreen(paranoidScreen, "THISISABIGACRONYM"); // true, but can vary
}
/**
* Displays the character ratios for a message
* @param strIn The string to test the ratios of
*/
public static void ratioTest(String strIn){
//Get the ratios
int[] ratios = AntiCaps.ratios(strIn);
//Print the string
System.out.println("SENTENCE: " + strIn);
//Print the ratios in a pretty table
String table = "%1$-5s%2$-5s%3$-5s%4$-5s%5$-5s%6$-5s\n";
System.out.format(table, "CAP", "LWR", "NUM", "WHT", "OTR", "TOT");
System.out.format(table, ratios[0], ratios[1], ratios[2], ratios[3], ratios[4], ratios[5]);
}
/**
* Screens and displays statistics for a message
* @param capsScreener The {@code AntiCaps} instance to use for the caps screening
* @param strIn The string to screen
*/
public static void quickScreen(AntiCaps capsScreener, String strIn){
//Print the details
ratioTest(strIn);
System.out.println("CAPS RATIO: " + capsScreener.capsRatio(strIn));
System.out.println("IS CAPS VIOLATION: " + capsScreener.isCapsViolation(strIn) + "\n");
}
/**
* Generates random case in an input string
* @param strIn The string to convert to random caps
* @return <b>String</b> The input string with random casing
*/
public static String randomCase(String strIn){
//Loop over the input string
for(int i=0; i<strIn.length(); i++){
//Get the character at index i in the input string and convert it to lowercase
char curChar = (Character.toLowerCase(strIn.charAt(i)));
//Generate a random number and check if it's greater than 0.5 (pseudo-random boolean)
if(AntiCaps.getRandomBool()){
//Convert the character to uppercase
curChar = (Character.toUpperCase(curChar));
}
//Replace the current character with the upper or lowercase variant instead
strIn = (strIn.substring(0, i) + curChar + strIn.substring(i+1));
}
//Return the random cased string
return strIn;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment