View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.artifact.versioning;
20  
21  import java.math.BigInteger;
22  import java.util.ArrayDeque;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Deque;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Objects;
30  import java.util.Properties;
31  
32  /**
33   * <p>
34   * Generic implementation of version comparison.
35   * </p>
36   * <p>
37   * Features:
38   * <ul>
39   * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li>
40   * <li>transition between characters and digits also constitutes a separator:
41   *     <code>1.0alpha1 =&gt; [1, [alpha, 1]]</code></li>
42   * <li>unlimited number of version components,</li>
43   * <li>version components in the text can be digits or strings,</li>
44   * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
45   *     Well-known qualifiers (case insensitive) are:<ul>
46   *     <li><code>alpha</code> or <code>a</code></li>
47   *     <li><code>beta</code> or <code>b</code></li>
48   *     <li><code>milestone</code> or <code>m</code></li>
49   *     <li><code>rc</code> or <code>cr</code></li>
50   *     <li><code>snapshot</code></li>
51   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
52   *     <li><code>sp</code></li>
53   *     </ul>
54   *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
55   *   </li>
56   * <li>a hyphen usually precedes a qualifier, and is always less important than digits/number, for example
57   *   {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over {@code 1.0.0.RC1}, and more
58   *   generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1}
59   *   over {@code 1.0.0.X1}.</li>
60   * </ul>
61   *
62   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
63   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
64   * @see <a href="https://maven.apache.org/pom.html#version-order-specification">"Versioning" in the POM reference</a>
65   */
66  public class ComparableVersion implements Comparable<ComparableVersion> {
67      private static final int MAX_INTITEM_LENGTH = 9;
68  
69      private static final int MAX_LONGITEM_LENGTH = 18;
70  
71      private String value;
72  
73      private String canonical;
74  
75      private ListItem items;
76  
77      private interface Item {
78          int INT_ITEM = 3;
79          int LONG_ITEM = 4;
80          int BIGINTEGER_ITEM = 0;
81          int STRING_ITEM = 1;
82          int LIST_ITEM = 2;
83          int COMBINATION_ITEM = 5;
84  
85          int compareTo(Item item);
86  
87          int getType();
88  
89          boolean isNull();
90      }
91  
92      /**
93       * Represents a numeric item in the version item list that can be represented with an int.
94       */
95      private static class IntItem implements Item {
96          private final int value;
97  
98          public static final IntItem ZERO = new IntItem();
99  
100         private IntItem() {
101             this.value = 0;
102         }
103 
104         IntItem(String str) {
105             this.value = Integer.parseInt(str);
106         }
107 
108         @Override
109         public int getType() {
110             return INT_ITEM;
111         }
112 
113         @Override
114         public boolean isNull() {
115             return value == 0;
116         }
117 
118         @Override
119         public int compareTo(Item item) {
120             if (item == null) {
121                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
122             }
123 
124             switch (item.getType()) {
125                 case INT_ITEM:
126                     int itemValue = ((IntItem) item).value;
127                     return Integer.compare(value, itemValue);
128                 case LONG_ITEM:
129                 case BIGINTEGER_ITEM:
130                     return -1;
131 
132                 case STRING_ITEM:
133                     return 1;
134                 case COMBINATION_ITEM:
135                     return 1; // 1.1 > 1-sp
136 
137                 case LIST_ITEM:
138                     return 1; // 1.1 > 1-1
139 
140                 default:
141                     throw new IllegalStateException("invalid item: " + item.getClass());
142             }
143         }
144 
145         @Override
146         public boolean equals(Object o) {
147             if (this == o) {
148                 return true;
149             }
150             if (o == null || getClass() != o.getClass()) {
151                 return false;
152             }
153 
154             IntItem intItem = (IntItem) o;
155 
156             return value == intItem.value;
157         }
158 
159         @Override
160         public int hashCode() {
161             return value;
162         }
163 
164         @Override
165         public String toString() {
166             return Integer.toString(value);
167         }
168     }
169 
170     /**
171      * Represents a numeric item in the version item list that can be represented with a long.
172      */
173     private static class LongItem implements Item {
174         private final long value;
175 
176         LongItem(String str) {
177             this.value = Long.parseLong(str);
178         }
179 
180         @Override
181         public int getType() {
182             return LONG_ITEM;
183         }
184 
185         @Override
186         public boolean isNull() {
187             return value == 0;
188         }
189 
190         @Override
191         public int compareTo(Item item) {
192             if (item == null) {
193                 return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
194             }
195 
196             switch (item.getType()) {
197                 case INT_ITEM:
198                     return 1;
199                 case LONG_ITEM:
200                     long itemValue = ((LongItem) item).value;
201                     return Long.compare(value, itemValue);
202                 case BIGINTEGER_ITEM:
203                     return -1;
204 
205                 case STRING_ITEM:
206                     return 1;
207                 case COMBINATION_ITEM:
208                     return 1; // 1.1 > 1-sp
209 
210                 case LIST_ITEM:
211                     return 1; // 1.1 > 1-1
212 
213                 default:
214                     throw new IllegalStateException("invalid item: " + item.getClass());
215             }
216         }
217 
218         @Override
219         public boolean equals(Object o) {
220             if (this == o) {
221                 return true;
222             }
223             if (o == null || getClass() != o.getClass()) {
224                 return false;
225             }
226 
227             LongItem longItem = (LongItem) o;
228 
229             return value == longItem.value;
230         }
231 
232         @Override
233         public int hashCode() {
234             return (int) (value ^ (value >>> 32));
235         }
236 
237         @Override
238         public String toString() {
239             return Long.toString(value);
240         }
241     }
242 
243     /**
244      * Represents a numeric item in the version item list.
245      */
246     private static class BigIntegerItem implements Item {
247         private final BigInteger value;
248 
249         BigIntegerItem(String str) {
250             this.value = new BigInteger(str);
251         }
252 
253         @Override
254         public int getType() {
255             return BIGINTEGER_ITEM;
256         }
257 
258         @Override
259         public boolean isNull() {
260             return BigInteger.ZERO.equals(value);
261         }
262 
263         @Override
264         public int compareTo(Item item) {
265             if (item == null) {
266                 return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
267             }
268 
269             switch (item.getType()) {
270                 case INT_ITEM:
271                 case LONG_ITEM:
272                     return 1;
273 
274                 case BIGINTEGER_ITEM:
275                     return value.compareTo(((BigIntegerItem) item).value);
276 
277                 case STRING_ITEM:
278                     return 1;
279                 case COMBINATION_ITEM:
280                     return 1; // 1.1 > 1-sp
281 
282                 case LIST_ITEM:
283                     return 1; // 1.1 > 1-1
284 
285                 default:
286                     throw new IllegalStateException("invalid item: " + item.getClass());
287             }
288         }
289 
290         @Override
291         public boolean equals(Object o) {
292             if (this == o) {
293                 return true;
294             }
295             if (o == null || getClass() != o.getClass()) {
296                 return false;
297             }
298 
299             BigIntegerItem that = (BigIntegerItem) o;
300 
301             return value.equals(that.value);
302         }
303 
304         @Override
305         public int hashCode() {
306             return value.hashCode();
307         }
308 
309         public String toString() {
310             return value.toString();
311         }
312     }
313 
314     /**
315      * Represents a string in the version item list, usually a qualifier.
316      */
317     private static class StringItem implements Item {
318         private static final List<String> QUALIFIERS =
319                 Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
320         private static final List<String> RELEASE_QUALIFIERS = Arrays.asList("ga", "final", "release");
321 
322         private static final Properties ALIASES = new Properties();
323 
324         static {
325             ALIASES.put("cr", "rc");
326         }
327 
328         /**
329          * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
330          * the version older than one without a qualifier, or more recent.
331          */
332         private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
333 
334         private final String value;
335 
336         StringItem(String value, boolean followedByDigit) {
337             if (followedByDigit && value.length() == 1) {
338                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
339                 switch (value.charAt(0)) {
340                     case 'a':
341                         value = "alpha";
342                         break;
343                     case 'b':
344                         value = "beta";
345                         break;
346                     case 'm':
347                         value = "milestone";
348                         break;
349                     default:
350                 }
351             }
352             this.value = ALIASES.getProperty(value, value);
353         }
354 
355         @Override
356         public int getType() {
357             return STRING_ITEM;
358         }
359 
360         @Override
361         public boolean isNull() {
362             return value == null || value.isEmpty();
363         }
364 
365         /**
366          * Returns a comparable value for a qualifier.
367          * <p>
368          * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
369          * ordering.
370          * <p>
371          *
372          * @param qualifier
373          * @return an equivalent value that can be used with lexical comparison
374          */
375         public static String comparableQualifier(String qualifier) {
376             if (RELEASE_QUALIFIERS.contains(qualifier)) {
377                 return String.valueOf(QUALIFIERS.indexOf(""));
378             }
379 
380             int i = QUALIFIERS.indexOf(qualifier);
381 
382             // Just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for
383             // -1
384             //  or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first
385             // character,
386             // so this is still fast. If more characters are needed then it requires a lexical sort anyway.
387             return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
388         }
389 
390         @Override
391         public int compareTo(Item item) {
392             if (item == null) {
393                 // 1-rc < 1, 1-ga > 1
394                 return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
395             }
396             switch (item.getType()) {
397                 case INT_ITEM:
398                 case LONG_ITEM:
399                 case BIGINTEGER_ITEM:
400                     return -1; // 1.any < 1.1 ?
401 
402                 case STRING_ITEM:
403                     return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
404 
405                 case COMBINATION_ITEM:
406                     int result = this.compareTo(((CombinationItem) item).getStringPart());
407                     if (result == 0) {
408                         return -1;
409                     }
410                     return result;
411 
412                 case LIST_ITEM:
413                     return -1; // 1.any < 1-1
414 
415                 default:
416                     throw new IllegalStateException("invalid item: " + item.getClass());
417             }
418         }
419 
420         @Override
421         public boolean equals(Object o) {
422             if (this == o) {
423                 return true;
424             }
425             if (o == null || getClass() != o.getClass()) {
426                 return false;
427             }
428 
429             StringItem that = (StringItem) o;
430 
431             return value.equals(that.value);
432         }
433 
434         @Override
435         public int hashCode() {
436             return value.hashCode();
437         }
438 
439         public String toString() {
440             return value;
441         }
442     }
443 
444     /**
445      * Represents a combination in the version item list.
446      * It is usually a combination of a string and a number, with the string first and the number second.
447      */
448     private static class CombinationItem implements Item {
449 
450         StringItem stringPart;
451 
452         Item digitPart;
453 
454         CombinationItem(String value) {
455             int index = 0;
456             for (int i = 0; i < value.length(); i++) {
457                 char c = value.charAt(i);
458                 if (Character.isDigit(c)) {
459                     index = i;
460                     break;
461                 }
462             }
463 
464             stringPart = new StringItem(value.substring(0, index), true);
465             digitPart = parseItem(true, value.substring(index));
466         }
467 
468         @Override
469         public int compareTo(Item item) {
470             if (item == null) {
471                 // 1-rc1 < 1, 1-ga1 > 1
472                 return stringPart.compareTo(item);
473             }
474             int result = 0;
475             switch (item.getType()) {
476                 case INT_ITEM:
477                 case LONG_ITEM:
478                 case BIGINTEGER_ITEM:
479                     return -1;
480 
481                 case STRING_ITEM:
482                     result = stringPart.compareTo(item);
483                     if (result == 0) {
484                         // X1 > X
485                         return 1;
486                     }
487                     return result;
488 
489                 case LIST_ITEM:
490                     return -1;
491 
492                 case COMBINATION_ITEM:
493                     result = stringPart.compareTo(((CombinationItem) item).getStringPart());
494                     if (result == 0) {
495                         return digitPart.compareTo(((CombinationItem) item).getDigitPart());
496                     }
497                     return result;
498                 default:
499                     return 0;
500             }
501         }
502 
503         public StringItem getStringPart() {
504             return stringPart;
505         }
506 
507         public Item getDigitPart() {
508             return digitPart;
509         }
510 
511         @Override
512         public int getType() {
513             return COMBINATION_ITEM;
514         }
515 
516         @Override
517         public boolean isNull() {
518             return false;
519         }
520 
521         @Override
522         public boolean equals(Object o) {
523             if (this == o) {
524                 return true;
525             }
526             if (o == null || getClass() != o.getClass()) {
527                 return false;
528             }
529             CombinationItem that = (CombinationItem) o;
530             return Objects.equals(stringPart, that.stringPart) && Objects.equals(digitPart, that.digitPart);
531         }
532 
533         @Override
534         public int hashCode() {
535             return Objects.hash(stringPart, digitPart);
536         }
537 
538         @Override
539         public String toString() {
540             return stringPart.toString() + digitPart.toString();
541         }
542     }
543 
544     /**
545      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
546      * with '-(number)' in the version specification).
547      */
548     private static class ListItem extends ArrayList<Item> implements Item {
549         @Override
550         public int getType() {
551             return LIST_ITEM;
552         }
553 
554         @Override
555         public boolean isNull() {
556             return (size() == 0);
557         }
558 
559         void normalize() {
560             for (int i = size() - 1; i >= 0; i--) {
561                 Item lastItem = get(i);
562 
563                 if (lastItem.isNull()) {
564                     if (i == size() - 1 || get(i + 1).getType() == STRING_ITEM) {
565                         remove(i);
566                     } else if (get(i + 1).getType() == LIST_ITEM) {
567                         Item item = ((ListItem) get(i + 1)).get(0);
568                         if (item.getType() == COMBINATION_ITEM || item.getType() == STRING_ITEM) {
569                             remove(i);
570                         }
571                     }
572                 }
573             }
574         }
575 
576         @Override
577         public int compareTo(Item item) {
578             if (item == null) {
579                 if (size() == 0) {
580                     return 0; // 1-0 = 1- (normalize) = 1
581                 }
582                 // Compare the entire list of items with null - not just the first one, MNG-6964
583                 for (Item i : this) {
584                     int result = i.compareTo(null);
585                     if (result != 0) {
586                         return result;
587                     }
588                 }
589                 return 0;
590             }
591             switch (item.getType()) {
592                 case INT_ITEM:
593                 case LONG_ITEM:
594                 case BIGINTEGER_ITEM:
595                     return -1; // 1-1 < 1.0.x
596 
597                 case STRING_ITEM:
598                     return 1;
599                 case COMBINATION_ITEM:
600                     return 1; // 1-1 > 1-sp
601 
602                 case LIST_ITEM:
603                     Iterator<Item> left = iterator();
604                     Iterator<Item> right = ((ListItem) item).iterator();
605 
606                     while (left.hasNext() || right.hasNext()) {
607                         Item l = left.hasNext() ? left.next() : null;
608                         Item r = right.hasNext() ? right.next() : null;
609 
610                         // if this is shorter, then invert the compare and mul with -1
611                         int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
612 
613                         if (result != 0) {
614                             return result;
615                         }
616                     }
617 
618                     return 0;
619 
620                 default:
621                     throw new IllegalStateException("invalid item: " + item.getClass());
622             }
623         }
624 
625         @Override
626         public String toString() {
627             StringBuilder buffer = new StringBuilder();
628             for (Item item : this) {
629                 if (buffer.length() > 0) {
630                     buffer.append((item instanceof ListItem) ? '-' : '.');
631                 }
632                 buffer.append(item);
633             }
634             return buffer.toString();
635         }
636 
637         /**
638          * Return the contents in the same format that is used when you call toString() on a List.
639          */
640         private String toListString() {
641             StringBuilder buffer = new StringBuilder();
642             buffer.append("[");
643             for (Item item : this) {
644                 if (buffer.length() > 1) {
645                     buffer.append(", ");
646                 }
647                 if (item instanceof ListItem) {
648                     buffer.append(((ListItem) item).toListString());
649                 } else {
650                     buffer.append(item);
651                 }
652             }
653             buffer.append("]");
654             return buffer.toString();
655         }
656     }
657 
658     public ComparableVersion(String version) {
659         parseVersion(version);
660     }
661 
662     @SuppressWarnings("checkstyle:innerassignment")
663     public final void parseVersion(String version) {
664         this.value = version;
665 
666         items = new ListItem();
667 
668         version = version.toLowerCase(Locale.ENGLISH);
669 
670         ListItem list = items;
671 
672         Deque<Item> stack = new ArrayDeque<>();
673         stack.push(list);
674 
675         boolean isDigit = false;
676 
677         boolean isCombination = false;
678 
679         int startIndex = 0;
680 
681         for (int i = 0; i < version.length(); i++) {
682             char c = version.charAt(i);
683 
684             if (c == '.') {
685                 if (i == startIndex) {
686                     list.add(IntItem.ZERO);
687                 } else {
688                     list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
689                 }
690                 isCombination = false;
691                 startIndex = i + 1;
692             } else if (c == '-') {
693                 if (i == startIndex) {
694                     list.add(IntItem.ZERO);
695                 } else {
696                     // X-1 is going to be treated as X1
697                     if (!isDigit && i != version.length() - 1) {
698                         char c1 = version.charAt(i + 1);
699                         if (Character.isDigit(c1)) {
700                             isCombination = true;
701                             continue;
702                         }
703                     }
704                     list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
705                 }
706                 startIndex = i + 1;
707 
708                 if (!list.isEmpty()) {
709                     list.add(list = new ListItem());
710                     stack.push(list);
711                 }
712                 isCombination = false;
713             } else if (Character.isDigit(c)) {
714                 if (!isDigit && i > startIndex) {
715                     // X1
716                     isCombination = true;
717 
718                     if (!list.isEmpty()) {
719                         list.add(list = new ListItem());
720                         stack.push(list);
721                     }
722                 }
723 
724                 isDigit = true;
725             } else {
726                 if (isDigit && i > startIndex) {
727                     list.add(parseItem(isCombination, true, version.substring(startIndex, i)));
728                     startIndex = i;
729 
730                     list.add(list = new ListItem());
731                     stack.push(list);
732                     isCombination = false;
733                 }
734 
735                 isDigit = false;
736             }
737         }
738 
739         if (version.length() > startIndex) {
740             // 1.0.0.X1 < 1.0.0-X2
741             // treat .X as -X for any string qualifier X
742             if (!isDigit && !list.isEmpty()) {
743                 list.add(list = new ListItem());
744                 stack.push(list);
745             }
746 
747             list.add(parseItem(isCombination, isDigit, version.substring(startIndex)));
748         }
749 
750         while (!stack.isEmpty()) {
751             list = (ListItem) stack.pop();
752             list.normalize();
753         }
754     }
755 
756     private static Item parseItem(boolean isDigit, String buf) {
757         return parseItem(false, isDigit, buf);
758     }
759 
760     private static Item parseItem(boolean isCombination, boolean isDigit, String buf) {
761         if (isCombination) {
762             return new CombinationItem(buf.replace("-", ""));
763         } else if (isDigit) {
764             buf = stripLeadingZeroes(buf);
765             if (buf.length() <= MAX_INTITEM_LENGTH) {
766                 // lower than 2^31
767                 return new IntItem(buf);
768             } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
769                 // lower than 2^63
770                 return new LongItem(buf);
771             }
772             return new BigIntegerItem(buf);
773         }
774         return new StringItem(buf, false);
775     }
776 
777     private static String stripLeadingZeroes(String buf) {
778         if (buf == null || buf.isEmpty()) {
779             return "0";
780         }
781         for (int i = 0; i < buf.length(); ++i) {
782             char c = buf.charAt(i);
783             if (c != '0') {
784                 return buf.substring(i);
785             }
786         }
787         return buf;
788     }
789 
790     @Override
791     public int compareTo(ComparableVersion o) {
792         return items.compareTo(o.items);
793     }
794 
795     @Override
796     public String toString() {
797         return value;
798     }
799 
800     public String getCanonical() {
801         if (canonical == null) {
802             canonical = items.toString();
803         }
804         return canonical;
805     }
806 
807     @Override
808     public boolean equals(Object o) {
809         return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items);
810     }
811 
812     @Override
813     public int hashCode() {
814         return items.hashCode();
815     }
816 
817     // CHECKSTYLE_OFF: LineLength
818 
819     /**
820      * Main to test version parsing and comparison.
821      * <p>
822      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
823      * <pre>java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"</pre>
824      * command to command line. Result of given command will be something like this:
825      * <pre>
826      * Display parameters as parsed by Maven (in canonical form) and comparison result:
827      * 1. 1.2.7 == 1.2.7
828      *    1.2.7 &gt; 1.2-SNAPSHOT
829      * 2. 1.2-SNAPSHOT == 1.2-snapshot
830      * </pre>
831      *
832      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
833      *             two adjacent will be compared.
834      */
835     // CHECKSTYLE_ON: LineLength
836     public static void main(String... args) {
837         System.out.println("Display parameters as parsed by Maven (in canonical form and as a list of tokens) and"
838                 + " comparison result:");
839         if (args.length == 0) {
840             return;
841         }
842 
843         ComparableVersion prev = null;
844         int i = 1;
845         for (String version : args) {
846             ComparableVersion c = new ComparableVersion(version);
847 
848             if (prev != null) {
849                 int compare = prev.compareTo(c);
850                 System.out.println("   " + prev.toString() + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">"))
851                         + ' ' + version);
852             }
853 
854             System.out.println(
855                     (i++) + ". " + version + " -> " + c.getCanonical() + "; tokens: " + c.items.toListString());
856 
857             prev = c;
858         }
859     }
860 }