diff --git a/src/dotnet/Fable.Compiler/Transforms/Fable2Babel.fs b/src/dotnet/Fable.Compiler/Transforms/Fable2Babel.fs index 262d2e350e..5fbfbba1d2 100644 --- a/src/dotnet/Fable.Compiler/Transforms/Fable2Babel.fs +++ b/src/dotnet/Fable.Compiler/Transforms/Fable2Babel.fs @@ -210,8 +210,6 @@ module Util = | Fable.ArrayAlloc(TransformExpr com ctx size) -> [|size|] NewExpression(Identifier jsName, args) :> Expression match typ with - | Fable.Char when com.Options.typedArrays -> - makeJsTypedArray "Uint16Array" | Fable.Number kind when com.Options.typedArrays -> getTypedArrayName com kind |> makeJsTypedArray | _ -> @@ -536,7 +534,7 @@ module Util = | Fable.Null _ -> upcast NullLiteral () | Fable.UnitConstant -> upcast NullLiteral () // TODO: Use `void 0`? | Fable.BoolConstant x -> upcast BooleanLiteral (x) - | Fable.CharConstant x -> upcast NumericLiteral (float x) + | Fable.CharConstant x -> upcast StringLiteral (string x) | Fable.StringConstant x -> upcast StringLiteral (x) | Fable.NumberConstant (x,_) -> if x < 0. @@ -849,8 +847,8 @@ module Util = | Fable.Any -> upcast BooleanLiteral true | Fable.Unit -> upcast BinaryExpression(BinaryEqual, com.TransformAsExpr(ctx, expr), NullLiteral(), ?loc=range) | Fable.Boolean -> jsTypeof "boolean" expr - | Fable.String _ | Fable.EnumType(Fable.StringEnumType, _) -> jsTypeof "string" expr - | Fable.Number _ | Fable.Char | Fable.EnumType(Fable.NumberEnumType, _) -> jsTypeof "number" expr + | Fable.Char | Fable.String _ | Fable.EnumType(Fable.StringEnumType, _) -> jsTypeof "string" expr + | Fable.Number _ | Fable.EnumType(Fable.NumberEnumType, _) -> jsTypeof "number" expr | Fable.Regex -> jsInstanceof (Identifier "RegExp") expr // TODO: Fail for functions, arrays, tuples and list because we cannot check generics? | Fable.FunctionType _ -> jsTypeof "function" expr diff --git a/src/dotnet/Fable.Compiler/Transforms/Replacements.fs b/src/dotnet/Fable.Compiler/Transforms/Replacements.fs index 28aec81e8a..799eb71178 100644 --- a/src/dotnet/Fable.Compiler/Transforms/Replacements.fs +++ b/src/dotnet/Fable.Compiler/Transforms/Replacements.fs @@ -306,8 +306,8 @@ let createAtom (value: Expr) = let toChar (arg: Expr) = match arg.Type with - | String -> Helper.InstanceCall(arg, "charCodeAt", Char, [makeIntConst 0]) - | _ -> arg + | Char | String -> arg + | _ -> Helper.GlobalCall("String", Char, [arg], memb="fromCharCode") let toString com r (args: Expr list) = match args with @@ -316,8 +316,7 @@ let toString com r (args: Expr list) = |> addErrorAndReturnNull com r | head::tail -> match head.Type with - | String -> head - | Char -> Helper.GlobalCall("String", String, [head], memb="fromCharCode") + | Char | String -> head | Unit | Boolean | Array _ | Tuple _ | FunctionType _ | EnumType _ -> Helper.GlobalCall("String", String, [head]) | Builtin (BclInt64 | BclUInt64) -> Helper.CoreCall("Long", "toString", String, args) @@ -420,7 +419,8 @@ let toInt com r (round: bool) targetType (args: Expr list) = | Number Decimal -> "toDecimal" | _ -> failwithf "Unexpected non-number type %A" typeTo match sourceType with - | Char | EnumType(NumberEnumType,_) -> args.Head + | EnumType(NumberEnumType,_) -> args.Head + | Char -> Helper.InstanceCall(args.Head, "charCodeAt", targetType, [makeIntConst 0]) | String -> match targetType with | Builtin (BclInt64|BclUInt64 as kind) -> @@ -484,7 +484,7 @@ let listToArray com r t (li: Expr) = Helper.CoreCall("Array", "ofList", t, args, ?loc=r) let stringToCharArray t e = - Helper.CoreCall("String", "toCharArray", t, [e]) + Helper.InstanceCall(e, "split", t, [makeStrConst ""]) let enumerator2iterator (e: Expr) = Helper.CoreCall("Seq", "toIterator", e.Type, [e]) @@ -497,16 +497,11 @@ let toSeq r t (e: Expr) = DelayedResolution(kind, t, r) // Convert to array to get 16-bit code units, see #1279 | String -> stringToCharArray t e - | GenericParam _ -> - match t with - // Runtime check for generics upcasted to `char seq` - | DeclaredType(_, [Char]) -> Helper.CoreCall("String", "toCharIterable", t, [e]) - | _ -> e | _ -> e -let iterate r ident body xs = +let iterate r ident body (xs: Expr) = let f = Function(Delegate [ident], body, None) - Helper.CoreCall("Seq", "iterate", Unit, [f; xs], ?loc=r) + Helper.CoreCall("Seq", "iterate", Unit, [f; toSeq r xs.Type xs], ?loc=r) let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) argTypes genArgs = let (|CustomOp|_|) com ctx opName argTypes = @@ -718,8 +713,8 @@ let makeHashSet (com: ICompiler) r t sourceSeq = let getZero (com: ICompiler) (t: Type) = match t with - | String -> makeStrConst "" - | Char | Builtin BclTimeSpan -> makeIntConst 0 + | Char | String -> makeStrConst "" + | Builtin BclTimeSpan -> makeIntConst 0 | Builtin BclDateTime as t -> Helper.CoreCall("Date", "minValue", t, []) | Builtin BclDateTimeOffset as t -> Helper.CoreCall("DateOffset", "minValue", t, []) | Builtin (FSharpSet genArg) as t -> makeSet com None t "Empty" [] genArg @@ -1125,29 +1120,25 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> None let chars (com: ICompiler) (_: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = - let icall r args argTypes memb = + let icall r t args argTypes memb = match args, argTypes with | thisArg::args, _::argTypes -> - let info = argInfo (toString com None [thisArg] |> Some) args (Some argTypes) - instanceCall r String info (makeStrConst memb |> Some) |> toChar |> Some + let info = argInfo (Some thisArg) args (Some argTypes) + instanceCall r t info (makeStrConst memb |> Some) |> Some | _ -> None match i.CompiledName with - | "ToUpper" -> icall r args i.SignatureArgTypes "toLocaleUpperCase" - | "ToUpperInvariant" -> icall r args i.SignatureArgTypes "toUpperCase" - | "ToLower" -> icall r args i.SignatureArgTypes "toLocaleLowerCase" - | "ToLowerInvariant" -> icall r args i.SignatureArgTypes "toLowerCase" + | "ToUpper" -> icall r t args i.SignatureArgTypes "toLocaleUpperCase" + | "ToUpperInvariant" -> icall r t args i.SignatureArgTypes "toUpperCase" + | "ToLower" -> icall r t args i.SignatureArgTypes "toLocaleLowerCase" + | "ToLowerInvariant" -> icall r t args i.SignatureArgTypes "toLowerCase" | "ToString" -> toString com r args |> Some | "GetUnicodeCategory" | "IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsUpper" | "IsLower" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsWhiteSpace" - | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogate" -> - let args = - match args with - | [str; index] -> [Helper.InstanceCall(str, "charCodeAt", Char, [index])] - | _ -> args - Helper.CoreCall("Char", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some - | "IsSurrogatePair" | "Parse" -> - Helper.CoreCall("Char", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some + | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogate" | "IsSurrogatePair" + | "Parse" -> + let methName = Naming.lowerFirst i.CompiledName + Helper.CoreCall("Char", methName, t, args, i.SignatureArgTypes, ?loc=r) |> Some | _ -> None let implementedStringFunctions = @@ -1159,7 +1150,6 @@ let implementedStringFunctions = "Insert" "IsNullOrEmpty" "IsNullOrWhiteSpace" - "Join" "PadLeft" "PadRight" "Remove" @@ -1167,12 +1157,27 @@ let implementedStringFunctions = |] let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let join nonDelimiterArgType args = + let hasSpread = + match i.Spread, nonDelimiterArgType with + | SeqSpread, _ -> true + | _, EntFullName Types.ienumerableGeneric -> true + | _ -> false + Helper.CoreCall("String", "join", t, args, hasSpread=hasSpread, ?loc=r) match i.CompiledName, thisArg, args with - | ".ctor", _, _ -> - match List.head i.SignatureArgTypes with - | Char -> Helper.CoreCall("String", "fromChar", t, args, i.SignatureArgTypes, ?loc=r) |> Some - | Array _ -> Helper.CoreCall("String", "fromCharArray", t, args, i.SignatureArgTypes, ?loc=r) |> Some - | _ -> fsFormat com ctx r t i thisArg args + | ".ctor", _, fstArg::_ -> + match fstArg.Type with + | Char -> + match args with + | [_; _] -> emitJs r t args "Array($1 + 1).join($0)" |> Some // String(char, int) + | _ -> addErrorAndReturnNull com r "Unexpected arguments in System.String constructor." |> Some + | Array _ -> + match args with + | [_] -> emitJs r t args "$0.join('')" |> Some // String(char[]) + | [_; _; _] -> emitJs r t args "$0.join('').substr($1, $2)" |> Some // String(char[], int, int) + | _ -> addErrorAndReturnNull com r "Unexpected arguments in System.String constructor." |> Some + | _ -> + fsFormat com ctx r t i thisArg args | "get_Length", Some c, _ -> get r t c "length" |> Some | "get_Chars", Some c, _ -> Helper.CoreCall("String", "getCharAtIndex", t, args, i.SignatureArgTypes, c, ?loc=r) |> Some @@ -1199,13 +1204,11 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc=r) |> Some | ("IndexOf" | "LastIndexOf"), Some c, _ -> match args with + | [ExprType Char] | [ExprType String] + | [ExprType Char; ExprType(Number Int32)] | [ExprType String; ExprType(Number Int32)] -> Helper.InstanceCall(c, Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some - | [ExprType Char] - | [ExprType Char; ExprType(Number Int32)] -> - let args = match args with head::tail -> (toString com None [head])::tail | [] -> [] - Helper.InstanceCall(c, Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some | _ -> "The only extra argument accepted for String.IndexOf/LastIndexOf is startIndex." |> addErrorAndReturnNull com r |> Some | ("Trim" | "TrimStart" | "TrimEnd"), Some c, _ -> @@ -1227,7 +1230,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | [Value(CharConstant _) as separator] | [Value(StringConstant _) as separator] | [Value(NewArray(ArrayValues [separator],_))] -> - Helper.InstanceCall(c, "split", t, [toString com None [separator]]) |> Some + Helper.InstanceCall(c, "split", t, [separator]) |> Some | [arg1; ExprType(EnumType _) as arg2] -> let arg1 = match arg1.Type with @@ -1237,9 +1240,12 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.CoreCall("String", "split", t, c::args, ?loc=r) |> Some | args -> Helper.CoreCall("String", "split", t, args, i.SignatureArgTypes, ?thisArg=thisArg, ?loc=r) |> Some + | "Join", None, [_delimiter; _parts; ExprType(Number _); ExprType(Number _)] -> + Helper.CoreCall("String", "joinWithIndices", t, args, ?loc=r) |> Some + | "Join", None, _ -> + join (List.item 1 i.SignatureArgTypes) args |> Some | "Concat", None, _ -> - // TODO: String.Concat can also accept non-string arguments - Helper.CoreCall("String", "join", t, (makeStrConst "")::args, ?loc=r) |> Some + join (List.head i.SignatureArgTypes) ((makeStrConst "")::args) |> Some | "CompareOrdinal", None, _ -> Helper.CoreCall("String", "compareOrdinal", t, args, ?loc=r) |> Some | Patterns.SetContains implementedStringFunctions, thisArg, args -> @@ -1252,11 +1258,15 @@ let stringModule (com: ICompiler) (_: Context) r t (i: CallInfo) (_: Expr option match i.CompiledName, args with | "Length", [arg] -> get r t arg "length" |> Some | ("Iterate" | "IterateIndexed" | "ForAll" | "Exists"), _ -> - // Cast the string to array, see #1279 + // Cast the string to char[], see #1279 let args = args |> List.replaceLast (fun e -> stringToCharArray e.Type e) - Helper.CoreCall("Array", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some + Helper.CoreCall("Seq", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some + | ("Map" | "MapIndexed" | "Collect"), _ -> + // Cast the string to char[], see #1279 let args = args |> List.replaceLast (fun e -> stringToCharArray e.Type e) + let name = Naming.lowerFirst i.CompiledName + emitJs r t [Helper.CoreCall("Seq", name, Any, args)] "Array.from($0).join('')" |> Some | "Concat", _ -> - Helper.CoreCall("String", "join", t, args, ?loc=r) |> Some + Helper.CoreCall("String", "join", t, args, hasSpread=true, ?loc=r) |> Some // Rest of StringModule methods | meth, args -> Helper.CoreCall("String", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc=r) |> Some @@ -1734,8 +1744,7 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr Helper.CoreCall("Util", "downcast", t, [arg]) |> Some | _ -> Some arg | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args - | "GetString", _, [str; idx] -> - Helper.CoreCall("String", "getCharAtIndex", t, [idx], i.SignatureArgTypes, str, ?loc=r) |> Some + | "GetString", _, [ar; idx] | "GetArray", _, [ar; idx] -> getExpr r t ar idx |> Some | "SetArray", _, [ar; idx; value] -> Set(ar, ExprSet idx, value, r) |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ar; lower; upper] -> @@ -1926,11 +1935,11 @@ let bitConvert (_: ICompiler) (_: Context) r (_: Type) (i: CallInfo) (_: Expr op if i.CompiledName = "GetBytes" then match args.Head.Type with | Boolean -> "getBytesBoolean" - | String -> "getBytesChar" + | Char | String -> "getBytesChar" | Number Int16 -> "getBytesInt16" | Number Int32 -> "getBytesInt32" | Builtin BclInt64 -> "getBytesInt64" - | Char | Number UInt16 -> "getBytesUInt16" + | Number UInt16 -> "getBytesUInt16" | Builtin BclUInt64 -> "getBytesUInt64" | Number UInt32 -> "getBytesUInt32" | Number Float32 -> "getBytesSingle" @@ -2158,7 +2167,8 @@ let encoding (_: ICompiler) (_: Context) r t (i: CallInfo) (thisArg: Expr option | ("get_Unicode" | "get_UTF8"), _, _ -> Helper.CoreCall("Encoding", i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some | ("GetBytes" | "GetString"), Some callee, (1 | 3) -> - Helper.InstanceCall(callee, Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc=r) |> Some + let meth = Naming.lowerFirst i.CompiledName + Helper.InstanceCall(callee, meth, t, args, i.SignatureArgTypes, ?loc=r) |> Some | _ -> None let enumerables (_: ICompiler) (_: Context) r t (i: CallInfo) (thisArg: Expr option) (_: Expr list) = diff --git a/src/js/fable-core/BitConverter.ts b/src/js/fable-core/BitConverter.ts index da27607954..b13ba73ec3 100644 --- a/src/js/fable-core/BitConverter.ts +++ b/src/js/fable-core/BitConverter.ts @@ -10,7 +10,7 @@ export function getBytesBoolean(value: boolean) { new DataView(bytes.buffer).setUint8(0, value ? 1 : 0); return bytes; } -export function getBytesString(value: string) { +export function getBytesChar(value: string) { const bytes = new Uint8Array(2); new DataView(bytes.buffer).setUint16(0, value.charCodeAt(0), littleEndian); return bytes; @@ -74,8 +74,10 @@ export function toBoolean(bytes: Uint8Array, offset: number): boolean { return new DataView(bytes.buffer).getUint8(offset) === 1 ? true : false; } export function toChar(bytes: Uint8Array, offset: number) { - return new DataView(bytes.buffer).getUint16(offset, littleEndian); + const code = new DataView(bytes.buffer).getUint16(offset, littleEndian); + return String.fromCharCode(code); } + export function toInt16(bytes: Uint8Array, offset: number) { return new DataView(bytes.buffer).getInt16(offset, littleEndian); } diff --git a/src/js/fable-core/Char.ts b/src/js/fable-core/Char.ts index c1ff6583bc..9933effd40 100644 --- a/src/js/fable-core/Char.ts +++ b/src/js/fable-core/Char.ts @@ -32,7 +32,8 @@ function getCategory() { categories[i / 2] = unicodeDeltas[i + 1]; } // binary search in unicode ranges - return (cp: number) => { + return (s: string, index?: number) => { + const cp = s.charCodeAt(index || 0); let hi = codepoints.length; let lo = 0; while (hi - lo > 1) { @@ -123,83 +124,86 @@ const isWhiteSpaceMask = 0 export const getUnicodeCategory = getCategory(); -export function isControl(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isControl(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isControlMask) !== 0; } -export function isDigit(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isDigit(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isDigitMask) !== 0; } -export function isLetter(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isLetter(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isLetterMask) !== 0; } -export function isLetterOrDigit(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isLetterOrDigit(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isLetterOrDigitMask) !== 0; } -export function isUpper(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isUpper(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isUpperMask) !== 0; } -export function isLower(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isLower(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isLowerMask) !== 0; } -export function isNumber(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isNumber(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isNumberMask) !== 0; } -export function isPunctuation(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isPunctuation(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isPunctuationMask) !== 0; } -export function isSeparator(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isSeparator(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isSeparatorMask) !== 0; } -export function isSymbol(cp: number) { - const test = 1 << getUnicodeCategory(cp); +export function isSymbol(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); return (test & isSymbolMask) !== 0; } -export function isWhiteSpace(cp: number) { - const test = 1 << getUnicodeCategory(cp); - return ((test & isWhiteSpaceMask) !== 0) - || (0x09 <= cp && cp <= 0x0D) || cp === 0x85 || cp === 0xA0; +export function isWhiteSpace(s: string, index?: number) { + const test = 1 << getUnicodeCategory(s, index); + if ((test & isWhiteSpaceMask) !== 0) { + return true; + } + const cp = s.charCodeAt(index || 0); + return (0x09 <= cp && cp <= 0x0D) || cp === 0x85 || cp === 0xA0; } -export function isHighSurrogate(cp: number) { - return 0xD800 <= cp && cp <= 0xDBFF; +export function isHighSurrogate(s: string, index?: number) { + return /[\uD800-\uDBFF]/.test(s.charAt(index || 0)); } -export function isLowSurrogate(cp: number) { - return 0xDC00 <= cp && cp <= 0xDFFF; +export function isLowSurrogate(s: string, index?: number) { + return /[\uDC00-\uDFFF]/.test(s.charAt(index || 0)); } -export function isSurrogate(cp: number) { - return 0xD800 <= cp && cp <= 0xDFFF; +export function isSurrogate(s: string, index?: number) { + return /[\uD800-\uDFFF]/.test(s.charAt(index || 0)); } -export function isSurrogatePair(s: string|number, index: number) { - return typeof s === "string" - ? isHighSurrogate(s.charCodeAt(index)) && isLowSurrogate(s.charCodeAt(index + 1)) +export function isSurrogatePair(s: string, index: string|number) { + return typeof index === "number" + ? isHighSurrogate(s, index) && isLowSurrogate(s, index + 1) : isHighSurrogate(s) && isLowSurrogate(index); -} + } export function parse(input: string) { if (input.length === 1) { - return input.charCodeAt(0); + return input[0]; } else { throw Error("String must be exactly one character long."); } diff --git a/src/js/fable-core/Seq.ts b/src/js/fable-core/Seq.ts index 1d77ae3a18..11d06226d9 100644 --- a/src/js/fable-core/Seq.ts +++ b/src/js/fable-core/Seq.ts @@ -528,8 +528,8 @@ export function rangeStep(first: number, step: number, last: number) { return delay(() => unfold((x) => step > 0 && x <= last || step < 0 && x >= last ? [x, x + step] : null, first)); } -export function rangeChar(first: number, last: number) { - return delay(() => unfold((x) => x <= last ? [x, x + 1] : null, first)); +export function rangeChar(first: string, last: string) { + return delay(() => unfold((x) => x <= last ? [x, String.fromCharCode(x.charCodeAt(0) + 1)] : null, first)); } export function range(first: number, last: number) { diff --git a/src/js/fable-core/String.ts b/src/js/fable-core/String.ts index b926466246..8412747710 100644 --- a/src/js/fable-core/String.ts +++ b/src/js/fable-core/String.ts @@ -1,36 +1,7 @@ import { toString as dateToString } from "./Date"; import Long, { fromBytes as longFromBytes, toBytes as longToBytes, toString as longToString } from "./Long"; import { escape } from "./RegExp"; -import { isArray, toString } from "./Util"; - -function asString(x: string|number): string { - return typeof x === "number" ? String.fromCharCode(x) : x; -} - -export function toCharArray(str: string): Uint16Array { - const len = str.length; - const ar = new Uint16Array(len); - for (let i = 0; i < len; i++) { - ar[i] = str.charCodeAt(i); - } - return ar; -} - -export function toCharIterable(source: any): Iterable { - return typeof source === "string" ? toCharArray(source) : source; -} - -export function fromCharArray(ar: Uint16Array|number[], startIndex?: number, count?: number): string { - const ar2 = startIndex == null - ? ar - // If count arg is undefined, startIndex becomes the count and startIndex is 0 - : (count == null ? ar.slice(0, startIndex) : ar.slice(startIndex, startIndex + count)); - return String.fromCharCode(...ar2); -} - -export function fromChar(char: number, count: number): string { - return String.fromCharCode(char).repeat(count); -} +import { toString } from "./Util"; const fsFormatRegExp = /(^|[^%])%([0+ ]*)(-?\d+)?(?:\.(\d+))?(\w)/; const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:(.+?))?\}/g; @@ -96,7 +67,7 @@ export function startsWith(str: string, pattern: string, ic: number) { return false; } -export function indexOfAny(str: string, anyOf: number[], ...args: number[]) { +export function indexOfAny(str: string, anyOf: string[], ...args: number[]) { if (str == null || str === "") { return -1; } @@ -113,7 +84,7 @@ export function indexOfAny(str: string, anyOf: number[], ...args: number[]) { } str = str.substr(startIndex, length); for (const c of anyOf) { - const index = str.indexOf(String.fromCharCode(c)); + const index = str.indexOf(c); if (index > -1) { return index + startIndex; } @@ -183,7 +154,7 @@ function formatOnce(str2: any, rep: any) { const plusPrefix = flags.indexOf("+") >= 0 && parseInt(rep, 10) >= 0; pad = parseInt(pad, 10); if (!isNaN(pad)) { - const ch = pad >= 0 && flags.indexOf("0") >= 0 ? 48 : 32; // "0" : " "; + const ch = pad >= 0 && flags.indexOf("0") >= 0 ? "0" : " "; rep = padLeft(String(rep), Math.abs(pad) - (plusPrefix ? 1 : 0), ch, pad < 0); } const once = prefix + (plusPrefix ? "+" + rep : rep); @@ -222,7 +193,7 @@ export function format(str: string, ...args: any[]) { return str.replace(formatRegExp, (match: any, idx: any, pad: any, pattern: any) => { let rep = args[idx]; - let padSymbol = 32; // " "; + let padSymbol = " "; if (typeof rep === "number" || rep instanceof Long) { switch ((pattern || "").substring(0, 1)) { case "f": case "F": @@ -249,7 +220,7 @@ export function format(str: string, ...args: any[]) { rep = rep.toFixed(decs = m[2].length - 1); } pad = "," + (m[1].length + (decs ? decs + 1 : 0)).toString(); - padSymbol = 48; // "0"; + padSymbol = "0"; } else if (pattern) { rep = pattern; } @@ -296,23 +267,21 @@ export function isNullOrWhiteSpace(str: string | any) { return typeof str !== "string" || /^\s*$/.test(str); } -export function join(delimiter: string|number, xs: ArrayLike) { - let xs2 = typeof xs === "string" ? [xs] : xs as any; - const len = arguments.length; - if (len > 2) { - xs2 = Array(len - 1); - for (let key = 1; key < len; key++) { - xs2[key - 1] = arguments[key]; - } - } else if (!Array.isArray(xs2)) { - xs2 = Array.from(xs2); +export function join(delimiter: string, ...xs: any[]): string { + return xs.map((x) => String(x)).join(delimiter); +} + +export function joinWithIndices(delimiter: string, xs: string[], startIndex: number, count: number) { + const endIndexPlusOne = startIndex + count; + if (endIndexPlusOne > xs.length) { + throw new Error("Index and count must refer to a location within the buffer."); } - return xs2.map((x: string) => toString(x)).join(asString(delimiter)); + return join(delimiter, ...xs.slice(startIndex, endIndexPlusOne)); } /** Validates UUID as specified in RFC4122 (versions 1-5). Trims braces. */ export function validateGuid(str: string, doNotThrow?: boolean): string|[boolean, string] { - const trimmed = trim(str, 123, 125); // "{","}" + const trimmed = trim(str, "{", "}"); if (guidRegex.test(trimmed)) { return doNotThrow ? [true, trimmed] : trimmed; } else if (doNotThrow) { @@ -424,8 +393,8 @@ export function fromBase64String(b64Encoded: string) { return bytes; } -export function padLeft(str: string, len: number, char?: number, isRight?: boolean) { - const ch = char == null ? " " : String.fromCharCode(char); +export function padLeft(str: string, len: number, ch?: string, isRight?: boolean) { + ch = ch || " "; len = len - str.length; for (let i = 0; i < len; i++) { str = isRight ? str + ch : ch + str; @@ -433,8 +402,8 @@ export function padLeft(str: string, len: number, char?: number, isRight?: boole return str; } -export function padRight(str: string, len: number, char?: number) { - return padLeft(str, len, char, true); +export function padRight(str: string, len: number, ch?: string) { + return padLeft(str, len, ch, true); } export function remove(str: string, startIndex: number, count?: number) { @@ -447,8 +416,8 @@ export function remove(str: string, startIndex: number, count?: number) { return str.slice(0, startIndex) + (typeof count === "number" ? str.substr(startIndex + count) : ""); } -export function replace(str: string, search: string|number, replace: string|number) { - return str.replace(new RegExp(escape(asString(search)), "g"), asString(replace)); +export function replace(str: string, search: string, replace: string) { + return str.replace(new RegExp(escape(search), "g"), replace); } export function replicate(n: number, x: string) { @@ -459,10 +428,10 @@ export function getCharAtIndex(input: string, index: number) { if (index < 0 || index >= input.length) { throw new Error("Index was outside the bounds of the array."); } - return input.charCodeAt(index); + return input[index]; } -export function split(str: string, splitters: Array, count?: number, removeEmpty?: number) { +export function split(str: string, splitters: string[], count?: number, removeEmpty?: number) { count = typeof count === "number" ? count : null; removeEmpty = typeof removeEmpty === "number" ? removeEmpty : null; if (count < 0) { @@ -471,9 +440,9 @@ export function split(str: string, splitters: Array, count?: numb if (count === 0) { return []; } - if (!isArray(splitters)) { + if (!Array.isArray(splitters)) { if (removeEmpty === 0) { - return str.split(asString(splitters as any), count); + return str.split(splitters, count); } const len = arguments.length; splitters = Array(len - 1); @@ -481,19 +450,11 @@ export function split(str: string, splitters: Array, count?: numb splitters[key - 1] = arguments[key]; } } - let pattern = " "; - const splittersLen = splitters.length; - if (splittersLen > 0) { - const temp = new Array(splittersLen); - // splitters may be an Uint16TypedArray of chars, we cannot use .map - for (let i = 0; i < splittersLen; i++) { - temp[i] = escape(asString(splitters[i])); - } - pattern = temp.join("|"); - } - const reg = new RegExp(pattern, "g"); - const splits: string[] = []; + splitters = splitters.map((x) => escape(x)); + splitters = splitters.length > 0 ? splitters : [" "]; let i = 0; + const splits: string[] = []; + const reg = new RegExp(splitters.join("|"), "g"); while (count == null || count > 1) { const m = reg.exec(str); if (m === null) { break; } @@ -509,43 +470,26 @@ export function split(str: string, splitters: Array, count?: numb return splits; } -export function trim(str: string, ...chars: number[]) { +export function trim(str: string, ...chars: string[]) { if (chars.length === 0) { return str.trim(); } - const pattern = "[" + escape(String.fromCharCode(...chars)) + "]+"; + const pattern = "[" + escape(chars.join("")) + "]+"; return str.replace(new RegExp("^" + pattern), "").replace(new RegExp(pattern + "$"), ""); } -export function trimStart(str: string, ...chars: number[]) { +export function trimStart(str: string, ...chars: string[]) { return chars.length === 0 ? (str as any).trimStart() - : str.replace(new RegExp("^[" + escape(String.fromCharCode(...chars)) + "]+"), ""); + : str.replace(new RegExp("^[" + escape(chars.join("")) + "]+"), ""); } -export function trimEnd(str: string, ...chars: number[]) { +export function trimEnd(str: string, ...chars: string[]) { return chars.length === 0 ? (str as any).trimEnd() - : str.replace(new RegExp("[" + escape(String.fromCharCode(...chars)) + "]+$"), ""); + : str.replace(new RegExp("[" + escape(chars.join("")) + "]+$"), ""); } -export function filter(pred: (c: number) => boolean, str: string) { - return fromCharArray(toCharArray(str).filter(pred)); -} - -export function map(f: (char: number) => number, str: string) { - return fromCharArray(toCharArray(str).map(f)); -} - -export function mapIndexed(f: (index: number, char: number) => number, str: string) { - return fromCharArray(toCharArray(str).map((c, i) => f(i, c))); -} - -export function collect(f: (char: number) => string, str: string) { - const ar1 = toCharArray(str); - const ar2 = new Array(ar1.length); - for (let i = 0; i < ar1.length; i++) { - ar2[i] = f(ar1[i]); - } - return ar2.join(""); +export function filter(pred: (i: string) => boolean, x: string) { + return x.split("").filter(pred).join(""); } diff --git a/tests/Main/CharTests.fs b/tests/Main/CharTests.fs index 7019d50a0e..aa98c16d38 100644 --- a/tests/Main/CharTests.fs +++ b/tests/Main/CharTests.fs @@ -37,7 +37,7 @@ let tests = Char.GetUnicodeCategory(str,2) |> int |> equal 8 //UnicodeCategory.DecimalDigitNumber testCase "Char.IsControl works" <| fun () -> - Char.IsControl('a') |> equal false + Char.IsControl('a') |> equal false Char.IsControl('\u0000') |> equal true Char.IsControl('\u001F') |> equal true Char.IsControl('\u007F') |> equal true @@ -142,6 +142,11 @@ let tests = Char.IsWhiteSpace(' ') |> equal true Char.IsWhiteSpace('\n') |> equal true Char.IsWhiteSpace('\t') |> equal true + Char.IsWhiteSpace('\009') |> equal true + Char.IsWhiteSpace('\013') |> equal true + Char.IsWhiteSpace('\133') |> equal true + Char.IsWhiteSpace('\160') |> equal true + Char.IsWhiteSpace('-') |> equal false testCase "Char.IsWhitespace works with two args" <| fun () -> let input = " \r" diff --git a/tests/Main/StringTests.fs b/tests/Main/StringTests.fs index d863d47d06..af1967fb30 100644 --- a/tests/Main/StringTests.fs +++ b/tests/Main/StringTests.fs @@ -518,16 +518,42 @@ let tests = arr |> Array.map (fun _ -> 1) |> Array.sum |> equal arr.Length + testCase "String enumeration handles surrogates pairs" <| fun () -> // See #1279 + let unicodeString = ".\U0001f404." + unicodeString |> List.ofSeq |> Seq.length |> equal 4 + String.length unicodeString |> equal 4 + let mutable len = 0 + for i in unicodeString do + len <- len + 1 + equal 4 len + testCase "String.Join works" <| fun () -> String.Join("--", "a", "b", "c") |> equal "a--b--c" String.Join("--", seq { yield "a"; yield "b"; yield "c" }) |> equal "a--b--c" - // TODO!!! - // String.Join("--", [|3I; 5I|]) - // |> equal "3--5" - // String.Join("--", 3I, 5I) - // |> equal "3--5" + + testCase "String.Join with indices works" <| fun () -> + String.Join("**", [|"a"; "b"; "c"; "d"|], 1, 2) + |> equal "b**c" + String.Join("*", [|"a"; "b"; "c"; "d"|], 1, 3) + |> equal "b*c*d" + + testCase "String.Join works with chars" <| fun () -> // See #1524 + String.Join("--", 'a', 'b', 'c') + |> equal "a--b--c" + String.Join("--", seq { yield 'a'; yield 'b'; yield 'c' }) + |> equal "a--b--c" + [0..10] + |> List.map (fun _ -> '*') + |> fun chars -> String.Join("", chars) + |> equal "***********" + + testCase "String.Join with big integers works" <| fun () -> + String.Join("--", [|3I; 5I|]) + |> equal "3--5" + String.Join("--", 3I, 5I) + |> equal "3--5" testCase "String.Join with single argument works" <| fun () -> // See #1182 String.Join(",", "abc") |> equal "abc"