@@ -9,6 +9,8 @@ import scala.util.matching.Regex.Match
9
9
10
10
import java .util .{Calendar , Date , Formattable }
11
11
12
+ import PartialFunction .cond
13
+
12
14
/** Formatter string checker. */
13
15
abstract class FormatChecker (using reporter : InterpolationReporter ):
14
16
@@ -19,7 +21,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
19
21
// count of args, for checking indexes
20
22
def argc : Int
21
23
22
- val allFlags = " -#+ 0,(< "
24
+ // match a conversion specifier
23
25
val formatPattern = """ %(?:(\d+)\$)?([-#+ 0,(<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""" .r
24
26
25
27
// ordinal is the regex group index in the format pattern
@@ -95,18 +97,16 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
95
97
extension (inline value : Boolean )
96
98
inline def or (inline body : => Unit ): Boolean = value || { body ; false }
97
99
inline def orElse (inline body : => Unit ): Boolean = value || { body ; true }
98
- inline def but (inline body : => Unit ): Boolean = value && { body ; false }
99
100
inline def and (inline body : => Unit ): Boolean = value && { body ; true }
101
+ inline def but (inline body : => Unit ): Boolean = value && { body ; false }
100
102
101
- /** A conversion specifier matched in the argi'th string part,
102
- * with `argc` arguments to interpolate.
103
- */
104
- sealed abstract class Conversion :
105
- // the match for this descriptor
106
- def descriptor : Match
107
- // the part number for reporting errors
108
- def argi : Int
103
+ enum Kind :
104
+ case StringXn , HashXn , BooleanXn , CharacterXn , IntegralXn , FloatingPointXn , DateTimeXn , LiteralXn , ErrorXn
105
+ import Kind .*
109
106
107
+ /** A conversion specifier matched in the argi'th string part, with `argc` arguments to interpolate.
108
+ */
109
+ final class Conversion (val descriptor : Match , val argi : Int , val kind : Kind ):
110
110
// the descriptor fields
111
111
val index : Option [Int ] = descriptor.intOf(Index )
112
112
val flags : String = descriptor.stringOf(Flags )
@@ -115,26 +115,86 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
115
115
val op : String = descriptor.stringOf(CC )
116
116
117
117
// the conversion char is the head of the op string (but see DateTimeXn)
118
- val cc : Char = if isError then '?' else op(0 )
118
+ val cc : Char =
119
+ kind match
120
+ case ErrorXn => '?'
121
+ case DateTimeXn => if op.length > 1 then op(1 ) else '?'
122
+ case _ => op(0 )
119
123
120
- def isError : Boolean = false
121
124
def isIndexed : Boolean = index.nonEmpty || hasFlag('<' )
122
- def isLiteral : Boolean = false
125
+ def isError : Boolean = kind == ErrorXn
126
+ def isLiteral : Boolean = kind == LiteralXn
123
127
124
128
// descriptor is at index 0 of the part string
125
129
def isLeading : Boolean = descriptor.at(Spec ) == 0
126
130
131
+ // flags and index in specifier are ok
132
+ private def goodies = goodFlags && goodIndex
133
+
127
134
// true if passes. Default checks flags and index
128
- def verify : Boolean = goodFlags && goodIndex
135
+ def verify : Boolean =
136
+ kind match {
137
+ case StringXn => goodies
138
+ case BooleanXn => goodies
139
+ case HashXn => goodies
140
+ case CharacterXn => goodies && noPrecision && only_-(" c conversion" )
141
+ case IntegralXn =>
142
+ def d_# = cc == 'd' && hasFlag('#' ) and badFlag('#' , " # not allowed for d conversion" )
143
+ def x_comma = cc != 'd' && hasFlag(',' ) and badFlag(',' , " ',' only allowed for d conversion of integral types" )
144
+ goodies && noPrecision && ! d_# && ! x_comma
145
+ case FloatingPointXn =>
146
+ goodies && (cc match {
147
+ case 'a' | 'A' =>
148
+ val badFlags = " ,(" .filter(hasFlag)
149
+ noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s " ' $badf' not allowed for a, A " ))
150
+ case _ => true
151
+ })
152
+ case DateTimeXn =>
153
+ def hasCC = op.length == 2 or errorAt(CC )(" Date/time conversion must have two characters" )
154
+ def goodCC = " HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc" .contains(cc) or errorAt(CC , 1 )(s " ' $cc' doesn't seem to be a date or time conversion " )
155
+ goodies && hasCC && goodCC && noPrecision && only_-(" date/time conversions" )
156
+ case LiteralXn =>
157
+ op match {
158
+ case " %" => goodies && noPrecision and width.foreach(_ => warningAt(Width )(" width ignored on literal" ))
159
+ case " n" => noFlags && noWidth && noPrecision
160
+ }
161
+ case ErrorXn =>
162
+ errorAt(CC )(s " illegal conversion character ' $cc' " )
163
+ false
164
+ }
129
165
130
166
// is the specifier OK with the given arg
131
- def accepts (arg : ClassTag [? ]): Boolean = true
167
+ def accepts (arg : ClassTag [? ]): Boolean =
168
+ kind match
169
+ case BooleanXn => arg == classTag[Boolean ] orElse warningAt(CC )(" Boolean format is null test for non-Boolean" )
170
+ case IntegralXn =>
171
+ arg == classTag[BigInt ] || ! cond(cc) {
172
+ case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) => " + (" .filter(hasFlag).foreach(bad => badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )) ; true
173
+ }
174
+ case _ => true
132
175
133
176
// what arg type if any does the conversion accept
134
- def acceptableVariants : List [ClassTag [? ]]
177
+ def acceptableVariants : List [ClassTag [? ]] =
178
+ kind match {
179
+ case StringXn => if hasFlag('#' ) then classTag[Formattable ] :: Nil else classTag[Any ] :: Nil
180
+ case BooleanXn => classTag[Boolean ] :: Conversion .FakeNullTag :: Nil
181
+ case HashXn => classTag[Any ] :: Nil
182
+ case CharacterXn => classTag[Char ] :: classTag[Byte ] :: classTag[Short ] :: classTag[Int ] :: Nil
183
+ case IntegralXn => classTag[Int ] :: classTag[Long ] :: classTag[Byte ] :: classTag[Short ] :: classTag[BigInt ] :: Nil
184
+ case FloatingPointXn => classTag[Double ] :: classTag[Float ] :: classTag[BigDecimal ] :: Nil
185
+ case DateTimeXn => classTag[Long ] :: classTag[Calendar ] :: classTag[Date ] :: Nil
186
+ case LiteralXn => Nil
187
+ case ErrorXn => Nil
188
+ }
135
189
136
- // what flags does the conversion accept? defaults to all
137
- protected def okFlags : String = allFlags
190
+ // what flags does the conversion accept?
191
+ private def okFlags : String =
192
+ kind match {
193
+ case StringXn => " -#<"
194
+ case BooleanXn | HashXn => " -<"
195
+ case LiteralXn => " -"
196
+ case _ => " -#+ 0,(<"
197
+ }
138
198
139
199
def hasFlag (f : Char ) = flags.contains(f)
140
200
def hasAnyFlag (fs : String ) = fs.exists(hasFlag)
@@ -146,6 +206,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
146
206
def errorAt (g : SpecGroup , i : Int = 0 )(msg : String ) = reporter.partError(msg, argi, descriptor.offset(g, i))
147
207
def warningAt (g : SpecGroup , i : Int = 0 )(msg : String ) = reporter.partWarning(msg, argi, descriptor.offset(g, i))
148
208
209
+ // various assertions
149
210
def noFlags = flags.isEmpty or errorAt(Flags )(" flags not allowed" )
150
211
def noWidth = width.isEmpty or errorAt(Width )(" width not allowed" )
151
212
def noPrecision = precision.isEmpty or errorAt(Precision )(" precision not allowed" )
@@ -162,84 +223,25 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
162
223
okRange || hasFlag('<' ) or errorAt(Index )(" Argument index out of range" )
163
224
object Conversion :
164
225
def apply (m : Match , i : Int ): Conversion =
165
- def badCC (msg : String ) = ErrorXn (m, i).tap(error => error.errorAt(if (error.op.isEmpty) Spec else CC )(msg))
166
- def cv (cc : Char ) = cc match
167
- case 's' | 'S' => StringXn (m, i)
168
- case 'h' | 'H' => HashXn (m, i)
169
- case 'b' | 'B' => BooleanXn (m, i)
170
- case 'c' | 'C' => CharacterXn (m, i)
226
+ def kindOf (cc : Char ) = cc match
227
+ case 's' | 'S' => StringXn
228
+ case 'h' | 'H' => HashXn
229
+ case 'b' | 'B' => BooleanXn
230
+ case 'c' | 'C' => CharacterXn
171
231
case 'd' | 'o' |
172
- 'x' | 'X' => IntegralXn (m, i)
232
+ 'x' | 'X' => IntegralXn
173
233
case 'e' | 'E' |
174
234
'f' |
175
235
'g' | 'G' |
176
- 'a' | 'A' => FloatingPointXn (m, i)
177
- case 't' | 'T' => DateTimeXn (m, i)
178
- case '%' | 'n' => LiteralXn (m, i)
179
- case _ => badCC( s " illegal conversion character ' $cc ' " )
180
- end cv
236
+ 'a' | 'A' => FloatingPointXn
237
+ case 't' | 'T' => DateTimeXn
238
+ case '%' | 'n' => LiteralXn
239
+ case _ => ErrorXn
240
+ end kindOf
181
241
m.group(CC ) match
182
- case Some (cc) => cv( cc(0 )).tap(_.verify)
183
- case None => badCC( s " Missing conversion operator in ' ${m.matched}'; $literalHelp" )
242
+ case Some (cc) => new Conversion (m, i, kindOf( cc(0 ) )).tap(_.verify)
243
+ case None => new Conversion (m, i, ErrorXn ).tap(_.errorAt( Spec )( s " Missing conversion operator in ' ${m.matched}'; $literalHelp" ) )
184
244
end apply
185
245
val literalHelp = " use %% for literal %, %n for newline"
246
+ private val FakeNullTag : ClassTag [? ] = null
186
247
end Conversion
187
- abstract class GeneralXn extends Conversion
188
- // s | S
189
- class StringXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
190
- val acceptableVariants =
191
- if hasFlag('#' ) then classTag[Formattable ] :: Nil
192
- else classTag[Any ] :: Nil
193
- override protected def okFlags = " -#<"
194
- // b | B
195
- class BooleanXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
196
- val FakeNullTag : ClassTag [? ] = null
197
- val acceptableVariants = classTag[Boolean ] :: FakeNullTag :: Nil
198
- override def accepts (arg : ClassTag [? ]): Boolean =
199
- arg == classTag[Boolean ] orElse warningAt(CC )(" Boolean format is null test for non-Boolean" )
200
- override protected def okFlags = " -<"
201
- // h | H
202
- class HashXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
203
- val acceptableVariants = classTag[Any ] :: Nil
204
- override protected def okFlags = " -<"
205
- // %% | %n
206
- class LiteralXn (val descriptor : Match , val argi : Int ) extends Conversion :
207
- override def isLiteral = true
208
- override def verify = op match
209
- case " %" => super .verify && noPrecision and width.foreach(_ => warningAt(Width )(" width ignored on literal" ))
210
- case " n" => noFlags && noWidth && noPrecision
211
- override protected val okFlags = " -"
212
- override def acceptableVariants = Nil
213
- class CharacterXn (val descriptor : Match , val argi : Int ) extends Conversion :
214
- override def verify = super .verify && noPrecision && only_-(" c conversion" )
215
- val acceptableVariants = classTag[Char ] :: classTag[Byte ] :: classTag[Short ] :: classTag[Int ] :: Nil
216
- class IntegralXn (val descriptor : Match , val argi : Int ) extends Conversion :
217
- override def verify =
218
- def d_# = cc == 'd' && hasFlag('#' ) and badFlag('#' , " # not allowed for d conversion" )
219
- def x_comma = cc != 'd' && hasFlag(',' ) and badFlag(',' , " ',' only allowed for d conversion of integral types" )
220
- super .verify && noPrecision && ! d_# && ! x_comma
221
- val acceptableVariants = classTag[Int ] :: classTag[Long ] :: classTag[Byte ] :: classTag[Short ] :: classTag[BigInt ] :: Nil
222
- override def accepts (arg : ClassTag [? ]): Boolean =
223
- arg == classTag[BigInt ] || {
224
- cc match
225
- case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) => " + (" .filter(hasFlag).foreach(bad => badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )) ; false
226
- case _ => true
227
- }
228
- class FloatingPointXn (val descriptor : Match , val argi : Int ) extends Conversion :
229
- override def verify = super .verify && (cc match {
230
- case 'a' | 'A' =>
231
- val badFlags = " ,(" .filter(hasFlag)
232
- noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s " ' $badf' not allowed for a, A " ))
233
- case _ => true
234
- })
235
- val acceptableVariants = classTag[Double ] :: classTag[Float ] :: classTag[BigDecimal ] :: Nil
236
- class DateTimeXn (val descriptor : Match , val argi : Int ) extends Conversion :
237
- override val cc : Char = if op.length > 1 then op(1 ) else '?'
238
- def hasCC = op.length == 2 or errorAt(CC )(" Date/time conversion must have two characters" )
239
- def goodCC = " HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc" .contains(cc) or errorAt(CC , 1 )(s " ' $cc' doesn't seem to be a date or time conversion " )
240
- override def verify = super .verify && hasCC && goodCC && noPrecision && only_-(" date/time conversions" )
241
- val acceptableVariants = classTag[Long ] :: classTag[Calendar ] :: classTag[Date ] :: Nil
242
- class ErrorXn (val descriptor : Match , val argi : Int ) extends Conversion :
243
- override def isError = true
244
- override def verify = false
245
- override def acceptableVariants = Nil
0 commit comments