/*
* Copyright 2011-2017 Kay Stenschke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kstenschke.shifter.utils;
import com.intellij.openapi.editor.Document;
import com.kstenschke.shifter.models.shiftableTypes.OperatorSign;
import com.kstenschke.shifter.utils.natorder.NaturalOrderComparator;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Static helper methods for analysis and manipulation of texts
*/
public class UtilsTextual {
private final static Pattern LTRIM = Pattern.compile("^\\s+");
private final static Pattern RTRIM = Pattern.compile("\\s+$");
public static String ltrim(String s) {
return LTRIM.matcher(s).replaceAll("");
}
public static String rtrim(String s) {
return RTRIM.matcher(s).replaceAll("");
}
/**
* @param str String to be checked
* @return boolean Is the given string fully lower case?
*/
public static boolean isAllUppercase(String str) {
return str.equals(str.toUpperCase());
}
public static boolean isMultiLine(@Nullable String str) {
return null != str && str.contains("\n");
}
/**
* @param lines
* @param shiftUp
* @return Given lines sorted alphabetically ascending / descending
*/
public static List<String> sortLinesNatural(List<String> lines, boolean shiftUp) {
DelimiterDetector delimiterDetector = new DelimiterDetector(lines);
Collections.sort(lines, new NaturalOrderComparator());
if (!shiftUp) {
Collections.reverse(lines);
}
boolean isDelimitedLastLine = delimiterDetector.isDelimitedLastLine();
if (delimiterDetector.isFoundDelimiter() && !isDelimitedLastLine) {
// Maintain detected lines delimiter (ex: comma-separated values, w/ last item w/o trailing comma)
lines = addDelimiter(lines, delimiterDetector.getCommonDelimiter(), false);
}
return lines;
}
/**
* @param haystack
* @param needle
* @return boolean
*/
public static boolean containsCaseInSensitive(@Nullable String haystack, String needle) {
return null != haystack && Pattern.compile(Pattern.quote(needle), Pattern.CASE_INSENSITIVE).matcher(haystack).find();
}
/**
* @param str
* @param characters
* @return boolean
*/
public static boolean containsOnly(@Nullable String str, String[] characters) {
if (null == str || str.isEmpty()) {
return false;
}
for(String c : characters) {
str = str.replaceAll(c, "");
}
return str.isEmpty();
}
public static boolean isWrappedIntoQuotes(@Nullable String str) {
return isWrappedWith(str, "'") || isWrappedWith(str, "\"");
}
/**
* @param str
* @param wrap
* @return boolean Is the given string wrapped into the wrapper string?
*/
public static boolean isWrappedWith(@Nullable String str, String wrap, boolean needsToBeTwoSided, boolean needsContent) {
if (null == str) {
return false;
}
int stringLength = str.length();
return !((needsContent && stringLength < 3) || (needsToBeTwoSided && stringLength < 2))
&& str.startsWith(wrap) && str.endsWith(wrap);
}
private static boolean isWrappedWith(@Nullable String str, String wrap) {
return isWrappedWith(str, wrap, false, false);
}
/**
* @param str String to be checked
* @return boolean Does the given string contain any slash or backslash?
*/
public static boolean containsSlashes(String str) {
return null != str && (str.contains("\\") || str.contains("/"));
}
/**
* @param str String to be checked
* @return boolean Does the given string contain single or double quotes?
*/
public static boolean containsQuotes(String str) {
return null != str && (str.contains("\"") || str.contains("'"));
}
/**
* @param str String to be checked
* @return String Given string w/ contained slashes swapped against backslashes and vise versa
*/
public static String swapSlashes(@Nullable String str) {
return null == str
? null
: str.
replace("\\", "###SHIFTERSLASH###").
replace("/", "\\").
replace("###SHIFTERSLASH###", "/");
}
/**
* @param str String to be checked
* @return String Given string w/ contained single quotes swapped against double quotes and vise versa
*/
public static String swapQuotes(String str) {
return null == str
? null
: str
.replace("\"", "###SHIFTERSINGLEQUOTE###")
.replace("'", "\"")
.replace("###SHIFTERSINGLEQUOTE###", "'");
}
/**
* @param str String to be converted
* @return String Given string converted to lower case w/ only first char in upper case
*/
public static String toUcFirst(@Nullable String str, boolean makeRestLower) {
if (null == str) {
return null;
}
if (str.isEmpty()) {
return "";
}
return Character.toUpperCase(str.charAt(0)) + (makeRestLower
? str.substring(1).toLowerCase()
: str.substring(1));
}
public static String toUcFirst(@Nullable String str) {
return toUcFirst(str, false);
}
/**
* @param str String to be converted
* @return String Given string w/ first char in lower case
*/
public static String toLcFirst(String str) {
return str.isEmpty() ? "" : Character.toLowerCase(str.charAt(0)) + str.substring(1);
}
public static boolean isLcFirst(String str) {
Character leadChar = str.charAt(0);
return Character.toLowerCase(leadChar) == leadChar;
}
/**
* Check whether given string is lower case w/ only first char in upper case
*
* @param str String to be checked
* @return boolean If the string is lower case w/ only first char in upper case.
*/
public static boolean isUcFirst(String str) {
return str.isEmpty() || str.equals(UtilsTextual.toUcFirst(str));
}
public static boolean isUcFirst(char c) {
return isUcFirst("" + c);
}
public static boolean isUpperCamelCase(@Nullable String str) {
return null != str && str.matches("[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*");
}
public static boolean isLowerCamelCase(@Nullable String str) {
return null != str && str.matches("[a-z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*");
}
public static boolean isCamelCase(@Nullable String str) {
return null != str && str.length() > 2 && (isLowerCamelCase(str) || isUpperCamelCase(str));
}
/**
* @param str CamelCased string w/ lower or upper lead character
* @return String[]
*/
@NotNull
public static String[] splitCamelCaseIntoWords(@Nullable String str) {
if (null == str) {
return new String[0];
}
boolean isUcFirst = isUcFirst(str);
if (isUcFirst) {
str = UtilsTextual.toLcFirst(str);
}
String parts[] = str.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])");
if (isUcFirst) {
parts[0] = toUcFirst(parts[0]);
}
return parts;
}
/**
* Find operator DIRECTLY neighbouring (closer than any other character)
*
* @param str
* @param offset
* @return Null|String
*/
public static String getOperatorAtOffset(CharSequence str, int offset) {
int textLength = str.length();
if (textLength == 0 || offset >= textLength || str.toString().trim().isEmpty()) {
return null;
}
String operatorOnLHS = offset > 2
? str.subSequence(offset - 2, offset + 1).toString()
: null;
if (operatorOnLHS != null && OperatorSign.isWhitespaceWrappedOperator(operatorOnLHS)) {
return operatorOnLHS.trim();
}
String operatorToTheRight =
offset < textLength - 2
&& offset > 0 && Character.isWhitespace(str.charAt(offset-1))
? str.subSequence(offset - 1, offset + 2).toString()
: null;
return (operatorToTheRight != null && OperatorSign.isWhitespaceWrappedOperator(operatorToTheRight))
? operatorToTheRight.trim()
// No operator found
: null;
}
/**
* @param str
* @param offset
* @return Integer
*/
public static Integer getStartOfOperatorAtOffset(CharSequence str, int offset) {
int textLength = str.length();
if (textLength == 0 || offset >= textLength) {
return null;
}
String operatorToTheLeft = offset > 2
? str.subSequence(offset - 2, offset + 1).toString()
: null;
if (operatorToTheLeft != null && OperatorSign.isWhitespaceWrappedOperator(operatorToTheLeft)) {
return offset - 1;
}
String operatorToTheRight = offset < (textLength - 2) &&
(offset > 0 && Character.isWhitespace(str.charAt(offset-1)))
? str.subSequence(offset - 1, offset + 2).toString()
: null;
return (operatorToTheRight != null && OperatorSign.isWhitespaceWrappedOperator(operatorToTheRight))
? offset
: null;
}
/**
* Get word at caret offset out of given text
*
* @param str The full text
* @param offset Character offset of caret
* @param allowHyphens Treat "-" as word character?
* @return The extracted word or null
*/
public static String getWordAtOffset(CharSequence str, int offset, boolean allowHyphens) {
int textLength = str.length();
if (textLength == 0 || offset < 0 || offset >= textLength) {
return null;
}
// Initialize offset
if (offset > 0
&& !isJavaIdentifierPart(str.charAt(offset), allowHyphens)
&& isJavaIdentifierPart(str.charAt(offset - 1), allowHyphens)
) {
offset--;
}
if (!isJavaIdentifierPart(str.charAt(offset), allowHyphens)) {
return null;
}
// Decrement offset until start of word or CharSequence
int start = offset;
int end = offset;
while (start > 0 && isJavaIdentifierPart(str.charAt(start - 1), allowHyphens)) {
start--;
}
// Increment offset until end of word or CharSequence
while (end < textLength && isJavaIdentifierPart(str.charAt(end), allowHyphens)) {
end++;
}
return str.subSequence(start, end).toString();
}
/**
* @param c
* @param allowHyphens
* @return boolean
*/
public static boolean isJavaIdentifierPart(char c, boolean allowHyphens) {
return allowHyphens
? Character.isJavaIdentifierPart(c) || c == '-'
: Character.isJavaIdentifierPart(c);
}
public static boolean isLetter(char c) {
return Character.toString(c).matches("[a-zA-Z]+");
}
public static boolean isCamelIdentifierPart(char c) {
return isCamelIdentifierPart(c, true);
}
public static boolean isCamelIdentifierPart(char c, boolean allowNumbers) {
return allowNumbers
? Character.toString(c).matches("[a-zA-Z0-9]+")
: Character.toString(c).matches("[a-zA-Z]+");
}
/**
* @param str
* @param offsetStart Sub sequence start character offset
* @param offsetEnd Sub sequence end character offset
* @return String Sub sequence of given offsets out of given text
*/
public static String getSubString(CharSequence str, int offsetStart, int offsetEnd) {
return str.length() == 0 ? null : str.subSequence(offsetStart, offsetEnd).toString();
}
/**
* @param str Full text
* @param offset Offset from before which to extract one character
* @return String Character BEFORE word at given caret offset
*/
public static String getCharBeforeOffset(CharSequence str, int offset) {
if (str.length() == 0 || offset == 0) {
return "";
}
return offset > 0
? str.subSequence(offset - 1, offset).toString()
: "";
}
/**
* @param str Full text
* @param offset Offset from after which to extract one character
* @return String Character AFTER word at caret offset
*/
public static String getCharAfterOffset(CharSequence str, int offset) {
if (str.length() < offset+2 || offset == 0) {
return "";
}
return offset > 0 ? str.subSequence(offset+1, offset+2).toString() : "";
}
public static int getEndOfWordAtOffset(CharSequence str, int offset) {
int strLength = str.length();
if (strLength == 0 || offset < 0) {
return 0;
}
if (offset > strLength) {
return strLength;
}
while(offset < strLength) {
if (!Character.isJavaIdentifierPart(str.charAt(offset))) {
return offset;
}
offset++;
}
return strLength;
}
/**
* @param str Text to be analyzed
* @param offset Character offset in text, intersecting the word dealing w/
* @return int Starting position offset of word at given offset in given CharSequence
*/
public static int getStartOfWordAtOffset(CharSequence str, int offset) {
int strLength = str.length();
if (strLength == 0 || offset < 0) {
return 0;
}
if (offset > strLength) {
return strLength;
}
if (offset > 0 && !Character.isJavaIdentifierPart(str.charAt(offset)) && Character.isJavaIdentifierPart(str.charAt(offset - 1))) {
offset--;
}
if (!Character.isJavaIdentifierPart(str.charAt(offset))) {
return 0;
}
int start = offset;
while (start > 0 && Character.isJavaIdentifierPart(str.charAt(start - 1))) {
start--;
}
return start;
}
/**
* @param doc Document
* @param startLine Number of first line to be extracted
* @param endLine Number of last line of extract
* @return List<String> Extracted list of lines
*/
public static List<String> extractLines(Document doc, int startLine, int endLine) {
List<String> lines = new ArrayList<String>(endLine - startLine);
for (int i = startLine; i <= endLine; i++) {
String line = UtilsTextual.getLine(doc, i);
lines.add(line);
}
return lines;
}
/**
* @param doc Document to extract the line from
* @param lineNumber Number of line to be extracted
* @return String The extracted line
*/
public static String getLine(Document doc, int lineNumber) {
int lineSeparatorLength = doc.getLineSeparatorLength(lineNumber);
int startOffset = doc.getLineStartOffset(lineNumber);
int endOffset = doc.getLineEndOffset(lineNumber) + lineSeparatorLength;
String line = doc.getCharsSequence().subSequence(startOffset, endOffset).toString();
// If last line has no \n, add it one
// This causes adding a \n at the end of file when sort is applied on whole file and the file does not end
// w/ \n... This is fixed after.
return line + (lineSeparatorLength == 0 ? "\n" : "");
}
/**
* @param str
* @param offset
* @return String
*/
public static String getLineAtOffset(String str, int offset) {
int lenText = str.length();
int offsetStart = offset;
while (offsetStart > 0 && str.charAt(offsetStart-1) != '\n') {
offsetStart--;
}
int offsetEnd = offset;
while (offsetEnd < lenText && str.charAt(offsetEnd) != '\n') {
offsetEnd++;
}
return str.substring(offsetStart, offsetEnd).trim();
}
/**
* @param lines List of lines (strings) to be joined
* @return StringBuilder
*/
public static StringBuilder joinLines(List<String> lines) {
return joinLines(lines, "");
}
public static StringBuilder joinLines(List<String> lines, String appendStr) {
StringBuilder builder = new StringBuilder();
for (String line : lines) {
builder.append(line + appendStr);
}
return builder;
}
public static String removeLineBreaks(String str) {
return str.replaceAll("\n", "").replaceAll("\r", "");
}
/**
* @param string
* @param toReplace
* @param replacement
* @return string Given string w/ last occurrence of "toReplace" replaced w/ "replacement"
*/
public static String replaceLast(String string, String toReplace, String replacement) {
int pos = string.lastIndexOf(toReplace);
return pos == -1
? string
: string.substring(0, pos)
+ replacement
+ string.substring(pos + toReplace.length(), string.length());
}
/**
* @param numberString
* @param length
* @return String Given numerical string, w/ given length (if >= original length)
*/
public static String formatAmountDigits(String numberString, int length) {
while (numberString.length() < length) {
numberString = "0" + numberString;
}
return numberString;
}
/**
* Check given (alphabetically sorted) lines for any line(s) being duplicated
*
* @param lines
* @return boolean
*/
public static boolean hasDuplicateLines(String lines) {
String[] linesArray = lines.split("\n");
String previousLine = "";
int index = 0;
for(String currentLine : linesArray) {
if (index > 0 && currentLine.equals(previousLine)) {
return true;
}
index++;
previousLine = currentLine;
}
return false;
}
public static String reduceDuplicateLines(String lines) {
String[] linesArray = lines.split("\n");
String[] resultLines = new String[linesArray.length];
int index = 0;
int resultIndex = 0;
String previousLine = "";
for(String currentLine : linesArray) {
if (index == 0 || !currentLine.equals(previousLine)) {
resultLines[resultIndex] = currentLine;
resultIndex++;
}
index++;
previousLine = currentLine;
}
return StringUtils.join(resultLines, "\n");
}
public static String getLeadWhitespace(@Nullable String str) {
if (null == str) {
return null;
}
String whitespace = "";
int offset = 0;
int length = str.length();
while (offset < length && Character.isWhitespace(str.charAt(offset))) {
whitespace += str.charAt(offset);
offset++;
}
return whitespace;
}
/**
* Use regEx matcher to extract all variables in given code
*
* @param str
* @return List<String> All PHP var names
*/
@NotNull
public static List<String> extractPhpVariables(String str) {
return getPregMatches(str, "\\$[a-zA-Z0-9_]+");
}
@NotNull
public static List<String> extractQuotedStrings(String text, String quoteCharacter) {
return getPregMatches(
text,
"(?<=" + quoteCharacter + ")[a-zA-Z0-9_]+(?=" + quoteCharacter + ")"
);
}
@NotNull
public static List<String> getPregMatches(@Nullable String str, String pattern) {
if (null == str) {
return new ArrayList<String>();
}
Matcher m = Pattern.compile(pattern).matcher(str);
List<String> allMatches = new ArrayList<String>();
while (m.find()) {
if (!allMatches.contains(m.group())) {
allMatches.add(m.group());
}
}
return allMatches;
}
/**
* @param str
* @return Name of primitive (PHP) data type
*/
public static String guessPhpDataTypeByName(String str) {
str = str.toLowerCase();
if (str.matches("\\w*name|\\w*title|\\w*url")) {
return "string";
}
if (str.matches("(\\w*delim(iter)*|\\w*dir(ectory)*|\\w*domain|\\w*key|\\w*link|\\w*name|\\w*path\\w*|\\w*prefix|\\w*suffix|charlist|comment|\\w*file(name)*|format|glue|haystack|html|intput|locale|message|name|needle|output|replace(ment)*|salt|separator|str(ing)*|url)\\d*")) {
return "string";
}
if (str.matches("(arr(ay)|\\w*pieces|\\w*list|\\w*items|\\w*ids)\\d*")) {
return "array";
}
if (str.matches("(\\w*day|\\w*end|\\w*expire|\\w*handle|\\w*height|\\w*hour(s)*|\\w*id|\\w*index|\\w*len(gth)*|\\w*mask|\\w*pointer|\\w*quality|\\w*s(e)*ize|\\w*steps|\\w*start|\\w*year\\w*|ascii|base|blue|ch|chunklen|fp|green|len|limit|max|min|mode|month|multiplier|now|num|offset|op(eration)*|red|time(stamp)*|week|wid(th)*|x|y)\\d*")) {
return "int";
}
if (str.matches("(has\\w+|is\\w+|return\\w*|should\\w*)")) {
return "bool";
}
if (str.matches("(\\w*gamma|percent)\\d*")) {
return "float";
}
if (str.matches("(\\wmodel|\\w*obj(ect)*)\\d*")) {
return "Object";
}
if (str.matches("(\\w*s)\\d*|\\w*arr(ay)*|\\w*items|\\w*data|data\\w*")) {
return "array";
}
return "unknown";
}
/**
* @param lines
* @param delimiter
* @param isDelimitedLastLine
* @return Given lines ending w/ given delimiter, optionally also the last line
*/
public static List<String> addDelimiter(List<String> lines, String delimiter, boolean isDelimitedLastLine) {
int amountLines = lines.size();
int index = 0;
for (String line : lines) {
line = line.trim();
boolean isLastLine = index + 1 == amountLines;
if ((!isLastLine || isDelimitedLastLine) && !line.endsWith(delimiter)) {
line = line + delimiter;
}
if (isLastLine && !isDelimitedLastLine && line.endsWith(delimiter)) {
// Remove delimiter from last line
line = line.substring(0, line.length() - 1);
}
lines.set(index, line + "\n");
index++;
}
return lines;
}
}