# Convert

> Explicit conversion between types — number to text, text to number, a Guid to its text form. Osy# will not convert silently where information could be lost, so you say it outright.

<!-- id: function-convert · area: function · stability: stable · html: https://osysharp.com/reference/function/convert/ -->

## Summary        {#summary}
`Convert.*` converts between types explicitly. Osy# does not convert silently where information could be lost — a
`decimal` does not quietly become an `int`, dropping the pence — so where a conversion is what you want, you write it.

## Signature      {#signature}
```osy syntax
Convert.ToInt(<value>)       // → int      (32-bit; truncates toward zero)
Convert.ToInt64(<value>)     // → long     (64-bit; truncates toward zero)
Convert.ToDecimal(<value>)   // → decimal
Convert.ToDouble(<value>)    // → double   (float64)
Convert.ToString(<value>)    // → string   (numbers, Guids, bools …)
Convert.ToBool(<value>)      // → bool

// C#'s own spellings work too — `Convert.ToInt32` IS `Convert.ToInt`, and `Convert.ToBoolean` IS
// `Convert.ToBool`. The same arms, so they behave identically; write whichever you reach for.
Convert.ToInt32(<value>)     // → int
Convert.ToBoolean(<value>)   // → bool
```

## Description    {#description}

### `Convert.*` or a cast?   {#convert-or-cast}
Both narrow, and they are not interchangeable. **Between two NUMBERS, reach for the cast** — `(int)x` — which is the
C# spelling and reads at a glance; see [(int)x — casts](https://osysharp.com/reference/function/cast/). Reach for `Convert.*` when the input might not be a number
at all, or when the output is text:

| | |
|---|---|
| `(int)amount` | a numeric conversion. Truncates toward zero, and **fails** if the value does not fit |
| `Convert.ToInt(s)` | a **parse**. Reads an integer out of text, and answers `0` for text it cannot read |
| `Convert.ToString(v)` | renders a value as text |

The difference in the failure is the reason they are separate: a cast that cannot produce a value stops, while a
parse of user-supplied text answers `0` and carries on — which is right for a form field and wrong for arithmetic.

> ⚠ **This is where Osy# and C# differ, and it is the difference most likely to cost you.** C#'s
> `Convert.ToInt32("abc")` throws `FormatException`. This answers **0** — no error, no null, a number that is
> wrong. So `osy lint` refuses `Convert.To*` over **text** at MUST tier
> (`correctness-parse-that-answers-zero`): say which you mean, and say it where the reader can see it.
>
> ```osy syntax
> if (decimal.TryParse(entered, out var cap)) { … } else { … }   // the C# spelling, and it compiles here
> ```
>
> A **numeric** conversion — `Convert.ToInt(Math.Floor(x))` — is untouched and behaves exactly as C# does. The
> lenient parse is still here and still useful; it just has to be asked for out loud.

### Narrowing is explicit   {#narrowing}
Widening happens on its own — an `int` is usable where a `decimal` is expected, because nothing is lost. Going the
other way loses the fraction, so you must say so:

```osy title="decimal to int, on purpose" test app=function-convert
int WholeUnits(decimal amount) {
  return Convert.ToInt(Math.Floor(amount));   // decide the rounding, THEN narrow
}
```

Note the `Math.Floor` first. `Convert.ToInt` truncates toward zero, which is a rounding decision — and a rounding
decision you make by accident is a rounding bug. Choose it deliberately: `Math.Floor`, `Math.Ceiling`, or
`Math.Round`.

