Skip to content

Let 'string' function return "None" if input is None #9595

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions src/fsharp/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -781,9 +781,9 @@ namespace Microsoft.FSharp.Core

let inline anyToString nullStr x =
match box x with
| :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture)
| null -> nullStr
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
| obj -> obj.ToString()
| _ -> x.ToString()

let anyToStringShowingNull x = anyToString "null" x

Expand Down Expand Up @@ -3746,6 +3746,8 @@ namespace Microsoft.FSharp.Core
open System.Diagnostics
open System.Collections.Generic
open System.Globalization
open System.Text
open System.Numerics
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.LanguagePrimitives
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
Expand Down Expand Up @@ -4457,23 +4459,52 @@ namespace Microsoft.FSharp.Core
when ^T : ^T = (^T : (static member op_Explicit: ^T -> nativeint) (value))

[<CompiledName("ToString")>]
let inline string (value: ^T) =
let string (value: 'T) =
anyToString "" value
// since we have static optimization conditionals for ints below, we need to special-case Enums.
// This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1")
when ^T struct = anyToString "" value
when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture)
when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : nativeint = (# "" value : nativeint #).ToString()
when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : unativeint = (# "" value : unativeint #).ToString()
when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture)
when 'T : string = (# "" value : string #) // force no-op

// Using 'let x = (# ... #) in x.ToString()' leads to better IL, without it, an extra stloc and ldloca.s (get address-of)
// gets emitted, which are unnecessary. With it, the extra address-of-variable is not created
when 'T : float = let x = (# "" value : float #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : float32 = let x = (# "" value : float32 #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : decimal = let x = (# "" value : decimal #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : BigInteger = let x = (# "" value : BigInteger #) in x.ToString(null, CultureInfo.InvariantCulture)

// no IFormattable
when 'T : char = let x = (# "" value : char #) in x.ToString()
when 'T : bool = let x = (# "" value : bool #) in x.ToString()

// For the int-types:
// It is not possible to distinguish statically between Enum and (any type of) int.
// This way we'll print their symbolic value, as opposed to their integral one
// E.g.: 'string ConsoleKey.Backspace' gives "Backspace", rather than "8")
when 'T : sbyte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : byte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : uint16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : uint32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : uint64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)

// native ints cannot be used for enums, and do not implement IFormattable
when 'T : nativeint = (# "" value : nativeint #).ToString()
when 'T : unativeint = (# "" value : unativeint #).ToString()

// other common mscorlib System struct types
when 'T : DateTime = let x = (# "" value : DateTime #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : DateTimeOffset = let x = (# "" value : DateTimeOffset #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : TimeSpan = let x = (# "" value : TimeSpan #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : Guid = let x = (# "" value : Guid #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T struct =
match box value with
| :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture)
| _ -> value.ToString()

// other commmon mscorlib reference types
when 'T : StringBuilder = let x = (# "" value : StringBuilder #) in x.ToString()
when 'T : IFormattable = let x = (# "" value : IFormattable #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : option<_> = let x = (# "" value : option<_> #) in match x with None -> "None" | _ -> x.ToString()

[<NoDynamicInvocation(isLegacy=true)>]
[<CompiledName("ToChar")>]
Expand Down
6 changes: 3 additions & 3 deletions src/fsharp/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -2824,12 +2824,12 @@ namespace Microsoft.FSharp.Core

/// <summary>Converts the argument to a string using <c>ToString</c>.</summary>
///
/// <remarks>For standard integer and floating point values the <c>ToString</c> conversion
/// uses <c>CultureInfo.InvariantCulture</c>. </remarks>
/// <remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c>
/// <c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
/// <param name="value">The input value.</param>
/// <returns>The converted string.</returns>
[<CompiledName("ToString")>]
val inline string : value:^T -> string
val string : value:'T -> string

