wip: general-purpose conversion

This commit is contained in:
Jacek Sieka 2020-04-28 16:36:37 +02:00
parent 8065e36c5a
commit 9f4e2bccdd
No known key found for this signature in database
GPG Key ID: A1B09461ABB656B8
2 changed files with 141 additions and 0 deletions

99
stew/conv.nim Normal file
View File

@ -0,0 +1,99 @@
## Explicit canonical conversions between types (`frm` because `from` is a keyword)
##
## `init` is often used to initialize a type, taking other types as arguments -
## however, `frm` and `to` imply conversions rather than initializations - the
## distinction is that the source value is typically "consumed" by the
## conversion wheras `init` might use or initialize itself with the value.
##
## Start by implementing `frm` - the other variations will be derived from it
## if possible. If `frm` is inefficient or impossible, implement `to`, `tryFrm`
## and `tryTo` explicitly.
##
## The `frm` conversion should:
## * always succeed
## * string-from-int for example
## * work well in the sentence "X was converted from Y"
##
## The `try` forms exist for conversions that might fail:
## * string-to-int (characters might be invalid)
##
## The `to` conversion should:
## * always succeeed
## * int-to-string for example
## * work well in the sentence "X was converted to Y"
##
## If both `frm` and `to` are implemented, it is expected that they roundtrip
## perfectly.
##
## Optionally, some converters may include a tag - this tag should be passed as
## the last argument:
## * string.frm(15, Hex)
import results, typetraits
export results
type
Canonical* = object
# Tag for canonical conversion between types
template frm*(T: type, v: auto): auto = frm(T, v, Canonical)
template tryFrm*(T: type, v: auto): Opt[T] = tryFrm(T, v, Canonical)
template to*(v: auto, T: type): auto = to(v, T, Canonical)
template tryTo*(v: auto, T: type): auto = tryTo(v, T, Canonical)
template frm*(T: type, v: T, tag: type Canonical): T = v
template tryFrm*(T: type, v: auto, tag: typed): auto =
# Default conversion to T from v
mixin frm
when compiles(frm(T, v, tag)):
ok(Opt[T], frm(T, v, tag))
else:
{.error: "Implement frm or tryFrm for " & name(T).}
template to*(v: auto, T: type, tag: typed): auto =
mixin frm
when compiles(frm(T, v, tag)):
frm(T, v, tag)
else:
{.error: "Implement to or frm for " & name(T) & " and " & name(typeof(v)).}
template tryTo*(v: auto, T: type, tag: typed): auto =
mixin frm, tryFrm, to
when compiles(to(v, T, tag)):
ok(Opt[T], to(v, T, tag))
elif compiles(frm(T, v, tag)):
ok(Opt[T], frm(T, v, tag))
elif compiles(tryFrm(T, v, tag)):
tryFrm(T, v, tag)
else:
{.error: "Implement to, frm, tryFrm or tryTo for " & name(T).}
template frm*(T: type string, v: SomeInteger, tag: type Canonical): T =
$v
type AsHex = object
chars*: int
type AsDefaultHex = object
template asHex*(T: type SomeInteger): auto = AsHex(chars: sizeof(T) * 2)
template asHex*(len: int): auto = AsHex(chars: len)
template asHex*(): auto = AsDefaultHex()
func frm*(T: type string, v: SomeInteger, tag: AsHex): T =
const
HexChars = "0123456789abcdef"
var
n = v
var res = newString(tag.chars)
for j in countdown(res.len-1, 0):
res[j] = HexChars[int(n and 0xF)]
n = n shr 4
# handle negative overflow
if n == 0 and v < 0: n = -1
res
template frm*(T: type string, v: SomeInteger, tag: AsDefaultHex): T =
mixin frm
frm(T, v, asHex(sizeof(v) * 2))

42
tests/test_conv.nim Normal file
View File

@ -0,0 +1,42 @@
import
../stew/conv
type
Obj = object
v: int
HasFrm = object
v: int
HasTo = object
v: int
HasTryFrom = object
v: int
HasTryTo = object
v: int
template frm(T: type HasFrm, o: Obj, tag = Canonical): T = T(v: o.v)
let o = Obj(v: 42)
let
hft = o.to(HasFrm)
hff = HasFrm.frm(o)
hftf = HasFrm.tryFrm(o)
hftt = o.tryTo(HasFrm)
doAssert hff.v == o.v
doAssert hftf.get().v == o.v
doAssert hft.v == o.v
doAssert hftt.get().v == o.v
doAssert (string.frm(42) == "42")
doAssert string.tryFrm(42)[] == "42"
doAssert 42.to(string) == "42"
doAssert 42.tryTo(string)[] == "42"
doAssert string.frm(10, asHex(4)) == "000a"
doAssert 10.to(string, asHex(int16)) == "000a"
doAssert 10'i16.to(string, asHex()) == "000a"