001/*
002 * Copyright (C) 2006 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.base;
016
017import static com.google.common.base.Preconditions.checkNotNull;
018import static java.util.Objects.requireNonNull;
019
020import com.google.common.annotations.GwtCompatible;
021import java.io.Serializable;
022import org.jspecify.annotations.Nullable;
023
024/**
025 * Utility class for converting between various ASCII case formats. Behavior is undefined for
026 * non-ASCII input.
027 *
028 * @author Mike Bostock
029 * @since 1.0
030 */
031@GwtCompatible
032public enum CaseFormat {
033  /**
034   * Hyphenated variable naming convention, e.g., "lower-hyphen". This format is also colloquially
035   * known as "kebab case".
036   */
037  LOWER_HYPHEN(CharMatcher.is('-'), "-") {
038    @Override
039    String normalizeWord(String word) {
040      return Ascii.toLowerCase(word);
041    }
042
043    @Override
044    String convert(CaseFormat format, String s) {
045      if (format == LOWER_UNDERSCORE) {
046        return s.replace('-', '_');
047      }
048      if (format == UPPER_UNDERSCORE) {
049        return Ascii.toUpperCase(s.replace('-', '_'));
050      }
051      return super.convert(format, s);
052    }
053  },
054
055  /** C++ variable naming convention, e.g., "lower_underscore". */
056  LOWER_UNDERSCORE(CharMatcher.is('_'), "_") {
057    @Override
058    String normalizeWord(String word) {
059      return Ascii.toLowerCase(word);
060    }
061
062    @Override
063    String convert(CaseFormat format, String s) {
064      if (format == LOWER_HYPHEN) {
065        return s.replace('_', '-');
066      }
067      if (format == UPPER_UNDERSCORE) {
068        return Ascii.toUpperCase(s);
069      }
070      return super.convert(format, s);
071    }
072  },
073
074  /** Java variable naming convention, e.g., "lowerCamel". */
075  LOWER_CAMEL(CharMatcher.inRange('A', 'Z'), "") {
076    @Override
077    String normalizeWord(String word) {
078      return firstCharOnlyToUpper(word);
079    }
080
081    @Override
082    String normalizeFirstWord(String word) {
083      return Ascii.toLowerCase(word);
084    }
085  },
086
087  /** Java and C++ class naming convention, e.g., "UpperCamel". */
088  UPPER_CAMEL(CharMatcher.inRange('A', 'Z'), "") {
089    @Override
090    String normalizeWord(String word) {
091      return firstCharOnlyToUpper(word);
092    }
093  },
094
095  /** Java and C++ constant naming convention, e.g., "UPPER_UNDERSCORE". */
096  UPPER_UNDERSCORE(CharMatcher.is('_'), "_") {
097    @Override
098    String normalizeWord(String word) {
099      return Ascii.toUpperCase(word);
100    }
101
102    @Override
103    String convert(CaseFormat format, String s) {
104      if (format == LOWER_HYPHEN) {
105        return Ascii.toLowerCase(s.replace('_', '-'));
106      }
107      if (format == LOWER_UNDERSCORE) {
108        return Ascii.toLowerCase(s);
109      }
110      return super.convert(format, s);
111    }
112  };
113
114  private final CharMatcher wordBoundary;
115  private final String wordSeparator;
116
117  CaseFormat(CharMatcher wordBoundary, String wordSeparator) {
118    this.wordBoundary = wordBoundary;
119    this.wordSeparator = wordSeparator;
120  }
121
122  /**
123   * Converts the specified {@code String str} from this format to the specified {@code format}. A
124   * "best effort" approach is taken; if {@code str} does not conform to the assumed format, then
125   * the behavior of this method is undefined but we make a reasonable effort at converting anyway.
126   */
127  public final String to(CaseFormat format, String str) {
128    checkNotNull(format);
129    checkNotNull(str);
130    return (format == this) ? str : convert(format, str);
131  }
132
133  /** Enum values can override for performance reasons. */
134  String convert(CaseFormat format, String s) {
135    // deal with camel conversion
136    StringBuilder out = null;
137    int i = 0;
138    int j = -1;
139    while ((j = wordBoundary.indexIn(s, ++j)) != -1) {
140      if (i == 0) {
141        // include some extra space for separators
142        out = new StringBuilder(s.length() + 4 * format.wordSeparator.length());
143        out.append(format.normalizeFirstWord(s.substring(i, j)));
144      } else {
145        requireNonNull(out).append(format.normalizeWord(s.substring(i, j)));
146      }
147      out.append(format.wordSeparator);
148      i = j + wordSeparator.length();
149    }
150    return (i == 0)
151        ? format.normalizeFirstWord(s)
152        : requireNonNull(out).append(format.normalizeWord(s.substring(i))).toString();
153  }
154
155  /**
156   * Returns a serializable {@code Converter} that converts strings from this format to {@code
157   * targetFormat}.
158   *
159   * @since 16.0
160   */
161  public Converter<String, String> converterTo(CaseFormat targetFormat) {
162    return new StringConverter(this, targetFormat);
163  }
164
165  private static final class StringConverter extends Converter<String, String>
166      implements Serializable {
167
168    private final CaseFormat sourceFormat;
169    private final CaseFormat targetFormat;
170
171    StringConverter(CaseFormat sourceFormat, CaseFormat targetFormat) {
172      this.sourceFormat = checkNotNull(sourceFormat);
173      this.targetFormat = checkNotNull(targetFormat);
174    }
175
176    @Override
177    protected String doForward(String s) {
178      return sourceFormat.to(targetFormat, s);
179    }
180
181    @Override
182    protected String doBackward(String s) {
183      return targetFormat.to(sourceFormat, s);
184    }
185
186    @Override
187    public boolean equals(@Nullable Object object) {
188      if (object instanceof StringConverter) {
189        StringConverter that = (StringConverter) object;
190        return sourceFormat.equals(that.sourceFormat) && targetFormat.equals(that.targetFormat);
191      }
192      return false;
193    }
194
195    @Override
196    public int hashCode() {
197      return sourceFormat.hashCode() ^ targetFormat.hashCode();
198    }
199
200    @Override
201    public String toString() {
202      return sourceFormat + ".converterTo(" + targetFormat + ")";
203    }
204
205    private static final long serialVersionUID = 0L;
206  }
207
208  abstract String normalizeWord(String word);
209
210  String normalizeFirstWord(String word) {
211    return normalizeWord(word);
212  }
213
214  private static String firstCharOnlyToUpper(String word) {
215    return word.isEmpty()
216        ? word
217        : Ascii.toUpperCase(word.charAt(0)) + Ascii.toLowerCase(word.substring(1));
218  }
219}