/// <summary>Converts the argument to System.Decimal using a direct conversion for all
/// primitive numeric types. For strings, the input is converted using <c>UInt64.Parse()</c>
Expand Down
60 changes: 60 additions & 0 deletions tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ namespace FSharp.Core.UnitTests.Operators
open System
open FSharp.Core.UnitTests.LibraryTestFx
open NUnit.Framework
open System.Globalization
open System.Threading

/// If this type compiles without error it is correct
/// Wrong if you see: FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope.
type TestFs0670Error<'T> =
| TestFs0670Error of 'T
override this.ToString() =
match this with
| TestFs0670Error x ->
// This used to raise FS0670 because the type is generic, and 'string' was inline
// See: https://github.com/dotnet/fsharp/issues/7958
Operators.string x

[<TestFixture>]
type OperatorsModule2() =
Expand Down Expand Up @@ -718,6 +731,53 @@ type OperatorsModule2() =
// reference type
let result = Operators.string "ABC"
Assert.AreEqual("ABC", result)

// reference type without a `ToString()` overload
let result = Operators.string (obj())
Assert.AreEqual("System.Object", result)

let result = Operators.string 1un
Assert.AreEqual("1", result)

let result = Operators.string (obj())
Assert.AreEqual("System.Object", result)

let result = Operators.string 123.456M
Assert.AreEqual("123.456", result)

// Following tests ensure that InvariantCulture is used if type implements IFormattable

// safe current culture, then switch culture
let currentCI = Thread.CurrentThread.CurrentCulture
Thread.CurrentThread.CurrentCulture <- CultureInfo.GetCultureInfo("de-DE")

// make sure the culture switch happened, and verify
let wrongResult = 123.456M.ToString()
Assert.AreEqual("123,456", wrongResult)

// test that culture has no influence on decimals with `string`
let correctResult = Operators.string 123.456M
Assert.AreEqual("123.456", correctResult)

// make sure that the German culture is indeed selected for DateTime
let dttm = DateTime(2020, 6, 23)
let wrongResult = dttm.ToString()
Assert.AreEqual("23.06.2020 00:00:00", wrongResult)

// test that culture has no influence on DateTime types when used with `string`
let correctResult = Operators.string dttm
Assert.AreEqual("06/23/2020 00:00:00", correctResult)

// reset the culture
Thread.CurrentThread.CurrentCulture <- currentCI

[<Test>]
member _.``string: don't raise FS0670 anymore``() =
// The type used here, when compiled, should not raise this error:
// "FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope."
// See: https://github.com/dotnet/fsharp/issues/7958
let result = TestFs0670Error 32uy |> Operators.string
Assert.AreEqual("32", result)

[<Test>]
member _.tan() =
Expand Down
11 changes: 6 additions & 5 deletions tests/fsharp/Compiler/Language/DefaultInterfaceMemberTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -813,15 +813,15 @@ type Test () =
member __.M(_x: int) = Console.Write("InTest")

member __.M<'Item> (x: int, y: 'Item) =
Console.Write(string x)
Console.Write(x.ToString())
Console.Write(y.ToString ())

member __.M<'TTT> (x: 'TTT) =
Console.Write(x.ToString ())

member __.M (x: int, text: string) =
Console.Write("ABC")
Console.Write(string x)
Console.Write(x.ToString())
Console.Write(text)

member __.M<'U> (_x: 'U, _y: int) = ()
Expand Down Expand Up @@ -1166,7 +1166,8 @@ type Test () =
let main _ =
let x = Test () :> I1
let y = Test () :> I2
Console.Write(string (x + y))
let result = x + y
Console.Write(result.ToString())
0
"""

Expand Down Expand Up @@ -4229,15 +4230,15 @@ type Test () =
member __.M(_x: int) = Console.Write("InTest")

member __.M<'Item> (x: int, y: 'Item) =
Console.Write(string x)
Console.Write(x.ToString())
Console.Write(y.ToString ())

member __.M<'TTT> (x: 'TTT) =
Console.Write(x.ToString ())

member __.M (x: int, text: string) =
Console.Write("ABC")
Console.Write(string x)
Console.Write(x.ToString())
Console.Write(text)

type Test2 () =
Expand Down