lexer: Factor out functions for counting columns.
[pspp] / src / libpspp / i18n.c
index dad224d34da922bd28fc964b5149e9ef03dce3da..4e04d32c25f39564af7fa7acc451389acaf4eebe 100644 (file)
@@ -1,5 +1,6 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2006, 2009, 2010, 2011, 2012, 2013, 2014, 2015 Free Software Foundation, Inc.
+   Copyright (C) 2006, 2009, 2010, 2011, 2012, 2013, 2014, 2015,
+   2016, 2021 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 #include <string.h>
 #include <unicase.h>
 #include <unigbrk.h>
+#include <uniwidth.h>
 
 #include "libpspp/assertion.h"
 #include "libpspp/compiler.h"
 #include "libpspp/hmapx.h"
 #include "libpspp/hash-functions.h"
+#include "libpspp/misc.h"
 #include "libpspp/pool.h"
 #include "libpspp/str.h"
 #include "libpspp/version.h"
 
+#include "gl/c-ctype.h"
 #include "gl/c-strcase.h"
 #include "gl/localcharset.h"
+#include <gl/localename.h>
 #include "gl/minmax.h"
 #include "gl/xalloc.h"
 #include "gl/relocatable.h"
@@ -60,7 +65,7 @@ static struct hmapx map;
 
 /* A wrapper around iconv_open */
 static struct converter *
