-
Notifications
You must be signed in to change notification settings - Fork 6.3k
8365675: Add String Unicode Case-Folding Support #27628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
👋 Welcome back sherman! A progress list of the required criteria for merging this PR into |
❗ This change is not yet ready to be integrated. |
@xuemingshen-oracle The following labels will be automatically applied to this pull request:
When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing lists. If you would like to change these labels, use the /label pull request command. |
Webrevs
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build changes look fine.
/reviewers 2
While working on Unicode 17 upgrade, I noticed that they changed the example from "MASSE"/"Maße" to "FUSS"/"Fuß" (https://www.unicode.org/L2/L2025/25085.htm#183-A59), so you might want to switch them as well |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API looks good.
Is the performance comparable to equalsIgnoreCase?
char[] folded1 = null; | ||
char[] folded2 = null; | ||
int k1 = 0, k2 = 0, fk1 = 0, fk2 = 0; | ||
while ((k1 < len1 || folded1 != null && fk1 < folded1.length) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many suggestions come to mind here on the algorithm, to optimize performance.
For example, many strings will have identical prefixes. Using Arrays.mismatch could quickly skip over the identical prefix.
Consider using code points (or a long, packing 4 chars) for the folded replacements, to avoid having to step through chars in char arrays. CaseFolding.foldIfDefined could return the full expansion as a long.
It may be profitable to use Arrays.mismatch again after expanded characters are determined to be equal.
Take another look at the data structure storing and doing the lookup of foldIfDefined both to increase the lookup performance.
|
||
private static class CaseFoldingEntry { | ||
final int cp; | ||
final char[] folding; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider storing the folding as a int or long directly to avoid the overhead of small char arrays.
Arrange to be able to compare the whole replacement with another codePoint, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I misunderstood the algorithm when comparing folded characters against non-folded sequences.
I still think a fast path for single character replacements will lower memory costs and improve performance.
The case of single-codepoint to single-codepoint dominates the case folding mappings.
return depth; | ||
} | ||
|
||
private void add(CaseFoldingEntry entry) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CDS can map whole objects/data structures into the heap; consider how to make this data structure so it can be mapped and not re-computed each startup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this patch obviously has so many performance optimization opportunities, I recommend handling those in subsequent RFEs so that we can review this purely from a specification point of view.
################################################################################ | ||
|
||
|
||
GENSRC_STRINGCASEFOLDING := $(SUPPORT_OUTPUTDIR)/gensrc/java.base/jdk/internal/java/lang/CaseFolding.java |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we target the package jdk.internal.lang
instead of jdk.internal.java.lang
? I think the previous one is the convention set forth by stable values.
There is adequate time before RPD1 (Dec 4, 25) to improve performance, but the feature should not be included in JDK 26 unless the performance is comparable to the existing |
Great progress thanks. Did you also consider a startsWith/containsCaseFold, I missed the case ignoring variants of those already. Or maybe provide an API to implement them on the cases folded intermediate buffers? If the API footprint gets too big on String as CaseFoldString.contains() helper maybe? |
I think for this purpose, we should rather introduce an API to case fold a string - we can use these operations on the case-fold-normalized strings. |
The new APIs mentioned would be more effective, leveraging the underlying implementation without needing to create new Strings. Earlier discussions of the support for folding, raised a concern about tempting developers to a more ambiguous situation in which folded and unfolded strings exist and can be confused. |
I don't think it's a good idea to have an explosion of case-folding variants of string operations if we are adding a case-folding overload for every operation. In that case, the confusion of case folding applicability would be less of a problem compared to the API bloat. |
import static java.util.Map.entry; | ||
|
||
/** | ||
* Utility class for {@code String.toCaseFold()} that handles Unicode case folding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make it clear this is a planned api (or not refer to it?)
Summary
Case folding is a key operation for case-insensitive matching (e.g., string equality, regex matching), where the goal is to eliminate case distinctions without applying locale or language specific conversions.
Currently, the JDK does not expose a direct API for Unicode-compliant case folding. Developers now rely on methods such as:
String.equalsIgnoreCase(String)
Character.toLowerCase(int) / Character.toUpperCase(int)
String.toLowerCase(Locale.ROOT) / String.toUpperCase(Locale.ROOT)
1:M mapping example, U+00DF (ß)
Motivation & Direction
Add Unicode standard-compliant case-less comparison methods to the String class, enabling & improving reliable and efficient Unicode-aware/compliant case-insensitive matching.
This PR proposes to introduce the following comparison methods in
String
classThese methods are intended to be the preferred choice when Unicode-compliant case-less matching is required.
*Note: An early draft also proposed a String.toCaseFold() method returning a new case-folded string.
However, during review this was considered error-prone, as the resulting string could easily be mistaken for a general transformation like toLowerCase() and then passed into APIs where case-folding semantics are not appropriate.
The New API
See CSR https://bugs.openjdk.org/browse/JDK-8369017
Usage Examples
Sharp s (U+00DF) case-folds to "ss"
Performance
The JMH microbenchmark StringCompareToIgnoreCase has been updated to compare performance of compareToFoldCase with the existing compareToIgnoreCase().
Refs
Unicode Standard 5.18.4 Caseless Matching
Unicode® Standard Annex #44: 5.6 Case and Case Mapping
Unicode Technical Standard #18: Unicode Regular Expressions RL1.5: Simple Loose Matches
Unicode SpecialCasing.txt
Unicode CaseFolding.txt
Other Languages
Python string.casefold()
The str.casefold() method in Python returns a casefolded version of a string. Casefolding is a more aggressive form of lowercasing, designed to remove all case distinctions in a string, particularly for the purpose of caseless string comparisons.
Perl’s fc()
Returns the casefolded version of EXPR. This is the internal function implementing the \F escape in double-quoted strings.
Casefolding is the process of mapping strings to a form where case differences are erased; comparing two strings in their casefolded form is effectively a way of asking if two strings are equal, regardless of case.
Perl only implements the full form of casefolding, but you can access the simple folds using "casefold()" in Unicode::UCD] ad "prop_invmap()" in Unicode::UCD].
ICU4J UCharacter.foldCase (Java)
Purpose: Provides extensions to the standard Java Character class, including support for more Unicode properties and handling of supplementary characters (code points beyond U+FFFF).
Method Signature (String based): public static String foldCase(String str, int options)
Method Signature (CharSequence & Appendable based): public static A foldCase(CharSequence src, A dest, int options, Edits edits)
Key Features:
Case Folding: Converts a string to its case-folded equivalent.
Locale Independent: Case folding in UCharacter.foldCase is generally not dependent on locale settings.
Context Insensitive: The mapping of a character is not affected by surrounding characters.
Turkic Option: An option exists to include or exclude special mappings for Turkish/Azerbaijani text.
Result Length: The resulting string can be longer or shorter than the original.
Edits Recording: Allows for recording of edits for index mapping, styled text, and getting only changes.
u_strFoldCase (C/C++)
A lower-level C API function for case folding a string.
Case Folding Options: Similar options as UCharacter.foldCase for controlling case folding behavior.
Availability: Found in the ustring.h and unistr.h headers in the ICU4C library.
Progress
Issues
Reviewers
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/27628/head:pull/27628
$ git checkout pull/27628
Update a local copy of the PR:
$ git checkout pull/27628
$ git pull https://git.openjdk.org/jdk.git pull/27628/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 27628
View PR using the GUI difftool:
$ git pr show -t 27628
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/27628.diff
Using Webrev
Link to Webrev Comment