ソースを参照

neW: ByteStreamer
neW: CharStreamer
neW: StringUtils

Ranides Atterwim 10 年 前
コミット
d09a0a0d97

+ 167 - 0
assira/src/main/java/net/ranides/assira/lexer/ByteStreamer.java

@@ -0,0 +1,167 @@
+/*
+ *  @author Ranides Atterwim <ranides@gmail.com>
+ *  @copyright Ranides Atterwim
+ *  @license WTFPL
+ *  @url http://ranides.net/projects/assira
+ */
+
+package net.ranides.assira.lexer;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+
+/**
+ * <p>
+ * Strumień przetwarzający kolejno elementy zbioru przydatny przy pisaniu
+ * lexerów <i>ad hoc</i>. Żadna funkcjonalność związana z obsługą gramatyki
+ * nie istnieje. Przydatny do przetwarzania sekwencji sterujących, kodów
+ * escape'ujących w stringach, wzorców podstawieniowych etc
+ * </p><p>
+ * Bardziej ściśle: do przetwarzania formatów, w których bezkontekstowe
+ * symbole nie są zagnieżdżane rekurencyjnie.
+ * </p>
+ * @param <UserArgument>
+ * @author ranides
+ */
+public class ByteStreamer<UserArgument> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final byte[] EMPTY = new byte[0];
+    private final BiConsumer<UserArgument, byte[]> consumer;
+    private byte[] tail;
+    private byte[] array;
+    private int recent;
+    private int position;
+
+
+    /**
+     * Tworzy nowy strumień, który przekazuje wyodrębnione fragmenty do podanego
+     * konsumenta.
+     * @param consumer
+     */
+    public ByteStreamer(BiConsumer<UserArgument, byte[]> consumer) {
+        this.tail      = null;
+        this.array     = EMPTY;
+        this.recent    = 0;
+        this.position  = 0;
+        this.consumer = consumer;
+    }
+
+    /**
+     * Odczytuje element ze strumienia i przesuwa kursor do przodu.
+     * @return
+     */
+    public byte get() {
+        return array[position++];
+    }
+
+    /**
+     * Odczytuje element ze strumienia nie przesuwając kursora
+     * @return
+     */
+    public byte peek() {
+        return array[position];
+    }
+
+    /**
+     * Pomija element
+     */
+    public void skip() {
+        position++;
+    }
+
+    /**
+     * Sprawdza, czy przetworzono wszystkie dostępne dane.
+     * @return
+     */
+    public boolean end() {
+        return position >= array.length;
+    }
+
+    /**
+     * Wysyła do odbiorcy fragment o wskazanej treści
+     * @param extra
+     * @param data
+     */
+    public void send(UserArgument extra, byte... data) {
+        consumer.accept(extra, data);
+    }
+
+    /**
+     * Odrzuca bieżący fragment i rozpoczyna przetwarzanie nowego od aktualnej
+     * pozycji kursora.
+     */
+    public void flush() {
+        recent = position;
+    }
+
+    /**
+     * Wysyła bieżący fragment do odbiorcy, ignorując {@code left} pierwszych
+     * znaków oraz {@code right} ostatnich. Następnie rozpoczyna przetwarzanie
+     * nowego fragmentu rozpoczynającego się od aktualnej pozycji kursora.
+     * @param extra
+     * @param lclip
+     * @param rclip
+     */
+    public void flush(UserArgument extra, int lclip, int rclip) {
+        if(recent < position && position <= array.length) {
+            send(extra, Arrays.copyOfRange(array, recent+lclip, position-rclip) );
+        }
+        flush();
+    }
+
+    /**
+     * Wysyła bieżący fragment do odbiorcy. Następnie rozpoczyna przetwarzanie
+     * nowego fragmentu od aktualnej pozycji kursora.
+     * @param extra
+     */
+    public void flush(UserArgument extra) {
+        flush(extra, 0, 0);
+    }
+
+    /**
+     * Rozpoczyna przetwarzanie kolejnej porcji danych
+     * @param input
+     * @throws IllegalStateException jeśli strumień jest w trakcie przetwarzania
+     * danych.
+     */
+    public void open(byte[] input) {
+        if( null != tail ) {
+            array = new byte[tail.length + input.length];
+            System.arraycopy(tail, 0, array, 0, tail.length);
+            System.arraycopy(input, 0, array, tail.length, input.length);
+            tail = null;
+        } else {
+            array = Arrays.copyOf(input, input.length);
+        }
+        position = 0;
+        recent = 0;
+    }
+
+    /**
+     * Kończy przetwarzanie danych
+     */
+    public void close() {
+        if(tail != null) {
+            if( array!=EMPTY ) {
+                throw new IllegalStateException("tail already in use");
+            }
+        } else {
+            tail = Arrays.copyOfRange(array, recent, array.length);
+            recent = 0;
+            position = 0;
+            array = EMPTY;
+        }
+    }
+
+    /**
+     * Sprawdza,czy przetwarzanie wykorzystało wszystkie przekazane dane
+     * @return
+     */
+    public boolean pending() {
+        return tail != null;
+    }
+
+}

