|
| 1 | +/** This script makes the Hall of Fame for the elapsed month. |
| 2 | + * It should be run every 1st day of the month, in the working tree, and |
| 3 | + * then a commit with the changes should be pushed to scala-lang. |
| 4 | + */ |
| 5 | + |
| 6 | +import java.net.URL |
| 7 | + |
| 8 | +import scala.annotation.switch |
| 9 | + |
| 10 | +import scala.io.Source |
| 11 | +import scala.util.parsing.json._ |
| 12 | + |
| 13 | +object MakeHallOfFame { |
| 14 | + object Category extends Enumeration { |
| 15 | + val Typesafe, EPFL, Community = Value |
| 16 | + } |
| 17 | + type Category = Category.Value |
| 18 | + |
| 19 | + // TODO Expand (and maintain) that list - or fetch it from some source |
| 20 | + val TypesafePeople = Set( |
| 21 | + "adriaanm", |
| 22 | + "dragos", |
| 23 | + "gkossakowski", |
| 24 | + "JamesIry", |
| 25 | + "jsuereth", |
| 26 | + "lexspoon", |
| 27 | + "paulp", |
| 28 | + "phaller", |
| 29 | + "retronym" |
| 30 | + ) |
| 31 | + |
| 32 | + // TODO Expand (and maintain) that list - or fetch it from some source |
| 33 | + val EPFLPeople = Set( |
| 34 | + "axel22", |
| 35 | + "heathermiller", |
| 36 | + "hubertp", |
| 37 | + "lrytz", |
| 38 | + "magarciaEPFL", |
| 39 | + "odersky", |
| 40 | + "TiarkRompf", |
| 41 | + "VladUreche", |
| 42 | + "xeno-by" |
| 43 | + ) |
| 44 | + |
| 45 | + class Author(val username: String, val gravatar: String) { |
| 46 | + val category: Category = |
| 47 | + if (TypesafePeople(username)) Category.Typesafe |
| 48 | + else if (EPFLPeople(username)) Category.EPFL |
| 49 | + else Category.Community |
| 50 | + |
| 51 | + var commits: Int = 0 |
| 52 | + var linesAdded: Int = 0 |
| 53 | + var linesDeleted: Int = 0 |
| 54 | + |
| 55 | + var isNewContributor: Boolean = false |
| 56 | + } |
| 57 | + |
| 58 | + var thisYear: Int = 0 |
| 59 | + var thisMonth: Int = 0 |
| 60 | + var thisMonthStr: String = "" |
| 61 | + |
| 62 | + def isWeekOfThisMonth(week: String): Boolean = |
| 63 | + week startsWith thisMonthStr |
| 64 | + |
| 65 | + def main(args: Array[String]) { |
| 66 | + val (year, month) = { |
| 67 | + if (args.size >= 2) (args(0).toInt, args(1).toInt) |
| 68 | + else getYearAndMonth() |
| 69 | + } |
| 70 | + thisYear = year |
| 71 | + thisMonth = month |
| 72 | + thisMonthStr = "%04d-%02d" format (year, month) |
| 73 | + |
| 74 | + progress(s"Building data for $thisMonthStr") |
| 75 | + |
| 76 | + val sourceDataString = loadSourceDataString() |
| 77 | + val sourceData = parseSourceData(sourceDataString) |
| 78 | + val authors = buildDataFromJSON(sourceData) |
| 79 | + |
| 80 | + progress("Sorting by category") |
| 81 | + val byCategory = authors.groupBy(_.category) |
| 82 | + val sorted = byCategory.mapValues(_.sortBy(-_.commits)) |
| 83 | + |
| 84 | + val output = buildOutput(sorted) |
| 85 | + writeOutputToFile(output) |
| 86 | + } |
| 87 | + |
| 88 | + def progress(msg: String) { |
| 89 | + Console.err.println(msg) |
| 90 | + } |
| 91 | + |
| 92 | + def getYearAndMonth(): (Int, Int) = { |
| 93 | + import java.util.Calendar._ |
| 94 | + val cal = new java.util.GregorianCalendar |
| 95 | + cal.set(DAY_OF_MONTH, 0) // this will wrap to the Month (and Year if necessary) |
| 96 | + (cal.get(YEAR), cal.get(MONTH)+1) |
| 97 | + } |
| 98 | + |
| 99 | + def loadSourceDataString(): String = { |
| 100 | + progress("Downloading source data") |
| 101 | + val source = Source.fromURL(new URL( |
| 102 | + "https://github.com/scala/scala/graphs/contributors-data")) |
| 103 | + try source.mkString |
| 104 | + finally source.close() |
| 105 | + } |
| 106 | + |
| 107 | + def parseSourceData(str: String): Any = { |
| 108 | + progress("Parsing JSON in source data") |
| 109 | + JSON.parseFull(str) getOrElse { |
| 110 | + throw new Exception("Parse error") |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + def buildDataFromJSON(jsonAuthors: Any): List[Author] = { |
| 115 | + progress("Building my data") |
| 116 | + val L(authors) = jsonAuthors |
| 117 | + val all = for { |
| 118 | + M(author0) <- authors |
| 119 | + M(authorData) = author0("author") |
| 120 | + S(username) = authorData("login") |
| 121 | + S(gravatar) = authorData("gravatar") |
| 122 | + I(totalCommits) = author0("total") |
| 123 | + L(jsonWeeks) = author0("weeks") |
| 124 | + } yield { |
| 125 | + val author = new Author(username, gravatar) |
| 126 | + |
| 127 | + for { |
| 128 | + M(week) <- jsonWeeks |
| 129 | + S(date) = week("w") |
| 130 | + if isWeekOfThisMonth(date) |
| 131 | + I(commits) = week("c") |
| 132 | + I(added) = week("a") |
| 133 | + I(deleted) = week("d") |
| 134 | + } yield { |
| 135 | + author.commits += commits |
| 136 | + author.linesAdded += added |
| 137 | + author.linesDeleted += deleted |
| 138 | + } |
| 139 | + |
| 140 | + author.isNewContributor = author.commits == totalCommits |
| 141 | + author |
| 142 | + } |
| 143 | + all filter (_.commits != 0) |
| 144 | + } |
| 145 | + |
| 146 | + def buildOutput(authorsByCategory: Map[Category, List[Author]]) = { |
| 147 | + progress("Outputting") |
| 148 | + |
| 149 | + val result = new scala.collection.mutable.ListBuffer[String] |
| 150 | + def outln(line: String) = result += line |
| 151 | + |
| 152 | + val thisMonthText = getMonthName(thisMonth) |
| 153 | + |
| 154 | + outln("---") |
| 155 | + outln("layout: famearchive") |
| 156 | + outln("title: Contributors of " + thisMonthText + " " + thisYear) |
| 157 | + outln("fame-year: " + thisYear) |
| 158 | + outln("fame-month: " + thisMonth) |
| 159 | + outln("fame-month-str: " + thisMonthText) |
| 160 | + outln("fame-categories:") |
| 161 | + |
| 162 | + for { |
| 163 | + category <- Seq(Category.Typesafe, Category.EPFL, Category.Community) |
| 164 | + } { |
| 165 | + outln(" - category: " + category.toString()) |
| 166 | + outln(" authors:") |
| 167 | + var rank = 0 |
| 168 | + var rankCommits = -1 |
| 169 | + for (author <- authorsByCategory(category)) { |
| 170 | + if (author.commits != rankCommits) { |
| 171 | + rank += 1 |
| 172 | + rankCommits = author.commits |
| 173 | + } |
| 174 | + |
| 175 | + outln(" - username: " + author.username) |
| 176 | + outln(" gravatar: " + author.gravatar) |
| 177 | + outln(" commits: " + author.commits) |
| 178 | + outln(" linesAdded: " + author.linesAdded) |
| 179 | + outln(" linesDeleted: " + author.linesDeleted) |
| 180 | + outln(" rank: " + rank) |
| 181 | + outln(" newContributor: " + author.isNewContributor) |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + outln("---") |
| 186 | + |
| 187 | + result.toList |
| 188 | + } |
| 189 | + |
| 190 | + def writeOutputToFile(output: List[String]) { |
| 191 | + val (postYear, postMonth) = { |
| 192 | + import java.util.Calendar._ |
| 193 | + val cal = new java.util.GregorianCalendar(thisYear, thisMonth-1, 1) |
| 194 | + cal.add(MONTH, 1) |
| 195 | + (cal.get(YEAR), cal.get(MONTH)+1) |
| 196 | + } |
| 197 | + val postDateStr = "%04d-%02d-01" format (postYear, postMonth) |
| 198 | + |
| 199 | + val fileName = s"../../get-involved/scala-fame-data/_posts/${postDateStr}-scala-fame-${thisMonthStr}.md" |
| 200 | + |
| 201 | + progress("Writing output to " + fileName) |
| 202 | + |
| 203 | + val writer = new java.io.PrintWriter(fileName) |
| 204 | + try output foreach writer.println |
| 205 | + finally writer.close() |
| 206 | + } |
| 207 | + |
| 208 | + def getMonthName(month: Int): String = (month: @switch) match { |
| 209 | + case 1 => "January" |
| 210 | + case 2 => "February" |
| 211 | + case 3 => "March" |
| 212 | + case 4 => "April" |
| 213 | + case 5 => "May" |
| 214 | + case 6 => "June" |
| 215 | + case 7 => "July" |
| 216 | + case 8 => "August" |
| 217 | + case 9 => "September" |
| 218 | + case 10 => "October" |
| 219 | + case 11 => "November" |
| 220 | + case 12 => "December" |
| 221 | + } |
| 222 | + |
| 223 | + // JSON extractors |
| 224 | + class CC[T] { |
| 225 | + def unapply(a: Any): Option[T] = Some(a.asInstanceOf[T]) |
| 226 | + } |
| 227 | + |
| 228 | + object M extends CC[Map[String, Any]] |
| 229 | + object L extends CC[List[Any]] |
| 230 | + object S extends CC[String] |
| 231 | + object D extends CC[Double] |
| 232 | + object B extends CC[Boolean] |
| 233 | + |
| 234 | + object I { |
| 235 | + def unapply(a: Any): Option[Int] = Some(a.asInstanceOf[Double].toInt) |
| 236 | + } |
| 237 | +} |
0 commit comments