summaryrefslogtreecommitdiff
path: root/examples/scala-js/javalib/src/main/scala/java/util/Formatter.scala
blob: 5e0ab22cfb5abb0851ca2fc3af3301396fb238b4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
package java.util

import scala.annotation.switch
import scala.scalajs.js

import java.io._
import java.lang._

final class Formatter(private val dest: Appendable) extends Closeable with Flushable {
  import Formatter._

  var closed = false

  def this() = this(new StringBuilder())

  def close(): Unit = {
    if (!closed) {
      dest match {
        case cl: Closeable => cl.close()
        case _ =>
      }
    }
    closed = true
  }

  def flush(): Unit = ifNotClosed {
    dest match {
      case fl: Flushable => fl.flush()
      case _ =>
    }
  }

  // Begin implem of format()

  def format(format_in: String, args: Array[AnyRef]): Formatter = ifNotClosed {
    import js.Any.fromDouble // to have .toFixed and .toExponential on Doubles

    var fmt: String = format_in
    var lastImplicitIndex: Int = 0
    var lastIndex: Int = 0 // required for < flag

    while (!fmt.isEmpty) {
      fmt match {
        case RegularChunk(matchResult) =>
          fmt = fmt.substring(matchResult(0).get.length)
          dest.append(matchResult(0).get)

        case DoublePercent(_) =>
          fmt = fmt.substring(2)
          dest.append('%')

        case EOLChunk(_) =>
          fmt = fmt.substring(2)
          dest.append('\n')

        case FormattedChunk(matchResult) =>
          fmt = fmt.substring(matchResult(0).get.length)

          val flags = matchResult(2).get
          def hasFlag(flag: String) = flags.indexOf(flag) >= 0

          val indexStr = matchResult(1).getOrElse("")
          val index = if (!indexStr.isEmpty) {
            Integer.parseInt(indexStr)
          } else if (hasFlag("<")) {
            lastIndex
          } else {
            lastImplicitIndex += 1
            lastImplicitIndex
          }
          lastIndex = index
          if (index <= 0 || index > args.length)
            throw new MissingFormatArgumentException(matchResult(5).get)
          val arg = args(index-1)

          val widthStr = matchResult(3).getOrElse("")
          val hasWidth = !widthStr.isEmpty
          val width =
            if (hasWidth) Integer.parseInt(widthStr)
            else 0

          val precisionStr = matchResult(4).getOrElse("")
          val hasPrecision = !precisionStr.isEmpty
          val precision =
            if (hasPrecision) Integer.parseInt(precisionStr)
            else 0

          val conversion = matchResult(5).get.charAt(0)

          def intArg: Int = (arg: Any) match {
            case arg: Int  => arg
            case arg: Char => arg.toInt
          }
          def numberArg: scala.Double = (arg: Any) match {
            case arg: Number => arg.doubleValue()
            case arg: Char   => arg.toDouble
          }

          def padCaptureSign(argStr: String, prefix: String) = {
            val firstChar = argStr.charAt(0)
            if (firstChar == '+' || firstChar == '-')
              pad(argStr.substring(1), firstChar+prefix)
            else
              pad(argStr, prefix)
          }

          def strRepeat(s: String, times: Int) = {
            var result: String = ""
            var i = times
            while (i > 0) {
              result += s
              i -= 1
            }
            result
          }

          def with_+(s: String, preventZero: scala.Boolean = false) = {
            if (s.charAt(0) != '-') {
              if (hasFlag("+"))
                pad(s, "+", preventZero)
              else if (hasFlag(" "))
                pad(s, " ", preventZero)
              else
                pad(s, "", preventZero)
            } else {
              if (hasFlag("("))
                pad(s.substring(1) + ")", "(", preventZero)
              else
                pad(s.substring(1), "-", preventZero)
            }
          }

          def pad(argStr: String, prefix: String = "",
                  preventZero: Boolean = false) = {
            val prePadLen = argStr.length + prefix.length

            val padStr = {
              if (width <= prePadLen) {
                prefix + argStr
              } else {
                val padRight = hasFlag("-")
                val padZero = hasFlag("0") && !preventZero
                val padLength = width - prePadLen
                val padChar: String = if (padZero) "0" else " "
                val padding = strRepeat(padChar, padLength)

                if (padZero && padRight)
                  throw new java.util.IllegalFormatFlagsException(flags)
                else if (padRight) prefix + argStr  + padding
                else if (padZero)  prefix + padding + argStr
                else padding + prefix + argStr
              }
            }

            val casedStr =
              if (conversion.isUpper) padStr.toUpperCase()
              else padStr
            dest.append(casedStr)
          }

          (conversion: @switch) match {
            case 'b' | 'B' => pad { arg match {
              case null => "false"
              case b: Boolean => String.valueOf(b)
              case _ => "true"
            } }
            case 'h' | 'H' => pad {
              if (arg eq null) "null"
              else Integer.toHexString(arg.hashCode)
            }
            case 's' | 'S' => arg match {
              case null if !hasFlag("#") => pad("null")
              case formattable: Formattable =>
                val flags = (
                  (if (hasFlag("-"))       FormattableFlags.LEFT_JUSTIFY else 0) |
                  (if (hasFlag("#"))       FormattableFlags.ALTERNATE    else 0) |
                  (if (conversion.isUpper) FormattableFlags.UPPERCASE    else 0)
                )

                formattable.formatTo(this, flags,
                                     if (hasWidth)     width.toInt     else -1,
                                     if (hasPrecision) precision.toInt else -1)
                None // no further processing
              case t: AnyRef if !hasFlag("#") => pad(t.toString)
              case _ =>
                throw new FormatFlagsConversionMismatchException("#", 's')
            }
            case 'c' | 'C' =>
              pad(js.String.fromCharCode(intArg))
            case 'd' =>
              with_+(numberArg.toString())
            case 'o' =>
              val str = (arg: Any) match {
                case arg: scala.Int  => Integer.toOctalString(arg)
                case arg: scala.Long => Long.toOctalString(arg)
              }
              padCaptureSign(str, if (hasFlag("#")) "0" else "")
            case 'x' | 'X' =>
              val str = (arg: Any) match {
                case arg: scala.Int  => Integer.toHexString(arg)
                case arg: scala.Long => Long.toHexString(arg)
              }
              padCaptureSign(str, if (hasFlag("#")) "0x" else "")
            case 'e' | 'E' =>
              sciNotation(if (hasPrecision) precision else 6)
            case 'g' | 'G' =>
              val m = Math.abs(numberArg)
              // precision handling according to JavaDoc
              // precision here means number of significant digits
              // not digits after decimal point
              val p =
                if (!hasPrecision) 6
                else if (precision == 0) 1
                else precision
              // between 1e-4 and 10e(p): display as fixed
              if (m >= 1e-4 && m < Math.pow(10, p)) {
                val sig = Math.ceil(Math.log10(m))
                with_+(numberArg.toFixed(Math.max(p - sig, 0)))
              } else sciNotation(p - 1)
            case 'f' =>
              with_+({
                // JavaDoc: 6 is default precision
                numberArg.toFixed(if (hasPrecision) precision else 6)
              }, numberArg.isNaN || numberArg.isInfinite)
          }

          def sciNotation(precision: Int) = {
            val exp = numberArg.toExponential(precision)
            with_+({
              // check if we need additional 0 padding in exponent
              // JavaDoc: at least 2 digits
              if ("e" == exp.charAt(exp.length - 3)) {
                exp.substring(0, exp.length - 1) + "0" +
                  exp.charAt(exp.length - 1)
              } else exp
            }, numberArg.isNaN || numberArg.isInfinite)
          }
      }
    }

    this
  }

  def ioException(): IOException = null
  def locale(): Locale = ifNotClosed { null }
  def out(): Appendable = ifNotClosed { dest }

  override def toString(): String = out().toString()

  @inline private def ifNotClosed[T](body: => T): T =
    if (closed) throwClosedException()
    else body

  private def throwClosedException(): Nothing =
    throw new FormatterClosedException()

}

object Formatter {

  private class RegExpExtractor(val regexp: js.RegExp) {
    def unapply(str: String): Option[js.RegExp.ExecResult] = {
      Option(regexp.exec(str))
    }
  }

  private val RegularChunk = new RegExpExtractor(new js.RegExp("""^[^\x25]+"""))
  private val DoublePercent = new RegExpExtractor(new js.RegExp("""^\x25{2}"""))
  private val EOLChunk = new RegExpExtractor(new js.RegExp("""^\x25n"""))
  private val FormattedChunk = new RegExpExtractor(new js.RegExp(
      """^\x25(?:([1-9]\d*)\$)?([-#+ 0,\(<]*)(\d*)(?:\.(\d+))?([A-Za-z])"""))

}