+ 167 - 0
assira/src/main/java/net/ranides/assira/lexer/CharStreamer.java

@@ -0,0 +1,167 @@
+/*
+ *  @author Ranides Atterwim <ranides@gmail.com>
+ *  @copyright Ranides Atterwim
+ *  @license WTFPL
+ *  @url http://ranides.net/projects/assira
+ */
+
+package net.ranides.assira.lexer;
+
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+
+/**
+ * <p>
+ * Strumień przetwarzający kolejno elementy zbioru przydatny przy pisaniu
+ * lexerów <i>ad hoc</i>. Żadna funkcjonalność związana z obsługą gramatyki
+ * nie istnieje. Przydatny do przetwarzania sekwencji sterujących, kodów
+ * escape'ujących w stringach, wzorców podstawieniowych etc
+ * </p><p>
+ * Bardziej ściśle: do przetwarzania formatów, w których bezkontekstowe
+ * symbole nie są zagnieżdżane rekurencyjnie.
+ * </p>
+ * @param <UserArgument> 
+ * @author ranides
+ */
+public class CharStreamer<UserArgument> {
+
+    private static final char[] EMPTY = new char[0];
+    private final BiConsumer<UserArgument, char[]> consumer;
+    private char[] tail;
+    private char[] array;
+    private int recent;
+    private int position;
+    
+    
+    /**
+     * Tworzy nowy strumień, który przekazuje wyodrębnione fragmenty do podanego
+     * konsumenta.
+     * @param consumer
+     */
+    public CharStreamer(BiConsumer<UserArgument, char[]> consumer) {
+        this.tail      = null;
+        this.array     = EMPTY;
+        this.recent    = 0;
+        this.position  = 0;
+        this.consumer  = consumer;
+    }
+    
+    /**
+     * Odczytuje element ze strumienia i przesuwa kursor do przodu.
+     * @return 
+     */
+    public char get() {
+        return array[position++];
+    }
+    
+    /**
+     * Odczytuje element ze strumienia nie przesuwając kursora
+     * @return 
+     */
+    public char peek() {
+        return array[position];
+    }
+    
+    /**
+     * Pomija element
+     */
+    public void skip() {
+        position++;
+    }
+    
+    /**
+     * Sprawdza, czy przetworzono wszystkie dostępne dane.
+     * @return 
+     */
+    public boolean end() {
+        return position >= array.length;
+    }
+    
+    /**
+     * Wysyła do odbiorcy fragment o wskazanej treści
+     * @param extra
+     * @param data 
+     */
+    public void send(UserArgument extra, char... data) {
+        consumer.accept(extra, data);
+    }
+    
+    /**
+     * Odrzuca bieżący fragment i rozpoczyna przetwarzanie nowego od aktualnej
+     * pozycji kursora.
+     */
+    public void flush() {
+        recent = position;
+    }
+    
+    /**
+     * Wysyła bieżący fragment do odbiorcy, ignorując {@code left} pierwszych
+     * znaków oraz {@code right} ostatnich. Następnie rozpoczyna przetwarzanie 
+     * nowego fragmentu rozpoczynającego się od aktualnej pozycji kursora.
+     * @param extra
+     * @param left
+     * @param right 
+     */
+    public void flush(UserArgument extra, int left, int right) {
+        if(recent < position && position <= array.length) {
+            send(extra, Arrays.copyOfRange(array, recent+left, position-right) );
+        } 
+        flush();
+    }
+
+    /**
+     * Wysyła bieżący fragment do odbiorcy. Następnie rozpoczyna przetwarzanie 
+     * nowego fragmentu od aktualnej pozycji kursora.
+     * @param extra 
+     */
+    public void flush(UserArgument extra) {
+        flush(extra, 0, 0);
+    }
+    
+    /**
+     * Rozpoczyna przetwarzanie kolejnej porcji danych
+     * @param input 
+     * @throws IllegalStateException jeśli strumień jest w trakcie przetwarzania
+     * danych.
+     */
+    public void open(char[] input) {
+        if(array != EMPTY) {
+            throw new IllegalStateException("already opened");
+        }
+        if( null != tail ) {
+            array = new char[tail.length + input.length];
+            System.arraycopy(tail, 0, array, 0, tail.length);
+            System.arraycopy(input, 0, array, tail.length, input.length);
+            tail = null;
+        } else {
+            array = Arrays.copyOf(input, input.length);
+        }
+        position = 0;
+        recent = 0;
+    }
+    
+    /**
+     * Kończy przetwarzanie danych
+     */
+    public void close() {
+        if(tail != null) { 
+            if( array!=EMPTY ) { 
+                throw new IllegalStateException("tail already in use");
+            }
+        } else {
+            tail = Arrays.copyOfRange(array, recent, array.length);
+            recent = 0;
+            position = 0;
+            array = EMPTY;
+        }
+    }
+    
+    /**
+     * Sprawdza,czy przetwarzanie wykorzystało wszystkie przekazane dane
+     * @return 
+     */
+    public boolean pending() {
+        return tail != null;
+    }
+
+}

