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