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 io.jawk.intermediate.UninitializedObject;
28 import io.jawk.intermediate.UntypedObject;
29
30 /**
31 * An AWK associative array.
32 * <p>
33 * This interface extends {@link Map} and provides AWK-specific behaviour:
34 * automatic key normalization (null and uninitialized values map to {@code ""}),
35 * numeric key coercion ({@code "1"} and {@code 1L} address the same slot), and
36 * auto-creation of blank entries on first access.
37 * </p>
38 * <p>
39 * Concrete implementations directly extend a JDK {@link Map} class to avoid
40 * delegation overhead:
41 * </p>
42 * <ul>
43 * <li>{@link HashAssocArray} — backed by {@link java.util.HashMap}</li>
44 * <li>{@link ListAssocArray} — materialized from a {@link java.util.List}
45 * and backed by {@link java.util.HashMap}</li>
46 * <li>{@link SortedAssocArray} — backed by {@link java.util.TreeMap} with
47 * AWK key ordering</li>
48 * </ul>
49 * <p>
50 * Use the factory methods to create instances:
51 * </p>
52 *
53 * <pre>
54 * AssocArray hash = AssocArray.createHash();
55 * AssocArray sorted = AssocArray.createSorted();
56 * AssocArray list = AssocArray.createFromList(values, sortedArrayKeys);
57 * AssocArray aa = AssocArray.create(sortedArrayKeys);
58 * </pre>
59 *
60 * @author Danny Daglas
61 */
62 public interface AssocArray extends Map<Object, Object> {
63
64 /** A blank (uninitialized) value shared across all AWK array accesses. */
65 UninitializedObject BLANK = new UninitializedObject();
66
67 /** An untyped value shared across newly referenced AWK array elements. */
68 UntypedObject UNTYPED = new UntypedObject();
69
70 // -------------------------------------------------------------------------
71 // Key-normalization helpers (used by concrete implementations)
72 // -------------------------------------------------------------------------
73
74 /**
75 * Converts a key to the canonical form expected by AWK: {@code null} and
76 * {@link UninitializedObject} map to the empty string, and internal input
77 * strings map to their string value.
78 *
79 * @param key the raw key
80 * @return the normalized key, never {@code null}
81 */
82 static Object normalizeKey(Object key) {
83 if (key == null || key instanceof UninitializedObject) {
84 return "";
85 }
86 if (key instanceof StrNum) {
87 return key.toString();
88 }
89 if (key instanceof Double || key instanceof Float) {
90 double numericKey = ((Number) key).doubleValue();
91 if (JRT.isActuallyLong(numericKey)) {
92 return Long.valueOf((long) Math.rint(numericKey));
93 }
94 }
95 return key;
96 }
97
98 /**
99 * Attempts to parse the key as a {@code Long}.
100 *
101 * @param key the key to parse (must not be {@code null})
102 * @return the {@code Long} value, or {@code null} if the key cannot be parsed
103 * as a long integer
104 */
105 static Long toLongKey(Object key) {
106 try {
107 return Long.parseLong(key.toString());
108 } catch (Exception e) { // NOPMD - EmptyCatchBlock: intentionally ignored
109 return null;
110 }
111 }
112
113 // -------------------------------------------------------------------------
114 // AWK-specific default methods
115 // -------------------------------------------------------------------------
116
117 /**
118 * Returns whether a particular key is contained within the associative array.
119 * <p>
120 * Unlike {@link #get(Object)}, which auto-creates a blank entry when the key
121 * is absent, this method does not modify the array. It exists to support the
122 * AWK {@code IN} keyword.
123 * </p>
124 *
125 * @param key Key to be checked
126 * @return {@code true} if the key (or its numeric equivalent) is present
127 */
128 default boolean isIn(Object key) {
129 key = normalizeKey(key);
130 if (containsKey(key)) {
131 return true;
132 }
133 try {
134 long iKey = Long.parseLong(key.toString());
135 return containsKey(iKey);
136 } catch (Exception e) { // NOPMD - EmptyCatchBlock: intentionally ignored
137 }
138 return false;
139 }
140
141 /**
142 * Provides a string representation of this associative array, recursively
143 * rendering nested arrays.
144 *
145 * @return a human-readable map string of the form {@code {key=value, ...}}
146 */
147 default String mapString() {
148 // Since extensions allow assoc arrays to become keys as well,
149 // we render nested arrays recursively rather than using toString().
150 StringBuilder sb = new StringBuilder().append('{');
151 int cnt = 0;
152 for (Map.Entry<Object, Object> entry : entrySet()) {
153 if (cnt > 0) {
154 sb.append(", ");
155 }
156 Object key = entry.getKey();
157 if (key instanceof AssocArray) {
158 sb.append(((AssocArray) key).mapString());
159 } else {
160 sb.append(key.toString());
161 }
162 sb.append('=');
163 Object value = entry.getValue();
164 if (value instanceof AssocArray) {
165 sb.append(((AssocArray) value).mapString());
166 } else {
167 sb.append(value.toString());
168 }
169 ++cnt;
170 }
171 return sb.append('}').toString();
172 }
173
174 /**
175 * Stores a value using a primitive {@code long} key, bypassing string parsing.
176 * <p>
177 * This is a convenience overload for callers that already hold a {@code long}
178 * key. The default implementation boxes the key and delegates to
179 * {@link #put(Object, Object)}.
180 * </p>
181 *
182 * @param key the long key
183 * @param value the value to associate with the key
184 * @return the previous value associated with the key, or {@code null}
185 */
186 default Object put(long key, Object value) {
187 return put(Long.valueOf(key), value);
188 }
189
190 /**
191 * Returns the specification version of the underlying JDK {@link Map} class
192 * that backs this implementation.
193 *
194 * @return the specification version string, or {@code null} if unavailable
195 */
196 default String getMapVersion() {
197 return getClass().getSuperclass().getPackage().getSpecificationVersion();
198 }
199
200 // -------------------------------------------------------------------------
201 // Factory methods
202 // -------------------------------------------------------------------------
203
204 /**
205 * Creates a new hash-based associative array (backed by {@link java.util.HashMap}).
206 *
207 * @return a new {@link HashAssocArray}
208 */
209 static AssocArray createHash() {
210 return new HashAssocArray();
211 }
212
213 /**
214 * Creates a new sorted associative array (backed by {@link java.util.TreeMap}
215 * with AWK key ordering).
216 *
217 * @return a new {@link SortedAssocArray}
218 */
219 static AssocArray createSorted() {
220 return new SortedAssocArray();
221 }
222
223 /**
224 * Creates a new associative array of the appropriate type.
225 *
226 * @param sortedArrayKeys {@code true} to create a sorted (tree-backed) array,
227 * {@code false} for a hash-backed array
228 * @return a new {@link AssocArray} instance
229 */
230 static AssocArray create(boolean sortedArrayKeys) {
231 return sortedArrayKeys ? createSorted() : createHash();
232 }
233
234 /**
235 * Creates a new associative array materialized from a Java {@link List}.
236 * <p>
237 * List elements are stored under zero-based {@link Long} keys. Nested
238 * {@link List} values are recursively materialized as associative arrays so
239 * JSON-like object trees can be traversed with AWK array syntax.
240 * </p>
241 *
242 * @param values list values to expose as an AWK array
243 * @param sortedArrayKeys {@code true} to create a sorted array, {@code false}
244 * for a hash-backed array
245 * @return a new {@link AssocArray} containing the list values
246 */
247 static AssocArray createFromList(List<?> values, boolean sortedArrayKeys) {
248 return ListAssocArray.createFromList(values, sortedArrayKeys);
249 }
250
251 /**
252 * Normalizes an externally supplied structured value before the AVM stores it.
253 * <p>
254 * {@link List} values are converted to {@link AssocArray} instances.
255 * {@link Map} values are kept in place to preserve direct-map performance, but
256 * their nested list values are recursively converted.
257 * </p>
258 *
259 * @param value value to normalize
260 * @param sortedArrayKeys {@code true} when converted lists should use sorted
261 * array keys
262 * @return the normalized value
263 */
264 static Object normalizeValue(Object value, boolean sortedArrayKeys) {
265 return ListAssocArray.normalizeValue(value, sortedArrayKeys);
266 }
267
268 }