-create_iconv (const char* tocode, const char* fromcode)
+create_iconv (const char* tocode, const char* fromcode, bool warn)
 {
   size_t hash;
   struct hmapx_node *node;
@@ -87,10 +92,11 @@ create_iconv (const char* tocode, const char* fromcode)
      as the converters have not yet been set up */
   if (error && strcmp (tocode, fromcode))
     {
-      fprintf (stderr,
-               "Warning: "
-               "cannot create a converter for `%s' to `%s': %s\n",
-               fromcode, tocode, strerror (error));
+      if (warn)
+        fprintf (stderr,
+                 "Warning: "
+                 "cannot create a converter for `%s' to `%s': %s\n",
+                 fromcode, tocode, strerror (error));
 
       free (converter->tocode);
       free (converter->fromcode);
@@ -105,21 +111,18 @@ create_iconv (const char* tocode, const char* fromcode)
   iconv_t bconv = iconv_open (tocode, "ASCII");
   if (bconv != (iconv_t) -1)
     {
-      ICONV_CONST  char *nullstr = strdup ("");
-      ICONV_CONST  char *outbuf = strdup ("XXXXXXXX");
-      ICONV_CONST  char *snullstr = nullstr;
-      ICONV_CONST  char *soutbuf = outbuf;
-
-      size_t inbytes = 1;
-      const size_t bytes = 8;
-      size_t outbytes = bytes;
-      if (-1 != iconv (bconv, &nullstr, &inbytes, &outbuf, &outbytes))
-       converter->null_char_width = bytes - outbytes;
-      free (snullstr);
-      free (soutbuf);
+      ICONV_CONST char inbuf[1] = "";
+      ICONV_CONST char *inptr = inbuf;
+      size_t inbytes = sizeof inbuf;
+
+      char outbuf[8];
+      char *outptr = outbuf;
+      size_t outbytes = sizeof outbuf;
+      if (-1 != iconv (bconv, &inptr, &inbytes, &outptr, &outbytes))
+       converter->null_char_width = outptr - outbuf;
       iconv_close (bconv);
     }
-  
+
   hmapx_insert (&map, converter, hash);
 
   return converter;
@@ -241,7 +244,7 @@ try_recode (struct converter *cvtr, char fallbackchar,
 
   for (i = 0 ; i < null_bytes ; ++i)
     *out++ = '\0';
-  
+
   return out - 1 - out_;
 }
 
@@ -263,10 +266,10 @@ recode_string_pool (const char *to, const char *from,
 {
   struct substring out;
 
-  if ( text == NULL )
+  if (text == NULL)
     return NULL;
 
-  if ( length == -1 )
+  if (length == -1)
     length = strlen (text);
 
   out = recode_substring_pool (to, from, ss_buffer (text, length), pool);
@@ -500,6 +503,55 @@ utf8_encoding_concat_len (const char *head, const char *tail,
   return prefix_len + tail_len;
 }
 
+/* Returns the number of display columns that would be occupied by the LENGTH
+   bytes of UTF-8 starting at S. */
+size_t
+utf8_count_columns (const char *s_, size_t length)
+{
+  const uint8_t *s = CHAR_CAST (const uint8_t *, s_);
+
+  size_t columns = 0;
+  for (int ofs = 0; ofs < length; )
+    {
+      ucs4_t uc;
+      ofs += u8_mbtouc (&uc, s + ofs, length - ofs);
+      if (uc != '\t')
+        {
+          int width = uc_width (uc, "UTF-8");
+          if (width > 0)
+            columns += width;
+        }
+      else
+        columns = ROUND_UP (columns + 1, 8);
+    }
+  return columns;
+}
+
+/* Returns the byte offset in LENGTH-byte UTF-8 string S that is N_COLUMNS
+   display columns into the string. */
+size_t
+utf8_columns_to_bytes (const char *s_, size_t length, size_t n_columns)
+{
+  const uint8_t *s = CHAR_CAST (const uint8_t *, s_);
+
+  size_t columns = 0;
+  int ofs;
+  for (ofs = 0; ofs < length && columns < n_columns; )
+    {
+      ucs4_t uc;
+      ofs += u8_mbtouc (&uc, s + ofs, length - ofs);
+      if (uc != '\t')
+        {
+          int width = uc_width (uc, "UTF-8");
+          if (width > 0)
+            columns += width;
+        }
+      else
+        columns = ROUND_UP (columns + 1, 8);
+    }
+  return ofs;
+}
+
 /* Returns an allocated, null-terminated string, owned by the caller,
    containing as many characters[*] from the beginning of S that would fit
    within MAX_LEN bytes if the returned string were to be re-encoded in
@@ -560,9 +612,9 @@ recode_substring_pool__ (const char *to, const char *from,
   if (from == NULL)
     from = default_encoding;
 
-  conv = create_iconv (to, from);
+  conv = create_iconv (to, from, true);
 
-  if ( NULL == conv )
+  if (NULL == conv)
     {
       if (fallbackchar)
         {
@@ -642,7 +694,9 @@ void
 i18n_init (void)
 {
   setlocale (LC_ALL, "");
-  bindtextdomain (PACKAGE, relocate(locale_dir));
+  char *allocated;
+  bindtextdomain (PACKAGE, relocate2 (locale_dir, &allocated));
+  free (allocated);
   textdomain (PACKAGE);
 
   assert (default_encoding == NULL);
@@ -664,9 +718,24 @@ set_default_encoding (const char *enc)
   default_encoding = xstrdup (enc);
 }
 
+/* Return the ISO two letter code for the current LC_MESSAGES
+   locale category.  */
+char *
+get_language (void)
+{
+  const char *localename = gl_locale_name (LC_MESSAGES, "LC_MESSAGES");
+  if (0 == strcmp (localename, "C"))
+    return NULL;
+  char *ln = xstrdup (localename);
+  char *end = strchr (ln, '_');
+  if (end)
+    *end = '\0';
+  return ln;
+}
+
 
 /* Attempts to set the encoding from a locale name
-   returns true if successfull.
+   returns true if successful.
    This function does not (should not!) alter the current locale.
 */
 bool
@@ -684,7 +753,7 @@ set_encoding_from_locale (const char *loc)
   loc_encoding = xstrdup (locale_charset ());
 
 
-  if ( 0 == strcmp (loc_encoding, c_encoding))
+  if (0 == strcmp (loc_encoding, c_encoding))
     {
       ok = false;
     }
@@ -736,7 +805,7 @@ valid_encoding (const char *enc)
 {
   iconv_t conv = iconv_open (UTF8, enc);
 
-  if ( conv == (iconv_t) -1)
+  if (conv == (iconv_t) -1)
     return false;
 
   iconv_close (conv);
@@ -746,7 +815,7 @@ valid_encoding (const char *enc)
 
 
 /* Return the system local's idea of the
-   decimal seperator character */
+   decimal separator character */
 char
 get_system_decimal (void)
 {
@@ -845,6 +914,80 @@ utf8_strncasecmp (const char *a, size_t an, const char *b, size_t bn)
   return result;
 }
 
+static bool
+is_all_digits (const uint8_t *s, size_t len)
+{
+  for (size_t i = 0; i < len; i++)
+    if (!c_isdigit (s[i]))
+      return false;
+  return true;
+}
+
+/* Compares UTF-8 strings A and B case-insensitively.  If the strings end in a
+   number, then they are compared numerically.  Returns a negative value if A <
+   B, zero if A == B, positive if A > B. */
+int
+utf8_strverscasecmp (const char *a, const char *b)
+{
+  /* Normalize A. */
+  uint8_t a_stub[64];
+  size_t a_len = sizeof a_stub;
+  uint8_t *a_norm = u8_casefold (CHAR_CAST (uint8_t *, a), strlen (a), NULL,
+                                 UNINORM_NFKD, a_stub, &a_len);
+
+  /* Normalize B. */
+  uint8_t b_stub[64];
+  size_t b_len = sizeof b_stub;
+  uint8_t *b_norm = u8_casefold (CHAR_CAST (uint8_t *, b), strlen (b), NULL,
+                                 UNINORM_NFKD, b_stub, &b_len);
+
+  int result;
+  if (!a_norm || !b_norm)
+    {
+      result = strcmp (a, b);
+      goto exit;
+    }
+
+  size_t len = MIN (a_len, b_len);
+  for (size_t i = 0; i < len; i++)
+    if (a_norm[i] != b_norm[i])
+      {
+        /* If both strings end in digits, compare them numerically. */
+        if (is_all_digits (&a_norm[i], a_len - i)
+            && is_all_digits (&b_norm[i], b_len - i))
+          {
+            /* Start by stripping leading zeros, since those don't matter for
+               numerical comparison. */
+            size_t ap, bp;
+            for (ap = i; ap < a_len; ap++)
+              if (a_norm[ap] != '0')
+                break;
+            for (bp = i; bp < b_len; bp++)
+              if (b_norm[bp] != '0')
+                break;
+
+            /* The number with more digits, if there is one, is larger. */
+            size_t a_digits = a_len - ap;
+            size_t b_digits = b_len - bp;
+            if (a_digits != b_digits)
+              result = a_digits > b_digits ? 1 : -1;
+            else
+              result = memcmp (&a_norm[ap], &b_norm[bp], a_digits);
+          }
+        else
+          result = a_norm[i] > b_norm[i] ? 1 : -1;
+        goto exit;
+      }
+  result = a_len < b_len ? -1 : a_len > b_len;
+
+exit:
+  if (a_norm != a_stub)
+    free (a_norm);
+  if (b_norm != b_stub)
+    free (b_norm);
+  return result;
+}
+
 static char *
 utf8_casemap (const char *s,
               uint8_t *(*f) (const uint8_t *, size_t, const char *, uninorm_t,
@@ -877,6 +1020,12 @@ utf8_to_lower (const char *s)
 {
   return utf8_casemap (s, u8_tolower);
 }
+
+char *
+utf8_to_title (const char *s)
+{
+  return utf8_casemap (s, u8_totitle);
+}
 \f
 bool
 get_encoding_info (struct encoding_info *e, const char *name)
@@ -959,8 +1108,8 @@ is_encoding_ebcdic_compatible (const char *encoding)
 bool
 is_encoding_supported (const char *encoding)
 {
-  return (create_iconv ("UTF-8", encoding)
-          && create_iconv (encoding, "UTF-8"));
+  return (create_iconv ("UTF-8", encoding, false)
+          && create_iconv (encoding, "UTF-8", false));
 }
 
 /* Returns true if E is the name of a UTF-8 encoding.