fffce206875cf663480d2041aac121f88b58d01a
2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.util
.List
;
32 import java
.util
.ArrayList
;
35 * StringUtils contains methods to:
37 * - Convert one or more long lines of strings into justified text
40 * - Unescape C0 control codes.
42 * - Read/write a line of RFC4180 comma-separated values strings to/from a
45 public class StringUtils
{
48 * Left-justify a string into a list of lines.
50 * @param str the string
51 * @param n the maximum number of characters in a line
52 * @return the list of lines
54 public static List
<String
> left(final String str
, final int n
) {
55 List
<String
> result
= new ArrayList
<String
>();
60 * 1. Split on '\n' into paragraphs.
62 * 2. Scan each line, noting the position of the last
63 * beginning-of-a-word.
65 * 3. Chop at the last #2 if the next beginning-of-a-word exceeds
68 * 4. Return the lines.
71 String
[] rawLines
= str
.split("\n");
72 for (int i
= 0; i
< rawLines
.length
; i
++) {
73 StringBuilder line
= new StringBuilder();
74 StringBuilder word
= new StringBuilder();
75 boolean inWord
= false;
76 for (int j
= 0; j
< rawLines
[i
].length(); j
++) {
77 char ch
= rawLines
[i
].charAt(j
);
78 if ((ch
== ' ') || (ch
== '\t')) {
80 // We have just transitioned from a word to
81 // whitespace. See if we have enough space to add
82 // the word to the line.
83 if (width(word
.toString()) + width(line
.toString()) > n
) {
84 // This word will exceed the line length. Wrap
86 result
.add(line
.toString());
87 line
= new StringBuilder();
89 if ((word
.toString().startsWith(" "))
90 && (width(line
.toString()) == 0)
92 line
.append(word
.substring(1));
96 word
= new StringBuilder();
100 // We are in the whitespace before another word. Do
104 if (inWord
== true) {
105 // We are appending to a word.
108 // We have transitioned from whitespace to a word.
113 } // for (int j = 0; j < rawLines[i].length(); j++)
115 if (width(word
.toString()) + width(line
.toString()) > n
) {
116 // This word will exceed the line length. Wrap at it
118 result
.add(line
.toString());
119 line
= new StringBuilder();
121 if ((word
.toString().startsWith(" "))
122 && (width(line
.toString()) == 0)
124 line
.append(word
.substring(1));
128 result
.add(line
.toString());
129 } // for (int i = 0; i < rawLines.length; i++) {
135 * Right-justify a string into a list of lines.
137 * @param str the string
138 * @param n the maximum number of characters in a line
139 * @return the list of lines
141 public static List
<String
> right(final String str
, final int n
) {
142 List
<String
> result
= new ArrayList
<String
>();
145 * Same as left(), but preceed each line with spaces to make it n
148 List
<String
> lines
= left(str
, n
);
149 for (String line
: lines
) {
150 StringBuilder sb
= new StringBuilder();
151 for (int i
= 0; i
< n
- width(line
); i
++) {
155 result
.add(sb
.toString());
162 * Center a string into a list of lines.
164 * @param str the string
165 * @param n the maximum number of characters in a line
166 * @return the list of lines
168 public static List
<String
> center(final String str
, final int n
) {
169 List
<String
> result
= new ArrayList
<String
>();
172 * Same as left(), but preceed/succeed each line with spaces to make
175 List
<String
> lines
= left(str
, n
);
176 for (String line
: lines
) {
177 StringBuilder sb
= new StringBuilder();
178 int l
= (n
- width(line
)) / 2;
179 int r
= n
- width(line
) - l
;
180 for (int i
= 0; i
< l
; i
++) {
184 for (int i
= 0; i
< r
; i
++) {
187 result
.add(sb
.toString());
194 * Fully-justify a string into a list of lines.
196 * @param str the string
197 * @param n the maximum number of characters in a line
198 * @return the list of lines
200 public static List
<String
> full(final String str
, final int n
) {
201 List
<String
> result
= new ArrayList
<String
>();
204 * Same as left(), but insert spaces between words to make each line
205 * n chars long. The "algorithm" here is pretty dumb: it performs a
206 * split on space and then re-inserts multiples of n between words.
208 List
<String
> lines
= left(str
, n
);
209 for (int lineI
= 0; lineI
< lines
.size() - 1; lineI
++) {
210 String line
= lines
.get(lineI
);
211 String
[] words
= line
.split(" ");
212 if (words
.length
> 1) {
214 for (int i
= 0; i
< words
.length
; i
++) {
215 charCount
+= words
[i
].length();
217 int spaceCount
= n
- charCount
;
218 int q
= spaceCount
/ (words
.length
- 1);
219 int r
= spaceCount
% (words
.length
- 1);
220 StringBuilder sb
= new StringBuilder();
221 for (int i
= 0; i
< words
.length
- 1; i
++) {
223 for (int j
= 0; j
< q
; j
++) {
231 for (int j
= 0; j
< r
; j
++) {
234 sb
.append(words
[words
.length
- 1]);
235 result
.add(sb
.toString());
240 if (lines
.size() > 0) {
241 result
.add(lines
.get(lines
.size() - 1));
248 * Convert raw strings into escaped strings that be splatted on the
251 * @param str the string
252 * @return a string that can be passed into Screen.putStringXY()
254 public static String
unescape(final String str
) {
255 StringBuilder sb
= new StringBuilder();
256 for (int i
= 0; i
< str
.length(); i
++) {
257 char ch
= str
.charAt(i
);
258 if ((ch
< 0x20) || (ch
== 0x7F)) {
285 return sb
.toString();
289 * Read a line of RFC4180 comma-separated values (CSV) into a list of
292 * @param line the CSV line, with or without without line terminators
293 * @return the list of strings
295 public static List
<String
> fromCsv(final String line
) {
296 List
<String
> result
= new ArrayList
<String
>();
298 StringBuilder str
= new StringBuilder();
299 boolean quoted
= false;
300 boolean fieldQuoted
= false;
302 for (int i
= 0; i
< line
.length(); i
++) {
303 char ch
= line
.charAt(i
);
306 System.err.println("ch '" + ch + "' str '" + str + "' " +
307 " fieldQuoted " + fieldQuoted + " quoted " + quoted);
311 if (fieldQuoted
&& quoted
) {
312 // Terminating a quoted field.
313 result
.add(str
.toString());
314 str
= new StringBuilder();
317 } else if (fieldQuoted
) {
318 // Still waiting to see the terminating quote for this
322 // An unmatched double-quote and comma. This should be
323 // an invalid sequence. We will treat it as a quote
324 // terminating the field.
326 result
.add(str
.toString());
327 str
= new StringBuilder();
331 // A field separator.
332 result
.add(str
.toString());
333 str
= new StringBuilder();
341 if ((str
.length() == 0) && (!fieldQuoted
)) {
342 // The opening quote to a quoted field.
345 // This is a double-quote.
349 // This is the beginning of a quote.
355 // Normal character, pass it on.
359 // Include the final field.
360 result
.add(str
.toString());
366 * Write a list of strings to on line of RFC4180 comma-separated values
369 * @param list the list of strings
370 * @return the CSV line, without any line terminators
372 public static String
toCsv(final List
<String
> list
) {
373 StringBuilder result
= new StringBuilder();
375 for (String str
: list
) {
377 if (!str
.contains("\"") && !str
.contains(",")) {
378 // Just append the string with a comma.
380 } else if (!str
.contains("\"") && str
.contains(",")) {
381 // Contains commas, but no quotes. Just double-quote it.
385 } else if (str
.contains("\"")) {
386 // Contains quotes and maybe commas. Double-quote it and
387 // replace quotes inside.
389 for (int j
= 0; j
< str
.length(); j
++) {
390 char ch
= str
.charAt(j
);
399 if (i
< list
.size() - 1) {
404 return result
.toString();
408 * Determine display width of a Unicode code point.
410 * @param ch the code point, can be char
411 * @return the number of text cell columns required to display this code
412 * point, one of 0, 1, or 2
414 public static int width(final int ch
) {
416 * This routine is a modified version of mk_wcwidth() available
417 * at: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
419 * The combining characters list has been omitted from this
420 * implementation. Hopefully no users will be impacted.
423 // 8-bit control characters: width 0
427 if ((ch
< 32) || ((ch
>= 0x7f) && (ch
< 0xa0))) {
431 // All others: either 1 or 2
434 // Hangul Jamo init. consonants
438 || ((ch
>= 0x2e80) && (ch
<= 0xa4cf) && (ch
!= 0x303f))
440 || ((ch
>= 0xac00) && (ch
<= 0xd7a3))
441 // CJK Compatibility Ideographs
442 || ((ch
>= 0xf900) && (ch
<= 0xfaff))
444 || ((ch
>= 0xfe10) && (ch
<= 0xfe19))
445 // CJK Compatibility Forms
446 || ((ch
>= 0xfe30) && (ch
<= 0xfe6f))
448 || ((ch
>= 0xff00) && (ch
<= 0xff60))
449 || ((ch
>= 0xffe0) && (ch
<= 0xffe6))
450 || ((ch
>= 0x20000) && (ch
<= 0x2fffd))
451 || ((ch
>= 0x30000) && (ch
<= 0x3fffd))
453 || ((ch
>= 0x1f004) && (ch
<= 0x1fffd))
462 * Determine display width of a string. This ASSUMES that no characters
463 * are combining. Hopefully no users will be impacted.
465 * @param str the string
466 * @return the number of text cell columns required to display this string
468 public static int width(final String str
) {
470 for (int i
= 0; i
< str
.length();) {
471 int ch
= str
.codePointAt(i
);
473 i
+= Character
.charCount(ch
);
479 * Check if character is in the CJK range.
481 * @param ch character to check
482 * @return true if this character is in the CJK range
484 public static boolean isCjk(final int ch
) {
485 return ((ch
>= 0x2e80) && (ch
<= 0x9fff));
489 * Check if character is in the emoji range.
491 * @param ch character to check
492 * @return true if this character is in the emoji range
494 public static boolean isEmoji(final int ch
) {
495 return ((ch
>= 0x1f004) && (ch
<= 0x1fffd));