**`Convert.ToInt64`** is the same narrowing to a 64-bit **`long`** instead of a 32-bit `int` — reach for it when the
value can exceed ±2.1 billion (an id, a byte count, a running total of small amounts). It truncates toward zero
exactly as `Convert.ToInt` does, so the same "round on purpose first" rule applies. See [long](https://osysharp.com/reference/types/long/) for what a
`long` is and why its range matters.

```osy title="decimal to long, on purpose" test app=function-convert
long TotalCents(decimal amount) {
  return Convert.ToInt64(Math.Round(amount * 100m));   // money as whole cents, 64-bit headroom
}
```

### How do I get a `double` for non-money maths?   {#to-double}
`Convert.ToDouble` is the float64 front door. Reach for it when the value in hand is a `decimal` and the arithmetic
downstream is not money — geometry, physics, a per-frame camera. `Math.Round`, `Math.Floor`, `Math.Ceiling` and
`Math.Truncate` all answer a **`decimal`** when given one, so this is the conversion back:

```osy title="a rounded decimal, back to float64" test app=function-convert
double SnappedTo(decimal value, decimal step) {
  return Convert.ToDouble(Math.Round(value / step) * step);
}
```

It coerces like its siblings: text is **parsed** (`0` for text it cannot read), `null` is `0`, `true` is `1`. Two
things are true of a `double` that are not true of an `int` or a `decimal`, and both show up here:

| | |
|---|---|
| `Convert.ToDouble("NaN")` · `("Infinity")` · `("-Infinity")` | those are values a `double` HAS, so the text reads (any casing, optional sign) |
| `Convert.ToDouble("1e400")` | `+Infinity` — an out-of-range magnitude is not a parse failure for this type (and `"1e-400"` is `0`) |

⚠ A `double` is **not** money. It cannot hold `0.1` exactly, so converting a price to one and back loses the pence
silently — see [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) for which type a value should have been in the first place.

### What counts as a number   {#grammar}
Every `Convert.To*` reads text the same way, so a string that parses for one parses for all of them. The accepted
shape is:

```text
[white space] [+ or -] digits[,digits…][.digits] [e or E [+ or -] digits] [white space]
```

| | |
|---|---|
| `"1,000"` · `"12,34"` · `"100,"` | group separators are accepted, and their PLACEMENT is not checked |
| `" 42 "` · `"\t42\n"` | a space, tab, newline, vertical tab, form feed or carriage return may pad it |
| `"1e3"` · `"1.5e+2"` · `".5"` · `"1."` | an exponent, a bare fraction and an empty fraction are all fine |
| `",100"` · `"1.0,5"` | ✗ a **leading** separator, or one in the **fraction** |
| `"(5)"` · `"5-"` · `"¤5"` | ✗ accounting parentheses, a **trailing** sign, a currency symbol |
| `" 42"` (non-breaking space) | ✗ a non-breaking space is not white space to a number |
| `"0x1F"` · `"nope"` · `""` | ✗ — and the answer is `0`, never an error and never `NaN` |

The exact same grammar runs in the browser and inside a query the database executes, so a conversion answers the same
thing wherever the expression happens to run. `Convert.ToDouble` reads three more forms, because a `double` has values
the other types do not: `"NaN"`, `"Infinity"` and `"-Infinity"`, in any casing.

An integer conversion additionally refuses a **non-zero fraction** — `Convert.ToInt("42.9")` is `0`, while
`Convert.ToInt("42.0")` is `42`. If you want the number rounded, round it: `Convert.ToInt(Math.Floor(x))`.

### How do I turn a number or a `Guid` into text?   {#to-string}
```osy title="numbers and ids as text" test app=function-convert
string Describe(int count, Guid id) {
  return "count=" + Convert.ToString(count) + " id=" + Convert.ToString(id);
}
```

For building a sentence out of several values, [string interpolation](https://osysharp.com/reference/function/string-interpolation/) reads better —
`$"count={count}"` converts for you.

### Money stays decimal   {#money}
Resist converting money to `int` or back. If you find yourself doing it to make arithmetic work, the accumulator is
probably an `int` that should have been seeded `0m` — see [var](https://osysharp.com/reference/function/var/).

## See also       {#see-also}
- [(int)x — casts](https://osysharp.com/reference/function/cast/) — `(int)x`/`(double)x`, the cast: converting between the NUMERIC types, and failing rather than answering 0
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the literal suffixes, and which type a bare `0` is
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `$"…{value}…"`, which converts for you
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — pinning a local's type at its declaration
