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