/* * [ST.java] * * Summary: Miscellaneous static methods for dealing with Strings in JDK 1.8+. * * Copyright: (c) 1997-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.5 2005-07-14 split off from Misc, allow for compilation with old compiler. * 1.6 2006-01-01 * 1.7 2006-03-04 format with IntelliJ and prepare Javadoc * 1.8 2006-10-15 add condense method. * 1.9 2008-03-10 add ST.firstWord * 2.0 2008-04-01 * 2.1 2008-12-16 new methods in BigDate * 2.2 2009-04-19 add countLeading, countTrailing, TrimLeading, TrimTrailing that take multiple trim chars. * 2.3 2009-04-29 add countInstances( String page, char lookFor ) * 2.4 2009-04-30 fix but in countLeading ( String text, String leads ). * add pruneExcessBlankLines * 2.5 2009-11-14 add haveCommonChar, isLetter, isDigit * 2.6 2010-02-11 removed removeHead (redundant - same as chopLeadingString). * renamed chopLeading to chopLeadingString to clarify does not trim a variety of chars. * 2.7 2010-12-09 BigDate parsing now more relaxed * 2.8 2011-11-13 add ST.nullSafeStringCompare and ST.nullSafeStringCompareIgnoreCase. * 2.9 2012-11-06 fix bug in nullSafeStringCompare. It was ignoring case. * 3.0 2014-04-29 add quoteForReplace to quote Regex replacement Strings, * stripNaughtyCharacters, reorderLetters, deDupLetters * 3.1 2015-11-02 add last and tail methods */ package com.mindprod.common18; import java.awt.Color; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import static java.lang.System.*; /** * Miscellaneous static methods for dealing with Strings in JDK 1.8+. *

* * @author Roedy Green, Canadian Mind Products * @version 3.1 2015-11-02 add last and tail methods * @noinspection WeakerAccess * @since 2003-05-15 */ public class ST { // declarations /** * true if you want extra debugging output and test code */ private static final boolean DEBUGGING = false; /** * used to efficiently generate Strings of spaces of varying length */ private static final String SOMESPACES = " "; /** * track which chars in range 0..017F are vowels. Lookup table takes only 48 bytes. */ private static final BitSet vt = new BitSet( 0x0180 ); // /declarations static { // initialize vt Vowel Table vt.set( 'A' ); vt.set( 'E' ); vt.set( 'I' ); vt.set( 'O' ); vt.set( 'U' ); vt.set( 'a' ); vt.set( 'e' ); vt.set( 'i' ); vt.set( 'o' ); vt.set( 'u' ); vt.set( '\u00c0', '\u00c6' ); vt.set( '\u00c8', '\u00cf' ); vt.set( '\u00d2', '\u00d6' ); vt.set( '\u00d8', '\u00dc' ); vt.set( '\u00e0', '\u00e6' ); vt.set( '\u00e8', '\u00ef' ); vt.set( '\u00f2', '\u00f6' ); vt.set( '\u00f8', '\u00fc' ); vt.set( '\u0100', '\u0105' ); vt.set( '\u0112', '\u011b' ); vt.set( '\u0128', '\u012f' ); vt.set( '\u0130' ); vt.set( '\u0132', '\u0133' ); vt.set( '\u014c', '\u014f' ); vt.set( '\u0150', '\u0153' ); vt.set( '\u0168', '\u016f' ); vt.set( '\u0170', '\u0173' ); } /** * Dummy constructor * ST contains only static methods. */ protected ST() { } // methods /** * makeshift system beep if awt.Toolkit.beep is not available. Works also in JDK 1.02. */ public static void beep() { out.print( "\007" ); out.flush(); }// /method /** * Convert String to canonical standard form. null -> "". Trims lead trail blanks. Never null. * * @param s String to be converted. * * @return String in canonical form. */ public static String canonical( String s ) { if ( s == null ) { return ""; } else { return s.trim(); } }// /method /** * remove leading string if present * * @param text text with possible leading string, possibly empty or null. * @param toChop the leading string of interest. Not a list of possible chars to chop, order matters. * * @return string with to toChop string removed if the text starts with it, * otherwise the original string unmodified. * @see #trimLeading(String, String) * @see #chopTrailingString(String, String) */ public static String chopLeadingString( String text, String toChop ) { if ( text != null && text.startsWith( toChop ) ) { return text.substring( toChop.length() ); } else { return text; } }// /method /** * remove trailing string if present * * @param text text with possible trailing string, possibly empty, but not null. * @param toChop the trailing string of interest. Not a list of possible chars to chop, order matters. * * @return string with to toChop string removed if the text ends with it, otherwise the original string unmodified. * @see #trimTrailing(String, String) * @see #chopLeadingString(String, String) */ public static String chopTrailingString( String text, String toChop ) { if ( text != null && text.endsWith( toChop ) ) { return text.substring( 0, text.length() - toChop.length() ); } else { return text; } }// /method /** * Collapse multiple whitespace in string down to a single space. Remove lead and trailing whitespace. * Earlier version collapsed only spaces, not whitespace. * Reader's digest condensed books were still readable. We do not squish. * * @param s String to condense whitespace. * * @return String with all whitespace condensed and lead/trail whitespace removed. * @noinspection WeakerAccess, SameParameterValue * @see #squish(String) * @see com.mindprod.common18.ST#condense(String) */ public static String condense( String s ) { if ( s == null ) { return null; } s = s.trim(); final int len = s.length(); if ( len == 0 ) { return s; } StringBuilder b = new StringBuilder( len ); boolean suppressSpaces = false; for ( int i = 0; i < len; i++ ) { char c = s.charAt( i ); if ( Character.isWhitespace( c ) ) { //noinspection StatementWithEmptyBody if ( suppressSpaces ) { // subsequent space } else { // first space b.append( ' ' ); suppressSpaces = true; } } else { // was not a space b.append( c ); suppressSpaces = false; } } // end for return b.toString(); }// /method /** * Does a give string contain a given character * Ideally would be an instance method of String * to match String.contains( CharSequence ) * * @param s the string * @param c the character * * @return true if string s contains character c * @see java.lang.String#contains(CharSequence) */ public static boolean contains( String s, char c ) { return s.indexOf( c ) >= 0; }// /method /** * Returns true if strings a and b have one or more characters in common, not necessarily at the same offset. * If you think of a and b as sets of chars, returns true if the intersection of those sets in not null. * It can also be thought of as like an indexOf that scans for multiple characters at once. * * @param a first string * @param b second string * * @return true if the strings have one or more characters in common. */ public static boolean containsAnyOf( String a, String b ) { for ( int i = 0; i < b.length(); i++ ) { if ( a.indexOf( b.charAt( i ) ) >= 0 ) { return true; } } return false; }// /method /** * Count how many times a String occurs on a page. * * @param page big String to look in. * @param lookFor small String to look for and count instances. * * @return number of times the String appears non-overlapping. */ public static int countInstances( String page, String lookFor ) { int count = 0; for ( int start = 0; ( start = page.indexOf( lookFor, start ) ) >= 0; start += lookFor.length() ) { count++; } return count; }// /method /** * Count how many times a char occurs in a String. * * @param page big String to look in. * @param lookFor char to lookfor count instances. * * @return number of times the char appears. */ public static int countInstances( String page, char lookFor ) { int count = 0; for ( int i = 0; i < page.length(); i++ ) { if ( page.charAt( i ) == lookFor ) { count++; } } return count; }// /method /** * count of how many leading characters there are on a string matching a given character. It does not remove them. * * @param text text with possible leading characters, possibly empty, but not null. * @param c the leading character of interest, usually ' ' or '\n' * * @return count of leading matching characters, possibly 0. * @noinspection WeakerAccess */ public static int countLeading( String text, char c ) { int count; //noinspection StatementWithEmptyBody for ( count = 0; count < text.length() && text.charAt( count ) == c; count++ ) { // empty loop } return count; }// /method /** * count of how many leading characters there are on a string matching a given character. It does not remove them. * * @param text text with possible leading characters, possibly empty, but not null. * @param possibleChars the leading characters of interest, usually ' ' or '\n' * * @return count of leading matching characters, possibly 0. * @noinspection WeakerAccess */ public static int countLeading( String text, String possibleChars ) { int count; //noinspection StatementWithEmptyBody for ( count = 0; count < text.length() && possibleChars.indexOf( text.charAt( count ) ) >= 0; count++ ) { // empty loop. } return count; }// /method /** * count of how many trailing characters there are on a string matching a given character. It does not remove them. * * @param text text with possible trailing characters, possibly empty, but not null. * @param c the trailing character of interest, usually ' ' or '\n' * * @return count of trailing matching characters, possibly 0. * @noinspection WeakerAccess */ public static int countTrailing( String text, char c ) { int length = text.length(); // need defined outside the for loop. int count; //noinspection StatementWithEmptyBody for ( count = 0; count < length && text.charAt( length - 1 - count ) == c; count++ ) { // empty loop } return count; }// /method /** * count of how many trailing characters there are on a string matching a given character. It does not remove them. * * @param text text with possible trailing characters, possibly empty, but not null. * @param possibleChars the trailing characters of interest, usually ' ' or '\n' * * @return count of trailing matching characters, possibly 0. * @noinspection WeakerAccess */ public static int countTrailing( String text, String possibleChars ) { int length = text.length(); // need defined outside the for loop. int count; //noinspection StatementWithEmptyBody for ( count = 0; count < length && possibleChars.indexOf( text.charAt( length - 1 - count ) ) >= 0; count++ ) { // empty loop } return count; }// /method /** * reorder the letters in a String in ascending order, case-sensitively, and then remove dups. * e.g. aCBBB" --> "BCa", "DAB" --> "ABD", " "Z X" --> "XZ", " " --> "" * * @param field field to sort. * * @return field with letters composing it sorted in ascending order. Blanks squeezed out, but not dups. * @see #reorderLetters(String) */ public static String deDupLetters( String field ) { final String squished = ST.squish( field ); if ( squished.length() <= 1 ) { return field; } else { final char[] ca = squished.toCharArray(); Arrays.sort( ca ); // case-sensitive sort letter by letter // most of the time, it will already be deDuped, check just in case. int dups = 0; for ( int i = 1; i < ca.length; i++ ) { if ( ca[ i ] == ca[ i - 1 ] ) { dups++; } } if ( dups == 0 ) { // no dups found, we are done. return String.valueOf( ca ); } // we have to tediously dedup but at least we know the precise size of the final result ahead of time. final char[] deDuped = new char[ ca.length - dups ]; if ( ca.length > 0 ) { // always copy first char if there is one. deDuped[ 0 ] = ca[ 0 ]; int j = 1; for ( int i = 1; i < ca.length; i++ ) { if ( ca[ i ] != ca[ i - 1 ] ) { deDuped[ j++ ] = ca[ i ]; } } } return String.valueOf( deDuped ); } }// /method /** * debugging dump of a string with hex * * @param s string to dump */ public static void dumpString( String s ) { final int lineLength = 40; final int lines = ( s.length() + lineLength + 1 ) / lineLength; int start = 0; int end = lineLength; for ( int line = 0; line < lines; line++ ) { if ( end > s.length() ) { end = s.length(); } final String text = s.substring( start, end ); start += lineLength; end += lineLength; for ( int i = 0; i < text.length(); i++ ) { out.print( " " ); final char c = text.charAt( i ); if ( ' ' <= c && c <= '~' || '\u00a1' <= c && c <= '\u024f' ) { out.print( c ); } else { out.print( '.' ); // probably unprintable. Don't risk breaking the alignment. } } out.println(); for ( int i = 0; i < text.length(); i++ ) { final int c = text.charAt( i ); out.print( ' ' ); out.print( toLZHexString( c, 4 ) ); } out.println(); } }// /method /** * gets the first word of a String, delimited by space or the end of the string. \n will not delimit a word. * If there are no blanks in the string, the result is the entire string. * * @param s the input String * * @return the first word of the String. * @see #lastWord(String) */ public static String firstWord( String s ) { s = s.trim(); final int place = s.indexOf( ' ' ); return ( place < 0 ) ? s : s.substring( 0, place ); }// /method /** * find the first instance of whitespace (space, \n, \r, \t in a string. * * @param s string to scan * * @return -1 if not found, offset relative to start of string where found */ public static int indexOfWhiteSpace( String s ) { return indexOfWhiteSpace( s, 0 ); }// /method /** * find the first instance of whitespace (space, \n, \r, \t in a string. * * @param s string to scan * @param startOffset where in string to start looking * * @return -1 if not found, offset relative to start of string where found, not relative to startOffset. */ public static int indexOfWhiteSpace( String s, int startOffset ) { final int length = s.length(); for ( int i = startOffset; i < length; i++ ) { switch ( s.charAt( i ) ) { case ' ': case '\n': case '\r': case '\t': return i; default: // keep looking } } // end for return -1; }// /method /** * Check if char is plain ASCII digit. * * @param c char to check. * * @return true if char is in range 0-9 * @see Character#isLetter(char) * @see #isUnsignedNumeric(String) */ public static boolean isDigit( char c ) { return '0' <= c && c <= '9'; }// /method /** * Is this string empty? In Java 1.6 + isEmpty is build in. Sun's version being an instance method cannot test for * null. * * @param s String to be tested for emptiness. * * @return true if the string is null or equal to the "" null string. or just blanks * @see Misc#nullToEmpty(String) * @see String#isEmpty() */ public static boolean isEmpty( String s ) { return ( s == null ) || s.trim().length() == 0; }// /method /** * Ensure the string contains only legal characters. * * @param candidate string to test. * @param legalChars characters than are legal for candidate. * * @return true if candidate is formed only of chars from the legal set. */ public static boolean isLegal( String candidate, String legalChars ) { for ( int i = 0; i < candidate.length(); i++ ) { if ( legalChars.indexOf( candidate.charAt( i ) ) < 0 ) { return false; } } return true; }// /method /** * Ensure the char is only one a set of legal characters. * * @param candidate char to test. * @param legalChars characters than are legal for candidate. * * @return true if candidate is one of the legallegal set. */ public static boolean isLegal( char candidate, String legalChars ) { return legalChars.indexOf( candidate ) >= 0; }// /method /** * is a string numeric, only formed of 0..9? No signs, commas, E * * @param s string * * @return true if numeric, false if has other chars or is empty. */ public static boolean isNumeric( String s ) { if ( ST.isEmpty( s ) ) { return false; } for ( int i = 0; i < s.length(); i++ ) { final char c = s.charAt( i ); if ( '0' <= c && c <= '9' ) { continue; } else { return false; } } return true; } // method// /method /** * Check if char is plain ASCII letter lower or upper case. * * @param c char to check. * * @return true if char is in range a..z A..Z * @see Character#isLowerCase(char) * @see Character#isUpperCase(char) * @see Character#isDigit(char) */ public static boolean isUnaccentedLetter( char c ) { return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'; }// /method /** * Check if char is plain ASCII lower case. * * @param c char to check. * * @return true if char is in range a..z. * @see Character#isLowerCase(char) * @see Character#isLetter(char) */ public static boolean isUnaccentedLowerCase( char c ) { return 'a' <= c && c <= 'z'; }// /method /** * Check if char is plain ASCII upper case. * * @param c char to check. * * @return true if char is in range A..Z. * @see Character#isUpperCase(char) * @see Character#isLetter(char) */ public static boolean isUnaccentedUpperCase( char c ) { return 'A' <= c && c <= 'Z'; }// /method /** * test if a string is numeric. Integer only. No lead - allowed. * * @param s String to test * * @return true if numeric * @see #isDigit(char) */ public static boolean isUnsignedNumeric( String s ) { for ( int i = 0; i < s.length(); i++ ) { char c = s.charAt( i ); if ( c < '0' || c > '9' ) { return false; } } return true; }// /method /** * is this character a vowel? * * @param c the character, any char upper or lower case, punctuation or symbol * * @return true if char is aeiou or AEIOU, or vowel accented in any way or ligature ae AE oe OE ij IJ */ public static boolean isVowel( char c ) { return c < 0x0180 && vt.get( c ); }// /method /** * join two arrays of Strings into one, faster than FastCat, StringBuilder. * * @param a first array of Strings * @param b second array of Strings * * @return new String array containing all elements of a and b. */ public static String[] join( String[] a, String[] b ) { String[] result = new String[ a.length + b.length ]; System.arraycopy( a, 0, result, 0, a.length ); System.arraycopy( b, 0, result, a.length, b.length ); return result; }// /method /** * Gets the last characters of a string. * * @param s string to extract from * @param count of chars to include * * @return last chars of the String. */ public static String last( String s, int count ) { final int start = s.length() - count; if ( start < 0 ) { return s; } if ( start >= s.length() ) { return ""; } return s.substring( start ); }// /method /** * gets the last word of a String, delimited by space or the end of the string. * * @param s the input String * * @return the last word of the String. * @see #firstWord(String) */ public static String lastWord( String s ) { s = s.trim(); return s.substring( s.lastIndexOf( ' ' ) + 1 ); }// /method /** * Pads the string value out to the given length by applying blanks on the right, left justifying the value. * * @param value value to be converted to string String to be padded/chopped. * @param newLen length of new String desired. * @param chop true if Strings longer than newLen should be truncated to newLen chars. * * @return String padded on right/chopped to the desired length. Spaces are inserted on the right. * @see #toLZ */ public static String leftJustified( int value, int newLen, boolean chop ) { return rightPad( Integer.toString( value ), newLen, chop ); }// /method /** * Pads the string out to the given length by applying blanks on the left, effectively rightjustifying. * * @param s String to be padded/chopped. * @param newLen length of new String desired. * @param chop true if Strings longer than newLen should be truncated to newLen chars. * * @return String padded on left/chopped to the desired length. Spaces are inserted on the left. * @see #toLZ */ public static String leftPad( String s, int newLen, boolean chop ) { int grow = newLen - s.length(); if ( grow <= 0 ) { if ( chop ) { return s.substring( 0, newLen ); } else { return s; } } else { return spaces( grow ) + s; } }// /method /** * Test harness, used in debugging * * @param args not used */ public static void main( String[] args ) { if ( DEBUGGING ) { out.println( ">>condense" ); out.println( condense( " this is spaced. " ) ); out.println( ">> trimLeading" ); out.println( trimLeading( "*****t*r*i*m****", '*' ) ); out.println( trimLeading( " trim " ) ); out.println( ">> trimTrailing" ); out.println( trimTrailing( " trim " ) ); out.println( trimTrailing( "*****t*r*i*m****", '*' ) ); out.println( ">> chopLeadingString" ); out.println( chopLeadingString( "abcdefg", "abc" ) ); out.println( ">> chopTrailingString" ); out.println( chopTrailingString( "say!", "!" ) ); out.println( toString( Color.red ) ); out.println( ">> toHexString" ); out.println( toHexString( -3 ) ); out.println( toHexString( 3 ) ); out.println( ">> countLeading" ); out.println( countLeading( "none", ' ' ) ); out.println( countLeading( "*one***", '*' ) ); out.println( countLeading( " abc ", " *" ) ); out.println( countLeading( " * * abc ", " *" ) ); out.println( countLeading( " * * abc ", "* " ) ); out.println( countLeading( "\n\ntw\n\n\no\n\n\n\n", '\n' ) ); out.println( ">> countTrailing" ); out.println( countTrailing( "none", ' ' ) ); out.println( countTrailing( "***one*", '*' ) ); out.println( countTrailing( "\n\n\n\nt\n\n\n\nwo\n\n", '\n' ) ); out.println( countTrailing( " abc * * ", " *" ) ); out.println( countTrailing( " * * abc * * ", " *" ) ); out.println( countTrailing( " * * abc * * ", "* " ) ); out.println( ">> quoteSQL" ); out.println( quoteSQL( "Judy's Place" ) ); out.println( ">> parseLongPennies" ); out.println( parseLongPennies( "$5.00" ) ); out.println( parseLongPennies( "$50" ) ); out.println( parseLongPennies( "50" ) ); out.println( parseLongPennies( "$50-" ) ); out.println( ">> penniesToString" ); out.println( penniesToString( 0 ) ); out.println( penniesToString( -1 ) ); out.println( penniesToString( 20 ) ); out.println( penniesToString( 302 ) ); out.println( penniesToString( -100000 ) ); out.println( ">> toBookTitleCase" ); out.println( toBookTitleCase( "handbook to HIGHER consciousness" ) ); out.println( toBookTitleCase( "THE HISTORY OF THE U.S.A." ) ); out.println( toBookTitleCase( "THE HISTORY OF THE USA" ) ); out.println( ">> rightPad" ); out.println( rightPad( "abc", 6, true ) + "*" ); out.println( rightPad( "abc", 2, true ) + "*" ); out.println( rightPad( "abc", 2, false ) + "*" ); out.println( rightPad( "abc", 3, true ) + "*" ); out.println( rightPad( "abc", 3, false ) + "*" ); out.println( rightPad( "abc", 0, true ) + "*" ); out.println( rightPad( "abc", 20, true ) + "*" ); out.println( rightPad( "abc", 29, true ) + "*" ); out.println( rightPad( "abc", 30, true ) + "*" ); out.println( rightPad( "abc", 31, true ) + "*" ); out.println( rightPad( "abc", 40, true ) + "*" ); out.println( ">> toUpperCase" ); out.println( toUpperCase( 'q' ) ); out.println( toUpperCase( 'Q' ) ); out.println( toUpperCase( "The quick brown fox was 10 feet tall." ) ); out.println( toUpperCase( "THE QUICK BROWN FOX WAS 10 FEET TALL." ) ); out.println( toUpperCase( "the quick brown fox was 10 feet tall." ) ); out.println( ">> toLowerCase" ); out.println( toLowerCase( 'q' ) ); out.println( toLowerCase( 'Q' ) ); out.println( toLowerCase( "The quick brown fox was 10 feet tall." ) ); out.println( toLowerCase( "THE QUICK BROWN FOX WAS 10 FEET TALL." ) ); out.println( toLowerCase( "the quick brown fox was 10 feet tall." ) ); out.println( ">> countInstances" ); out.println( "count instances should be 4: " + countInstances( " abab abcdefgab", "ab" ) ); } }// /method /** * Like String.compare except it is not phased * by null parameters. * null compares before everything else. * * @param a first string to compare, possibly null. * @param b second string to compare, possibly null. * * @return -1 if a is less than b, * 0 if a == b, * and +1 if a is * greater than b * comparing case-sensitively, e.g. all lower case letters come * after all upper case letters.. */ public static int nullSafeStringCompare( final String a, final String b ) { // catches 100% of equal if interned, otherwise just some, //noinspection StringEquality if ( a == b ) { return 0; } else if ( a == null ) { // b cannot be null; return -1; } else if ( b == null ) { // a cannot be null return 1; } else { // a and b cannot be null, case-sensitive return a.compareTo( b ); } }// /method /** * Like String.compareIgnoreCase except it is not phased * by null parameters. * null compares before everything else. * * @param a first string to compare, possibly null. * @param b second string to compare, possibly null. * * @return -1 if a is less than b, * 0 if a == b, * and +1 if a is greater than b * comparing case-insensitively, e.g. interleaving upper and * lower case letters. */ public static int nullSafeStringCompareIgnoreCase( final String a, final String b ) { // catches 100% of equal if interned, otherwise just some, //noinspection StringEquality if ( a == b ) { return 0; } else if ( a == null ) { // b cannot be null; return -1; } else if ( b == null ) { // a cannot be null return 1; } else { // a and b cannot be null return a.compareToIgnoreCase( b ); } }// /method /** * convert a String to a long. The routine is very forgiving. It ignores invalid chars, lead trail, embedded spaces, * decimal points etc. Dash is treated as a minus sign. * * @param numStr String to be parsed. * * @return long value of String with junk characters stripped. * @throws NumberFormatException if the number is too big to fit in a long. */ public static long parseDirtyLong( String numStr ) { numStr = numStr.trim(); // strip commas, spaces, decimals + etc StringBuilder b = new StringBuilder( numStr.length() ); boolean negative = false; for ( int i = 0, n = numStr.length(); i < n; i++ ) { char c = numStr.charAt( i ); if ( c == '-' ) { negative = true; } else if ( '0' <= c && c <= '9' ) { b.append( c ); } } // end for numStr = b.toString(); if ( numStr.length() == 0 ) { return 0; } long num = Long.parseLong( numStr ); if ( negative ) { num = -num; } return num; }// /method /** * convert a String into long pennies. It ignores invalid chars, lead trail, embedded spaces. Dash is treated as a * minus sign. 0 or 2 decimal places are permitted. * * @param numStr String to be parsed. * * @return long pennies. * @throws NumberFormatException if the number is too big to fit in a long. * @noinspection WeakerAccess */ public static long parseLongPennies( String numStr ) { numStr = numStr.trim(); // strip commas, spaces, + etc StringBuilder b = new StringBuilder( numStr.length() ); boolean negative = false; int decpl = -1; for ( int i = 0, n = numStr.length(); i < n; i++ ) { char c = numStr.charAt( i ); switch ( c ) { case '-': negative = true; break; case '.': if ( decpl == -1 ) { decpl = 0; } else { throw new NumberFormatException( "more than one decimal point" ); } break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if ( decpl != -1 ) { decpl++; } b.append( c ); break; default: // ignore junk chars break; } // end switch } // end for if ( numStr.length() != b.length() ) { numStr = b.toString(); } if ( numStr.length() == 0 ) { return 0; } long num = Long.parseLong( numStr ); if ( decpl == -1 || decpl == 0 ) { num *= 100; } else //noinspection StatementWithEmptyBody if ( decpl == 2 ) {/* it is fine as is */ } else { throw new NumberFormatException( "wrong number of decimal places." ); } if ( negative ) { num = -num; } return num; }// /method /** * Print dollar currency, stored internally as scaled int. convert pennies to a string with a decorative decimal * point. * * @param pennies long amount in pennies. * * @return amount with decorative decimal point, but no lead $. * @noinspection WeakerAccess */ public static String penniesToString( long pennies ) { boolean negative; if ( pennies < 0 ) { pennies = -pennies; negative = true; } else { negative = false; } String s = Long.toString( pennies ); int len = s.length(); switch ( len ) { case 1: s = "0.0" + s; break; case 2: s = "0." + s; break; default: s = s.substring( 0, len - 2 ) + "." + s.substring( len - 2, len ); break; } // end switch if ( negative ) { s = "-" + s; } return s; }// /method /** * Extracts a number from a string, returns 0 if malformed. * * @param s String containing the integer. * * @return integer. */ public static int pluck( String s ) { int result = 0; try { result = Integer.parseInt( s ); } catch ( NumberFormatException e ) { // leave result at 0 } return result; }// /method /** * Collapse multiple blank lines down to one. Discards lead and trail blank lines. * Blank lines are lines that when trimmed have length 0. * * @param lines array of lines to tidy. * @param minBlankLinesToKeep usually 1 meaning 1+ consecutive blank lines become 1, effectively collapsing * runs of blank lines down to 1. * if 2, 1 blank line is removed, and 2+ consecutive blanks lines become 1, * effectively undouble spacing. * if zero, non-blank lines will be separated by one blank line, even if there was not * one there to begin with, completely independent of preexisting blank lines, * effectively double spacing.. * 9999 effectively removes all blank lines. * * @return array of lines with lead and trail blank lines removed, and excess blank lines collapsed down to one * or 0. * The results are NOT trimmed. */ public static String[] pruneExcessBlankLines( String[] lines, int minBlankLinesToKeep ) { int firstNonBlankLine = lines.length; for ( int i = 0; i < lines.length; i++ ) { if ( lines[ i ].trim().length() > 0 ) { firstNonBlankLine = i; break; } } int lastNonBlankLine = -1; for ( int i = lines.length - 1; i > 0; i-- ) { if ( lines[ i ].trim().length() > 0 ) { lastNonBlankLine = i; break; } } if ( firstNonBlankLine > lastNonBlankLine ) { return new String[ 0 ]; } // collapse blank lines in the middle chunk ArrayList keep = new ArrayList<>( lastNonBlankLine - firstNonBlankLine + 1 ); int pendingBlankLines = 0; for ( int i = firstNonBlankLine; i <= lastNonBlankLine; i++ ) { if ( lines[ i ].trim().length() == 0 ) { pendingBlankLines++; } else { if ( pendingBlankLines >= minBlankLinesToKeep ) { keep.add( "" ); } keep.add( lines[ i ] ); // we don't trim. That is up to caller. pendingBlankLines = 0; } } return keep.toArray( new String[ keep.size() ] ); }// /method /** * ST.quoteForReplace is like Regex.quote, * but quotes replacement strings which have only two reserved characters $ and \ * Regex.quote will not work for replacement Strings. * * @param s string to prepare to insert literally in a replacement regex. * * @return quoted string with each $ -> \$ and \ -> \\ */ public static String quoteForReplace( String s ) { // non regex, all instances, Or so dont quote \ created by $ return s.replace( "\\", "\\\\" ).replace( "$", "\\$" ); }// /method /** * used to prepare SQL string literals by doubling each embedded ' and wrapping in ' at each end. Further quoting is * required to use the results in Java String literals. If you use PreparedStatement, then this method is not * needed. The ' quoting is automatically handled for you. * * @param sql Raw SQL string literal * * @return sql String literal enclosed in ' * @noinspection WeakerAccess, SameParameterValue */ public static String quoteSQL( String sql ) { StringBuilder sb = new StringBuilder( sql.length() + 5 ); sb.append( '\'' ); for ( int i = 0; i < sql.length(); i++ ) { char c = sql.charAt( i ); if ( c == '\'' ) { sb.append( "\'\'" ); } else { sb.append( c ); } } sb.append( '\'' ); return sb.toString(); }// /method /** * reorder the letters in a String in ascending order, case-sensitively. * e.g. "DAB" --> "ABD", "aCBBB" --> "BBBCa", "Z X" --> "XZ", " " --> "" * * @param field field to sort. * * @return field with letters composing it sorted in ascending order. Blanks squeezed out, but not dups. * @see #deDupLetters(String) */ public static String reorderLetters( String field ) { final String squished = ST.squish( field ); if ( squished.length() <= 1 ) { return field; } else { final char[] ca = squished.toCharArray(); Arrays.sort( ca ); // case-sensitive sort letter by letter return String.valueOf( ca ); } }// /method /** * Produce a String of a given repeating character. * * @param c the character to repeat * @param count the number of times to repeat * * @return String, e.g. rep('*',4) returns "****" * @noinspection WeakerAccess, SameParameterValue * @see #spaces(int) */ public static String rep( char c, int count ) { if ( c == ' ' && count <= SOMESPACES.length() ) { return SOMESPACES.substring( 0, count ); } char[] s = new char[ count ]; for ( int i = 0; i < count; i++ ) { s[ i ] = c; } return new String( s ).intern(); }// /method /** * Pads the string value out to the given length by applying blanks on the left, right justifying the value. * * @param value value to be converted to string String to be padded/chopped. * @param newLen length of new String desired. * @param chop true if Strings longer than newLen should be truncated to newLen chars. * * @return String padded on left/chopped to the desired length. Spaces are inserted on the left. * @see #toLZ */ public static String rightJustified( int value, int newLen, boolean chop ) { return leftPad( Integer.toString( value ), newLen, chop ); }// /method /** * Pads the string out to the given length by applying blanks on the right, effectively left-justifying. * * @param s String to be padded/chopped. * @param newLen length of new String desired. * @param chop true if Strings longer than newLen should be truncated to newLen chars. * * @return String padded on right/chopped to the desired length. Spaces are inserted on the right. * @noinspection WeakerAccess, SameParameterValue */ public static String rightPad( String s, int newLen, boolean chop ) { int grow = newLen - s.length(); if ( grow <= 0 ) { if ( chop ) { return s.substring( 0, newLen ); } else { return s; } } else { return s + spaces( grow ); } }// /method /** * sort chars in String in case-sensitive alphabetical order * * @param s String to sort * * @return sorted String */ public static String sort( String s ) { final char[] ca = s.toCharArray(); // encoding not relevant here Arrays.sort( ca ); final String sorted = new String( ca ); if ( sorted.equals( s ) ) { return s; } else { return sorted; } }// /method /** * insert spaces between the letters L I K E T H I S * * @param s string of letters * * @return string with a space between each letter. */ public static String spaceOut( String s ) { StringBuilder sb = new StringBuilder( s.length() * 2 - 1 ); for ( int i = 0, n = s.length(); i < n; i++ ) { if ( i != 0 ) { sb.append( ' ' ); } sb.append( s.charAt( i ) ); } return sb.toString(); }// /method /** * Generate a string of spaces n chars long. * * @param n how many spaces long * * @return a string of spaces n chars long. * @see #rep(char, int) */ public static String spaces( int n ) { if ( n <= SOMESPACES.length() ) { if ( n <= 0 ) { return ""; } else { return SOMESPACES.substring( 0, n ); } } else { return rep( ' ', n ); } }// /method /** * Remove all spaces from a String. Does not touch other whitespace. * * @param s String to strip of blanks. * * @return String with all blanks, lead/trail/embedded removed. * @see #condense(String) */ public static String squish( String s ) { if ( s == null ) { return null; } s = s.trim(); if ( s.indexOf( ' ' ) < 0 ) { return s; } int len = s.length(); StringBuilder b = new StringBuilder( len - 1 ); for ( int i = 0; i < len; i++ ) { char c = s.charAt( i ); if ( c != ' ' ) { b.append( c ); } } // end for return b.toString(); }// /method /** * Strip out any commas since they confuse parse. * * @param amount string of digits decimal point and optional commas. * * @return amount with any commas removed. */ public static String stripCommas( String amount ) { return stripNaughtyCharacters( amount, "," ); } /** * remove multiple unwanted characters from a String, * e.g. could be used to remove , and _ from numeric strings * before parsing. * * @param s string to strip * @param naughty list of characters to strip out * * @return string witch naughty characters removed. */ public static String stripNaughtyCharacters( String s, String naughty ) { final StringBuilder sb = new StringBuilder( s.length() ); for ( int i = 0; i < s.length(); i++ ) { final char c = s.charAt( i ); // contains wants a string, not char, so we have to use indexOf if ( naughty.indexOf( c ) < 0 ) { // keep char, it was not naughty sb.append( c ); } } return sb.toString(); }// /method /** * Gets the tail end of a string. * * @param s string to extract from * @param start index of first char to include. * * @return last chars of the String. */ public static String tail( String s, int start ) { if ( start >= s.length() ) { return s; } if ( start < 0 ) { return ""; } return s.substring( start ); }// /method /** * Pluralise a string like 0 ducks, 1 duck, 2 ducks. * * @param count how many items there are. * @param what that the items are. * * @return "2 ducks" */ public static String things( int count, String what ) { if ( count == 1 || what.endsWith( "s" ) ) { return count + " " + what; } else { return count + " " + what + "s"; } }// /method /** * convert to Book Title case, with first letter of each word capitalised. e.g. "handbook to HIGHER consciousness" * -> "Handbook to Higher Consciousness" e.g. "THE HISTORY OF THE U.S.A." -> "The History of the U.S.A." e.g. "THE * HISTORY OF THE USA" -> "The History of the Usa" (sorry about that.) Don't confuse this with Character.isTitleCase * which concerns ligatures. * * @param s String to convert. May be any mixture of case. * * @return String with each word capitalised, except embedded words "the" "of" "to" * @noinspection WeakerAccess */ public static String toBookTitleCase( String s ) { if ( s == null ) { return null; } if ( s.length() == 0 ) { return ""; } char[] ca = s.toCharArray(); // Track if we changed anything so that // we can avoid creating a duplicate String // object if the String is already in Title case. boolean changed = false; boolean capitalise = true; boolean firstCap = true; for ( int i = 0; i < ca.length; i++ ) { char oldLetter = ca[ i ]; if ( oldLetter == '\'' ) { // Might have O'Brian or Molly's capitalise = ( i > 1 && ca[ i - 1 ] == 'o' || ca[ i - 1 ] == 'O' ); } else if ( oldLetter <= '/' || ':' <= oldLetter && oldLetter <= '?' || ']' <= oldLetter && oldLetter <= '`' ) { /* whitespace, control chars or punctuation */ /* Next normal char should be capitalised */ /* apostrophe treated as letter so Molly's won't come out Molly'S */ capitalise = true; } else { if ( capitalise && !firstCap ) { // might be the_ of_ or to_ capitalise = !( s.substring( i, Math.min( i + 4, s.length() ) ) .equalsIgnoreCase( "the " ) || s.substring( i, Math.min( i + 3, s.length() ) ) .equalsIgnoreCase( "of " ) || s.substring( i, Math.min( i + 3, s.length() ) ) .equalsIgnoreCase( "to " ) ); } // end if char newLetter = capitalise ? Character.toUpperCase( oldLetter ) : Character.toLowerCase( oldLetter ); ca[ i ] = newLetter; changed |= ( newLetter != oldLetter ); capitalise = false; firstCap = false; } // end if } // end for if ( changed ) { s = new String( ca ); } return s; }// /method /** * Convert int to 8-char hex with lead zeroes * * @param h number you want to convert to hex * * @return 0x followed by unsigned hex 8-digit representation * @noinspection WeakerAccess * @see #toString(Color) * @see Integer#toString(int, int) */ public static String toHexString( int h ) { String s = Integer.toHexString( h ); if ( s.length() < 8 ) {// pad on left with zeros s = "00000000".substring( 0, 8 - s.length() ) + s; } return "0x" + s; }// /method /** * Convert an integer to a String, with left zeroes. * * @param i the integer to be converted * @param len the length of the resulting string. Warning. It will chop the result on the left if it is too long. * * @return String representation of the int e.g. 007 * @see #leftPad */ public static String toLZ( int i, int len ) { // Since String is final, we could not add this method there. String s = Integer.toString( i ); if ( s.length() > len ) {/* return rightmost len chars */ return s.substring( s.length() - len ); } else if ( s.length() < len ) // pad on left with zeros { return "000000000000000000000000000000".substring( 0, len - s.length() ) + s; } else { return s; } }// /method /** * Convert an integer as STring to a String, with left zeroes. * * @param s the STring to be converted * @param len the length of the resulting string. Warning. It will chop the result on the left if it is too long. * * @return String representation of the int e.g. 007 * @see #leftPad */ public static String toLZ( String s, int len ) { // Since String is final, we could not add this method there. if ( s.length() > len ) {/* return rightmost len chars */ return s.substring( s.length() - len ); } else if ( s.length() < len ) // pad on left with zeros { return "000000000000000000000000000000".substring( 0, len - s.length() ) + s; } else { return s; } }// /method /** * Convert an long to a String, with left zeroes. * * @param l the long to be converted * @param len the length of the resulting string. Warning. It will chop the result on the left if it is too long. * * @return String representation of the long e.g. 007 * @see #leftPad */ public static String toLZ( long l, int len ) { // Since String is final, we could not add this method there. String s = Long.toString( l ); if ( s.length() > len ) {/* return rightmost len chars */ return s.substring( s.length() - len ); } else if ( s.length() < len ) // pad on left with zeros { return "000000000000000000000000000000".substring( 0, len - s.length() ) + s; } else { return s; } }// /method /** * convert an integer value to unsigned hex with leading zeroes. * * @param value integer to convert. * @param len how many characters you want in the result. * * @return value in hex, padded to len chars with 0s on the left. * @see Integer#toString(int, int) */ public static String toLZHexString( int value, int len ) { // Since String is final, we could not add this method there. final String s = Integer.toHexString( value ); if ( s.length() > len ) {/* return rightmost len chars */ return s.substring( s.length() - len ); } else if ( s.length() < len ) // pad on left with zeros. at most 7 will be prepended { return "0000000".substring( 0, len - s.length() ) + s; } else { return s; } }// /method /** * convert an integer value to unsigned octal with leading zeroes. * * @param value integer to convert. * @param len how many characters you want in the result. * * @return value in octal, padded to len chars with 0s on the left. * @see Integer#toString(int, int) */ public static String toLZOctalString( int value, int len ) { // Since String is final, we could not add this method there. final String s = Integer.toOctalString( value ); if ( s.length() > len ) {/* return rightmost len chars */ return s.substring( s.length() - len ); } else if ( s.length() < len ) // pad on left with zeros. at most 7 will be prepended { return "0000000".substring( 0, len - s.length() ) + s; } else { return s; } }// /method /** * Quick replacement for Character.toLowerCase for use with English-only. It does not deal with accented * characters. * * @param c character to convert * * @return character converted to lower case */ public static char toLowerCase( char c ) { return 'A' <= c && c <= 'Z' ? ( char ) ( c + ( 'a' - 'A' ) ) : c; }// /method /** * Quick replacement for Character.toLowerCase for use with English-only. It does not deal with accented * characters. * * @param s String to convert * * @return String converted to lower case */ public static String toLowerCase( String s ) { final char[] ca = s.toCharArray(); final int length = ca.length; boolean changed = false; // can't use for:each since we need the index to set. for ( int i = 0; i < length; i++ ) { final char c = ca[ i ]; if ( 'A' <= c && c <= 'Z' ) { // found a char that needs conversion. ca[ i ] = ( char ) ( c + ( 'a' - 'A' ) ); changed = true; } } // give back same string if unchanged. return changed ? new String( ca ) : s; }// /method /** * Get #ffffff html hex number for a colour * * @param c Color object whose html colour number you want as a string * * @return # followed by 6 hex digits * @noinspection WeakerAccess, SameParameterValue * @see #toHexString(int) * @see Integer#toString(int, int) */ public static String toString( Color c ) { String s = Integer.toHexString( c.getRGB() & 0xffffff ); if ( s.length() < 6 ) {// pad on left with zeros s = "000000".substring( 0, 6 - s.length() ) + s; } return '#' + s; }// /method /** * Quick replacement for Character.toUpperCase for use with English-only. It does not deal with accented * characters. * * @param c character to convert * * @return character converted to upper case */ public static char toUpperCase( char c ) { return 'a' <= c && c <= 'z' ? ( char ) ( c + ( 'A' - 'a' ) ) : c; }// /method /** * Quick replacement for Character.toUpperCase for use with English-only. It does not deal with accented * characters. * * @param s String to convert * * @return String converted to upper case */ public static String toUpperCase( String s ) { final char[] ca = s.toCharArray(); final int length = ca.length; boolean changed = false; // can't use for:each since we need the index to set. for ( int i = 0; i < length; i++ ) { final char c = ca[ i ]; if ( 'a' <= c && c <= 'z' ) { // found a char that needs conversion. ca[ i ] = ( char ) ( c + ( 'A' - 'a' ) ); changed = true; } } // give back same string if unchanged. return changed ? new String( ca ) : s; }// /method /** * prune junk characters from bead, tail and middle of a string * * @param text text to clean * @param junkChars chars to consider as junn * * @return string with any of those junk characters removed. */ public static String trimJunk( String text, String junkChars ) { final StringBuffer sb = new StringBuffer( text.length() ); //do not convert to FastCat for ( int i = 0; i < text.length(); i++ ) { final char c = text.charAt( i ); if ( junkChars.indexOf( c ) < 0 ) { sb.append( c ); } } return sb.toString(); }// /method /** * Removes white space from beginning this string. * * @param s String to process. As always the original in unchanged. * * @return this string, with leading white space removed * @noinspection WeakerAccess, WeakerAccess, WeakerAccess, SameParameterValue, WeakerAccess * @see #trimLeading(String, char) *

* All characters that have codes less than or equal to '\u0020' (the space character) are * considered to be white space. */ public static String trimLeading( String s ) { if ( s == null ) { return null; } int len = s.length(); int st = 0; while ( ( st < len ) && ( s.charAt( st ) <= ' ' ) ) { st++; } return ( st > 0 ) ? s.substring( st, len ) : s; }// /method /** * trim leading characters there are on a string matching a given character. * * @param text text with possible trailing characters, possibly empty, but not null. * @param c the leading character of interest, usually ' ' or '\n' * * @return string with any of those trailing characters removed. * @see #trimLeading(String) */ public static String trimLeading( String text, char c ) { int count = countLeading( text, c ); // substring will optimise the 0 case. return text.substring( count ); }// /method /** * trim leading characters there are on a string matching a given characters * * @param text text with possible trailing characters, possibly empty, but not null. * @param charsToTrim list of leading characters of interest, usually ' ' or '\n' * * @return string with any of those leading characters removed. * @see #trimTrailing(String) * @see #chopLeadingString(String, String) */ public static String trimLeading( String text, String charsToTrim ) { int count = countLeading( text, charsToTrim ); // substring will optimise the 0 case. return text.substring( count ); }// /method /** * Removes white space from end this string. * * @param s String to process. As always the original in unchanged. * * @return this string, with trailing white space removed * @see #trimTrailing(String, char) *

* All characters that have codes less than or equal to '\u0020' (the space character) are * considered to be white space. */ public static String trimTrailing( String s ) { if ( s == null ) { return null; } int len = s.length(); int origLen = len; while ( ( len > 0 ) && ( s.charAt( len - 1 ) <= ' ' ) ) { len--; } return ( len != origLen ) ? s.substring( 0, len ) : s; }// /method /** * trim trailing characters there are on a string matching a given character, could be more than one. * * @param text text with possible trailing characters, possibly empty, but not null. * @param c the trailing character of interest, usually ' ' or '\n' * * @return string with any of those trailing characters removed. * @see #trimTrailing(String) */ public static String trimTrailing( String text, char c ) { int count = countTrailing( text, c ); // substring will optimise the 0 case. return text.substring( 0, text.length() - count ); }// /method /** * trim trailing characters there are on a string matching given characters. * * @param text text with possible trailing characters, possibly empty, but not null. * @param charsToTrim list of the trailing characters of interest, usually ' ' or '\n' * * @return string with any of those trailing characters removed. ".com" would not only chop .com, * but any combination of those letters e.g. mc.moc * @see #trimTrailing(String) * @see #chopTrailingString(String, String) */ public static String trimTrailing( String text, String charsToTrim ) { int count = countTrailing( text, charsToTrim ); // substring will optimise the 0 case. return text.substring( 0, text.length() - count ); }// /method // /methods }