001    package org.hd.d.pg2k.ai.scorer;
002    
003    import java.util.ArrayList;
004    import java.util.Collection;
005    import java.util.Collections;
006    import java.util.HashMap;
007    import java.util.Iterator;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.regex.Pattern;
011    
012    import org.hd.d.pg2k.svrCore.MemoryTools;
013    import org.hd.d.pg2k.svrCore.Rnd;
014    import org.hd.d.pg2k.svrCore.Tuple.Pair;
015    
016    /**Class to implement some common Scorer features in one place.
017     *
018     * @author dhd
019     */
020    public abstract class AbstractScorer implements ScorerIF
021        {
022        /**Default constructor uses class-based name and allows no parameterisation. */
023        protected AbstractScorer()
024            { nameAndParameters = getDefaultName(); }
025    
026        /**Construct instance with name and optional parameters.
027         * The name-and-parameters value must be synactically valid (or at least the name must be),
028         * and the Scorer name must match the "default" name (at least for now),
029         * but the parameters are not interpreted here.
030         *
031         * @throws IllegalArgumentException  if parameter syntax or Scorer bad name is wrong
032         */
033        protected AbstractScorer(final String nameAndParameters)
034            throws IllegalArgumentException
035            {
036            // Quick extraction of base name.
037            this.nameAndParameters = nameAndParameters;
038    
039            if(nameAndParameters == null) { throw new IllegalArgumentException(); }
040            final int fs = nameAndParameters.indexOf(AbstractScorer.SEPARATOR);
041            final boolean hasParams = (fs != -1);
042            final String baseName = hasParams ? nameAndParameters.substring(0, fs) : nameAndParameters;
043            if(!getDefaultName().equals(baseName))
044                { throw new IllegalArgumentException("Scorer base name mismatch"); }
045             }
046    
047        /* inherit javadoc */
048        protected AbstractScorer(final String baseName, final List<ScorerParam> parameters)
049            throws IllegalArgumentException
050            { this(baseName + paramListAsString(parameters)); }
051    
052        /**Get default name for this Scorer; always a valid ScorerName.
053         * Must not be overrideable else it will not be safe to use in constructors.
054         */
055        protected final String getDefaultName()
056            {
057            final String fullName = this.getClass().getName();
058            final String[] tokens = fullName.split("[.$]"); // Allow for inner classes too.
059            final String name = tokens[tokens.length-1];  // Use final class-name component.
060            assert(isValidScorerName(name));
061            return(name);
062            }
063    
064        /**Create parameter String from in-order catenation of parameter values; never null but may be empty.
065         * The result is directly suitable to append to a base Scorer name.
066         *
067         * @param params parameter list may be empty but is never null
068         * @return  in-order non-null parameter list (each parameter being preceded by the separator);
069         *     "" in the case of empty parameter list
070         */
071        public static String paramListAsString(final List<ScorerParam> params)
072            {
073            if(params == null) { throw new IllegalArgumentException(); }
074            if(params.isEmpty()) { return(""); }
075            final StringBuilder sb = new StringBuilder(params.size() * 8);
076            for(final ScorerParam p : params)
077                { sb.append(SEPARATOR).append(p.toNameValueString()); }
078            return(sb.toString());
079            }
080    
081        /**Create parameter Map from parameter values; never null but may be empty.
082         * The this will discard early parameters whose names are re-used later in the list.
083         *
084         * @param params parameter list may be empty but is never null
085         * @return  in-order non-null parameter list (each parameter being preceded by the separator);
086         *     "" in the case of empty parameter list
087         */
088        public static Map<String, ScorerParam> paramListAsMap(final List<ScorerParam> params)
089            {
090            if(params == null) { throw new IllegalArgumentException(); }
091            if(params.isEmpty()) { return(Collections.emptyMap()); }
092            final Map<String, ScorerParam> m = new HashMap<String, ScorerParam>(2 * params.size());
093            for(final ScorerParam p : params)
094                { m.put(p.getName(), p); }
095            return(m);
096            }
097    
098        /**Name and parameters of this scorer always starting with our default name and syntactically valid; never null.
099         * By default this has no parameters, and is based on the implementing class' name.
100         */
101        protected final String nameAndParameters;
102    
103        /**Return the base name (the last component of the class name unless overridden); never null. */
104        public String getBaseName()
105            {
106            final int sep = nameAndParameters.indexOf(SEPARATOR);
107            if(sep == -1) { return(nameAndParameters); /* No parameters to remove. */ }
108            return(nameAndParameters.substring(0, sep));
109            }
110    
111        /**Return the name with any parameters for this instance; never null. */
112        public String getNameAndParameters() { return(nameAndParameters); }
113    
114        /* (non-Javadoc)
115         * @see org.hd.d.pg2k.ai.scorer.ScorerIF#getPerturbedDefsAndValues()
116         */
117        public List<ScorerParam> getPerturbedDefsAndValues()
118            {
119            final Collection<ScorerParam> params = getParameterDefsAndValues();
120            final int pSize = params.size();
121            final List<ScorerParam> result = new ArrayList<ScorerParam>(pSize);
122            int i = (pSize < 2) ? 0 : Rnd.goodRnd.nextInt(pSize);
123            for(final ScorerParam p : params)
124                {
125                if((i-- == 0) || Rnd.goodRnd.nextBoolean())
126                    { result.add(p.perturb()); /* Mutate. */ }
127                else
128                    { result.add(p); /* Leave as-is. */ }
129                }
130            return(result);
131            }
132    
133        /**Create perturbed (gently mutated) variant. */
134        public ScorerIF createPerturbedVariant()
135            { return(createVariant(getBaseName(), getPerturbedDefsAndValues())); }
136    
137        /**Create canonical form of Scorer (and any parameters); never null. */
138        public static String canonicalise(final ScorerIF scorer)
139            {
140            if(scorer == null) { throw new IllegalArgumentException(); }
141    
142            final Collection<ScorerParam> params = scorer.getParameterDefsAndValues();
143            // If no parameters then just return the base name.
144            final int pSize = params.size();
145            if(pSize == 0) { return(scorer.getBaseName()); }
146    
147            // Append canonical parameter values and order.
148            final StringBuilder sb = new StringBuilder();
149            sb.append(scorer.getBaseName());
150            for(final ScorerParam p : params)
151                { sb.append(SEPARATOR).append(p.toNameValueString()); }
152            return(sb.toString());
153            }
154    
155        /**Get (immutable, ordered) parameter definitions and current values (immutable) for this Scorer, empty by default; never null. */
156        public List<ScorerParam> getParameterDefsAndValues() { return(Collections.emptyList()); }
157    
158        /**Regex pattern to match for Scorer parameter-name validity; never null. */
159        public static final Pattern parameterNameRegex = Pattern.compile("[a-zA-Z_][0-9a-zA-Z_]*");
160    
161        /**Regex pattern to match for Scorer name validity; never null.
162         * Currently the same as the parameter name regex for simplicity.
163         */
164        public static final Pattern scorerNameRegex = parameterNameRegex;
165    
166        /**Returns true iff the parameter name supplied is non-null and is valid. */
167        public static boolean isValidParameterName(final String name)
168            {
169            if(null == name) { return(false); }
170            return(parameterNameRegex.matcher(name).matches());
171            }
172    
173        /**Returns true iff the Scorer name supplied is non-null and is valid. */
174        public static boolean isValidScorerName(final String name)
175            {
176            if(null == name) { return(false); }
177            return(scorerNameRegex.matcher(name).matches());
178            }
179    
180        /**The character used to tokenise the scorer name-plus-parameters form <i>ScorerName{:name=value}*</i>.
181         * A printable non-whitespace 7-bit ASCII character.
182         */
183        public static final char SEPARATOR = ':';
184    
185        /**Double separator, not permitted in Scorer name-and-parameters. */
186        private static final String DOUBLE_SEP = ("" + AbstractScorer.SEPARATOR) + AbstractScorer.SEPARATOR;
187    
188        /**Regex pattern to separate parameters; never null. */
189        public static final Pattern parameterSepRegex = Pattern.compile("["+AbstractScorer.SEPARATOR+"]");
190    
191        /**Parse name-and-parameters into name and name-value immutable Map; never null.
192         *
193         * @param nameAndParameters  Scorer name-and-parameters; never null
194         *
195         * @return non-null non-empty Scorer base name and non-null possibly empty parameter set
196         *
197         * @throws IllegalArgmentException  for null/invalid argument
198         */
199        public static Pair<String, Map<String,String>> parseNameAndParameters(final String nameAndParameters)
200            throws IllegalArgumentException
201            {
202            // Reject common syntax/validity problems quickly.
203            if((nameAndParameters == null) ||
204               (nameAndParameters.length() == 0) ||
205               (nameAndParameters.charAt(0) == AbstractScorer.SEPARATOR) ||
206               (nameAndParameters.indexOf(DOUBLE_SEP) != -1))
207                { throw new IllegalArgumentException(); }
208    
209            // Fast-track the no-parameters case...
210            if(nameAndParameters.indexOf(AbstractScorer.SEPARATOR) == -1)
211                {
212                if(!isValidScorerName(nameAndParameters)) { throw new IllegalArgumentException("bad Scorer simple name"); }
213                // There should be very few distinct Scorer base names, so this should be worth intern()ing.
214                return(new Pair<String, Map<String, String>>(MemoryTools.intern(nameAndParameters), Collections.<String, String>emptyMap()));
215                }
216    
217            final String tokens[] = parameterSepRegex.split(nameAndParameters);
218            if(tokens.length < 2) { throw new IllegalArgumentException(); }
219    
220            // There should be very few distinct Scorer base names, so this should be worth intern()ing.
221            if(!isValidScorerName(tokens[0])) { throw new IllegalArgumentException("bad Scorer name with parameters"); }
222            final String name = MemoryTools.intern(tokens[0]);
223    
224            // Parse the parameters.
225            // We intern() the parameter names as from an assumed-small-set,
226            // but we DO NOT intern() the values as they may well be sparse and short-lived.
227            final HashMap<String, String> m = new HashMap<String, String>(2 * tokens.length);
228            for(int i = tokens.length; --i > 0; )
229                {
230                final String token = tokens[i];
231                final int ei = token.indexOf('=');
232                if(ei <= 0) { throw new IllegalArgumentException("missing/initial '=' in parameter"); }
233                if(ei != token.lastIndexOf('=')) { throw new IllegalArgumentException("multiple '=' in parameter"); }
234                final String paramName = token.substring(0, ei);
235                if(!isValidParameterName(paramName)) { throw new IllegalArgumentException("invalid parameter name"); }
236                // We intern the parameter name as likely common to all instances.
237                m.put(MemoryTools.intern(paramName), token.substring(ei+1));
238                }
239    
240            return(new Pair<String, Map<String, String>>(name, Collections.unmodifiableMap(m)));
241            }
242    
243        /**Returns true if the two supplied Scorer instances are 'genetically'/'genotypically' very similar.
244         * Always returns true for identical Scorers.
245         * <p>
246         * Always returns false for Scorers with different base names.
247         *
248         * @param scorerInstance1  non-null instance
249         * @param scorerInstance2  non-null instance
250         * @return  false if a different base name or at least one parameter value significantly different
251         */
252        public static boolean verySimilar(final ScorerIF scorerInstance1, final ScorerIF scorerInstance2)
253            {
254            if((scorerInstance1 == null) || (scorerInstance2 == null)) { throw new IllegalArgumentException(); }
255    
256            // Deal with some trivial cases quickly.
257            // Any Scorer instance is always verySimilar() to itself.
258            if(scorerInstance1 == scorerInstance2) { return(true); }
259            if(scorerInstance1.getNameAndParameters().equals(scorerInstance2.getNameAndParameters())) { return(true); }
260            // No two Scorers with different base names can be verySimilar().
261            if(!scorerInstance1.getBaseName().equals(scorerInstance2.getBaseName())) { return(false); }
262    
263            // Check the parameter values from the Scorers against one another.
264            // We always expect the same named/typed parameter order given the same base name.
265            final Iterator<ScorerParam> p2it = scorerInstance2.getParameterDefsAndValues().iterator();
266            for(final ScorerParam p1 : scorerInstance1.getParameterDefsAndValues())
267                {
268                final ScorerParam p2 = p2it.next();
269    
270                // Assert that the parameters are at least superficially in the same order.
271                assert(p1.getName().equals(p2.getName()));
272    
273                // Just one dissimilar parameter makes the two Scorer instances NOT verySimilar(),
274                // so we can return false immediately on finding a difference.
275                if(!p1.similar(p2)) { return(false); }
276    
277                // Similarity should be symmetric.
278                assert(p2.similar(p1));
279                }
280    
281            return(true); // The two Scorer instances seem almost identical...
282            }
283    
284        /**Returns true if the two supplied Scorer instances are 'genetically'/'genotypically' similar.
285         * Always returns true for identical Scorers.
286         * <p>
287         * Always returns false for Scorers with different base names.
288         *
289         * @param scorerInstance1  non-null instance
290         * @param scorerInstance2  non-null instance
291         * @param minSimilarParams  minimum number of parameters that must be similar
292         *     for the two Scorers must be for this routine to return true;
293         *     non-negative (the result is always true for same-base-name Scorers if zero)
294         * @return  true if the Scorers are as similar as specified, false otherwise
295         */
296        public static boolean similarNParams(final ScorerIF scorerInstance1, final ScorerIF scorerInstance2, final int minSimilarParams)
297            {
298            if((scorerInstance1 == null) || (scorerInstance2 == null)) { throw new IllegalArgumentException(); }
299            if(minSimilarParams < 0) { throw new IllegalArgumentException(); }
300    
301            // Deal with some trivial cases quickly.
302            // Any Scorer instance is always similar to itself.
303            if(scorerInstance1 == scorerInstance2) { return(true); }
304            if(scorerInstance1.getNameAndParameters().equals(scorerInstance2.getNameAndParameters())) { return(true); }
305            // No two Scorers with different base names can be verySimilar().
306            if(!scorerInstance1.getBaseName().equals(scorerInstance2.getBaseName())) { return(false); }
307    
308            if(minSimilarParams == 0) { return(true); /* Optimisation. */ }
309    
310            final Collection<ScorerParam> p2s = scorerInstance2.getParameterDefsAndValues();
311            final int p2Size = p2s.size();
312            if(p2Size == 0) { return(true); } // Cannot help but be similar!
313            // Check the parameter values from the Scorers against one another.
314            // We always expect the same named/typed parameter order given the same base name.
315            final Iterator<ScorerParam> p2it = p2s.iterator();
316            int parametersSimilar = 0;
317            for(final ScorerParam p1 : scorerInstance1.getParameterDefsAndValues())
318                {
319                final ScorerParam p2 = p2it.next();
320    
321                // Assert that the parameters are at least superficially in the same order.
322                assert(p1.getName().equals(p2.getName()));
323    
324                if(p1.similar(p2)) { ++parametersSimilar; }
325                }
326    
327            return(parametersSimilar >= minSimilarParams);
328            }
329        }