AI2 Component  (Version nb184)
JavaStringUtils.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2017-2020 MIT, All rights reserve
3 // Released under the Apache License, Version 2.0
4 // http://www.apache.org/licenses/LICENSE-2.0
5 
6 package com.google.appinventor.components.runtime.util;
7 
8 import android.util.Log;
9 
10 import java.util.ArrayList;
11 import java.util.Collections;
12 import java.util.Comparator;
13 import java.util.HashMap;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.TreeSet;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 
27 public class JavaStringUtils {
34  private static class MappingOrder {
40  public void changeOrder(List<String> keys, String text) {
41  // Default option: Do nothing (dictionary order)
42  }
43  }
44 
48  private static class MappingLongestStringFirstOrder extends MappingOrder {
49  @Override
50  public void changeOrder(List<String> keys, String text) {
51  Collections.sort(keys, new Comparator<String>() {
52  @Override
53  public int compare(String s, String t1) {
54  // Sort in descending order of string length
55  return Integer.compare(t1.length(), s.length());
56  }
57  });
58  }
59  }
60 
70  private static class MappingEarliestOccurrenceFirstOrder extends MappingOrder {
71  @Override
72  public void changeOrder(List<String> keys, String text) {
73  // Construct a map for first index of occurrence for String
74  final Map<String, Integer> occurrenceIndices = new HashMap<>();
75 
76  // TODO: Can we optimize the O(mn) loop with m = length of text,
77  // TODO: n = number of keys?
78  for (String key : keys) {
79  int firstIndex = text.indexOf(key);
80 
81  // No first index; Key should gain less priority than
82  // other occurrences (this value can be arbitrary)
83  if (firstIndex == -1) {
84  firstIndex = text.length() + occurrenceIndices.size();
85  }
86 
87  // Map key to first index of occurrence
88  occurrenceIndices.put(key, firstIndex);
89  }
90 
91  Collections.sort(keys, new Comparator<String>() {
92  @Override
93  public int compare(String s, String t1) {
94  // Sort in ascending order by first index in String
95  int id1 = occurrenceIndices.get(s);
96  int id2 = occurrenceIndices.get(t1);
97 
98  if (id1 == id2) {
99  // Use longer string instead if indices equal
100  return Integer.compare(t1.length(), s.length());
101  } else {
102  // Take smaller index first
103  return Integer.compare(id1, id2);
104  }
105  }
106  });
107  }
108  }
109 
114  private static class Range {
115  int start; // Inclusive start index
116  int end; // Exclusive end index
117  String text; // Replacement text
118 
126  public Range(int start, int end, String text) {
127  this.start = start;
128  this.end = end;
129  this.text = text;
130  }
131  }
132  /*
133  * Range comparator that sorts ranges in descending order of range
134  * end times. Overlapping ranges are considered equal by the comparator.
135  */
136  private static class RangeComparator implements Comparator<Range> {
137  @Override
138  public int compare(Range r1, Range r2) {
139  // First, test for overlap. We do this by taking the maximum
140  // start point of a range, and the minimum end point of a range.
141  int maxStart = Math.max(r1.start, r2.start);
142  int minEnd = Math.min(r1.end, r2.end);
143 
144  // If maxStart <= minEnd, the ranges overlap (or touch). Since the
145  // min end index is exclusive, we take the strictly less < instead,
146  // since the ranges could touch (due to the end ID being overlapping)
147  if (maxStart < minEnd) {
148  // Ranges overlap. Consider them equal, thus not inserting a new range.
149  return 0;
150  } else {
151  // Ranges unequal. Sort by endpoint in descending order to get the last
152  // range first in the TreeSet.
153  return Integer.compare(r2.end, r1.end);
154  }
155  }
156  }
157 
158  public static final String LOG_TAG_JOIN_STRINGS = "JavaJoinListOfStrings";
159  private static final boolean DEBUG = false;
160 
165  private static final MappingOrder mappingOrderDictionary = new MappingOrder();
166  private static final MappingOrder mappingOrderLongestStringFirst = new MappingLongestStringFirstOrder();
167  private static final MappingOrder mappingOrderEarliestOccurrence = new MappingEarliestOccurrenceFirstOrder();
168  private static final Comparator<Range> rangeComparator = new RangeComparator();
169 
189  public static String joinStrings(List<Object> listOfStrings, String separator) {
190  // We would use String.join, but that is Java 8
191  if (DEBUG) {
192  Log.i(LOG_TAG_JOIN_STRINGS, "calling joinStrings");
193  }
194  return join(listOfStrings, separator);
195  }
196 
197  private static String join(List<Object> list, String separator)
198  {
199  StringBuilder sb = new StringBuilder();
200  boolean first = true;
201  for (Object item : list)
202  {
203  if (first)
204  first = false;
205  else
206  sb.append(separator);
207  sb.append(item.toString());
208  }
209  return sb.toString();
210  }
211 
222  public static String replaceAllMappingsDictionaryOrder(String text, Map<Object, Object> mappings) {
223  return replaceAllMappings(text, mappings, mappingOrderDictionary);
224  }
225 
236  public static String replaceAllMappingsLongestStringOrder(String text, Map<Object, Object> mappings) {
237  return replaceAllMappings(text, mappings, mappingOrderLongestStringFirst);
238  }
239 
250  public static String replaceAllMappingsEarliestOccurrenceOrder(String text, Map<Object, Object> mappings) {
251  return replaceAllMappings(text, mappings, mappingOrderEarliestOccurrence);
252  }
253 
265  public static String replaceAllMappings(String text, Map<Object, Object> mappings, MappingOrder order) {
266  // Iterate over all the mappings
267  Iterator<Map.Entry<Object, Object>> it = mappings.entrySet().iterator();
268 
269  // Construct a new map for <String, String> mappings in order to support
270  // look-ups for non-pure String values (e.g. numbers)
271  Map<String, String> stringMappings = new HashMap<>();
272 
273  // Construct a new List to store the Map's keys
274  List<String> keys = new ArrayList<>();
275 
276  while (it.hasNext()) {
277  Map.Entry<Object, Object> current = it.next();
278 
279  // Get Key & Value as strings
280  // This is needed to convert any non-String values to a String,
281  // e.g. numbers to String literals
282  String key = current.getKey().toString();
283  String value = current.getValue().toString();
284 
285  // Add key only if it was not added before (to reduce potential
286  // redundancy on potentially duplicate keys)
287  if (!stringMappings.containsKey(key)) {
288  keys.add(key);
289  }
290 
291  // Update map
292  stringMappings.put(key, value);
293  }
294 
295  // Change the order of the keys based on the given Order object
296  // TODO: If we have stream support, we can sort the stringMappings map
297  // TODO: directly provided we initialize it to a LinkedHashMap.
298  // TODO: It could save some memory in the long run.
299  order.changeOrder(keys, text);
300 
301  // Apply mappings owith re-ordered keys and mappings
302  return applyMappings(text, stringMappings, keys);
303  }
304 
330  private static String applyMappings(String text, Map<String, String> mappings, List<String> keys) {
331  // Create a set of ranges to keep track of which index ranges in the
332  // original text string are already set for replacement, together with the
333  // text to replace with. We sort this TreeSet in descending order to preserve
334  // indices of all ranges after replacement.
335  TreeSet<Range> ranges = new TreeSet<Range>(rangeComparator);
336 
337  // Range construction step: Iterate through all the keys,
338  // and fill in ranges set & replacements map.
339  for (String key : keys) {
340  // Convert key to pattern, and create a matcher to find all
341  // occurrences of the current string.
342  Pattern keyPattern = Pattern.compile(Pattern.quote(key));
343  Matcher matcher = keyPattern.matcher(text);
344 
345  // Keep track of the String to replace key with
346  String replacement = mappings.get(key);
347 
348  // Iterate until the key can no longer be found in text.
349  while (matcher.find()) {
350  // Get start & end indices of the string to be replaced.
351  int startId = matcher.start();
352  int endId = matcher.end();
353 
354  // Create a closed open range (closed since startId is inclusive,
355  // and open because endId is exclusive), and add the range
356  // to our TreeSet of ranges. If the range is already covered (i.e.
357  // there exists an overlapping range), it is simply not added.
358  Range range = new Range(startId, endId, replacement);
359  ranges.add(range);
360  }
361  }
362 
363  // Go through each entry that we want to replace. Since we used
364  // a TreeSet, we have an order that will not break things;
365  // We first replace the substring with the largest end index, which,
366  // because of overlap, will not affect the previous range indices
367  // because no ranges overlap in our range set.
368  // If we did not have this order, then we would have to update all indices
369  // of all ranges upon replacement.
370  for (Range range : ranges) {
371  // Combine strings: L + M + R, where:
372  // L - substring from start of string until endpoint
373  // M - middle string (the one that we use as replacement)
374  // R - remainder of the string after replacement
375  String left = text.substring(0, range.start);
376  String middle = range.text;
377  String end = text.substring(range.end);
378  text = left + middle + end;
379  }
380 
381  return text;
382  }
383 }
com.google.appinventor.components.runtime.util.JavaStringUtils
Definition: JavaStringUtils.java:27
com.google.appinventor.components.runtime.util.JavaStringUtils.replaceAllMappingsLongestStringOrder
static String replaceAllMappingsLongestStringOrder(String text, Map< Object, Object > mappings)
Definition: JavaStringUtils.java:236
com.google.appinventor.components.runtime.util.JavaStringUtils.replaceAllMappings
static String replaceAllMappings(String text, Map< Object, Object > mappings, MappingOrder order)
Definition: JavaStringUtils.java:265
com.google.appinventor.components.runtime.util.JavaStringUtils.LOG_TAG_JOIN_STRINGS
static final String LOG_TAG_JOIN_STRINGS
Definition: JavaStringUtils.java:158
com.google.appinventor.components.runtime.util.JavaStringUtils.replaceAllMappingsEarliestOccurrenceOrder
static String replaceAllMappingsEarliestOccurrenceOrder(String text, Map< Object, Object > mappings)
Definition: JavaStringUtils.java:250
com.google.appinventor.components.runtime.util.JavaStringUtils.joinStrings
static String joinStrings(List< Object > listOfStrings, String separator)
Definition: JavaStringUtils.java:189
com.google.appinventor.components.runtime.Map< String, Integer >
com.google.appinventor.components.runtime.MapFeatureContainerBase.iterator
Iterator< MapFeature > iterator()
Definition: MapFeatureContainerBase.java:347
com.google.appinventor.components.runtime.util.JavaStringUtils.replaceAllMappingsDictionaryOrder
static String replaceAllMappingsDictionaryOrder(String text, Map< Object, Object > mappings)
Definition: JavaStringUtils.java:222