// CAST to character strings and LOBs, and the text of a value (spec M4d2
// §5.3, §5.5).
///|
fn padded(n : Int64, width : Int32) -> String {
n.to_string().pad_start(width, '0')
}
///|
/// `digits` with a point after the first digit, as a mantissa.
fn mantissa(digits : String) -> String {
if digits.length() == 1 {
digits
} else {
digits.view(end_offset=1).to_owned() +
"." +
digits.view(start_offset=1).to_owned()
}
}
///|
/// An exact decimal as plain digits with the scale of the value (`1.50`,
/// `1000` for `1d3`). A value outside the range of DECIMAL(38), by its
/// adjusted exponent, is in scientific form, so that the text stays short.
/// Zero has no sign.
fn decimal_text(d : @ion_core.Decimal) -> String {
let c = d.coefficient()
let e = d.exponent().to_int64()
if c.is_zero() && e >= 0L {
return "0"
}
let digits = abs_int(c).to_string()
let sign = if is_negative_int(c) { "-" } else { "" }
let n = digits.length()
let adjusted = n.to_int64() - 1L + e
if adjusted < -39L || adjusted > 37L {
return "\{sign}\{mantissa(digits)}E\{adjusted}"
}
if e >= 0L {
return sign + digits + "0".repeat(e.to_int())
}
let k = (-e).to_int()
if n > k {
sign +
digits.view(end_offset=n - k).to_owned() +
"." +
digits.view(start_offset=n - k).to_owned()
} else {
sign + "0." + "0".repeat(k - n) + digits
}
}
///|
/// A float in SQL approximate form with the shortest digits that give it
/// back (`1.5E0`); NaN and the infinities in their Ion spellings.
fn float_text(f : Double) -> String {
if f.is_nan() {
return "nan"
}
if f.is_pos_inf() {
return "+inf"
}
if f.is_neg_inf() {
return "-inf"
}
let sign = if f.reinterpret_as_int64() < 0L { "-" } else { "" }
if f == 0.0 {
return sign + "0E0"
}
guard parse_decimal(f.abs().to_string()) is Some(shortest) else {
return f.to_string()
}
let mut c = shortest.coefficient()
let mut e = shortest.exponent()
while (c % 10N).is_zero() {
c = c / 10N
e += 1
}
let digits = c.to_string()
"\{sign}\{mantissa(digits)}E\{digits.length() - 1 + e}"
}
///|
fn date_text(d : @value.Date) -> String {
let year = if d.year < 0 {
"-" + padded((-d.year).to_int64(), 4)
} else {
padded(d.year.to_int64(), 4)
}
"\{year}-\{padded(d.month.to_int64(), 2)}-\{padded(d.day.to_int64(), 2)}"
}
///|
/// `HH:MM:SS`, then exactly the precision of the value in fraction digits,
/// then the offset when the time has one.
fn time_text(t : @value.Time) -> String {
let clock = "\{padded(t.hour.to_int64(), 2)}:\{padded(t.minute.to_int64(), 2)}:\{padded(t.second.to_int64(), 2)}"
let fraction = if t.precision > 0 {
"." +
padded(t.nanos.to_int64(), 9).view(end_offset=t.precision.min(9)).to_owned()
} else {
""
}
let offset = match t.offset {
None => ""
Some(minutes) => {
let sign = if minutes < 0 { "-" } else { "+" }
let m = minutes.abs()
"\{sign}\{padded((m / 60).to_int64(), 2)}:\{padded((m % 60).to_int64(), 2)}"
}
}
clock + fraction + offset
}
///|
/// `(k)` when `n` has more digits than the default precision `default`.
fn precision_text(digits : Int32, default : Int32) -> String {
if digits > default {
"(\{digits})"
} else {
""
}
}
///|
/// `INTERVAL 'Y-M' YEAR TO MONTH`, with a leading `-` in the string when
/// negative. The value keeps only its class, so the literal names the whole
/// class; a leading precision above the default 2 is written out.
fn ym_text(i : @value.IntervalYM) -> String {
let months = big64(i.months)
let sign = if is_negative_int(months) { "-" } else { "" }
let m = abs_int(months)
let years = m / 12N
"INTERVAL '\{sign}\{years}-\{m % 12N}' YEAR\{precision_text(digit_count(years), 2)} TO MONTH"
}
///|
/// `INTERVAL 'D HH:MM:SS[.f]' DAY TO SECOND` with the fewest fraction digits,
/// a leading `-` in the string when negative, and the leading (default 2)
/// and fractional (default 6) precisions written out when larger.
fn dt_text(i : @value.IntervalDT) -> String {
let n = dt_nanos(i)
let sign = if is_negative_int(n) { "-" } else { "" }
let a = abs_int(n)
let days = a / nanos_per_day
let rest = (a % nanos_per_day).to_int64()
let seconds = rest / 1000000000L
let nanos = (rest % 1000000000L).to_int()
let digits = if nanos == 0 { 0 } else { needed_precision(nanos) }
let fraction = if nanos == 0 {
""
} else {
"." + padded(nanos.to_int64(), 9).view(end_offset=digits).to_owned()
}
let clock = "\{padded(seconds / 3600L, 2)}:\{padded(seconds % 3600L / 60L, 2)}:\{padded(seconds % 60L, 2)}"
"INTERVAL '\{sign}\{days} \{clock}\{fraction}' DAY\{precision_text(digit_count(days), 2)} TO SECOND\{precision_text(digits, 6)}"
}
///|
/// The text of a scalar value as CAST to a character string gives it (spec
/// M4d2 §5.5); `None` for a kind that has no text. A datetime or interval is
/// an SQL (SQL 6.13: "the shortest character string that conforms
/// to the definition of "), so that CAST reads it back.
fn display_text(x : @value.Value) -> String? {
match x {
Bool(b) => Some(if b { "TRUE" } else { "FALSE" })
Int(n) => Some(n.to_string())
BigInt(n) => Some(n.to_string())
Decimal(d) => Some(decimal_text(d))
Float(f) => Some(float_text(f))
Date(d) => Some("DATE '\{date_text(d)}'")
Time(t) => Some("TIME '\{time_text(t)}'")
Timestamp(ts) =>
Some("TIMESTAMP '\{date_text(ts.date)} \{time_text(ts.time)}'")
IntervalYM(i) => Some(ym_text(i))
IntervalDT(i) => Some(dt_text(i))
_ => None
}
}
///|
/// `text` in at most `n` code points. Character text is cut (SQL gives only
/// a warning); the text of another kind that does not fit is out of range
/// (SQL 22001).
fn fit_text(
text : String,
n : Int32,
character : Bool,
name : String,
) -> String raise OpFailure {
let chars = text.to_array()
if chars.length() <= n {
text
} else if character {
String::from_array(chars[:n])
} else {
raise DataFailure("'\{text}' is longer than \{name}")
}
}
///|
fn to_text(x : @value.Value, kind : TextKind) -> @value.Value raise OpFailure {
let name = target_name(Text(kind))
let (text, character) = match x {
String(s) => (s, true)
Clob(b) => {
let s = @utf8.decode(b[:]) catch {
_ => raise TypeFailure("a CLOB that is not UTF-8")
}
(s, true)
}
_ =>
match display_text(x) {
Some(t) => (t, false)
None => raise TypeFailure("CAST of \{kind_name(x)} to \{name}")
}
}
match kind {
String => @value.Value::from_string(text)
Varchar(n) => @value.Value::from_string(fit_text(text, n, character, name))
Char(n) => {
let fitted = fit_text(text, n, character, name)
@value.Value::from_string(fitted + " ".repeat(n - fitted.char_length()))
}
}
}
///|
/// CLOB from text (UTF-8) or a LOB; BLOB from a LOB (spec M4d2 §5.3).
fn to_lob(
x : @value.Value,
target : CastTarget,
) -> @value.Value raise OpFailure {
match (x, target) {
(String(s), Clob) => @value.Value::from_clob(@utf8.encode(s.view()))
(Clob(b) | Blob(b), Clob) => @value.Value::from_clob(b)
(Clob(b) | Blob(b), Blob) => @value.Value::from_blob(b)
_ => raise TypeFailure("CAST of \{kind_name(x)} to \{target_name(target)}")
}
}