Move justify List into StringUtils (tests needed)
[nikiroo-utils.git] / src / be / nikiroo / utils / StringUtils.java
index dc40e878d9c68a3b8b25af2ba03011ef39f57f7c..b8468a132430f624775249180cc424f2c462bec1 100644 (file)
@@ -1,25 +1,21 @@
 package be.nikiroo.utils;
 
-import java.awt.Image;
-import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
+import java.util.Map.Entry;
 import java.util.Scanner;
 import java.util.regex.Pattern;
-import java.util.zip.ZipInputStream;
-
-import javax.imageio.ImageIO;
 
 import org.unbescape.html.HtmlEscape;
 import org.unbescape.html.HtmlEscapeLevel;
@@ -33,19 +29,49 @@ import org.unbescape.html.HtmlEscapeType;
 public class StringUtils {
        /**
         * This enum type will decide the alignment of a {@link String} when padding
-        * is applied or if there is enough horizontal space for it to be aligned.
+        * or justification is applied (if there is enough horizontal space for it
+        * to be aligned).
         */
        public enum Alignment {
                /** Aligned at left. */
-               Beginning,
+               LEFT,
                /** Centered. */
-               Center,
+               CENTER,
                /** Aligned at right. */
-               End
+               RIGHT,
+               /** Full justified (to both left and right). */
+               JUSTIFY,
+
+               // Old Deprecated values:
+
+               /** DEPRECATED: please use LEFT. */
+               @Deprecated
+               Beginning,
+               /** DEPRECATED: please use CENTER. */
+               @Deprecated
+               Center,
+               /** DEPRECATED: please use RIGHT. */
+               @Deprecated
+               End;
+
+               /**
+                * Return the non-deprecated version of this enum if needed (or return
+                * self if not).
+                * 
+                * @return the non-deprecated value
+                */
+               Alignment undeprecate() {
+                       if (this == Beginning)
+                               return LEFT;
+                       if (this == Center)
+                               return CENTER;
+                       if (this == End)
+                               return RIGHT;
+                       return this;
+               }
        }
 
-       static private Pattern marks = Pattern
-                       .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
+       static private Pattern marks = getMarks();
 
        /**
         * Fix the size of the given {@link String} either with space-padding or by
@@ -59,7 +85,7 @@ public class StringUtils {
         * @return the resulting {@link String} of size <i>size</i>
         */
        static public String padString(String text, int width) {
-               return padString(text, width, true, Alignment.Beginning);
+               return padString(text, width, true, null);
        }
 
        /**
@@ -75,13 +101,19 @@ public class StringUtils {
         *            cut the {@link String} shorter if needed
         * @param align
         *            align the {@link String} in this position if we have enough
-        *            space
+        *            space (default is Alignment.Beginning)
         * 
         * @return the resulting {@link String} of size <i>size</i> minimum
         */
        static public String padString(String text, int width, boolean cut,
                        Alignment align) {
 
+               if (align == null) {
+                       align = Alignment.LEFT;
+               }
+
+               align = align.undeprecate();
+
                if (width >= 0) {
                        if (text == null)
                                text = "";
@@ -92,23 +124,23 @@ public class StringUtils {
                                if (cut)
                                        text = text.substring(0, width);
                        } else if (diff > 0) {
-                               if (diff < 2 && align != Alignment.End)
-                                       align = Alignment.Beginning;
+                               if (diff < 2 && align != Alignment.RIGHT)
+                                       align = Alignment.LEFT;
 
                                switch (align) {
-                               case Beginning:
-                                       text = text + new String(new char[diff]).replace('\0', ' ');
-                                       break;
-                               case End:
+                               case RIGHT:
                                        text = new String(new char[diff]).replace('\0', ' ') + text;
                                        break;
-                               case Center:
-                               default:
+                               case CENTER:
                                        int pad1 = (diff) / 2;
                                        int pad2 = (diff + 1) / 2;
                                        text = new String(new char[pad1]).replace('\0', ' ') + text
                                                        + new String(new char[pad2]).replace('\0', ' ');
                                        break;
+                               case LEFT:
+                               default:
+                                       text = text + new String(new char[diff]).replace('\0', ' ');
+                                       break;
                                }
                        }
                }
@@ -116,6 +148,176 @@ public class StringUtils {
                return text;
        }
 
+       /**
+        * Justify a text into width-sized (at the maximum) lines.
+        * 
+        * @param text
+        *            the {@link String} to justify
+        * @param width
+        *            the maximum size of the resulting lines
+        * 
+        * @return a list of justified text lines
+        */
+       static public List<String> justifyText(String text, int width) {
+               return justifyText(text, width, null);
+       }
+
+       /**
+        * Justify a text into width-sized (at the maximum) lines.
+        * 
+        * @param text
+        *            the {@link String} to justify
+        * @param width
+        *            the maximum size of the resulting lines
+        * @param align
+        *            align the lines in this position (default is
+        *            Alignment.Beginning)
+        * 
+        * @return a list of justified text lines
+        */
+       static public List<String> justifyText(String text, int width,
+                       Alignment align) {
+               if (align == null) {
+                       align = Alignment.LEFT;
+               }
+
+               align = align.undeprecate();
+
+               switch (align) {
+               case CENTER:
+                       return StringJustifier.center(text, width);
+               case RIGHT:
+                       return StringJustifier.right(text, width);
+               case JUSTIFY:
+                       return StringJustifier.full(text, width);
+               case LEFT:
+               default:
+                       return StringJustifier.left(text, width);
+               }
+       }
+
+       /**
+        * Justify a text into width-sized (at the maximum) lines.
+        * 
+        * @param text
+        *            the {@link String} to justify
+        * @param width
+        *            the maximum size of the resulting lines
+        * 
+        * @return a list of justified text lines
+        */
+       static public List<String> justifyText(List<String> text, int width) {
+               return justifyText(text, width, null);
+       }
+
+       /**
+        * Justify a text into width-sized (at the maximum) lines.
+        * 
+        * @param text
+        *            the {@link String} to justify
+        * @param width
+        *            the maximum size of the resulting lines
+        * @param align
+        *            align the lines in this position (default is
+        *            Alignment.Beginning)
+        * 
+        * @return a list of justified text lines
+        */
+       static public List<String> justifyText(List<String> text, int width,
+                       Alignment align) {
+               List<String> result = new ArrayList<String>();
+
+               // Content <-> Bullet spacing (null = no spacing)
+               List<Entry<String, String>> lines = new ArrayList<Entry<String, String>>();
+               StringBuilder previous = null;
+               StringBuilder tmp = new StringBuilder();
+               String previousItemBulletSpacing = null;
+               String itemBulletSpacing = null;
+               for (String inputLine : text) {
+                       boolean previousLineComplete = true;
+
+                       String current = inputLine.replace("\t", "    ");
+                       itemBulletSpacing = getItemSpacing(current);
+                       boolean bullet = isItemLine(current);
+                       if ((previousItemBulletSpacing == null || itemBulletSpacing
+                                       .length() <= previousItemBulletSpacing.length()) && !bullet) {
+                               itemBulletSpacing = null;
+                       }
+
+                       if (itemBulletSpacing != null) {
+                               current = current.trim();
+                               if (!current.isEmpty() && bullet) {
+                                       current = current.substring(1);
+                               }
+                               current = current.trim();
+                               previousLineComplete = bullet;
+                       } else {
+                               tmp.setLength(0);
+                               for (String word : current.split(" ")) {
+                                       if (word.isEmpty()) {
+                                               continue;
+                                       }
+
+                                       if (tmp.length() > 0) {
+                                               tmp.append(' ');
+                                       }
+                                       tmp.append(word.trim());
+                               }
+                               current = tmp.toString();
+
+                               previousLineComplete = current.isEmpty()
+                                               || previousItemBulletSpacing != null
+                                               || (previous != null && isFullLine(previous));
+                       }
+
+                       if (previous == null) {
+                               previous = new StringBuilder();
+                       } else {
+                               if (previousLineComplete) {
+                                       lines.add(new AbstractMap.SimpleEntry<String, String>(
+                                                       previous.toString(), previousItemBulletSpacing));
+                                       previous.setLength(0);
+                                       previousItemBulletSpacing = itemBulletSpacing;
+                               } else {
+                                       previous.append(' ');
+                               }
+                       }
+
+                       previous.append(current);
+
+               }
+
+               if (previous != null) {
+                       lines.add(new AbstractMap.SimpleEntry<String, String>(previous
+                                       .toString(), previousItemBulletSpacing));
+               }
+
+               for (Entry<String, String> line : lines) {
+                       String content = line.getKey();
+                       String spacing = line.getValue();
+
+                       String bullet = "- ";
+                       if (spacing == null) {
+                               bullet = "";
+                               spacing = "";
+                       }
+
+                       if (spacing.length() > width + 3) {
+                               spacing = "";
+                       }
+
+                       for (String subline : StringUtils.justifyText(content, width
+                                       - (spacing.length() + bullet.length()), align)) {
+                               result.add(spacing + bullet + subline);
+                               if (!bullet.isEmpty()) {
+                                       bullet = "  ";
+                               }
+                       }
+               }
+
+               return result;
+       }
+
        /**
         * Sanitise the given input to make it more Terminal-friendly by removing
         * combining characters.
@@ -150,7 +352,9 @@ public class StringUtils {
 
                if (removeAllAccents) {
                        input = Normalizer.normalize(input, Form.NFKD);
-                       input = marks.matcher(input).replaceAll("");
+                       if (marks != null) {
+                               input = marks.matcher(input).replaceAll("");
+                       }
                }
 
                input = Normalizer.normalize(input, Form.NFKC);
@@ -172,11 +376,15 @@ public class StringUtils {
        }
 
        /**
-        * Convert between time in milliseconds to {@link String} in a "static" way
-        * (to exchange data over the wire, for instance).
+        * Convert between the time in milliseconds to a {@link String} in a "fixed"
+        * way (to exchange data over the wire, for instance).
+        * <p>
+        * Precise to the second.
         * 
         * @param time
-        *            the time in milliseconds
+        *            the specified number of milliseconds since the standard base
+        *            time known as "the epoch", namely January 1, 1970, 00:00:00
+        *            GMT
         * 
         * @return the time as a {@link String}
         */
@@ -186,96 +394,24 @@ public class StringUtils {
        }
 
        /**
-        * Convert between time as a {@link String} to milliseconds in a "static"
+        * Convert between the time as a {@link String} to milliseconds in a "fixed"
         * way (to exchange data over the wire, for instance).
+        * <p>
+        * Precise to the second.
         * 
         * @param displayTime
         *            the time as a {@link String}
         * 
-        * @return the time in milliseconds
-        */
-       static public long toTime(String displayTime) {
-               SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-               try {
-                       return sdf.parse(displayTime).getTime();
-               } catch (ParseException e) {
-                       return -1;
-               }
-       }
-
-       /**
-        * Convert the given {@link Image} object into a Base64 representation of
-        * the same {@link Image}. object.
+        * @return the number of milliseconds since the standard base time known as
+        *         "the epoch", namely January 1, 1970, 00:00:00 GMT, or -1 in case
+        *         of error
         * 
-        * @param image
-        *            the {@link Image} object to convert
-        * 
-        * @return the Base64 representation
-        * 
-        * @throws IOException
-        *             in case of IO error
+        * @throws ParseException
+        *             in case of parse error
         */
-       static public String fromImage(BufferedImage image) throws IOException {
-               String imageString = null;
-               ByteArrayOutputStream out = new ByteArrayOutputStream();
-
-               ImageIO.write(image, "jpeg", out);
-               byte[] imageBytes = out.toByteArray();
-
-               imageString = new String(Base64.encodeBytes(imageBytes));
-
-               out.close();
-
-               return imageString;
-       }
-
-       /**
-        * Convert the given image into a Base64 representation of the same
-        * {@link File}.
-        * 
-        * @param in
-        *            the image to convert
-        * 
-        * @return the Base64 representation
-        * 
-        * @throws IOException
-        *             in case of IO error
-        */
-       static public String fromStream(InputStream in) throws IOException {
-               String fileString = null;
-               ByteArrayOutputStream out = new ByteArrayOutputStream();
-
-               byte[] buf = new byte[8192];
-
-               int c = 0;
-               while ((c = in.read(buf, 0, buf.length)) > 0) {
-                       out.write(buf, 0, c);
-               }
-               out.flush();
-               in.close();
-
-               fileString = new String(Base64.encodeBytes(out.toByteArray()));
-               out.close();
-
-               return fileString;
-       }
-
-       /**
-        * Convert the given Base64 representation of an image into an {@link Image}
-        * object.
-        * 
-        * @param b64data
-        *            the {@link Image} in Base64 format
-        * 
-        * @return the {@link Image} object
-        * 
-        * @throws IOException
-        *             in case of IO error
-        */
-       static public BufferedImage toImage(String b64data) throws IOException {
-               ByteArrayInputStream in = new ByteArrayInputStream(
-                               Base64.decode(b64data));
-               return IOUtils.toImage(in);
+       static public long toTime(String displayTime) throws ParseException {
+               SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+               return sdf.parse(displayTime).getTime();
        }
 
        /**
@@ -286,10 +422,10 @@ public class StringUtils {
         * 
         * @return the hash
         */
-       static public String getHash(String input) {
+       static public String getMd5Hash(String input) {
                try {
                        MessageDigest md = MessageDigest.getInstance("MD5");
-                       md.update(input.getBytes());
+                       md.update(input.getBytes("UTF-8"));
                        byte byteData[] = md.digest();
 
                        StringBuffer hexString = new StringBuffer();
@@ -303,6 +439,8 @@ public class StringUtils {
                        return hexString.toString();
                } catch (NoSuchAlgorithmException e) {
                        return input;
+               } catch (UnsupportedEncodingException e) {
+                       return input;
                }
        }
 
@@ -370,6 +508,14 @@ public class StringUtils {
                                HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
        }
 
+       /**
+        * Zip the data and then encode it into Base64.
+        * 
+        * @param data
+        *            the data
+        * 
+        * @return the Base64 zipped version
+        */
        public static String zip64(String data) {
                try {
                        return Base64.encodeBytes(data.getBytes(), Base64.GZIP);
@@ -379,6 +525,17 @@ public class StringUtils {
                }
        }
 
+       /**
+        * Unconvert from Base64 then unzip the content.
+        * 
+        * @param data
+        *            the data in Base64 format
+        * 
+        * @return the raw data
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
        public static String unzip64(String data) throws IOException {
                ByteArrayInputStream in = new ByteArrayInputStream(Base64.decode(data,
                                Base64.GZIP));
@@ -391,4 +548,44 @@ public class StringUtils {
                        scan.close();
                }
        }
+
+       /**
+        * The "remove accents" pattern.
+        * 
+        * @return the pattern, or NULL if a problem happens
+        */
+       private static Pattern getMarks() {
+               try {
+                       return Pattern
+                                       .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
+               } catch (Exception e) {
+                       // Can fail on Android...
+                       return null;
+               }
+       }
+
+       // justify List<String> related:
+
+       static private boolean isFullLine(StringBuilder line) {
+               return line.length() == 0 //
+                               || line.charAt(line.length() - 1) == '.'
+                               || line.charAt(line.length() - 1) == '"'
+                               || line.charAt(line.length() - 1) == 'ยป';
+       }
+
+       static private boolean isItemLine(String line) {
+               String spacing = getItemSpacing(line);
+               return spacing != null && line.charAt(spacing.length()) == '-';
+       }
+
+       static private String getItemSpacing(String line) {
+               int i;
+               for (i = 0; i < line.length(); i++) {
+                       if (line.charAt(i) != ' ') {
+                               return line.substring(0, i);
+                       }
+               }
+
+               return "";
+       }
 }