View Javadoc
1   package io.jawk.jrt;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2026 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  import java.util.List;
26  import java.util.Map;
27  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28  import io.jawk.intermediate.UninitializedObject;
29  import io.jawk.intermediate.UntypedObject;
30  
31  /**
32   * An AWK associative array.
33   * <p>
34   * This interface extends {@link Map} and provides AWK-specific behaviour:
35   * automatic key normalization (null and uninitialized values map to {@code ""}),
36   * numeric key coercion ({@code "1"} and {@code 1L} address the same slot), and
37   * auto-creation of blank entries on first access.
38   * </p>
39   * <p>
40   * Concrete implementations directly extend a JDK {@link Map} class to avoid
41   * delegation overhead:
42   * </p>
43   * <ul>
44   * <li>{@link HashAssocArray} &mdash; backed by {@link java.util.HashMap}</li>
45   * <li>{@link ListAssocArray} &mdash; materialized from a {@link java.util.List}
46   * and backed by {@link java.util.HashMap}</li>
47   * <li>{@link SortedAssocArray} &mdash; backed by {@link java.util.TreeMap} with
48   * AWK key ordering</li>
49   * </ul>
50   * <p>
51   * Use the factory methods to create instances:
52   * </p>
53   *
54   * <pre>
55   * AssocArray hash = AssocArray.createHash();
56   * AssocArray sorted = AssocArray.createSorted();
57   * AssocArray list = AssocArray.createFromList(values, sortedArrayKeys);
58   * AssocArray aa = AssocArray.create(sortedArrayKeys);
59   * </pre>
60   *
61   * @author Danny Daglas
62   */
63  public interface AssocArray extends Map<Object, Object> {
64  
65  	/** A blank (uninitialized) value shared across all AWK array accesses. */
66  	UninitializedObject BLANK = new UninitializedObject();
67  
68  	/** An untyped value shared across newly referenced AWK array elements. */
69  	UntypedObject UNTYPED = new UntypedObject();
70  
71  // -------------------------------------------------------------------------
72  // Key-normalization helpers (used by concrete implementations)
73  // -------------------------------------------------------------------------
74  
75  	/**
76  	 * Converts a key to the canonical form expected by AWK: {@code null} and
77  	 * {@link UninitializedObject} map to the empty string, and internal input
78  	 * strings map to their string value.
79  	 *
80  	 * @param key the raw key
81  	 * @return the normalized key, never {@code null}
82  	 */
83  	static Object normalizeKey(Object key) {
84  		if (key == null || key instanceof UninitializedObject) {
85  			return "";
86  		}
87  		if (key instanceof StrNum) {
88  			return key.toString();
89  		}
90  		if (key instanceof Double || key instanceof Float) {
91  			double numericKey = ((Number) key).doubleValue();
92  			if (JRT.isActuallyLong(numericKey)) {
93  				return Long.valueOf((long) Math.rint(numericKey));
94  			}
95  		}
96  		return key;
97  	}
98  
99  	/**
100 	 * Returns whether a subscript value is an integral number that
101 	 * {@link #normalizeKey(Object)} canonicalizes to the same {@code Long}
102 	 * key as its AWK string form. Such a subscript can be used as a key
103 	 * directly, without the CONVFMT string conversion that every other
104 	 * scalar goes through: the resulting key is identical, and skipping the
105 	 * string round-trip keeps integer-indexed loops fast.
106 	 *
107 	 * @param value the subscript value to examine
108 	 * @return {@code true} if the value can be used as a key without string
109 	 *         conversion
110 	 */
111 	static boolean isIntegralNumberKey(Object value) {
112 		if (value instanceof Long || value instanceof Integer) {
113 			return true;
114 		}
115 		return value instanceof Double && JRT.toScalarNumber((Double) value) instanceof Long;
116 	}
117 
118 	/**
119 	 * Attempts to parse the key as a {@code Long}.
120 	 * <p>
121 	 * This accepts and rejects exactly the same inputs as
122 	 * {@link Long#parseLong(String)} (radix 10), but scans the string without
123 	 * ever constructing an exception: most keys are not integer strings (every
124 	 * multidimensional {@code arr[x,y]} key, for instance), and paying the cost
125 	 * of a {@link NumberFormatException} stack-trace fill on every array access
126 	 * is prohibitive.
127 	 * </p>
128 	 *
129 	 * @param key the key to parse (must not be {@code null})
130 	 * @return the {@code Long} value, or {@code null} if the key cannot be parsed
131 	 *         as a long integer
132 	 */
133 	@SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Defensive check against contract-violating toString() implementations returning null")
134 	static Long toLongKey(Object key) {
135 		final String str;
136 		if (key instanceof String) {
137 			str = (String) key;
138 		} else {
139 			try {
140 				str = key.toString();
141 			} catch (RuntimeException e) { // NOPMD - EmptyCatchBlock: intentionally ignored
142 				// e.g. an AssocArray used as a key throws on toString():
143 				// treat such keys as non-numeric
144 				return null;
145 			}
146 			if (str == null) {
147 				// a contract-violating toString(): treat the key as non-numeric,
148 				// like the former Long.parseLong(null) NumberFormatException path
149 				return null;
150 			}
151 		}
152 		final int len = str.length();
153 		if (len == 0) {
154 			return null;
155 		}
156 		int i = 0;
157 		boolean negative = false;
158 		long limit = -Long.MAX_VALUE;
159 		final char firstChar = str.charAt(0);
160 		if (firstChar < '0') { // possible leading "+" or "-"
161 			if (firstChar == '-') {
162 				negative = true;
163 				limit = Long.MIN_VALUE;
164 			} else if (firstChar != '+') {
165 				return null;
166 			}
167 			if (len == 1) { // lone "+" or "-"
168 				return null;
169 			}
170 			i = 1;
171 		}
172 		// Accumulate negatively, exactly like Long.parseLong, so that
173 		// Long.MIN_VALUE (whose magnitude exceeds Long.MAX_VALUE) parses too
174 		final long multmin = limit / 10;
175 		long result = 0;
176 		while (i < len) {
177 			// Character.digit keeps parity with Long.parseLong on non-ASCII digits
178 			final int digit = Character.digit(str.charAt(i++), 10);
179 			if (digit < 0 || result < multmin) {
180 				return null;
181 			}
182 			result *= 10;
183 			if (result < limit + digit) {
184 				return null;
185 			}
186 			result -= digit;
187 		}
188 		return negative ? result : -result;
189 	}
190 
191 // -------------------------------------------------------------------------
192 // AWK-specific default methods
193 // -------------------------------------------------------------------------
194 
195 	/**
196 	 * Returns whether a particular key is contained within the associative array.
197 	 * <p>
198 	 * Unlike {@link #get(Object)}, which auto-creates a blank entry when the key
199 	 * is absent, this method does not modify the array. It exists to support the
200 	 * AWK {@code IN} keyword.
201 	 * </p>
202 	 *
203 	 * @param key Key to be checked
204 	 * @return {@code true} if the key (or its numeric equivalent) is present
205 	 */
206 	default boolean isIn(Object key) {
207 		key = normalizeKey(key);
208 		if (containsKey(key)) {
209 			return true;
210 		}
211 		Long lKey = toLongKey(key);
212 		return lKey != null && containsKey(lKey);
213 	}
214 
215 	/**
216 	 * Provides a string representation of this associative array, recursively
217 	 * rendering nested arrays.
218 	 *
219 	 * @return a human-readable map string of the form {@code {key=value, ...}}
220 	 */
221 	default String mapString() {
222 // Since extensions allow assoc arrays to become keys as well,
223 // we render nested arrays recursively rather than using toString().
224 		StringBuilder sb = new StringBuilder().append('{');
225 		int cnt = 0;
226 		for (Map.Entry<Object, Object> entry : entrySet()) {
227 			if (cnt > 0) {
228 				sb.append(", ");
229 			}
230 			Object key = entry.getKey();
231 			if (key instanceof AssocArray) {
232 				sb.append(((AssocArray) key).mapString());
233 			} else {
234 				sb.append(key.toString());
235 			}
236 			sb.append('=');
237 			Object value = entry.getValue();
238 			if (value instanceof AssocArray) {
239 				sb.append(((AssocArray) value).mapString());
240 			} else {
241 				sb.append(value.toString());
242 			}
243 			++cnt;
244 		}
245 		return sb.append('}').toString();
246 	}
247 
248 	/**
249 	 * Stores a value using a primitive {@code long} key, bypassing string parsing.
250 	 * <p>
251 	 * This is a convenience overload for callers that already hold a {@code long}
252 	 * key. The default implementation boxes the key and delegates to
253 	 * {@link #put(Object, Object)}.
254 	 * </p>
255 	 *
256 	 * @param key the long key
257 	 * @param value the value to associate with the key
258 	 * @return the previous value associated with the key, or {@code null}
259 	 */
260 	default Object put(long key, Object value) {
261 		return put(Long.valueOf(key), value);
262 	}
263 
264 	/**
265 	 * Returns the specification version of the underlying JDK {@link Map} class
266 	 * that backs this implementation.
267 	 *
268 	 * @return the specification version string, or {@code null} if unavailable
269 	 */
270 	default String getMapVersion() {
271 		return getClass().getSuperclass().getPackage().getSpecificationVersion();
272 	}
273 
274 // -------------------------------------------------------------------------
275 // Factory methods
276 // -------------------------------------------------------------------------
277 
278 	/**
279 	 * Creates a new hash-based associative array (backed by {@link java.util.HashMap}).
280 	 *
281 	 * @return a new {@link HashAssocArray}
282 	 */
283 	static AssocArray createHash() {
284 		return new HashAssocArray();
285 	}
286 
287 	/**
288 	 * Creates a new sorted associative array (backed by {@link java.util.TreeMap}
289 	 * with AWK key ordering).
290 	 *
291 	 * @return a new {@link SortedAssocArray}
292 	 */
293 	static AssocArray createSorted() {
294 		return new SortedAssocArray();
295 	}
296 
297 	/**
298 	 * Creates a new associative array of the appropriate type.
299 	 *
300 	 * @param sortedArrayKeys {@code true} to create a sorted (tree-backed) array,
301 	 *        {@code false} for a hash-backed array
302 	 * @return a new {@link AssocArray} instance
303 	 */
304 	static AssocArray create(boolean sortedArrayKeys) {
305 		return sortedArrayKeys ? createSorted() : createHash();
306 	}
307 
308 	/**
309 	 * Creates a new associative array materialized from a Java {@link List}.
310 	 * <p>
311 	 * List elements are stored under zero-based {@link Long} keys. Nested
312 	 * {@link List} values are recursively materialized as associative arrays so
313 	 * JSON-like object trees can be traversed with AWK array syntax.
314 	 * </p>
315 	 *
316 	 * @param values list values to expose as an AWK array
317 	 * @param sortedArrayKeys {@code true} to create a sorted array, {@code false}
318 	 *        for a hash-backed array
319 	 * @return a new {@link AssocArray} containing the list values
320 	 */
321 	static AssocArray createFromList(List<?> values, boolean sortedArrayKeys) {
322 		return ListAssocArray.createFromList(values, sortedArrayKeys);
323 	}
324 
325 	/**
326 	 * Normalizes an externally supplied structured value before the AVM stores it.
327 	 * <p>
328 	 * {@link List} values are converted to {@link AssocArray} instances.
329 	 * {@link Map} values are kept in place to preserve direct-map performance, but
330 	 * their nested list values are recursively converted.
331 	 * </p>
332 	 *
333 	 * @param value value to normalize
334 	 * @param sortedArrayKeys {@code true} when converted lists should use sorted
335 	 *        array keys
336 	 * @return the normalized value
337 	 */
338 	static Object normalizeValue(Object value, boolean sortedArrayKeys) {
339 		return ListAssocArray.normalizeValue(value, sortedArrayKeys);
340 	}
341 
342 }