|
| 1 | +package rx.lang.scala |
| 2 | + |
| 3 | +import java.util.Calendar |
| 4 | + |
| 5 | +import scala.collection.SortedMap |
| 6 | +import scala.reflect.runtime.universe |
| 7 | +import scala.reflect.runtime.universe.Symbol |
| 8 | +import scala.reflect.runtime.universe.Type |
| 9 | +import scala.reflect.runtime.universe.typeOf |
| 10 | + |
| 11 | +import org.junit.Ignore |
| 12 | +import org.junit.Test |
| 13 | +import org.scalatest.junit.JUnitSuite |
| 14 | + |
| 15 | +/** |
| 16 | + * These tests can be used to check if all methods of the Java Observable have a corresponding |
| 17 | + * method in the Scala Observable. |
| 18 | + * |
| 19 | + * These tests don't contain any assertions, so they will always succeed, but they print their |
| 20 | + * results to stdout. |
| 21 | + */ |
| 22 | +class CompletenessTest extends JUnitSuite { |
| 23 | + |
| 24 | + // some frequently used comments: |
| 25 | + val unnecessary = "[considered unnecessary in Scala land]" |
| 26 | + val deprecated = "[deprecated in RxJava]" |
| 27 | + val averageProblem = "[We can't have a general average method because Scala's `Numeric` does not have " + |
| 28 | + "scalar multiplication (we would need to calculate `(1.0/numberOfElements)*sum`). " + |
| 29 | + "You can use `fold` instead to accumulate `sum` and `numberOfElements` and divide at the end.]" |
| 30 | + val commentForFirstWithPredicate = "[use `.filter(condition).first`]" |
| 31 | + val fromFuture = "[TODO: Decide how Scala Futures should relate to Observables. Should there be a " + |
| 32 | + "common base interface for Future and Observable? And should Futures also have an unsubscribe method?]" |
| 33 | + |
| 34 | + /** |
| 35 | + * Maps each method from the Java Observable to its corresponding method in the Scala Observable |
| 36 | + */ |
| 37 | + val correspondence = defaultMethodCorrespondence ++ correspondenceChanges // ++ overrides LHS with RHS |
| 38 | + |
| 39 | + /** |
| 40 | + * Creates default method correspondence mappings, assuming that Scala methods have the same |
| 41 | + * name and the same argument types as in Java |
| 42 | + */ |
| 43 | + def defaultMethodCorrespondence: Map[String, String] = { |
| 44 | + val allMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.Observable[_]]) |
| 45 | + val tuples = for (javaM <- allMethods) yield (javaM, javaMethodSignatureToScala(javaM)) |
| 46 | + tuples.toMap |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Manually added mappings from Java Observable methods to Scala Observable methods |
| 51 | + */ |
| 52 | + def correspondenceChanges = Map( |
| 53 | + // manually added entries for Java instance methods |
| 54 | + "aggregate(Func2[T, T, T])" -> "reduce((U, U) => U)", |
| 55 | + "aggregate(R, Func2[R, _ >: T, R])" -> "foldLeft(R)((R, T) => R)", |
| 56 | + "all(Func1[_ >: T, Boolean])" -> "forall(T => Boolean)", |
| 57 | + "buffer(Long, Long, TimeUnit)" -> "buffer(Duration, Duration)", |
| 58 | + "buffer(Long, Long, TimeUnit, Scheduler)" -> "buffer(Duration, Duration, Scheduler)", |
| 59 | + "count()" -> "length", |
| 60 | + "dematerialize()" -> "dematerialize(<:<[Observable[T], Observable[Notification[U]]])", |
| 61 | + "elementAt(Int)" -> "[use `.drop(index).first`]", |
| 62 | + "elementAtOrDefault(Int, T)" -> "[use `.drop(index).firstOrElse(default)`]", |
| 63 | + "first(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate, |
| 64 | + "firstOrDefault(T)" -> "firstOrElse(=> U)", |
| 65 | + "firstOrDefault(Func1[_ >: T, Boolean], T)" -> "[use `.filter(condition).firstOrElse(default)`]", |
| 66 | + "groupBy(Func1[_ >: T, _ <: K], Func1[_ >: T, _ <: R])" -> "[use `groupBy` and `map`]", |
| 67 | + "mapMany(Func1[_ >: T, _ <: Observable[_ <: R]])" -> "flatMap(T => Observable[R])", |
| 68 | + "mapWithIndex(Func2[_ >: T, Integer, _ <: R])" -> "[combine `zipWithIndex` with `map` or with a for comprehension]", |
| 69 | + "onErrorResumeNext(Func1[Throwable, _ <: Observable[_ <: T]])" -> "onErrorResumeNext(Throwable => Observable[U])", |
| 70 | + "onErrorResumeNext(Observable[_ <: T])" -> "onErrorResumeNext(Observable[U])", |
| 71 | + "onErrorReturn(Func1[Throwable, _ <: T])" -> "onErrorReturn(Throwable => U)", |
| 72 | + "onExceptionResumeNext(Observable[_ <: T])" -> "onExceptionResumeNext(Observable[U])", |
| 73 | + "parallel(Func1[Observable[T], Observable[R]])" -> "parallel(Observable[T] => Observable[R])", |
| 74 | + "parallel(Func1[Observable[T], Observable[R]], Scheduler)" -> "parallel(Observable[T] => Observable[R], Scheduler)", |
| 75 | + "reduce(Func2[T, T, T])" -> "reduce((U, U) => U)", |
| 76 | + "reduce(R, Func2[R, _ >: T, R])" -> "foldLeft(R)((R, T) => R)", |
| 77 | + "scan(Func2[T, T, T])" -> unnecessary, |
| 78 | + "scan(R, Func2[R, _ >: T, R])" -> "scan(R)((R, T) => R)", |
| 79 | + "skip(Int)" -> "drop(Int)", |
| 80 | + "skipWhile(Func1[_ >: T, Boolean])" -> "dropWhile(T => Boolean)", |
| 81 | + "skipWhileWithIndex(Func2[_ >: T, Integer, Boolean])" -> unnecessary, |
| 82 | + "startWith(Iterable[T])" -> "[unnecessary because we can just use `++` instead]", |
| 83 | + "takeFirst()" -> "first", |
| 84 | + "takeFirst(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate, |
| 85 | + "takeLast(Int)" -> "takeRight(Int)", |
| 86 | + "takeWhileWithIndex(Func2[_ >: T, _ >: Integer, Boolean])" -> "[use `.zipWithIndex.takeWhile{case (elem, index) => condition}.map(_._1)`]", |
| 87 | + "toList()" -> "toSeq", |
| 88 | + "toSortedList()" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sorted)`]", |
| 89 | + "toSortedList(Func2[_ >: T, _ >: T, Integer])" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sortWith(f))`]", |
| 90 | + "where(Func1[_ >: T, Boolean])" -> "filter(T => Boolean)", |
| 91 | + "window(Long, Long, TimeUnit)" -> "window(Duration, Duration)", |
| 92 | + "window(Long, Long, TimeUnit, Scheduler)" -> "window(Duration, Duration, Scheduler)", |
| 93 | + |
| 94 | + // manually added entries for Java static methods |
| 95 | + "average(Observable[Integer])" -> averageProblem, |
| 96 | + "averageDoubles(Observable[Double])" -> averageProblem, |
| 97 | + "averageFloats(Observable[Float])" -> averageProblem, |
| 98 | + "averageLongs(Observable[Long])" -> averageProblem, |
| 99 | + "create(OnSubscribeFunc[T])" -> "apply(Observer[T] => Subscription)", |
| 100 | + "combineLatest(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "combineLatest(Observable[U])", |
| 101 | + "concat(Observable[_ <: Observable[_ <: T]])" -> "concat(<:<[Observable[T], Observable[Observable[U]]])", |
| 102 | + "defer(Func0[_ <: Observable[_ <: T]])" -> "defer(=> Observable[T])", |
| 103 | + "empty()" -> "apply(T*)", |
| 104 | + "error(Throwable)" -> "apply(Throwable)", |
| 105 | + "from(Array[T])" -> "apply(T*)", |
| 106 | + "from(Iterable[_ <: T])" -> "apply(T*)", |
| 107 | + "from(Future[_ <: T])" -> fromFuture, |
| 108 | + "from(Future[_ <: T], Long, TimeUnit)" -> fromFuture, |
| 109 | + "from(Future[_ <: T], Scheduler)" -> fromFuture, |
| 110 | + "just(T)" -> "apply(T*)", |
| 111 | + "merge(Observable[_ <: T], Observable[_ <: T])" -> "merge(Observable[U])", |
| 112 | + "merge(Observable[_ <: Observable[_ <: T]])" -> "flatten(<:<[Observable[T], Observable[Observable[U]]])", |
| 113 | + "mergeDelayError(Observable[_ <: T], Observable[_ <: T])" -> "mergeDelayError(Observable[U])", |
| 114 | + "mergeDelayError(Observable[_ <: Observable[_ <: T]])" -> "flattenDelayError(<:<[Observable[T], Observable[Observable[U]]])", |
| 115 | + "range(Int, Int)" -> "apply(Range)", |
| 116 | + "sequenceEqual(Observable[_ <: T], Observable[_ <: T])" -> "[use `(first zip second) map (p => p._1 == p._2)`]", |
| 117 | + "sequenceEqual(Observable[_ <: T], Observable[_ <: T], Func2[_ >: T, _ >: T, Boolean])" -> "[use `(first zip second) map (p => equality(p._1, p._2))`]", |
| 118 | + "sum(Observable[Integer])" -> "sum(Numeric[U])", |
| 119 | + "sumDoubles(Observable[Double])" -> "sum(Numeric[U])", |
| 120 | + "sumFloats(Observable[Float])" -> "sum(Numeric[U])", |
| 121 | + "sumLongs(Observable[Long])" -> "sum(Numeric[U])", |
| 122 | + "synchronize(Observable[T])" -> "synchronize", |
| 123 | + "switchDo(Observable[_ <: Observable[_ <: T]])" -> deprecated, |
| 124 | + "switchOnNext(Observable[_ <: Observable[_ <: T]])" -> "switch(<:<[Observable[T], Observable[Observable[U]]])", |
| 125 | + "zip(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "[use instance method `zip` and `map`]", |
| 126 | + "zip(Observable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]", |
| 127 | + "zip(Iterable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]" |
| 128 | + ) ++ List.iterate("T", 9)(s => s + ", T").map( |
| 129 | + // all 9 overloads of startWith: |
| 130 | + "startWith(" + _ + ")" -> "[unnecessary because we can just use `++` instead]" |
| 131 | + ).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map( |
| 132 | + // concat 2-9 |
| 133 | + "concat(" + _ + ")" -> "[unnecessary because we can use `++` instead or `Observable(o1, o2, ...).concat`]" |
| 134 | + ).drop(1).toMap ++ List.iterate("T", 10)(s => s + ", T").map( |
| 135 | + // all 10 overloads of from: |
| 136 | + "from(" + _ + ")" -> "apply(T*)" |
| 137 | + ).toMap ++ (3 to 9).map(i => { |
| 138 | + // zip3-9: |
| 139 | + val obsArgs = (1 to i).map(j => s"Observable[_ <: T$j], ").mkString("") |
| 140 | + val funcParams = (1 to i).map(j => s"_ >: T$j, ").mkString("") |
| 141 | + ("zip(" + obsArgs + "Func" + i + "[" + funcParams + "_ <: R])", unnecessary) |
| 142 | + }).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map( |
| 143 | + // merge 3-9: |
| 144 | + "merge(" + _ + ")" -> "[unnecessary because we can use `Observable(o1, o2, ...).flatten` instead]" |
| 145 | + ).drop(2).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map( |
| 146 | + // mergeDelayError 3-9: |
| 147 | + "mergeDelayError(" + _ + ")" -> "[unnecessary because we can use `Observable(o1, o2, ...).flattenDelayError` instead]" |
| 148 | + ).drop(2).toMap ++ (3 to 9).map(i => { |
| 149 | + // combineLatest 3-9: |
| 150 | + val obsArgs = (1 to i).map(j => s"Observable[_ <: T$j], ").mkString("") |
| 151 | + val funcParams = (1 to i).map(j => s"_ >: T$j, ").mkString("") |
| 152 | + ("combineLatest(" + obsArgs + "Func" + i + "[" + funcParams + "_ <: R])", "[If C# doesn't need it, Scala doesn't need it either ;-)]") |
| 153 | + }).toMap |
| 154 | + |
| 155 | + def removePackage(s: String) = s.replaceAll("(\\w+\\.)+(\\w+)", "$2") |
| 156 | + |
| 157 | + def methodMembersToMethodStrings(members: Iterable[Symbol]): Iterable[String] = { |
| 158 | + for (member <- members; alt <- member.asTerm.alternatives) yield { |
| 159 | + val m = alt.asMethod |
| 160 | + // multiple parameter lists in case of curried functions |
| 161 | + val paramListStrs = for (paramList <- m.paramss) yield { |
| 162 | + paramList.map( |
| 163 | + symb => removePackage(symb.typeSignature.toString.replaceAll(",(\\S)", ", $1")) |
| 164 | + ).mkString("(", ", ", ")") |
| 165 | + } |
| 166 | + val name = alt.asMethod.name.decoded |
| 167 | + name + paramListStrs.mkString("") |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + def getPublicInstanceMethods(tp: Type): Iterable[String] = { |
| 172 | + // declarations: => only those declared in Observable |
| 173 | + // members => also those of superclasses |
| 174 | + methodMembersToMethodStrings(tp.declarations.filter(m => m.isMethod && m.isPublic)) |
| 175 | + // TODO how can we filter out instance methods which were put into companion because |
| 176 | + // of extends AnyVal in a way which does not depend on implementation-chosen name '$extension'? |
| 177 | + .filter(! _.contains("$extension")) |
| 178 | + } |
| 179 | + |
| 180 | + // also applicable for Java types |
| 181 | + def getPublicInstanceAndCompanionMethods(tp: Type): Iterable[String] = |
| 182 | + getPublicInstanceMethods(tp) ++ |
| 183 | + getPublicInstanceMethods(tp.typeSymbol.companionSymbol.typeSignature) |
| 184 | + |
| 185 | + def printMethodSet(title: String, tp: Type) { |
| 186 | + println("\n" + title) |
| 187 | + println(title.map(_ => '-') + "\n") |
| 188 | + getPublicInstanceMethods(tp).toList.sorted.foreach(println(_)) |
| 189 | + } |
| 190 | + |
| 191 | + @Ignore // because spams output |
| 192 | + @Test def printJavaInstanceMethods: Unit = { |
| 193 | + printMethodSet("Instance methods of rx.Observable", |
| 194 | + typeOf[rx.Observable[_]]) |
| 195 | + } |
| 196 | + |
| 197 | + @Ignore // because spams output |
| 198 | + @Test def printScalaInstanceMethods: Unit = { |
| 199 | + printMethodSet("Instance methods of rx.lang.scala.Observable", |
| 200 | + typeOf[rx.lang.scala.Observable[_]]) |
| 201 | + } |
| 202 | + |
| 203 | + @Ignore // because spams output |
| 204 | + @Test def printJavaStaticMethods: Unit = { |
| 205 | + printMethodSet("Static methods of rx.Observable", |
| 206 | + typeOf[rx.Observable[_]].typeSymbol.companionSymbol.typeSignature) |
| 207 | + } |
| 208 | + |
| 209 | + @Ignore // because spams output |
| 210 | + @Test def printScalaCompanionMethods: Unit = { |
| 211 | + printMethodSet("Companion methods of rx.lang.scala.Observable", |
| 212 | + typeOf[rx.lang.scala.Observable.type]) |
| 213 | + } |
| 214 | + |
| 215 | + def javaMethodSignatureToScala(s: String): String = { |
| 216 | + s.replaceAllLiterally("Long, TimeUnit", "Duration") |
| 217 | + .replaceAll("Action0", "() => Unit") |
| 218 | + // nested [] can't be parsed with regex, so these will have to be added manually |
| 219 | + .replaceAll("Action1\\[([^]]*)\\]", "$1 => Unit") |
| 220 | + .replaceAll("Action2\\[([^]]*), ([^]]*)\\]", "($1, $2) => Unit") |
| 221 | + .replaceAll("Func0\\[([^]]*)\\]", "() => $1") |
| 222 | + .replaceAll("Func1\\[([^]]*), ([^]]*)\\]", "$1 => $2") |
| 223 | + .replaceAll("Func2\\[([^]]*), ([^]]*), ([^]]*)\\]", "($1, $2) => $3") |
| 224 | + .replaceAllLiterally("_ <: ", "") |
| 225 | + .replaceAllLiterally("_ >: ", "") |
| 226 | + .replaceAll("(\\w+)\\(\\)", "$1") |
| 227 | + } |
| 228 | + |
| 229 | + @Ignore // because spams output |
| 230 | + @Test def printDefaultMethodCorrespondence: Unit = { |
| 231 | + println("\nDefault Method Correspondence") |
| 232 | + println( "-----------------------------\n") |
| 233 | + val c = SortedMap(defaultMethodCorrespondence.toSeq : _*) |
| 234 | + val len = c.keys.map(_.length).max + 2 |
| 235 | + for ((javaM, scalaM) <- c) { |
| 236 | + println(s""" %-${len}s -> %s,""".format("\"" + javaM + "\"", "\"" + scalaM + "\"")) |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + @Ignore // because spams output |
| 241 | + @Test def printCorrectedMethodCorrespondence: Unit = { |
| 242 | + println("\nCorrected Method Correspondence") |
| 243 | + println( "-------------------------------\n") |
| 244 | + val c = SortedMap(correspondence.toSeq : _*) |
| 245 | + for ((javaM, scalaM) <- c) { |
| 246 | + println("%s -> %s,".format("\"" + javaM + "\"", "\"" + scalaM + "\"")) |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + def checkMethodPresence(expectedMethods: Iterable[String], tp: Type): Unit = { |
| 251 | + val actualMethods = getPublicInstanceAndCompanionMethods(tp).toSet |
| 252 | + val expMethodsSorted = expectedMethods.toList.sorted |
| 253 | + var good = 0 |
| 254 | + var bad = 0 |
| 255 | + for (m <- expMethodsSorted) if (actualMethods.contains(m) || m.charAt(0) == '[') { |
| 256 | + good += 1 |
| 257 | + } else { |
| 258 | + bad += 1 |
| 259 | + println(s"Warning: $m is NOT present in $tp") |
| 260 | + } |
| 261 | + val status = if (bad == 0) "SUCCESS" else "BAD" |
| 262 | + println(s"$status: $bad out of ${bad+good} methods were not found in $tp") |
| 263 | + } |
| 264 | + |
| 265 | + @Test def checkScalaMethodPresenceVerbose: Unit = { |
| 266 | + println("\nTesting that all mentioned Scala methods exist") |
| 267 | + println( "----------------------------------------------\n") |
| 268 | + |
| 269 | + val actualMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.lang.scala.Observable[_]]).toSet |
| 270 | + var good = 0 |
| 271 | + var bad = 0 |
| 272 | + for ((javaM, scalaM) <- SortedMap(correspondence.toSeq :_*)) { |
| 273 | + if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') { |
| 274 | + good += 1 |
| 275 | + } else { |
| 276 | + bad += 1 |
| 277 | + println(s"Warning:") |
| 278 | + println(s"$scalaM is NOT present in Scala Observable") |
| 279 | + println(s"$javaM is the method in Java Observable generating this warning") |
| 280 | + } |
| 281 | + } |
| 282 | + val status = if (bad == 0) "SUCCESS" else "BAD" |
| 283 | + println(s"\n$status: $bad out of ${bad+good} methods were not found in Scala Observable") |
| 284 | + } |
| 285 | + |
| 286 | + def setTodoForMissingMethods(corresp: Map[String, String]): Map[String, String] = { |
| 287 | + val actualMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.lang.scala.Observable[_]]).toSet |
| 288 | + for ((javaM, scalaM) <- corresp) yield |
| 289 | + (javaM, if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') scalaM else "[**TODO: missing**]") |
| 290 | + } |
| 291 | + |
| 292 | + @Test def checkJavaMethodPresence: Unit = { |
| 293 | + println("\nTesting that all mentioned Java methods exist") |
| 294 | + println( "---------------------------------------------\n") |
| 295 | + checkMethodPresence(correspondence.keys, typeOf[rx.Observable[_]]) |
| 296 | + } |
| 297 | + |
| 298 | + @Ignore // because we prefer the verbose version |
| 299 | + @Test def checkScalaMethodPresence: Unit = { |
| 300 | + checkMethodPresence(correspondence.values, typeOf[rx.lang.scala.Observable[_]]) |
| 301 | + } |
| 302 | + |
| 303 | + def scalaToJavaSignature(s: String) = |
| 304 | + s.replaceAllLiterally("_ <:", "? extends") |
| 305 | + .replaceAllLiterally("_ >:", "? super") |
| 306 | + .replaceAllLiterally("[", "<") |
| 307 | + .replaceAllLiterally("]", ">") |
| 308 | + .replaceAllLiterally("Array<T>", "T[]") |
| 309 | + |
| 310 | + def escapeJava(s: String) = |
| 311 | + s.replaceAllLiterally("<", "<") |
| 312 | + .replaceAllLiterally(">", ">") |
| 313 | + |
| 314 | + @Ignore // because spams output |
| 315 | + @Test def printMarkdownCorrespondenceTable() { |
| 316 | + def isInteresting(p: (String, String)): Boolean = |
| 317 | + p._1.replaceAllLiterally("()", "") != p._2 |
| 318 | + def groupingKey(p: (String, String)): (String, String) = |
| 319 | + (if (p._1.startsWith("average")) "average" else p._1.takeWhile(_ != '('), p._2) |
| 320 | + def formatJavaCol(name: String, alternatives: Iterable[String]): String = { |
| 321 | + alternatives.toList.sorted.map(scalaToJavaSignature(_)).map(s => { |
| 322 | + if (s.length > 64) { |
| 323 | + val toolTip = escapeJava(s) |
| 324 | + "<span title=\"" + toolTip + "\"><code>" + name + "(...)</code></span>" |
| 325 | + } else { |
| 326 | + "`" + s + "`" |
| 327 | + } |
| 328 | + }).mkString("<br/>") |
| 329 | + } |
| 330 | + def formatScalaCol(s: String): String = |
| 331 | + if (s.startsWith("[") && s.endsWith("]")) s.drop(1).dropRight(1) else "`" + s + "`" |
| 332 | + def escape(s: String) = s.replaceAllLiterally("[", "<").replaceAllLiterally("]", ">") |
| 333 | + |
| 334 | + println(""" |
| 335 | +## Comparison of Scala Observable and Java Observable |
| 336 | + |
| 337 | +Note: |
| 338 | +* This table contains both static methods and instance methods. |
| 339 | +* If a signature is too long, move your mouse over it to get the full signature. |
| 340 | +
|
| 341 | + |
| 342 | +| Java Method | Scala Method | |
| 343 | +|-------------|--------------|""") |
| 344 | + |
| 345 | + val ps = setTodoForMissingMethods(correspondence) |
| 346 | + |
| 347 | + (for (((javaName, scalaCol), pairs) <- ps.groupBy(groupingKey(_)).toList.sortBy(_._1._1)) yield { |
| 348 | + "| " + formatJavaCol(javaName, pairs.map(_._1)) + " | " + formatScalaCol(scalaCol) + " |" |
| 349 | + }).foreach(println(_)) |
| 350 | + println(s"\nThis table was generated on ${Calendar.getInstance().getTime()}.") |
| 351 | + println(s"**Do not edit**. Instead, edit `${getClass().getCanonicalName()}`.") |
| 352 | + } |
| 353 | + |
| 354 | +} |
0 commit comments