001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: NamedArgumentFormatter.java 64 2011-03-22 15:15:43Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.string;
009
010 import java.util.ArrayList;
011 import java.util.Collections;
012 import java.util.HashMap;
013 import java.util.Map;
014 import java.util.Set;
015 import java.util.regex.Matcher;
016 import java.util.regex.Pattern;
017
018 /**
019 * Interpreter for printf-style formatting strings that identifies arguments using names rather than indicies.
020 *
021 * <p>
022 * Works just like {@link java.util.Formatter} but arguments are specified using names instead of numbers.
023 * A mapping from argument name to argument value must be provided during the format operation.
024 * </p>
025 *
026 * @since 1.0.64
027 */
028 public class NamedArgumentFormatter {
029
030 private static final Pattern FORMAT_PARAM_PATTERN = Pattern.compile("%(\\w+)\\$");
031
032 /**
033 * The list of argument names in the order they are used. This is the inverse mapping of {@link #fieldMap}.
034 */
035 protected final ArrayList<String> fieldList = new ArrayList<String>();
036
037 /**
038 * Mapping from argument name to argument list index. This is the inverse mapping of {@link #fieldList}.
039 */
040 protected final HashMap<String, Integer> fieldMap = new HashMap<String, Integer>();
041
042 /**
043 * The original format string provided to the constructor.
044 */
045 protected final String originalFormat;
046
047 /**
048 * The modified format string containing indexes instead of argument names.
049 */
050 protected final String indexedFormat;
051
052 /**
053 * Constructor.
054 *
055 * @param format format string containing argument names instead of indicies
056 */
057 public NamedArgumentFormatter(final String format) {
058 Matcher matcher = FORMAT_PARAM_PATTERN.matcher(format);
059 StringBuilder buf = new StringBuilder();
060 for (int offset = 0; true; offset = matcher.end(1)) {
061
062 // Find next conversion
063 if (!matcher.find(offset)) {
064 buf.append(format.substring(offset));
065 break;
066 }
067
068 // Extract argument name
069 String fieldName = matcher.group(1);
070
071 // If we haven't seen this argument name yet, assign it the next parameter index
072 Integer fieldIndex = this.fieldMap.get(fieldName);
073 if (fieldIndex == null) {
074 fieldIndex = this.fieldList.size();
075 this.fieldMap.put(fieldName, fieldIndex);
076 this.fieldList.add(fieldName);
077 }
078
079 // Replace field name in format string with parameter index
080 buf.append(format.substring(offset, matcher.start(1)));
081 buf.append(fieldIndex + 1);
082 }
083 this.originalFormat = format;
084 this.indexedFormat = buf.toString();
085 }
086
087 /**
088 * Format the string using the given arguments.
089 *
090 * @param argMap mapping from argument name to argument value
091 * @throws IllegalFormatException if the format provided to the constructor contained illegal syntax
092 * @throws IllegalFormatException if an argument value is incompatible or missing (and null would be invalid)
093 */
094 public String format(Map<String, Object> argMap) {
095
096 // Put values into format parameter array
097 ArrayList<Object> parameterList = new ArrayList<Object>(this.fieldList.size());
098 for (String fieldName : this.fieldList)
099 parameterList.add(argMap.get(fieldName));
100
101 // Format string
102 return String.format(this.indexedFormat, parameterList.toArray());
103 }
104
105 /**
106 * Get the original format string provided to the constructor.
107 */
108 public String getFormat() {
109 return this.originalFormat;
110 }
111
112 /**
113 * Get the argument names found in the configured format string.
114 *
115 * @return argument names as a unmodifiable set
116 */
117 public Set<String> getArgumentNames() {
118 return Collections.unmodifiableSet(this.fieldMap.keySet());
119 }
120 }
121