Simplify comparers

This commit is contained in:
Ron Buckton 2017-10-25 17:46:39 -07:00
parent 130c407708
commit c0ed26e605
10 changed files with 165 additions and 258 deletions

View file

@ -1483,6 +1483,38 @@ namespace ts {
return headChain;
}
/**
* Compare two values for their equality.
*/
export function equateValues<T>(a: T, b: T) {
return a === b;
}
export function equateStringsCaseInsensitive(a: string, b: string) {
return a === b
|| a !== undefined
&& b !== undefined
&& a.toUpperCase() === b.toUpperCase();
}
export function equateStringsCaseSensitive(a: string, b: string) {
return equateValues(a, b);
}
/**
* Compare equality between two strings using an ordinal comparison.
*
* Case-insensitive comparisons compare both strings after applying `toUpperCase` to
* each string.
*/
export function equateStrings(a: string, b: string, ignoreCase: boolean) {
return ignoreCase ? equateStringsCaseInsensitive(a, b) : equateStringsCaseSensitive(a, b);
}
export function getStringEqualityComparer(ignoreCase: boolean) {
return ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
}
/**
* Compare two values for their order relative to each other.
*/
@ -1495,280 +1527,157 @@ namespace ts {
}
/**
* Compare two values for their equality.
* Compare two strings using an ordinal comparison.
*
* Ordinal comparisons are based on the difference between the unicode code points of
* both strings. Characters with multiple unicode representations are considered
* unequal. Ordinal comparisons provide predictable ordering, but place "a" after "B".
*
* Case-insensitive comparisons compare both strings after applying `toUpperCase` to
* each string.
*/
export function equateValues<T>(a: T, b: T) {
return a === b;
export function compareStrings(a: string, b: string, ignoreCase: boolean) {
return ignoreCase ? compareStringsCaseInsensitive(a, b) : compareStringsCaseSensitive(a, b);
}
export interface StringCollator {
compare(a: string | undefined, b: string | undefined): number;
equate(a: string | undefined, b: string | undefined): boolean;
export function compareStringsCaseInsensitive(a: string, b: string) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
a = a.toUpperCase();
b = b.toUpperCase();
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
export interface StringCollators {
/**
* Gets a string collator for case-insensitive ordinal comparisons of strings.
*
* Ordinal comparisons are based on the difference between the unicode code points of
* both strings. Characters with multiple unicode representations are considered
* unequal.
*
* Case-insensitive comparisons compare both strings after applying `toUpperCase` to
* each string.
*/
readonly ordinalCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive ordinal comparisons of strings.
*
* Ordinal comparisons are based on the difference between the unicode code points of
* both strings. Characters with multiple unicode representations are considered
* unequal. They provide predictable ordering, but place "a" after "B".
*/
readonly ordinalCaseSensitive: StringCollator;
/**
* Gets or sets a string collator for case-insensitive comparisons of strings in the host default locale.
*
* UI comparisons are based on the sort order of the host default locale. Ordering is not
* predictable between different host locales, but is best for displaying ordered data
* for UI presentation. Characters with multiple unicode representations may be considered
* equal.
*
* Case-insensitive comparisons compare strings that differ in only base characters or
* accents/diacritic marks as unequal.
*/
readonly uiCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive comparisons of strings in the host default locale.
*
* UI comparisons are based on the sort order of the host default locale. Ordering is not
* predictable between different host locales, but is best for displaying ordered data
* for UI presentation. Characters with multiple unicode representations may be considered
* equal.
*/
readonly uiCaseSensitive: StringCollator;
/**
* Gets a string collator for case-insensitive comparisons of strings in an invariant locale.
*
* Invariant comparisons are based on the sort order of an invariant locale ('en-US').
* They provide predictable ordering, placing "a" before "B". Characters with multiple
* unicode representations may be considered equal. Invariant comparisons are best used
* when interacting with the file system.
*
* Case-insensitive comparisons compare strings that differ in only base characters or
* accents/diacritic marks as unequal.
*/
readonly invariantCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive comparisons of strings in an invariant locale.
*
* Invariant comparisons are based on the sort order of an invariant locale ('en-US').
* They provide predictable ordering, placing "a" before "B". Characters with multiple
* unicode representations may be considered equal. Invariant comparisons are best used
* when interacting with the file system.
*/
readonly invariantCaseSensitive: StringCollator;
/**
* Gets or sets the locale for UI collators
*/
uiLocale: string | undefined;
/**
* Creates a `StringCollator` for a specific locale and case sensitivity.
*/
create(locale: string | undefined, caseSensitive: boolean): StringCollator;
/**
* Gets the ordinal `StringCollator` for the provided case sensitivity.
*/
getOrdinalCollator(caseSensitive: boolean): StringCollator;
/**
* Gets the UI `StringCollator` for the provided case sensitivity.
*/
getUICollator(caseSensitive: boolean): StringCollator;
/**
* Gets the invariant `StringCollator` for the provided case sensitivity.
*/
getInvariantCollator(caseSensitive: boolean): StringCollator;
/**
* Gets a `StringCollator` for comparing code fragments for code generation.
*/
getCodeCollator(caseSensitive: boolean): StringCollator;
/**
* Gets a `StringCollator` for comparing paths.
*/
getPathCollator(caseSensitive: boolean): StringCollator;
export function compareStringsCaseSensitive(a: string, b: string) {
return compareValues(a, b);
}
export const StringCollator: StringCollators = (function () {
const invariantLocaleName = "en-US"; // we use en-US for the invariant locale
const create = getStringCollatorFactory();
const ordinalCS: StringCollator = {
compare: compareValues,
equate: equateValues
};
const ordinalCI: StringCollator = {
compare: (a, b) => compareValues(toUpperCase(a), toUpperCase(b)),
equate: (a, b) => toUpperCase(a) === toUpperCase(b)
};
let invariantCI: StringCollator | undefined;
let invariantCS: StringCollator | undefined;
let uiCI: StringCollator | undefined;
let uiCS: StringCollator | undefined;
let uiLocale: string | undefined;
export function getStringComparer(ignoreCase: boolean) {
return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive;
}
return {
get ordinalCaseInsensitive() { return ordinalCI; },
get ordinalCaseSensitive() { return ordinalCS; },
get uiCaseInsensitive() { return uiCI || (uiCI = create(uiLocale, /*caseInsensitive*/ true)); },
get uiCaseSensitive() { return uiCS || (uiCS = create(uiLocale, /*caseInsensitive*/ false)); },
get invariantCaseInsensitive() { return invariantCI || (invariantCI = create(invariantLocaleName, /*caseInsensitive*/ true)); },
get invariantCaseSensitive() { return invariantCS || (invariantCS = create(invariantLocaleName, /*caseInsensitive*/ false)); },
get uiLocale() { return uiLocale; },
set uiLocale(value) {
if (uiLocale !== value) {
uiLocale = value;
uiCI = undefined;
uiCS = undefined;
}
},
create,
getOrdinalCollator,
getUICollator,
getInvariantCollator,
getCodeCollator: getInvariantCollator,
getPathCollator: getInvariantCollator
};
function getOrdinalCollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.ordinalCaseInsensitive : StringCollator.ordinalCaseSensitive;
/**
* Creates a string comparer for use with string collation in the UI.
*/
const createStringComparer = (function () {
// If the host supports Intl, we use it for comparisons using the default locale.
if (typeof Intl === "object" && typeof Intl.Collator === "function") {
return createIntlCollatorStringComparer;
}
function getUICollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.uiCaseInsensitive : StringCollator.uiCaseSensitive;
// If the host does not support Intl, we fall back to localeCompare.
// localeCompare in Node v0.10 is just an ordinal comparison, so don't use it.
if (typeof String.prototype.localeCompare === "function" &&
typeof String.prototype.toLocaleUpperCase === "function" &&
"a".localeCompare("B") < 0) {
return createLocaleCompareStringComparer;
}
function getInvariantCollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.invariantCaseInsensitive : StringCollator.invariantCaseSensitive;
}
function toUpperCase(value: string | undefined): string | undefined {
return value === undefined ? undefined : value.toUpperCase();
}
function compareDefined(a: string, b: string) {
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
// Otherwise, fall back to ordinal comparison:
return createFallbackStringComparer;
function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) {
return a === b ? Comparison.EqualTo :
a === undefined ? Comparison.LessThan :
b === undefined ? Comparison.GreaterThan :
toComparison(comparer(a, b));
}
function toComparison(value: number) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
const value = comparer(a, b);
return value < 0 ? Comparison.LessThan : value > 0 ? Comparison.GreaterThan : Comparison.EqualTo;
}
function createIntlStringCollator(locale: string | undefined, caseInsensitive: boolean): StringCollator {
function createIntlCollatorStringComparer(locale: string | undefined, caseInsensitive: boolean): Comparer<string> {
// Initialize the sort collator on first use
let sortComparer: Comparer<string> = (a, b) => {
let comparer: Comparer<string> = (a, b) => {
// Intl.Collator.prototype.compare is bound to the collator. See NOTE in
// http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare
sortComparer = new Intl.Collator(locale, { usage: "sort", sensitivity: caseInsensitive ? "accent" : "variant" }).compare;
return sortComparer(a, b);
};
// Initialize the search collator on first use
let searchComparer: Comparer<string> = (a, b) => {
// Intl.Collator.prototype.compare is bound to the collator. See NOTE in
// http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare
searchComparer = new Intl.Collator(locale, { usage: "search", sensitivity: caseInsensitive ? "accent" : "variant" }).compare;
return searchComparer(a, b);
};
return {
compare: (a, b) => compareWithCallback(a, b, sortComparer),
equate: (a, b) => compareWithCallback(a, b, searchComparer) === 0
comparer = new Intl.Collator(locale, { usage: "sort", sensitivity: caseInsensitive ? "accent" : "variant" }).compare;
return comparer(a, b);
};
return (a, b) => compareWithCallback(a, b, comparer);
}
function createLocaleCompareStringCollator(locale: string | undefined, caseInsensitive: boolean): StringCollator {
if (locale !== undefined) return getFallbackStringCollator(/*locale*/ undefined, caseInsensitive);
if (caseInsensitive) {
function createLocaleCompareStringComparer(locale: string | undefined, caseInsensitive: boolean): Comparer<string> {
// if the locale is not the default locale (`undefined`), use the fallback comparer.
return locale !== undefined ? createFallbackStringComparer(locale, caseInsensitive) :
caseInsensitive ? (a, b) => compareWithCallback(a, b, compareCaseInsensitive) :
(a, b) => compareWithCallback(a, b, compareCaseSensitive);
function compareCaseInsensitive(a: string, b: string) {
// for case-insensitive comparisons we always map both strings to their
// upper-case form as some unicode characters do not properly round-trip to
// lowercase (such as `ẞ` (German sharp capital s)).
return {
compare: (a, b) => compareWithCallback(a, b, localeCompareCaseInsensitive),
equate: (a, b) => compareWithCallback(a, b, localeCompareCaseInsensitive) === 0
};
}
else {
return {
compare: (a, b) => compareWithCallback(a, b, localeCompare),
equate: (a, b) => compareWithCallback(a, b, localeCompare) === 0
};
return compareCaseSensitive(a.toLocaleUpperCase(), b.toLocaleUpperCase());
}
function localeCompareCaseInsensitive(a: string, b: string) {
return a.toLocaleUpperCase().localeCompare(b.toLocaleUpperCase());
}
function localeCompare(a: string, b: string) {
function compareCaseSensitive(a: string, b: string) {
return a.localeCompare(b);
}
}
function getFallbackStringCollator(_locale: string | undefined, caseInsensitive: boolean): StringCollator {
if (caseInsensitive) return ordinalCI;
function createFallbackStringComparer(_locale: string | undefined, caseInsensitive: boolean): Comparer<string> {
return caseInsensitive ? (a, b) => compareWithCallback(a, b, compareCaseInsensitive) :
(a, b) => compareWithCallback(a, b, compareCaseSensitiveDictionaryOrder);
function compareLowerCaseFirst(a: string, b: string) {
function compareCaseInsensitive(a: string, b: string) {
// for case-insensitive comparisons we always map both strings to their
// upper-case form as some unicode characters do not properly round-trip to
// lowercase (such as `ẞ` (German sharp capital s)).
return compareCaseSensitive(a.toUpperCase(), b.toUpperCase());
}
function compareCaseSensitive(a: string, b: string) {
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
function compareCaseSensitiveDictionaryOrder(a: string, b: string) {
// An ordinal comparison puts "A" after "b", but for the UI we want "A" before "b".
// We first sort case insensitively. So "Aaa" will come before "baa".
// Then we sort case sensitively, so "aaa" will come before "Aaa".
return compareDefined(a.toUpperCase(), b.toUpperCase()) || compareDefined(a, b);
return compareCaseInsensitive(a, b) || compareCaseSensitive(a, b);
}
return {
compare: (a, b) => compareWithCallback(a, b, compareLowerCaseFirst),
equate: ordinalCS.equate
};
}
function getStringCollatorFactory() {
// If the host supports Intl (ECMA-402), we use Intl for comparisons using the default
// locale:
if (typeof Intl === "object" && typeof Intl.Collator === "function") {
return createIntlStringCollator;
}
// If the host does not support Intl, we fall back to localeCompare:
//
// Node v0.10 provides incorrect results for comparisons using localeCompare, so we must
// verify the implementation.
if (typeof String.prototype.localeCompare === "function" &&
typeof String.prototype.toLocaleUpperCase === "function" &&
"a".localeCompare("B") < 0) {
return createLocaleCompareStringCollator;
}
// Otherwise, fall back to ordinal comparison:
return getFallbackStringCollator;
}
})();
let uiCS: Comparer<string> | undefined;
let uiCI: Comparer<string> | undefined;
let uiLocale: string | undefined;
export function setUILocale(value: string) {
if (uiLocale !== value) {
uiLocale = value;
uiCS = undefined;
uiCI = undefined;
}
}
export function compareStringsCaseInsensitiveUI(a: string, b: string) {
const comparer = uiCS || (uiCS = createStringComparer(uiLocale, /*caseInsensitive*/ false));
return comparer(a, b);
}
export function compareStringsCaseSensitiveUI(a: string, b: string) {
const comparer = uiCI || (uiCI = createStringComparer(uiLocale, /*caseInsensitive*/ true));
return comparer(a, b);
}
/**
* Compare two strings using the sort behavior of the UI locale.
*
* Ordering is not predictable between different host locales, but is best for displaying
* ordered data for UI presentation. Characters with multiple unicode representations may
* be considered equal.
*
* Case-insensitive comparisons compare strings that differ in only base characters or
* accents/diacritic marks as unequal.
*/
export function compareStringsUI(a: string, b: string, ignoreCase: boolean) {
return ignoreCase ? compareStringsCaseInsensitiveUI(a, b) : compareStringsCaseSensitiveUI(a, b);
}
export function getStringComparerUI(ignoreCase: boolean) {
return ignoreCase ? compareStringsCaseInsensitiveUI : compareStringsCaseSensitiveUI;
}
function getDiagnosticFileName(diagnostic: Diagnostic): string {
return diagnostic.file ? diagnostic.file.fileName : undefined;
}
@ -2151,9 +2060,9 @@ namespace ts {
const aComponents = getNormalizedPathComponents(a, currentDirectory);
const bComponents = getNormalizedPathComponents(b, currentDirectory);
const sharedLength = Math.min(aComponents.length, bComponents.length);
const collator = StringCollator.getPathCollator(ignoreCase);
const comparer = getStringComparer(ignoreCase);
for (let i = 0; i < sharedLength; i++) {
const result = collator.compare(aComponents[i], bComponents[i]);
const result = comparer(aComponents[i], bComponents[i]);
if (result !== Comparison.EqualTo) {
return result;
}
@ -2175,9 +2084,9 @@ namespace ts {
}
// File-system comparisons should use predictable ordering
const collator = StringCollator.getPathCollator(ignoreCase);
const equalityComparer = getStringEqualityComparer(ignoreCase);
for (let i = 0; i < parentComponents.length; i++) {
if (!collator.equate(parentComponents[i], childComponents[i])) {
if (!equalityComparer(parentComponents[i], childComponents[i])) {
return false;
}
}
@ -2433,7 +2342,7 @@ namespace ts {
// If there are no "includes", then just put everything in results[0].
const results: string[][] = includeFileRegexes ? includeFileRegexes.map(() => []) : [[]];
const collator = StringCollator.getPathCollator(!useCaseSensitiveFileNames);
const comparer = getStringComparer(!useCaseSensitiveFileNames);
for (const basePath of patterns.basePaths) {
visitDirectory(basePath, combinePaths(currentDirectory, basePath), depth);
}
@ -2442,7 +2351,7 @@ namespace ts {
function visitDirectory(path: string, absolutePath: string, depth: number | undefined) {
let { files, directories } = getFileSystemEntries(path);
files = files.slice().sort(collator.compare);
files = files.slice().sort(comparer);
for (const current of files) {
const name = combinePaths(path, current);
@ -2467,7 +2376,7 @@ namespace ts {
}
}
directories = directories.slice().sort(collator.compare);
directories = directories.slice().sort(comparer);
for (const current of directories) {
const name = combinePaths(path, current);
const absoluteName = combinePaths(absolutePath, current);
@ -2498,8 +2407,7 @@ namespace ts {
}
// Sort the offsets array using either the literal or canonical path representations.
const collator = StringCollator.getPathCollator(!useCaseSensitiveFileNames);
includeBasePaths.sort(collator.compare);
includeBasePaths.sort(getStringComparer(!useCaseSensitiveFileNames));
// Iterate over each include base path and include unique base paths that are not a
// subpath of an existing base path

View file

@ -6069,7 +6069,7 @@ namespace ts {
const checkJsDirectiveMatchResult = checkJsDirectiveRegEx.exec(comment);
if (checkJsDirectiveMatchResult) {
checkJsDirective = {
enabled: StringCollator.ordinalCaseInsensitive.equate(checkJsDirectiveMatchResult[1], "@ts-check"),
enabled: equateStringsCaseInsensitive(checkJsDirectiveMatchResult[1], "@ts-check"),
end: range.end,
pos: range.pos
};

View file

@ -1103,12 +1103,12 @@ namespace ts {
// otherwise, using options specified in '--lib' instead of '--target' default library file
// File-system ordering should use a predictable order
const collator = StringCollator.getPathCollator(!host.useCaseSensitiveFileNames());
const equalityComparer = getStringEqualityComparer(!host.useCaseSensitiveFileNames());
if (!options.lib) {
return collator.equate(file.fileName, getDefaultLibraryFileName());
return equalityComparer(file.fileName, getDefaultLibraryFileName());
}
else {
return forEach(options.lib, libFileName => collator.equate(file.fileName, combinePaths(defaultLibraryPath, libFileName)));
return forEach(options.lib, libFileName => equalityComparer(file.fileName, combinePaths(defaultLibraryPath, libFileName)));
}
}

View file

@ -3950,7 +3950,7 @@ namespace ts {
}
// Set the locale for UI collation
StringCollator.uiLocale = locale;
setUILocale(locale);
function trySetLanguageAndTerritory(language: string, territory: string, errors?: Push<Diagnostic>): boolean {
const compilerFilePath = normalizePath(sys.getExecutingFilePath());

View file

@ -1699,8 +1699,8 @@ namespace Harness {
export function *iterateOutputs(outputFiles: Harness.Compiler.GeneratedFile[]): IterableIterator<[string, string]> {
// Collect, test, and sort the fileNames
// As this uses the file system, use a predictable order
const collator = ts.StringCollator.getPathCollator(/*ignoreCase*/ false);
outputFiles.sort((a, b) => collator.compare(cleanName(a.fileName), cleanName(b.fileName)));
const comparer = ts.getStringComparer(/*ignoreCase*/ false);
outputFiles.sort((a, b) => comparer(cleanName(a.fileName), cleanName(b.fileName)));
const dupeCase = ts.createMap<number>();
// Yield them
for (const outputFile of outputFiles) {

View file

@ -14,9 +14,9 @@ namespace ts.projectSystem {
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[];
// File-system ordering should use a predictable order
const collator = StringCollator.getPathCollator(/*ignoreCase*/ false);
const actualResult = response.sort((list1, list2) => collator.compare(list1.projectFileName, list2.projectFileName));
expectedFileList = expectedFileList.sort((list1, list2) => collator.compare(list1.projectFileName, list2.projectFileName));
const comparer = getStringComparer(/*ignoreCase*/ false);
const actualResult = response.sort((list1, list2) => comparer(list1.projectFileName, list2.projectFileName));
expectedFileList = expectedFileList.sort((list1, list2) => comparer(list1.projectFileName, list2.projectFileName));
assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`);

View file

@ -1186,7 +1186,6 @@ namespace ts.server {
const completions = project.getLanguageService().getCompletionsAtPosition(file, position);
if (simplifiedResult) {
const comparer = StringCollator.uiCaseSensitive.compare;
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) {
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry;
@ -1194,7 +1193,7 @@ namespace ts.server {
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined };
}
}).sort((a, b) => comparer(a.name, b.name));
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
}
else {
return completions;

View file

@ -176,7 +176,7 @@ namespace ts.NavigateTo {
function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem): number {
// TODO(cyrusn): get the gamut of comparisons that VS already uses here.
return i1.matchKind - i2.matchKind ||
StringCollator.uiCaseSensitive.compare(i1.name, i2.name);
compareStringsCaseSensitiveUI(i1.name, i2.name);
}
function createNavigateToItem(rawItem: RawNavigateToItem): NavigateToItem {

View file

@ -368,7 +368,7 @@ namespace ts.NavigationBar {
function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode): number {
const name1 = tryGetName(child1.node), name2 = tryGetName(child2.node);
return StringCollator.uiCaseInsensitive.compare(name1, name2)
return compareStringsCaseInsensitiveUI(name1, name2)
|| navigationBarNodeKind(child1) - navigationBarNodeKind(child2);
}

View file

@ -1156,7 +1156,7 @@ namespace ts.refactor.extractSymbol {
const name2 = type2.symbol ? type2.symbol.getName() : "";
// This is for code generation, use a predictable comparer.
const nameDiff = StringCollator.invariantCaseSensitive.compare(name1, name2);
const nameDiff = compareStringsCaseSensitive(name1, name2);
if (nameDiff !== 0) {
return nameDiff;
}