Reputation: 41246
I've been pondering the possible implementations of string formatting for swift, most of which boil down to "use NSString(format:...)" That's all well and good, but I wanted a concise and readable format, so I decided to implement something like python's % formatting operator:
@infix func % (value:Double, format:String) -> String {
return NSString(format:format, value)
}
This works great for Double's as I can use:
println("PI = " + M_PI % "%.3f")
which results in:
PI = 3.142
While I can create 5 of these trivially, I'd like to turn it into a generic function:
@infix func %<T> (value:T, format:String) -> String {
return NSString(format:format, value)
}
But that results in the message:
Could not find an overload for 'init' that accepts the supplied arguments
Reasonable enough, I could be passing in a tuple, or something equally non-objective-C. (Note that to really do this Python-style, I want to pass in a tuple, but that's another matter and beyond the scope of this question)
I tried declaring my own empty protocol and implementing that on Double, but it didn't help at all.
protocol NSStringFormattable {}
extension Double : NSStringFormattable {}
@infix func % <T:NSStringFormattable> (value:T, format:String) -> String {
return NSString(format:format, value)
}
I could obviously do something like add a format function to each class and then just define the operator in terms of the format function, but in many ways that's not any better than just defining 5 different operator overloads.
protocol NSStringFormattable {
func format(format:String) -> String
}
extension Double : NSStringFormattable {
func format(format:String) -> String {
return NSString(format:format, self)
}
}
@infix func % <T:NSStringFormattable> (value:T, format:String) -> String {
return value.format(format)
}
How can I restrict T to only those types that can be passed to NSString(format:...)
?
Upvotes: 3
Views: 827
Reputation: 299663
You're very close, but you don't need a fixed-length tuple. That's what's causing your headaches. Just use an array instead.
@infix func % (values:CVarArg[], format:String) -> String {
return NSString(format:format, arguments:getVaList(values))
}
[M_PI, 6] % "%.3f->%d"
==> "3.142->6"
[M_PI, M_PI_2] % "%.3f %.3f"
==> "3.142 1.571"
Of course this is highly type-unsafe because it's an unchecked printf as you say.
BTW, this even works with mixed-type stuff, and with non-literals:
let x = 1
let y = 1.5
let z = "yes"
[x, y, z] % "%d, %.2f, %@"
==> "1, 1.50, yes"
I don't know if that part is going to be fragile, however. Mixed-type literals are promoted to NSArray
, which seems a dangerous thing to do automatically, so they may change it. But NSArray
is acceptable as a CVarArg[]
.
Note that not all types can be converted this way. Characters currently cannot for instance. You can overcome this by extending them to do so:
extension Character : CVarArg {
func encode() -> Word[] {
var result = Word[]()
let s = String(self)
for c in s.unicodeScalars {
result.append(Word(c.value))
}
return result
}
}
let c:Character = "c"
["I", c, 2*3] % "%@, %lc, %d"
==> "I, c, 6"
I'm wondering if there's an easier way to write encode()
, but I'm not sure yet. Hopefully a Character encoding will be provided by Swift in the future. But the lesson here is that arbitrary types could be given an encode
and be formatted.
class Car {
let make = "Ford"
}
extension Car : CVarArg {
func encode() -> Word[] {
return NSString(string:self.make).encode()
}
}
let car = Car()
[car] % "%@"
The lesson here is that you can turn arbitrary things into a CVarArg
or into any protocol, via extensions.
Upvotes: 2
Reputation: 41246
Found it!
@infix func % (value:CVarArg, format:String) -> String {
return NSString(format:format, value)
}
This single function allows for:
5 % "%04x"
3.4 % "%.3f"
M_PI % "%.3f"
Int64(32) % "%04X"
Unfortunately, it also allows for:
"String" % "%3.3s"
and produces garbage, but welcome to printf without argument type checking
Even further, by defining a set of functions:
@infix func % (values:(CVarArg, CVarArg), format:String) -> String {
return NSString(format:format, values.0, values.1)
}
@infix func % (values:(CVarArg, CVarArg, CVarArg), format:String) -> String {
return NSString(format:format, values.0, values.1, values.2)
}
We can achieve python-like affects:
(M_PI, 5) % "%.3f->%d"
Kind of ugly to have to define one per tuple-length, but I'll keep hacking on it :)
Upvotes: 2
Reputation: 22517
As far as I have discovered (having gone through the same journey as you), generics are not the solution here, rather multiple overloading (which isn't pretty) -
operator infix % { }
@infix func % (format: String, value: Double) -> String {
return NSString(format:format, value)
}
@infix func % (format: String, value: Float) -> String {
return NSString(format:format, value)
}
@infix func % (format: String, value: Int) -> String {
return NSString(format:format, value)
}
sorry, reverse parameter order from your example - allows
println("PI = %.3f" % M_PI)
Upvotes: 0