/* gunicollate.c - Collation
*
- * Copyright 2001 Red Hat, Inc.
+ * Copyright 2001,2005 Red Hat, Inc.
*
* The Gnome Library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
*
* You should have received a copy of the GNU Lesser General Public
* License along with the Gnome Library; see the file COPYING.LIB. If not,
- * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
- * Boston, MA 02111-1307, USA.
+ * see <http://www.gnu.org/licenses/>.
*/
+#include "config.h"
+
#include <locale.h>
#include <string.h>
#ifdef __STDC_ISO_10646__
#include <wchar.h>
#endif
-#include "glib.h"
+#ifdef HAVE_CARBON
+#include <CoreServices/CoreServices.h>
+#endif
+
+#include "gmem.h"
+#include "gunicode.h"
+#include "gunicodeprivate.h"
+#include "gstring.h"
+#include "gstrfuncs.h"
+#include "gtestutils.h"
+#include "gcharset.h"
+#ifndef __STDC_ISO_10646__
+#include "gconvert.h"
+#endif
+
-extern gunichar *_g_utf8_normalize_wc (const gchar *str,
- gssize max_len,
- GNormalizeMode mode);
+#ifdef _MSC_VER
+/* Workaround for bug in MSVCR80.DLL */
+static gsize
+msc_strxfrm_wrapper (char *string1,
+ const char *string2,
+ gsize count)
+{
+ if (!string1 || count <= 0)
+ {
+ char tmp;
+
+ return strxfrm (&tmp, string2, 1);
+ }
+ return strxfrm (string1, string2, count);
+}
+#define strxfrm msc_strxfrm_wrapper
+#endif
/**
* g_utf8_collate:
* @str2: a UTF-8 encoded string
*
* Compares two strings for ordering using the linguistically
- * correct rules for the current locale. When sorting a large
- * number of strings, it will be significantly faster to
- * obtain collation keys with g_utf8_collate_key() and
- * compare the keys with <function>strcmp()</function> when
- * sorting instead of sorting the original strings.
+ * correct rules for the [current locale][setlocale].
+ * When sorting a large number of strings, it will be significantly
+ * faster to obtain collation keys with g_utf8_collate_key() and
+ * compare the keys with strcmp() when sorting instead of sorting
+ * the original strings.
*
- * Return value: -1 if @str1 compares before @str2, 0 if they
- * compare equal, 1 if @str1 compares after @str2.
+ * Returns: < 0 if @str1 compares before @str2,
+ * 0 if they compare equal, > 0 if @str1 compares after @str2.
**/
gint
g_utf8_collate (const gchar *str1,
const gchar *str2)
{
gint result;
-
-#ifdef __STDC_ISO_10646__
- gunichar *str1_norm = _g_utf8_normalize_wc (str1, -1, G_NORMALIZE_ALL_COMPOSE);
- gunichar *str2_norm = _g_utf8_normalize_wc (str2, -1, G_NORMALIZE_ALL_COMPOSE);
+#ifdef HAVE_CARBON
+
+ UniChar *str1_utf16;
+ UniChar *str2_utf16;
+ glong len1;
+ glong len2;
+ SInt32 retval = 0;
+
+ g_return_val_if_fail (str1 != NULL, 0);
+ g_return_val_if_fail (str2 != NULL, 0);
+
+ str1_utf16 = g_utf8_to_utf16 (str1, -1, NULL, &len1, NULL);
+ str2_utf16 = g_utf8_to_utf16 (str2, -1, NULL, &len2, NULL);
+
+ UCCompareTextDefault (kUCCollateStandardOptions,
+ str1_utf16, len1, str2_utf16, len2,
+ NULL, &retval);
+ result = retval;
+
+ g_free (str2_utf16);
+ g_free (str1_utf16);
+
+#elif defined(__STDC_ISO_10646__)
+
+ gunichar *str1_norm;
+ gunichar *str2_norm;
+
+ g_return_val_if_fail (str1 != NULL, 0);
+ g_return_val_if_fail (str2 != NULL, 0);
+
+ str1_norm = _g_utf8_normalize_wc (str1, -1, G_NORMALIZE_ALL_COMPOSE);
+ str2_norm = _g_utf8_normalize_wc (str2, -1, G_NORMALIZE_ALL_COMPOSE);
result = wcscoll ((wchar_t *)str1_norm, (wchar_t *)str2_norm);
#else /* !__STDC_ISO_10646__ */
const gchar *charset;
- gchar *str1_norm = g_utf8_normalize (str1, -1, G_NORMALIZE_ALL_COMPOSE);
- gchar *str2_norm = g_utf8_normalize (str2, -1, G_NORMALIZE_ALL_COMPOSE);
+ gchar *str1_norm;
+ gchar *str2_norm;
+
+ g_return_val_if_fail (str1 != NULL, 0);
+ g_return_val_if_fail (str2 != NULL, 0);
+
+ str1_norm = g_utf8_normalize (str1, -1, G_NORMALIZE_ALL_COMPOSE);
+ str2_norm = g_utf8_normalize (str2, -1, G_NORMALIZE_ALL_COMPOSE);
if (g_get_charset (&charset))
{
}
else
{
- gchar *str1_locale = g_convert (str1_norm, -1, "UTF-8", charset, NULL, NULL, NULL);
- gchar *str2_locale = g_convert (str2_norm, -1, "UTF-8", charset, NULL, NULL, NULL);
+ gchar *str1_locale = g_convert (str1_norm, -1, charset, "UTF-8", NULL, NULL, NULL);
+ gchar *str2_locale = g_convert (str2_norm, -1, charset, "UTF-8", NULL, NULL, NULL);
if (str1_locale && str2_locale)
result = strcoll (str1_locale, str2_locale);
return result;
}
-#ifdef __STDC_ISO_10646__
+#if defined(__STDC_ISO_10646__) || defined(HAVE_CARBON)
/* We need UTF-8 encoding of numbers to encode the weights if
* we are using wcsxfrm. However, we aren't encoding Unicode
* characters, so we can't simply use g_unichar_to_utf8.
return retval;
}
-#endif /* __STDC_ISO_10646__ */
+#endif /* __STDC_ISO_10646__ || HAVE_CARBON */
+
+#ifdef HAVE_CARBON
+
+static gchar *
+collate_key_to_string (UCCollationValue *key,
+ gsize key_len)
+{
+ gchar *result;
+ gsize result_len = 0;
+ const gsize start = 2 * sizeof (void *) / sizeof (UCCollationValue);
+ gsize i;
+
+ /* The first codes should be skipped: the same string on the same
+ * system can get different values at runtime in those positions,
+ * and they do not sort correctly. The exact size of the prefix
+ * depends on whether we are building 64 or 32 bit.
+ */
+ if (key_len <= start)
+ return g_strdup ("");
+
+ for (i = start; i < key_len; i++)
+ result_len += utf8_encode (NULL, g_htonl (key[i] + 1));
+
+ result = g_malloc (result_len + 1);
+ result_len = 0;
+ for (i = start; i < key_len; i++)
+ result_len += utf8_encode (result + result_len, g_htonl (key[i] + 1));
+
+ result[result_len] = '\0';
+
+ return result;
+}
+
+static gchar *
+carbon_collate_key_with_collator (const gchar *str,
+ gssize len,
+ CollatorRef collator)
+{
+ UniChar *str_utf16 = NULL;
+ glong len_utf16;
+ OSStatus ret;
+ UCCollationValue staticbuf[512];
+ UCCollationValue *freeme = NULL;
+ UCCollationValue *buf;
+ ItemCount buf_len;
+ ItemCount key_len;
+ ItemCount try_len;
+ gchar *result = NULL;
+
+ str_utf16 = g_utf8_to_utf16 (str, len, NULL, &len_utf16, NULL);
+ try_len = len_utf16 * 5 + 2;
+
+ if (try_len <= sizeof staticbuf)
+ {
+ buf = staticbuf;
+ buf_len = sizeof staticbuf;
+ }
+ else
+ {
+ freeme = g_new (UCCollationValue, try_len);
+ buf = freeme;
+ buf_len = try_len;
+ }
+
+ ret = UCGetCollationKey (collator, str_utf16, len_utf16,
+ buf_len, &key_len, buf);
+
+ if (ret == kCollateBufferTooSmall)
+ {
+ freeme = g_renew (UCCollationValue, freeme, try_len * 2);
+ buf = freeme;
+ buf_len = try_len * 2;
+ ret = UCGetCollationKey (collator, str_utf16, len_utf16,
+ buf_len, &key_len, buf);
+ }
+
+ if (ret == 0)
+ result = collate_key_to_string (buf, key_len);
+ else
+ result = g_strdup ("");
+
+ g_free (freeme);
+ g_free (str_utf16);
+ return result;
+}
+
+static gchar *
+carbon_collate_key (const gchar *str,
+ gssize len)
+{
+ static CollatorRef collator;
+
+ if (G_UNLIKELY (!collator))
+ {
+ UCCreateCollator (NULL, 0, kUCCollateStandardOptions, &collator);
+
+ if (!collator)
+ {
+ static gboolean been_here;
+ if (!been_here)
+ g_warning ("%s: UCCreateCollator failed", G_STRLOC);
+ been_here = TRUE;
+ return g_strdup ("");
+ }
+ }
+
+ return carbon_collate_key_with_collator (str, len, collator);
+}
+
+static gchar *
+carbon_collate_key_for_filename (const gchar *str,
+ gssize len)
+{
+ static CollatorRef collator;
+
+ if (G_UNLIKELY (!collator))
+ {
+ /* http://developer.apple.com/qa/qa2004/qa1159.html */
+ UCCreateCollator (NULL, 0,
+ kUCCollateComposeInsensitiveMask
+ | kUCCollateWidthInsensitiveMask
+ | kUCCollateCaseInsensitiveMask
+ | kUCCollateDigitsOverrideMask
+ | kUCCollateDigitsAsNumberMask
+ | kUCCollatePunctuationSignificantMask,
+ &collator);
+
+ if (!collator)
+ {
+ static gboolean been_here;
+ if (!been_here)
+ g_warning ("%s: UCCreateCollator failed", G_STRLOC);
+ been_here = TRUE;
+ return g_strdup ("");
+ }
+ }
+
+ return carbon_collate_key_with_collator (str, len, collator);
+}
+
+#endif /* HAVE_CARBON */
/**
* g_utf8_collate_key:
* @len: length of @str, in bytes, or -1 if @str is nul-terminated.
*
* Converts a string into a collation key that can be compared
- * with other collation keys using <function>strcmp()</function>.
+ * with other collation keys produced by the same function using
+ * strcmp().
+ *
* The results of comparing the collation keys of two strings
- * with <function>strcmp()</function> will always be the same as
- * comparing the two original keys with g_utf8_collate().
+ * with strcmp() will always be the same as comparing the two
+ * original keys with g_utf8_collate().
+ *
+ * Note that this function depends on the [current locale][setlocale].
*
- * Return value: a newly allocated string. This string should
+ * Returns: a newly allocated string. This string should
* be freed with g_free() when you are done with it.
**/
gchar *
gssize len)
{
gchar *result;
- size_t xfrm_len;
-
-#ifdef __STDC_ISO_10646__
- gunichar *str_norm = _g_utf8_normalize_wc (str, len, G_NORMALIZE_ALL_COMPOSE);
+#ifdef HAVE_CARBON
+
+ g_return_val_if_fail (str != NULL, NULL);
+ result = carbon_collate_key (str, len);
+
+#elif defined(__STDC_ISO_10646__)
+
+ gsize xfrm_len;
+ gunichar *str_norm;
wchar_t *result_wc;
- size_t i;
- size_t result_len = 0;
+ gsize i;
+ gsize result_len = 0;
- setlocale (LC_COLLATE, "");
+ g_return_val_if_fail (str != NULL, NULL);
+
+ str_norm = _g_utf8_normalize_wc (str, len, G_NORMALIZE_ALL_COMPOSE);
xfrm_len = wcsxfrm (NULL, (wchar_t *)str_norm, 0);
result_wc = g_new (wchar_t, xfrm_len + 1);
wcsxfrm (result_wc, (wchar_t *)str_norm, xfrm_len + 1);
- for (i=0; i < xfrm_len; i++)
+ for (i = 0; i < xfrm_len; i++)
result_len += utf8_encode (NULL, result_wc[i]);
result = g_malloc (result_len + 1);
result_len = 0;
- for (i=0; i < xfrm_len; i++)
+ for (i = 0; i < xfrm_len; i++)
result_len += utf8_encode (result + result_len, result_wc[i]);
result[result_len] = '\0';
return result;
#else /* !__STDC_ISO_10646__ */
+ gsize xfrm_len;
const gchar *charset;
- gchar *str_norm = g_utf8_normalize (str, len, G_NORMALIZE_ALL_COMPOSE);
+ gchar *str_norm;
+
+ g_return_val_if_fail (str != NULL, NULL);
+
+ str_norm = g_utf8_normalize (str, len, G_NORMALIZE_ALL_COMPOSE);
+
+ result = NULL;
if (g_get_charset (&charset))
{
xfrm_len = strxfrm (NULL, str_norm, 0);
- result = g_malloc (xfrm_len + 1);
- strxfrm (result, str_norm, xfrm_len + 1);
+ if (xfrm_len >= 0 && xfrm_len < G_MAXINT - 2)
+ {
+ result = g_malloc (xfrm_len + 1);
+ strxfrm (result, str_norm, xfrm_len + 1);
+ }
}
else
{
- gchar *str_locale = g_convert (str_norm, -1, "UTF-8", charset, NULL, NULL, NULL);
+ gchar *str_locale = g_convert (str_norm, -1, charset, "UTF-8", NULL, NULL, NULL);
if (str_locale)
{
xfrm_len = strxfrm (NULL, str_locale, 0);
+ if (xfrm_len < 0 || xfrm_len >= G_MAXINT - 2)
+ {
+ g_free (str_locale);
+ str_locale = NULL;
+ }
+ }
+ if (str_locale)
+ {
result = g_malloc (xfrm_len + 2);
result[0] = 'A';
strxfrm (result + 1, str_locale, xfrm_len + 1);
g_free (str_locale);
}
- else
- {
- xfrm_len = strlen (str_norm);
- result = g_malloc (xfrm_len + 2);
- result[0] = 'B';
- memcpy (result + 1, str_norm, xfrm_len);
- result[xfrm_len+1] = '\0';
- }
+ }
+
+ if (!result)
+ {
+ xfrm_len = strlen (str_norm);
+ result = g_malloc (xfrm_len + 2);
+ result[0] = 'B';
+ memcpy (result + 1, str_norm, xfrm_len);
+ result[xfrm_len+1] = '\0';
}
g_free (str_norm);
return result;
}
+
+/* This is a collation key that is very very likely to sort before any
+ * collation key that libc strxfrm generates. We use this before any
+ * special case (dot or number) to make sure that its sorted before
+ * anything else.
+ */
+#define COLLATION_SENTINEL "\1\1\1"
+
+/**
+ * g_utf8_collate_key_for_filename:
+ * @str: a UTF-8 encoded string.
+ * @len: length of @str, in bytes, or -1 if @str is nul-terminated.
+ *
+ * Converts a string into a collation key that can be compared
+ * with other collation keys produced by the same function using strcmp().
+ *
+ * In order to sort filenames correctly, this function treats the dot '.'
+ * as a special case. Most dictionary orderings seem to consider it
+ * insignificant, thus producing the ordering "event.c" "eventgenerator.c"
+ * "event.h" instead of "event.c" "event.h" "eventgenerator.c". Also, we
+ * would like to treat numbers intelligently so that "file1" "file10" "file5"
+ * is sorted as "file1" "file5" "file10".
+ *
+ * Note that this function depends on the [current locale][setlocale].
+ *
+ * Returns: a newly allocated string. This string should
+ * be freed with g_free() when you are done with it.
+ *
+ * Since: 2.8
+ */
+gchar *
+g_utf8_collate_key_for_filename (const gchar *str,
+ gssize len)
+{
+#ifndef HAVE_CARBON
+ GString *result;
+ GString *append;
+ const gchar *p;
+ const gchar *prev;
+ const gchar *end;
+ gchar *collate_key;
+ gint digits;
+ gint leading_zeros;
+
+ /*
+ * How it works:
+ *
+ * Split the filename into collatable substrings which do
+ * not contain [.0-9] and special-cased substrings. The collatable
+ * substrings are run through the normal g_utf8_collate_key() and the
+ * resulting keys are concatenated with keys generated from the
+ * special-cased substrings.
+ *
+ * Special cases: Dots are handled by replacing them with '\1' which
+ * implies that short dot-delimited substrings are before long ones,
+ * e.g.
+ *
+ * a\1a (a.a)
+ * a-\1a (a-.a)
+ * aa\1a (aa.a)
+ *
+ * Numbers are handled by prepending to each number d-1 superdigits
+ * where d = number of digits in the number and SUPERDIGIT is a
+ * character with an integer value higher than any digit (for instance
+ * ':'). This ensures that single-digit numbers are sorted before
+ * double-digit numbers which in turn are sorted separately from
+ * triple-digit numbers, etc. To avoid strange side-effects when
+ * sorting strings that already contain SUPERDIGITs, a '\2'
+ * is also prepended, like this
+ *
+ * file\21 (file1)
+ * file\25 (file5)
+ * file\2:10 (file10)
+ * file\2:26 (file26)
+ * file\2::100 (file100)
+ * file:foo (file:foo)
+ *
+ * This has the side-effect of sorting numbers before everything else (except
+ * dots), but this is probably OK.
+ *
+ * Leading digits are ignored when doing the above. To discriminate
+ * numbers which differ only in the number of leading digits, we append
+ * the number of leading digits as a byte at the very end of the collation
+ * key.
+ *
+ * To try avoid conflict with any collation key sequence generated by libc we
+ * start each switch to a special cased part with a sentinel that hopefully
+ * will sort before anything libc will generate.
+ */
+
+ if (len < 0)
+ len = strlen (str);
+
+ result = g_string_sized_new (len * 2);
+ append = g_string_sized_new (0);
+
+ end = str + len;
+
+ /* No need to use utf8 functions, since we're only looking for ascii chars */
+ for (prev = p = str; p < end; p++)
+ {
+ switch (*p)
+ {
+ case '.':
+ if (prev != p)
+ {
+ collate_key = g_utf8_collate_key (prev, p - prev);
+ g_string_append (result, collate_key);
+ g_free (collate_key);
+ }
+
+ g_string_append (result, COLLATION_SENTINEL "\1");
+
+ /* skip the dot */
+ prev = p + 1;
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ if (prev != p)
+ {
+ collate_key = g_utf8_collate_key (prev, p - prev);
+ g_string_append (result, collate_key);
+ g_free (collate_key);
+ }
+
+ g_string_append (result, COLLATION_SENTINEL "\2");
+
+ prev = p;
+
+ /* write d-1 colons */
+ if (*p == '0')
+ {
+ leading_zeros = 1;
+ digits = 0;
+ }
+ else
+ {
+ leading_zeros = 0;
+ digits = 1;
+ }
+
+ while (++p < end)
+ {
+ if (*p == '0' && !digits)
+ ++leading_zeros;
+ else if (g_ascii_isdigit(*p))
+ ++digits;
+ else
+ {
+ /* count an all-zero sequence as
+ * one digit plus leading zeros
+ */
+ if (!digits)
+ {
+ ++digits;
+ --leading_zeros;
+ }
+ break;
+ }
+ }
+
+ while (digits > 1)
+ {
+ g_string_append_c (result, ':');
+ --digits;
+ }
+
+ if (leading_zeros > 0)
+ {
+ g_string_append_c (append, (char)leading_zeros);
+ prev += leading_zeros;
+ }
+
+ /* write the number itself */
+ g_string_append_len (result, prev, p - prev);
+
+ prev = p;
+ --p; /* go one step back to avoid disturbing outer loop */
+ break;
+
+ default:
+ /* other characters just accumulate */
+ break;
+ }
+ }
+
+ if (prev != p)
+ {
+ collate_key = g_utf8_collate_key (prev, p - prev);
+ g_string_append (result, collate_key);
+ g_free (collate_key);
+ }
+
+ g_string_append (result, append->str);
+ g_string_free (append, TRUE);
+
+ return g_string_free (result, FALSE);
+#else /* HAVE_CARBON */
+ return carbon_collate_key_for_filename (str, len);
+#endif
+}