+ 704 - 0
assira/src/main/java/net/ranides/assira/text/StringUtils.java

@@ -0,0 +1,704 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+
+package net.ranides.assira.text;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import net.ranides.assira.collection.arrays.NativeArrayAllocator;
+import net.ranides.assira.lexer.CharStreamer;
+import net.ranides.assira.math.MathUtils;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public final class StringUtils {
+
+	private StringUtils() {
+		/* utility class */
+	}
+	
+	public static String quote(String value) {
+        if (null == value || value.isEmpty()) {
+            return "\"\"";
+        }
+        
+        int offset = 1, size = quoteCount(value);
+        char c, result[] = new char[size];
+        
+        for(int i=0, n=value.length(); i<n; i++) {
+            c = value.charAt(i);
+            if( c=='\\' || c=='"') { result[offset++] = '\\'; }
+            result[offset++] = c;
+        }
+        result[0] = '"';
+        result[size-1] = '"';
+        return new String(result);
+    }
+	
+	private static int quoteCount(String value) {
+        int size = value.length() + 2;
+        char c;
+        for(int i=0, n=value.length(); i<n; i++) {
+            c = value.charAt(i);
+            if( c=='\\' || c=='"') { size++; }
+        }
+        return size;
+    }
+	
+	public static String unquote(String value) {
+        String test = unquoteCheck(value);
+        if(null != test) { return test; }
+
+        // tak, obie funkcje są "brzydkie" ale kod działa
+        // 5-10 razy szybciej od String#replaceAll
+        // 2 razy szybciej od apache.commons.StrBuilder#replaceAll
+        char[] result = new char[value.length()-2];
+        return new String(result, 0, removeQuotes(result, removeSlashes(result, value) ) );
+    }
+    
+    private static String unquoteCheck(String value) {
+        if (null == value || value.isEmpty() ) {
+            return "";
+        }
+        if("\"".equals(value)) {
+            throw new IllegalArgumentException("string quotes must be opened and closed but only single quote found.");
+        }
+        boolean as = value.charAt(0)=='"';
+        boolean at = value.charAt(value.length()-1)=='"';
+        if( at!=as) {
+            throw new IllegalArgumentException("string quotes must be opened and closed.");
+        } else {
+            return !at ? value : null;
+        }
+    }
+    
+    private static int removeSlashes(char[] buffer, String source) {
+        int position    = 0;
+        boolean skip    = false;
+
+        for(int i=1, length=source.length()-1; i<length; i++) {
+            char recent = source.charAt(i);
+            if(recent=='\\') {
+                if(skip) { 
+                    skip = false; 
+                } else {
+                    buffer[position++] = recent;
+                    skip = true;
+                }
+            } else {
+                buffer[position++] = recent;
+                skip = false;
+            }
+        }
+        return position;
+    }
+    
+    private static int removeQuotes(char[] buffer, int length) {
+        int position    = 0;
+        boolean skip       = false;
+        
+        for(int i=0; i<length; i++) {
+            char c = buffer[i];
+            if(c=='"') {
+                if(skip) { 
+                    skip = false; 
+                    buffer[position-1] = '"';
+                } else {
+                    buffer[position++] = c;
+                    skip = true;
+                }
+            } else {
+                buffer[position++] = c;
+                skip = (c=='\\');
+            }
+        }
+        return position;
+    }
+	
+	/**
+	 * Rozbija tekst jako separatora używając spacji. Fragmenty wzięte w " " traktuje jako całość.
+	 * @param text
+	 * @return 
+	 */
+	public static List<String> split(String text) {
+        List<String> result = new ArrayList<>();
+        boolean inquote = false;
+        boolean escape = false;
+        int last = 0;
+        for(int i=0; i<text.length(); i++) {
+            if( !escape && '\\' == text.charAt(i) ) { 
+                escape = true;
+                continue;
+            }
+            if(escape) {
+                escape = false;
+                continue;
+            }
+            if( '"' == text.charAt(i) ) { 
+                inquote = !inquote; 
+                continue;
+            }
+            if( ' ' == text.charAt(i) && !inquote ) {
+                if( last-i != 0 ) {
+                    result.add( unquote(text.substring(last, i)) );
+                }
+                last = i+1;
+            }
+        }
+        String tail = text.substring(last);
+        if(!tail.trim().isEmpty()) {
+            result.add( unquote(tail) );
+        }
+        return result;
+    }
+	
+	private enum CharsType { STR, OCT }
+	
+	private static final char[] C_ESC_SEQ	= new char[]{'\033','['};
+	private static final char[] C_ESC		= new char[]{'\033'};
+	private static final char[] C_SLASH		= new char[]{'\\'};
+	private static final char[] C_BKSPACE	= new char[]{'\b'};
+	private static final char[] C_FORM		= new char[]{'\f'};
+	private static final char[] C_TAB		= new char[]{'\t'};
+	private static final char[] C_CR		= new char[]{'\r'};
+	private static final char[] C_NL		= new char[]{'\n'};
+	
+	public static String compile(String input) {
+		StringBuilder builder = new StringBuilder();
+        
+        CharStreamer<CharsType> stream = new CharStreamer<>((type, data)->{
+			switch(type) {
+				case STR: 
+					builder.append(data); 
+					break;
+				case OCT: 
+					builder.append( (char)Integer.parseInt(new String(data),8) );
+					break;
+				default: 
+					throw new AssertionError("Unsupported type: " + type);
+			}
+		});
+        stream.open(input.toCharArray());
+        
+        while( !stream.end() ) {
+            char c = stream.peek();
+            if( c=='\\' ) {
+                stream.flush(CharsType.STR);
+                stream.skip();
+                processEsc(stream);
+            } else {
+                stream.skip();
+            }
+        }
+        stream.flush(CharsType.STR);
+        stream.close();
+        return builder.toString();
+    }
+    
+    private static void processEsc(CharStreamer<CharsType> stream) {
+        if( stream.end() ) {
+            stream.close();
+            return;
+        } 
+        
+        char code = stream.get();
+        
+        if('n'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_NL);
+        } 
+        else if('r'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_CR);
+        } 
+        else if('t'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_TAB);
+        } 
+        else if('f'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_FORM);
+        } 
+        else if('b'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_BKSPACE);
+        } 
+        else if('\\'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_SLASH);
+        } 
+        else if('e'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_ESC);
+        } 
+        else if('^'==code) {
+            stream.flush();
+            stream.send(CharsType.STR, C_ESC_SEQ);
+        } 
+        else if(Character.isDigit(code)) {
+            processOct(stream);
+        }    
+    }
+	
+    
+    private static boolean isOct(char ch) {
+        return (ch>='0' && ch<='7');
+    }
+        
+    private static void processOct(CharStreamer<CharsType> stream) {
+        if( isOct(stream.peek()) ) { stream.skip(); } 
+        if( isOct(stream.peek()) ) { stream.skip(); } 
+        stream.flush(CharsType.OCT, 1,0);
+    }
+	
+	public static String decodeURL(String input) {
+        return decodeURL(input, Charsets.UTF8);
+    }
+	
+	public static String decodeURL(String input, Charset charset) {
+        try {
+            return URLDecoder.decode(input, charset.name());
+        } catch(UnsupportedEncodingException cause) {
+			throw new AssertionError("Unsupported charset: " + charset, cause);
+        }
+    }
+	
+    public static String remove(String text, String fragment) {
+        StringTokenizer tokenizer = new StringTokenizer(text, fragment, false);
+        StringBuilder result = new StringBuilder( text.length() );
+        while (tokenizer.hasMoreElements()) {
+            result.append( tokenizer.nextElement() );
+        }
+        return result.toString();
+    }
+
+    public static String remove(String text, char value) {
+        final int size = text.length();
+        final char buffer[] = new char[size];
+        char c;
+        int position = 0;
+        for(int i=0; i<size; i++) {
+            c = text.charAt(i);
+            if(c!=value) { buffer[position++] = c; }
+        }
+        return new String(buffer, 0, position);
+    }
+	
+    public static String removePrefix(String text, String prefix) {
+        if(text.startsWith(prefix)) {
+            return text.substring(prefix.length());
+        }
+        return text;
+    }
+
+    public static String removeSuffix(String text, String suffix) {
+        if(text.endsWith(suffix)) {
+            return text.substring(0, text.length() - suffix.length());
+        }
+        return text;
+    }
+	
+	public static String condense(String text, char value) {
+        final int size = text.length();
+        final char buffer[] = new char[size];
+        char c;
+        boolean pc = false;
+        int position = 0;
+        for(int i=0; i<size; i++) {
+            c = text.charAt(i);
+            if(c == value) {
+                if(!pc) {
+                    buffer[position++] = c;
+                    pc = true;
+                }
+            } else {
+                buffer[position++] = c;
+                pc = false;
+            }
+        }
+        return new String(buffer, 0, position);
+    }
+	
+    public static String ltrim(String source) {
+        int n = source.length(), offset = 0;
+        while( offset<n && Character.isWhitespace(source.charAt(offset)) ) { 
+			offset++; 
+		}
+        return source.substring(offset);
+    }
+
+    public static String rtrim(String source) {
+        int offset = source.length() - 1;
+        while(offset >= 0 && Character.isWhitespace(source.charAt(offset)) ) { 
+			offset--; 
+		}
+        return source.substring(0, offset+1);
+    }
+
+    public static String trim(String source){
+        return source.trim();
+    }
+	
+	public static String replace(String text, Pattern pattern, Function<Matcher, String> converter) {
+        Matcher matcher = pattern.matcher(text);
+        StringBuffer buffer = new StringBuffer();
+        while(matcher.find()) {
+            String str = converter.apply(matcher);
+            if( null!=str ) { 
+				matcher.appendReplacement(buffer, ""); 
+				buffer.append(str); 
+			}
+        }
+        matcher.appendTail(buffer);
+        return buffer.toString();
+    }
+
+    public static String replace(String text, char search, char newval) {
+        final char[] buffer = text.toCharArray();
+        for(int i=0; i<buffer.length; i++) {
+            if(buffer[i]==search) { 
+				buffer[i] = newval; 
+			}
+        }
+        return new String(buffer);
+    }
+
+    public static String replace(String text, char search, String newval) {
+        if(text.isEmpty()) {
+            return text;
+        }
+        int start = 0;
+        int end = text.indexOf(search, start);
+        if (end == -1) {
+            return text;
+        }
+        int delta = (newval.length()-1);
+
+        StringBuilder buf = new StringBuilder(text.length() + 16*delta );
+        while (end != -1) {
+            buf.append(text.substring(start, end)).append(newval);
+            start = end + 1;
+            end = text.indexOf(search, start);
+        }
+        buf.append(text.substring(start));
+        return buf.toString();
+    }
+	
+	public static <T> String join(Iterable<T> values, String separator) {
+		return join(values, separator, Object::toString);
+	}
+	
+	public static <T> String join(Iterable<? extends T> values, String separator, Function<T,String> function) {
+		Iterator<? extends T> itr = values.iterator();
+        if ( !itr.hasNext() ) {
+            return "";
+        }
+        StringBuilder builder = new StringBuilder();
+        for (;;) {
+            T item = itr.next();
+            builder.append(function.apply(item));
+            if ( !itr.hasNext() ) {
+                return builder.toString();
+            }
+            builder.append(separator);
+        }
+	}
+	
+	public static <T> String join(T[] values, String separator) {
+		return join(values, separator, Object::toString);
+	}
+	
+	public static <T> String join(T[] values, String separator, Function<T,String> function) {
+        if ( 0==values.length ) {
+            return "";
+        }
+        StringBuilder builder = new StringBuilder();
+		builder.append(function.apply(values[0]));
+        for (int i=1; i<values.length; i++) {
+            builder.append(separator).append(function.apply(values[i]));
+        }
+		return builder.toString();
+	}
+	
+	public static String capitalize(String value) {
+		return capitalize(value, Locale.ROOT);
+	}
+	
+	public static String capitalize(String value, Locale locale) {
+        if( value.isEmpty() ) {
+            return "";
+        }
+        if( 1==value.length() ) {
+            return value.toUpperCase(locale);
+        }
+        return new StringBuilder(value.length())
+            .append( value.substring(0, 1).toUpperCase(locale) )
+            .append(value.substring(1))
+            .toString();
+    }
+	
+	public static String uncapitalize(String value) {
+		return uncapitalize(value, Locale.ROOT);
+	}
+
+    public static String uncapitalize(String value, Locale locale) {
+        if( value.isEmpty() ) {
+            return "";
+        }
+        if( 1==value.length() ) {
+            return value.toLowerCase(locale);
+        }
+        return new StringBuilder(value.length())
+            .append( value.substring(0, 1).toLowerCase(locale) )
+            .append(value.substring(1))
+            .toString();
+    }
+	
+	public static String camelize(String value) {
+		return camelize(value, Locale.ROOT);
+	}
+	
+    public static String camelize(String value, Locale locale) {
+        String[] parts = value.split("_+");
+        if(0==parts.length) {
+            return "";
+        }
+        if(1==parts.length) {
+            return parts[0].toLowerCase(locale);
+        }
+
+        StringBuilder result = new StringBuilder(value.length()-parts.length);
+		int i=0;
+		for(;i<parts.length; i++) {
+			if(!parts[i].isEmpty()) { break; }
+		}
+		result.append(parts[i].toLowerCase(locale));
+		
+		for(;i<parts.length; i++) {
+			result.append(parts[i].substring(0, 1).toUpperCase(locale));
+			result.append(parts[i].substring(1).toLowerCase(locale));
+		}
+        return result.toString();
+    }
+	
+	public static String uncamelize(String value) {
+		return uncamelize(value, Locale.ROOT);
+	}
+
+    public static String uncamelize(String value, Locale locale) {
+        return replace(value, Pattern.compile("(\\p{Lu})"), (hit) -> {
+			String v = hit.group(1).toLowerCase(locale);
+			return (0 == hit.start(1)) ? v : "_"+v;
+        });
+    }
+	
+	public static String clip(String text, int size) {
+        return text.substring(0, MathUtils.clip(size, 0, text.length()));
+    }
+	
+    public static String lpadd(String text, char padd, int size) {
+        int n = text.length();
+        int d = size - n;
+        if(d <= 0) {
+            return text;
+        }
+        char[] buffer = new char[size];
+        text.getChars(0, n, buffer, 0);
+        for(int i=n; i<size; i++) { buffer[i] = padd; }
+        return new String(buffer);
+    }
+
+    public static String rpadd(String text, char padd, int size) {
+		
+		Collection<String> c = null;
+		int cs = 44;
+		c.stream().map((s)->s.substring(cs));
+		
+        int n = text.length();
+        int d = size - n;
+        if(d <= 0) {
+            return text;
+        }
+        char[] buffer = new char[size];
+        text.getChars(0, n, buffer, d);
+        for(int i=0; i<d; i++) { buffer[i] = padd; }
+        return new String(buffer);
+    }
+	
+    public static String commonPrefix(String a, String b) {
+        if( a.isEmpty() || b.isEmpty() ) { 
+            return ""; 
+        }
+        int n = Math.min(a.length(), b.length());
+        for(int i=0; i<n; i++) {
+            if( a.charAt(i)!=b.charAt(i) ) { return a.substring(0,i); }
+        }
+        return a.substring(0,n);
+    }
+
+    public static String commonPrefix(Collection<String> values) {
+		if(values.isEmpty()) {
+			return "";
+		}
+		return values.stream().reduce(values.iterator().next(), StringUtils::commonPrefix);
+    }
+	
+	public static String commonSuffix(String a, String b) {
+        if( a.isEmpty() || b.isEmpty() ) { 
+            return ""; 
+        }
+		int as = a.length();
+		int bs = b.length();
+        int n = Math.min(as, bs);
+        for(int i=0; i<n; i++) {
+            if( a.charAt(as-i)!=b.charAt(bs-i) ) { return a.substring(as-i,i); }
+        }
+        return a.substring(as-n,n);
+    }
+	
+	public static String commonSuffix(Collection<String> values) {
+		if(values.isEmpty()) {
+			return "";
+		}
+		return values.stream().reduce(values.iterator().next(), StringUtils::commonSuffix);
+    }
+	
+	public static String reverse(String value) {
+        return value.isEmpty() ? value : new StringBuilder(value).reverse().toString();
+    }
+	
+    public static String before(String text, String end) {
+        int iend = text.indexOf(end);
+        return (-1 == iend) ? "" : text.substring(0, iend);
+    }
+    
+    public static String after(String text, String start) {
+        int istart = text.indexOf(start);
+        return (-1 == istart) ? "" : text.substring(istart + start.length());
+    }
+	
+	public static String between(String text, String start, String end) {
+        if(start == null) {
+            return before(text, end);
+        }
+        if(end == null) {
+            return after(text, start);
+        }
+        int istart = text.indexOf(start);
+        if( istart == -1 ) {
+            return "";
+        }
+        istart += start.length();
+        int iend = text.indexOf(end, istart);
+        if( iend == -1 ) {
+            return "";
+        }
+        return text.substring(istart, iend);
+    }
+	
+    public static CharSequence repeat(int n, String text) {
+        return new NSSequence(n, text);
+    }
+
+    public static CharSequence repeat(int n, char chr) {
+        return new NCSequence(n, chr);
+    }
+
+    private static class NCSequence implements CharSequence {
+        private final int count;
+        private final char chr;
+
+        public NCSequence(int count, char chr) {
+            this.count = count;
+            this.chr = chr;
+        }
+        @Override
+        public char charAt(int index) {
+            if( index >= count ) {
+                throw new IndexOutOfBoundsException(index + " out of bound " + count);
+            }
+            return chr;
+        }
+		
+        @Override
+        public int length() {
+            return count;
+        }
+		
+        @Override
+        public CharSequence subSequence(int start, int end) {
+			NativeArrayAllocator.ensureFromTo(count, start, end);
+            return new NCSequence(end-start, chr);
+        }
+
+        @Override
+        public String toString() {
+            int n = length();
+            char[] data = new char[n];
+            Arrays.fill(data, chr);
+            return new String(data);
+        }
+    }
+
+    private static class NSSequence implements CharSequence {
+        private final int start;
+        private final int end;
+        private final String text;
+
+        public NSSequence(int count, String text) {
+            this(text, 0, count * text.length());
+        }
+
+        public NSSequence(String text, int start, int end) {
+            this.start  = start;
+            this.end    = end;
+            this.text   = text;
+        }
+
+        @Override
+        public char charAt(int index) {
+            if( index >= end ) {
+                throw new IndexOutOfBoundsException(index + " out of bound " + end);
+            }
+            return text.charAt( (index+start) % text.length() );
+        }
+
+        @Override
+        public int length() {
+            return end-start;
+        }
+
+        @Override
+        public CharSequence subSequence(int start, int end) {
+			NativeArrayAllocator.ensureFromTo(length(), start, end);
+            return new NSSequence(text, start, end);
+        }
+
+        @Override
+        public String toString() {
+            int n = length();
+            char[] data = new char[n];
+            for(int i=0; i<n; i++) { data[i] = charAt(i); }
+            return new String(data);
+        }
+
+    }
+
+}