You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2025/11/28 12:37:06 UTC
(juneau) 01/03: org.apache.juneau.common.reflect API improvements
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
commit 7edf985bce9b3d2fc936b19c90434e46052c1a79
Author: James Bognar <ja...@salesforce.com>
AuthorDate: Fri Nov 28 07:32:27 2025 -0500
org.apache.juneau.common.reflect API improvements
---
TODO-StringUtils.md | 2 +-
.../apache/juneau/common/collections/Cache.java | 145 +++-
.../apache/juneau/common/collections/Cache2.java | 90 +-
.../apache/juneau/common/collections/Cache3.java | 90 +-
.../apache/juneau/common/collections/Cache4.java | 90 +-
.../apache/juneau/common/collections/Cache5.java | 90 +-
.../apache/juneau/common/reflect/PackageInfo.java | 3 +-
.../apache/juneau/common/utils/FormatString.java | 724 ++++++++++++++++
.../apache/juneau/common/utils/StringFormat.java | 724 ++++++++++++++++
.../apache/juneau/common/utils/StringUtils.java | 371 +++++----
.../java/org/apache/juneau/common/utils/Utils.java | 53 ++
.../juneau/common/utils/FormatString_Test.java | 416 ++++++++++
.../juneau/common/utils/StringFormat_Test.java | 416 ++++++++++
.../juneau/common/utils/StringUtils_Test.java | 909 +++++++++++++--------
.../org/apache/juneau/common/utils/Utils_Test.java | 51 ++
15 files changed, 3583 insertions(+), 591 deletions(-)
diff --git a/TODO-StringUtils.md b/TODO-StringUtils.md
index 82f8861fcf..40ba02dc58 100644
--- a/TODO-StringUtils.md
+++ b/TODO-StringUtils.md
@@ -220,7 +220,7 @@ The `StringUtils` class currently has 225+ public static methods covering:
## Existing Methods to Consider Enhancing
-- `format(String pattern, Object...args)` - Could be enhanced with more formatting options
+- ✅ `format(String pattern, Object...args)` - Could be enhanced with more formatting options
- `parseNumber(String s, Class<? extends Number> type)` - Could add more number types
- `getDuration(String s)` - Could support more duration formats
- `replaceVars(String s, Map<String,Object> m)` - Could add more variable syntax options
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache.java
index a417db4489..184f11df93 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache.java
@@ -153,7 +153,10 @@ import org.apache.juneau.common.function.*;
public class Cache<K,V> {
// Internal map with Tuple1 keys for content-based equality (especially for arrays)
+ // If threadLocal is true, this is null and threadLocalMap is used instead
private final Map<Tuple1<K>,V> map;
+ private final ThreadLocal<Map<Tuple1<K>,V>> threadLocalMap;
+ private final boolean isThreadLocal;
/**
* Cache of Tuple1 wrapper objects to minimize object creation on repeated get/put calls.
@@ -161,8 +164,24 @@ public class Cache<K,V> {
* <p>
* Uses WeakHashMap so wrappers can be GC'd when keys are no longer referenced.
* This provides a significant performance improvement for caches with repeated key access.
+ * If threadLocal is true, this is null and threadLocalWrapperCache is used instead.
*/
private final Map<K,Tuple1<K>> wrapperCache;
+ private final ThreadLocal<Map<K,Tuple1<K>>> threadLocalWrapperCache;
+
+ /**
+ * Gets the map for the current thread.
+ *
+ * @return The map for the current thread.
+ */
+ private Map<Tuple1<K>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
+
+ /**
+ * Gets the wrapper cache for the current thread.
+ *
+ * @return The wrapper cache for the current thread.
+ */
+ private Map<K,Tuple1<K>> getWrapperCache() { return isThreadLocal ? threadLocalWrapperCache.get() : wrapperCache; }
/**
* Gets or creates a Tuple1 wrapper for the given key.
@@ -175,7 +194,7 @@ public class Cache<K,V> {
* @return A cached or new Tuple1 wrapper for the key.
*/
private Tuple1<K> wrap(K key) {
- return wrapperCache.computeIfAbsent(key, k -> Tuple1.of(k));
+ return getWrapperCache().computeIfAbsent(key, k -> Tuple1.of(k));
}
/**
@@ -211,6 +230,7 @@ public class Cache<K,V> {
int maxSize;
String id;
boolean logOnExit;
+ boolean threadLocal;
Function<K,V> supplier;
Builder() {
@@ -266,6 +286,68 @@ public class Cache<K,V> {
return this;
}
+ /**
+ * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
+ *
+ * <p>
+ * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
+ *
+ * <p>
+ * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
+ * garbage collected when keys are no longer strongly referenced elsewhere.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Weak caching for Class metadata</jc>
+ * Cache<Class<?>,ClassMeta> <jv>cache</jv> = Cache.<jsm>of</jsm>(Class.<jk>class</jk>, ClassMeta.<jk>class</jk>)
+ * .<jsm>weak</jsm>()
+ * .build();
+ * </p>
+ *
+ * @return This object for method chaining.
+ * @see #cacheMode(CacheMode)
+ */
+ public Builder<K,V> weak() {
+ return cacheMode(WEAK);
+ }
+
+ /**
+ * Enables thread-local caching.
+ *
+ * <p>
+ * When enabled, each thread gets its own separate cache instance. This is useful for
+ * thread-unsafe objects like {@link java.text.MessageFormat} that need to be cached per thread.
+ *
+ * <p>
+ * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Thread-local cache for MessageFormat instances</jc>
+ * Cache<String,MessageFormat> <jv>cache</jv> = Cache
+ * .<jsm>of</jsm>(String.<jk>class</jk>, MessageFormat.<jk>class</jk>)
+ * .maxSize(100)
+ * .<jsm>threadLocal</jsm>()
+ * .build();
+ * </p>
+ *
+ * <p>
+ * This is equivalent to:
+ * <p class='bjava'>
+ * ThreadLocal<Cache<String,MessageFormat>> <jv>cache</jv> =
+ * ThreadLocal.<jsm>withInitial</jsm>(() -> Cache
+ * .<jsm>of</jsm>(String.<jk>class</jk>, MessageFormat.<jk>class</jk>)
+ * .maxSize(100)
+ * .build());
+ * </p>
+ *
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> threadLocal() {
+ threadLocal = true;
+ return this;
+ }
+
/**
* Enables logging of cache statistics when the JVM exits.
*
@@ -424,12 +506,30 @@ public class Cache<K,V> {
this.maxSize = builder.maxSize;
this.disableCaching = builder.cacheMode == NONE;
this.supplier = builder.supplier != null ? builder.supplier : (K) -> null;
- if (builder.cacheMode == WEAK) {
- this.map = synchronizedMap(new WeakHashMap<>());
- this.wrapperCache = synchronizedMap(new WeakHashMap<>());
+ this.isThreadLocal = builder.threadLocal;
+
+ if (isThreadLocal) {
+ // Thread-local mode: each thread gets its own map
+ if (builder.cacheMode == WEAK) {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> synchronizedMap(new WeakHashMap<>()));
+ this.threadLocalWrapperCache = ThreadLocal.withInitial(() -> synchronizedMap(new WeakHashMap<>()));
+ } else {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
+ this.threadLocalWrapperCache = ThreadLocal.withInitial(() -> synchronizedMap(new WeakHashMap<>()));
+ }
+ this.map = null;
+ this.wrapperCache = null;
} else {
- this.map = new ConcurrentHashMap<>();
- this.wrapperCache = synchronizedMap(new WeakHashMap<>());
+ // Normal mode: shared map across all threads
+ if (builder.cacheMode == WEAK) {
+ this.map = synchronizedMap(new WeakHashMap<>());
+ this.wrapperCache = synchronizedMap(new WeakHashMap<>());
+ } else {
+ this.map = new ConcurrentHashMap<>();
+ this.wrapperCache = synchronizedMap(new WeakHashMap<>());
+ }
+ this.threadLocalMap = null;
+ this.threadLocalWrapperCache = null;
}
if (builder.logOnExit) {
shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size());
@@ -505,16 +605,17 @@ public class Cache<K,V> {
assertArgNotNull("supplier", supplier);
if (disableCaching)
return supplier.get();
+ var m = getMap();
Tuple1<K> wrapped = wrap(key);
- V v = map.get(wrapped);
+ V v = m.get(wrapped);
if (v == null) {
if (size() > maxSize)
clear();
v = supplier.get();
if (v == null)
- map.remove(wrapped);
+ m.remove(wrapped);
else
- map.putIfAbsent(wrapped, v);
+ m.putIfAbsent(wrapped, v);
} else {
cacheHits.incrementAndGet();
}
@@ -529,13 +630,14 @@ public class Cache<K,V> {
* @return The previous value associated with the key, or <jk>null</jk> if there was no mapping.
*/
public V put(K key, V value) {
+ var m = getMap();
if (value == null) {
Tuple1<K> wrapped = wrap(key);
- V result = map.remove(wrapped);
- wrapperCache.remove(key); // Clean up wrapper cache
+ V result = m.remove(wrapped);
+ getWrapperCache().remove(key); // Clean up wrapper cache
return result;
}
- return map.put(wrap(key), value);
+ return m.put(wrap(key), value);
}
/**
@@ -572,7 +674,7 @@ public class Cache<K,V> {
* @return The number of cached entries.
*/
public int size() {
- return map.size();
+ return getMap().size();
}
/**
@@ -580,7 +682,7 @@ public class Cache<K,V> {
*
* @return <jk>true</jk> if the cache is empty.
*/
- public boolean isEmpty() { return map.isEmpty(); }
+ public boolean isEmpty() { return getMap().isEmpty(); }
/**
* Returns <jk>true</jk> if the cache contains a mapping for the specified key.
@@ -589,7 +691,7 @@ public class Cache<K,V> {
* @return <jk>true</jk> if the cache contains the key.
*/
public boolean containsKey(K key) {
- return map.containsKey(wrap(key));
+ return getMap().containsKey(wrap(key));
}
/**
@@ -602,7 +704,7 @@ public class Cache<K,V> {
// ConcurrentHashMap doesn't allow null values, so null can never be in the cache
if (value == null)
return false;
- return map.containsValue(value);
+ return getMap().containsValue(value);
}
/**
@@ -612,9 +714,10 @@ public class Cache<K,V> {
* @return The previous value associated with the key, or <jk>null</jk> if there was no mapping.
*/
public V remove(K key) {
- Tuple1<K> wrapped = wrap(key);
- V result = map.remove(wrapped);
- wrapperCache.remove(key); // Clean up wrapper cache
+ var m = getMap();
+ var wrapped = wrap(key);
+ V result = m.remove(wrapped);
+ getWrapperCache().remove(key); // Clean up wrapper cache
return result;
}
@@ -622,7 +725,7 @@ public class Cache<K,V> {
* Removes all entries from the cache.
*/
public void clear() {
- map.clear();
- wrapperCache.clear(); // Clean up wrapper cache
+ getMap().clear();
+ getWrapperCache().clear(); // Clean up wrapper cache
}
}
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache2.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache2.java
index da1f85619f..57ec837a16 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache2.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache2.java
@@ -137,7 +137,10 @@ import org.apache.juneau.common.function.*;
public class Cache2<K1,K2,V> {
// Internal map with Tuple2 keys for content-based equality (especially for arrays)
+ // If threadLocal is true, this is null and threadLocalMap is used instead
private final java.util.Map<Tuple2<K1,K2>,V> map;
+ private final ThreadLocal<Map<Tuple2<K1,K2>,V>> threadLocalMap;
+ private final boolean isThreadLocal;
/**
* Builder for creating configured {@link Cache2} instances.
@@ -165,6 +168,7 @@ public class Cache2<K1,K2,V> {
int maxSize;
String id;
boolean logOnExit;
+ boolean threadLocal;
Function2<K1,K2,V> supplier;
Builder() {
@@ -202,6 +206,41 @@ public class Cache2<K1,K2,V> {
return this;
}
+ /**
+ * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
+ *
+ * <p>
+ * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
+ *
+ * <p>
+ * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
+ * garbage collected when keys are no longer strongly referenced elsewhere.
+ *
+ * @return This object for method chaining.
+ * @see #cacheMode(CacheMode)
+ */
+ public Builder<K1,K2,V> weak() {
+ return cacheMode(WEAK);
+ }
+
+ /**
+ * Enables thread-local caching.
+ *
+ * <p>
+ * When enabled, each thread gets its own separate cache instance. This is useful for
+ * thread-unsafe objects that need to be cached per thread.
+ *
+ * <p>
+ * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
+ *
+ * @return This object for method chaining.
+ * @see Cache.Builder#threadLocal()
+ */
+ public Builder<K1,K2,V> threadLocal() {
+ threadLocal = true;
+ return this;
+ }
+
/**
* Enables logging of cache statistics when the JVM exits.
*
@@ -365,16 +404,37 @@ public class Cache2<K1,K2,V> {
this.maxSize = builder.maxSize;
this.cacheMode = builder.cacheMode;
this.supplier = builder.supplier;
- if (builder.cacheMode == WEAK) {
- this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ this.isThreadLocal = builder.threadLocal;
+
+ if (isThreadLocal) {
+ // Thread-local mode: each thread gets its own map
+ if (builder.cacheMode == WEAK) {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
+ } else {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
+ }
+ this.map = null;
} else {
- this.map = new ConcurrentHashMap<>();
+ // Normal mode: shared map across all threads
+ if (builder.cacheMode == WEAK) {
+ this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ } else {
+ this.map = new ConcurrentHashMap<>();
+ }
+ this.threadLocalMap = null;
}
if (builder.logOnExit) {
shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size());
}
}
+ /**
+ * Gets the map for the current thread.
+ *
+ * @return The map for the current thread.
+ */
+ private Map<Tuple2<K1,K2>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
+
/**
* Retrieves a cached value by key pair using the default supplier.
*
@@ -446,16 +506,17 @@ public class Cache2<K1,K2,V> {
assertArgNotNull("supplier", supplier);
if (cacheMode == NONE)
return supplier.get();
+ var m = getMap();
var wrapped = Tuple2.of(key1, key2);
- V v = map.get(wrapped);
+ V v = m.get(wrapped);
if (v == null) {
if (size() > maxSize)
clear();
v = supplier.get();
if (v == null)
- map.remove(wrapped);
+ m.remove(wrapped);
else
- map.putIfAbsent(wrapped, v);
+ m.putIfAbsent(wrapped, v);
} else {
cacheHits.incrementAndGet();
}
@@ -471,9 +532,10 @@ public class Cache2<K1,K2,V> {
* @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping.
*/
public V put(K1 key1, K2 key2, V value) {
+ var m = getMap();
if (value == null)
- return map.remove(Tuple2.of(key1, key2));
- return map.put(Tuple2.of(key1, key2), value);
+ return m.remove(Tuple2.of(key1, key2));
+ return m.put(Tuple2.of(key1, key2), value);
}
/**
@@ -484,7 +546,7 @@ public class Cache2<K1,K2,V> {
* @return The previous value associated with the key pair, or <jk>null</jk> if there was no mapping.
*/
public V remove(K1 key1, K2 key2) {
- return map.remove(Tuple2.of(key1, key2));
+ return getMap().remove(Tuple2.of(key1, key2));
}
/**
@@ -495,7 +557,7 @@ public class Cache2<K1,K2,V> {
* @return <jk>true</jk> if the cache contains the key pair.
*/
public boolean containsKey(K1 key1, K2 key2) {
- return map.containsKey(Tuple2.of(key1, key2));
+ return getMap().containsKey(Tuple2.of(key1, key2));
}
/**
@@ -508,7 +570,7 @@ public class Cache2<K1,K2,V> {
// ConcurrentHashMap doesn't allow null values, so null can never be in the cache
if (value == null)
return false;
- return map.containsValue(value);
+ return getMap().containsValue(value);
}
/**
@@ -517,7 +579,7 @@ public class Cache2<K1,K2,V> {
* @return The number of cached entries.
*/
public int size() {
- return map.size();
+ return getMap().size();
}
/**
@@ -525,13 +587,13 @@ public class Cache2<K1,K2,V> {
*
* @return <jk>true</jk> if the cache is empty.
*/
- public boolean isEmpty() { return map.isEmpty(); }
+ public boolean isEmpty() { return getMap().isEmpty(); }
/**
* Removes all entries from the cache.
*/
public void clear() {
- map.clear();
+ getMap().clear();
}
/**
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache3.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache3.java
index 468775c69a..70790724d9 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache3.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache3.java
@@ -81,7 +81,10 @@ import org.apache.juneau.common.function.*;
public class Cache3<K1,K2,K3,V> {
// Internal map with Tuple3 keys for content-based equality (especially for arrays)
+ // If threadLocal is true, this is null and threadLocalMap is used instead
private final java.util.Map<Tuple3<K1,K2,K3>,V> map;
+ private final ThreadLocal<Map<Tuple3<K1,K2,K3>,V>> threadLocalMap;
+ private final boolean isThreadLocal;
/**
* Builder for creating configured {@link Cache3} instances.
@@ -96,6 +99,7 @@ public class Cache3<K1,K2,K3,V> {
int maxSize;
String id;
boolean logOnExit;
+ boolean threadLocal;
Function3<K1,K2,K3,V> supplier;
Builder() {
@@ -133,6 +137,41 @@ public class Cache3<K1,K2,K3,V> {
return this;
}
+ /**
+ * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
+ *
+ * <p>
+ * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
+ *
+ * <p>
+ * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
+ * garbage collected when keys are no longer strongly referenced elsewhere.
+ *
+ * @return This object for method chaining.
+ * @see #cacheMode(CacheMode)
+ */
+ public Builder<K1,K2,K3,V> weak() {
+ return cacheMode(WEAK);
+ }
+
+ /**
+ * Enables thread-local caching.
+ *
+ * <p>
+ * When enabled, each thread gets its own separate cache instance. This is useful for
+ * thread-unsafe objects that need to be cached per thread.
+ *
+ * <p>
+ * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
+ *
+ * @return This object for method chaining.
+ * @see Cache.Builder#threadLocal()
+ */
+ public Builder<K1,K2,K3,V> threadLocal() {
+ threadLocal = true;
+ return this;
+ }
+
/**
* Enables logging of cache statistics when the JVM exits.
*
@@ -233,16 +272,37 @@ public class Cache3<K1,K2,K3,V> {
this.maxSize = builder.maxSize;
this.cacheMode = builder.cacheMode;
this.supplier = builder.supplier;
- if (builder.cacheMode == WEAK) {
- this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ this.isThreadLocal = builder.threadLocal;
+
+ if (isThreadLocal) {
+ // Thread-local mode: each thread gets its own map
+ if (builder.cacheMode == WEAK) {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
+ } else {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
+ }
+ this.map = null;
} else {
- this.map = new ConcurrentHashMap<>();
+ // Normal mode: shared map across all threads
+ if (builder.cacheMode == WEAK) {
+ this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ } else {
+ this.map = new ConcurrentHashMap<>();
+ }
+ this.threadLocalMap = null;
}
if (builder.logOnExit) {
shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size());
}
}
+ /**
+ * Gets the map for the current thread.
+ *
+ * @return The map for the current thread.
+ */
+ private Map<Tuple3<K1,K2,K3>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
+
/**
* Retrieves a cached value by key triplet using the default supplier.
*
@@ -269,16 +329,17 @@ public class Cache3<K1,K2,K3,V> {
assertArgNotNull("supplier", supplier);
if (cacheMode == NONE)
return supplier.get();
+ Map<Tuple3<K1,K2,K3>,V> m = getMap();
var wrapped = Tuple3.of(key1, key2, key3);
- V v = map.get(wrapped);
+ V v = m.get(wrapped);
if (v == null) {
if (size() > maxSize)
clear();
v = supplier.get();
if (v == null)
- map.remove(wrapped);
+ m.remove(wrapped);
else
- map.putIfAbsent(wrapped, v);
+ m.putIfAbsent(wrapped, v);
} else {
cacheHits.incrementAndGet();
}
@@ -295,9 +356,10 @@ public class Cache3<K1,K2,K3,V> {
* @return The previous value associated with the key triplet, or <jk>null</jk> if there was no mapping.
*/
public V put(K1 key1, K2 key2, K3 key3, V value) {
+ java.util.Map<Tuple3<K1,K2,K3>,V> m = getMap();
if (value == null)
- return map.remove(Tuple3.of(key1, key2, key3));
- return map.put(Tuple3.of(key1, key2, key3), value);
+ return m.remove(Tuple3.of(key1, key2, key3));
+ return m.put(Tuple3.of(key1, key2, key3), value);
}
/**
@@ -309,7 +371,7 @@ public class Cache3<K1,K2,K3,V> {
* @return The previous value associated with the key triplet, or <jk>null</jk> if there was no mapping.
*/
public V remove(K1 key1, K2 key2, K3 key3) {
- return map.remove(Tuple3.of(key1, key2, key3));
+ return getMap().remove(Tuple3.of(key1, key2, key3));
}
/**
@@ -321,7 +383,7 @@ public class Cache3<K1,K2,K3,V> {
* @return <jk>true</jk> if the cache contains the key triplet.
*/
public boolean containsKey(K1 key1, K2 key2, K3 key3) {
- return map.containsKey(Tuple3.of(key1, key2, key3));
+ return getMap().containsKey(Tuple3.of(key1, key2, key3));
}
/**
@@ -334,7 +396,7 @@ public class Cache3<K1,K2,K3,V> {
// ConcurrentHashMap doesn't allow null values, so null can never be in the cache
if (value == null)
return false;
- return map.containsValue(value);
+ return getMap().containsValue(value);
}
/**
@@ -343,7 +405,7 @@ public class Cache3<K1,K2,K3,V> {
* @return The number of cached entries.
*/
public int size() {
- return map.size();
+ return getMap().size();
}
/**
@@ -351,13 +413,13 @@ public class Cache3<K1,K2,K3,V> {
*
* @return <jk>true</jk> if the cache is empty.
*/
- public boolean isEmpty() { return map.isEmpty(); }
+ public boolean isEmpty() { return getMap().isEmpty(); }
/**
* Removes all entries from the cache.
*/
public void clear() {
- map.clear();
+ getMap().clear();
}
/**
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache4.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache4.java
index 7161d0896b..ee00dcb86f 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache4.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache4.java
@@ -67,7 +67,10 @@ import org.apache.juneau.common.function.*;
public class Cache4<K1,K2,K3,K4,V> {
// Internal map with Tuple4 keys for content-based equality (especially for arrays)
+ // If threadLocal is true, this is null and threadLocalMap is used instead
private final java.util.Map<Tuple4<K1,K2,K3,K4>,V> map;
+ private final ThreadLocal<java.util.Map<Tuple4<K1,K2,K3,K4>,V>> threadLocalMap;
+ private final boolean isThreadLocal;
/**
* Builder for creating configured {@link Cache4} instances.
@@ -83,6 +86,7 @@ public class Cache4<K1,K2,K3,K4,V> {
int maxSize;
String id;
boolean logOnExit;
+ boolean threadLocal;
Function4<K1,K2,K3,K4,V> supplier;
Builder() {
@@ -120,6 +124,41 @@ public class Cache4<K1,K2,K3,K4,V> {
return this;
}
+ /**
+ * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
+ *
+ * <p>
+ * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
+ *
+ * <p>
+ * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
+ * garbage collected when keys are no longer strongly referenced elsewhere.
+ *
+ * @return This object for method chaining.
+ * @see #cacheMode(CacheMode)
+ */
+ public Builder<K1,K2,K3,K4,V> weak() {
+ return cacheMode(WEAK);
+ }
+
+ /**
+ * Enables thread-local caching.
+ *
+ * <p>
+ * When enabled, each thread gets its own separate cache instance. This is useful for
+ * thread-unsafe objects that need to be cached per thread.
+ *
+ * <p>
+ * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
+ *
+ * @return This object for method chaining.
+ * @see Cache.Builder#threadLocal()
+ */
+ public Builder<K1,K2,K3,K4,V> threadLocal() {
+ threadLocal = true;
+ return this;
+ }
+
/**
* Enables logging of cache statistics when the JVM exits.
*
@@ -223,16 +262,37 @@ public class Cache4<K1,K2,K3,K4,V> {
this.maxSize = builder.maxSize;
this.cacheMode = builder.cacheMode;
this.supplier = builder.supplier;
- if (builder.cacheMode == WEAK) {
- this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ this.isThreadLocal = builder.threadLocal;
+
+ if (isThreadLocal) {
+ // Thread-local mode: each thread gets its own map
+ if (builder.cacheMode == WEAK) {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
+ } else {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
+ }
+ this.map = null;
} else {
- this.map = new ConcurrentHashMap<>();
+ // Normal mode: shared map across all threads
+ if (builder.cacheMode == WEAK) {
+ this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ } else {
+ this.map = new ConcurrentHashMap<>();
+ }
+ this.threadLocalMap = null;
}
if (builder.logOnExit) {
shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size());
}
}
+ /**
+ * Gets the map for the current thread.
+ *
+ * @return The map for the current thread.
+ */
+ private Map<Tuple4<K1,K2,K3,K4>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
+
/**
* Retrieves a cached value by four-part key using the default supplier.
*
@@ -263,16 +323,17 @@ public class Cache4<K1,K2,K3,K4,V> {
assertArgNotNull("supplier", supplier);
if (cacheMode == NONE)
return supplier.get();
+ var m = getMap();
var wrapped = Tuple4.of(key1, key2, key3, key4);
- V v = map.get(wrapped);
+ V v = m.get(wrapped);
if (v == null) {
if (size() > maxSize)
clear();
v = supplier.get();
if (v == null)
- map.remove(wrapped);
+ m.remove(wrapped);
else
- map.putIfAbsent(wrapped, v);
+ m.putIfAbsent(wrapped, v);
} else {
cacheHits.incrementAndGet();
}
@@ -291,9 +352,10 @@ public class Cache4<K1,K2,K3,K4,V> {
*
*/
public V put(K1 key1, K2 key2, K3 key3, K4 key4, V value) {
+ var m = getMap();
if (value == null)
- return map.remove(Tuple4.of(key1, key2, key3, key4));
- return map.put(Tuple4.of(key1, key2, key3, key4), value);
+ return m.remove(Tuple4.of(key1, key2, key3, key4));
+ return m.put(Tuple4.of(key1, key2, key3, key4), value);
}
/**
@@ -307,7 +369,7 @@ public class Cache4<K1,K2,K3,K4,V> {
*
*/
public V remove(K1 key1, K2 key2, K3 key3, K4 key4) {
- return map.remove(Tuple4.of(key1, key2, key3, key4));
+ return getMap().remove(Tuple4.of(key1, key2, key3, key4));
}
/**
@@ -320,7 +382,7 @@ public class Cache4<K1,K2,K3,K4,V> {
* @return <jk>true</jk> if the cache contains the four-part key.
*/
public boolean containsKey(K1 key1, K2 key2, K3 key3, K4 key4) {
- return map.containsKey(Tuple4.of(key1, key2, key3, key4));
+ return getMap().containsKey(Tuple4.of(key1, key2, key3, key4));
}
/**
@@ -333,7 +395,7 @@ public class Cache4<K1,K2,K3,K4,V> {
// ConcurrentHashMap doesn't allow null values, so null can never be in the cache
if (value == null)
return false;
- return map.containsValue(value);
+ return getMap().containsValue(value);
}
/**
@@ -342,7 +404,7 @@ public class Cache4<K1,K2,K3,K4,V> {
* @return The number of cached entries.
*/
public int size() {
- return map.size();
+ return getMap().size();
}
/**
@@ -350,13 +412,13 @@ public class Cache4<K1,K2,K3,K4,V> {
*
* @return <jk>true</jk> if the cache is empty.
*/
- public boolean isEmpty() { return map.isEmpty(); }
+ public boolean isEmpty() { return getMap().isEmpty(); }
/**
* Removes all entries from the cache.
*/
public void clear() {
- map.clear();
+ getMap().clear();
}
/**
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache5.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache5.java
index 5c04504991..befb2b10e6 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache5.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/collections/Cache5.java
@@ -69,7 +69,10 @@ import org.apache.juneau.common.function.*;
public class Cache5<K1,K2,K3,K4,K5,V> {
// Internal map with Tuple5 keys for content-based equality (especially for arrays)
+ // If threadLocal is true, this is null and threadLocalMap is used instead
private final java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V> map;
+ private final ThreadLocal<java.util.Map<Tuple5<K1,K2,K3,K4,K5>,V>> threadLocalMap;
+ private final boolean isThreadLocal;
/**
* Builder for creating configured {@link Cache5} instances.
@@ -86,6 +89,7 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
int maxSize;
String id;
boolean logOnExit;
+ boolean threadLocal;
Function5<K1,K2,K3,K4,K5,V> supplier;
Builder() {
@@ -123,6 +127,41 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
return this;
}
+ /**
+ * Sets the caching mode to {@link CacheMode#WEAK WEAK}.
+ *
+ * <p>
+ * This is a shortcut for calling <c>cacheMode(CacheMode.WEAK)</c>.
+ *
+ * <p>
+ * Weak caching uses {@link WeakHashMap} for storage, allowing cache entries to be
+ * garbage collected when keys are no longer strongly referenced elsewhere.
+ *
+ * @return This object for method chaining.
+ * @see #cacheMode(CacheMode)
+ */
+ public Builder<K1,K2,K3,K4,K5,V> weak() {
+ return cacheMode(WEAK);
+ }
+
+ /**
+ * Enables thread-local caching.
+ *
+ * <p>
+ * When enabled, each thread gets its own separate cache instance. This is useful for
+ * thread-unsafe objects that need to be cached per thread.
+ *
+ * <p>
+ * This is a shortcut for wrapping a cache in a {@link ThreadLocal}, but provides a cleaner API.
+ *
+ * @return This object for method chaining.
+ * @see Cache.Builder#threadLocal()
+ */
+ public Builder<K1,K2,K3,K4,K5,V> threadLocal() {
+ threadLocal = true;
+ return this;
+ }
+
/**
* Enables logging of cache statistics when the JVM exits.
*
@@ -229,16 +268,37 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
this.maxSize = builder.maxSize;
this.cacheMode = builder.cacheMode;
this.supplier = builder.supplier;
- if (builder.cacheMode == WEAK) {
- this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ this.isThreadLocal = builder.threadLocal;
+
+ if (isThreadLocal) {
+ // Thread-local mode: each thread gets its own map
+ if (builder.cacheMode == WEAK) {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> Collections.synchronizedMap(new WeakHashMap<>()));
+ } else {
+ this.threadLocalMap = ThreadLocal.withInitial(() -> new ConcurrentHashMap<>());
+ }
+ this.map = null;
} else {
- this.map = new ConcurrentHashMap<>();
+ // Normal mode: shared map across all threads
+ if (builder.cacheMode == WEAK) {
+ this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ } else {
+ this.map = new ConcurrentHashMap<>();
+ }
+ this.threadLocalMap = null;
}
if (builder.logOnExit) {
shutdownMessage(() -> builder.id + ": hits=" + cacheHits.get() + ", misses: " + size());
}
}
+ /**
+ * Gets the map for the current thread.
+ *
+ * @return The map for the current thread.
+ */
+ private Map<Tuple5<K1,K2,K3,K4,K5>,V> getMap() { return isThreadLocal ? threadLocalMap.get() : map; }
+
/**
* Retrieves a cached value by five-part key using the default supplier.
*
@@ -271,16 +331,17 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
assertArgNotNull("supplier", supplier);
if (cacheMode == NONE)
return supplier.get();
+ var m = getMap();
var wrapped = Tuple5.of(key1, key2, key3, key4, key5);
- V v = map.get(wrapped);
+ V v = m.get(wrapped);
if (v == null) {
if (size() > maxSize)
clear();
v = supplier.get();
if (v == null)
- map.remove(wrapped);
+ m.remove(wrapped);
else
- map.putIfAbsent(wrapped, v);
+ m.putIfAbsent(wrapped, v);
} else {
cacheHits.incrementAndGet();
}
@@ -300,9 +361,10 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
*
*/
public V put(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5, V value) {
+ var m = getMap();
if (value == null)
- return map.remove(Tuple5.of(key1, key2, key3, key4, key5));
- return map.put(Tuple5.of(key1, key2, key3, key4, key5), value);
+ return m.remove(Tuple5.of(key1, key2, key3, key4, key5));
+ return m.put(Tuple5.of(key1, key2, key3, key4, key5), value);
}
/**
@@ -317,7 +379,7 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
*
*/
public V remove(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) {
- return map.remove(Tuple5.of(key1, key2, key3, key4, key5));
+ return getMap().remove(Tuple5.of(key1, key2, key3, key4, key5));
}
/**
@@ -331,7 +393,7 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
* @return <jk>true</jk> if the cache contains the five-part key.
*/
public boolean containsKey(K1 key1, K2 key2, K3 key3, K4 key4, K5 key5) {
- return map.containsKey(Tuple5.of(key1, key2, key3, key4, key5));
+ return getMap().containsKey(Tuple5.of(key1, key2, key3, key4, key5));
}
/**
@@ -344,7 +406,7 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
// ConcurrentHashMap doesn't allow null values, so null can never be in the cache
if (value == null)
return false;
- return map.containsValue(value);
+ return getMap().containsValue(value);
}
/**
@@ -353,7 +415,7 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
* @return The number of cached entries.
*/
public int size() {
- return map.size();
+ return getMap().size();
}
/**
@@ -361,13 +423,13 @@ public class Cache5<K1,K2,K3,K4,K5,V> {
*
* @return <jk>true</jk> if the cache is empty.
*/
- public boolean isEmpty() { return map.isEmpty(); }
+ public boolean isEmpty() { return getMap().isEmpty(); }
/**
* Removes all entries from the cache.
*/
public void clear() {
- map.clear();
+ getMap().clear();
}
/**
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/PackageInfo.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/PackageInfo.java
index 074bf97b07..2850a63067 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/PackageInfo.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/PackageInfo.java
@@ -122,7 +122,8 @@ public class PackageInfo implements Annotatable {
protected PackageInfo(Package inner) {
assertArgNotNull("inner", inner);
this.inner = inner;
- this.annotations = memoize(() -> opt(inner).map(pkg -> stream(pkg.getAnnotations()).flatMap(a -> AnnotationUtils.streamRepeated(a)).map(a -> AnnotationInfo.of(this, a)).toList()).orElse(liste()));
+ this.annotations = memoize(
+ () -> opt(inner).map(pkg -> stream(pkg.getAnnotations()).flatMap(a -> AnnotationUtils.streamRepeated(a)).map(a -> AnnotationInfo.of(this, a)).toList()).orElse(liste()));
}
/**
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/FormatString.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/FormatString.java
new file mode 100644
index 0000000000..d3a2e9dc69
--- /dev/null
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/FormatString.java
@@ -0,0 +1,724 @@
+/*
+ * 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.juneau.common.utils;
+
+import static org.apache.juneau.common.utils.AssertionUtils.*;
+import static org.apache.juneau.common.utils.CollectionUtils.*;
+import static org.apache.juneau.common.utils.StateEnum.*;
+import static org.apache.juneau.common.utils.Utils.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.stream.*;
+import java.util.MissingFormatArgumentException;
+
+import org.apache.juneau.common.collections.*;
+
+/**
+ * Unified string formatter supporting both MessageFormat-style and printf-style formatting in the same pattern.
+ *
+ * <p>
+ * This class provides a thread-safe, cacheable formatter that can handle mixed format styles in a single pattern.
+ * It supports both MessageFormat syntax (<js>"{0}"</js>, <js>"{1,number}"</js>) and printf syntax (<js>"%s"</js>, <js>"%d"</js>)
+ * within the same string.
+ *
+ * <h5 class='section'>Features:</h5>
+ * <ul>
+ * <li><b>Dual Format Support:</b> Mix MessageFormat and printf-style placeholders in the same pattern</li>
+ * <li><b>Thread-Safe:</b> Immutable class, safe for concurrent use</li>
+ * <li><b>Cacheable:</b> Use {@link #of(String)} for cached instances</li>
+ * <li><b>Argument Sharing:</b> Both format styles share the same argument array</li>
+ * </ul>
+ *
+ * <h5 class='section'>Format Style Detection:</h5>
+ * <p>
+ * The formatter automatically detects which style to use for each placeholder:
+ * <ul>
+ * <li><b>MessageFormat style:</b> <js>"{0}"</js>, <js>"{1,number}"</js>, <js>"{2,date}"</js>, etc.</li>
+ * <li><b>Printf style:</b> <js>"%s"</js>, <js>"%d"</js>, <js>"%.2f"</js>, <js>"%1$s"</js>, etc.</li>
+ * </ul>
+ *
+ * <h5 class='section'>Argument Mapping:</h5>
+ * <p>
+ * Arguments are processed in order of appearance:
+ * <ul>
+ * <li><b>MessageFormat placeholders:</b> Use explicit indices (e.g., <js>"{0}"</js> uses <c>args[0]</c>)</li>
+ * <li><b>Printf placeholders:</b> Use sequential indices starting after the highest MessageFormat index</li>
+ * </ul>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Mixed format styles</jc>
+ * FormatString <jv>fmt</jv> = FormatString.<jsm>of</jsm>(<js>"Hello {0}, you have %d items"</js>);
+ * String <jv>result</jv> = <jv>fmt</jv>.<jsm>format</jsm>(<js>"John"</js>, 5);
+ * <jc>// Returns: "Hello John, you have 5 items"</jc>
+ *
+ * <jc>// MessageFormat with explicit indices, printf with sequential</jc>
+ * FormatString <jv>fmt2</jv> = FormatString.<jsm>of</jsm>(<js>"User {0} has %s and {1} items"</js>);
+ * String <jv>result2</jv> = <jv>fmt2</jv>.<jsm>format</jsm>(<js>"Alice"</js>, 10, <js>"admin"</js>);
+ * <jc>// Returns: "User Alice has admin and 10 items"</jc>
+ * <jc>// {0} -> "Alice", {1} -> 10, %s -> "admin"</jc>
+ *
+ * <jc>// Printf with explicit indices</jc>
+ * FormatString <jv>fmt3</jv> = FormatString.<jsm>of</jsm>(<js>"%1$s loves %2$s, and {0} also loves %3$s"</js>);
+ * String <jv>result3</jv> = <jv>fmt3</jv>.<jsm>format</jsm>(<js>"Alice"</js>, <js>"Bob"</js>, <js>"Charlie"</js>);
+ * <jc>// Returns: "Alice loves Bob, and Alice also loves Charlie"</jc>
+ * </p>
+ *
+ * <h5 class='section'>Caching:</h5>
+ * <p>
+ * Use {@link #of(String)} to get cached instances. The cache is thread-safe and limited to 1000 entries.
+ * For uncached instances, use the constructor directly.
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is immutable and thread-safe. Multiple threads can safely use the same instance concurrently.
+ * </p>
+ *
+ * @see StringUtils#format(String, Object...)
+ * @see StringUtils#mformat(String, Object...)
+ */
+public final class FormatString {
+
+ private static final Cache<String,FormatString> CACHE = Cache.of(String.class, FormatString.class).maxSize(1000).weak().build();
+
+ private static final Cache2<Locale,String,MessageFormat> MESSAGE_FORMAT_CACHE = Cache2.of(Locale.class, String.class, MessageFormat.class).maxSize(100).threadLocal().weak()
+ .supplier((locale, content) -> new MessageFormat(content, locale)).build();
+
+ private static final Cache<Locale,NumberFormat> NUMBER_FORMAT_CACHE = Cache.of(Locale.class, NumberFormat.class).maxSize(50).threadLocal().weak().supplier(NumberFormat::getInstance).build();
+
+ private static final Cache<Locale,DateFormat> DATE_FORMAT_CACHE = Cache.of(Locale.class, DateFormat.class).maxSize(50).threadLocal().weak()
+ .supplier(locale -> DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale)).build();
+
+ private static final AsciiSet PRINTF_CONVERSION_CHARS = AsciiSet.of("bBhHsScCdoxXeEfgGaAtTn%");
+ private static final AsciiSet PRINTF_FORMAT_CHARS = AsciiSet.of("-+ 0(#.*$");
+ private static final AsciiSet FORMAT_CHARS = AsciiSet.of("%{'");
+
+ /**
+ * Formats a pattern string with the given arguments using the default locale.
+ *
+ * <p>
+ * This is a convenience method that creates a FormatString instance and formats it.
+ * If the pattern contains no format specifiers (no '%', '{', or '''), the pattern is returned as-is.
+ *
+ * @param pattern The format pattern.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk> or format specifiers are invalid.
+ */
+ public static String format(String pattern, Object...args) {
+ if (!hasArgs(pattern))
+ return pattern;
+ return of(pattern).format(args);
+ }
+
+ /**
+ * Formats a pattern string with the given arguments using the specified locale.
+ *
+ * <p>
+ * This is a convenience method that creates a FormatString instance and formats it.
+ * If the pattern contains no format specifiers (no '%', '{', or '''), the pattern is returned as-is.
+ *
+ * @param pattern The format pattern.
+ * @param locale The locale to use for formatting. If <jk>null</jk>, uses the default locale.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk> or format specifiers are invalid.
+ */
+ public static String format(String pattern, Locale locale, Object...args) {
+ if (!hasArgs(pattern))
+ return pattern;
+ return of(pattern).format(locale, args);
+ }
+
+ /**
+ * Checks if a pattern contains any format specifiers.
+ *
+ * <p>
+ * Returns <jk>true</jk> if the pattern contains any of the following characters:
+ * <ul>
+ * <li><js>'%'</js> - Printf-style format specifier</li>
+ * <li><js>'{'</js> - MessageFormat-style placeholder</li>
+ * <li><js>'''</js> - MessageFormat quote</li>
+ * </ul>
+ *
+ * <p>
+ * Uses {@link AsciiSet} for efficient single-pass scanning of the string.
+ *
+ * @param pattern The pattern to check.
+ * @return <jk>true</jk> if the pattern contains format specifiers, <jk>false</jk> otherwise.
+ */
+ private static boolean hasArgs(String pattern) {
+ if (pattern == null || pattern.isEmpty())
+ return false;
+ for (var i = 0; i < pattern.length(); i++) {
+ if (FORMAT_CHARS.contains(pattern.charAt(i)))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Base class for format tokens.
+ */
+ private abstract static class Token {
+ /**
+ * Appends the formatted content to the StringBuilder.
+ *
+ * @param sb The StringBuilder to append to.
+ * @param args The arguments array.
+ * @param locale The locale for formatting (can be null for default).
+ */
+ abstract void append(StringBuilder sb, Object[] args, Locale locale);
+ }
+
+ /**
+ * Literal text token.
+ */
+ private static final class LiteralToken extends Token {
+ private final String text;
+
+ LiteralToken(String text) {
+ this.text = text;
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ sb.append(text);
+ }
+
+ @Override
+ public String toString() {
+ return "[L:" + text + "]";
+ }
+ }
+
+ /**
+ * MessageFormat-style token (e.g., {0}, {1,number}).
+ */
+ private static final class MessageFormatToken extends Token {
+ private final char format;
+ private final String content; // null for simple tokens, normalized pattern like "{0,number}" for complex tokens
+ private final int index; // 0-based index
+ private final String placeholder; // Original placeholder text like "{0}" or "{0,number}"
+
+ /**
+ * @param content - The variable content such as "{}" or "{0}", or "{0,number}"
+ * The content should have the curly-braces already removed so that we're only looking at the inner parts.
+ * @param index - The zero-based index of the variable in the message (used for sequential {} placeholders).
+ */
+ MessageFormatToken(String content, int index) {
+ if (content.isBlank()) {
+ this.content = null;
+ this.index = index;
+ this.format = 's';
+ this.placeholder = "{" + index + "}";
+ } else if (content.indexOf(',') == -1) {
+ this.content = null;
+ this.index = parseIndexMF(content);
+ this.format = 's';
+ this.placeholder = "{" + this.index + "}";
+ } else {
+ var tokens = content.split(",", 2);
+ this.index = parseIndexMF(tokens[0]);
+ this.content = "{0," + tokens[1] + "}";
+ this.format = 'o';
+ this.placeholder = "{" + this.index + "," + tokens[1] + "}";
+ }
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ // MessageFormat inserts the placeholder text if argument is missing
+ if (args == null || index >= args.length || index < 0) {
+ sb.append(placeholder);
+ return;
+ }
+ var o = args[index];
+ var l = locale == null ? Locale.getDefault() : locale;
+ switch (format) {
+ case 's':
+ if (o == null) {
+ sb.append("null");
+ } else if (o instanceof Number o2) {
+ sb.append(NUMBER_FORMAT_CACHE.get(l).format(o2));
+ } else if (o instanceof Date o2) {
+ sb.append(DATE_FORMAT_CACHE.get(l).format(o2));
+ } else {
+ sb.append(o.toString());
+ }
+ break;
+ default:
+ // Use Cache2 with Locale and content as separate keys to avoid string concatenation
+ var mf = MESSAGE_FORMAT_CACHE.get(l, content);
+ sb.append(mf.format(a(o)));
+ break;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[M:" + format + index + (content == null ? "" : (':' + content)) + "]";
+ }
+ }
+
+ /**
+ * Printf-style token (e.g., %s, %d, %.2f).
+ */
+ private static final class StringFormatToken extends Token {
+ private final char format; // 's' = simple (handle directly), 'o' = other (use String.format)
+ private final String content; // The format string to pass to String.format (null for simple formats)
+ private final int index; // 0-based index
+
+ StringFormatToken(String content, int index) {
+ // content is everything after '%' (e.g., "s", "1$s", "d", ".2f", "1$.2f")
+ var $ = content.indexOf('$');
+ if ($ >= 0) {
+ index = parseIndexSF(content.substring(0, $)) - 1;
+ content = content.substring($ + 1);
+ }
+ this.format = content.length() == 1 ? content.charAt(content.length() - 1) : 'z';
+ this.index = index;
+ this.content = "%" + content;
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ // String.format() throws MissingFormatArgumentException when argument is missing
+ if (args == null || index >= args.length || index < 0) {
+ throw new MissingFormatArgumentException(content);
+ }
+ var o = args[index];
+ var l = locale == null ? Locale.getDefault() : locale;
+ var dl = locale == null || locale.equals(Locale.getDefault());
+ switch (format) {
+ case 'b':
+ // String.format() with %b converts:
+ // - null -> "false"
+ // - Boolean -> toString()
+ // - Any other non-null value -> "true"
+ if (o == null) {
+ sb.append("false");
+ } else if (o instanceof Boolean) {
+ sb.append(o.toString());
+ } else {
+ sb.append("true");
+ }
+ return;
+ case 'B':
+ // String.format() with %B converts:
+ // - null -> "FALSE"
+ // - Boolean -> toString().toUpperCase()
+ // - Any other non-null value -> "TRUE"
+ if (o == null) {
+ sb.append("FALSE");
+ } else if (o instanceof Boolean) {
+ sb.append(o.toString().toUpperCase());
+ } else {
+ sb.append("TRUE");
+ }
+ return;
+ case 's':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ sb.append(o.toString());
+ return;
+ case 'S':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ sb.append(o.toString().toUpperCase());
+ return;
+ case 'd':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Number o2) {
+ if (dl) {
+ if (o instanceof Integer || o instanceof Long || o instanceof Byte || o instanceof Short) {
+ sb.append(o);
+ } else {
+ // For other Number types (BigDecimal, BigInteger, etc.), convert to long
+ sb.append(o2.longValue());
+ }
+ return;
+ }
+ // For non-default locales, use String.format to ensure printf-style consistency
+ sb.append(sf(l, "%d", o));
+ return;
+ }
+ break;
+ case 'x':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toHexString(o2));
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toHexString(o2));
+ return;
+ }
+ break;
+ case 'X':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toHexString(o2).toUpperCase());
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toHexString(o2).toUpperCase());
+ return;
+ }
+ break;
+ case 'o':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toOctalString(o2));
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toOctalString(o2));
+ return;
+ }
+ break;
+ case 'c':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Character) {
+ sb.append(o);
+ return;
+ } else if (o instanceof Integer o2) {
+ sb.append((char)o2.intValue());
+ return;
+ }
+ break;
+ case 'C':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ if (o instanceof Character o2) {
+ sb.append(Character.toUpperCase(o2));
+ return;
+ } else if (o instanceof Integer o2) {
+ sb.append(Character.toUpperCase((char)o2.intValue()));
+ return;
+ }
+ break;
+ case 'f':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ // Always use String.format() to match exact behavior (precision, etc.)
+ if (o instanceof Number) {
+ sb.append(sf(l, "%f", o));
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Fallback to String.format for any other simple format
+ sb.append(sf(l, content, o));
+ }
+
+ @Override
+ public String toString() {
+ return "[S:" + format + index + ":" + content + "]";
+ }
+ }
+
+ private static String sf(Locale l, String s, Object o) {
+ return String.format(l, s, a(o));
+ }
+
+ private final String pattern;
+ private final Token[] tokens;
+
+ /**
+ * Creates a new FormatString instance.
+ *
+ * @param pattern The format pattern. Can contain both MessageFormat and printf-style placeholders.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk>.
+ */
+ public FormatString(String pattern) {
+ this.pattern = assertArgNotNull("pattern", pattern);
+ this.tokens = parseTokens(pattern).toArray(Token[]::new);
+ }
+
+ /**
+ * Returns a cached FormatString instance for the given pattern.
+ *
+ * <p>
+ * This method uses a thread-safe cache to avoid recreating FormatString instances for the same pattern.
+ * The cache is limited to 1000 entries.
+ *
+ * @param pattern The format pattern.
+ * @return A cached or new FormatString instance.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk>.
+ */
+ public static FormatString of(String pattern) {
+ assertArgNotNull("pattern", pattern);
+ return CACHE.get(pattern, () -> new FormatString(pattern));
+ }
+
+ /**
+ * Formats the pattern with the given arguments using the default locale.
+ *
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If format specifiers are invalid or arguments don't match.
+ */
+ public String format(Object...args) {
+ return format(Locale.getDefault(), args);
+ }
+
+ /**
+ * Formats the pattern with the given arguments using the specified locale.
+ *
+ * <p>
+ * The locale affects both MessageFormat and printf-style formatting:
+ * <ul>
+ * <li><b>MessageFormat:</b> Locale-specific number, date, and time formatting</li>
+ * <li><b>Printf:</b> Locale-specific number formatting (decimal separators, etc.)</li>
+ * </ul>
+ *
+ * @param locale The locale to use for formatting. If <jk>null</jk>, uses the default locale.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If format specifiers are invalid or arguments don't match.
+ */
+ public String format(Locale locale, Object...args) {
+ var sb = new StringBuilder(pattern.length() + 64);
+ for (var token : tokens) {
+ token.append(sb, args, locale);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parses the pattern into a list of tokens.
+ */
+ private static List<Token> parseTokens(String pattern) {
+ var tokens = new ArrayList<Token>();
+ var length = pattern.length();
+ var i = 0;
+ var sequentialIndex = 0; // 0-based index for sequential placeholders
+
+ // Possible String.format variable formats:
+ // %[argument_index$][flags][width][.precision]conversion
+ // %[argument_index$][flags][width]conversion
+ // %[flags][width]conversion
+
+ // Possible MessageFormat variable formats:
+ // {}
+ // {#,formatType}
+ // {#,formatType,formatStyle}
+
+ // S1 - In literal, looking for %, {, or '
+ // S2 - Found %, looking for conversion char or t or T
+ // S3 - Found {, looking for }
+ // S4 - Found ', in quoted section (MessageFormat single quotes escape special chars), looking for '
+ var state = S1;
+
+ var nestedBracketDepth = 0;
+
+ var mark = 0;
+ while (i < length) {
+ var ch = pattern.charAt(i++);
+
+ if (state == S1) {
+ if (ch == '%') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S2;
+ mark = i;
+ } else if (ch == '{') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S3;
+ mark = i - 1;
+ } else if (ch == '\'') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S4;
+ mark = i;
+ }
+ } else if (state == S2) {
+ if (ch == '%') {
+ tokens.add(new LiteralToken("%"));
+ state = S1;
+ mark = i;
+ } else if (ch == 't' || ch == 'T') {
+ // Do nothing. Part of 2-character time conversion.
+ } else if (PRINTF_CONVERSION_CHARS.contains(ch)) {
+ sf(tokens, pattern, mark, i, sequentialIndex++);
+ state = S1;
+ mark = i;
+ } else if (PRINTF_FORMAT_CHARS.contains(ch) || Character.isDigit(ch)) {
+ // Do nothing.
+ } else {
+ // Unknown character - could be invalid conversion or end of format
+ // Create StringFormatToken and let String.format() validate it
+ // This allows String.format() to throw IllegalFormatException for invalid conversions like %F
+ // printfStart is position after '%', so substring from printfStart-1 (the '%') to i (after the char)
+ sf(tokens, pattern, mark, i, sequentialIndex++);
+ state = S1;
+ mark = i;
+ }
+ } else if (state == S3) {
+ if (ch == '{') {
+ nestedBracketDepth++;
+ } else if (ch == '}') {
+ if (nestedBracketDepth > 0) {
+ nestedBracketDepth--;
+ } else {
+ mf(tokens, pattern, mark + 1, i - 1, sequentialIndex++);
+ state = S1;
+ mark = i;
+ }
+ }
+ } else /* if (state == S4) */ {
+ if (ch == '\'') {
+ if (mark == i - 1) {
+ lit(tokens, pattern, mark, i); // '' becomes '
+ state = S1;
+ mark = i;
+ } else {
+ lit(tokens, pattern, mark, i - 1);
+ state = S1;
+ mark = i;
+ }
+ }
+ }
+ }
+
+ // Process remaining content based on final state
+ if (state == S1) {
+ lit(tokens, pattern, mark);
+ } else if (state == S2) {
+ // Dangling '%' without conversion - throw exception to match String.format() behavior
+ // UnknownFormatConversionException constructor takes just the conversion character
+ throw new java.util.UnknownFormatConversionException("%");
+ } else if (state == S3) {
+ // Unmatched '{' - throw exception to match MessageFormat behavior
+ throw new IllegalArgumentException("Unmatched braces in the pattern.");
+ } else /* if (state == S4) */ {
+ // Unmatched quote - MessageFormat treats it as ending the quoted section
+ // Add the quoted content as literal (from mark to end of pattern)
+ lit(tokens, pattern, mark);
+ }
+
+ return tokens;
+ }
+
+ private static void lit(List<Token> tokens, String pattern, int start, int end) {
+ if (start == end)
+ return;
+ tokens.add(new LiteralToken(pattern.substring(start, end)));
+ }
+
+ private static void lit(List<Token> tokens, String pattern, int start) {
+ if (start == pattern.length())
+ return;
+ tokens.add(new LiteralToken(pattern.substring(start)));
+ }
+
+ private static void sf(List<Token> tokens, String pattern, int start, int end, int index) {
+ tokens.add(new StringFormatToken(pattern.substring(start, end), index));
+ }
+
+ private static void mf(List<Token> tokens, String pattern, int start, int end, int index) {
+ tokens.add(new MessageFormatToken(pattern.substring(start, end), index));
+ }
+
+ private static int parseIndexMF(String s) {
+ try {
+ return Integer.parseInt(s.trim());
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ throw new IllegalArgumentException("can't parse argument number: " + s);
+ }
+ }
+
+ private static int parseIndexSF(String s) {
+ return Integer.parseInt(s.trim());
+ }
+
+ @Override
+ public String toString() {
+ return pattern;
+ }
+
+ /**
+ * Returns a debug representation of the parsed pattern showing the token structure.
+ *
+ * <p>
+ * This method is useful for debugging and understanding how a pattern was parsed.
+ * It returns a string showing each token in the format:
+ * <ul>
+ * <li><b>Literal tokens:</b> <js>"[L:text]"</js> - Literal text</li>
+ * <li><b>MessageFormat tokens (simple):</b> <js>"[M:s0]"</js> - Simple MessageFormat placeholder (format='s', index=0)</li>
+ * <li><b>MessageFormat tokens (complex):</b> <js>"[M:o0:{0,number,currency}]"</js> - Complex MessageFormat placeholder (format='o', index=0, content)</li>
+ * <li><b>StringFormat tokens (simple):</b> <js>"[S:s0:%s]"</js> - Simple printf placeholder (format='s', index=0, content)</li>
+ * <li><b>StringFormat tokens (complex):</b> <js>"[S:z0:%.2f]"</js> - Complex printf placeholder (format='z', index=0, content)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Token Format:</h5>
+ * <ul>
+ * <li><b>L</b> = Literal token</li>
+ * <li><b>M</b> = MessageFormat token</li>
+ * <li><b>S</b> = StringFormat (printf) token</li>
+ * <li><b>Format character:</b> 's' = simple, 'o' = other/complex, 'z' = complex printf</li>
+ * <li><b>Index:</b> 0-based argument index</li>
+ * <li><b>Content:</b> The format string content (for complex tokens)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * FormatString <jv>fmt</jv> = FormatString.<jsm>of</jsm>(<js>"Hello {0}, you have %d items"</js>);
+ * <jv>fmt</jv>.<jsm>toPattern</jsm>();
+ * <jc>// Returns: "[L:Hello ][M:s0][L:, you have ][S:d1:%d][L: items]"</jc>
+ * </p>
+ *
+ * @return A debug string showing the parsed token structure.
+ */
+ public String toPattern() {
+ return Arrays.stream(tokens).map(Object::toString).collect(Collectors.joining());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof FormatString o2 && eq(this, o2, (x, y) -> eq(x.pattern, y.pattern));
+ }
+
+ @Override
+ public int hashCode() {
+ return pattern.hashCode();
+ }
+}
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringFormat.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringFormat.java
new file mode 100644
index 0000000000..834ea88678
--- /dev/null
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringFormat.java
@@ -0,0 +1,724 @@
+/*
+ * 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.juneau.common.utils;
+
+import static org.apache.juneau.common.utils.AssertionUtils.*;
+import static org.apache.juneau.common.utils.CollectionUtils.*;
+import static org.apache.juneau.common.utils.StateEnum.*;
+import static org.apache.juneau.common.utils.Utils.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.stream.*;
+import java.util.MissingFormatArgumentException;
+
+import org.apache.juneau.common.collections.*;
+
+/**
+ * Unified string formatter supporting both MessageFormat-style and printf-style formatting in the same pattern.
+ *
+ * <p>
+ * This class provides a thread-safe, cacheable formatter that can handle mixed format styles in a single pattern.
+ * It supports both MessageFormat syntax (<js>"{0}"</js>, <js>"{1,number}"</js>) and printf syntax (<js>"%s"</js>, <js>"%d"</js>)
+ * within the same string.
+ *
+ * <h5 class='section'>Features:</h5>
+ * <ul>
+ * <li><b>Dual Format Support:</b> Mix MessageFormat and printf-style placeholders in the same pattern</li>
+ * <li><b>Thread-Safe:</b> Immutable class, safe for concurrent use</li>
+ * <li><b>Cacheable:</b> Use {@link #of(String)} for cached instances</li>
+ * <li><b>Argument Sharing:</b> Both format styles share the same argument array</li>
+ * </ul>
+ *
+ * <h5 class='section'>Format Style Detection:</h5>
+ * <p>
+ * The formatter automatically detects which style to use for each placeholder:
+ * <ul>
+ * <li><b>MessageFormat style:</b> <js>"{0}"</js>, <js>"{1,number}"</js>, <js>"{2,date}"</js>, etc.</li>
+ * <li><b>Printf style:</b> <js>"%s"</js>, <js>"%d"</js>, <js>"%.2f"</js>, <js>"%1$s"</js>, etc.</li>
+ * </ul>
+ *
+ * <h5 class='section'>Argument Mapping:</h5>
+ * <p>
+ * Arguments are processed in order of appearance:
+ * <ul>
+ * <li><b>MessageFormat placeholders:</b> Use explicit indices (e.g., <js>"{0}"</js> uses <c>args[0]</c>)</li>
+ * <li><b>Printf placeholders:</b> Use sequential indices starting after the highest MessageFormat index</li>
+ * </ul>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Mixed format styles</jc>
+ * StringFormat <jv>fmt</jv> = StringFormat.<jsm>of</jsm>(<js>"Hello {0}, you have %d items"</js>);
+ * String <jv>result</jv> = <jv>fmt</jv>.<jsm>format</jsm>(<js>"John"</js>, 5);
+ * <jc>// Returns: "Hello John, you have 5 items"</jc>
+ *
+ * <jc>// MessageFormat with explicit indices, printf with sequential</jc>
+ * StringFormat <jv>fmt2</jv> = StringFormat.<jsm>of</jsm>(<js>"User {0} has %s and {1} items"</js>);
+ * String <jv>result2</jv> = <jv>fmt2</jv>.<jsm>format</jsm>(<js>"Alice"</js>, 10, <js>"admin"</js>);
+ * <jc>// Returns: "User Alice has admin and 10 items"</jc>
+ * <jc>// {0} -> "Alice", {1} -> 10, %s -> "admin"</jc>
+ *
+ * <jc>// Printf with explicit indices</jc>
+ * StringFormat <jv>fmt3</jv> = StringFormat.<jsm>of</jsm>(<js>"%1$s loves %2$s, and {0} also loves %3$s"</js>);
+ * String <jv>result3</jv> = <jv>fmt3</jv>.<jsm>format</jsm>(<js>"Alice"</js>, <js>"Bob"</js>, <js>"Charlie"</js>);
+ * <jc>// Returns: "Alice loves Bob, and Alice also loves Charlie"</jc>
+ * </p>
+ *
+ * <h5 class='section'>Caching:</h5>
+ * <p>
+ * Use {@link #of(String)} to get cached instances. The cache is thread-safe and limited to 1000 entries.
+ * For uncached instances, use the constructor directly.
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is immutable and thread-safe. Multiple threads can safely use the same instance concurrently.
+ * </p>
+ *
+ * @see StringUtils#format(String, Object...)
+ * @see StringUtils#mformat(String, Object...)
+ */
+public final class StringFormat {
+
+ private static final Cache<String,StringFormat> CACHE = Cache.of(String.class, StringFormat.class).maxSize(1000).weak().build();
+
+ private static final Cache2<Locale,String,MessageFormat> MESSAGE_FORMAT_CACHE = Cache2.of(Locale.class, String.class, MessageFormat.class).maxSize(100).threadLocal().weak()
+ .supplier((locale, content) -> new MessageFormat(content, locale)).build();
+
+ private static final Cache<Locale,NumberFormat> NUMBER_FORMAT_CACHE = Cache.of(Locale.class, NumberFormat.class).maxSize(50).threadLocal().weak().supplier(NumberFormat::getInstance).build();
+
+ private static final Cache<Locale,DateFormat> DATE_FORMAT_CACHE = Cache.of(Locale.class, DateFormat.class).maxSize(50).threadLocal().weak()
+ .supplier(locale -> DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale)).build();
+
+ private static final AsciiSet PRINTF_CONVERSION_CHARS = AsciiSet.of("bBhHsScCdoxXeEfgGaAtTn%");
+ private static final AsciiSet PRINTF_FORMAT_CHARS = AsciiSet.of("-+ 0(#.*$");
+ private static final AsciiSet FORMAT_CHARS = AsciiSet.of("%{'");
+
+ /**
+ * Formats a pattern string with the given arguments using the default locale.
+ *
+ * <p>
+ * This is a convenience method that creates a StringFormat instance and formats it.
+ * If the pattern contains no format specifiers (no '%', '{', or '''), the pattern is returned as-is.
+ *
+ * @param pattern The format pattern.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk> or format specifiers are invalid.
+ */
+ public static String format(String pattern, Object...args) {
+ if (!hasArgs(pattern))
+ return pattern;
+ return of(pattern).format(args);
+ }
+
+ /**
+ * Formats a pattern string with the given arguments using the specified locale.
+ *
+ * <p>
+ * This is a convenience method that creates a StringFormat instance and formats it.
+ * If the pattern contains no format specifiers (no '%', '{', or '''), the pattern is returned as-is.
+ *
+ * @param pattern The format pattern.
+ * @param locale The locale to use for formatting. If <jk>null</jk>, uses the default locale.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk> or format specifiers are invalid.
+ */
+ public static String format(String pattern, Locale locale, Object...args) {
+ if (!hasArgs(pattern))
+ return pattern;
+ return of(pattern).format(locale, args);
+ }
+
+ /**
+ * Checks if a pattern contains any format specifiers.
+ *
+ * <p>
+ * Returns <jk>true</jk> if the pattern contains any of the following characters:
+ * <ul>
+ * <li><js>'%'</js> - Printf-style format specifier</li>
+ * <li><js>'{'</js> - MessageFormat-style placeholder</li>
+ * <li><js>'''</js> - MessageFormat quote</li>
+ * </ul>
+ *
+ * <p>
+ * Uses {@link AsciiSet} for efficient single-pass scanning of the string.
+ *
+ * @param pattern The pattern to check.
+ * @return <jk>true</jk> if the pattern contains format specifiers, <jk>false</jk> otherwise.
+ */
+ private static boolean hasArgs(String pattern) {
+ if (pattern == null || pattern.isEmpty())
+ return false;
+ for (var i = 0; i < pattern.length(); i++) {
+ if (FORMAT_CHARS.contains(pattern.charAt(i)))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Base class for format tokens.
+ */
+ private abstract static class Token {
+ /**
+ * Appends the formatted content to the StringBuilder.
+ *
+ * @param sb The StringBuilder to append to.
+ * @param args The arguments array.
+ * @param locale The locale for formatting (can be null for default).
+ */
+ abstract void append(StringBuilder sb, Object[] args, Locale locale);
+ }
+
+ /**
+ * Literal text token.
+ */
+ private static final class LiteralToken extends Token {
+ private final String text;
+
+ LiteralToken(String text) {
+ this.text = text;
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ sb.append(text);
+ }
+
+ @Override
+ public String toString() {
+ return "[L:" + text + "]";
+ }
+ }
+
+ /**
+ * MessageFormat-style token (e.g., {0}, {1,number}).
+ */
+ private static final class MessageFormatToken extends Token {
+ private final char format;
+ private final String content; // null for simple tokens, normalized pattern like "{0,number}" for complex tokens
+ private final int index; // 0-based index
+ private final String placeholder; // Original placeholder text like "{0}" or "{0,number}"
+
+ /**
+ * @param content - The variable content such as "{}" or "{0}", or "{0,number}"
+ * The content should have the curly-braces already removed so that we're only looking at the inner parts.
+ * @param index - The zero-based index of the variable in the message (used for sequential {} placeholders).
+ */
+ MessageFormatToken(String content, int index) {
+ if (content.isBlank()) {
+ this.content = null;
+ this.index = index;
+ this.format = 's';
+ this.placeholder = "{" + index + "}";
+ } else if (content.indexOf(',') == -1) {
+ this.content = null;
+ this.index = parseIndexMF(content);
+ this.format = 's';
+ this.placeholder = "{" + this.index + "}";
+ } else {
+ var tokens = content.split(",", 2);
+ this.index = parseIndexMF(tokens[0]);
+ this.content = "{0," + tokens[1] + "}";
+ this.format = 'o';
+ this.placeholder = "{" + this.index + "," + tokens[1] + "}";
+ }
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ // MessageFormat inserts the placeholder text if argument is missing
+ if (args == null || index >= args.length || index < 0) {
+ sb.append(placeholder);
+ return;
+ }
+ var o = args[index];
+ var l = locale == null ? Locale.getDefault() : locale;
+ switch (format) {
+ case 's':
+ if (o == null) {
+ sb.append("null");
+ } else if (o instanceof Number o2) {
+ sb.append(NUMBER_FORMAT_CACHE.get(l).format(o2));
+ } else if (o instanceof Date o2) {
+ sb.append(DATE_FORMAT_CACHE.get(l).format(o2));
+ } else {
+ sb.append(o.toString());
+ }
+ break;
+ default:
+ // Use Cache2 with Locale and content as separate keys to avoid string concatenation
+ var mf = MESSAGE_FORMAT_CACHE.get(l, content);
+ sb.append(mf.format(a(o)));
+ break;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[M:" + format + index + (content == null ? "" : (':' + content)) + "]";
+ }
+ }
+
+ /**
+ * Printf-style token (e.g., %s, %d, %.2f).
+ */
+ private static final class StringFormatToken extends Token {
+ private final char format; // 's' = simple (handle directly), 'o' = other (use String.format)
+ private final String content; // The format string to pass to String.format (null for simple formats)
+ private final int index; // 0-based index
+
+ StringFormatToken(String content, int index) {
+ // content is everything after '%' (e.g., "s", "1$s", "d", ".2f", "1$.2f")
+ var $ = content.indexOf('$');
+ if ($ >= 0) {
+ index = parseIndexSF(content.substring(0, $)) - 1;
+ content = content.substring($ + 1);
+ }
+ this.format = content.length() == 1 ? content.charAt(content.length() - 1) : 'z';
+ this.index = index;
+ this.content = "%" + content;
+ }
+
+ @Override
+ void append(StringBuilder sb, Object[] args, Locale locale) {
+ // String.format() throws MissingFormatArgumentException when argument is missing
+ if (args == null || index >= args.length || index < 0) {
+ throw new MissingFormatArgumentException(content);
+ }
+ var o = args[index];
+ var l = locale == null ? Locale.getDefault() : locale;
+ var dl = locale == null || locale.equals(Locale.getDefault());
+ switch (format) {
+ case 'b':
+ // String.format() with %b converts:
+ // - null -> "false"
+ // - Boolean -> toString()
+ // - Any other non-null value -> "true"
+ if (o == null) {
+ sb.append("false");
+ } else if (o instanceof Boolean) {
+ sb.append(o.toString());
+ } else {
+ sb.append("true");
+ }
+ return;
+ case 'B':
+ // String.format() with %B converts:
+ // - null -> "FALSE"
+ // - Boolean -> toString().toUpperCase()
+ // - Any other non-null value -> "TRUE"
+ if (o == null) {
+ sb.append("FALSE");
+ } else if (o instanceof Boolean) {
+ sb.append(o.toString().toUpperCase());
+ } else {
+ sb.append("TRUE");
+ }
+ return;
+ case 's':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ sb.append(o.toString());
+ return;
+ case 'S':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ sb.append(o.toString().toUpperCase());
+ return;
+ case 'd':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Number o2) {
+ if (dl) {
+ if (o instanceof Integer || o instanceof Long || o instanceof Byte || o instanceof Short) {
+ sb.append(o);
+ } else {
+ // For other Number types (BigDecimal, BigInteger, etc.), convert to long
+ sb.append(o2.longValue());
+ }
+ return;
+ }
+ // For non-default locales, use String.format to ensure printf-style consistency
+ sb.append(sf(l, "%d", o));
+ return;
+ }
+ break;
+ case 'x':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toHexString(o2));
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toHexString(o2));
+ return;
+ }
+ break;
+ case 'X':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toHexString(o2).toUpperCase());
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toHexString(o2).toUpperCase());
+ return;
+ }
+ break;
+ case 'o':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Integer o2) {
+ sb.append(Integer.toOctalString(o2));
+ return;
+ } else if (o instanceof Long o2) {
+ sb.append(Long.toOctalString(o2));
+ return;
+ }
+ break;
+ case 'c':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ if (o instanceof Character) {
+ sb.append(o);
+ return;
+ } else if (o instanceof Integer o2) {
+ sb.append((char)o2.intValue());
+ return;
+ }
+ break;
+ case 'C':
+ if (o == null) {
+ sb.append("NULL");
+ return;
+ }
+ if (o instanceof Character o2) {
+ sb.append(Character.toUpperCase(o2));
+ return;
+ } else if (o instanceof Integer o2) {
+ sb.append(Character.toUpperCase((char)o2.intValue()));
+ return;
+ }
+ break;
+ case 'f':
+ if (o == null) {
+ sb.append("null");
+ return;
+ }
+ // Always use String.format() to match exact behavior (precision, etc.)
+ if (o instanceof Number) {
+ sb.append(sf(l, "%f", o));
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Fallback to String.format for any other simple format
+ sb.append(sf(l, content, o));
+ }
+
+ @Override
+ public String toString() {
+ return "[S:" + format + index + ":" + content + "]";
+ }
+ }
+
+ private static String sf(Locale l, String s, Object o) {
+ return String.format(l, s, a(o));
+ }
+
+ private final String pattern;
+ private final Token[] tokens;
+
+ /**
+ * Creates a new StringFormat instance.
+ *
+ * @param pattern The format pattern. Can contain both MessageFormat and printf-style placeholders.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk>.
+ */
+ public StringFormat(String pattern) {
+ this.pattern = assertArgNotNull("pattern", pattern);
+ this.tokens = parseTokens(pattern).toArray(Token[]::new);
+ }
+
+ /**
+ * Returns a cached StringFormat instance for the given pattern.
+ *
+ * <p>
+ * This method uses a thread-safe cache to avoid recreating StringFormat instances for the same pattern.
+ * The cache is limited to 1000 entries.
+ *
+ * @param pattern The format pattern.
+ * @return A cached or new StringFormat instance.
+ * @throws IllegalArgumentException If the pattern is <jk>null</jk>.
+ */
+ public static StringFormat of(String pattern) {
+ assertArgNotNull("pattern", pattern);
+ return CACHE.get(pattern, () -> new StringFormat(pattern));
+ }
+
+ /**
+ * Formats the pattern with the given arguments using the default locale.
+ *
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If format specifiers are invalid or arguments don't match.
+ */
+ public String format(Object...args) {
+ return format(Locale.getDefault(), args);
+ }
+
+ /**
+ * Formats the pattern with the given arguments using the specified locale.
+ *
+ * <p>
+ * The locale affects both MessageFormat and printf-style formatting:
+ * <ul>
+ * <li><b>MessageFormat:</b> Locale-specific number, date, and time formatting</li>
+ * <li><b>Printf:</b> Locale-specific number formatting (decimal separators, etc.)</li>
+ * </ul>
+ *
+ * @param locale The locale to use for formatting. If <jk>null</jk>, uses the default locale.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws IllegalArgumentException If format specifiers are invalid or arguments don't match.
+ */
+ public String format(Locale locale, Object...args) {
+ var sb = new StringBuilder(pattern.length() + 64);
+ for (var token : tokens) {
+ token.append(sb, args, locale);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parses the pattern into a list of tokens.
+ */
+ private static List<Token> parseTokens(String pattern) {
+ var tokens = new ArrayList<Token>();
+ var length = pattern.length();
+ var i = 0;
+ var sequentialIndex = 0; // 0-based index for sequential placeholders
+
+ // Possible String.format variable formats:
+ // %[argument_index$][flags][width][.precision]conversion
+ // %[argument_index$][flags][width]conversion
+ // %[flags][width]conversion
+
+ // Possible MessageFormat variable formats:
+ // {}
+ // {#,formatType}
+ // {#,formatType,formatStyle}
+
+ // S1 - In literal, looking for %, {, or '
+ // S2 - Found %, looking for conversion char or t or T
+ // S3 - Found {, looking for }
+ // S4 - Found ', in quoted section (MessageFormat single quotes escape special chars), looking for '
+ var state = S1;
+
+ var nestedBracketDepth = 0;
+
+ var mark = 0;
+ while (i < length) {
+ var ch = pattern.charAt(i++);
+
+ if (state == S1) {
+ if (ch == '%') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S2;
+ mark = i;
+ } else if (ch == '{') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S3;
+ mark = i - 1;
+ } else if (ch == '\'') {
+ lit(tokens, pattern, mark, i - 1);
+ state = S4;
+ mark = i;
+ }
+ } else if (state == S2) {
+ if (ch == '%') {
+ tokens.add(new LiteralToken("%"));
+ state = S1;
+ mark = i;
+ } else if (ch == 't' || ch == 'T') {
+ // Do nothing. Part of 2-character time conversion.
+ } else if (PRINTF_CONVERSION_CHARS.contains(ch)) {
+ sf(tokens, pattern, mark, i, sequentialIndex++);
+ state = S1;
+ mark = i;
+ } else if (PRINTF_FORMAT_CHARS.contains(ch) || Character.isDigit(ch)) {
+ // Do nothing.
+ } else {
+ // Unknown character - could be invalid conversion or end of format
+ // Create StringFormatToken and let String.format() validate it
+ // This allows String.format() to throw IllegalFormatException for invalid conversions like %F
+ // printfStart is position after '%', so substring from printfStart-1 (the '%') to i (after the char)
+ sf(tokens, pattern, mark, i, sequentialIndex++);
+ state = S1;
+ mark = i;
+ }
+ } else if (state == S3) {
+ if (ch == '{') {
+ nestedBracketDepth++;
+ } else if (ch == '}') {
+ if (nestedBracketDepth > 0) {
+ nestedBracketDepth--;
+ } else {
+ mf(tokens, pattern, mark + 1, i - 1, sequentialIndex++);
+ state = S1;
+ mark = i;
+ }
+ }
+ } else /* if (state == S4) */ {
+ if (ch == '\'') {
+ if (mark == i - 1) {
+ lit(tokens, pattern, mark, i); // '' becomes '
+ state = S1;
+ mark = i;
+ } else {
+ lit(tokens, pattern, mark, i - 1);
+ state = S1;
+ mark = i;
+ }
+ }
+ }
+ }
+
+ // Process remaining content based on final state
+ if (state == S1) {
+ lit(tokens, pattern, mark);
+ } else if (state == S2) {
+ // Dangling '%' without conversion - throw exception to match String.format() behavior
+ // UnknownFormatConversionException constructor takes just the conversion character
+ throw new java.util.UnknownFormatConversionException("%");
+ } else if (state == S3) {
+ // Unmatched '{' - throw exception to match MessageFormat behavior
+ throw new IllegalArgumentException("Unmatched braces in the pattern.");
+ } else /* if (state == S4) */ {
+ // Unmatched quote - MessageFormat treats it as ending the quoted section
+ // Add the quoted content as literal (from mark to end of pattern)
+ lit(tokens, pattern, mark);
+ }
+
+ return tokens;
+ }
+
+ private static void lit(List<Token> tokens, String pattern, int start, int end) {
+ if (start == end)
+ return;
+ tokens.add(new LiteralToken(pattern.substring(start, end)));
+ }
+
+ private static void lit(List<Token> tokens, String pattern, int start) {
+ if (start == pattern.length())
+ return;
+ tokens.add(new LiteralToken(pattern.substring(start)));
+ }
+
+ private static void sf(List<Token> tokens, String pattern, int start, int end, int index) {
+ tokens.add(new StringFormatToken(pattern.substring(start, end), index));
+ }
+
+ private static void mf(List<Token> tokens, String pattern, int start, int end, int index) {
+ tokens.add(new MessageFormatToken(pattern.substring(start, end), index));
+ }
+
+ private static int parseIndexMF(String s) {
+ try {
+ return Integer.parseInt(s.trim());
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ throw new IllegalArgumentException("can't parse argument number: " + s);
+ }
+ }
+
+ private static int parseIndexSF(String s) {
+ return Integer.parseInt(s.trim());
+ }
+
+ @Override
+ public String toString() {
+ return pattern;
+ }
+
+ /**
+ * Returns a debug representation of the parsed pattern showing the token structure.
+ *
+ * <p>
+ * This method is useful for debugging and understanding how a pattern was parsed.
+ * It returns a string showing each token in the format:
+ * <ul>
+ * <li><b>Literal tokens:</b> <js>"[L:text]"</js> - Literal text</li>
+ * <li><b>MessageFormat tokens (simple):</b> <js>"[M:s0]"</js> - Simple MessageFormat placeholder (format='s', index=0)</li>
+ * <li><b>MessageFormat tokens (complex):</b> <js>"[M:o0:{0,number,currency}]"</js> - Complex MessageFormat placeholder (format='o', index=0, content)</li>
+ * <li><b>StringFormat tokens (simple):</b> <js>"[S:s0:%s]"</js> - Simple printf placeholder (format='s', index=0, content)</li>
+ * <li><b>StringFormat tokens (complex):</b> <js>"[S:z0:%.2f]"</js> - Complex printf placeholder (format='z', index=0, content)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Token Format:</h5>
+ * <ul>
+ * <li><b>L</b> = Literal token</li>
+ * <li><b>M</b> = MessageFormat token</li>
+ * <li><b>S</b> = StringFormat (printf) token</li>
+ * <li><b>Format character:</b> 's' = simple, 'o' = other/complex, 'z' = complex printf</li>
+ * <li><b>Index:</b> 0-based argument index</li>
+ * <li><b>Content:</b> The format string content (for complex tokens)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * StringFormat <jv>fmt</jv> = StringFormat.<jsm>of</jsm>(<js>"Hello {0}, you have %d items"</js>);
+ * <jv>fmt</jv>.<jsm>toPattern</jsm>();
+ * <jc>// Returns: "[L:Hello ][M:s0][L:, you have ][S:d1:%d][L: items]"</jc>
+ * </p>
+ *
+ * @return A debug string showing the parsed token structure.
+ */
+ public String toPattern() {
+ return Arrays.stream(tokens).map(Object::toString).collect(Collectors.joining());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof StringFormat o2 && eq(this, o2, (x, y) -> eq(x.pattern, y.pattern));
+ }
+
+ @Override
+ public int hashCode() {
+ return pattern.hashCode();
+ }
+}
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
index a273e86014..f342752c0f 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
@@ -105,7 +105,7 @@ public class StringUtils {
* <li>Object allocation for frequently used patterns</li>
* </ul>
*/
- private static final ThreadLocal<Cache<String,MessageFormat>> MESSAGE_FORMAT_CACHE = ThreadLocal.withInitial(() -> Cache.of(String.class, MessageFormat.class).maxSize(100).build());
+ private static final Cache<String,MessageFormat> MESSAGE_FORMAT_CACHE = Cache.of(String.class, MessageFormat.class).maxSize(100).threadLocal().build();
private static final AsciiSet numberChars = AsciiSet.of("-xX.+-#pP0123456789abcdefABCDEF");
@@ -746,9 +746,8 @@ public class StringUtils {
if (c % 2 != 0)
throw new AssertionError("Dangling single quote found in pattern: " + pattern);
- // Get or create a cached MessageFormat for this pattern (thread-safe via ThreadLocal)
- var cache = MESSAGE_FORMAT_CACHE.get();
- var mf = cache.get(pattern, () -> new MessageFormat(pattern));
+ // Get or create a cached MessageFormat for this pattern (thread-safe via thread-local cache)
+ var mf = MESSAGE_FORMAT_CACHE.get(pattern, () -> new MessageFormat(pattern));
// Determine which arguments have format types and need to preserve their original type
var formats = mf.getFormatsByArgumentIndex();
@@ -764,6 +763,97 @@ public class StringUtils {
return mf.format(args2);
}
+ /**
+ * Formats a string using printf-style format specifiers.
+ *
+ * <p>
+ * This method provides printf-style formatting similar to C's <c>printf()</c> function and Java's
+ * {@link String#format(String, Object...)}. It uses the same format specifier syntax.
+ *
+ * <p>
+ * This is the primary formatting method using printf-style syntax (e.g., <js>"%s"</js>, <js>"%d"</js>, <js>"%f"</js>).
+ * For MessageFormat-style formatting, use {@link #mformat(String, Object...)}.
+ *
+ * <h5 class='section'>Format Specifiers:</h5>
+ * <ul>
+ * <li><b>%s</b> - String</li>
+ * <li><b>%d</b> - Decimal integer</li>
+ * <li><b>%f</b> - Floating point</li>
+ * <li><b>%x</b> - Hexadecimal (lowercase)</li>
+ * <li><b>%X</b> - Hexadecimal (uppercase)</li>
+ * <li><b>%o</b> - Octal</li>
+ * <li><b>%b</b> - Boolean</li>
+ * <li><b>%c</b> - Character</li>
+ * <li><b>%e</b> - Scientific notation (lowercase)</li>
+ * <li><b>%E</b> - Scientific notation (uppercase)</li>
+ * <li><b>%g</b> - General format (lowercase)</li>
+ * <li><b>%G</b> - General format (uppercase)</li>
+ * <li><b>%n</b> - Platform-specific line separator</li>
+ * <li><b>%%</b> - Literal percent sign</li>
+ * </ul>
+ *
+ * <h5 class='section'>Format Specifier Syntax:</h5>
+ * <p>
+ * Format specifiers follow this pattern: <c>%[argument_index$][flags][width][.precision]conversion</c>
+ * </p>
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Basic string and number formatting</jc>
+ * format(<js>"Hello %s, you have %d items"</js>, <js>"John"</js>, 5);
+ * <jc>// Returns: "Hello John, you have 5 items"</jc>
+ *
+ * <jc>// Floating point with precision</jc>
+ * format(<js>"Price: $%.2f"</js>, 19.99);
+ * <jc>// Returns: "Price: $19.99"</jc>
+ *
+ * <jc>// Width and alignment</jc>
+ * format(<js>"Name: %-20s Age: %3d"</js>, <js>"John"</js>, 25);
+ * <jc>// Returns: "Name: John Age: 25"</jc>
+ *
+ * <jc>// Hexadecimal</jc>
+ * format(<js>"Color: #%06X"</js>, 0xFF5733);
+ * <jc>// Returns: "Color: #FF5733"</jc>
+ *
+ * <jc>// Scientific notation</jc>
+ * format(<js>"Value: %.2e"</js>, 1234567.0);
+ * <jc>// Returns: "Value: 1.23e+06"</jc>
+ *
+ * <jc>// Argument index (reuse arguments)</jc>
+ * format(<js>"%1$s loves %2$s, and %1$s also loves %3$s"</js>, <js>"Alice"</js>, <js>"Bob"</js>, <js>"Charlie"</js>);
+ * <jc>// Returns: "Alice loves Bob, and Alice also loves Charlie"</jc>
+ * </p>
+ *
+ * <h5 class='section'>Comparison with mformat():</h5>
+ * <p class='bjava'>
+ * <jc>// Printf style (this method)</jc>
+ * format(<js>"Hello %s, you have %d items"</js>, <js>"John"</js>, 5);
+ *
+ * <jc>// MessageFormat style</jc>
+ * mformat(<js>"Hello {0}, you have {1} items"</js>, <js>"John"</js>, 5);
+ * </p>
+ *
+ * <h5 class='section'>Null Handling:</h5>
+ * <p>
+ * Null arguments are formatted as the string <js>"null"</js> for string conversions,
+ * or cause a {@link NullPointerException} for numeric conversions (consistent with {@link String#format(String, Object...)}).
+ * </p>
+ *
+ * @param pattern The printf-style format string.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @throws java.util.IllegalFormatException If the format string is invalid or arguments don't match the format specifiers.
+ * @see String#format(String, Object...)
+ * @see #mformat(String, Object...) for MessageFormat-style formatting
+ */
+ public static String format(String pattern, Object...args) {
+ if (pattern == null)
+ return null;
+ if (args == null || args.length == 0)
+ return pattern;
+ return String.format(pattern, args);
+ }
+
/**
* Converts a hexadecimal character string to a byte array.
*
@@ -1716,7 +1806,7 @@ public class StringUtils {
// Remove spaces and hyphens
var cleaned = str.replaceAll("[\\s\\-]", "");
// Must be all digits and 13-19 digits long
- if (!cleaned.matches("^\\d{13,19}$"))
+ if (! cleaned.matches("^\\d{13,19}$"))
return false;
// Apply Luhn algorithm
var sum = 0;
@@ -1729,7 +1819,7 @@ public class StringUtils {
digit = (digit % 10) + 1;
}
sum += digit;
- alternate = !alternate;
+ alternate = ! alternate;
}
return (sum % 10) == 0;
}
@@ -1845,7 +1935,7 @@ public class StringUtils {
return false;
try {
// Try IPv4 first
- if (ip.contains(".") && !ip.contains(":")) {
+ if (ip.contains(".") && ! ip.contains(":")) {
var parts = ip.split("\\.");
if (parts.length != 4)
return false;
@@ -1896,12 +1986,12 @@ public class StringUtils {
public static boolean isValidMacAddress(String mac) {
if (isEmpty(mac))
return false;
-
+
// Remove separators and check if it's 12 hex digits
var cleaned = mac.replaceAll("[:-]", "").toUpperCase();
if (cleaned.length() != 12)
return false;
-
+
// Check if all characters are valid hex digits
return cleaned.matches("^[0-9A-F]{12}$");
}
@@ -1933,41 +2023,41 @@ public class StringUtils {
public static boolean isValidHostname(String hostname) {
if (isEmpty(hostname))
return false;
-
+
// Cannot start or end with a dot
if (hostname.startsWith(".") || hostname.endsWith("."))
return false;
-
+
// Total length cannot exceed 253 characters
if (hostname.length() > 253)
return false;
-
+
// Split by dots (use -1 to preserve trailing empty strings)
var labels = hostname.split("\\.", -1);
-
+
// Must have at least one label
if (labels.length == 0)
return false;
-
+
// Check each label
for (var label : labels) {
// Label cannot be empty
if (label.isEmpty())
return false;
-
+
// Label cannot exceed 63 characters
if (label.length() > 63)
return false;
-
+
// Label cannot start or end with hyphen
if (label.startsWith("-") || label.endsWith("-"))
return false;
-
+
// Label can only contain letters, digits, and hyphens
- if (!label.matches("^[a-zA-Z0-9-]+$"))
+ if (! label.matches("^[a-zA-Z0-9-]+$"))
return false;
}
-
+
return true;
}
@@ -1991,14 +2081,14 @@ public class StringUtils {
public static int wordCount(String str) {
if (isEmpty(str))
return 0;
-
+
var count = 0;
var inWord = false;
-
+
for (var i = 0; i < str.length(); i++) {
var c = str.charAt(i);
if (Character.isLetterOrDigit(c) || c == '_') {
- if (!inWord) {
+ if (! inWord) {
count++;
inWord = true;
}
@@ -2006,7 +2096,7 @@ public class StringUtils {
inWord = false;
}
}
-
+
return count;
}
@@ -2029,7 +2119,7 @@ public class StringUtils {
public static int lineCount(String str) {
if (isEmpty(str))
return 0;
-
+
var count = 1; // At least one line
for (var i = 0; i < str.length(); i++) {
var c = str.charAt(i);
@@ -2043,7 +2133,7 @@ public class StringUtils {
count++;
}
}
-
+
return count;
}
@@ -2066,11 +2156,11 @@ public class StringUtils {
public static char mostFrequentChar(String str) {
if (isEmpty(str))
return '\0';
-
+
var charCounts = new int[Character.MAX_VALUE + 1];
var maxCount = 0;
var maxChar = '\0';
-
+
// Count occurrences of each character
for (var i = 0; i < str.length(); i++) {
var c = str.charAt(i);
@@ -2080,7 +2170,7 @@ public class StringUtils {
maxChar = c;
}
}
-
+
return maxChar;
}
@@ -2106,26 +2196,26 @@ public class StringUtils {
public static double entropy(String str) {
if (isEmpty(str))
return 0.0;
-
+
var length = str.length();
if (length == 0)
return 0.0;
-
+
// Count character frequencies
var charCounts = new int[Character.MAX_VALUE + 1];
for (var i = 0; i < length; i++) {
charCounts[str.charAt(i)]++;
}
-
+
// Calculate entropy
var entropy = 0.0;
for (var count : charCounts) {
if (count > 0) {
- var probability = (double) count / length;
+ var probability = (double)count / length;
entropy -= probability * (Math.log(probability) / Math.log(2.0));
}
}
-
+
return entropy;
}
@@ -2152,11 +2242,11 @@ public class StringUtils {
public static double readabilityScore(String str) {
if (isEmpty(str))
return 0.0;
-
+
var words = extractWords(str);
if (words.isEmpty())
return 0.0;
-
+
// Count sentences (ending with . ! ?)
var sentenceCount = 0;
for (var i = 0; i < str.length(); i++) {
@@ -2167,22 +2257,22 @@ public class StringUtils {
}
if (sentenceCount == 0)
sentenceCount = 1; // At least one sentence
-
+
// Calculate average words per sentence
- var avgWordsPerSentence = (double) words.size() / sentenceCount;
-
+ var avgWordsPerSentence = (double)words.size() / sentenceCount;
+
// Estimate average syllables per word (simplified: count vowel groups)
var totalSyllables = 0;
for (var word : words) {
totalSyllables += estimateSyllables(word);
}
- var avgSyllablesPerWord = (double) totalSyllables / words.size();
-
+ var avgSyllablesPerWord = (double)totalSyllables / words.size();
+
// Simplified Flesch Reading Ease formula
// Score = 206.835 - (1.015 * ASL) - (84.6 * ASW)
// Where ASL = average sentence length (words), ASW = average syllables per word
var score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
-
+
// Clamp to 0-100 range
return Math.max(0.0, Math.min(100.0, score));
}
@@ -2193,26 +2283,26 @@ public class StringUtils {
private static int estimateSyllables(String word) {
if (word == null || word.isEmpty())
return 1;
-
+
var lower = word.toLowerCase();
var count = 0;
var prevWasVowel = false;
-
+
for (var i = 0; i < lower.length(); i++) {
var c = lower.charAt(i);
var isVowel = (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y');
-
- if (isVowel && !prevWasVowel) {
+
+ if (isVowel && ! prevWasVowel) {
count++;
}
prevWasVowel = isVowel;
}
-
+
// Handle silent 'e' at the end
if (lower.endsWith("e") && count > 1) {
count--;
}
-
+
// At least one syllable
return Math.max(1, count);
}
@@ -2422,7 +2512,7 @@ public class StringUtils {
* @param args The map containing the named argument values.
* @return The formatted string with placeholders replaced, or the original template if args is null or empty.
*/
- public static String formatWithNamedArgs(String template, Map<String, Object> args) {
+ public static String formatWithNamedArgs(String template, Map<String,Object> args) {
if (template == null)
return null;
if (args == null || args.isEmpty())
@@ -2448,16 +2538,16 @@ public class StringUtils {
* @param variables The map containing the variable values.
* @return The interpolated string with variables replaced, or the original template if variables is null or empty.
*/
- public static String interpolate(String template, Map<String, Object> variables) {
+ public static String interpolate(String template, Map<String,Object> variables) {
if (template == null)
return null;
if (variables == null || variables.isEmpty())
return template;
-
+
var result = new StringBuilder();
var i = 0;
var length = template.length();
-
+
while (i < length) {
var dollarIndex = template.indexOf("${", i);
if (dollarIndex == -1) {
@@ -2465,10 +2555,10 @@ public class StringUtils {
result.append(template.substring(i));
break;
}
-
+
// Append text before the variable
result.append(template.substring(i, dollarIndex));
-
+
// Find the closing brace
var braceIndex = template.indexOf('}', dollarIndex + 2);
if (braceIndex == -1) {
@@ -2476,11 +2566,11 @@ public class StringUtils {
result.append(template.substring(dollarIndex));
break;
}
-
+
// Extract variable name
var varName = template.substring(dollarIndex + 2, braceIndex);
var value = variables.get(varName);
-
+
if (variables.containsKey(varName)) {
// Variable exists in map (even if null)
result.append(value != null ? value.toString() : "null");
@@ -2488,10 +2578,10 @@ public class StringUtils {
// Variable not found, keep the original placeholder
result.append("${").append(varName).append("}");
}
-
+
i = braceIndex + 1;
}
-
+
return result.toString();
}
@@ -2519,25 +2609,23 @@ public class StringUtils {
return word;
if (count == 1)
return word;
-
+
var lower = word.toLowerCase();
var length = word.length();
-
+
// Words ending in s, x, z, ch, sh -> add "es"
- if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") ||
- lower.endsWith("ch") || lower.endsWith("sh")) {
+ if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
return word + "es";
}
-
+
// Words ending in "y" preceded by a consonant -> replace "y" with "ies"
if (length > 1 && lower.endsWith("y")) {
var secondLast = lower.charAt(length - 2);
- if (secondLast != 'a' && secondLast != 'e' && secondLast != 'i' &&
- secondLast != 'o' && secondLast != 'u') {
+ if (secondLast != 'a' && secondLast != 'e' && secondLast != 'i' && secondLast != 'o' && secondLast != 'u') {
return word.substring(0, length - 1) + "ies";
}
}
-
+
// Words ending in "f" or "fe" -> replace with "ves" (basic rule)
if (lower.endsWith("f")) {
return word.substring(0, length - 1) + "ves";
@@ -2545,7 +2633,7 @@ public class StringUtils {
if (lower.endsWith("fe")) {
return word.substring(0, length - 2) + "ves";
}
-
+
// Default: add "s"
return word + "s";
}
@@ -2569,7 +2657,7 @@ public class StringUtils {
public static String ordinal(int number) {
var abs = Math.abs(number);
var suffix = "th";
-
+
// Special cases for 11, 12, 13 (all use "th")
if (abs % 100 != 11 && abs % 100 != 12 && abs % 100 != 13) {
var lastDigit = abs % 10;
@@ -2580,7 +2668,7 @@ public class StringUtils {
else if (lastDigit == 3)
suffix = "rd";
}
-
+
return number + suffix;
}
@@ -2672,12 +2760,7 @@ public class StringUtils {
if (str == null)
return null;
// Must unescape & last to avoid interfering with other entities
- return str.replace("<", "<")
- .replace(">", ">")
- .replace(""", "\"")
- .replace("'", "'")
- .replace("'", "'")
- .replace("&", "&");
+ return str.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("'", "'").replace("&", "&");
}
/**
@@ -2745,11 +2828,7 @@ public class StringUtils {
if (str == null)
return null;
// Must unescape & last to avoid interfering with other entities
- return str.replace("<", "<")
- .replace(">", ">")
- .replace(""", "\"")
- .replace("'", "'")
- .replace("&", "&");
+ return str.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&");
}
/**
@@ -2793,20 +2872,8 @@ public class StringUtils {
if (str == null)
return null;
// Escape regex special characters: . * + ? ^ $ { } ( ) [ ] | \
- return str.replace("\\", "\\\\")
- .replace(".", "\\.")
- .replace("*", "\\*")
- .replace("+", "\\+")
- .replace("?", "\\?")
- .replace("^", "\\^")
- .replace("$", "\\$")
- .replace("{", "\\{")
- .replace("}", "\\}")
- .replace("(", "\\(")
- .replace(")", "\\)")
- .replace("[", "\\[")
- .replace("]", "\\]")
- .replace("|", "\\|");
+ return str.replace("\\", "\\\\").replace(".", "\\.").replace("*", "\\*").replace("+", "\\+").replace("?", "\\?").replace("^", "\\^").replace("$", "\\$").replace("{", "\\{").replace("}", "\\}")
+ .replace("(", "\\(").replace(")", "\\)").replace("[", "\\[").replace("]", "\\]").replace("|", "\\|");
}
/**
@@ -2883,16 +2950,16 @@ public class StringUtils {
return -1;
if (str2 == null)
return 1;
-
+
var len1 = str1.length();
var len2 = str2.length();
var i1 = 0;
var i2 = 0;
-
+
while (i1 < len1 && i2 < len2) {
var c1 = str1.charAt(i1);
var c2 = str2.charAt(i2);
-
+
// If both are digits, compare numerically
if (Character.isDigit(c1) && Character.isDigit(c2)) {
// Skip leading zeros
@@ -2900,7 +2967,7 @@ public class StringUtils {
i1++;
while (i2 < len2 && str2.charAt(i2) == '0')
i2++;
-
+
// Find end of number sequences
var end1 = i1;
var end2 = i2;
@@ -2908,13 +2975,13 @@ public class StringUtils {
end1++;
while (end2 < len2 && Character.isDigit(str2.charAt(end2)))
end2++;
-
+
// Compare lengths first (longer number is larger)
var lenNum1 = end1 - i1;
var lenNum2 = end2 - i2;
if (lenNum1 != lenNum2)
return lenNum1 - lenNum2;
-
+
// Same length, compare digit by digit
for (var j = 0; j < lenNum1; j++) {
var d1 = str1.charAt(i1 + j);
@@ -2922,7 +2989,7 @@ public class StringUtils {
if (d1 != d2)
return d1 - d2;
}
-
+
i1 = end1;
i2 = end2;
} else {
@@ -2934,7 +3001,7 @@ public class StringUtils {
i2++;
}
}
-
+
return len1 - len2;
}
@@ -2960,18 +3027,18 @@ public class StringUtils {
str1 = "";
if (str2 == null)
str2 = "";
-
+
var len1 = str1.length();
var len2 = str2.length();
-
+
// Use dynamic programming with optimized space (only need previous row)
var prev = new int[len2 + 1];
var curr = new int[len2 + 1];
-
+
// Initialize first row
for (var j = 0; j <= len2; j++)
prev[j] = j;
-
+
for (var i = 1; i <= len1; i++) {
curr[0] = i;
for (var j = 1; j <= len2; j++) {
@@ -2986,7 +3053,7 @@ public class StringUtils {
prev = curr;
curr = temp;
}
-
+
return prev[len2];
}
@@ -3012,14 +3079,14 @@ public class StringUtils {
str1 = "";
if (str2 == null)
str2 = "";
-
+
if (str1.equals(str2))
return 1.0;
-
+
var maxLen = Math.max(str1.length(), str2.length());
if (maxLen == 0)
return 1.0;
-
+
var distance = levenshteinDistance(str1, str2);
return 1.0 - ((double)distance / maxLen);
}
@@ -3959,7 +4026,7 @@ public class StringUtils {
* @param remove The substrings to remove.
* @return The string with all specified substrings removed, or <jk>null</jk> if input is <jk>null</jk>.
*/
- public static String removeAll(String str, String... remove) {
+ public static String removeAll(String str, String...remove) {
if (str == null)
return null;
if (isEmpty(str) || remove == null || remove.length == 0)
@@ -4140,14 +4207,14 @@ public class StringUtils {
// Split into words first, then combine words that fit
var words = line.split(" +"); // Split on one or more spaces
var currentLine = new StringBuilder();
-
+
for (var word : words) {
if (word.isEmpty())
continue;
-
+
var wordLength = word.length();
var currentLength = currentLine.length();
-
+
if (currentLength == 0) {
// First word on line
// Only break single words if there are multiple words in the input
@@ -4205,7 +4272,7 @@ public class StringUtils {
}
}
}
-
+
// Append any remaining line
if (currentLine.length() > 0) {
if (result.length() > 0)
@@ -5081,11 +5148,11 @@ public class StringUtils {
* @param trimKeys If <jk>true</jk>, trims whitespace from keys and values.
* @return A map containing the parsed key-value pairs, or an empty map if the string is <jk>null</jk> or empty.
*/
- public static Map<String, String> parseMap(String str, char keyValueDelimiter, char entryDelimiter, boolean trimKeys) {
- var result = new LinkedHashMap<String, String>();
+ public static Map<String,String> parseMap(String str, char keyValueDelimiter, char entryDelimiter, boolean trimKeys) {
+ var result = new LinkedHashMap<String,String>();
if (isEmpty(str))
return result;
-
+
var entries = split(str, entryDelimiter);
for (var entry : entries) {
if (isEmpty(entry))
@@ -5128,7 +5195,7 @@ public class StringUtils {
public static List<String> extractNumbers(String str) {
if (isEmpty(str))
return Collections.emptyList();
-
+
var result = new ArrayList<String>();
var pattern = Pattern.compile("\\d+(?:\\.\\d+)?");
var matcher = pattern.matcher(str);
@@ -5156,7 +5223,7 @@ public class StringUtils {
public static List<String> extractEmails(String str) {
if (isEmpty(str))
return Collections.emptyList();
-
+
var result = new ArrayList<String>();
// Email regex pattern (same as isEmail but without ^ and $ anchors)
var pattern = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
@@ -5185,7 +5252,7 @@ public class StringUtils {
public static List<String> extractUrls(String str) {
if (isEmpty(str))
return Collections.emptyList();
-
+
var result = new ArrayList<String>();
// Basic URL pattern: protocol://domain/path
var pattern = Pattern.compile("(?:https?|ftp)://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+", Pattern.CASE_INSENSITIVE);
@@ -5214,7 +5281,7 @@ public class StringUtils {
public static List<String> extractWords(String str) {
if (isEmpty(str))
return Collections.emptyList();
-
+
var result = new ArrayList<String>();
// Word pattern: sequence of word characters (letters, digits, underscore)
var pattern = Pattern.compile("\\w+");
@@ -5247,7 +5314,7 @@ public class StringUtils {
public static List<String> extractBetween(String str, String start, String end) {
if (isEmpty(str) || isEmpty(start) || isEmpty(end))
return Collections.emptyList();
-
+
var result = new ArrayList<String>();
var startIndex = 0;
while (true) {
@@ -5292,7 +5359,7 @@ public class StringUtils {
return str;
if (fromChars.length() != toChars.length())
throw new IllegalArgumentException("fromChars and toChars must have the same length");
-
+
var sb = new StringBuilder(str.length());
for (var i = 0; i < str.length(); i++) {
var c = str.charAt(i);
@@ -5325,16 +5392,16 @@ public class StringUtils {
public static String soundex(String str) {
if (isEmpty(str))
return null;
-
+
var upper = str.toUpperCase();
var result = new StringBuilder(4);
result.append(upper.charAt(0));
-
+
// Soundex mapping: 0 = AEIOUHWY, 1 = BFPV, 2 = CGJKQSXZ, 3 = DT, 4 = L, 5 = MN, 6 = R
// H/W/Y don't get codes but don't break sequences either
// Initialize lastCode to a value that won't match any real code
var lastCode = '\0';
-
+
for (var i = 1; i < upper.length() && result.length() < 4; i++) {
var c = upper.charAt(i);
var code = getSoundexCode(c);
@@ -5349,12 +5416,12 @@ public class StringUtils {
}
// If code == lastCode, skip it (consecutive same codes)
}
-
+
// Pad with zeros if needed
while (result.length() < 4) {
result.append('0');
}
-
+
return result.toString();
}
@@ -5399,15 +5466,15 @@ public class StringUtils {
public static String metaphone(String str) {
if (isEmpty(str))
return null;
-
+
var upper = str.toUpperCase().replaceAll("[^A-Z]", "");
if (upper.isEmpty())
return "";
-
+
var result = new StringBuilder();
var i = 0;
var len = upper.length();
-
+
// Handle initial characters
if (upper.startsWith("KN") || upper.startsWith("GN") || upper.startsWith("PN") || upper.startsWith("AE") || upper.startsWith("WR")) {
i = 1;
@@ -5418,20 +5485,20 @@ public class StringUtils {
result.append('W');
i = 2;
}
-
+
// Process remaining characters
while (i < len && result.length() < 4) {
var c = upper.charAt(i);
var prev = i > 0 ? upper.charAt(i - 1) : '\0';
var next = i < len - 1 ? upper.charAt(i + 1) : '\0';
var next2 = i < len - 2 ? upper.charAt(i + 2) : '\0';
-
+
// Skip duplicates (except C)
if (c == prev && c != 'C') {
i++;
continue;
}
-
+
switch (c) {
case 'B':
if (prev != 'M' || next != '\0')
@@ -5478,7 +5545,7 @@ public class StringUtils {
}
break;
case 'H':
- if (!isVowel(prev) || !isVowel(next))
+ if (! isVowel(prev) || ! isVowel(next))
result.append('H');
break;
case 'K':
@@ -5534,10 +5601,12 @@ public class StringUtils {
case 'Z':
result.append('S');
break;
+ default:
+ break;
}
i++;
}
-
+
return result.length() > 0 ? result.toString() : upper.substring(0, Math.min(1, upper.length()));
}
@@ -5567,17 +5636,17 @@ public class StringUtils {
public static String[] doubleMetaphone(String str) {
if (isEmpty(str))
return null;
-
+
// For simplicity, return the same code for both primary and alternate
// A full Double Metaphone implementation would be much more complex
var primary = metaphone(str);
if (primary == null)
return null;
-
+
// Generate alternate code (simplified - full implementation would have different rules)
var alternate = primary;
-
- return new String[]{primary, alternate};
+
+ return new String[] { primary, alternate };
}
/**
@@ -5620,10 +5689,10 @@ public class StringUtils {
public static String removeAccents(String str) {
if (str == null)
return null;
-
+
// Normalize to NFD (decomposed form)
var normalized = Normalizer.normalize(str, Normalizer.Form.NFD);
-
+
// Remove combining diacritical marks (Unicode category Mn)
var sb = new StringBuilder(normalized.length());
for (var i = 0; i < normalized.length(); i++) {
@@ -5634,7 +5703,7 @@ public class StringUtils {
sb.append(c);
}
}
-
+
return sb.toString();
}
@@ -6988,9 +7057,7 @@ public class StringUtils {
return null; // NOSONAR - Intentional.
if (predicate == null)
return new String[0];
- return Arrays.stream(array)
- .filter(predicate)
- .toArray(String[]::new);
+ return Arrays.stream(array).filter(predicate).toArray(String[]::new);
}
/**
@@ -7014,14 +7081,12 @@ public class StringUtils {
* @param mapper The function to apply to each element. Can be <jk>null</jk>.
* @return A new array with the mapped elements, or <jk>null</jk> if the array was <jk>null</jk>.
*/
- public static String[] mapped(String[] array, Function<String, String> mapper) {
+ public static String[] mapped(String[] array, Function<String,String> mapper) {
if (array == null)
return null; // NOSONAR - Intentional.
if (mapper == null)
return Arrays.copyOf(array, array.length);
- return Arrays.stream(array)
- .map(mapper)
- .toArray(String[]::new);
+ return Arrays.stream(array).map(mapper).toArray(String[]::new);
}
/**
@@ -7044,9 +7109,7 @@ public class StringUtils {
public static String[] distinct(String[] array) {
if (array == null)
return null; // NOSONAR - Intentional.
- return Arrays.stream(array)
- .collect(Collectors.toCollection(LinkedHashSet::new))
- .toArray(new String[0]);
+ return Arrays.stream(array).collect(Collectors.toCollection(LinkedHashSet::new)).toArray(new String[0]);
}
/**
@@ -7365,7 +7428,7 @@ public class StringUtils {
}
// Suggest interning for medium-length strings that might be repeated
- if (length > 10 && length < 100 && !isInterned(str)) {
+ if (length > 10 && length < 100 && ! isInterned(str)) {
suggestions.add("Consider interning if this string is used frequently");
}
diff --git a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
index c2d9646942..71eee98710 100644
--- a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
+++ b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
@@ -406,6 +406,59 @@ public class Utils {
return () -> mformat(pattern, args);
}
+ /**
+ * Shortcut for calling {@link StringUtils#format(String, Object...)}.
+ *
+ * <p>
+ * This method provides a convenient shorthand for printf-style string formatting.
+ *
+ * <h5 class='section'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Basic formatting</jc>
+ * f(<js>"Hello %s, you have %d items"</js>, <js>"John"</js>, 5);
+ * <jc>// Returns: "Hello John, you have 5 items"</jc>
+ *
+ * <jc>// Floating point</jc>
+ * f(<js>"Price: $%.2f"</js>, 19.99);
+ * <jc>// Returns: "Price: $19.99"</jc>
+ * </p>
+ *
+ * @param pattern The printf-style format string.
+ * @param args The arguments to format.
+ * @return The formatted string.
+ * @see StringUtils#format(String, Object...)
+ * @see #mf(String, Object...) for MessageFormat-style formatting
+ */
+ public static String f(String pattern, Object...args) {
+ return format(pattern, args);
+ }
+
+ /**
+ * Creates a formatted string supplier with printf-style format arguments for lazy evaluation.
+ *
+ * <p>This method returns a {@link Supplier} that formats the string pattern with the provided arguments
+ * only when the supplier's {@code get()} method is called. This is useful for expensive string formatting
+ * operations that may not always be needed, such as error messages in assertions.</p>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Lazy evaluation - string is only formatted if assertion fails</jc>
+ * assertTrue(condition, fs(<js>"Expected %s but got %s"</js>, expected, actual));
+ *
+ * <jc>// Can be used anywhere a Supplier<String> is expected</jc>
+ * Supplier<String> <jv>messageSupplier</jv> = fs(<js>"Processing item %d of %d"</js>, i, total);
+ * </p>
+ *
+ * @param pattern The printf-style format string using <js>%s</js>, <js>%d</js>, etc. placeholders.
+ * @param args The arguments to substitute into the pattern placeholders.
+ * @return A {@link Supplier} that will format the string when {@code get()} is called.
+ * @see StringUtils#format(String, Object...)
+ * @see #mfs(String, Object...) for MessageFormat-style formatting
+ */
+ public static Supplier<String> fs(String pattern, Object...args) {
+ return () -> format(pattern, args);
+ }
+
/**
* Calculates a hash code for the specified values.
*
diff --git a/juneau-utest/src/test/java/org/apache/juneau/common/utils/FormatString_Test.java b/juneau-utest/src/test/java/org/apache/juneau/common/utils/FormatString_Test.java
new file mode 100644
index 0000000000..a8a4445544
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/common/utils/FormatString_Test.java
@@ -0,0 +1,416 @@
+/*
+ * 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.juneau.common.utils;
+
+import static org.apache.juneau.common.utils.Utils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static java.util.stream.Collectors.*;
+
+import java.math.*;
+import java.text.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.common.function.*;
+import org.junit.jupiter.api.*;
+
+class FormatString_Test extends TestBase {
+
+ private static StringFormat fs(String pattern) {
+ return StringFormat.of(pattern);
+ }
+
+ private static String stringify(ThrowingSupplier<String> supplier) {
+ try {
+ return supplier.get();
+ } catch (Throwable t) {
+ return t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ }
+
+ private static void assertStringFormat(String pattern, Locale locale, Object... args) {
+ var expected = stringify(()->String.format(locale, pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertStringFormat(String pattern, Object... args) {
+ var expected = stringify(()->String.format(pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMessageFormat(String pattern, Locale locale, Object... args) {
+ var expected = stringify(()->new MessageFormat(pattern, locale).format(args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMessageFormat(String pattern, Object... args) {
+ var expected = stringify(()->MessageFormat.format(pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMixedFormat(String expected, String pattern, Locale locale, Object... args) {
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMixedFormat(String expected, String pattern, Object... args) {
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ //====================================================================================================
+ // MessageFormat tests
+ //====================================================================================================
+ @Test void a01_messageFormat() {
+ assertMessageFormat("Hello {0}", "John");
+ assertMessageFormat("Price: {0,number,currency}", 19.99);
+ assertMessageFormat("{0} has {1} items and {2} friends", "John", 5, 3);
+ assertMessageFormat("Hello {0} world", "John");
+ assertMessageFormat("Count: {0,number,integer}", 1234);
+ assertMessageFormat("Date: {0,date,short}", new Date(0));
+ assertMessageFormat("Time: {0,time,short}", new Date(0));
+ // Simple {0} with Date - uses DATE_FORMAT_CACHE for formatting
+ assertMessageFormat("Date: {0}", new Date(0));
+ assertMessageFormat("Value: {0}", (String)null);
+ assertMessageFormat("Name: {0}", "");
+ assertMessageFormat("Text: {0}\nNewline\tTab", "Hello");
+ assertMessageFormat("Unicode: {0} 中文", "Test");
+ assertMessageFormat("{0}{1}", "A", "B");
+ assertMessageFormat("{0} and {0} again", "Hello");
+ assertMessageFormat("Price: {0,number,currency}, Count: {1,number,integer}, Date: {2,date,short}", 19.99, 42, new java.util.Date());
+ assertMessageFormat("Price: {0,number,currency}", Locale.US, 19.99);
+ assertMessageFormat("Price: {0,number,currency}", Locale.FRANCE, 19.99);
+ assertMessageFormat("a '{0}' b");
+ assertMessageFormat("a ''{0}'' b", 1);
+ assertMessageFormat("'{0}'");
+ assertMessageFormat("''{0}''", 1);
+
+ // Errors
+ assertMessageFormat("Set: {{0}}", 50);
+ assertMessageFormat("Set: {{0}} and {{1}}", "A", "B");
+ assertMessageFormat("Hello {0}");
+ assertMessageFormat("{0} has {1} items and {2} friends", "John", 5);
+ assertMessageFormat("Hello {");
+ assertMessageFormat("Hello {0");
+ assertMessageFormat("Hello '");
+ assertMessageFormat("Hello 'x");
+ }
+
+ //====================================================================================================
+ // StringFormat (printf) tests
+ //====================================================================================================
+ @Test void a02_stringFormat() {
+ assertStringFormat("Hello %s", "John");
+ assertStringFormat("Price: $%.2f", 19.99);
+ assertStringFormat("Name: %-10s Age: %3d", "John", 25);
+ assertStringFormat("Color: #%06X", 0xFF5733);
+ assertStringFormat("Hello world");
+ assertStringFormat("Progress: %d%%", 50);
+ assertStringFormat("");
+ assertStringFormat("%1$s loves %2$s, and %1$s also loves %3$s", "Alice", "Bob", "Charlie");
+ assertStringFormat("Hello %1$s", "John");
+ assertStringFormat("Price: %1$.2f", 19.99);
+ assertStringFormat("Octal: %o", 64);
+ assertStringFormat("Octal: %o", 255);
+ assertStringFormat("Octal: %o", (Number)null);
+ assertStringFormat("Flag: %b", true);
+ assertStringFormat("Flag: %b", false);
+ assertStringFormat("Flag: %b", (Boolean)null);
+ // %B uppercase boolean formatting
+ assertStringFormat("Flag: %B", true);
+ assertStringFormat("Flag: %B", false);
+ assertStringFormat("Flag: %B", (Boolean)null); // Line 281: null -> "FALSE"
+ assertStringFormat("Flag: %B", "hello"); // Line 285: non-Boolean -> "TRUE"
+ assertStringFormat("Flag: %B", 42); // Line 285: non-Boolean -> "TRUE"
+ assertStringFormat("Char: %c", 'A');
+ assertStringFormat("Char: %c", "A");
+ assertStringFormat("Char: %c", (String)null);
+ assertStringFormat("Value: %.2e", 1234567.0);
+ assertStringFormat("Value: %.2e", (Number)null);
+ assertStringFormat("Number: %+10.2f", 19.99);
+ assertStringFormat("ID: %05d", 42);
+ assertStringFormat("Value: %d", (Number)null); // Line 304: null -> "null"
+ assertStringFormat("Value: %s", (String)null);
+ assertStringFormat("Name: %s", "");
+ assertStringFormat("%s%s", "A", "B");
+ assertStringFormat("Progress: %d%% Complete: %d%%", 50, 75);
+ assertStringFormat("%1$s and %1$s again", "Hello");
+ assertStringFormat("Hex: 0x%08X, Decimal: %+d, Float: %10.3f", 255, 42, 3.14159);
+ assertStringFormat("Price: %.2f", Locale.US, 19.99);
+ assertStringFormat("Price: %.2f", Locale.FRANCE, 19.99);
+ assertStringFormat("Value: %s", (Object)null);
+ assertStringFormat("Int: %d", 42);
+ assertStringFormat("Long: %d", 1234567890L);
+ assertStringFormat("Byte: %d", (byte)127);
+ assertStringFormat("Short: %d", (short)32767);
+ assertStringFormat("Int: %d", Locale.FRANCE, 1234);
+ assertStringFormat("Long: %d", Locale.GERMANY, 1234567L);
+ assertStringFormat("Value: %d", "not-a-number");
+ assertStringFormat("Hex: %x", 255); // Line 328: Integer -> Integer.toHexString()
+ assertStringFormat("Hex: %x", 255L);
+ assertStringFormat("Hex: %x", (byte)255);
+ assertStringFormat("Hex: %x", (Number)null); // Line 324: null -> "null"
+ assertStringFormat("Hex: %X", 255);
+ assertStringFormat("Hex: %X", 0xABCL);
+ assertStringFormat("Hex: %X", (short)255);
+ assertStringFormat("Hex: %X", (Number)null); // Line 337: null -> "null"
+ assertStringFormat("Octal: %o", 255L);
+ assertStringFormat("Octal: %o", (byte)64);
+ assertStringFormat("Value: %b", "hello");
+ assertStringFormat("Value: %b", 42);
+ assertStringFormat("Char: %c", 65);
+ assertStringFormat("Char: %c", 65L);
+ assertStringFormat("Char: %c", "X");
+ assertStringFormat("Char: %C", (Character)null); // Line 376: null -> "null"
+ assertStringFormat("Char: %C", 66); // Line 382-383: Number (Integer) -> Character.toUpperCase((char)o2.intValue())
+ assertStringFormat("Char: %C", 66L); // Line 382-383: Number (Long) -> Character.toUpperCase((char)o2.intValue())
+ assertStringFormat("Float: %f", 3.14f);
+ assertStringFormat("Double: %f", 3.14159);
+ assertStringFormat("Float: %f", Locale.FRANCE, 3.14f);
+ assertStringFormat("Double: %f", Locale.GERMANY, 1234.56);
+ assertStringFormat("Value: %f", (Number)null); // Line 389: null -> "null"
+ assertStringFormat("Value: %f", "not-a-number");
+ assertStringFormat("Value: %.2e", 1234.56);
+ assertStringFormat("Value: %S", "hello");
+ assertStringFormat("Value: %S", (String)null); // Line 297: null -> "null"
+ assertStringFormat("Value: %B", true);
+ assertStringFormat("Char: %C", 'a');
+ assertStringFormat("Float: %F", 3.14);
+
+ // Errors
+ assertStringFormat("Hello %s");
+ assertStringFormat("Hello %s and %s", "John");
+ assertStringFormat("Hello %");
+ assertStringFormat("Hello %s and %", "John");
+ assertStringFormat("Hello %x$s", "John");
+ }
+
+ //====================================================================================================
+ // Mixed format tests
+ //====================================================================================================
+ @Test void a03_mixedFormat() {
+ assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
+ assertMixedFormat("User Alice has admin and 10 items", "User {0} has %s and {2} items", "Alice", "admin", 10);
+ assertMixedFormat("Alice loves Bob, and Alice also loves Charlie", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob", "Charlie");
+ assertMixedFormat("Alice has 5 items, Bob has 3 items, total: 8", "{0} has %d items, {2} has %d items, total: %d", "Alice", 5, "Bob", 3, 8);
+ assertMixedFormat("Alice Bob Charlie", "{0} %2$s {2}", "Alice", "Bob", "Charlie");
+ assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
+ assertMixedFormat("A B B D C", "{0} %s {1} %s {2}", "A", "B", "C", "D");
+ assertMixedFormat("ABB", "{0}%s{1}", "A", "B", "C");
+ assertMixedFormat("Hello and Hello are the same", "{0} and %1$s are the same", "Hello");
+
+ // Errors
+ assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "Hello {0} and %s", "John");
+ assertMixedFormat("John has 5 items and {2} friends", "{0} has %d items and {2} friends", "John", 5);
+ assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob");
+ }
+
+ //====================================================================================================
+ // Supported but deviates from MessageFormat/String.format
+ //====================================================================================================
+ @Test void a04_supportedButDeviatesFromMessageFormat() {
+ // {} is not supported by MessageFormat, only by StringFormat as an extension
+ assertMixedFormat("Hello John world", "Hello {} world", "John");
+ assertMixedFormat("A B C", "{} {} {}", "A", "B", "C");
+ // BigDecimal with %d - String.format throws exception, but our optimized code handles it
+ assertMixedFormat("Number: 42", "Number: %d", new BigDecimal("42"));
+ // MessageFormat throws NullPointerException when locale is null, but StringFormat handles it
+ // So we test StringFormat's behavior directly instead of comparing with MessageFormat
+ assertMixedFormat("Price: $19.99", "Price: {0,number,currency}", (Locale)null, 19.99);
+ }
+
+ //====================================================================================================
+ // Error handling
+ //====================================================================================================
+ @Test void a05_errors() {
+ assertThrows(IllegalArgumentException.class, () -> new StringFormat(null));
+ assertThrows(IllegalArgumentException.class, () -> fs(null));
+ }
+
+ @Test void a06_caching() {
+ // Should return the same instance due to caching
+ assertSame(fs("Hello {0}"), fs("Hello {0}"));
+
+ // Different patterns should return different instances
+ assertNotSame(fs("Hello {0}"), fs("Hello %s"));
+
+ // Constructor doesn't use cache, so instances should be different
+ var fmt1 = new StringFormat("Hello {0}");
+ var fmt2 = new StringFormat("Hello {0}");
+ assertNotSame(fmt1, fmt2);
+ assertEquals(fmt1, fmt2); // But they should be equal
+ }
+
+ @Test void a07_equalsAndHashCode() {
+ var fmt1 = StringFormat.of("Hello {0}");
+ var fmt2 = StringFormat.of("Hello {0}");
+ var fmt3 = StringFormat.of("Hello %s");
+
+ assertEquals(fmt1, fmt2);
+ assertNotEquals(fmt1, fmt3);
+ assertEquals(fmt1.hashCode(), fmt2.hashCode());
+ }
+
+ @Test void a08_toString() {
+ assertEquals("Hello {0}", fs("Hello {0}").toString());
+ }
+
+ @Test void a09_toPattern() {
+ // Literal tokens
+ assertEquals("[L:Hello ]", fs("Hello ").toPattern());
+ assertEquals("[L:a ][L:{0}][L: b]", fs("a '{0}' b").toPattern()); // Single quotes don't escape MessageFormat
+
+ // MessageFormat tokens - simple (content == null) - Line 228: content == null branch
+ assertEquals("[L:Hello ][M:s0]", fs("Hello {0}").toPattern());
+ assertEquals("[L:Hello ][M:s0][L: ][M:s1]", fs("Hello {0} {1}").toPattern());
+
+ // MessageFormat tokens - complex (content != null) - Line 228: content != null branch
+ assertEquals("[L:Price: ][M:o0:{0,number,currency}]", fs("Price: {0,number,currency}").toPattern());
+ assertEquals("[L:Count: ][M:o0:{0,number,integer}]", fs("Count: {0,number,integer}").toPattern());
+ assertEquals("[L:Date: ][M:o0:{0,date,short}]", fs("Date: {0,date,short}").toPattern());
+
+ // StringFormat tokens - Line 406: StringFormatToken.toString()
+ assertEquals("[L:Hello ][S:s0:%s]", fs("Hello %s").toPattern()); // Simple format: 's'
+ assertEquals("[L:Number: ][S:d0:%d]", fs("Number: %d").toPattern()); // Simple format: 'd'
+ assertEquals("[L:Hex: ][S:x0:%x]", fs("Hex: %x").toPattern()); // Simple format: 'x'
+ assertEquals("[L:Float: ][S:z0:%.2f]", fs("Float: %.2f").toPattern()); // Complex format: 'z' (other)
+ assertEquals("[L:ID: ][S:z0:%05d]", fs("ID: %05d").toPattern()); // Complex format: 'z' (other)
+
+ // Mixed formats
+ assertEquals("[L:Hello ][M:s0][L:, you have ][S:d1:%d][L: items]", fs("Hello {0}, you have %d items").toPattern());
+ assertEquals("[L:Price: ][M:o0:{0,number,currency}][L: and ][S:s1:%s]", fs("Price: {0,number,currency} and %s").toPattern());
+
+ // Time conversions (2-character) - Line 529: 't' or 'T' handling
+ assertEquals("[L:Month: ][S:z0:%tm]", fs("Month: %tm").toPattern()); // %tm is 2-character time conversion
+ assertEquals("[L:Year: ][S:z0:%tY]", fs("Year: %tY").toPattern()); // %tY is 2-character time conversion
+ assertEquals("[L:Date: ][S:z0:%TD]", fs("Date: %TD").toPattern()); // %TD is 2-character time conversion
+ }
+
+ @Test void a10_veryLongPattern() {
+ var pattern = "Start: " + IntStream.range(0, 10).mapToObj(i -> "{" + i + "}").collect(joining(" ")) + " ";
+ var args = IntStream.range(0, 10).boxed().toArray();
+ assertMessageFormat(pattern, args);
+ }
+
+ @Test void a11_parseIndexErrors() {
+ assertThrows(IllegalArgumentException.class, () -> fs("Hello {abc}"));
+ }
+
+ @Test void a12_localeHandling() {
+ // Lines 259-260: Test locale null checks and default locale detection in StringFormatToken
+ // Line 259: var l = locale == null ? Locale.getDefault() : locale;
+ // Line 260: var dl = locale == null || locale.equals(Locale.getDefault());
+
+ // Test with null locale (covers locale == null on both lines)
+ assertStringFormat("Hello %s", (Locale)null, "John");
+ assertStringFormat("Number: %d", (Locale)null, 42);
+ assertStringFormat("Float: %.2f", (Locale)null, 3.14); // Use .2f for consistent formatting
+
+ // Test with default locale (covers locale.equals(Locale.getDefault()) on line 260)
+ assertStringFormat("Hello %s", Locale.getDefault(), "John");
+ assertStringFormat("Number: %d", Locale.getDefault(), 42);
+ assertStringFormat("Float: %.2f", Locale.getDefault(), 3.14); // Use .2f for consistent formatting
+
+ // Test with non-default locale (covers else branch on line 259 and false case on line 260)
+ assertStringFormat("Hello %s", Locale.FRANCE, "John");
+ assertStringFormat("Number: %d", Locale.GERMANY, 42);
+ assertStringFormat("Float: %.2f", Locale.JAPAN, 3.14); // Use .2f for consistent formatting
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringFormat_Test.java b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringFormat_Test.java
new file mode 100644
index 0000000000..ea6ec942d8
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringFormat_Test.java
@@ -0,0 +1,416 @@
+/*
+ * 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.juneau.common.utils;
+
+import static org.apache.juneau.common.utils.Utils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static java.util.stream.Collectors.*;
+
+import java.math.*;
+import java.text.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.common.function.*;
+import org.junit.jupiter.api.*;
+
+class StringFormat_Test extends TestBase {
+
+ private static StringFormat fs(String pattern) {
+ return StringFormat.of(pattern);
+ }
+
+ private static String stringify(ThrowingSupplier<String> supplier) {
+ try {
+ return supplier.get();
+ } catch (Throwable t) {
+ return t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ }
+
+ private static void assertStringFormat(String pattern, Locale locale, Object... args) {
+ var expected = stringify(()->String.format(locale, pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertStringFormat(String pattern, Object... args) {
+ var expected = stringify(()->String.format(pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMessageFormat(String pattern, Locale locale, Object... args) {
+ var expected = stringify(()->new MessageFormat(pattern, locale).format(args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMessageFormat(String pattern, Object... args) {
+ var expected = stringify(()->MessageFormat.format(pattern, args));
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMixedFormat(String expected, String pattern, Locale locale, Object... args) {
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(locale, args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ private static void assertMixedFormat(String expected, String pattern, Object... args) {
+ var actual = "";
+ var fmt = (StringFormat)null;
+ try {
+ var fmt2 = fs(pattern);
+ fmt = fmt2;
+ actual = stringify(()->fmt2.format(args));
+ } catch (Throwable t) {
+ actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
+ }
+ if (!expected.equals(actual)) {
+ System.out.println("Pattern: " + pattern);
+ var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
+ System.out.println("toPattern(): " + toPattern);
+ fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
+ }
+ }
+
+ //====================================================================================================
+ // MessageFormat tests
+ //====================================================================================================
+ @Test void a01_messageFormat() {
+ assertMessageFormat("Hello {0}", "John");
+ assertMessageFormat("Price: {0,number,currency}", 19.99);
+ assertMessageFormat("{0} has {1} items and {2} friends", "John", 5, 3);
+ assertMessageFormat("Hello {0} world", "John");
+ assertMessageFormat("Count: {0,number,integer}", 1234);
+ assertMessageFormat("Date: {0,date,short}", new Date(0));
+ assertMessageFormat("Time: {0,time,short}", new Date(0));
+ // Simple {0} with Date - uses DATE_FORMAT_CACHE for formatting
+ assertMessageFormat("Date: {0}", new Date(0));
+ assertMessageFormat("Value: {0}", (String)null);
+ assertMessageFormat("Name: {0}", "");
+ assertMessageFormat("Text: {0}\nNewline\tTab", "Hello");
+ assertMessageFormat("Unicode: {0} 中文", "Test");
+ assertMessageFormat("{0}{1}", "A", "B");
+ assertMessageFormat("{0} and {0} again", "Hello");
+ assertMessageFormat("Price: {0,number,currency}, Count: {1,number,integer}, Date: {2,date,short}", 19.99, 42, new java.util.Date());
+ assertMessageFormat("Price: {0,number,currency}", Locale.US, 19.99);
+ assertMessageFormat("Price: {0,number,currency}", Locale.FRANCE, 19.99);
+ assertMessageFormat("a '{0}' b");
+ assertMessageFormat("a ''{0}'' b", 1);
+ assertMessageFormat("'{0}'");
+ assertMessageFormat("''{0}''", 1);
+
+ // Errors
+ assertMessageFormat("Set: {{0}}", 50);
+ assertMessageFormat("Set: {{0}} and {{1}}", "A", "B");
+ assertMessageFormat("Hello {0}");
+ assertMessageFormat("{0} has {1} items and {2} friends", "John", 5);
+ assertMessageFormat("Hello {");
+ assertMessageFormat("Hello {0");
+ assertMessageFormat("Hello '");
+ assertMessageFormat("Hello 'x");
+ }
+
+ //====================================================================================================
+ // StringFormat (printf) tests
+ //====================================================================================================
+ @Test void a02_stringFormat() {
+ assertStringFormat("Hello %s", "John");
+ assertStringFormat("Price: $%.2f", 19.99);
+ assertStringFormat("Name: %-10s Age: %3d", "John", 25);
+ assertStringFormat("Color: #%06X", 0xFF5733);
+ assertStringFormat("Hello world");
+ assertStringFormat("Progress: %d%%", 50);
+ assertStringFormat("");
+ assertStringFormat("%1$s loves %2$s, and %1$s also loves %3$s", "Alice", "Bob", "Charlie");
+ assertStringFormat("Hello %1$s", "John");
+ assertStringFormat("Price: %1$.2f", 19.99);
+ assertStringFormat("Octal: %o", 64);
+ assertStringFormat("Octal: %o", 255);
+ assertStringFormat("Octal: %o", (Number)null);
+ assertStringFormat("Flag: %b", true);
+ assertStringFormat("Flag: %b", false);
+ assertStringFormat("Flag: %b", (Boolean)null);
+ // %B uppercase boolean formatting
+ assertStringFormat("Flag: %B", true);
+ assertStringFormat("Flag: %B", false);
+ assertStringFormat("Flag: %B", (Boolean)null); // Line 281: null -> "FALSE"
+ assertStringFormat("Flag: %B", "hello"); // Line 285: non-Boolean -> "TRUE"
+ assertStringFormat("Flag: %B", 42); // Line 285: non-Boolean -> "TRUE"
+ assertStringFormat("Char: %c", 'A');
+ assertStringFormat("Char: %c", "A");
+ assertStringFormat("Char: %c", (String)null);
+ assertStringFormat("Value: %.2e", 1234567.0);
+ assertStringFormat("Value: %.2e", (Number)null);
+ assertStringFormat("Number: %+10.2f", 19.99);
+ assertStringFormat("ID: %05d", 42);
+ assertStringFormat("Value: %d", (Number)null); // Line 304: null -> "null"
+ assertStringFormat("Value: %s", (String)null);
+ assertStringFormat("Name: %s", "");
+ assertStringFormat("%s%s", "A", "B");
+ assertStringFormat("Progress: %d%% Complete: %d%%", 50, 75);
+ assertStringFormat("%1$s and %1$s again", "Hello");
+ assertStringFormat("Hex: 0x%08X, Decimal: %+d, Float: %10.3f", 255, 42, 3.14159);
+ assertStringFormat("Price: %.2f", Locale.US, 19.99);
+ assertStringFormat("Price: %.2f", Locale.FRANCE, 19.99);
+ assertStringFormat("Value: %s", (Object)null);
+ assertStringFormat("Int: %d", 42);
+ assertStringFormat("Long: %d", 1234567890L);
+ assertStringFormat("Byte: %d", (byte)127);
+ assertStringFormat("Short: %d", (short)32767);
+ assertStringFormat("Int: %d", Locale.FRANCE, 1234);
+ assertStringFormat("Long: %d", Locale.GERMANY, 1234567L);
+ assertStringFormat("Value: %d", "not-a-number");
+ assertStringFormat("Hex: %x", 255); // Line 328: Integer -> Integer.toHexString()
+ assertStringFormat("Hex: %x", 255L);
+ assertStringFormat("Hex: %x", (byte)255);
+ assertStringFormat("Hex: %x", (Number)null); // Line 324: null -> "null"
+ assertStringFormat("Hex: %X", 255);
+ assertStringFormat("Hex: %X", 0xABCL);
+ assertStringFormat("Hex: %X", (short)255);
+ assertStringFormat("Hex: %X", (Number)null); // Line 337: null -> "null"
+ assertStringFormat("Octal: %o", 255L);
+ assertStringFormat("Octal: %o", (byte)64);
+ assertStringFormat("Value: %b", "hello");
+ assertStringFormat("Value: %b", 42);
+ assertStringFormat("Char: %c", 65);
+ assertStringFormat("Char: %c", 65L);
+ assertStringFormat("Char: %c", "X");
+ assertStringFormat("Char: %C", (Character)null); // Line 376: null -> "null"
+ assertStringFormat("Char: %C", 66); // Line 382-383: Number (Integer) -> Character.toUpperCase((char)o2.intValue())
+ assertStringFormat("Char: %C", 66L); // Line 382-383: Number (Long) -> Character.toUpperCase((char)o2.intValue())
+ assertStringFormat("Float: %f", 3.14f);
+ assertStringFormat("Double: %f", 3.14159);
+ assertStringFormat("Float: %f", Locale.FRANCE, 3.14f);
+ assertStringFormat("Double: %f", Locale.GERMANY, 1234.56);
+ assertStringFormat("Value: %f", (Number)null); // Line 389: null -> "null"
+ assertStringFormat("Value: %f", "not-a-number");
+ assertStringFormat("Value: %.2e", 1234.56);
+ assertStringFormat("Value: %S", "hello");
+ assertStringFormat("Value: %S", (String)null); // Line 297: null -> "null"
+ assertStringFormat("Value: %B", true);
+ assertStringFormat("Char: %C", 'a');
+ assertStringFormat("Float: %F", 3.14);
+
+ // Errors
+ assertStringFormat("Hello %s");
+ assertStringFormat("Hello %s and %s", "John");
+ assertStringFormat("Hello %");
+ assertStringFormat("Hello %s and %", "John");
+ assertStringFormat("Hello %x$s", "John");
+ }
+
+ //====================================================================================================
+ // Mixed format tests
+ //====================================================================================================
+ @Test void a03_mixedFormat() {
+ assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
+ assertMixedFormat("User Alice has admin and 10 items", "User {0} has %s and {2} items", "Alice", "admin", 10);
+ assertMixedFormat("Alice loves Bob, and Alice also loves Charlie", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob", "Charlie");
+ assertMixedFormat("Alice has 5 items, Bob has 3 items, total: 8", "{0} has %d items, {2} has %d items, total: %d", "Alice", 5, "Bob", 3, 8);
+ assertMixedFormat("Alice Bob Charlie", "{0} %2$s {2}", "Alice", "Bob", "Charlie");
+ assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
+ assertMixedFormat("A B B D C", "{0} %s {1} %s {2}", "A", "B", "C", "D");
+ assertMixedFormat("ABB", "{0}%s{1}", "A", "B", "C");
+ assertMixedFormat("Hello and Hello are the same", "{0} and %1$s are the same", "Hello");
+
+ // Errors
+ assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "Hello {0} and %s", "John");
+ assertMixedFormat("John has 5 items and {2} friends", "{0} has %d items and {2} friends", "John", 5);
+ assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob");
+ }
+
+ //====================================================================================================
+ // Supported but deviates from MessageFormat/String.format
+ //====================================================================================================
+ @Test void a04_supportedButDeviatesFromMessageFormat() {
+ // {} is not supported by MessageFormat, only by StringFormat as an extension
+ assertMixedFormat("Hello John world", "Hello {} world", "John");
+ assertMixedFormat("A B C", "{} {} {}", "A", "B", "C");
+ // BigDecimal with %d - String.format throws exception, but our optimized code handles it
+ assertMixedFormat("Number: 42", "Number: %d", new BigDecimal("42"));
+ // MessageFormat throws NullPointerException when locale is null, but StringFormat handles it
+ // So we test StringFormat's behavior directly instead of comparing with MessageFormat
+ assertMixedFormat("Price: $19.99", "Price: {0,number,currency}", (Locale)null, 19.99);
+ }
+
+ //====================================================================================================
+ // Error handling
+ //====================================================================================================
+ @Test void a05_errors() {
+ assertThrows(IllegalArgumentException.class, () -> new StringFormat(null));
+ assertThrows(IllegalArgumentException.class, () -> fs(null));
+ }
+
+ @Test void a06_caching() {
+ // Should return the same instance due to caching
+ assertSame(fs("Hello {0}"), fs("Hello {0}"));
+
+ // Different patterns should return different instances
+ assertNotSame(fs("Hello {0}"), fs("Hello %s"));
+
+ // Constructor doesn't use cache, so instances should be different
+ var fmt1 = new StringFormat("Hello {0}");
+ var fmt2 = new StringFormat("Hello {0}");
+ assertNotSame(fmt1, fmt2);
+ assertEquals(fmt1, fmt2); // But they should be equal
+ }
+
+ @Test void a07_equalsAndHashCode() {
+ var fmt1 = StringFormat.of("Hello {0}");
+ var fmt2 = StringFormat.of("Hello {0}");
+ var fmt3 = StringFormat.of("Hello %s");
+
+ assertEquals(fmt1, fmt2);
+ assertNotEquals(fmt1, fmt3);
+ assertEquals(fmt1.hashCode(), fmt2.hashCode());
+ }
+
+ @Test void a08_toString() {
+ assertEquals("Hello {0}", fs("Hello {0}").toString());
+ }
+
+ @Test void a09_toPattern() {
+ // Literal tokens
+ assertEquals("[L:Hello ]", fs("Hello ").toPattern());
+ assertEquals("[L:a ][L:{0}][L: b]", fs("a '{0}' b").toPattern()); // Single quotes don't escape MessageFormat
+
+ // MessageFormat tokens - simple (content == null) - Line 228: content == null branch
+ assertEquals("[L:Hello ][M:s0]", fs("Hello {0}").toPattern());
+ assertEquals("[L:Hello ][M:s0][L: ][M:s1]", fs("Hello {0} {1}").toPattern());
+
+ // MessageFormat tokens - complex (content != null) - Line 228: content != null branch
+ assertEquals("[L:Price: ][M:o0:{0,number,currency}]", fs("Price: {0,number,currency}").toPattern());
+ assertEquals("[L:Count: ][M:o0:{0,number,integer}]", fs("Count: {0,number,integer}").toPattern());
+ assertEquals("[L:Date: ][M:o0:{0,date,short}]", fs("Date: {0,date,short}").toPattern());
+
+ // StringFormat tokens - Line 406: StringFormatToken.toString()
+ assertEquals("[L:Hello ][S:s0:%s]", fs("Hello %s").toPattern()); // Simple format: 's'
+ assertEquals("[L:Number: ][S:d0:%d]", fs("Number: %d").toPattern()); // Simple format: 'd'
+ assertEquals("[L:Hex: ][S:x0:%x]", fs("Hex: %x").toPattern()); // Simple format: 'x'
+ assertEquals("[L:Float: ][S:z0:%.2f]", fs("Float: %.2f").toPattern()); // Complex format: 'z' (other)
+ assertEquals("[L:ID: ][S:z0:%05d]", fs("ID: %05d").toPattern()); // Complex format: 'z' (other)
+
+ // Mixed formats
+ assertEquals("[L:Hello ][M:s0][L:, you have ][S:d1:%d][L: items]", fs("Hello {0}, you have %d items").toPattern());
+ assertEquals("[L:Price: ][M:o0:{0,number,currency}][L: and ][S:s1:%s]", fs("Price: {0,number,currency} and %s").toPattern());
+
+ // Time conversions (2-character) - Line 529: 't' or 'T' handling
+ assertEquals("[L:Month: ][S:z0:%tm]", fs("Month: %tm").toPattern()); // %tm is 2-character time conversion
+ assertEquals("[L:Year: ][S:z0:%tY]", fs("Year: %tY").toPattern()); // %tY is 2-character time conversion
+ assertEquals("[L:Date: ][S:z0:%TD]", fs("Date: %TD").toPattern()); // %TD is 2-character time conversion
+ }
+
+ @Test void a10_veryLongPattern() {
+ var pattern = "Start: " + IntStream.range(0, 10).mapToObj(i -> "{" + i + "}").collect(joining(" ")) + " ";
+ var args = IntStream.range(0, 10).boxed().toArray();
+ assertMessageFormat(pattern, args);
+ }
+
+ @Test void a11_parseIndexErrors() {
+ assertThrows(IllegalArgumentException.class, () -> fs("Hello {abc}"));
+ }
+
+ @Test void a12_localeHandling() {
+ // Lines 259-260: Test locale null checks and default locale detection in StringFormatToken
+ // Line 259: var l = locale == null ? Locale.getDefault() : locale;
+ // Line 260: var dl = locale == null || locale.equals(Locale.getDefault());
+
+ // Test with null locale (covers locale == null on both lines)
+ assertStringFormat("Hello %s", (Locale)null, "John");
+ assertStringFormat("Number: %d", (Locale)null, 42);
+ assertStringFormat("Float: %.2f", (Locale)null, 3.14); // Use .2f for consistent formatting
+
+ // Test with default locale (covers locale.equals(Locale.getDefault()) on line 260)
+ assertStringFormat("Hello %s", Locale.getDefault(), "John");
+ assertStringFormat("Number: %d", Locale.getDefault(), 42);
+ assertStringFormat("Float: %.2f", Locale.getDefault(), 3.14); // Use .2f for consistent formatting
+
+ // Test with non-default locale (covers else branch on line 259 and false case on line 260)
+ assertStringFormat("Hello %s", Locale.FRANCE, "John");
+ assertStringFormat("Number: %d", Locale.GERMANY, 42);
+ assertStringFormat("Float: %.2f", Locale.JAPAN, 3.14); // Use .2f for consistent formatting
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
index 355b73a196..538c864bd8 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
@@ -39,7 +39,8 @@ class StringUtils_Test extends TestBase {
// isNumeric(String,Class)
// parseNumber(String,Class)
//====================================================================================================
- @Test void a01_testParser() {
+ @Test
+ void a01_testParser() {
// Integers
assertTrue(isNumeric("123"));
@@ -144,9 +145,9 @@ class StringUtils_Test extends TestBase {
assertTrue(isNumeric("0x123e1"));
assertEquals(0x123e1, parseNumber("0x123e1", null));
- assertThrows(NumberFormatException.class, ()->parseNumber("x", Number.class));
- assertThrows(NumberFormatException.class, ()->parseNumber("x", null));
- assertThrowsWithMessage(NumberFormatException.class, "Unsupported Number type", ()->parseNumber("x", BadNumber.class));
+ assertThrows(NumberFormatException.class, () -> parseNumber("x", Number.class));
+ assertThrows(NumberFormatException.class, () -> parseNumber("x", null));
+ assertThrowsWithMessage(NumberFormatException.class, "Unsupported Number type", () -> parseNumber("x", BadNumber.class));
}
@SuppressWarnings("serial")
@@ -155,7 +156,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// test - Basic tests
//====================================================================================================
- @Test void a02_numberRanges() {
+ @Test
+ void a02_numberRanges() {
// An integer range is -2,147,483,648 to 2,147,483,647
assertFalse(isNumeric(null));
@@ -246,17 +248,18 @@ class StringUtils_Test extends TestBase {
s = String.valueOf("214748364x");
assertFalse(isNumeric(s));
- assertThrows(NumberFormatException.class, ()->parseNumber("214748364x", Number.class));
+ assertThrows(NumberFormatException.class, () -> parseNumber("214748364x", Number.class));
s = String.valueOf("2147483640x");
assertFalse(isNumeric(s));
- assertThrows(NumberFormatException.class, ()->parseNumber("2147483640x", Long.class));
+ assertThrows(NumberFormatException.class, () -> parseNumber("2147483640x", Long.class));
}
//====================================================================================================
// testReplaceVars
//====================================================================================================
- @Test void a03_replaceVars() throws Exception {
+ @Test
+ void a03_replaceVars() throws Exception {
var m = JsonMap.ofJson("{a:'A',b:1,c:true,d:'{e}',e:'E{f}E',f:'F',g:'{a}',h:'a',i:null}");
assertEquals("xxx", replaceVars("xxx", m));
@@ -291,39 +294,13 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// isFloat(String)
//====================================================================================================
- @Test void a04_isFloat() {
- var valid = a(
- "+1.0",
- "-1.0",
- ".0",
- "NaN",
- "Infinity",
- "1e1",
- "-1e-1",
- "+1e+1",
- "-1.1e-1",
- "+1.1e+1",
- "1.1f",
- "1.1F",
- "1.1d",
- "1.1D",
- "0x1.fffffffffffffp1023",
- "0x1.FFFFFFFFFFFFFP1023"
- );
+ @Test
+ void a04_isFloat() {
+ var valid = a("+1.0", "-1.0", ".0", "NaN", "Infinity", "1e1", "-1e-1", "+1e+1", "-1.1e-1", "+1.1e+1", "1.1f", "1.1F", "1.1d", "1.1D", "0x1.fffffffffffffp1023", "0x1.FFFFFFFFFFFFFP1023");
for (var s : valid)
assertTrue(isFloat(s));
- var invalid = a(
- null,
- "",
- "a",
- "+",
- "-",
- ".",
- "a",
- "+a",
- "11a"
- );
+ var invalid = a(null, "", "a", "+", "-", ".", "a", "+a", "11a");
for (var s : invalid)
assertFalse(isFloat(s));
}
@@ -331,35 +308,13 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// isDecimal(String)
//====================================================================================================
- @Test void a05_isDecimal() {
- var valid = a(
- "+1",
- "-1",
- "0x123",
- "0X123",
- "0xdef",
- "0XDEF",
- "#def",
- "#DEF",
- "0123"
- );
+ @Test
+ void a05_isDecimal() {
+ var valid = a("+1", "-1", "0x123", "0X123", "0xdef", "0XDEF", "#def", "#DEF", "0123");
for (var s : valid)
assertTrue(isDecimal(s));
- var invalid = a(
- null,
- "",
- "a",
- "+",
- "-",
- ".",
- "0xdeg",
- "0XDEG",
- "#deg",
- "#DEG",
- "0128",
- "012A"
- );
+ var invalid = a(null, "", "a", "+", "-", ".", "0xdeg", "0XDEG", "#deg", "#DEG", "0128", "012A");
for (var s : invalid)
assertFalse(isDecimal(s));
}
@@ -372,34 +327,36 @@ class StringUtils_Test extends TestBase {
// join(int[],char)
// join(Collection,char)
//====================================================================================================
- @Test void a01_join() {
+ @Test
+ void a01_join() {
assertNull(StringUtils.join((Object[])null, ","));
assertEquals("1", StringUtils.join(a(1), ","));
- assertEquals("1,2", StringUtils.join(a(1,2), ","));
+ assertEquals("1,2", StringUtils.join(a(1, 2), ","));
assertNull(StringUtils.join((Collection<?>)null, ","));
assertEquals("1", StringUtils.join(l(a(1)), ","));
- assertEquals("1,2", StringUtils.join(l(a(1,2)), ","));
+ assertEquals("1,2", StringUtils.join(l(a(1, 2)), ","));
assertNull(StringUtils.join((Object[])null, ','));
- assertEquals("x,y,z", StringUtils.join(a("x,y","z"), ','));
+ assertEquals("x,y,z", StringUtils.join(a("x,y", "z"), ','));
assertNull(StringUtils.join((int[])null, ','));
assertEquals("1", StringUtils.join(ints(1), ','));
- assertEquals("1,2", StringUtils.join(ints(1,2), ','));
+ assertEquals("1,2", StringUtils.join(ints(1, 2), ','));
assertNull(StringUtils.join((Collection<?>)null, ','));
assertEquals("1", StringUtils.join(l(a(1)), ','));
- assertEquals("1,2", StringUtils.join(l(a(1,2)), ','));
+ assertEquals("1,2", StringUtils.join(l(a(1, 2)), ','));
assertNull(StringUtils.joine((List<?>)null, ','));
- assertEquals("x\\,y,z", StringUtils.joine(l(a("x,y","z")), ','));
+ assertEquals("x\\,y,z", StringUtils.joine(l(a("x,y", "z")), ','));
}
//====================================================================================================
// split(String,char)
//====================================================================================================
- @Test void a07_split() {
+ @Test
+ void a07_split() {
assertNull(StringUtils.splita((String)null));
assertEmpty(StringUtils.splita(""));
assertList(StringUtils.splita("1"), "1");
@@ -413,7 +370,8 @@ class StringUtils_Test extends TestBase {
assertList(StringUtils.splita("1,2\\\\,"), "1", "2\\", "");
}
- @Test void a08_split2() {
+ @Test
+ void a08_split2() {
assertEmpty(split2test(null));
assertString("[]", split2test(""));
assertString("[1]", split2test("1"));
@@ -436,7 +394,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// split(String,char,int)
//====================================================================================================
- @Test void a09_splitWithLimit() {
+ @Test
+ void a09_splitWithLimit() {
assertString("[boo,and,foo]", StringUtils.splita("boo:and:foo", ':', 10));
assertString("[boo,and:foo]", StringUtils.splita("boo:and:foo", ':', 2));
assertString("[boo:and:foo]", StringUtils.splita("boo:and:foo", ':', 1));
@@ -449,7 +408,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// nullIfEmpty(String)
//====================================================================================================
- @Test void a10_nullIfEmpty() {
+ @Test
+ void a10_nullIfEmpty() {
assertNull(StringUtils.nullIfEmpty(null));
assertNull(StringUtils.nullIfEmpty(""));
assertNotNull(StringUtils.nullIfEmpty("x"));
@@ -458,7 +418,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// emptyIfNull(String)
//====================================================================================================
- @Test void a11_emptyIfNull() {
+ @Test
+ void a11_emptyIfNull() {
assertEquals("", StringUtils.emptyIfNull(null));
assertEquals("", StringUtils.emptyIfNull(""));
assertEquals("x", StringUtils.emptyIfNull("x"));
@@ -469,7 +430,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// defaultIfEmpty(String, String)
//====================================================================================================
- @Test void a12_defaultIfEmpty() {
+ @Test
+ void a12_defaultIfEmpty() {
assertEquals("default", StringUtils.defaultIfEmpty(null, "default"));
assertEquals("default", StringUtils.defaultIfEmpty("", "default"));
assertEquals("x", StringUtils.defaultIfEmpty("x", "default"));
@@ -479,7 +441,8 @@ class StringUtils_Test extends TestBase {
assertEquals("x", StringUtils.defaultIfEmpty("x", null)); // "x" is not empty, so return "x"
}
- @Test void a13_defaultIfEmpty_withNullDefault() {
+ @Test
+ void a13_defaultIfEmpty_withNullDefault() {
assertNull(StringUtils.defaultIfEmpty(null, null));
assertNull(StringUtils.defaultIfEmpty("", null));
assertEquals("x", StringUtils.defaultIfEmpty("x", null));
@@ -488,7 +451,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// defaultIfBlank(String, String)
//====================================================================================================
- @Test void a14_defaultIfBlank() {
+ @Test
+ void a14_defaultIfBlank() {
assertEquals("default", StringUtils.defaultIfBlank(null, "default"));
assertEquals("default", StringUtils.defaultIfBlank("", "default"));
assertEquals("default", StringUtils.defaultIfBlank(" ", "default"));
@@ -501,14 +465,16 @@ class StringUtils_Test extends TestBase {
assertEquals("x", StringUtils.defaultIfBlank("x", null)); // "x" is not blank, so return "x"
}
- @Test void a15_defaultIfBlank_withNullDefault() {
+ @Test
+ void a15_defaultIfBlank_withNullDefault() {
assertNull(StringUtils.defaultIfBlank(null, null));
assertNull(StringUtils.defaultIfBlank("", null));
assertNull(StringUtils.defaultIfBlank(" ", null));
assertEquals("x", StringUtils.defaultIfBlank("x", null));
}
- @Test void a16_defaultIfBlank_whitespaceOnly() {
+ @Test
+ void a16_defaultIfBlank_whitespaceOnly() {
assertEquals("default", StringUtils.defaultIfBlank(" ", "default"));
assertEquals("default", StringUtils.defaultIfBlank("\t\n\r", "default"));
// Note: \u00A0 (non-breaking space) may or may not be considered blank depending on Java version
@@ -521,7 +487,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toString(Object)
//====================================================================================================
- @Test void a17_toString() {
+ @Test
+ void a17_toString() {
assertNull(StringUtils.toString(null));
assertEquals("hello", StringUtils.toString("hello"));
assertEquals("123", StringUtils.toString(123));
@@ -529,7 +496,8 @@ class StringUtils_Test extends TestBase {
assertEquals("1.5", StringUtils.toString(1.5));
}
- @Test void a18_toString_withObjects() {
+ @Test
+ void a18_toString_withObjects() {
var list = List.of("a", "b", "c");
assertNotNull(StringUtils.toString(list));
assertTrue(StringUtils.toString(list).contains("a"));
@@ -539,7 +507,8 @@ class StringUtils_Test extends TestBase {
assertTrue(StringUtils.toString(map).contains("key"));
}
- @Test void a19_toString_withCustomObject() {
+ @Test
+ void a19_toString_withCustomObject() {
var obj = new Object() {
@Override
public String toString() {
@@ -552,7 +521,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toString(Object, String)
//====================================================================================================
- @Test void a20_toStringWithDefault() {
+ @Test
+ void a20_toStringWithDefault() {
assertEquals("default", StringUtils.toString(null, "default"));
assertEquals("hello", StringUtils.toString("hello", "default"));
assertEquals("123", StringUtils.toString(123, "default"));
@@ -560,17 +530,20 @@ class StringUtils_Test extends TestBase {
assertEquals("1.5", StringUtils.toString(1.5, "default"));
}
- @Test void a21_toStringWithDefault_withNullDefault() {
+ @Test
+ void a21_toStringWithDefault_withNullDefault() {
assertNull(StringUtils.toString(null, null));
assertEquals("hello", StringUtils.toString("hello", null));
}
- @Test void a22_toStringWithDefault_withEmptyDefault() {
+ @Test
+ void a22_toStringWithDefault_withEmptyDefault() {
assertEquals("", StringUtils.toString(null, ""));
assertEquals("hello", StringUtils.toString("hello", ""));
}
- @Test void a23_toStringWithDefault_withObjects() {
+ @Test
+ void a23_toStringWithDefault_withObjects() {
var list = List.of("a", "b", "c");
var result = StringUtils.toString(list, "default");
assertNotNull(result);
@@ -579,7 +552,8 @@ class StringUtils_Test extends TestBase {
assertEquals("default", StringUtils.toString(null, "default"));
}
- @Test void a24_toStringWithDefault_withCustomObject() {
+ @Test
+ void a24_toStringWithDefault_withCustomObject() {
var obj = new Object() {
@Override
public String toString() {
@@ -593,7 +567,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// unescapeChars(String,char[],char)
//====================================================================================================
- @Test void a25_unescapeChars() {
+ @Test
+ void a25_unescapeChars() {
var escape = AsciiSet.of("\\,|");
assertNull(unEscapeChars(null, escape));
@@ -615,7 +590,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// decodeHex(String)
//====================================================================================================
- @Test void a26_decodeHex() {
+ @Test
+ void a26_decodeHex() {
assertNull(decodeHex(null));
assertEquals("19azAZ", decodeHex("19azAZ"));
assertEquals("[0][1][ffff]", decodeHex("\u0000\u0001\uFFFF"));
@@ -624,7 +600,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// startsWith(String,char)
//====================================================================================================
- @Test void a27_startsWith() {
+ @Test
+ void a27_startsWith() {
assertFalse(startsWith(null, 'a'));
assertFalse(startsWith("", 'a'));
assertTrue(startsWith("a", 'a'));
@@ -634,7 +611,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// endsWith(String,char)
//====================================================================================================
- @Test void a28_endsWith() {
+ @Test
+ void a28_endsWith() {
assertFalse(endsWith(null, 'a'));
assertFalse(endsWith("", 'a'));
assertTrue(endsWith("a", 'a'));
@@ -645,19 +623,21 @@ class StringUtils_Test extends TestBase {
// base64EncodeToString(String)
// base64DecodeToString(String)
//====================================================================================================
- @Test void a29_base64EncodeToString() {
+ @Test
+ void a29_base64EncodeToString() {
assertNull(base64DecodeToString(base64EncodeToString(null)));
assertEquals("", base64DecodeToString(base64EncodeToString("")));
assertEquals("foobar", base64DecodeToString(base64EncodeToString("foobar")));
assertEquals("\u0000\uffff", base64DecodeToString(base64EncodeToString("\u0000\uffff")));
- assertThrowsWithMessage(IllegalArgumentException.class, "Invalid BASE64 string length. Must be multiple of 4.", ()->base64Decode("a"));
- assertThrows(IllegalArgumentException.class, ()->base64Decode("aaa"));
+ assertThrowsWithMessage(IllegalArgumentException.class, "Invalid BASE64 string length. Must be multiple of 4.", () -> base64Decode("a"));
+ assertThrows(IllegalArgumentException.class, () -> base64Decode("aaa"));
}
//====================================================================================================
// generateUUID(String)
//====================================================================================================
- @Test void a30_generateUUID() {
+ @Test
+ void a30_generateUUID() {
for (var i = 0; i < 10; i++) {
var s = random(i);
assertEquals(i, s.length());
@@ -669,7 +649,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// trim(String)
//====================================================================================================
- @Test void a31_trim() {
+ @Test
+ void a31_trim() {
assertNull(trim(null));
assertEquals("", trim(""));
assertEquals("", trim(" "));
@@ -678,7 +659,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// parseISO8601Date(String)
//====================================================================================================
- @Test void a32_parseISO8601Date() throws Exception {
+ @Test
+ void a32_parseISO8601Date() throws Exception {
assertNull(parseIsoDate(null));
assertNull(parseIsoDate(""));
@@ -702,7 +684,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// parseMap(String,char,char,boolean)
//====================================================================================================
- @Test void a33_splitMap() {
+ @Test
+ void a33_splitMap() {
assertString("{a=1}", StringUtils.splitMap("a=1", true));
assertString("{a=1,b=2}", StringUtils.splitMap("a=1,b=2", true));
assertString("{a=1,b=2}", StringUtils.splitMap(" a = 1 , b = 2 ", true));
@@ -718,7 +701,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// isAbsoluteUri(String)
//====================================================================================================
- @Test void a10_isAbsoluteUri() {
+ @Test
+ void a10_isAbsoluteUri() {
assertFalse(isAbsoluteUri(null));
assertFalse(isAbsoluteUri(""));
assertTrue(isAbsoluteUri("http://foo"));
@@ -734,7 +718,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// getAuthorityUri(String)
//====================================================================================================
- @Test void a21_getAuthorityUri() {
+ @Test
+ void a21_getAuthorityUri() {
assertEquals("http://foo", getAuthorityUri("http://foo"));
assertEquals("http://foo:123", getAuthorityUri("http://foo:123"));
assertEquals("http://foo:123", getAuthorityUri("http://foo:123/"));
@@ -744,7 +729,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// splitQuoted(String)
//====================================================================================================
- @Test void a22_splitQuoted() {
+ @Test
+ void a22_splitQuoted() {
assertNull(StringUtils.splitQuoted(null));
assertEmpty(StringUtils.splitQuoted(""));
assertEmpty(StringUtils.splitQuoted(" \t "));
@@ -769,7 +755,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// firstNonWhitespaceChar(String)
//====================================================================================================
- @Test void a23_firstNonWhitespaceChar() {
+ @Test
+ void a23_firstNonWhitespaceChar() {
assertEquals('f', firstNonWhitespaceChar("foo"));
assertEquals('f', firstNonWhitespaceChar(" foo"));
assertEquals('f', firstNonWhitespaceChar("\tfoo"));
@@ -782,7 +769,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// lastNonWhitespaceChar(String)
//====================================================================================================
- @Test void a24_lastNonWhitespaceChar() {
+ @Test
+ void a24_lastNonWhitespaceChar() {
assertEquals('r', lastNonWhitespaceChar("bar"));
assertEquals('r', lastNonWhitespaceChar(" bar "));
assertEquals('r', lastNonWhitespaceChar("\tbar\t"));
@@ -795,7 +783,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// testIsJsonObject(Object)
//====================================================================================================
- @Test void a25_isJsonObject() {
+ @Test
+ void a25_isJsonObject() {
assertTrue(isJsonObject("{foo:'bar'}", true));
assertTrue(isJsonObject(" { foo:'bar' } ", true));
assertFalse(isJsonObject(" { foo:'bar' ", true));
@@ -806,7 +795,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// isJsonArray(Object)
//====================================================================================================
- @Test void a26_isJsonArray() {
+ @Test
+ void a26_isJsonArray() {
assertTrue(isJsonArray("[123,'bar']", true));
assertTrue(isJsonArray(" [ 123,'bar' ] ", true));
assertFalse(isJsonArray(" [ 123,'bar' ", true));
@@ -817,7 +807,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// addLineNumbers(String)
//====================================================================================================
- @Test void a27_addLineNumbers() {
+ @Test
+ void a27_addLineNumbers() {
assertNull(getNumberedLines(null));
assertEquals("1: \n", getNumberedLines(""));
assertEquals("1: foo\n", getNumberedLines("foo"));
@@ -827,18 +818,20 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// compare(String,String)
//====================================================================================================
- @Test void a28_compare() {
- assertTrue(compare("a","b") < 0);
- assertTrue(compare("b","a") > 0);
- assertTrue(compare(null,"b") < 0);
- assertTrue(compare("b",null) > 0);
- assertEquals(0, compare(null,null));
+ @Test
+ void a28_compare() {
+ assertTrue(compare("a", "b") < 0);
+ assertTrue(compare("b", "a") > 0);
+ assertTrue(compare(null, "b") < 0);
+ assertTrue(compare("b", null) > 0);
+ assertEquals(0, compare(null, null));
}
//====================================================================================================
// matchPattern(String)
//====================================================================================================
- @Test void a29_getMatchPattern() {
+ @Test
+ void a29_getMatchPattern() {
assertTrue(StringUtils.getMatchPattern("a").matcher("a").matches());
assertTrue(StringUtils.getMatchPattern("*a*").matcher("aaa").matches());
assertFalse(StringUtils.getMatchPattern("*b*").matcher("aaa").matches());
@@ -847,7 +840,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// getDuration(String)
//====================================================================================================
- @Test void a30_getDuration() {
+ @Test
+ void a30_getDuration() {
assertEquals(-1, getDuration(null));
assertEquals(-1, getDuration(""));
assertEquals(-1, getDuration(" "));
@@ -855,83 +849,79 @@ class StringUtils_Test extends TestBase {
assertEquals(10, getDuration("10"));
assertEquals(10, getDuration("10"));
- long
- s = 1000,
- m = s * 60,
- h = m * 60,
- d = h * 24,
- w = d * 7;
-
- assertEquals(10*s, getDuration("10s"));
- assertEquals(10*s, getDuration("10 s"));
- assertEquals(10*s, getDuration(" 10 s "));
- assertEquals(10*s, getDuration("10sec"));
- assertEquals(10*s, getDuration("10 sec"));
- assertEquals(10*s, getDuration(" 10 sec "));
- assertEquals(10*s, getDuration("10seconds"));
- assertEquals(10*s, getDuration("10 seconds"));
- assertEquals(10*s, getDuration(" 10 seconds "));
- assertEquals(10*s, getDuration("10S"));
- assertEquals(10*s, getDuration("10 S"));
- assertEquals(10*s, getDuration(" 10 S "));
-
- assertEquals(10*m, getDuration("10m"));
- assertEquals(10*m, getDuration("10 m"));
- assertEquals(10*m, getDuration(" 10 m "));
- assertEquals(10*m, getDuration("10min"));
- assertEquals(10*m, getDuration("10 min"));
- assertEquals(10*m, getDuration(" 10 min "));
- assertEquals(10*m, getDuration("10minutes"));
- assertEquals(10*m, getDuration("10 minutes"));
- assertEquals(10*m, getDuration(" 10 minutes "));
- assertEquals(10*m, getDuration("10M"));
- assertEquals(10*m, getDuration("10 M"));
- assertEquals(10*m, getDuration(" 10 M "));
-
- assertEquals(10*h, getDuration("10h"));
- assertEquals(10*h, getDuration("10 h"));
- assertEquals(10*h, getDuration(" 10 h "));
- assertEquals(10*h, getDuration("10hour"));
- assertEquals(10*h, getDuration("10 hour"));
- assertEquals(10*h, getDuration(" 10 hour "));
- assertEquals(10*h, getDuration("10hours"));
- assertEquals(10*h, getDuration("10 hours"));
- assertEquals(10*h, getDuration(" 10 hours "));
- assertEquals(10*h, getDuration("10H"));
- assertEquals(10*h, getDuration("10 H"));
- assertEquals(10*h, getDuration(" 10 H "));
-
- assertEquals(10*d, getDuration("10d"));
- assertEquals(10*d, getDuration("10 d"));
- assertEquals(10*d, getDuration(" 10 d "));
- assertEquals(10*d, getDuration("10day"));
- assertEquals(10*d, getDuration("10 day"));
- assertEquals(10*d, getDuration(" 10 day "));
- assertEquals(10*d, getDuration("10days"));
- assertEquals(10*d, getDuration("10 days"));
- assertEquals(10*d, getDuration(" 10 days "));
- assertEquals(10*d, getDuration("10D"));
- assertEquals(10*d, getDuration("10 D"));
- assertEquals(10*d, getDuration(" 10 D "));
-
- assertEquals(10*w, getDuration("10w"));
- assertEquals(10*w, getDuration("10 w"));
- assertEquals(10*w, getDuration(" 10 w "));
- assertEquals(10*w, getDuration("10week"));
- assertEquals(10*w, getDuration("10 week"));
- assertEquals(10*w, getDuration(" 10 week "));
- assertEquals(10*w, getDuration("10weeks"));
- assertEquals(10*w, getDuration("10 weeks"));
- assertEquals(10*w, getDuration(" 10 weeks "));
- assertEquals(10*w, getDuration("10W"));
- assertEquals(10*w, getDuration("10 W"));
- assertEquals(10*w, getDuration(" 10 W "));
+ long s = 1000, m = s * 60, h = m * 60, d = h * 24, w = d * 7;
+
+ assertEquals(10 * s, getDuration("10s"));
+ assertEquals(10 * s, getDuration("10 s"));
+ assertEquals(10 * s, getDuration(" 10 s "));
+ assertEquals(10 * s, getDuration("10sec"));
+ assertEquals(10 * s, getDuration("10 sec"));
+ assertEquals(10 * s, getDuration(" 10 sec "));
+ assertEquals(10 * s, getDuration("10seconds"));
+ assertEquals(10 * s, getDuration("10 seconds"));
+ assertEquals(10 * s, getDuration(" 10 seconds "));
+ assertEquals(10 * s, getDuration("10S"));
+ assertEquals(10 * s, getDuration("10 S"));
+ assertEquals(10 * s, getDuration(" 10 S "));
+
+ assertEquals(10 * m, getDuration("10m"));
+ assertEquals(10 * m, getDuration("10 m"));
+ assertEquals(10 * m, getDuration(" 10 m "));
+ assertEquals(10 * m, getDuration("10min"));
+ assertEquals(10 * m, getDuration("10 min"));
+ assertEquals(10 * m, getDuration(" 10 min "));
+ assertEquals(10 * m, getDuration("10minutes"));
+ assertEquals(10 * m, getDuration("10 minutes"));
+ assertEquals(10 * m, getDuration(" 10 minutes "));
+ assertEquals(10 * m, getDuration("10M"));
+ assertEquals(10 * m, getDuration("10 M"));
+ assertEquals(10 * m, getDuration(" 10 M "));
+
+ assertEquals(10 * h, getDuration("10h"));
+ assertEquals(10 * h, getDuration("10 h"));
+ assertEquals(10 * h, getDuration(" 10 h "));
+ assertEquals(10 * h, getDuration("10hour"));
+ assertEquals(10 * h, getDuration("10 hour"));
+ assertEquals(10 * h, getDuration(" 10 hour "));
+ assertEquals(10 * h, getDuration("10hours"));
+ assertEquals(10 * h, getDuration("10 hours"));
+ assertEquals(10 * h, getDuration(" 10 hours "));
+ assertEquals(10 * h, getDuration("10H"));
+ assertEquals(10 * h, getDuration("10 H"));
+ assertEquals(10 * h, getDuration(" 10 H "));
+
+ assertEquals(10 * d, getDuration("10d"));
+ assertEquals(10 * d, getDuration("10 d"));
+ assertEquals(10 * d, getDuration(" 10 d "));
+ assertEquals(10 * d, getDuration("10day"));
+ assertEquals(10 * d, getDuration("10 day"));
+ assertEquals(10 * d, getDuration(" 10 day "));
+ assertEquals(10 * d, getDuration("10days"));
+ assertEquals(10 * d, getDuration("10 days"));
+ assertEquals(10 * d, getDuration(" 10 days "));
+ assertEquals(10 * d, getDuration("10D"));
+ assertEquals(10 * d, getDuration("10 D"));
+ assertEquals(10 * d, getDuration(" 10 D "));
+
+ assertEquals(10 * w, getDuration("10w"));
+ assertEquals(10 * w, getDuration("10 w"));
+ assertEquals(10 * w, getDuration(" 10 w "));
+ assertEquals(10 * w, getDuration("10week"));
+ assertEquals(10 * w, getDuration("10 week"));
+ assertEquals(10 * w, getDuration(" 10 week "));
+ assertEquals(10 * w, getDuration("10weeks"));
+ assertEquals(10 * w, getDuration("10 weeks"));
+ assertEquals(10 * w, getDuration(" 10 weeks "));
+ assertEquals(10 * w, getDuration("10W"));
+ assertEquals(10 * w, getDuration("10 W"));
+ assertEquals(10 * w, getDuration(" 10 W "));
}
//====================================================================================================
// getDuration(String)
//====================================================================================================
- @Test void a31_stripInvalidHttpHeaderChars() {
+ @Test
+ void a31_stripInvalidHttpHeaderChars() {
assertEquals("xxx", stripInvalidHttpHeaderChars("xxx"));
assertEquals("\t []^x", stripInvalidHttpHeaderChars("\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u0020\\[]^x"));
}
@@ -939,7 +929,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// abbreviate(String,int)
//====================================================================================================
- @Test void a32_abbrevate() {
+ @Test
+ void a32_abbrevate() {
assertNull(abbreviate(null, 0));
assertEquals("foo", abbreviate("foo", 3));
assertEquals("...", abbreviate("fooo", 3));
@@ -950,10 +941,11 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// splitMethodArgs(String)
//====================================================================================================
- @Test void a33_splitMethodArgs() {
+ @Test
+ void a33_splitMethodArgs() {
assertList(StringUtils.splitMethodArgs("java.lang.String"), "java.lang.String");
assertList(StringUtils.splitMethodArgs("java.lang.String,java.lang.Integer"), "java.lang.String", "java.lang.Integer");
- assertList(StringUtils.splitMethodArgs("x,y"), "x","y");
+ assertList(StringUtils.splitMethodArgs("x,y"), "x", "y");
assertList(StringUtils.splitMethodArgs("x,y<a,b>,z"), "x", "y<a,b>", "z");
assertList(StringUtils.splitMethodArgs("x,y<a<b,c>,d<e,f>>,z"), "x", "y<a<b,c>,d<e,f>>", "z");
}
@@ -961,7 +953,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// fixUrl(String)
//====================================================================================================
- @Test void a34_fixUrl() {
+ @Test
+ void a34_fixUrl() {
assertEquals(null, fixUrl(null));
assertEquals("", fixUrl(""));
assertEquals("xxx", fixUrl("xxx"));
@@ -974,7 +967,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// diffPosition(String,String)
//====================================================================================================
- @Test void a35_diffPosition() {
+ @Test
+ void a35_diffPosition() {
assertEquals(-1, diffPosition("a", "a"));
assertEquals(-1, diffPosition(null, null));
assertEquals(0, diffPosition("a", "b"));
@@ -988,7 +982,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// diffPositionIc(String,String)
//====================================================================================================
- @Test void a36_diffPositionIc() {
+ @Test
+ void a36_diffPositionIc() {
assertEquals(-1, diffPositionIc("a", "a"));
assertEquals(-1, diffPositionIc("a", "A"));
assertEquals(-1, diffPositionIc(null, null));
@@ -1003,7 +998,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// splitNested(String)
//====================================================================================================
- @Test void a37_splitNested() {
+ @Test
+ void a37_splitNested() {
assertNull(StringUtils.splitNested(null));
assertEmpty(StringUtils.splitNested(""));
assertList(StringUtils.splitNested("a"), "a");
@@ -1019,9 +1015,10 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// splitNestedInner(String)
//====================================================================================================
- @Test void a38_splitNestedInner() {
- assertThrowsWithMessage(IllegalArgumentException.class, "String was null.", ()->StringUtils.splitNestedInner(null));
- assertThrowsWithMessage(IllegalArgumentException.class, "String was empty.", ()->StringUtils.splitNestedInner(""));
+ @Test
+ void a38_splitNestedInner() {
+ assertThrowsWithMessage(IllegalArgumentException.class, "String was null.", () -> StringUtils.splitNestedInner(null));
+ assertThrowsWithMessage(IllegalArgumentException.class, "String was empty.", () -> StringUtils.splitNestedInner(""));
assertList(StringUtils.splitNestedInner("a{b}"), "b");
assertList(StringUtils.splitNestedInner(" a { b } "), "b");
assertList(StringUtils.splitNestedInner("a{b,c}"), "b", "c");
@@ -1031,7 +1028,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toHex2(int)
//====================================================================================================
- @Test void a38_toHex2() {
+ @Test
+ void a38_toHex2() {
// Test zero
assertString("00", toHex2(0));
@@ -1045,14 +1043,14 @@ class StringUtils_Test extends TestBase {
assertString("FF", toHex2(255));
// Test values outside valid range - should throw exception
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(256));
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(511));
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(Integer.MAX_VALUE));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(256));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(511));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(Integer.MAX_VALUE));
// Test negative numbers - should throw exception
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(-1));
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(-100));
- assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", ()->toHex2(Integer.MIN_VALUE));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(-1));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(-100));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex2 can only be used on numbers between 0 and 255", () -> toHex2(Integer.MIN_VALUE));
// Test edge cases
assertString("0A", toHex2(10));
@@ -1065,7 +1063,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toHex4(int)
//====================================================================================================
- @Test void a39_toHex4() {
+ @Test
+ void a39_toHex4() {
// Test zero
assertString("0000", toHex4(0));
@@ -1091,9 +1090,9 @@ class StringUtils_Test extends TestBase {
assertString("FFFF", toHex4(Integer.MAX_VALUE));
// Test negative numbers - should throw exception
- assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", ()->toHex4(-1));
- assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", ()->toHex4(-100));
- assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", ()->toHex4(Integer.MIN_VALUE));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", () -> toHex4(-1));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", () -> toHex4(-100));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex4 can only be used on non-negative numbers", () -> toHex4(Integer.MIN_VALUE));
// Test edge cases
assertString("000A", toHex4(10));
@@ -1106,7 +1105,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toHex8(long)
//====================================================================================================
- @Test void a40_toHex8() {
+ @Test
+ void a40_toHex8() {
// Test zero
assertString("00000000", toHex8(0));
@@ -1135,9 +1135,9 @@ class StringUtils_Test extends TestBase {
assertString("FFFFFFFF", toHex8(Long.MAX_VALUE));
// Test negative numbers - should throw exception
- assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", ()->toHex8(-1));
- assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", ()->toHex8(-100));
- assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", ()->toHex8(Long.MIN_VALUE));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", () -> toHex8(-1));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", () -> toHex8(-100));
+ assertThrowsWithMessage(NumberFormatException.class, "toHex8 can only be used on non-negative numbers", () -> toHex8(Long.MIN_VALUE));
// Test edge cases
assertString("0000000A", toHex8(10));
@@ -1151,7 +1151,8 @@ class StringUtils_Test extends TestBase {
// String validation methods
//====================================================================================================
- @Test void a41_isBlank() {
+ @Test
+ void a41_isBlank() {
assertTrue(StringUtils.isBlank(null));
assertTrue(StringUtils.isBlank(""));
assertTrue(StringUtils.isBlank(" "));
@@ -1161,7 +1162,8 @@ class StringUtils_Test extends TestBase {
assertFalse(StringUtils.isBlank("a"));
}
- @Test void a42_isNotBlank() {
+ @Test
+ void a42_isNotBlank() {
assertFalse(isNotBlank(null));
assertFalse(isNotBlank(""));
assertFalse(isNotBlank(" "));
@@ -1171,7 +1173,8 @@ class StringUtils_Test extends TestBase {
assertTrue(isNotBlank("a"));
}
- @Test void a43_isEmpty() {
+ @Test
+ void a43_isEmpty() {
assertTrue(Utils.isEmpty((String)null));
assertTrue(Utils.isEmpty(""));
assertFalse(Utils.isEmpty(" "));
@@ -1179,7 +1182,8 @@ class StringUtils_Test extends TestBase {
assertFalse(Utils.isEmpty("a"));
}
- @Test void a44_hasText() {
+ @Test
+ void a44_hasText() {
assertFalse(hasText(null));
assertFalse(hasText(""));
assertFalse(hasText(" "));
@@ -1189,7 +1193,8 @@ class StringUtils_Test extends TestBase {
assertTrue(hasText("a"));
}
- @Test void a45_isAlpha() {
+ @Test
+ void a45_isAlpha() {
assertFalse(isAlpha(null));
assertFalse(isAlpha(""));
assertTrue(isAlpha("abc"));
@@ -1201,7 +1206,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isAlpha("123"));
}
- @Test void a46_isAlphaNumeric() {
+ @Test
+ void a46_isAlphaNumeric() {
assertFalse(isAlphaNumeric(null));
assertFalse(isAlphaNumeric(""));
assertTrue(isAlphaNumeric("abc"));
@@ -1213,7 +1219,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isAlphaNumeric("abc_123"));
}
- @Test void a47_isDigit() {
+ @Test
+ void a47_isDigit() {
assertFalse(isDigit(null));
assertFalse(isDigit(""));
assertTrue(isDigit("123"));
@@ -1225,7 +1232,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isDigit("12-3"));
}
- @Test void a48_isWhitespace() {
+ @Test
+ void a48_isWhitespace() {
assertFalse(isWhitespace(null));
assertTrue(isWhitespace(""));
assertTrue(isWhitespace(" "));
@@ -1235,7 +1243,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isWhitespace("hello"));
}
- @Test void a49_isEmpty() {
+ @Test
+ void a49_isEmpty() {
assertTrue(StringUtils.isEmpty(null));
assertTrue(StringUtils.isEmpty(""));
assertFalse(StringUtils.isEmpty(" "));
@@ -1243,7 +1252,8 @@ class StringUtils_Test extends TestBase {
assertFalse(StringUtils.isEmpty("hello"));
}
- @Test void a50_isEmail() {
+ @Test
+ void a50_isEmail() {
assertFalse(isEmail(null));
assertFalse(isEmail(""));
assertFalse(isEmail(" "));
@@ -1267,7 +1277,8 @@ class StringUtils_Test extends TestBase {
assertTrue(isEmail("user.name+tag+sorting@example.com"));
}
- @Test void a51_isPhoneNumber() {
+ @Test
+ void a51_isPhoneNumber() {
assertFalse(isPhoneNumber(null));
assertFalse(isPhoneNumber(""));
assertFalse(isPhoneNumber(" "));
@@ -1289,7 +1300,8 @@ class StringUtils_Test extends TestBase {
assertTrue(isPhoneNumber("+1 (123) 456-7890"));
}
- @Test void a52_isCreditCard() {
+ @Test
+ void a52_isCreditCard() {
assertFalse(isCreditCard(null));
assertFalse(isCreditCard(""));
assertFalse(isCreditCard(" "));
@@ -1310,7 +1322,8 @@ class StringUtils_Test extends TestBase {
assertTrue(isCreditCard("5555555555554444")); // MasterCard test card
}
- @Test void a53_indexOf() {
+ @Test
+ void a53_indexOf() {
assertEquals(6, StringUtils.indexOf("hello world", "world"));
assertEquals(0, StringUtils.indexOf("hello world", "hello"));
assertEquals(2, StringUtils.indexOf("hello world", "llo"));
@@ -1322,7 +1335,8 @@ class StringUtils_Test extends TestBase {
assertEquals(-1, StringUtils.indexOf("hello", "hello world"));
}
- @Test void a54_indexOfIgnoreCase() {
+ @Test
+ void a54_indexOfIgnoreCase() {
assertEquals(6, indexOfIgnoreCase("Hello World", "world"));
assertEquals(6, indexOfIgnoreCase("Hello World", "WORLD"));
assertEquals(0, indexOfIgnoreCase("Hello World", "hello"));
@@ -1334,7 +1348,8 @@ class StringUtils_Test extends TestBase {
assertEquals(-1, indexOfIgnoreCase(null, null));
}
- @Test void a55_lastIndexOf() {
+ @Test
+ void a55_lastIndexOf() {
assertEquals(12, lastIndexOf("hello world world", "world"));
assertEquals(6, lastIndexOf("hello world", "world"));
assertEquals(0, lastIndexOf("hello world", "hello"));
@@ -1345,7 +1360,8 @@ class StringUtils_Test extends TestBase {
assertEquals(4, lastIndexOf("ababab", "ab")); // "ab" appears at positions 0, 2, 4
}
- @Test void a56_lastIndexOfIgnoreCase() {
+ @Test
+ void a56_lastIndexOfIgnoreCase() {
assertEquals(12, lastIndexOfIgnoreCase("Hello World World", "world"));
assertEquals(12, lastIndexOfIgnoreCase("Hello World World", "WORLD"));
assertEquals(6, lastIndexOfIgnoreCase("Hello World", "world"));
@@ -1358,7 +1374,8 @@ class StringUtils_Test extends TestBase {
assertEquals(4, lastIndexOfIgnoreCase("AbAbAb", "ab"));
}
- @Test void a57_containsIgnoreCase() {
+ @Test
+ void a57_containsIgnoreCase() {
assertTrue(containsIgnoreCase("Hello World", "world"));
assertTrue(containsIgnoreCase("Hello World", "WORLD"));
assertTrue(containsIgnoreCase("Hello World", "hello"));
@@ -1371,7 +1388,8 @@ class StringUtils_Test extends TestBase {
assertTrue(containsIgnoreCase("Hello", "hello"));
}
- @Test void a58_startsWithIgnoreCase() {
+ @Test
+ void a58_startsWithIgnoreCase() {
assertTrue(startsWithIgnoreCase("Hello World", "hello"));
assertTrue(startsWithIgnoreCase("Hello World", "HELLO"));
assertTrue(startsWithIgnoreCase("Hello World", "Hello"));
@@ -1384,7 +1402,8 @@ class StringUtils_Test extends TestBase {
assertTrue(startsWithIgnoreCase("Hello", "hello"));
}
- @Test void a59_endsWithIgnoreCase() {
+ @Test
+ void a59_endsWithIgnoreCase() {
assertTrue(endsWithIgnoreCase("Hello World", "world"));
assertTrue(endsWithIgnoreCase("Hello World", "WORLD"));
assertTrue(endsWithIgnoreCase("Hello World", "World"));
@@ -1397,7 +1416,8 @@ class StringUtils_Test extends TestBase {
assertTrue(endsWithIgnoreCase("Hello", "hello"));
}
- @Test void a60_matches() {
+ @Test
+ void a60_matches() {
assertTrue(matches("12345", "\\d+"));
assertTrue(matches("abc123", "^[a-z]+\\d+$"));
assertTrue(matches("hello", "^hello$"));
@@ -1411,7 +1431,8 @@ class StringUtils_Test extends TestBase {
assertThrows(java.util.regex.PatternSyntaxException.class, () -> matches("test", "["));
}
- @Test void a61_countMatches() {
+ @Test
+ void a61_countMatches() {
assertEquals(2, countMatches("hello world world", "world"));
assertEquals(3, countMatches("ababab", "ab"));
assertEquals(4, countMatches("aaaa", "a"));
@@ -1428,8 +1449,9 @@ class StringUtils_Test extends TestBase {
assertEquals(2, countMatches("aaaa", "aa")); // "aa" appears at positions 0 and 2
}
- @Test void a62_formatWithNamedArgs() {
- var args = new HashMap<String, Object>();
+ @Test
+ void a62_formatWithNamedArgs() {
+ var args = new HashMap<String,Object>();
args.put("name", "John");
args.put("age", 30);
args.put("city", "New York");
@@ -1441,14 +1463,73 @@ class StringUtils_Test extends TestBase {
assertEquals("Template", formatWithNamedArgs("Template", null));
assertEquals("Template", formatWithNamedArgs("Template", new HashMap<>()));
// Test with null values
- var argsWithNull = new HashMap<String, Object>();
+ var argsWithNull = new HashMap<String,Object>();
argsWithNull.put("name", "John");
argsWithNull.put("value", null);
assertEquals("Hello John, value: ", formatWithNamedArgs("Hello {name}, value: {value}", argsWithNull));
}
- @Test void a63_interpolate() {
- var vars = new HashMap<String, Object>();
+ //====================================================================================================
+ // format(String, Object...)
+ //====================================================================================================
+ @Test
+ void a62b_format() {
+ // Basic string and number formatting
+ assertEquals("Hello John, you have 5 items", format("Hello %s, you have %d items", "John", 5));
+ assertEquals("Hello world", format("Hello %s", "world"));
+
+ // Floating point with precision
+ assertEquals("Price: $19.99", format("Price: $%.2f", 19.99));
+ assertEquals("Value: 3.14", format("Value: %.2f", 3.14159));
+ assertEquals("Value: 3.142", format("Value: %.3f", 3.14159));
+
+ // Width and alignment
+ assertEquals("Name: John Age: 25", format("Name: %-20s Age: %3d", "John", 25));
+ assertEquals("Name: John Age: 25", format("Name: %20s Age: %3d", "John", 25));
+ assertEquals("Number: 42", format("Number: %4d", 42));
+ assertEquals("Number: 0042", format("Number: %04d", 42));
+
+ // Hexadecimal
+ assertEquals("Color: #FF5733", format("Color: #%06X", 0xFF5733));
+ assertEquals("Hex: ff5733", format("Hex: %x", 0xFF5733));
+ assertEquals("Hex: FF5733", format("Hex: %X", 0xFF5733));
+ assertEquals("Hex: 255", format("Hex: %d", 0xFF));
+
+ // Octal
+ assertEquals("Octal: 377", format("Octal: %o", 255));
+
+ // Scientific notation
+ assertEquals("Value: 1.23e+06", format("Value: %.2e", 1234567.0));
+ assertEquals("Value: 1.23E+06", format("Value: %.2E", 1234567.0));
+
+ // Boolean
+ assertEquals("Flag: true", format("Flag: %b", true));
+ assertEquals("Flag: false", format("Flag: %b", false));
+ assertEquals("Flag: true", format("Flag: %b", "anything"));
+
+ // Character
+ assertEquals("Char: A", format("Char: %c", 'A'));
+ assertEquals("Char: A", format("Char: %c", 65));
+
+ // Argument index (reuse arguments)
+ assertEquals("Alice loves Bob, and Alice also loves Charlie", format("%1$s loves %2$s, and %1$s also loves %3$s", "Alice", "Bob", "Charlie"));
+
+ // Literal percent sign
+ assertEquals("Progress: 50%", format("Progress: %d%%", 50));
+ assertEquals("Discount: 25% off", format("Discount: %d%% off", 25));
+
+ // Line separator
+ var result = format("Line 1%nLine 2");
+ assertTrue(result.contains("Line 1"));
+ assertTrue(result.contains("Line 2"));
+
+ // Multiple format specifiers
+ assertEquals("Name: John, Age: 30, Salary: $50000.00", format("Name: %s, Age: %d, Salary: $%.2f", "John", 30, 50000.0));
+ }
+
+ @Test
+ void a63_interpolate() {
+ var vars = new HashMap<String,Object>();
vars.put("name", "John");
vars.put("city", "New York");
vars.put("age", 30);
@@ -1460,18 +1541,18 @@ class StringUtils_Test extends TestBase {
assertEquals("Template", interpolate("Template", null));
assertEquals("Template", interpolate("Template", new HashMap<>()));
// Test with null values
- var varsWithNull = new HashMap<String, Object>();
+ var varsWithNull = new HashMap<String,Object>();
varsWithNull.put("name", "John");
varsWithNull.put("value", null);
assertEquals("Hello John, value: null", interpolate("Hello ${name}, value: ${value}", varsWithNull));
// Test with incomplete placeholder
assertEquals("Hello ${name", interpolate("Hello ${name", vars));
// Test with multiple variables
- assertEquals("John is 30 years old and lives in New York",
- interpolate("${name} is ${age} years old and lives in ${city}", vars));
+ assertEquals("John is 30 years old and lives in New York", interpolate("${name} is ${age} years old and lives in ${city}", vars));
}
- @Test void a64_pluralize() {
+ @Test
+ void a64_pluralize() {
// Singular (count = 1)
assertEquals("cat", pluralize("cat", 1));
assertEquals("box", pluralize("box", 1));
@@ -1509,7 +1590,8 @@ class StringUtils_Test extends TestBase {
assertEquals("as", pluralize("a", 2));
}
- @Test void a65_ordinal() {
+ @Test
+ void a65_ordinal() {
// Basic ordinals
assertEquals("1st", ordinal(1));
assertEquals("2nd", ordinal(2));
@@ -1552,7 +1634,8 @@ class StringUtils_Test extends TestBase {
assertEquals("0th", ordinal(0));
}
- @Test void a66_sanitize() {
+ @Test
+ void a66_sanitize() {
assertNull(sanitize(null));
assertEquals("", sanitize(""));
assertEquals("Hello World", sanitize("Hello World"));
@@ -1561,7 +1644,8 @@ class StringUtils_Test extends TestBase {
assertEquals("<img src="x" onerror="alert(1)">", sanitize("<img src=\"x\" onerror=\"alert(1)\">"));
}
- @Test void a67_escapeHtml() {
+ @Test
+ void a67_escapeHtml() {
assertNull(escapeHtml(null));
assertEquals("", escapeHtml(""));
assertEquals("Hello World", escapeHtml("Hello World"));
@@ -1578,7 +1662,8 @@ class StringUtils_Test extends TestBase {
assertEquals("'", escapeHtml("'"));
}
- @Test void a68_unescapeHtml() {
+ @Test
+ void a68_unescapeHtml() {
assertNull(unescapeHtml(null));
assertEquals("", unescapeHtml(""));
assertEquals("Hello World", unescapeHtml("Hello World"));
@@ -1593,7 +1678,8 @@ class StringUtils_Test extends TestBase {
assertEquals("<script>alert('xss')</script>", unescapeHtml(escapeHtml("<script>alert('xss')</script>")));
}
- @Test void a69_escapeXml() {
+ @Test
+ void a69_escapeXml() {
assertNull(escapeXml(null));
assertEquals("", escapeXml(""));
assertEquals("Hello World", escapeXml("Hello World"));
@@ -1610,7 +1696,8 @@ class StringUtils_Test extends TestBase {
assertEquals("'", escapeXml("'"));
}
- @Test void a70_unescapeXml() {
+ @Test
+ void a70_unescapeXml() {
assertNull(unescapeXml(null));
assertEquals("", unescapeXml(""));
assertEquals("Hello World", unescapeXml("Hello World"));
@@ -1624,7 +1711,8 @@ class StringUtils_Test extends TestBase {
assertEquals("<tag>text</tag>", unescapeXml(escapeXml("<tag>text</tag>")));
}
- @Test void a71_escapeSql() {
+ @Test
+ void a71_escapeSql() {
assertNull(escapeSql(null));
assertEquals("", escapeSql(""));
assertEquals("Hello World", escapeSql("Hello World"));
@@ -1635,7 +1723,8 @@ class StringUtils_Test extends TestBase {
assertEquals("John''s book", escapeSql("John's book"));
}
- @Test void a72_escapeRegex() {
+ @Test
+ void a72_escapeRegex() {
assertNull(escapeRegex(null));
assertEquals("", escapeRegex(""));
assertEquals("Hello World", escapeRegex("Hello World"));
@@ -1657,7 +1746,8 @@ class StringUtils_Test extends TestBase {
assertFalse(pattern.matcher("filextxt").matches());
}
- @Test void a73_equalsIgnoreCase() {
+ @Test
+ void a73_equalsIgnoreCase() {
assertTrue(equalsIgnoreCase("Hello", "hello"));
assertTrue(equalsIgnoreCase("HELLO", "hello"));
assertTrue(equalsIgnoreCase("Hello", "HELLO"));
@@ -1669,7 +1759,8 @@ class StringUtils_Test extends TestBase {
assertTrue(equalsIgnoreCase("", ""));
}
- @Test void a74_compareIgnoreCase() {
+ @Test
+ void a74_compareIgnoreCase() {
assertTrue(compareIgnoreCase("apple", "BANANA") < 0);
assertTrue(compareIgnoreCase("BANANA", "apple") > 0);
assertEquals(0, compareIgnoreCase("Hello", "hello"));
@@ -1681,7 +1772,8 @@ class StringUtils_Test extends TestBase {
assertTrue(compareIgnoreCase("test", null) > 0);
}
- @Test void a75_naturalCompare() {
+ @Test
+ void a75_naturalCompare() {
// Numbers should be compared numerically
assertTrue(naturalCompare("file2.txt", "file10.txt") < 0);
assertTrue(naturalCompare("file10.txt", "file2.txt") > 0);
@@ -1710,7 +1802,8 @@ class StringUtils_Test extends TestBase {
assertTrue(naturalCompare("banana", "Apple") > 0);
}
- @Test void a76_levenshteinDistance() {
+ @Test
+ void a76_levenshteinDistance() {
assertEquals(0, levenshteinDistance("hello", "hello"));
assertEquals(3, levenshteinDistance("kitten", "sitting")); // kitten -> sitten (s), sitten -> sittin (i), sittin -> sitting (g)
assertEquals(3, levenshteinDistance("abc", ""));
@@ -1725,11 +1818,12 @@ class StringUtils_Test extends TestBase {
assertEquals(5, levenshteinDistance(null, "hello"));
}
- @Test void a77_similarity() {
+ @Test
+ void a77_similarity() {
assertEquals(1.0, similarity("hello", "hello"), 0.0001);
assertEquals(0.0, similarity("abc", "xyz"), 0.0001);
// kitten -> sitting: distance = 3, maxLen = 7, similarity = 1 - 3/7 = 4/7 ≈ 0.571
- assertEquals(4.0/7.0, similarity("kitten", "sitting"), 0.01);
+ assertEquals(4.0 / 7.0, similarity("kitten", "sitting"), 0.01);
assertEquals(1.0, similarity("", ""), 0.0001);
assertEquals(0.0, similarity("abc", ""), 0.0001);
assertEquals(0.0, similarity("", "abc"), 0.0001);
@@ -1742,7 +1836,8 @@ class StringUtils_Test extends TestBase {
assertEquals(0.8, similarity("hello", "hallo"), 0.0001);
}
- @Test void a78_isSimilar() {
+ @Test
+ void a78_isSimilar() {
assertTrue(isSimilar("hello", "hello", 0.8));
assertTrue(isSimilar("hello", "hello", 1.0));
assertFalse(isSimilar("kitten", "sitting", 0.8));
@@ -1754,7 +1849,8 @@ class StringUtils_Test extends TestBase {
assertTrue(isSimilar(null, null, 0.8));
}
- @Test void a79_generateUUID() {
+ @Test
+ void a79_generateUUID() {
// Generate multiple UUIDs and verify format
for (var i = 0; i < 10; i++) {
var uuid = generateUUID();
@@ -1769,7 +1865,8 @@ class StringUtils_Test extends TestBase {
assertNotEquals(uuid1, uuid2);
}
- @Test void a80_randomAlphabetic() {
+ @Test
+ void a80_randomAlphabetic() {
// Test various lengths
for (var len = 0; len <= 20; len++) {
var s = randomAlphabetic(len);
@@ -1791,7 +1888,8 @@ class StringUtils_Test extends TestBase {
assertTrue(strings.size() > 1, "Should generate different strings");
}
- @Test void a81_randomAlphanumeric() {
+ @Test
+ void a81_randomAlphanumeric() {
// Test various lengths
for (var len = 0; len <= 20; len++) {
var s = randomAlphanumeric(len);
@@ -1817,15 +1915,19 @@ class StringUtils_Test extends TestBase {
for (var i = 0; i < 1000; i++) {
var s = randomAlphanumeric(20);
for (var j = 0; j < s.length(); j++) {
- if (Character.isLetter(s.charAt(j))) hasLetter = true;
- if (Character.isDigit(s.charAt(j))) hasDigit = true;
+ if (Character.isLetter(s.charAt(j)))
+ hasLetter = true;
+ if (Character.isDigit(s.charAt(j)))
+ hasDigit = true;
}
- if (hasLetter && hasDigit) break;
+ if (hasLetter && hasDigit)
+ break;
}
assertTrue(hasLetter && hasDigit, "Should generate both letters and digits");
}
- @Test void a82_randomNumeric() {
+ @Test
+ void a82_randomNumeric() {
// Test various lengths
for (var len = 0; len <= 20; len++) {
var s = randomNumeric(len);
@@ -1847,7 +1949,8 @@ class StringUtils_Test extends TestBase {
assertTrue(strings.size() > 1, "Should generate different strings");
}
- @Test void a83_randomAscii() {
+ @Test
+ void a83_randomAscii() {
// Test various lengths
for (var len = 0; len <= 20; len++) {
var s = randomAscii(len);
@@ -1869,7 +1972,8 @@ class StringUtils_Test extends TestBase {
assertTrue(strings.size() > 1, "Should generate different strings");
}
- @Test void a84_randomString() {
+ @Test
+ void a84_randomString() {
// Test with various character sets
var s1 = randomString(10, "ABC");
assertNotNull(s1);
@@ -1912,7 +2016,8 @@ class StringUtils_Test extends TestBase {
assertTrue(strings.size() > 1, "Should generate different strings");
}
- @Test void a85_parseMap() {
+ @Test
+ void a85_parseMap() {
// Basic parsing
var map1 = parseMap("key1=value1,key2=value2", '=', ',', false);
assertEquals(2, map1.size());
@@ -1954,7 +2059,8 @@ class StringUtils_Test extends TestBase {
assertEquals("value2", map6.get("key"));
}
- @Test void a86_extractNumbers() {
+ @Test
+ void a86_extractNumbers() {
// Basic extraction
var numbers1 = extractNumbers("Price: $19.99, Quantity: 5");
assertEquals(2, numbers1.size());
@@ -1988,7 +2094,8 @@ class StringUtils_Test extends TestBase {
assertTrue(extractNumbers("").isEmpty());
}
- @Test void a87_extractEmails() {
+ @Test
+ void a87_extractEmails() {
// Basic extraction
var emails1 = extractEmails("Contact: user@example.com or admin@test.org");
assertEquals(2, emails1.size());
@@ -2014,7 +2121,8 @@ class StringUtils_Test extends TestBase {
assertTrue(extractEmails("").isEmpty());
}
- @Test void a88_extractUrls() {
+ @Test
+ void a88_extractUrls() {
// Basic extraction
var urls1 = extractUrls("Visit https://example.com or http://test.org");
assertEquals(2, urls1.size());
@@ -2043,7 +2151,8 @@ class StringUtils_Test extends TestBase {
assertTrue(extractUrls("").isEmpty());
}
- @Test void a89_extractWords() {
+ @Test
+ void a89_extractWords() {
// Basic extraction
var words1 = extractWords("Hello world! This is a test.");
assertEquals(6, words1.size());
@@ -2080,7 +2189,8 @@ class StringUtils_Test extends TestBase {
assertTrue(extractWords("").isEmpty());
}
- @Test void a90_extractBetween() {
+ @Test
+ void a90_extractBetween() {
// Basic extraction
var results1 = extractBetween("<tag>content</tag>", "<", ">");
assertEquals(2, results1.size());
@@ -2120,7 +2230,8 @@ class StringUtils_Test extends TestBase {
assertTrue(extractBetween("text", "<", null).isEmpty());
}
- @Test void a91_transliterate() {
+ @Test
+ void a91_transliterate() {
// Basic transliteration
assertEquals("h2ll4", transliterate("hello", "aeiou", "12345"));
assertEquals("XYZ", transliterate("ABC", "ABC", "XYZ"));
@@ -2147,7 +2258,8 @@ class StringUtils_Test extends TestBase {
assertThrows(IllegalArgumentException.class, () -> transliterate("hello", "abc", "12"));
}
- @Test void a92_soundex() {
+ @Test
+ void a92_soundex() {
// Standard Soundex examples
assertEquals("S530", soundex("Smith"));
assertEquals("S530", soundex("Smythe"));
@@ -2173,7 +2285,8 @@ class StringUtils_Test extends TestBase {
assertEquals("A000", soundex("AEIOU"));
}
- @Test void a93_metaphone() {
+ @Test
+ void a93_metaphone() {
// Basic metaphone examples
var code1 = metaphone("Smith");
assertNotNull(code1);
@@ -2198,7 +2311,8 @@ class StringUtils_Test extends TestBase {
assertFalse(code4.isEmpty());
}
- @Test void a94_doubleMetaphone() {
+ @Test
+ void a94_doubleMetaphone() {
// Basic double metaphone
var codes1 = doubleMetaphone("Smith");
assertNotNull(codes1);
@@ -2215,7 +2329,8 @@ class StringUtils_Test extends TestBase {
assertNull(doubleMetaphone(""));
}
- @Test void a95_normalizeUnicode() {
+ @Test
+ void a95_normalizeUnicode() {
// Basic normalization
var normalized = normalizeUnicode("café");
assertNotNull(normalized);
@@ -2229,7 +2344,8 @@ class StringUtils_Test extends TestBase {
assertEquals("hello", normalized2);
}
- @Test void a96_removeAccents() {
+ @Test
+ void a96_removeAccents() {
// Basic accent removal
assertEquals("cafe", removeAccents("café"));
assertEquals("naive", removeAccents("naïve"));
@@ -2254,7 +2370,8 @@ class StringUtils_Test extends TestBase {
assertEquals("Ecole", removeAccents("École"));
}
- @Test void a97_isValidRegex() {
+ @Test
+ void a97_isValidRegex() {
// Valid regex patterns
assertTrue(isValidRegex("[a-z]+"));
assertTrue(isValidRegex("\\d+"));
@@ -2272,7 +2389,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidRegex(""));
}
- @Test void a98_isValidDateFormat() {
+ @Test
+ void a98_isValidDateFormat() {
// Valid dates
assertTrue(isValidDateFormat("2023-12-25", "yyyy-MM-dd"));
assertTrue(isValidDateFormat("25/12/2023", "dd/MM/yyyy"));
@@ -2292,7 +2410,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidDateFormat("2023-12-25", ""));
}
- @Test void a99_isValidTimeFormat() {
+ @Test
+ void a99_isValidTimeFormat() {
// Valid times
assertTrue(isValidTimeFormat("14:30:00", "HH:mm:ss"));
assertTrue(isValidTimeFormat("02:30:00 PM", "hh:mm:ss a"));
@@ -2312,7 +2431,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidTimeFormat("14:30:00", ""));
}
- @Test void a100_isValidIpAddress() {
+ @Test
+ void a100_isValidIpAddress() {
// Valid IPv4 addresses
assertTrue(isValidIpAddress("192.168.1.1"));
assertTrue(isValidIpAddress("0.0.0.0"));
@@ -2340,7 +2460,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidIpAddress(""));
}
- @Test void a101_isValidMacAddress() {
+ @Test
+ void a101_isValidMacAddress() {
// Valid MAC addresses - colon format
assertTrue(isValidMacAddress("00:1B:44:11:3A:B7"));
assertTrue(isValidMacAddress("00:1b:44:11:3a:b7")); // Lowercase
@@ -2365,7 +2486,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidMacAddress(""));
}
- @Test void a102_isValidHostname() {
+ @Test
+ void a102_isValidHostname() {
// Valid hostnames
assertTrue(isValidHostname("example.com"));
assertTrue(isValidHostname("sub.example.com"));
@@ -2396,7 +2518,8 @@ class StringUtils_Test extends TestBase {
assertFalse(isValidHostname(""));
}
- @Test void a103_wordCount() {
+ @Test
+ void a103_wordCount() {
// Basic word counting
assertEquals(2, wordCount("Hello world"));
assertEquals(4, wordCount("The quick brown fox"));
@@ -2424,7 +2547,8 @@ class StringUtils_Test extends TestBase {
assertEquals(0, wordCount("!@#$%^&*()"));
}
- @Test void a104_lineCount() {
+ @Test
+ void a104_lineCount() {
// Basic line counting
assertEquals(3, lineCount("line1\nline2\nline3"));
assertEquals(1, lineCount("single line"));
@@ -2448,7 +2572,8 @@ class StringUtils_Test extends TestBase {
assertEquals(0, lineCount(""));
}
- @Test void a105_mostFrequentChar() {
+ @Test
+ void a105_mostFrequentChar() {
// Basic frequency
assertEquals('l', mostFrequentChar("hello"));
assertEquals('a', mostFrequentChar("aabbcc")); // First encountered
@@ -2473,7 +2598,8 @@ class StringUtils_Test extends TestBase {
assertEquals('\0', mostFrequentChar(""));
}
- @Test void a106_entropy() {
+ @Test
+ void a106_entropy() {
// No randomness (all same character)
assertEquals(0.0, entropy("aaaa"), 0.0001);
@@ -2501,7 +2627,8 @@ class StringUtils_Test extends TestBase {
assertTrue(entropy4 > entropy("hello"));
}
- @Test void a107_readabilityScore() {
+ @Test
+ void a107_readabilityScore() {
// Simple sentence (should have higher score)
var score1 = readabilityScore("The cat sat.");
assertTrue(score1 > 0.0 && score1 <= 100.0);
@@ -2539,7 +2666,8 @@ class StringUtils_Test extends TestBase {
// String manipulation methods
//====================================================================================================
- @Test void a49_capitalize() {
+ @Test
+ void a49_capitalize() {
assertNull(capitalize(null));
assertEquals("", capitalize(""));
assertEquals("Hello", capitalize("hello"));
@@ -2549,7 +2677,8 @@ class StringUtils_Test extends TestBase {
assertEquals("123", capitalize("123"));
}
- @Test void a50_uncapitalize() {
+ @Test
+ void a50_uncapitalize() {
assertNull(uncapitalize(null));
assertEquals("", uncapitalize(""));
assertEquals("hello", uncapitalize("hello"));
@@ -2559,7 +2688,8 @@ class StringUtils_Test extends TestBase {
assertEquals("123", uncapitalize("123"));
}
- @Test void a51_reverse() {
+ @Test
+ void a51_reverse() {
assertNull(StringUtils.reverse(null));
assertEquals("", reverse(""));
assertEquals("olleh", reverse("hello"));
@@ -2567,7 +2697,8 @@ class StringUtils_Test extends TestBase {
assertEquals("cba", reverse("abc"));
}
- @Test void a52_remove() {
+ @Test
+ void a52_remove() {
assertNull(remove(null, "x"));
assertEquals("hello", remove("hello", null));
assertEquals("hello", remove("hello", ""));
@@ -2576,7 +2707,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", remove("xxx", "x"));
}
- @Test void a53_removeStart() {
+ @Test
+ void a53_removeStart() {
assertNull(removeStart(null, "x"));
assertEquals("hello", removeStart("hello", null));
assertEquals("hello", removeStart("hello", ""));
@@ -2585,7 +2717,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", removeStart("hello", "hello"));
}
- @Test void a54_removeEnd() {
+ @Test
+ void a54_removeEnd() {
assertNull(removeEnd(null, "x"));
assertEquals("hello", removeEnd("hello", null));
assertEquals("hello", removeEnd("hello", ""));
@@ -2594,7 +2727,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", removeEnd("hello", "hello"));
}
- @Test void a55_substringBefore() {
+ @Test
+ void a55_substringBefore() {
assertNull(substringBefore(null, "."));
assertEquals("hello.world", substringBefore("hello.world", null));
assertEquals("hello", substringBefore("hello.world", "."));
@@ -2602,7 +2736,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", substringBefore(".world", "."));
}
- @Test void a56_substringAfter() {
+ @Test
+ void a56_substringAfter() {
assertNull(substringAfter(null, "."));
assertEquals("", substringAfter("hello.world", null));
assertEquals("world", substringAfter("hello.world", "."));
@@ -2610,7 +2745,8 @@ class StringUtils_Test extends TestBase {
assertEquals("world", substringAfter("hello.world", "."));
}
- @Test void a57_substringBetween() {
+ @Test
+ void a57_substringBetween() {
assertNull(substringBetween(null, "<", ">"));
assertNull(substringBetween("<hello>", null, ">"));
assertNull(substringBetween("<hello>", "<", null));
@@ -2620,7 +2756,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", substringBetween("<>", "<", ">"));
}
- @Test void a58_left() {
+ @Test
+ void a58_left() {
assertNull(left(null, 3));
assertEquals("", left("", 3));
assertEquals("hel", left("hello", 3));
@@ -2629,7 +2766,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", left("hello", -1));
}
- @Test void a59_right() {
+ @Test
+ void a59_right() {
assertNull(right(null, 3));
assertEquals("", right("", 3));
assertEquals("llo", right("hello", 3));
@@ -2638,7 +2776,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", right("hello", -1));
}
- @Test void a60_mid() {
+ @Test
+ void a60_mid() {
assertNull(mid(null, 1, 3));
assertEquals("", mid("", 1, 3));
assertEquals("ell", mid("hello", 1, 3));
@@ -2648,7 +2787,8 @@ class StringUtils_Test extends TestBase {
assertEquals("", mid("hello", 1, -1));
}
- @Test void a61_padLeft() {
+ @Test
+ void a61_padLeft() {
assertEquals(" ", padLeft(null, 5, ' '));
assertEquals(" ", padLeft("", 5, ' '));
assertEquals(" hello", padLeft("hello", 8, ' '));
@@ -2656,7 +2796,8 @@ class StringUtils_Test extends TestBase {
assertEquals("00123", padLeft("123", 5, '0'));
}
- @Test void a62_padRight() {
+ @Test
+ void a62_padRight() {
assertEquals(" ", padRight(null, 5, ' '));
assertEquals(" ", padRight("", 5, ' '));
assertEquals("hello ", padRight("hello", 8, ' '));
@@ -2664,7 +2805,8 @@ class StringUtils_Test extends TestBase {
assertEquals("12300", padRight("123", 5, '0'));
}
- @Test void a63_padCenter() {
+ @Test
+ void a63_padCenter() {
assertEquals(" ", padCenter(null, 5, ' '));
assertEquals(" ", padCenter("", 5, ' '));
assertEquals(" hi ", padCenter("hi", 6, ' '));
@@ -2677,7 +2819,8 @@ class StringUtils_Test extends TestBase {
// String joining and splitting methods
//====================================================================================================
- @Test void a64_joinObjectArray() {
+ @Test
+ void a64_joinObjectArray() {
assertNull(StringUtils.join((Object[])null, ","));
assertEquals("", StringUtils.join(a(), ","));
assertEquals("a,b,c", StringUtils.join(a("a", "b", "c"), ","));
@@ -2687,7 +2830,8 @@ class StringUtils_Test extends TestBase {
assertEquals("a;b;c", StringUtils.join(a("a", "b", "c"), ";"));
}
- @Test void a65_joinIntArray() {
+ @Test
+ void a65_joinIntArray() {
assertEquals("", StringUtils.join((int[])null, ","));
assertEquals("", StringUtils.join(ints(), ","));
assertEquals("1,2,3", StringUtils.join(ints(1, 2, 3), ","));
@@ -2695,7 +2839,8 @@ class StringUtils_Test extends TestBase {
assertEquals("123", StringUtils.join(ints(1, 2, 3), ""));
}
- @Test void a66_joinCollection() {
+ @Test
+ void a66_joinCollection() {
assertNull(StringUtils.join((Collection<?>)null, ","));
assertEquals("", StringUtils.join(Collections.emptyList(), ","));
assertEquals("a,b,c", StringUtils.join(l("a", "b", "c"), ","));
@@ -2703,17 +2848,20 @@ class StringUtils_Test extends TestBase {
assertEquals("a,null,c", StringUtils.join(l("a", null, "c"), ","));
}
- @Test void a67_joinObjectArrayChar() {
+ @Test
+ void a67_joinObjectArrayChar() {
assertEquals("a,b,c", StringUtils.join(a("a", "b", "c"), ','));
assertEquals("1-2-3", StringUtils.join(a(1, 2, 3), '-'));
}
- @Test void a68_joinIntArrayChar() {
+ @Test
+ void a68_joinIntArrayChar() {
assertEquals("1,2,3", StringUtils.join(ints(1, 2, 3), ','));
assertEquals("1-2-3", StringUtils.join(ints(1, 2, 3), '-'));
}
- @Test void a69_joinCollectionChar() {
+ @Test
+ void a69_joinCollectionChar() {
assertEquals("a,b,c", StringUtils.join(l("a", "b", "c"), ','));
assertEquals("1-2-3", StringUtils.join(l(1, 2, 3), '-'));
}
@@ -2722,7 +2870,8 @@ class StringUtils_Test extends TestBase {
// String cleaning and sanitization methods
//====================================================================================================
- @Test void a72_clean() {
+ @Test
+ void a72_clean() {
assertNull(clean(null));
assertEquals("", clean(""));
assertEquals("hello world", clean("hello\u0000\u0001world"));
@@ -2730,7 +2879,8 @@ class StringUtils_Test extends TestBase {
assertEquals("test", clean("test"));
}
- @Test void a73_normalizeWhitespace() {
+ @Test
+ void a73_normalizeWhitespace() {
assertNull(normalizeWhitespace(null));
assertEquals("", normalizeWhitespace(""));
assertEquals("hello world", normalizeWhitespace("hello \t\n world"));
@@ -2738,7 +2888,8 @@ class StringUtils_Test extends TestBase {
assertEquals("a b c", normalizeWhitespace("a b c"));
}
- @Test void a74_removeControlChars() {
+ @Test
+ void a74_removeControlChars() {
assertNull(removeControlChars(null));
assertEquals("", removeControlChars(""));
assertEquals("hello world", removeControlChars("hello\u0000\u0001world"));
@@ -2746,14 +2897,16 @@ class StringUtils_Test extends TestBase {
assertEquals("test", removeControlChars("test"));
}
- @Test void a75_removeNonPrintable() {
+ @Test
+ void a75_removeNonPrintable() {
assertNull(removeNonPrintable(null));
assertEquals("", removeNonPrintable(""));
assertEquals("helloworld", removeNonPrintable("hello\u0000world"));
assertEquals("test", removeNonPrintable("test"));
}
- @Test void a76_swapCase() {
+ @Test
+ void a76_swapCase() {
assertNull(swapCase(null));
assertEquals("", swapCase(""));
assertEquals("hELLO wORLD", swapCase("Hello World"));
@@ -2764,14 +2917,16 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// lc / uc
//====================================================================================================
- @Test void a77_lc() {
+ @Test
+ void a77_lc() {
assertNull(lc(null));
assertEquals("", lc(""));
assertEquals("hello", lc("HELLO"));
assertEquals("hello world", lc("Hello World"));
}
- @Test void a78_uc() {
+ @Test
+ void a78_uc() {
assertNull(uc(null));
assertEquals("", uc(""));
assertEquals("HELLO", uc("hello"));
@@ -2781,7 +2936,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// eqic
//====================================================================================================
- @Test void a79_eqic() {
+ @Test
+ void a79_eqic() {
assertTrue(eqic(null, null));
assertFalse(eqic("test", null));
assertFalse(eqic(null, "test"));
@@ -2796,7 +2952,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// articlized
//====================================================================================================
- @Test void a80_articlized() {
+ @Test
+ void a80_articlized() {
assertEquals("an apple", articlized("apple"));
assertEquals("an Apple", articlized("Apple"));
assertEquals("a banana", articlized("banana"));
@@ -2810,7 +2967,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// obfuscate
//====================================================================================================
- @Test void a81_obfuscate() {
+ @Test
+ void a81_obfuscate() {
assertEquals("*", obfuscate(null));
assertEquals("*", obfuscate(""));
assertEquals("*", obfuscate("a"));
@@ -2822,7 +2980,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// firstNonEmpty / firstNonBlank
//====================================================================================================
- @Test void a82_firstNonEmpty() {
+ @Test
+ void a82_firstNonEmpty() {
assertEquals("test", firstNonEmpty("test"));
assertEquals("test", firstNonEmpty(null, "test"));
assertEquals("test", firstNonEmpty("", "test"));
@@ -2834,7 +2993,8 @@ class StringUtils_Test extends TestBase {
assertEquals(" ", firstNonEmpty(" "));
}
- @Test void a83_firstNonBlank() {
+ @Test
+ void a83_firstNonBlank() {
assertEquals("test", firstNonBlank("test"));
assertEquals("test", firstNonBlank(null, "test"));
assertEquals("test", firstNonBlank("", "test"));
@@ -2850,7 +3010,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// cdlToList / cdlToSet
//====================================================================================================
- @Test void a86_cdlToList() {
+ @Test
+ void a86_cdlToList() {
assertEquals(l("a", "b", "c"), cdlToList("a,b,c"));
assertEquals(l("a", "b", "c"), cdlToList(" a , b , c "));
assertEquals(l(), cdlToList(null));
@@ -2858,7 +3019,8 @@ class StringUtils_Test extends TestBase {
assertEquals(l("a"), cdlToList("a"));
}
- @Test void a87_cdlToSet() {
+ @Test
+ void a87_cdlToSet() {
assertEquals(new LinkedHashSet<>(l("a", "b", "c")), cdlToSet("a,b,c"));
assertEquals(new LinkedHashSet<>(l("a", "b", "c")), cdlToSet(" a , b , c "));
assertEquals(set(), cdlToSet(null));
@@ -2869,13 +3031,15 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// join
//====================================================================================================
- @Test void a88_join_varargs() {
+ @Test
+ void a88_join_varargs() {
assertEquals("a,b,c", join("a", "b", "c"));
assertEquals("a", join("a"));
assertEquals("", join());
}
- @Test void a89_join_collection() {
+ @Test
+ void a89_join_collection() {
assertEquals("a,b,c", join(l("a", "b", "c")));
assertEquals("1,2,3", join(l(1, 2, 3)));
assertEquals("a", join(l("a")));
@@ -2885,7 +3049,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// contains / notContains
//====================================================================================================
- @Test void a90_contains_strings() {
+ @Test
+ void a90_contains_strings() {
assertTrue(contains("test", "te"));
assertTrue(contains("test", "st"));
assertTrue(contains("test", "test"));
@@ -2896,7 +3061,8 @@ class StringUtils_Test extends TestBase {
assertFalse(contains("test", (String[])null));
}
- @Test void a91_contains_chars() {
+ @Test
+ void a91_contains_chars() {
assertTrue(contains("test", 't'));
assertTrue(contains("test", 'e'));
assertTrue(contains("test", 't', 'x'));
@@ -2906,13 +3072,15 @@ class StringUtils_Test extends TestBase {
assertFalse(contains("test", (char[])null));
}
- @Test void a92_notContains_strings() {
+ @Test
+ void a92_notContains_strings() {
assertFalse(notContains("test", "te"));
assertTrue(notContains("test", "xx"));
assertTrue(notContains(null, "test"));
}
- @Test void a93_notContains_chars() {
+ @Test
+ void a93_notContains_chars() {
assertFalse(notContains("test", 't'));
assertTrue(notContains("test", 'x'));
assertTrue(notContains(null, 't'));
@@ -2921,7 +3089,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// stringSupplier
//====================================================================================================
- @Test void a94_stringSupplier() {
+ @Test
+ void a94_stringSupplier() {
assertEquals("test", stringSupplier(() -> "test").get());
assertEquals("[1,2,3]", stringSupplier(() -> l(1, 2, 3)).get());
assertNull(stringSupplier(() -> null).get());
@@ -2930,19 +3099,21 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// readable
//====================================================================================================
- @Test void a95_readable() {
+ @Test
+ void a95_readable() {
assertNull(readable(null));
assertEquals("[a,b,c]", readable(l("a", "b", "c")));
assertEquals("{foo=bar}", readable(m("foo", "bar")));
assertEquals("[1,2,3]", readable(ints(1, 2, 3)));
- assertEquals("test", readable(opt("test")));
- assertNull(readable(opte()));
+ assertEquals("test", readable(opt("test")));
+ assertNull(readable(opte()));
}
//====================================================================================================
// removeAll(String, String...)
//====================================================================================================
- @Test void a96_removeAll() {
+ @Test
+ void a96_removeAll() {
assertNull(removeAll(null, "x"));
assertEquals("hello world test", removeAll("hello world test"));
assertEquals("hello world test", removeAll("hello world test", (String[])null));
@@ -2957,7 +3128,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// camelCase(String)
//====================================================================================================
- @Test void a97_camelCase() {
+ @Test
+ void a97_camelCase() {
assertNull(camelCase(null));
assertEquals("", camelCase(""));
assertEquals("helloWorld", camelCase("hello world"));
@@ -2980,7 +3152,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// snakeCase(String)
//====================================================================================================
- @Test void a98_snakeCase() {
+ @Test
+ void a98_snakeCase() {
assertNull(snakeCase(null));
assertEquals("", snakeCase(""));
assertEquals("hello_world", snakeCase("hello world"));
@@ -2998,7 +3171,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// kebabCase(String)
//====================================================================================================
- @Test void a99_kebabCase() {
+ @Test
+ void a99_kebabCase() {
assertNull(kebabCase(null));
assertEquals("", kebabCase(""));
assertEquals("hello-world", kebabCase("hello world"));
@@ -3016,7 +3190,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// pascalCase(String)
//====================================================================================================
- @Test void a100_pascalCase() {
+ @Test
+ void a100_pascalCase() {
assertNull(pascalCase(null));
assertEquals("", pascalCase(""));
assertEquals("HelloWorld", pascalCase("hello world"));
@@ -3034,7 +3209,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// titleCase(String)
//====================================================================================================
- @Test void a101_titleCase() {
+ @Test
+ void a101_titleCase() {
assertNull(titleCase(null));
assertEquals("", titleCase(""));
assertEquals("Hello World", titleCase("hello world"));
@@ -3052,7 +3228,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// wrap(String, int)
//====================================================================================================
- @Test void a102_wrap() {
+ @Test
+ void a102_wrap() {
assertNull(wrap(null, 10));
assertEquals("", wrap("", 10));
assertEquals("hello\nworld", wrap("hello world", 10));
@@ -3070,7 +3247,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// wrap(String, int, String)
//====================================================================================================
- @Test void a103_wrapWithNewline() {
+ @Test
+ void a103_wrapWithNewline() {
assertNull(wrap(null, 10, "<br>"));
assertEquals("", wrap("", 10, "<br>"));
assertEquals("hello<br>world", wrap("hello world", 10, "<br>"));
@@ -3092,7 +3270,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// toStringArray(Collection<String>)
//====================================================================================================
- @Test void a104_toStringArray() {
+ @Test
+ void a104_toStringArray() {
assertNull(toStringArray(null));
assertList(toStringArray(Collections.emptyList()));
assertList(toStringArray(l("a", "b", "c")), "a", "b", "c");
@@ -3115,7 +3294,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// filter(String[], Predicate<String>)
//====================================================================================================
- @Test void a105_filter() {
+ @Test
+ void a105_filter() {
assertNull(filter(null, NOT_EMPTY));
assertList(filter(a(), NOT_EMPTY));
assertList(filter(a("foo", "", "bar", null, "baz"), NOT_EMPTY), "foo", "bar", "baz");
@@ -3130,7 +3310,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// map(String[], Function<String, String>)
//====================================================================================================
- @Test void a106_mapped() {
+ @Test
+ void a106_mapped() {
assertNull(mapped(null, String::toUpperCase));
assertList(mapped(a(), String::toUpperCase));
assertList(mapped(a("foo", "bar", "baz"), String::toUpperCase), "FOO", "BAR", "BAZ");
@@ -3144,7 +3325,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// distinct(String[])
//====================================================================================================
- @Test void a107_distinct() {
+ @Test
+ void a107_distinct() {
assertNull(distinct(null));
assertList(distinct(a()));
assertList(distinct(a("foo", "bar", "baz")), "foo", "bar", "baz");
@@ -3158,7 +3340,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// sort(String[])
//====================================================================================================
- @Test void a108_sort() {
+ @Test
+ void a108_sort() {
assertNull(sort(null));
assertList(sort(a()));
assertList(sort(a("c", "a", "b")), "a", "b", "c");
@@ -3172,7 +3355,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// sortIgnoreCase(String[])
//====================================================================================================
- @Test void a109_sortIgnoreCase() {
+ @Test
+ void a109_sortIgnoreCase() {
assertNull(sortIgnoreCase(null));
assertList(sortIgnoreCase(a()));
assertList(sortIgnoreCase(a("c", "a", "b")), "a", "b", "c");
@@ -3190,7 +3374,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// appendIfNotEmpty(StringBuilder, String)
//====================================================================================================
- @Test void a110_appendIfNotEmpty() {
+ @Test
+ void a110_appendIfNotEmpty() {
var sb = new StringBuilder();
assertSame(sb, appendIfNotEmpty(sb, "hello"));
assertEquals("hello", sb.toString());
@@ -3214,7 +3399,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// appendIfNotBlank(StringBuilder, String)
//====================================================================================================
- @Test void a111_appendIfNotBlank() {
+ @Test
+ void a111_appendIfNotBlank() {
var sb = new StringBuilder();
assertSame(sb, appendIfNotBlank(sb, "hello"));
assertEquals("hello", sb.toString());
@@ -3244,7 +3430,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// appendWithSeparator(StringBuilder, String, String)
//====================================================================================================
- @Test void a112_appendWithSeparator() {
+ @Test
+ void a112_appendWithSeparator() {
var sb = new StringBuilder();
assertSame(sb, appendWithSeparator(sb, "first", ", "));
assertEquals("first", sb.toString());
@@ -3279,7 +3466,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// buildString(Consumer<StringBuilder>)
//====================================================================================================
- @Test void a113_buildString() {
+ @Test
+ void a113_buildString() {
var result = buildString(sb -> {
sb.append("Hello");
sb.append(" ");
@@ -3316,7 +3504,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// String Constants
//====================================================================================================
- @Test void a114_stringConstants() {
+ @Test
+ void a114_stringConstants() {
// EMPTY
assertEquals("", EMPTY);
assertTrue(EMPTY.isEmpty());
@@ -3369,7 +3558,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// intern(String)
//====================================================================================================
- @Test void a115_intern() {
+ @Test
+ void a115_intern() {
assertNull(intern(null));
var s1 = new String("test");
@@ -3391,7 +3581,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// isInterned(String)
//====================================================================================================
- @Test void a116_isInterned() {
+ @Test
+ void a116_isInterned() {
assertFalse(isInterned(null));
// String literals are automatically interned
@@ -3411,7 +3602,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// getStringSize(String)
//====================================================================================================
- @Test void a117_getStringSize() {
+ @Test
+ void a117_getStringSize() {
assertEquals(0, getStringSize(null));
assertEquals(40, getStringSize("")); // 24 + 16 = 40 bytes overhead
assertEquals(50, getStringSize("hello")); // 40 + (5 * 2) = 50 bytes
@@ -3432,7 +3624,8 @@ class StringUtils_Test extends TestBase {
//====================================================================================================
// optimizeString(String)
//====================================================================================================
- @Test void a118_optimizeString() {
+ @Test
+ void a118_optimizeString() {
assertNull(optimizeString(null));
assertNull(optimizeString("short")); // No suggestions for short strings
diff --git a/juneau-utest/src/test/java/org/apache/juneau/common/utils/Utils_Test.java b/juneau-utest/src/test/java/org/apache/juneau/common/utils/Utils_Test.java
index 59e4e62e1a..42438b8e5f 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/common/utils/Utils_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/common/utils/Utils_Test.java
@@ -38,5 +38,56 @@ class Utils_Test extends TestBase {
assertFalse(nn(null));
}
+
+ //====================================================================================================
+ // f(String, Object...)
+ //====================================================================================================
+ @Test
+ void a02_f() {
+ // Basic formatting
+ assertEquals("Hello John, you have 5 items", f("Hello %s, you have %d items", "John", 5));
+ assertEquals("Hello world", f("Hello %s", "world"));
+
+ // Floating point
+ assertEquals("Price: $19.99", f("Price: $%.2f", 19.99));
+ assertEquals("Value: 3.14", f("Value: %.2f", 3.14159));
+
+ // Multiple arguments
+ assertEquals("Name: John, Age: 30, Salary: $50000.00",
+ f("Name: %s, Age: %d, Salary: $%.2f", "John", 30, 50000.0));
+
+ // Null handling
+ assertEquals("Value: null", f("Value: %s", (String)null));
+ assertNull(f(null, "test"));
+ assertEquals("test", f("test"));
+ }
+
+ //====================================================================================================
+ // fs(String, Object...)
+ //====================================================================================================
+ @Test
+ void a03_fs() {
+ // Basic supplier
+ var supplier = fs("Hello %s, you have %d items", "John", 5);
+ assertNotNull(supplier);
+ assertEquals("Hello John, you have 5 items", supplier.get());
+
+ // Lazy evaluation - format only when get() is called
+ var lazySupplier = fs("Price: $%.2f", 19.99);
+ assertEquals("Price: $19.99", lazySupplier.get());
+
+ // Multiple calls return same result
+ var supplier2 = fs("Value: %s", "test");
+ assertEquals("Value: test", supplier2.get());
+ assertEquals("Value: test", supplier2.get());
+
+ // Null handling
+ var nullSupplier = fs(null, "test");
+ assertNull(nullSupplier.get());
+
+ // Empty pattern
+ var emptySupplier = fs("");
+ assertEquals("", emptySupplier.get());
+ }
}