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