1 package io.jawk.ext;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import java.io.File;
26 import java.math.BigInteger;
27
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Calendar;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.Date;
35 import java.util.GregorianCalendar;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.TimeZone;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44
45 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
46 import io.jawk.backend.AVM;
47 import io.jawk.ext.annotations.JawkAssocArray;
48 import io.jawk.ext.annotations.JawkBeforeStart;
49 import io.jawk.ext.annotations.JawkFunction;
50 import io.jawk.ext.annotations.JawkOptional;
51 import io.jawk.ext.annotations.JawkRawValue;
52 import io.jawk.ext.annotations.JawkRegexp;
53 import io.jawk.intermediate.UninitializedObject;
54 import io.jawk.intermediate.UntypedObject;
55 import io.jawk.jrt.IllegalAwkArgumentException;
56 import io.jawk.jrt.JRT;
57 import io.jawk.jrt.StrNum;
58
59
60
61
62 public class GawkExtension extends AbstractExtension implements JawkExtension {
63
64 private static final String VAL_TYPE_ASC = "@val_type_asc";
65
66
67 private static final String DEFAULT_STRFTIME_FORMAT = "%a %b %e %H:%M:%S %Z %Y";
68
69
70 private static final String DEFAULT_FPAT = "[^\\s]+";
71
72
73 private static final String DEFAULT_TEXTDOMAIN = "messages";
74
75
76
77
78
79
80
81
82 private static final String DEFAULT_LOCALE_DIRECTORY = "/usr/share/locale";
83
84
85 private static final Set<String> LOCALE_CATEGORIES = Collections
86 .unmodifiableSet(
87 new HashSet<String>(
88 Arrays
89 .asList(
90 "LC_ALL",
91 "LC_COLLATE",
92 "LC_CTYPE",
93 "LC_MESSAGES",
94 "LC_MONETARY",
95 "LC_NUMERIC",
96 "LC_TIME")));
97
98
99 private AVM avm;
100
101
102 private Set<String> warnedComparators;
103
104
105 private Map<String, String> textdomainBindings;
106
107
108
109
110
111
112
113
114
115 public GawkExtension() {
116
117 }
118
119 private static final class SortEntry {
120 private final Object index;
121 private final Object value;
122
123 private SortEntry(Object indexParam, Object valueParam) {
124 this.index = indexParam;
125 this.value = valueParam;
126 }
127 }
128
129
130 @Override
131 public String getExtensionName() {
132 return "GawkExtension";
133 }
134
135
136
137
138
139
140
141
142
143 @JawkBeforeStart
144 @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "The extension is a per-engine instance deliberately bound to its interpreter")
145 public void initializeGawkVariables(AVM avmParam, JRT jrt) {
146 this.avm = avmParam;
147 avm.setForInKeyOrder(this::orderForInKeys);
148 }
149
150
151
152
153
154
155
156
157
158 @JawkFunction("asort")
159 public Long asort(
160 @JawkAssocArray Map<Object, Object> source,
161 @JawkOptional @JawkAssocArray Map<Object, Object> dest,
162 @JawkOptional Object how) {
163 return sort(source, dest, how, false);
164 }
165
166
167
168
169
170
171
172
173
174 @JawkFunction("asorti")
175 public Long asorti(
176 @JawkAssocArray Map<Object, Object> source,
177 @JawkOptional @JawkAssocArray Map<Object, Object> dest,
178 @JawkOptional Object how) {
179 return sort(source, dest, how, true);
180 }
181
182
183
184
185
186
187
188
189 @JawkFunction("typeof")
190 public String typeof(@JawkRawValue Object value, @JawkOptional @JawkAssocArray Map<Object, Object> meta) {
191 if (meta != null) {
192 meta.clear();
193 if (value instanceof Map) {
194 meta.put("array_type", arrayType((Map<?, ?>) value));
195 }
196 }
197 return typeOf(value);
198 }
199
200
201
202
203
204
205
206 @JawkFunction("isarray")
207 public Long isarray(@JawkRawValue Object value) {
208 return value instanceof Map ? Long.valueOf(1L) : Long.valueOf(0L);
209 }
210
211
212
213
214
215
216
217 @JawkFunction("mkbool")
218 public GawkBool mkbool(Object value) {
219
220
221 return new GawkBool(getJrt().toBoolean(value));
222 }
223
224
225
226
227
228
229
230
231
232
233 @JawkFunction("gensub")
234 public String gensub(@JawkRegexp Object regexp, Object replacement, Object how, @JawkOptional Object target) {
235 Pattern pattern = regexp instanceof Pattern ?
236 (Pattern) regexp : Pattern.compile(toAwkString(regexp));
237
238 pattern = getJrt().caseAwarePattern(pattern);
239 Object targetValue = target == null ? getJrt().getInputLine() : target;
240 Matcher matcher = pattern.matcher(toAwkString(targetValue));
241 String repl = JRT.prepareReplacement(toAwkString(replacement), pattern.matcher("").groupCount());
242 String selector = toAwkString(how);
243
244 if (!selector.isEmpty() && (selector.charAt(0) == 'g' || selector.charAt(0) == 'G')) {
245 return matcher.replaceAll(repl);
246 }
247
248
249
250 double selected = JRT.toDouble(how);
251 if (selected < 1.0D) {
252 warnAtCurrentLine("gensub: third argument `%s' treated as 1", selector);
253 selected = 1.0D;
254 }
255 int occurrence = (int) selected;
256 if (occurrence == 1) {
257 return matcher.replaceFirst(repl);
258 }
259 StringBuffer result = new StringBuffer();
260 int seen = 0;
261 while (matcher.find()) {
262 seen++;
263 if (seen == occurrence) {
264 matcher.appendReplacement(result, repl);
265 break;
266 }
267 }
268 matcher.appendTail(result);
269 return result.toString();
270 }
271
272
273
274
275
276
277 @JawkFunction("systime")
278 public Long systime() {
279 return Long.valueOf(System.currentTimeMillis() / 1000L);
280 }
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296 @JawkFunction("mktime")
297 public Long mktime(Object datespec, @JawkOptional Object utcFlag) {
298 String[] fields = toAwkString(datespec).trim().split("\\s+");
299 if (fields.length < 6 || fields.length > 7) {
300 return Long.valueOf(-1L);
301 }
302 int[] values = new int[fields.length];
303 for (int i = 0; i < fields.length; i++) {
304 try {
305 values[i] = Integer.parseInt(fields[i]);
306 } catch (NumberFormatException e) {
307 return Long.valueOf(-1L);
308 }
309 }
310 boolean utc = utcFlag != null && getJrt().toBoolean(utcFlag);
311 TimeZone timeZone = utc ? TimeZone.getTimeZone("UTC") : localTimeZone();
312 GregorianCalendar calendar = new GregorianCalendar(timeZone);
313
314 calendar.setGregorianChange(new Date(Long.MIN_VALUE));
315 calendar.setLenient(true);
316 calendar.clear();
317 calendar.set(values[0], values[1] - 1, values[2], values[3], values[4], values[5]);
318 if (fields.length == 7 && !utc && values[6] >= 0) {
319
320
321 calendar.set(Calendar.DST_OFFSET, values[6] > 0 ? timeZone.getDSTSavings() : 0);
322 }
323 return Long.valueOf(Math.floorDiv(calendar.getTimeInMillis(), 1000L));
324 }
325
326
327
328
329
330
331
332
333
334
335 @JawkFunction("strftime")
336 public String strftime(
337 @JawkOptional Object format,
338 @JawkOptional Object timestamp,
339 @JawkOptional Object utcFlag) {
340 String formatString = format == null ? defaultStrftimeFormat() : toAwkString(format);
341 long seconds = timestamp == null ?
342 System.currentTimeMillis() / 1000L : (long) JRT.toDouble(timestamp);
343 boolean utc = utcFlag != null && getJrt().toBoolean(utcFlag);
344 TimeZone timeZone = utc ? TimeZone.getTimeZone("UTC") : localTimeZone();
345 return Strftime.format(formatString, seconds, timeZone);
346 }
347
348
349
350
351
352
353
354
355
356
357
358 private TimeZone localTimeZone() {
359 Object environ = getVm().getVariable("ENVIRON");
360 if (environ instanceof Map) {
361 @SuppressWarnings("unchecked")
362 Map<Object, Object> environMap = (Map<Object, Object>) environ;
363 String tz = getJrt().getAwkStringEntry(environMap, "TZ");
364 if (tz != null) {
365 if (tz.startsWith(":")) {
366
367
368 tz = tz.substring(1);
369 }
370
371 return TimeZone.getTimeZone(tz.isEmpty() ? "UTC" : tz);
372 }
373 }
374 return TimeZone.getDefault();
375 }
376
377
378 private String defaultStrftimeFormat() {
379 Object procinfo = getVm().getVariable("PROCINFO");
380 if (procinfo instanceof Map) {
381 @SuppressWarnings("unchecked")
382 Map<Object, Object> procinfoMap = (Map<Object, Object>) procinfo;
383 String format = getJrt().getAwkStringEntry(procinfoMap, "strftime");
384 if (format != null) {
385 return format;
386 }
387 }
388 return DEFAULT_STRFTIME_FORMAT;
389 }
390
391
392
393
394
395
396
397
398
399 @JawkFunction("strtonum")
400 public Number strtonum(@JawkRawValue Object value) {
401 if (value instanceof Number) {
402 return (Number) value;
403 }
404 if (value instanceof StrNum && ((StrNum) value).isNumber()) {
405
406
407 return Double.valueOf(((StrNum) value).doubleValue());
408 }
409 String text = toAwkString(value);
410 switch (numberBase(text)) {
411 case 16:
412 return parseNonDecimal(text, 2, 16);
413 case 8:
414 return parseNonDecimal(text, 1, 8);
415 default:
416 return Double.valueOf(JRT.toDouble(text));
417 }
418 }
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435 private static int numberBase(String text) {
436 if (text.length() < 2 || text.charAt(0) != '0') {
437 return 10;
438 }
439 char second = text.charAt(1);
440 if (second == 'x' || second == 'X') {
441 return 16;
442 }
443 for (int i = 1; i < text.length(); i++) {
444 char c = text.charAt(i);
445 if (c == '.' || c == 'e' || c == 'E') {
446 return 10;
447 }
448 if (c < '0' || c > '9') {
449 break;
450 }
451 if (c > '7') {
452 return 10;
453 }
454 }
455 return 8;
456 }
457
458
459
460
461
462
463
464 private static Number parseNonDecimal(String text, int offset, int base) {
465 int end = offset;
466 while (end < text.length() && Character.digit(text.charAt(end), base) >= 0) {
467 end++;
468 }
469 if (end == offset) {
470 return Long.valueOf(0L);
471 }
472 String digits = text.substring(offset, end);
473 try {
474 return Long.valueOf(Long.parseLong(digits, base));
475 } catch (NumberFormatException overflow) {
476 return Double.valueOf(new BigInteger(digits, base).doubleValue());
477 }
478 }
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493 @JawkFunction("patsplit")
494 public Long patsplit(
495 Object source,
496 @JawkAssocArray Map<Object, Object> array,
497 @JawkOptional @JawkRegexp Object fieldpat,
498 @JawkOptional @JawkAssocArray Map<Object, Object> seps) {
499 if (array == seps) {
500 throw new IllegalAwkArgumentException("patsplit: cannot use the same array for second and fourth args");
501 }
502 String str = toAwkString(source);
503 Pattern pattern = fieldPattern(fieldpat);
504 array.clear();
505 if (seps != null) {
506 seps.clear();
507 }
508 if (str.isEmpty()) {
509 return Long.valueOf(0L);
510 }
511
512
513
514
515
516
517
518
519
520 Matcher matcher = pattern.matcher(str);
521 int length = str.length();
522 int pos = 0;
523 int previousEnd = 0;
524 long fieldCount = 0L;
525 boolean lastMatchNonEmpty = false;
526 while (pos <= length) {
527 matcher.region(pos, length);
528 if (!matcher.find()) {
529 break;
530 }
531 int start = matcher.start();
532 int end = matcher.end();
533 if (end > start) {
534 lastMatchNonEmpty = true;
535 putSeparator(seps, fieldCount, str.substring(previousEnd, start));
536 array.put(Long.valueOf(++fieldCount), getJrt().toInputScalar(str.substring(start, end)));
537 previousEnd = end;
538 pos = end;
539 if (pos >= length) {
540 break;
541 }
542 } else if (lastMatchNonEmpty) {
543 lastMatchNonEmpty = false;
544 pos = str.offsetByCodePoints(pos, 1);
545 } else {
546 putSeparator(seps, fieldCount, str.substring(previousEnd, start));
547 array.put(Long.valueOf(++fieldCount), getJrt().toInputScalar(""));
548 previousEnd = start;
549 if (start >= length) {
550
551 break;
552 }
553 pos = str.offsetByCodePoints(start, 1);
554 }
555 }
556
557
558 putSeparator(seps, fieldCount, str.substring(previousEnd));
559 return Long.valueOf(fieldCount);
560 }
561
562
563 private void putSeparator(Map<Object, Object> separators, long index, String value) {
564 if (separators != null) {
565 separators.put(Long.valueOf(index), getJrt().toInputScalar(value));
566 }
567 }
568
569
570
571
572
573
574
575 private Pattern fieldPattern(Object fieldpat) {
576 if (fieldpat instanceof Pattern) {
577 Pattern pattern = (Pattern) fieldpat;
578 requireNonEmptyFieldPattern(pattern.pattern());
579 return getJrt().caseAwarePattern(pattern);
580 }
581 String expression;
582 if (fieldpat != null) {
583 expression = toAwkString(fieldpat);
584 } else {
585 Object fpat = getVm().getVariable("FPAT");
586 if (fpat == null || fpat instanceof UninitializedObject) {
587 return getJrt().dynamicPattern(DEFAULT_FPAT);
588 }
589 expression = toAwkString(fpat);
590 }
591 requireNonEmptyFieldPattern(expression);
592 return getJrt().dynamicPattern(expression);
593 }
594
595
596 private static void requireNonEmptyFieldPattern(String expression) {
597 if (expression.isEmpty()) {
598 throw new IllegalAwkArgumentException("patsplit: field pattern must be non-null");
599 }
600 }
601
602
603
604
605
606
607
608
609
610
611
612 @JawkFunction("dcgettext")
613 public String dcgettext(Object string, @JawkOptional Object domain, @JawkOptional Object category) {
614 checkLocaleCategory(category);
615
616
617 return toAwkString(string);
618 }
619
620
621
622
623
624
625
626
627
628
629
630
631
632 @JawkFunction("dcngettext")
633 public String dcngettext(
634 Object singular,
635 Object plural,
636 Object number,
637 @JawkOptional Object domain,
638 @JawkOptional Object category) {
639 checkLocaleCategory(category);
640 return (long) JRT.toDouble(number) == 1L ? toAwkString(singular) : toAwkString(plural);
641 }
642
643
644
645
646
647 private void checkLocaleCategory(Object category) {
648 if (category == null) {
649 return;
650 }
651 String name = toAwkString(category);
652 if (!LOCALE_CATEGORIES.contains(name)) {
653 throw new IllegalAwkArgumentException("dcgettext: `" + name + "' is not a valid locale category");
654 }
655 }
656
657
658
659
660
661
662
663
664
665
666 @JawkFunction("bindtextdomain")
667 public String bindtextdomain(Object directory, @JawkOptional Object domain) {
668 String domainName = domain == null ? currentTextdomain() : toAwkString(domain);
669 if (domainName.isEmpty()) {
670
671
672 return "";
673 }
674 String directoryName = toAwkString(directory);
675 if (textdomainBindings == null) {
676 textdomainBindings = new HashMap<String, String>();
677 }
678 if (!directoryName.isEmpty()) {
679 textdomainBindings.put(domainName, directoryName);
680 }
681 String bound = textdomainBindings.get(domainName);
682 return bound == null ? DEFAULT_LOCALE_DIRECTORY : bound;
683 }
684
685
686 private String currentTextdomain() {
687 Object textdomain = getVm().getVariable("TEXTDOMAIN");
688 String name = textdomain == null ? "" : toAwkString(textdomain);
689 return name.isEmpty() ? DEFAULT_TEXTDOMAIN : name;
690 }
691
692
693
694
695
696 private void warnAtCurrentLine(String format, Object... args) {
697 String source = avm == null ? null : avm.getSourceDescription();
698 String basename = source == null ? "" : new File(source).getName();
699 getJrt()
700 .printWarning(
701 String
702 .format(
703 "gawk: %s:%d: warning: %s",
704 basename,
705 avm == null ? 0 : avm.getCurrentLineNumber(),
706 String.format(format, args)));
707 }
708
709
710
711
712
713
714 private Collection<Object> orderForInKeys(Map<Object, Object> map) {
715 String mode = currentSortedIn();
716 if (mode == null || mode.isEmpty() || "@unsorted".equals(mode)) {
717 return map.keySet();
718 }
719 return sortedKeys(map, effectiveSortMode(mode, VAL_TYPE_ASC), getJrt(), currentIgnoreCase());
720 }
721
722 private String currentSortedIn() {
723 Object procinfo = getVm().getVariable("PROCINFO");
724 if (!(procinfo instanceof Map)) {
725 return null;
726 }
727 @SuppressWarnings("unchecked")
728 Map<Object, Object> procinfoMap = (Map<Object, Object>) procinfo;
729 return getJrt().getAwkStringEntry(procinfoMap, "sorted_in");
730 }
731
732
733
734
735
736
737 private String effectiveSortMode(String mode, String defaultMode) {
738 if (mode.isEmpty()) {
739
740 return defaultMode;
741 }
742 if (mode.charAt(0) != '@') {
743 warnUnsupportedComparator(mode);
744 return defaultMode;
745 }
746 return mode;
747 }
748
749 private void warnUnsupportedComparator(String name) {
750 if (warnedComparators == null) {
751 warnedComparators = new HashSet<String>();
752 }
753 if (warnedComparators.add(name)) {
754 warnAtCurrentLine("sort comparison function `%s' is not supported; using default ordering", name);
755 }
756 }
757
758 private Long sort(Map<Object, Object> source, Map<Object, Object> dest, Object how, boolean indicesAsValues) {
759 Map<Object, Object> destination = dest == null ? source : dest;
760
761
762 String defaultMode = indicesAsValues ? "@ind_str_asc" : VAL_TYPE_ASC;
763 String mode = how == null ? defaultMode : effectiveSortMode(toAwkString(how), defaultMode);
764 List<SortEntry> entries = entries(source);
765
766 if (!"@unsorted".equals(mode)) {
767 Collections.sort(entries, comparator(mode, getJrt(), currentIgnoreCase()));
768 }
769 destination.clear();
770 long idx = 1L;
771 for (SortEntry entry : entries) {
772
773
774 Object value = indicesAsValues ? getJrt().toAwkString(entry.index) : entry.value;
775 destination.put(Long.valueOf(idx++), value);
776 }
777 return Long.valueOf(entries.size());
778 }
779
780
781
782
783
784
785
786
787
788
789 private static List<Object> sortedKeys(Map<Object, Object> map, String mode, JRT jrt, boolean ignoreCase) {
790 List<SortEntry> entries = entries(map);
791 Collections.sort(entries, comparator(mode, jrt, ignoreCase));
792 List<Object> keys = new ArrayList<Object>(entries.size());
793 for (SortEntry entry : entries) {
794 keys.add(entry.index);
795 }
796 return keys;
797 }
798
799 private static List<SortEntry> entries(Map<Object, Object> map) {
800 List<SortEntry> entries = new ArrayList<SortEntry>(map.size());
801 for (Map.Entry<Object, Object> entry : map.entrySet()) {
802 entries.add(new SortEntry(entry.getKey(), entry.getValue()));
803 }
804 return entries;
805 }
806
807 private boolean currentIgnoreCase() {
808 return getJrt().isIgnoreCase();
809 }
810
811
812
813
814
815 private static Comparator<SortEntry> comparator(String effectiveMode, JRT jrt, boolean ignoreCase) {
816 boolean desc = effectiveMode.endsWith("_desc");
817 Comparator<SortEntry> comparator;
818
819
820
821
822
823
824
825 switch (effectiveMode) {
826 case "@ind_num_asc":
827 case "@ind_num_desc":
828 comparator = (left, right) -> compareNumericThenText(left.index, right.index, jrt, ignoreCase);
829 break;
830 case "@ind_str_asc":
831 case "@ind_str_desc":
832 comparator = (left, right) -> compareStrings(left.index, right.index, jrt, ignoreCase);
833 break;
834 case "@ind_type_asc":
835 case "@ind_type_desc":
836 comparator = (left, right) -> compareByTypeThenValue(left.index, right.index, jrt, ignoreCase);
837 break;
838 case "@val_num_asc":
839 case "@val_num_desc":
840 comparator = (left, right) -> compareNumericThenText(left.value, right.value, jrt, ignoreCase);
841 break;
842 case "@val_str_asc":
843 case "@val_str_desc":
844 comparator = (left, right) -> compareStrings(left.value, right.value, jrt, ignoreCase);
845 break;
846 case "@val_type_asc":
847 case "@val_type_desc":
848 comparator = (left, right) -> compareByTypeThenValue(left.value, right.value, jrt, ignoreCase);
849 break;
850 default:
851 throw new IllegalAwkArgumentException("Invalid sort comparison mode '" + effectiveMode + "'");
852 }
853 return desc ? comparator.reversed() : comparator;
854 }
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872 private static int compareByTypeThenValue(Object left, Object right, JRT jrt, boolean ignoreCase) {
873 int leftRank = typeRank(left);
874 int rightRank = typeRank(right);
875 if (leftRank != rightRank) {
876 return Integer.compare(leftRank, rightRank);
877 }
878 if (left instanceof Map && right instanceof Map) {
879 return 0;
880 }
881 if (leftRank == 0) {
882 return compareNumbers(left, right);
883 }
884 return compareStrings(left, right, jrt, ignoreCase);
885 }
886
887
888 private static int typeRank(Object value) {
889 if (value instanceof Number || isStrnum(value)) {
890 return 0;
891 }
892 if (value instanceof Map) {
893 return 2;
894 }
895 return 1;
896 }
897
898
899 private static int compareNumbers(Object left, Object right) {
900 return Double.compare(numericSortValue(left), numericSortValue(right));
901 }
902
903 private static double numericSortValue(Object value) {
904 return value instanceof Map ? 0.0D : JRT.toDouble(value);
905 }
906
907
908
909
910
911
912 private static int compareNumericThenText(Object left, Object right, JRT jrt, boolean ignoreCase) {
913 if (left instanceof Map || right instanceof Map) {
914 if (left instanceof Map && right instanceof Map) {
915 return 0;
916 }
917 return left instanceof Map ? 1 : -1;
918 }
919 int numeric = compareNumbers(left, right);
920 if (numeric != 0) {
921 return numeric;
922 }
923 return compareStrings(left, right, jrt, ignoreCase);
924 }
925
926
927
928
929
930 private static int compareStrings(Object left, Object right, JRT jrt, boolean ignoreCase) {
931 if (left instanceof Map || right instanceof Map) {
932 if (left instanceof Map && right instanceof Map) {
933 return 0;
934 }
935 return left instanceof Map ? 1 : -1;
936 }
937 String leftString = jrt.toAwkString(left);
938 String rightString = jrt.toAwkString(right);
939
940
941 return ignoreCase ? leftString.compareToIgnoreCase(rightString) : leftString.compareTo(rightString);
942 }
943
944 private static String typeOf(Object value) {
945 if (value == null || value instanceof UntypedObject) {
946 return "untyped";
947 }
948 if (value instanceof Map) {
949 return "array";
950 }
951 if (value instanceof GawkBool) {
952 return "number|bool";
953 }
954 if (value instanceof Number) {
955 return "number";
956 }
957 if (value instanceof Pattern) {
958 return "regexp";
959 }
960 if (value instanceof UninitializedObject) {
961 return "unassigned";
962 }
963 return isStrnum(value) ? "strnum" : "string";
964 }
965
966 private static boolean isStrnum(Object value) {
967 return value instanceof StrNum && ((StrNum) value).isNumber();
968 }
969
970 private static String arrayType(Map<?, ?> map) {
971 if (map.isEmpty()) {
972 return "null";
973 }
974 boolean allNonNegativeIntegral = true;
975 for (Object key : map.keySet()) {
976 if (!(key instanceof Number)) {
977 return "str";
978 }
979 long longValue = ((Number) key).longValue();
980 double doubleValue = ((Number) key).doubleValue();
981 if (Double.compare(doubleValue, (double) longValue) != 0) {
982 return "str";
983 }
984 if (longValue < 0) {
985 allNonNegativeIntegral = false;
986 }
987 }
988 return allNonNegativeIntegral ? "cint" : "int";
989 }
990 }