[LANG-697] Add FormattableUtils class

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@1095833 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 6358b8e..4bec62a 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -60,6 +60,7 @@
     [LANG-676] Documented potential NPE if auto-boxing occurs for some BooleanUtils methods
     [LANG-678] Add support for ConcurrentMap.putIfAbsent()
     [LANG-692] Add hashCodeMulti varargs method
+    [LANG-697] Add FormattableUtils class
 
 REMOVALS IN 3.0
 ===============
diff --git a/src/main/java/org/apache/commons/lang3/util/FormattableUtils.java b/src/main/java/org/apache/commons/lang3/util/FormattableUtils.java
new file mode 100644
index 0000000..93d0ef1
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/util/FormattableUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.lang3.util;
+
+import static java.util.FormattableFlags.LEFT_JUSTIFY;
+
+import java.util.Formattable;
+import java.util.Formatter;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Provides utilities for working with {@link Formattable}s.
+ * 
+ * @since Lang 3.0
+ * @version $Id$
+ */
+public class FormattableUtils {
+
+    private static final String SIMPLEST_FORMAT = "%1$s";
+
+    /**
+     * <p>{@link FormattableUtils} instances should NOT be constructed in
+     * standard programming. Instead, the methods of the class should be invoked
+     * statically.</p>
+     * 
+     * <p>This constructor is public to permit tools that require a JavaBean
+     * instance to operate.</p>
+     */
+    public FormattableUtils() {
+        super();
+    }
+
+    /**
+     * Get the default formatted representation of the specified
+     * {@link Formattable}.
+     * 
+     * @param formattable
+     * @return String
+     */
+    public static String toString(Formattable formattable) {
+        return String.format(SIMPLEST_FORMAT, formattable);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append,
+     * with no ellipsis on precision overflow, and padding width underflow with
+     * spaces.
+     * 
+     * @param seq to handle
+     * @param formatter destination
+     * @param flags for formatting
+     * @param width of output
+     * @param precision of output
+     * @return {@code formatter}
+     */
+    public static Formatter append(CharSequence seq, Formatter formatter, int flags, int width,
+            int precision) {
+        return append(seq, formatter, flags, width, precision, ' ', null);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append,
+     * with no ellipsis on precision overflow.
+     * 
+     * @param seq to handle
+     * @param formatter destination
+     * @param flags for formatting
+     * @param width of output
+     * @param precision of output
+     * @param padChar to use
+     * @return {@code formatter}
+     */
+    public static Formatter append(CharSequence seq, Formatter formatter, int flags, int width,
+            int precision, char padChar) {
+        return append(seq, formatter, flags, width, precision, padChar, null);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append,
+     * padding width underflow with spaces.
+     * 
+     * @param seq to handle
+     * @param formatter destination
+     * @param flags for formatting
+     * @param width of output
+     * @param precision of output
+     * @param ellipsis to use when precision dictates truncation; a {@code null}
+     * or empty value causes a hard truncation
+     * @return {@code formatter}
+     */
+    public static Formatter append(CharSequence seq, Formatter formatter, int flags, int width,
+            int precision, CharSequence ellipsis) {
+        return append(seq, formatter, flags, width, precision, ' ', ellipsis);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append.
+     * 
+     * @param seq to handle
+     * @param formatter destination
+     * @param flags for formatting
+     * @param width of output
+     * @param precision of output
+     * @param padChar to use
+     * @param ellipsis to use when precision dictates truncation; a {@code null}
+     * or empty value causes a hard truncation
+     * @return {@code formatter}
+     */
+    public static Formatter append(CharSequence seq, Formatter formatter, int flags, int width,
+            int precision, char padChar, CharSequence ellipsis) {
+        Validate.isTrue(ellipsis == null || precision < 0 || ellipsis.length() <= precision,
+                "Specified ellipsis '%1$s' exceeds precision of %2$s", ellipsis, precision);
+        StringBuilder buf = new StringBuilder(seq);
+        if (precision >= 0 && precision < seq.length()) {
+            CharSequence _ellipsis = ObjectUtils.defaultIfNull(ellipsis, "");
+            buf.replace(precision - _ellipsis.length(), seq.length(), _ellipsis.toString());
+        }
+        boolean leftJustify = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY;
+        for (int i = buf.length(); i < width; i++) {
+            buf.insert(leftJustify ? i : 0, padChar);
+        }
+        formatter.format(buf.toString());
+        return formatter;
+    }
+}
diff --git a/src/test/java/org/apache/commons/lang3/util/FormattableUtilsTest.java b/src/test/java/org/apache/commons/lang3/util/FormattableUtilsTest.java
new file mode 100644
index 0000000..0c91a2b
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/util/FormattableUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.lang3.util;
+
+import static java.util.FormattableFlags.LEFT_JUSTIFY;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Formatter;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.Test;
+
+/**
+ * Unit tests {@link FormattableUtils}.
+ *
+ * @version $Id$
+ */
+public class FormattableUtilsTest {
+
+    @Test
+    public void testDefaultAppend() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1).toString());
+        assertEquals("fo", FormattableUtils.append("foo", new Formatter(), 0, -1, 2).toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1).toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1).toString());
+        assertEquals(" fo", FormattableUtils.append("foo", new Formatter(), 0, 3, 2).toString());
+        assertEquals("   fo", FormattableUtils.append("foo", new Formatter(), 0, 5, 2).toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1).toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1).toString());
+        assertEquals("fo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2).toString());
+        assertEquals("fo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2).toString());
+    }
+
+    @Test
+    public void testAlternatePadCharacter() {
+        char pad='_';
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, pad).toString());
+        assertEquals("fo", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, pad).toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, pad).toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, pad).toString());
+        assertEquals("_fo", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, pad).toString());
+        assertEquals("___fo", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, pad).toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, pad).toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, pad).toString());
+        assertEquals("fo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, pad).toString());
+        assertEquals("fo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, pad).toString());
+    }
+
+    @Test
+    public void testEllipsis() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, "*").toString());
+        assertEquals("f*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, "*").toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, "*").toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, "*").toString());
+        assertEquals(" f*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, "*").toString());
+        assertEquals("   f*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, "*").toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, "*").toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, "*").toString());
+        assertEquals("f* ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, "*").toString());
+        assertEquals("f*   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, "*").toString());
+
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, "+*").toString());
+        assertEquals("+*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, "+*").toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, "+*").toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, "+*").toString());
+        assertEquals(" +*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, "+*").toString());
+        assertEquals("   +*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, "+*").toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, "+*").toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, "+*").toString());
+        assertEquals("+* ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, "+*").toString());
+        assertEquals("+*   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, "+*").toString());
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testIllegalEllipsis() {
+        FormattableUtils.append("foo", new Formatter(), 0, -1, 1, "xx");
+    }
+
+    @Test
+    public void testAlternatePadCharAndEllipsis() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, '_', "*").toString());
+        assertEquals("f*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, '_', "*").toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, '_', "*").toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, '_', "*").toString());
+        assertEquals("_f*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, '_', "*").toString());
+        assertEquals("___f*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, '_', "*").toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, '_', "*").toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, '_', "*").toString());
+        assertEquals("f*_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, '_', "*").toString());
+        assertEquals("f*___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, '_', "*").toString());
+
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, '_', "+*").toString());
+        assertEquals("+*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, '_', "+*").toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, '_', "+*").toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, '_', "+*").toString());
+        assertEquals("_+*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, '_', "+*").toString());
+        assertEquals("___+*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, '_', "+*").toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, '_', "+*").toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, '_', "+*").toString());
+        assertEquals("+*_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, '_', "+*").toString());
+        assertEquals("+*___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, '_', "+*").toString());
+    }
+
+    @Test
+    public void testToStringFormattable() {
+        assertEquals("(Key,Value)", FormattableUtils.toString(Pair.of("Key", "Value")));
+    }
+}