All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apple.foundationdb.util.StringUtils Maven / Gradle / Ivy

The newest version!
/*
 * StringUtils.java
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
 *
 * 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.apple.foundationdb.util;

import com.google.common.base.Preconditions;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.PriorityQueue;

/**
 * Utility methods for operating with {@link String}s.
 */
public class StringUtils {
    /**
     * Whether the string is a non-empty string containing only numeric characters.
     *
     * @param s string to test for numeric characters
     * @return whether {@code s} contains only numeric characters
     */
    public static boolean isNumeric(@Nonnull String s) {
        return isNumeric(s, 0);
    }

    /**
     * Whether the substring beginning at {@code beginIndex} is non-empty and contains only
     * numeric characters. This should be equivalent to calling {@code isNumeric(s.substring(beginIndex))}.
     *
     * @param s string to test for numeric characters
     * @param beginIndex index to begin testing for numeric characters
     * @return whether the substring of {@code s} beginning at {@code beginIndex} is non-empty and contains only
     *     numeric characters
     */
    public static boolean isNumeric(@Nonnull String s, int beginIndex) {
        return isNumeric(s, beginIndex, s.length());
    }

    /**
     * Whether the substring from {@code beginIndex} to {@code endIndex} is non-empty and contains only
     * numeric characters. This should be equivalent to calling {@code isNumeric(s.substring(beginIndex, endIndex))}.
     *
     * @param s string to test for numeric characters
     * @param beginIndex index to begin testing for numeric characters
     * @param endIndex index to end testing for numeric characters
     * @return whether the substring of {@code s} beginning at {@code beginIndex} and ending at {@code endIndex}
     *     is non-empty and contains only numeric characters
     */
    public static boolean isNumeric(@Nonnull String s, int beginIndex, int endIndex) {
        Preconditions.checkArgument(beginIndex >= 0 && beginIndex <= s.length(),
                "beginIndex should be within bounds");
        Preconditions.checkArgument(endIndex >= beginIndex && endIndex <= s.length(),
                "endIndex should be within bounds");
        if (beginIndex == endIndex) {
            // Empty string is not vacuously numeric. There must be at least one numeric character
            return false;
        }
        for (int i = beginIndex; i < endIndex; i++) {
            if (!Character.isDigit(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Construct a {@link String} with {@code n} occurrences of a character {@code c}. If {@code n} is less
     * than or equal to zero, the empty string will be returned. Note that if the character is
     * in the high surrogate or low surrogate range (characters necessary to represent
     * Unicode codepoints above U+FFFF in UTF-16), then the returned string will not be valid
     * UTF-16.
     *
     * @param c a character
     * @param n the number of times to repeat {@code c}
     * @return a string with {@code n} occurrences of the string {@code c}
     * @see Character for more on high and low surrogates
     */
    public static String repeat(char c, int n) {
        if (n <= 0) {
            return "";
        }
        char[] arr = new char[n];
        Arrays.fill(arr, c);
        return new String(arr);
    }

    private static final class Replacement implements Comparable {
        private final int index;
        private final String toReplace;
        private final String replaceWith;

        Replacement(int index, String toReplace, String replaceWith) {
            this.index = index;
            this.toReplace = toReplace;
            this.replaceWith = replaceWith;
        }

        @Override
        public int compareTo(final Replacement o) {
            // Order first by index. This means that replacements earlier in the
            // source string will be processed first
            final int indexComparison = Integer.compare(index, o.index);
            if (indexComparison != 0) {
                return indexComparison;
            }

            // For replacements at the same index, prefer the maximum string. If
            // two strings are found at the same location in a source string, then
            // one string must be a prefix of the other. Prefer the longer as it
            // is more specific
            return -1 * toReplace.compareTo(o.toReplace);
        }

        @Nullable
        public Replacement nextOccurrence(@Nonnull String source) {
            int nextIndex = source.indexOf(toReplace, index + 1);
            return nextIndex < 0 ? null : new Replacement(nextIndex, toReplace, replaceWith);
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final Replacement that = (Replacement)o;
            return index == that.index && Objects.equals(toReplace, that.toReplace) && Objects.equals(replaceWith, that.replaceWith);
        }

        @Override
        public int hashCode() {
            return Objects.hash(index, toReplace, replaceWith);
        }
    }

    /**
     * Replace all occurrences of the keys of the {@code replaceMap} in the {@code source} string with their
     * corresponding value in the map. Elements from the {@code replaceMap} are processed in order in the
     * {@code source} string, so if two keys overlap, the one with the lower index will be processed. For example:
     *
     * 
{@code
     *     replaceEach("abc", Map.of("ab", "x", "bc", "y")) = "xc";
     *     replaceEach("abb", Map.of("ab", "x", "bb", "y")) = "xb";
     *     replaceEach("abbb", Map.of("ab", "x", "bb", "y")) = "xy";
     * }
* *

* If one replacement key is a prefix of another key, then the longer key will be preferred when * the longer string is present. (If the other choice was taken, it would never process the longer key, as * any time the longer key is found, the shorter key would also be found.) For example: *

* *
{@code
     *     replaceEach("abc", Map.of("ab", "x", "abc", "y")) = "y";
     *     replaceEach("ababc", Map.of("ab", "x", "abc", "y")) = "xy";
     *     replaceEach("abcab", Map.of("ab", "x", "abc", "y")) = "yx";
     * }
* * @param source string to perform the find and replace on * @param replaceMap mapping used to transform the {@code source} string * @return a string composed by replacing all occurrences of the {@code replaceMap} keys in {@code source} * with their values */ @Nonnull public static String replaceEach(@Nonnull String source, @Nonnull Map replaceMap) { if (source.isEmpty() || replaceMap.isEmpty()) { return source; } // Maintain a priority queue of replacements. This first is ordered by location in the source, // so we'll process the occurrences of strings in order. Ties are broken by taking the longer // string first. Note that if two replaced strings are found at the same index, we retain // both in the priority queue (but will only actually replace one). The other element is retained // so that we know to search for it later on in the string final PriorityQueue replacements = new PriorityQueue<>(replaceMap.size()); for (Map.Entry replaceEntry : replaceMap.entrySet()) { final String replaceString = replaceEntry.getKey(); if (replaceString.isEmpty()) { continue; } int replacementIndex = source.indexOf(replaceString); if (replacementIndex >= 0) { replacements.add(new Replacement(replacementIndex, replaceString, replaceEntry.getValue())); } } if (replacements.isEmpty()) { return source; } int consumedSoFar = 0; final StringBuilder builder = new StringBuilder(source.length()); while (!replacements.isEmpty()) { Replacement replacement = replacements.poll(); if (replacement.index >= consumedSoFar) { if (replacement.index > consumedSoFar) { builder.append(source, consumedSoFar, replacement.index); consumedSoFar = replacement.index; } consumedSoFar += replacement.toReplace.length(); builder.append(replacement.replaceWith); } @Nullable Replacement next = replacement.nextOccurrence(source); if (next != null) { replacements.add(next); } } if (consumedSoFar < source.length()) { builder.append(source.substring(consumedSoFar)); } return builder.toString(); } /** * Returns whether some substring of {@code source} contains {@code searchStr}, ignoring case. * This uses the same criteria as {@link String#equalsIgnoreCase(String)} to determine if * characters are equal ignoring case. * * @param source the source string * @param searchStr the string to search through * @return whether some substring of {@code source} is equal to {@code searchStr} ignoring case */ public static boolean containsIgnoreCase(@Nonnull String source, @Nonnull String searchStr) { if (source.length() < searchStr.length()) { return false; } else if (source.length() == searchStr.length()) { return source.equalsIgnoreCase(searchStr); } else { for (int i = 0; i <= source.length() - searchStr.length(); i++) { // The "true" argument to regionMatches indicates that we are ignoring case. The // underlying method should return true iff: // source.substring(i, searchStr.length()).equalsIgnoreCase(searchStr) if (source.regionMatches(true, i, searchStr, 0, searchStr.length())) { return true; } } return false; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy