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 }