summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Scala/objsets/.classpath12
-rw-r--r--Scala/objsets/.project12
-rw-r--r--Scala/objsets/.settings/org.scala-ide.sdt.core.prefs5
-rw-r--r--Scala/objsets/build.sbt110
-rw-r--r--Scala/objsets/lib_managed/jars/junit/junit/junit-4.10.jarbin0 -> 253160 bytes
-rw-r--r--Scala/objsets/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jarbin0 -> 76643 bytes
-rw-r--r--Scala/objsets/lib_managed/jars/org.scala-lang/scala-actors/scala-actors-2.10.0.jarbin0 -> 451307 bytes
-rw-r--r--Scala/objsets/lib_managed/jars/org.scala-lang/scala-reflect/scala-reflect-2.10.0.jarbin0 -> 3149351 bytes
-rw-r--r--Scala/objsets/lib_managed/jars/org.scalatest/scalatest_2.10/scalatest_2.10-1.9.1.jarbin0 -> 3114958 bytes
-rw-r--r--Scala/objsets/project/CourseraHttp.scala223
-rw-r--r--Scala/objsets/project/GradingFeedback.scala218
-rw-r--r--Scala/objsets/project/ProgFunBuild.scala646
-rw-r--r--Scala/objsets/project/RecordingLogger.scala35
-rw-r--r--Scala/objsets/project/RichJsValue.scala28
-rw-r--r--Scala/objsets/project/ScalaTestRunner.scala169
-rw-r--r--Scala/objsets/project/Settings.scala48
-rw-r--r--Scala/objsets/project/StyleChecker.scala77
-rw-r--r--Scala/objsets/project/build.properties1
-rw-r--r--Scala/objsets/project/buildSettings.sbt72
-rw-r--r--Scala/objsets/project/project/buildPlugins.sbt2
-rw-r--r--Scala/objsets/project/scalastyle_config.xml136
-rw-r--r--Scala/objsets/src/main/scala/common/package.scala40
-rw-r--r--Scala/objsets/src/main/scala/objsets/TweetData.scala719
-rw-r--r--Scala/objsets/src/main/scala/objsets/TweetReader.scala78
-rw-r--r--Scala/objsets/src/main/scala/objsets/TweetSet.scala200
-rw-r--r--Scala/objsets/src/test/scala/objsets/TweetSetSuite.scala72
26 files changed, 2903 insertions, 0 deletions
diff --git a/Scala/objsets/.classpath b/Scala/objsets/.classpath
new file mode 100644
index 0000000..572103c
--- /dev/null
+++ b/Scala/objsets/.classpath
@@ -0,0 +1,12 @@
+<classpath>
+ <classpathentry output="target/scala-2.10/classes" path="src/main/scala" kind="src"></classpathentry>
+ <classpathentry output="target/scala-2.10/test-classes" path="src/test/scala" kind="src"></classpathentry>
+ <classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.scalatest/scalatest_2.10/scalatest_2.10-1.9.1.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.scala-lang/scala-actors/scala-actors-2.10.0.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.scala-lang/scala-reflect/scala-reflect-2.10.0.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/junit/junit/junit-4.10.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar" kind="lib"></classpathentry>
+ <classpathentry path="org.eclipse.jdt.launching.JRE_CONTAINER" kind="con"></classpathentry>
+ <classpathentry path="bin" kind="output"></classpathentry>
+</classpath> \ No newline at end of file
diff --git a/Scala/objsets/.project b/Scala/objsets/.project
new file mode 100644
index 0000000..2c1b75d
--- /dev/null
+++ b/Scala/objsets/.project
@@ -0,0 +1,12 @@
+<projectDescription>
+ <name>progfun-objsets</name>
+ <buildSpec>
+ <buildCommand>
+ <name>org.scala-ide.sdt.core.scalabuilder</name>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.scala-ide.sdt.core.scalanature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription> \ No newline at end of file
diff --git a/Scala/objsets/.settings/org.scala-ide.sdt.core.prefs b/Scala/objsets/.settings/org.scala-ide.sdt.core.prefs
new file mode 100644
index 0000000..3293297
--- /dev/null
+++ b/Scala/objsets/.settings/org.scala-ide.sdt.core.prefs
@@ -0,0 +1,5 @@
+#Generated by sbteclipse
+#Fri Apr 12 08:09:40 CEST 2013
+deprecation=true
+feature=true
+scala.compiler.useProjectSettings=true
diff --git a/Scala/objsets/build.sbt b/Scala/objsets/build.sbt
new file mode 100644
index 0000000..470f461
--- /dev/null
+++ b/Scala/objsets/build.sbt
@@ -0,0 +1,110 @@
+name <<= submitProjectName(pname => "progfun-"+ pname)
+
+version := "1.0.0"
+
+scalaVersion := "2.10.1"
+
+scalacOptions ++= Seq("-deprecation", "-feature")
+
+libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1" % "test"
+
+libraryDependencies += "junit" % "junit" % "4.10" % "test"
+
+// This setting defines the project to which a solution is submitted. When creating a
+// handout, the 'createHandout' task will make sure that its value is correct.
+submitProjectName := "objsets"
+
+// See documentation in ProgFunBuild.scala
+projectDetailsMap := {
+val currentCourseId = "progfun-002"
+Map(
+ "example" -> ProjectDetails(
+ packageName = "example",
+ assignmentPartId = "fTzFogNl",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "recfun" -> ProjectDetails(
+ packageName = "recfun",
+ assignmentPartId = "3Rarn9Ki",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "funsets" -> ProjectDetails(
+ packageName = "funsets",
+ assignmentPartId = "fBXOL6Qd",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "objsets" -> ProjectDetails(
+ packageName = "objsets",
+ assignmentPartId = "95dMMEz7",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "patmat" -> ProjectDetails(
+ packageName = "patmat",
+ assignmentPartId = "3gPmpcif",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "forcomp" -> ProjectDetails(
+ packageName = "forcomp",
+ assignmentPartId = "fG1oZGIO",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId),
+ "streams" -> ProjectDetails(
+ packageName = "streams",
+ assignmentPartId = "CWKgCFCi",
+ maxScore = 10d,
+ styleScoreRatio = 0.2,
+ courseId=currentCourseId)//,
+ // "simulations" -> ProjectDetails(
+ // packageName = "simulations",
+ // assignmentPartId = "iYs4GARk",
+ // maxScore = 10d,
+ // styleScoreRatio = 0.2,
+ // courseId="progfun2-001"),
+ // "interpreter" -> ProjectDetails(
+ // packageName = "interpreter",
+ // assignmentPartId = "1SZhe1Ut",
+ // maxScore = 10d,
+ // styleScoreRatio = 0.2,
+ // courseId="progfun2-001")
+)
+}
+
+// Files that we hand out to the students
+handoutFiles <<= (baseDirectory, projectDetailsMap, commonSourcePackages) map { (basedir, detailsMap, commonSrcs) =>
+ (projectName: String) => {
+ val details = detailsMap.getOrElse(projectName, sys.error("Unknown project name: "+ projectName))
+ val commonFiles = (PathFinder.empty /: commonSrcs)((files, pkg) =>
+ files +++ (basedir / "src" / "main" / "scala" / pkg ** "*.scala")
+ )
+ (basedir / "src" / "main" / "scala" / details.packageName ** "*.scala") +++
+ commonFiles +++
+ (basedir / "src" / "main" / "resources" / details.packageName ** "*") +++
+ (basedir / "src" / "test" / "scala" / details.packageName ** "*.scala") +++
+ (basedir / "build.sbt") +++
+ (basedir / "project" / "build.properties") +++
+ (basedir / "project" ** ("*.scala" || "*.sbt")) +++
+ (basedir / "project" / "scalastyle_config.xml") +++
+ (basedir / "lib_managed" ** "*.jar") +++
+ (basedir * (".classpath" || ".project")) +++
+ (basedir / ".settings" / "org.scala-ide.sdt.core.prefs")
+ }
+}
+
+// This setting allows to restrict the source files that are compiled and tested
+// to one specific project. It should be either the empty string, in which case all
+// projects are included, or one of the project names from the projectDetailsMap.
+currentProject := ""
+
+// Packages in src/main/scala that are used in every project. Included in every
+// handout, submission.
+commonSourcePackages += "common"
+
+// Packages in src/test/scala that are used for grading projects. Always included
+// compiling tests, grading a project.
+gradingTestPackages += "grading"
diff --git a/Scala/objsets/lib_managed/jars/junit/junit/junit-4.10.jar b/Scala/objsets/lib_managed/jars/junit/junit/junit-4.10.jar
new file mode 100644
index 0000000..954851e
--- /dev/null
+++ b/Scala/objsets/lib_managed/jars/junit/junit/junit-4.10.jar
Binary files differ
diff --git a/Scala/objsets/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar b/Scala/objsets/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar
new file mode 100644
index 0000000..e5149be
--- /dev/null
+++ b/Scala/objsets/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar
Binary files differ
diff --git a/Scala/objsets/lib_managed/jars/org.scala-lang/scala-actors/scala-actors-2.10.0.jar b/Scala/objsets/lib_managed/jars/org.scala-lang/scala-actors/scala-actors-2.10.0.jar
new file mode 100644
index 0000000..bb4600c
--- /dev/null
+++ b/Scala/objsets/lib_managed/jars/org.scala-lang/scala-actors/scala-actors-2.10.0.jar
Binary files differ
diff --git a/Scala/objsets/lib_managed/jars/org.scala-lang/scala-reflect/scala-reflect-2.10.0.jar b/Scala/objsets/lib_managed/jars/org.scala-lang/scala-reflect/scala-reflect-2.10.0.jar
new file mode 100644
index 0000000..6489599
--- /dev/null
+++ b/Scala/objsets/lib_managed/jars/org.scala-lang/scala-reflect/scala-reflect-2.10.0.jar
Binary files differ
diff --git a/Scala/objsets/lib_managed/jars/org.scalatest/scalatest_2.10/scalatest_2.10-1.9.1.jar b/Scala/objsets/lib_managed/jars/org.scalatest/scalatest_2.10/scalatest_2.10-1.9.1.jar
new file mode 100644
index 0000000..6be20e6
--- /dev/null
+++ b/Scala/objsets/lib_managed/jars/org.scalatest/scalatest_2.10/scalatest_2.10-1.9.1.jar
Binary files differ
diff --git a/Scala/objsets/project/CourseraHttp.scala b/Scala/objsets/project/CourseraHttp.scala
new file mode 100644
index 0000000..5f55b12
--- /dev/null
+++ b/Scala/objsets/project/CourseraHttp.scala
@@ -0,0 +1,223 @@
+import dispatch.{Request, Http, NoLogging, StatusCode, url}
+import cc.spray.json.{JsNull, JsonParser, DefaultJsonProtocol, JsValue}
+import RichJsValue._
+import org.apache.commons.codec.digest.DigestUtils
+import org.apache.commons.codec.binary.{Hex, Base64}
+import java.io.{IOException, File, FileInputStream}
+import scalaz.Scalaz.{mkIdentity, ValidationNEL}
+
+import Settings._
+
+case class JsonSubmission(api_state: String, user_info: JsValue, submission_metadata: JsValue, solutions: JsValue, submission_encoding: String, submission: String)
+//case class JsonQueueResult(submission: JsonSubmission)
+object SubmitJsonProtocol extends DefaultJsonProtocol {
+ implicit val jsonSubmissionFormat = jsonFormat6(JsonSubmission)
+// implicit val jsonQueueResultFormat = jsonFormat1(JsonQueueResult)
+}
+
+object CourseraHttp {
+ private lazy val http = new Http with NoLogging
+
+ private def executeRequest[T](req: Request)(parse: String => ValidationNEL[String, T]): ValidationNEL[String, T] = {
+ try {
+ http(req >- { res =>
+ parse(res)
+ })
+ } catch {
+ case ex: IOException =>
+ ("Connection failed\n"+ ex.toString()).failNel
+ case StatusCode(code, message) =>
+ ("HTTP failed with status "+ code +"\n"+ message).failNel
+ }
+ }
+
+
+ /******************************
+ * SUBMITTING
+ */
+
+ def getChallenge(email: String, submitProject: ProjectDetails): ValidationNEL[String, Challenge] = {
+ val baseReq = url(challengeUrl(submitProject.courseId))
+ val withArgs = baseReq << Map("email_address" -> email,
+ "assignment_part_sid" -> submitProject.assignmentPartId,
+ "response_encoding" -> "delim")
+
+ executeRequest(withArgs) { res =>
+ // example result. there might be an `aux_data` value at the end.
+ // |email_address|a@b.com|challenge_key|XXYYXXYYXXYY|state|XXYYXXYYXXYY|challenge_aux_data|
+ val parts = res.split('|').filterNot(_.isEmpty)
+ if (parts.length < 7)
+ ("Unexpected challenge format: \n"+ res).failNel
+ else
+ Challenge(parts(1), parts(3), parts(5)).successNel
+ }
+ }
+
+ def submitSolution(sourcesJar: File, submitProject: ProjectDetails, challenge: Challenge, chResponse: String): ValidationNEL[String, String] = {
+ val fileLength = sourcesJar.length()
+ if (!sourcesJar.exists()) {
+ ("Sources jar archive does not exist\n"+ sourcesJar.getAbsolutePath).failNel
+ } else if (fileLength == 0l) {
+ ("Sources jar archive is empty\n"+ sourcesJar.getAbsolutePath).failNel
+ } else if (fileLength > maxSubmitFileSize) {
+ ("Sources jar archive is too big. Allowed size: "+
+ maxSubmitFileSize +" bytes, found "+ fileLength +" bytes.\n"+
+ sourcesJar.getAbsolutePath).failNel
+ } else {
+ val bytes = new Array[Byte](fileLength.toInt)
+ val sizeRead = try {
+ val is = new FileInputStream(sourcesJar)
+ val read = is.read(bytes)
+ is.close()
+ read
+ } catch {
+ case ex: IOException =>
+ ("Failed to read sources jar archive\n"+ ex.toString()).failNel
+ }
+ if (sizeRead != bytes.length) {
+ ("Failed to read the sources jar archive, size read: "+ sizeRead).failNel
+ } else {
+ val fileData = encodeBase64(bytes)
+ val baseReq = url(submitUrl(submitProject.courseId))
+ val withArgs = baseReq << Map("assignment_part_sid" -> submitProject.assignmentPartId,
+ "email_address" -> challenge.email,
+ "submission" -> fileData,
+ "submission_aux" -> "",
+ "challenge_response" -> chResponse,
+ "state" -> challenge.state)
+ executeRequest(withArgs) { res =>
+ // the API returns HTTP 200 even if there are failures, how impolite...
+ if (res.contains("Your submission has been accepted"))
+ res.successNel
+ else
+ res.failNel
+ }
+ }
+ }
+ }
+
+ def challengeResponse(challenge: Challenge, otPassword: String): String =
+ shaHexDigest(challenge.challengeKey + otPassword)
+
+
+ /********************************
+ * DOWNLOADING SUBMISSIONS
+ */
+
+ // def downloadFromQueue(queue: String, targetJar: File, apiKey: String): ValidationNEL[String, QueueResult] = {
+ // val baseReq = url(Settings.submitQueueUrl)
+ // val withArgsAndHeader = baseReq << Map("queue" -> queue) <:< Map("X-api-key" -> apiKey)
+
+ // executeRequest(withArgsAndHeader) { res =>
+ // extractJson(res, targetJar)
+ // }
+ // }
+
+ def readJsonFile(jsonFile: File, targetJar: File): ValidationNEL[String, QueueResult] = {
+ extractJson(sbt.IO.read(jsonFile), targetJar)
+ }
+
+ def extractJson(jsonData: String, targetJar: File): ValidationNEL[String, QueueResult] = {
+ import SubmitJsonProtocol._
+ for {
+ jsonSubmission <- {
+ try {
+ val parsed = JsonParser(jsonData)
+ val submission = parsed \ "submission"
+ if (submission == JsNull) {
+ ("Nothing to grade, queue is empty.").failNel
+ } else {
+ submission.convertTo[JsonSubmission].successNel
+ }
+ } catch {
+ case e: Exception =>
+ ("Could not parse submission\n"+ jsonData +"\n"+ fullExceptionString(e)).failNel
+ }
+ }
+ queueResult <- {
+ val encodedFile = jsonSubmission.submission
+ val jarContent = decodeBase64(encodedFile)
+ try {
+ sbt.IO.write(targetJar, jarContent)
+ QueueResult(jsonSubmission.api_state).successNel
+ } catch {
+ case e: IOException =>
+ ("Failed to write jar file to "+ targetJar.getAbsolutePath +"\n"+ e.toString).failNel
+ }
+ }
+ } yield queueResult
+ }
+
+ def unpackJar(file: File, targetDirectory: File): ValidationNEL[String, Unit] = {
+ try {
+ val files = sbt.IO.unzip(file, targetDirectory)
+ if (files.isEmpty)
+ ("No files found when unpacking jar file "+ file.getAbsolutePath).failNel
+ else
+ ().successNel
+ } catch {
+ case e: IOException =>
+ val msg = "Error while unpacking the jar file "+ file.getAbsolutePath +" to "+ targetDirectory.getAbsolutePath +"\n"+ e.toString
+ if (Settings.offlineMode) {
+ println("[offline mode] "+ msg)
+ ().successNel
+ } else {
+ msg.failNel
+ }
+ }
+ }
+
+
+ /********************************
+ * SUBMITTING GRADES
+ */
+
+ def submitGrade(feedback: String, score: String, apiState: String, apiKey: String, gradeProject: ProjectDetails): ValidationNEL[String, Unit] = {
+ import DefaultJsonProtocol._
+ val baseReq = url(Settings.uploadFeedbackUrl(gradeProject.courseId))
+ val withArgs = baseReq << Map("api_state" -> apiState, "score" -> score, "feedback" -> feedback) <:< Map("X-api-key" -> apiKey)
+ executeRequest(withArgs) { res =>
+ try {
+ val js = JsonParser(res)
+ val status = (js \ "status").convertTo[String]
+ if (status == "202")
+ ().successNel
+ else
+ ("Unexpected result from submit request: "+ status).failNel
+ } catch {
+ case e: Exception =>
+ ("Failed to parse response while submitting grade\n"+ res +"\n"+ fullExceptionString(e)).failNel
+ }
+ }
+ }
+
+
+ /*********************************
+ * TOOLS AND STUFF
+ */
+
+ def shaHexDigest(s: String): String = {
+ val chars = Hex.encodeHex(DigestUtils.sha(s))
+ new String(chars)
+ }
+
+
+ def fullExceptionString(e: Throwable) =
+ e.toString +"\n"+ e.getStackTrace.map(_.toString).mkString("\n")
+
+
+ /* Base 64 tools */
+
+ def encodeBase64(bytes: Array[Byte]): String =
+ new String(Base64.encodeBase64(bytes))
+
+ def decodeBase64(str: String): Array[Byte] = {
+ // codecs 1.4 has a version accepting a string, but not 1.2; jar hell.
+ Base64.decodeBase64(str.getBytes)
+ }
+}
+
+case class Challenge(email: String, challengeKey: String, state: String)
+
+case class QueueResult(apiState: String)
+
diff --git a/Scala/objsets/project/GradingFeedback.scala b/Scala/objsets/project/GradingFeedback.scala
new file mode 100644
index 0000000..5d78c54
--- /dev/null
+++ b/Scala/objsets/project/GradingFeedback.scala
@@ -0,0 +1,218 @@
+import collection.mutable.ListBuffer
+import org.apache.commons.lang3.StringEscapeUtils
+
+object GradingFeedback {
+
+ private val feedbackSummary = new ListBuffer[String]()
+ private val feedbackDetails = new ListBuffer[String]()
+
+ private def addSummary(msg: String) { feedbackSummary += msg; feedbackSummary += "\n\n" }
+ private def addDetails(msg: String) { feedbackDetails += msg; feedbackDetails += "\n\n" }
+
+ /**
+ * Converts the string to HTML - coursera displays the feedback in an html page.
+ */
+ def feedbackString(html: Boolean = true) = {
+ val total = totalGradeMessage(totalScore) + "\n\n"
+ // trim removes the newlines at the end
+ val s = (total + feedbackSummary.mkString + feedbackDetails.mkString).trim
+ if (html)
+ "<pre>"+ StringEscapeUtils.escapeHtml4(s) +"</pre>"
+ else
+ s
+ }
+
+ private var vTestScore: Double = 0d
+ private var vStyleScore: Double = 0d
+ def totalScore = vTestScore + vStyleScore
+
+ private var vMaxTestScore: Double = 0d
+ private var vMaxStyleScore: Double = 0d
+ def maxTestScore = vMaxTestScore
+ def maxStyleScore = vMaxStyleScore
+
+ // a string obtained from coursera when downloading an assignment. it has to be
+ // used again when uploading the grade.
+ var apiState: String = ""
+
+ /**
+ * `failed` means that there was an unexpected error during grading. This includes
+ * - student's code does not compile
+ * - our tests don't compile (against the student's code)
+ * - crash while executing ScalaTest (not test failures, but problems trying to run the tests!)
+ * - crash while executing the style checker (again, not finding style problems!)
+ *
+ * When failed is `true`, later grading stages will not be executed: this is handled automatically
+ * by SBT, tasks depending on a failed one are not run.
+ *
+ * However, these dependent tasks still fail (i.e. mapR on them is invoked). The variable below
+ * allows us to know if something failed before. In this case, we don't add any more things to
+ * the log. (see `ProgFunBuild.handleFailure`)
+ */
+ private var failed = false
+ def isFailed = failed
+
+ def initialize() {
+ feedbackSummary.clear()
+ feedbackDetails.clear()
+ vTestScore = 0d
+ vStyleScore = 0d
+ apiState = ""
+ failed = false
+ }
+
+ def setMaxScore(maxScore: Double, styleScoreRatio: Double) {
+ vMaxTestScore = maxScore * (1-styleScoreRatio)
+ vMaxStyleScore = maxScore * styleScoreRatio
+ }
+
+
+ /* Methods to build up the feedback log */
+
+ def downloadUnpackFailed(log: String) {
+ failed = true
+ addSummary(downloadUnpackFailedMessage)
+ addDetails("======== FAILURES WHILE DOWNLOADING OR EXTRACTING THE SUBMISSION ========")
+ addDetails(log)
+ }
+
+
+ def compileFailed(log: String) {
+ failed = true
+ addSummary(compileFailedMessage)
+ addDetails("======== COMPILATION FAILURES ========")
+ addDetails(log)
+ }
+
+ def testCompileFailed(log: String) {
+ failed = true
+ addSummary(testCompileFailedMessage)
+ addDetails("======== TEST COMPILATION FAILURES ========")
+ addDetails(log)
+ }
+
+
+
+ def allTestsPassed() {
+ addSummary(allTestsPassedMessage)
+ vTestScore = maxTestScore
+ }
+
+ def testsFailed(log: String, score: Double) {
+ addSummary(testsFailedMessage(score))
+ vTestScore = score
+ addDetails("======== LOG OF FAILED TESTS ========")
+ addDetails(log)
+ }
+
+ def testExecutionFailed(log: String) {
+ failed = true
+ addSummary(testExecutionFailedMessage)
+ addDetails("======== ERROR LOG OF TESTING TOOL ========")
+ addDetails(log)
+ }
+
+ def testExecutionDebugLog(log: String) {
+ addDetails("======== DEBUG OUTPUT OF TESTING TOOL ========")
+ addDetails(log)
+ }
+
+
+
+ def perfectStyle() {
+ addSummary(perfectStyleMessage)
+ vStyleScore = maxStyleScore
+ }
+
+ def styleProblems(log: String, score: Double) {
+ addSummary(styleProblemsMessage(score))
+ vStyleScore = score
+ addDetails("======== CODING STYLE ISSUES ========")
+ addDetails(log)
+ }
+
+
+
+ /* Feedback Messages */
+
+ private val downloadUnpackFailedMessage =
+ """We were not able to download your submission from the coursera servers, or extracting the
+ |archive containing your source code failed.
+ |
+ |If you see this error message as your grade feedback, please contact one of the teaching
+ |assistants. See below for a detailed error log.""".stripMargin
+
+ private val compileFailedMessage =
+ """We were not able to compile the source code you submitted. This is not expected to happen,
+ |because the `submit` command in SBT can only be executed if your source code compiles.
+ |
+ |Please verify the following points:
+ | - You should use the `submit` command in SBT to upload your solution
+ | - You should not perform any changes to the SBT project definition files, i.e. the *.sbt
+ | files, and the files in the `project/` directory
+ |
+ |Take a careful look at the compiler output below - maybe you can find out what the problem is.
+ |
+ |If you cannot find a solution, ask for help on the discussion forums on the course website.""".stripMargin
+
+
+ private val testCompileFailedMessage =
+ """We were not able to compile our tests, and therefore we could not correct your submission.
+ |
+ |The most likely reason for this problem is that your submitted code uses different names
+ |for methods, classes, objects or different types than expected.
+ |
+ |In principle, this can only arise if you changed some names or types in the code that we
+ |provide, for instance a method name or a parameter type.
+ |
+ |To diagnose your problem, perform the following steps:
+ | - Run the tests that we provide with our hand-out. These tests verify that all names and
+ | types are correct. In case these tests pass, but you still see this message, please post
+ | a report on the forums [1].
+ | - Take a careful look at the error messages from the Scala compiler below. They should give
+ | you a hint where your code has an unexpected shape.
+ |
+ |If you cannot find a solution, ask for help on the discussion forums on the course website.""".stripMargin
+
+
+ private def testsFailedMessage(score: Double) =
+ """The code you submitted did not pass all of our tests: your submission achieved a score of
+ |%.2f out of %.2f in our tests.
+ |
+ |In order to find bugs in your code, we advise to perform the following steps:
+ | - Take a close look at the test output that you can find below: it should point you to
+ | the part of your code that has bugs.
+ | - Run the tests that we provide with the handout on your code.
+ | - The tests we provide do not test your code in depth: they are very incomplete. In order
+ | to test more aspects of your code, write your own unit tests.
+ | - Take another very careful look at the assignment description. Try to find out if you
+ | misunderstood parts of it. While reading through the assignment, write more tests.
+ |
+ |Below you can find a short feedback for every individual test that failed.""".stripMargin.format(score, vMaxTestScore)
+
+ // def so that we read the right value of vMaxTestScore (initialize modifies it)
+ private def allTestsPassedMessage =
+ """Your solution passed all of our tests, congratulations! You obtained the maximal test
+ |score of %.2f.""".stripMargin.format(vMaxTestScore)
+
+ private val testExecutionFailedMessage =
+ """An error occured while running our tests on your submission. This is not expected to
+ |happen, it means there is a bug in our testing environment.
+ |
+ |In order for us to help you, please contact one of the teaching assistants and send
+ |them the entire feedback message that you recieved.""".stripMargin
+
+ // def so that we read the right value of vMaxStyleScore (initialize modifies it)
+ private def perfectStyleMessage =
+ """Our automated style checker tool could not find any issues with your code. You obtained the maximal
+ |style score of %.2f.""".stripMargin.format(vMaxStyleScore)
+
+
+ private def styleProblemsMessage(score: Double) =
+ """Our automated style checker tool found issues in your code with respect to coding style: it
+ |computed a style score of %.2f out of %.2f for your submission. See below for detailed feedback.""".stripMargin.format(score, vMaxStyleScore)
+
+
+ private def totalGradeMessage(score: Double) =
+ """Your overall score for this assignment is %.2f out of %.2f""".format(score, vMaxTestScore + vMaxStyleScore)
+}
diff --git a/Scala/objsets/project/ProgFunBuild.scala b/Scala/objsets/project/ProgFunBuild.scala
new file mode 100644
index 0000000..93d4b9d
--- /dev/null
+++ b/Scala/objsets/project/ProgFunBuild.scala
@@ -0,0 +1,646 @@
+import sbt._
+import Keys._
+
+import scalaz.Scalaz.mkIdentity
+import scalaz.{Success, Failure}
+import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
+
+/**
+ * See README.md for high-level overview
+ *
+ * Libraries Doc Links
+ *
+ * Coursera API
+ * - http://support.coursera.org/customer/portal/articles/573466-programming-assignments
+ * - the python script 'submit.py' that can be downloaded from the above site
+ *
+ * SBT
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-Full-Def
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-Custom-Settings
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-More-About-Settings
+ * - https://github.com/harrah/xsbt/wiki/Input-Tasks
+ * - https://github.com/harrah/xsbt/wiki/Tasks
+ * - http://harrah.github.com/xsbt/latest/api/index.html
+ * - https://groups.google.com/forum/?fromgroups#!forum/simple-build-tool
+ *
+ * Dispatch
+ * - http://dispatch-classic.databinder.net/Response+Bodies.html
+ * - http://www.flotsam.nl/dispatch-periodic-table.html
+ * - http://databinder.net/dispatch-doc/
+ *
+ * Scalaz
+ * - http://www.lunatech-research.com/archives/2012/03/02/validation-scala
+ * - http://scalaz.github.com/scalaz/scalaz-2.9.1-6.0.4/doc/index.html#scalaz.Validation
+ *
+ * Apache Commons Codec 1.4
+ * - http://www.jarvana.com/jarvana/view/commons-codec/commons-codec/1.4/commons-codec-1.4-javadoc.jar!/index.html
+ *
+ * Scalatest
+ * - http://doc.scalatest.org/1.9.1/index.html#org.scalatest.package
+ */
+object ProgFunBuild extends Build {
+
+ /***********************************************************
+ * MAIN PROJECT DEFINITION
+ */
+
+ lazy val assignmentProject = Project(id = "assignment", base = file(".")) settings(
+ // 'submit' depends on 'packageSrc', so needs to be a project-level setting: on build-level, 'packageSrc' is not defined
+ submitSetting,
+ createHandoutSetting,
+ // put all libs in the lib_managed directory, that way we can distribute eclipse project files
+ retrieveManaged := true,
+ EclipseKeys.relativizeLibs := true,
+ // Avoid generating eclipse source entries for the java directories
+ (unmanagedSourceDirectories in Compile) <<= (scalaSource in Compile)(Seq(_)),
+ (unmanagedSourceDirectories in Test) <<= (scalaSource in Test)(Seq(_)),
+ commonSourcePackages := Seq(), // see build.sbt
+ gradingTestPackages := Seq(), // see build.sbt
+ selectMainSources,
+ selectTestSources,
+ scalaTestSetting,
+ styleCheckSetting,
+ setTestPropertiesSetting,
+ setTestPropertiesHook
+ ) settings (packageSubmissionFiles: _*)
+
+
+ /***********************************************************
+ * SETTINGS AND TASKS
+ */
+
+ /** The 'submit' task uses this project name (defined in the build.sbt file) to know where to submit the solution */
+ val submitProjectName = SettingKey[String]("submitProjectName")
+
+ /** Project-specific settings, see main build.sbt */
+ val projectDetailsMap = SettingKey[Map[String, ProjectDetails]]("projectDetailsMap")
+
+ /**
+ * The files that are handed out to students. Accepts a string denoting the project name for
+ * which a handout will be generated.
+ */
+ val handoutFiles = TaskKey[String => PathFinder]("handoutFiles")
+
+ /**
+ * This setting allows to restrict the source files that are compiled and tested
+ * to one specific project. It should be either the empty string, in which case all
+ * projects are included, or one of the project names from the projectDetailsMap.
+ */
+ val currentProject = SettingKey[String]("currentProject")
+
+ /** Package names of source packages common for all projects, see comment in build.sbt */
+ val commonSourcePackages = SettingKey[Seq[String]]("commonSourcePackages")
+
+ /** Package names of test sources for grading, see comment in build.sbt */
+ val gradingTestPackages = SettingKey[Seq[String]]("gradingTestPackages")
+
+ /************************************************************
+ * SUBMITTING A SOLUTION TO COURSERA
+ */
+
+ val packageSubmission = TaskKey[File]("packageSubmission")
+
+ val packageSubmissionFiles = {
+ // the packageSrc task uses Defaults.packageSrcMappings, which is defined as concatMappings(resourceMappings, sourceMappings)
+ // in the packageSubmisson task we only use the sources, not the resources.
+ inConfig(Compile)(Defaults.packageTaskSettings(packageSubmission, Defaults.sourceMappings))
+ }
+
+ /** Task to submit a solution to coursera */
+ val submit = InputKey[Unit]("submit")
+
+ lazy val submitSetting = submit <<= inputTask { argTask =>
+ (argTask, compile in Compile, currentProject, (packageSubmission in Compile), submitProjectName, projectDetailsMap, streams) map { (args, _, currentProject, sourcesJar, projectName, detailsMap, s) =>
+ if (currentProject != "") {
+ val msg =
+ """The 'currentProject' setting is not empty: '%s'
+ |
+ |This error only appears if there are mistakes in the build scripts. Please re-download the assignment
+ |from the coursera webiste. Make sure that you did not perform any changes to the build files in the
+ |`project/` directory. If this error persits, ask for help on the course forums.""".format(currentProject).stripMargin +"\n "
+ s.log.error(msg)
+ failSubmit()
+ } else {
+ lazy val wrongNameMsg =
+ """Unknown project name: %s
+ |
+ |This error only appears if there are mistakes in the build scripts. Please re-download the assignment
+ |from the coursera webiste. Make sure that you did not perform any changes to the build files in the
+ |`project/` directory. If this error persits, ask for help on the course forums.""".format(projectName).stripMargin +"\n "
+ // log strips empty lines at the ond of `msg`. to have one we add "\n "
+ val details = detailsMap.getOrElse(projectName, {s.log.error(wrongNameMsg); failSubmit()})
+ args match {
+ case email :: otPassword :: Nil =>
+ submitSources(sourcesJar, details, email, otPassword, s.log)
+ case _ =>
+ val msg =
+ """No e-mail address and / or submission password provided. The required syntax for `submit` is
+ | submit <e-mail> <submissionPassword>
+ |
+ |The submission password, which is NOT YOUR LOGIN PASSWORD, can be obtained from the assignment page
+ | https://class.coursera.org/%s/assignment/index""".format(details.courseId).stripMargin +"\n "
+ s.log.error(msg)
+ failSubmit()
+ }
+ }
+ }
+ }
+
+
+ def submitSources(sourcesJar: File, submitProject: ProjectDetails, email: String, otPassword: String, logger: Logger) {
+ import CourseraHttp._
+ logger.info("Connecting to coursera. Obtaining challenge...")
+ val res = for {
+ challenge <- getChallenge(email, submitProject)
+ chResponse <- {
+ logger.info("Computing challenge response...")
+ challengeResponse(challenge, otPassword).successNel[String]
+ }
+ response <- {
+ logger.info("Submitting solution...")
+ submitSolution(sourcesJar, submitProject, challenge, chResponse)
+ }
+ } yield response
+
+ res match {
+ case Failure(msgs) =>
+ for (msg <- msgs.list) logger.error(msg)
+ failSubmit()
+ case Success(response) =>
+ logger.success("Your code was successfully submitted: "+ response)
+ }
+ }
+
+
+ def failSubmit(): Nothing = {
+ sys.error("Submission failed")
+ }
+
+
+
+ /***********************************************************
+ * CREATE THE HANDOUT ZIP FILE
+ */
+
+ val createHandout = InputKey[File]("createHandout")
+
+ // depends on "compile in Test" to make sure everything compiles. also makes sure that
+ // all dependencies are downloaded, because we pack the .jar files into the handout.
+ lazy val createHandoutSetting = createHandout <<= inputTask { argTask =>
+ (argTask, currentProject, baseDirectory, handoutFiles, submitProjectName, target, projectDetailsMap, compile in Test) map { (args, currentProject, basedir, filesFinder, submitProject, targetDir, detailsMap, _) =>
+ if (currentProject != "")
+ sys.error("\nthe 'currentProject' setting in build.sbt needs to be \"\" in order to create a handout")
+ else args match {
+ case handoutProjectName :: eclipseDone :: Nil if eclipseDone == "eclipseWasCalled" =>
+ if (handoutProjectName != submitProject)
+ sys.error("\nThe `submitProjectName` setting in `build.sbt` must match the project name for which a handout is generated\n ")
+ val files = filesFinder(handoutProjectName).get
+ val filesWithRelativeNames = files.x_!(relativeTo(basedir)) map {
+ case (file, name) => (file, handoutProjectName+"/"+name)
+ }
+ val targetZip = targetDir / (handoutProjectName +".zip")
+ IO.zip(filesWithRelativeNames, targetZip)
+ targetZip
+ case _ =>
+ val msg ="""
+ |
+ |Failed to create handout. Syntax: `createHandout <projectName> <eclipseWasCalled>`
+ |
+ |Valid project names are: %s
+ |
+ |The argument <eclipseWasCalled> needs to be the string "eclipseWasCalled". This is to remind
+ |you that you **need** to manually run the `eclipse` command before running `createHandout`.
+ | """.stripMargin.format(detailsMap.keys.mkString(", "))
+ sys.error(msg)
+ }
+ }
+ }
+
+
+ /************************************************************
+ * LIMITING SOURCES TO CURRENT PROJECT
+ */
+
+ def filter(basedir: File, packages: Seq[String]) = new FileFilter {
+ def accept(file: File) = {
+ basedir.equals(file) || {
+ IO.relativize(basedir, file) match {
+ case Some(str) =>
+ packages exists { pkg =>
+ str.startsWith(pkg)
+ }
+ case _ =>
+ sys.error("unexpected test file: "+ file +"\nbase dir: "+ basedir)
+ }
+ }
+ }
+ }
+
+ def projectFiles(allFiles: Seq[File], basedir: File, projectName: String, globalPackages: Seq[String], detailsMap: Map[String, ProjectDetails]) = {
+ if (projectName == "") allFiles
+ else detailsMap.get(projectName) match {
+ case Some(project) =>
+ val finder = allFiles ** filter(basedir, globalPackages :+ project.packageName)
+ finder.get
+ case None =>
+ sys.error("currentProject is set to an invalid name: "+ projectName)
+ }
+ }
+
+ /**
+ * Only include source files of 'currentProject', helpful when preparign a specific assignment.
+ * Also keeps the source packages in 'commonSourcePackages'.
+ */
+ val selectMainSources = {
+ (unmanagedSources in Compile) <<= (unmanagedSources in Compile, scalaSource in Compile, projectDetailsMap, currentProject, commonSourcePackages) map { (sources, srcMainScalaDir, detailsMap, projectName, commonSrcs) =>
+ projectFiles(sources, srcMainScalaDir, projectName, commonSrcs, detailsMap)
+ }
+ }
+
+ /**
+ * Only include the test files which are defined in the package of the current project.
+ * Also keeps test sources in packages listed in 'gradingTestPackages'.
+ */
+ val selectTestSources = {
+ (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in Test, projectDetailsMap, currentProject, gradingTestPackages) map { (sources, srcTestScalaDir, detailsMap, projectName, gradingSrcs) =>
+ projectFiles(sources, srcTestScalaDir, projectName, gradingSrcs, detailsMap)
+ }
+ }
+
+
+ /************************************************************
+ * PARAMETERS FOR RUNNING THE TESTS
+ *
+ * Setting some system properties that are parameters for the GradingSuite test
+ * suite mixin. This is for running the `test` task in SBT's JVM. When running
+ * the `scalaTest` task, the ScalaTestRunner creates a new JVM and passes the
+ * same properties.
+ */
+
+ val setTestProperties = TaskKey[Unit]("setTestProperties")
+ val setTestPropertiesSetting = setTestProperties := {
+ import scala.util.Properties._
+ import Settings._
+ setProp(scalaTestIndividualTestTimeoutProperty, individualTestTimeout.toString)
+ setProp(scalaTestDefaultWeigthProperty, scalaTestDefaultWeigth.toString)
+ }
+
+ val setTestPropertiesHook = (test in Test) <<= (test in Test).dependsOn(setTestProperties)
+
+
+ /************************************************************
+ * RUNNING WEIGHTED SCALATEST & STYLE CHECKER ON DEVELOPMENT SOURCES
+ */
+
+ def copiedResourceFiles(copied: collection.Seq[(java.io.File, java.io.File)]): List[File] = {
+ copied collect {
+ case (from, to) if to.isFile => to
+ } toList
+ }
+
+ val scalaTest = TaskKey[Unit]("scalaTest")
+ val scalaTestSetting = scalaTest <<=
+ (compile in Compile,
+ compile in Test,
+ fullClasspath in Test,
+ copyResources in Compile,
+ classDirectory in Test,
+ baseDirectory,
+ streams) map { (_, _, classpath, resources, testClasses, basedir, s) =>
+ // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed`
+ val logger = s.log
+ val outfile = basedir / Settings.testResultsFileName
+ val policyFile = basedir / Settings.policyFileName
+ val (score, maxScore, feedback, runLog) = ScalaTestRunner.runScalaTest(classpath, testClasses, outfile, policyFile, copiedResourceFiles(resources), logger.error(_))
+ logger.info(feedback)
+ logger.info("Test Score: "+ score +" out of "+ maxScore)
+ if (!runLog.isEmpty) {
+ logger.info("Console output of ScalaTest process")
+ logger.info(runLog)
+ }
+ }
+
+ val styleCheck = TaskKey[Unit]("styleCheck")
+
+ /**
+ * depend on compile to make sure the sources pass the compiler
+ */
+ val styleCheckSetting = styleCheck <<= (compile in Compile, sources in Compile, streams) map { (_, sourceFiles, s) =>
+ val logger = s.log
+ val (feedback, score) = StyleChecker.assess(sourceFiles)
+ logger.info(feedback)
+ logger.info("Style Score: "+ score +" out of "+ StyleChecker.maxResult)
+ }
+
+
+ /************************************************************
+ * PROJECT DEFINITION FOR GRADING
+ */
+
+ lazy val submissionProject = Project(id = "submission", base = file(Settings.submissionDirName)) settings(
+ /** settings we take over from the assignment project */
+ version <<= (version in assignmentProject),
+ name <<= (name in assignmentProject),
+ scalaVersion <<= (scalaVersion in assignmentProject),
+ scalacOptions <<= (scalacOptions in assignmentProject),
+ libraryDependencies <<= (libraryDependencies in assignmentProject),
+
+ /** settings specific to the grading project */
+ initGradingSetting,
+ // default value, don't change. see comment on `val partIdOfGradingProject`
+ partIdOfGradingProject := "",
+ gradeProjectDetailsSetting,
+ setMaxScoreSetting,
+ setMaxScoreHook,
+ // default value, don't change. see comment on `val apiKey`
+ apiKey := "",
+ getSubmissionSetting,
+ getSubmissionHook,
+ submissionLoggerSetting,
+ readCompileLog,
+ readTestCompileLog,
+ setTestPropertiesSetting,
+ setTestPropertiesHook,
+ resourcesFromAssignment,
+ selectResourcesForProject,
+ testSourcesFromAssignment,
+ selectTestsForProject,
+ scalaTestSubmissionSetting,
+ styleCheckSubmissionSetting,
+ gradeSetting,
+ EclipseKeys.skipProject := true
+ )
+
+ /**
+ * The assignment part id of the project to be graded. Don't hard code this setting in .sbt or .scala, this
+ * setting should remain a (command-line) parameter of the `submission/grade` task, defined when invoking sbt.
+ * See also feedback string in "val gradeProjectDetailsSetting".
+ */
+ val partIdOfGradingProject = SettingKey[String]("partIdOfGradingProject")
+
+ /**
+ * The api key to access non-public api parts on coursera. This key is secret! It's defined in
+ * 'submission/settings.sbt', which is not part of the handout.
+ *
+ * Default value 'apiKey' to make the handout sbt project work
+ * - In the handout, apiKey needs to be defined, otherwise the build doesn't compile
+ * - When correcting, we define 'apiKey' in the 'submission/sectrets.sbt' file
+ * - The value in the .sbt file will take precedence when correcting (settings in .sbt take
+ * precedence over those in .scala)
+ */
+ val apiKey = SettingKey[String]("apiKey")
+
+
+ /************************************************************
+ * GRADING INITIALIZATION
+ */
+
+ val initGrading = TaskKey[Unit]("initGrading")
+ lazy val initGradingSetting = initGrading <<= (clean, sourceDirectory, baseDirectory) map { (_, submissionSrcDir, basedir) =>
+ deleteFiles(submissionSrcDir, basedir)
+ GradingFeedback.initialize()
+ RecordingLogger.clear()
+ }
+
+ def deleteFiles(submissionSrcDir: File, basedir: File) {
+ // don't delete anything in offline mode, useful for us when hacking testing / stylechecking
+ if (!Settings.offlineMode){
+ IO.delete(submissionSrcDir)
+ IO.delete(basedir / Settings.submissionJarFileName)
+ IO.delete(basedir / Settings.testResultsFileName)
+ }
+ }
+
+ /** ProjectDetails of the project that we are grading */
+ val gradeProjectDetails = TaskKey[ProjectDetails]("gradeProjectDetails")
+
+ // here we depend on `initialize` because we already use the GradingFeedback
+ lazy val gradeProjectDetailsSetting = gradeProjectDetails <<= (initGrading, partIdOfGradingProject, projectDetailsMap in assignmentProject) map { (_, partId, detailsMap) =>
+ detailsMap.find(_._2.assignmentPartId == partId) match {
+ case Some((_, details)) =>
+ details
+ case None =>
+ val validIds = detailsMap.map(_._2.assignmentPartId)
+ val msgRaw =
+ """Unknown assignment part id: %s
+ |Valid part ids are: %s
+ |
+ |In order to grade a project, the `partIdOfGradingProject` setting has to be defined. If you are running
+ |interactively in the sbt console, type `set (partIdOfGradingProject in submissionProject) := "idString"`.
+ |When running the grading task from the command line, add the above `set` command, e.g. execute
+ |
+ | sbt 'set (partIdOfGradingProject in submissionProject) := "idString"' submission/grade"""
+ val msg = msgRaw.stripMargin.format(partId, validIds.mkString(", ")) + "\n "
+ GradingFeedback.downloadUnpackFailed(msg)
+ sys.error(msg)
+ }
+ }
+
+ val setMaxScore = TaskKey[Unit]("setMaxScore")
+ val setMaxScoreSetting = setMaxScore <<= (gradeProjectDetails) map { project =>
+ GradingFeedback.setMaxScore(project.maxScore, project.styleScoreRatio)
+ }
+
+ // set the maximal score before running compile / test / ...
+ val setMaxScoreHook = (compile in Compile) <<= (compile in Compile).dependsOn(setMaxScore)
+
+
+ /************************************************************
+ * DOWNLOADING AND EXTRACTING SUBMISSION
+ */
+
+ val getSubmission = TaskKey[Unit]("getSubmission")
+ val getSubmissionSetting = getSubmission <<= (baseDirectory, scalaSource in Compile) map { (baseDir, scalaSrcDir) =>
+ readAndUnpackSubmission(baseDir, scalaSrcDir)
+ }
+
+ def readAndUnpackSubmission(baseDir: File, targetSourceDir: File) {
+ try {
+ val jsonFile = baseDir / Settings.submissionJsonFileName
+ val targetJar = baseDir / Settings.submissionJarFileName
+ val res = for {
+ queueResult <- {
+ if (Settings.offlineMode) {
+ println("[not unpacking from json file]")
+ QueueResult("").successNel
+ } else {
+ CourseraHttp.readJsonFile(jsonFile, targetJar)
+ }
+ }
+ _ <- {
+ GradingFeedback.apiState = queueResult.apiState
+ CourseraHttp.unpackJar(targetJar, targetSourceDir)
+ }
+ } yield ()
+
+ res match {
+ case Failure(msgs) =>
+ GradingFeedback.downloadUnpackFailed(msgs.list.mkString("\n"))
+ case _ =>
+ ()
+ }
+ } catch {
+ case e: Throwable =>
+ // generate some useful feedback in case something fails
+ GradingFeedback.downloadUnpackFailed(CourseraHttp.fullExceptionString(e))
+ throw e
+ }
+ if (GradingFeedback.isFailed) failDownloadUnpack()
+ }
+
+ // dependsOn makes sure that `getSubmission` is executed *before* `unmanagedSources`
+ val getSubmissionHook = (unmanagedSources in Compile) <<= (unmanagedSources in Compile).dependsOn(getSubmission)
+
+ def failDownloadUnpack(): Nothing = {
+ sys.error("Download or Unpack failed")
+ }
+
+ /************************************************************
+ * READING COMPILATION AND TEST COMPILATION LOGS
+ */
+
+
+ // extraLoggers need to be defined globally. (extraLoggers in Compile) does not work - sbt only
+ // looks at the global extraLoggers when creating the LogManager.
+ val submissionLoggerSetting = extraLoggers ~= { currentFunction =>
+ (key: ScopedKey[_]) => {
+ new FullLogger(RecordingLogger) +: currentFunction(key)
+ }
+ }
+
+ val readCompileLog = (compile in Compile) <<= (compile in Compile) mapR handleFailure(compileFailed)
+ val readTestCompileLog = (compile in Test) <<= (compile in Test) mapR handleFailure(compileTestFailed)
+
+ def handleFailure[R](handler: (Incomplete, String) => Unit) = (res: Result[R]) => res match {
+ case Inc(inc) =>
+ // Only call the handler of the task that actually failed. See comment in GradingFeedback.failed
+ if (!GradingFeedback.isFailed)
+ handler(inc, RecordingLogger.readAndClear())
+ throw inc
+ case Value(v) => v
+ }
+
+ def compileFailed(inc: Incomplete, log: String) {
+ GradingFeedback.compileFailed(log)
+ }
+
+ def compileTestFailed(inc: Incomplete, log: String) {
+ GradingFeedback.testCompileFailed(log)
+ }
+
+
+ /************************************************************
+ * RUNNING SCALATEST
+ */
+
+ /** The submission project takes resource files from the main (assignment) project */
+ val resourcesFromAssignment = {
+ (resourceDirectory in Compile) <<= (resourceDirectory in (assignmentProject, Compile))
+ }
+
+ /**
+ * Only include the resource files which are defined in the package of the current project.
+ */
+ val selectResourcesForProject = {
+ (resources in Compile) <<= (resources in Compile, resourceDirectory in (assignmentProject, Compile), gradeProjectDetails) map { (resources, resourceDir, project) =>
+ val finder = resources ** filter(resourceDir, List(project.packageName))
+ finder.get
+ }
+ }
+
+ /** The submission project takes test files from the main (assignment) project */
+ val testSourcesFromAssignment = {
+ (sourceDirectory in Test) <<= (sourceDirectory in (assignmentProject, Test))
+ }
+
+ /**
+ * Only include the test files which are defined in the package of the current project.
+ * Also keeps test sources in packages listed in 'gradingTestPackages'
+ */
+ val selectTestsForProject = {
+ (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in (assignmentProject, Test), gradingTestPackages in assignmentProject, gradeProjectDetails) map { (sources, testSrcScalaDir, gradingSrcs, project) =>
+ val finder = sources ** filter(testSrcScalaDir, gradingSrcs :+ project.packageName)
+ finder.get
+ }
+ }
+
+ val scalaTestSubmission = TaskKey[Unit]("scalaTestSubmission")
+ val scalaTestSubmissionSetting = scalaTestSubmission <<=
+ (compile in Compile,
+ compile in Test,
+ fullClasspath in Test,
+ copyResources in Compile,
+ classDirectory in Test,
+ baseDirectory) map { (_, _, classpath, resources, testClasses, basedir) =>
+ // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed`
+ val outfile = basedir / Settings.testResultsFileName
+ val policyFile = basedir / ".." / Settings.policyFileName
+ ScalaTestRunner.scalaTestGrade(classpath, testClasses, outfile, policyFile, copiedResourceFiles(resources))
+ }
+
+
+
+ /************************************************************
+ * STYLE CHECKING
+ */
+
+ val styleCheckSubmission = TaskKey[Unit]("styleCheckSubmission")
+
+ /**
+ * - depend on scalaTestSubmission so that test get executed before style checking. the transitive
+ * dependencies also ensures that the "sources in Compile" don't have compilation errors
+ * - using `map` makes this task execute only if all its dependencies succeeded.
+ */
+ val styleCheckSubmissionSetting = styleCheckSubmission <<= (sources in Compile, scalaTestSubmission) map { (sourceFiles, _) =>
+ val (feedback, score) = StyleChecker.assess(sourceFiles)
+ if (score == StyleChecker.maxResult) {
+ GradingFeedback.perfectStyle()
+ } else {
+ val gradeScore = GradingFeedback.maxStyleScore * score / StyleChecker.maxResult
+ GradingFeedback.styleProblems(feedback, gradeScore)
+ }
+ }
+
+
+
+ /************************************************************
+ * SUBMITTING GRADES TO COURSERA
+ */
+
+ val grade = TaskKey[Unit]("grade")
+
+ // mapR: submit the grade / feedback in any case, also on failure
+ val gradeSetting = grade <<= (scalaTestSubmission, styleCheckSubmission, apiKey, gradeProjectDetails, streams) mapR { (_, _, apiKeyR, projectDetailsR, s) =>
+ val logOpt = s match {
+ case Value(v) => Some(v.log)
+ case _ => None
+ }
+ logOpt.foreach(_.info(GradingFeedback.feedbackString(html = false)))
+ apiKeyR match {
+ case Value(apiKey) if (!apiKey.isEmpty) =>
+ // if build failed early, we did not even get the api key from the submission queue
+ if (!GradingFeedback.apiState.isEmpty && !Settings.offlineMode) {
+ val scoreString = "%.2f".format(GradingFeedback.totalScore)
+ val Value(projectDetails) = projectDetailsR
+ CourseraHttp.submitGrade(GradingFeedback.feedbackString(), scoreString, GradingFeedback.apiState, apiKey, projectDetails) match {
+ case Failure(msgs) =>
+ sys.error(msgs.list.mkString("\n"))
+ case _ =>
+ ()
+ }
+ } else if(Settings.offlineMode) {
+ logOpt.foreach(_.info(" \nSettings.offlineMode enabled, not uploading the feedback"))
+ } else {
+ sys.error("Could not submit feedback - apiState not initialized")
+ }
+ case _ =>
+ sys.error("Could not submit feedback - apiKey not defined: "+ apiKeyR)
+ }
+ }
+}
+
+case class ProjectDetails(packageName: String,
+ assignmentPartId: String,
+ maxScore: Double,
+ styleScoreRatio: Double,
+ courseId: String)
diff --git a/Scala/objsets/project/RecordingLogger.scala b/Scala/objsets/project/RecordingLogger.scala
new file mode 100644
index 0000000..b886768
--- /dev/null
+++ b/Scala/objsets/project/RecordingLogger.scala
@@ -0,0 +1,35 @@
+import sbt._
+import collection.mutable.ListBuffer
+
+/**
+ * Logger to capture compiler output, test output
+ */
+
+object RecordingLogger extends Logger {
+ private val buffer = ListBuffer[String]()
+
+ def hasErrors = buffer.nonEmpty
+
+ def readAndClear() = {
+ val res = buffer.mkString("\n")
+ buffer.clear()
+ res
+ }
+
+ def clear() {
+ buffer.clear()
+ }
+
+ def log(level: Level.Value, message: => String) =
+ if (level == Level.Error) {
+ buffer += message
+ }
+
+ // we don't log success here
+ def success(message: => String) = ()
+
+ // invoked when a task throws an exception. invoked late, when the exception is logged, i.e.
+ // just before returning to the prompt. therefore we do nothing: storing the exception in the
+ // buffer would happen *after* the `handleFailure` reads the buffer.
+ def trace(t: => Throwable) = ()
+}
diff --git a/Scala/objsets/project/RichJsValue.scala b/Scala/objsets/project/RichJsValue.scala
new file mode 100644
index 0000000..ca9ad94
--- /dev/null
+++ b/Scala/objsets/project/RichJsValue.scala
@@ -0,0 +1,28 @@
+import cc.spray.json._
+
+class RichJsValue(js: JsValue) {
+ def \ (name: String): JsValue = js match {
+ case JsObject(fields) =>
+ fields(name)
+ case _ =>
+ throw new IllegalArgumentException("Cannot select field "+ name +" from non-JsObject "+ js)
+ }
+
+ def hasFieldNamed(name: String) = js match {
+ case JsObject(fields) =>
+ fields.contains(name)
+ case _ =>
+ false
+ }
+
+ def arrayValues: List[JsValue] = js match {
+ case JsArray(values) =>
+ values
+ case _ =>
+ throw new IllegalArgumentException("Trying to select values from non-JsArray"+ js)
+ }
+}
+
+object RichJsValue {
+ implicit def enrichJsValue(js: JsValue) = new RichJsValue(js)
+}
diff --git a/Scala/objsets/project/ScalaTestRunner.scala b/Scala/objsets/project/ScalaTestRunner.scala
new file mode 100644
index 0000000..af63495
--- /dev/null
+++ b/Scala/objsets/project/ScalaTestRunner.scala
@@ -0,0 +1,169 @@
+import sbt._
+import Keys._
+import sys.process.{Process => SysProc, ProcessLogger}
+import java.util.concurrent._
+import collection.mutable.ListBuffer
+
+object ScalaTestRunner {
+
+ class LimitedStringBuffer {
+ val buf = new ListBuffer[String]()
+ private var lines = 0
+ private var lengthCropped = false
+
+ override def toString() = buf.mkString("\n").trim
+
+ def append(s: String) =
+ if (lines < Settings.maxOutputLines) {
+ val shortS =
+ if (s.length > Settings.maxOutputLineLength) {
+ if (!lengthCropped) {
+ val msg =
+ """WARNING: OUTPUT LINES CROPPED
+ |Your program generates very long lines on the standard (or error) output. Some of
+ |the lines have been cropped.
+ |This should not have an impact on your grade or the grading process; however it is
+ |bad style to leave `print` statements in production code, so consider removing and
+ |replacing them by proper tests.
+ |""".stripMargin
+ buf.prepend(msg)
+ lengthCropped = true
+ }
+ s.substring(0, Settings.maxOutputLineLength)
+ } else s
+ buf.append(shortS)
+ lines += 1
+ } else if (lines == Settings.maxOutputLines) {
+ val msg =
+ """WARNING: PROGRAM OUTPUT TOO LONG
+ |Your program generates massive amounts of data on the standard (or error) output.
+ |You are probably using `print` statements to debug your code.
+ |This should not have an impact on your grade or the grading process; however it is
+ |bad style to leave `print` statements in production code, so consider removing and
+ |replacing them by proper tests.
+ |""".stripMargin
+ buf.prepend(msg)
+ lines += 1
+ }
+ }
+
+ private def forkProcess(proc: SysProc, timeout: Int) {
+ val executor = Executors.newSingleThreadExecutor()
+ val future: Future[Unit] = executor.submit(new Callable[Unit] {
+ def call { proc.exitValue() }
+ })
+ try {
+ future.get(timeout, TimeUnit.SECONDS)
+ } catch {
+ case to: TimeoutException =>
+ future.cancel(true)
+ throw to
+ } finally {
+ executor.shutdown()
+ }
+ }
+
+ private def runPathString(file: File) = file.getAbsolutePath().replace(" ", "\\ ")
+
+ private def extractWeights(s: String, logError: String => Unit) = {
+ try {
+ val (nums, rest) = s.span(c => c != '\n')
+ val List(grade, max) = nums.split(';').toList
+ (grade.toInt, max.toInt, rest.drop(1))
+ } catch {
+ case e: Throwable =>
+ val msg = "Could not extract weight from grading feedback\n"+ s
+ logError(msg)
+ throw e
+ }
+ }
+
+
+ def runScalaTest(classpath: Classpath, testClasses: File, outfile: File, policyFile: File, resourceFiles: List[File], logError: String => Unit) = {
+ val classpathString = classpath map {
+ case Attributed(file) => file.getAbsolutePath()
+ } mkString(":")
+
+ val testRunpath = runPathString(testClasses)
+
+ val outfileStr = outfile.getAbsolutePath
+ val policyFileStr = policyFile.getAbsolutePath
+ val resourceFilesString = resourceFiles.map(_.getAbsolutePath).mkString(":")
+ // Deleting the file is helpful: it makes reading the file below crash in case ScalaTest doesn't
+ // run as expected. Problem is, it's hard to detect if ScalaTest ran successfully or not: it
+ // exits with non-zero if there are failed tests, and also if it crashes...
+ new java.io.File(outfileStr).delete()
+
+ def prop(name: String, value: String) = "-D"+ name +"="+ value
+
+ // we don't specify "-w packageToTest" - the build file only compiles the tests
+ // for the current project. so we don't need to do it again here.
+ val cmd = "java" ::
+ "-Djava.security.manager" ::
+ prop("java.security.policy", policyFileStr) ::
+ prop(Settings.scalaTestReportFileProperty, outfileStr) ::
+ prop(Settings.scalaTestIndividualTestTimeoutProperty, Settings.individualTestTimeout.toString) ::
+ prop(Settings.scalaTestReadableFilesProperty, resourceFilesString) ::
+ prop(Settings.scalaTestDefaultWeigthProperty, Settings.scalaTestDefaultWeigth.toString) ::
+ "-cp" :: classpathString ::
+ "org.scalatest.tools.Runner" ::
+ "-R" :: testRunpath ::
+ "-C" :: "grading.CourseraReporter" ::
+ Nil
+
+ // process deadlocks in Runner.PassFailReporter.allTestsPassed on runDoneSemaphore.acquire() when
+ // something is wrong, e.g. when there's an error.. So we have to run it with a timeout.
+
+ val out = new LimitedStringBuffer()
+ var p: SysProc = null
+ try {
+ p = SysProc(cmd).run(ProcessLogger(out.append(_), out.append(_)))
+ forkProcess(p, Settings.scalaTestTimeout)
+ } catch {
+ case e: TimeoutException =>
+ val msg = "Timeout when running ScalaTest\n"+ out.toString()
+ logError(msg)
+ p.destroy()
+ sys.error(msg)
+
+ case e: Throwable =>
+ val msg = "Error occured while running the ScalaTest command\n"+ e.toString +"\n"+ out.toString()
+ logError(msg)
+ p.destroy()
+ throw e
+ }
+
+
+ val feedbackFileContent = try {
+ io.Source.fromFile(outfileStr).mkString
+ } catch {
+ case e: Throwable =>
+ val msg = "Error occured while reading the output file of ScalaTest\n"+ e.toString +"\n"+ out.toString()
+ logError(msg)
+ throw e
+ }
+
+ val (score, maxScore, feedback) = extractWeights(feedbackFileContent, logError)
+ val runLog = out.toString()
+ (score, maxScore, feedback, runLog)
+ }
+
+ def scalaTestGrade(classpath: Classpath, testClasses: File, outfile: File, policyFile: File, resourceFiles: List[File]) {
+ val (score, maxScore, feedback, runLog) = runScalaTest(classpath, testClasses, outfile, policyFile, resourceFiles, GradingFeedback.testExecutionFailed)
+ if (score == maxScore) {
+ GradingFeedback.allTestsPassed()
+ } else {
+ val scaledScore = GradingFeedback.maxTestScore * score / maxScore
+ GradingFeedback.testsFailed(feedback, scaledScore)
+ }
+
+ // The output `out` should in principle be empty: the reporter we use writes its results to a file.
+ // however, `out` contains valuable logs in case scalatest fails. We need to put them into the student
+ // feedback in order to have a chance of debugging problems.
+
+ if (!runLog.isEmpty) {
+ GradingFeedback.testExecutionDebugLog(runLog)
+ }
+ }
+}
+
diff --git a/Scala/objsets/project/Settings.scala b/Scala/objsets/project/Settings.scala
new file mode 100644
index 0000000..c8de201
--- /dev/null
+++ b/Scala/objsets/project/Settings.scala
@@ -0,0 +1,48 @@
+object Settings {
+ // when changing this, also look at 'scripts/gradingImpl' and the files in s3/settings
+ // val courseId = "progfun-2012-001"
+
+ def challengeUrl(courseId: String) = "https://class.coursera.org/"+ courseId +"/assignment/challenge"
+
+ def submitUrl(courseId: String) = "https://class.coursera.org/"+ courseId +"/assignment/submit"
+
+ // def forumUrl(courseId: String) = "https://class.coursera.org/"+ courseId +"/forum/index"
+
+ // def submitQueueUrl(courseId: String) = "https://class.coursera.org/"+ courseId +"/assignment/api/pending_submission"
+
+ def uploadFeedbackUrl(courseId: String) = "https://class.coursera.org/"+ courseId +"/assignment/api/score"
+
+ val maxSubmitFileSize = {
+ val mb = 1024 * 1024
+ 10 * mb
+ }
+
+ val submissionDirName = "submission"
+
+ val testResultsFileName = "scalaTestLog.txt"
+ val policyFileName = "allowAllPolicy"
+ val submissionJsonFileName = "submission.json"
+ val submissionJarFileName = "submittedSrc.jar"
+
+ // time in seconds that we give scalatest for running
+ val scalaTestTimeout = 240
+ val individualTestTimeout = 30
+
+ // default weight of each test in a GradingSuite, in case no weight is given
+ val scalaTestDefaultWeigth = 10
+
+ // when students leave print statements in their code, they end up in the output of the
+ // system process running ScalaTest (ScalaTestRunner.scala); we need some limits.
+ val maxOutputLines = 10*1000
+ val maxOutputLineLength = 1000
+
+ val scalaTestReportFileProperty = "scalatest.reportFile"
+ val scalaTestIndividualTestTimeoutProperty = "scalatest.individualTestTimeout"
+ val scalaTestReadableFilesProperty = "scalatest.readableFiles"
+ val scalaTestDefaultWeigthProperty = "scalatest.defaultWeight"
+
+ // debugging / developping options
+
+ // don't decode json and unpack the submission sources, don't upload feedback
+ val offlineMode = false
+}
diff --git a/Scala/objsets/project/StyleChecker.scala b/Scala/objsets/project/StyleChecker.scala
new file mode 100644
index 0000000..fbc1cdf
--- /dev/null
+++ b/Scala/objsets/project/StyleChecker.scala
@@ -0,0 +1,77 @@
+import sbt.File
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import org.scalastyle._
+
+object StyleChecker {
+ val maxResult = 100
+
+ class CustomTextOutput[T <: FileSpec]() extends Output[T] {
+ private val messageHelper = new MessageHelper(this.getClass().getClassLoader())
+
+ var fileCount: Int = _
+ override def message(m: Message[T]): Unit = m match {
+ case StartWork() =>
+ case EndWork() =>
+ case StartFile(file) =>
+ print("Checking file " + file + "...")
+ fileCount = 0
+ case EndFile(file) =>
+ if (fileCount == 0) println(" OK!")
+ case StyleError(file, clazz, key, level, args, line, column, customMessage) =>
+ report(line, column, messageHelper.text(level.name),
+ findMessage(messageHelper, clazz, key, args, customMessage))
+ case StyleException(file, clazz, message, stacktrace, line, column) =>
+ report(line, column, "error", message)
+ }
+
+ private def report(line: Option[Int], column: Option[Int], level: String, message: String) {
+ if (fileCount == 0) println("")
+ fileCount += 1
+ println(" " + fileCount + ". " + level + pos(line, column) + ":")
+ println(" " + message)
+ }
+
+ private def pos(line: Option[Int], column: Option[Int]): String = line match {
+ case Some(line) => " at line " + line + (column match {
+ case Some(column) => " character " + column
+ case None => ""
+ })
+ case None => ""
+ }
+ }
+
+ def score(outputResult: OutputResult) = {
+ val penalties = outputResult.errors + outputResult.warnings
+ scala.math.max(maxResult - penalties, 0)
+ }
+
+ def assess(allSources: Seq[File]): (String, Int) = {
+ val configFile = new File("project/scalastyle_config.xml").getAbsolutePath
+
+ val sources = allSources.filterNot{ f =>
+ val path = f.getAbsolutePath
+ path.contains("interpreter") ||
+ path.contains("simulations") ||
+ path.contains("fetchtweets")
+ }
+
+ val messages = new ScalastyleChecker().checkFiles(
+ ScalastyleConfiguration.readFromXml(configFile),
+ Directory.getFiles(sources : _*))
+
+ val output = new ByteArrayOutputStream()
+ val outputResult = Console.withOut(new PrintStream(output)) {
+ new CustomTextOutput().output(messages)
+ }
+
+ val msg =
+ output.toString +
+ "Processed " + outputResult.files + " file(s)\n" +
+ "Found " + outputResult.errors + " errors\n" +
+ "Found " + outputResult.warnings + " warnings\n" +
+ (if (outputResult.errors+outputResult.warnings > 0) "Consult the style guide at https://class.coursera.org/progfun-002/wiki/view?page=GradingPolicy" else "")
+
+ (msg, score(outputResult))
+ }
+}
diff --git a/Scala/objsets/project/build.properties b/Scala/objsets/project/build.properties
new file mode 100644
index 0000000..4474a03
--- /dev/null
+++ b/Scala/objsets/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.12.1
diff --git a/Scala/objsets/project/buildSettings.sbt b/Scala/objsets/project/buildSettings.sbt
new file mode 100644
index 0000000..1ed6540
--- /dev/null
+++ b/Scala/objsets/project/buildSettings.sbt
@@ -0,0 +1,72 @@
+// needed for custom scalastyle package
+resolvers += "namin.github.com/maven-repository" at "http://namin.github.com/maven-repository/"
+
+resolvers += "Spray Repository" at "http://repo.spray.cc/"
+
+libraryDependencies += "net.databinder" %% "dispatch-http" % "0.8.8"
+
+ libraryDependencies += "org.scalastyle" % "scalastyle_2.9.1" % "0.1.3-SNAPSHOT"
+
+libraryDependencies += "cc.spray" %% "spray-json" % "1.1.1"
+
+// need scalatest also as a build dependency: the build implements a custom reporter
+libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1"
+
+// dispatch uses commons-codec, in version 1.4, so we can't go for 1.6.
+// libraryDependencies += "commons-codec" % "commons-codec" % "1.4"
+
+libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.1"
+
+// sbteclipse-plugin uses scalaz-core 6.0.3, so we can't go 6.0.4
+// libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.3"
+
+scalacOptions ++= Seq("-deprecation")
+
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0")
+
+// for dependency-graph plugin
+// net.virtualvoid.sbt.graph.Plugin.graphSettings
+
+
+// [info] default:default-3fdafc_2.9.1:0.1-SNAPSHOT
+// [info] +-cc.spray:spray-json_2.9.1:1.1.1
+// [info] | +-org.parboiled:parboiled-scala:1.0.2
+// [info] | | +-org.parboiled:parboiled-core:1.0.2
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-com.typesafe.sbteclipse:sbteclipse-plugin:2.1.0
+// [info] | +-com.typesafe.sbteclipse:sbteclipse-core:2.1.0
+// [info] | +-org.scalaz:scalaz-core_2.9.1:6.0.3
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-net.databinder:dispatch-http_2.9.1:0.8.8
+// [info] | +-net.databinder:dispatch-core_2.9.1:0.8.8
+// [info] | | +-org.apache.httpcomponents:httpclient:4.1.3
+// [info] | | | +-commons-codec:commons-codec:1.4
+// [info] | | | +-commons-logging:commons-logging:1.1.1
+// [info] | | | +-org.apache.httpcomponents:httpcore:4.1.4
+// [info] | | |
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-net.databinder:dispatch-futures_2.9.1:0.8.8
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.apache.httpcomponents:httpclient:4.1.3
+// [info] | | +-commons-codec:commons-codec:1.4
+// [info] | | +-commons-logging:commons-logging:1.1.1
+// [info] | | +-org.apache.httpcomponents:httpcore:4.1.4
+// [info] | |
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-org.scala-lang:scala-library:2.9.1
+// [info] +-org.scalastyle:scalastyle_2.9.1:0.1.3-SNAPSHOT
+// [info] | +-com.github.scopt:scopt_2.9.1:2.0.0
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.scalariform:scalariform_2.9.1:0.1.1
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-org.scalatest:scalatest_2.9.1:1.8
+// [info] +-org.scala-lang:scala-library:2.9.1
diff --git a/Scala/objsets/project/project/buildPlugins.sbt b/Scala/objsets/project/project/buildPlugins.sbt
new file mode 100644
index 0000000..47557f4
--- /dev/null
+++ b/Scala/objsets/project/project/buildPlugins.sbt
@@ -0,0 +1,2 @@
+// the dependency-graph plugin
+// addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.6.0")
diff --git a/Scala/objsets/project/scalastyle_config.xml b/Scala/objsets/project/scalastyle_config.xml
new file mode 100644
index 0000000..9171ed3
--- /dev/null
+++ b/Scala/objsets/project/scalastyle_config.xml
@@ -0,0 +1,136 @@
+<scalastyle commentFilter="disabled">
+ <name>scalastyle Coursera Configuration</name>
+ <check level="warning" class="org.scalastyle.file.FileTabChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.FileLengthChecker" enabled="true">
+ <parameters>
+ <parameter name="maxFileLength"><![CDATA[800]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.file.HeaderMatchesChecker" enabled="false">
+ <parameters>
+ <parameter name="header"><![CDATA[// Copyright (C) 2011-2012 the original author or authors.
+// See the LICENCE.txt file distributed with this work for additional
+// information regarding copyright ownership.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.SpacesAfterPlusChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.WhitespaceEndOfLineChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.scalariform.SpacesBeforePlusChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.FileLineLengthChecker" enabled="false">
+ <parameters>
+ <parameter name="maxLineLength"><![CDATA[160]]></parameter>
+ <parameter name="tabSize"><![CDATA[4]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ClassNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ObjectNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.PackageObjectNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[a-z][A-Za-z]*$]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.EqualsHashCodeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
+ <parameters>
+ <parameter name="illegalImports"><![CDATA[sun._,java.awt._]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ParameterNumberChecker" enabled="true">
+ <parameters>
+ <parameter name="maxParameters"><![CDATA[8]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MagicNumberChecker" enabled="false">
+ <parameters>
+ <parameter name="ignore"><![CDATA[-1,0,1,2,3]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.scalariform.ReturnChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NullChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoCloneChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoFinalizeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.CovariantEqualsChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.StructuralTypeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.file.RegexChecker" enabled="false">
+ <parameters>
+ <parameter name="regex"><![CDATA[println]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.TokenChecker" enabled="false">
+ <parameters>
+ <parameter name="regex"><![CDATA[^isInstanceOf$]]></parameter>
+ <customMessage>Avoid isInstanceOf.</customMessage>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NumberOfTypesChecker" enabled="true">
+ <parameters>
+ <parameter name="maxTypes"><![CDATA[30]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.CyclomaticComplexityChecker" enabled="true">
+ <parameters>
+ <parameter name="maximum"><![CDATA[10]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.UppercaseLChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.SimplifyBooleanExpressionChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.IfBraceChecker" enabled="false">
+ <parameters>
+ <parameter name="singleLineAllowed"><![CDATA[true]]></parameter>
+ <parameter name="doubleLineAllowed"><![CDATA[false]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodLengthChecker" enabled="true">
+ <parameters>
+ <parameter name="maxLength"><![CDATA[50]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="false">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[^A-Z].*$]]></parameter>
+ <customMessage>Method name should not start with an upper case letter.</customMessage>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NumberOfMethodsInTypeChecker" enabled="true">
+ <parameters>
+ <parameter name="maxMethods"><![CDATA[30]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.VarFieldChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.VarLocalChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.WhileChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.RedundantIfChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.TokenChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[^(ArrayList|ArrayBuffer|mutable)$]]></parameter>
+ <customMessage>Avoid using mutable collections.</customMessage>
+ </parameters>
+ </check>
+</scalastyle> \ No newline at end of file
diff --git a/Scala/objsets/src/main/scala/common/package.scala b/Scala/objsets/src/main/scala/common/package.scala
new file mode 100644
index 0000000..f1c74c3
--- /dev/null
+++ b/Scala/objsets/src/main/scala/common/package.scala
@@ -0,0 +1,40 @@
+import java.io.File
+
+package object common {
+
+ /** An alias for the `Nothing` type.
+ * Denotes that the type should be filled in.
+ */
+ type ??? = Nothing
+
+ /** An alias for the `Any` type.
+ * Denotes that the type should be filled in.
+ */
+ type *** = Any
+
+
+ /**
+ * Get a child of a file. For example,
+ *
+ * subFile(homeDir, "b", "c")
+ *
+ * corresponds to ~/b/c
+ */
+ def subFile(file: File, children: String*) = {
+ children.foldLeft(file)((file, child) => new File(file, child))
+ }
+
+ /**
+ * Get a resource from the `src/main/resources` directory. Eclipse does not copy
+ * resources to the output directory, then the class loader cannot find them.
+ */
+ def resourceAsStreamFromSrc(resourcePath: List[String]): Option[java.io.InputStream] = {
+ val classesDir = new File(getClass.getResource(".").toURI)
+ val projectDir = classesDir.getParentFile.getParentFile.getParentFile.getParentFile
+ val resourceFile = subFile(projectDir, ("src" :: "main" :: "resources" :: resourcePath): _*)
+ if (resourceFile.exists)
+ Some(new java.io.FileInputStream(resourceFile))
+ else
+ None
+ }
+}
diff --git a/Scala/objsets/src/main/scala/objsets/TweetData.scala b/Scala/objsets/src/main/scala/objsets/TweetData.scala
new file mode 100644
index 0000000..8af9e68
--- /dev/null
+++ b/Scala/objsets/src/main/scala/objsets/TweetData.scala
@@ -0,0 +1,719 @@
+package objsets
+
+// real tweet data, collected on Oct 1
+object TweetData {
+ val gizmodo = """[
+{ "user": "gizmodo", "text": "Kindle Paperwhite Review: Forget Everything Else, This Is the E-Reader You Want http://t.co/737W6aNC", "retweets": 51.0 },
+{ "user": "gizmodo", "text": "These new Apple patents give a sneak peek at what future iPhone cameras might have in store. http://t.co/0YT9rjxp", "retweets": 49.0 },
+{ "user": "gizmodo", "text": "Ever wonder why the sky is dark at night? Here's your answer. http://t.co/eTKxkcaE", "retweets": 86.0 },
+{ "user": "gizmodo", "text": "The head of Homeland Security stays secure by just not using email, at all. http://t.co/W6KAFEUu", "retweets": 37.0 },
+{ "user": "gizmodo", "text": "This is how graphene will grow the flexible semiconductors of the future: http://t.co/IoEvuxp4", "retweets": 43.0 },
+{ "user": "gizmodo", "text": "It's the tech-based reality TV show you never knew you didn't want: http://t.co/j9J8gAo8", "retweets": 19.0 },
+{ "user": "gizmodo", "text": "How do you make your Steve Jobs sculpture stand out? Easy, mix in some trash you stole from him. http://t.co/mvHBj3CH", "retweets": 15.0 },
+{ "user": "gizmodo", "text": "This awesome baggage roller coaster will make you wish you were a suitcase. http://t.co/ECaE2hgd", "retweets": 17.0 },
+{ "user": "gizmodo", "text": "This price cut is putting the Kindle Paperwhite and the Nook with GlowLight head to head. Fight! http://t.co/1x6nOJGY", "retweets": 15.0 },
+{ "user": "gizmodo", "text": "18 unlucky people who already broke the iPhone 5: http://t.co/9RpvX4te", "retweets": 79.0 },
+{ "user": "gizmodo", "text": "Here's how to ditch cash and start paying for everything electronically. http://t.co/tgrZ50XM", "retweets": 30.0 },
+{ "user": "gizmodo", "text": "If you're going to get a Nexus 7, you should just go out and buy it right now. http://t.co/tACY1YYG", "retweets": 33.0 },
+{ "user": "gizmodo", "text": "The one thing you can buy that will make your smartphone better: http://t.co/rlA32n4r", "retweets": 74.0 },
+{ "user": "gizmodo", "text": "Cold brew Irish coffee! It's the best part of waking up (drunk) http://t.co/cr3oCPKf", "retweets": 23.0 },
+{ "user": "gizmodo", "text": "Maybe the best reminder yet that the coffee shop is not your office: http://t.co/T8aNd3SX", "retweets": 47.0 },
+{ "user": "gizmodo", "text": "Why a brand new, billion-dollar battleship still needs old-timey wooden ladders: http://t.co/75yOeche", "retweets": 13.0 },
+{ "user": "gizmodo", "text": "Time for another story bundle! Seven crime novels, pay what you want, support indie authors! http://t.co/kI7tFMvM", "retweets": 14.0 },
+{ "user": "gizmodo", "text": "Apple's added a Maps section to its App Store to help you find an Apple Maps replacement: http://t.co/rpP0tCMI", "retweets": 114.0 },
+{ "user": "gizmodo", "text": "Man jailed for accidentally sexting his whole address book (including some tweens) http://t.co/5IxaTHkz #WTFriday", "retweets": 65.0 },
+{ "user": "gizmodo", "text": "An Iranian news agency thought an Onion article was real--and plagiarized it: http://t.co/C4lWe1Ij", "retweets": 112.0 },
+{ "user": "gizmodo", "text": "Yep, the TSA is definitely stealing iPads: http://t.co/THYd8MKe", "retweets": 79.0 },
+{ "user": "gizmodo", "text": "A week with the iPhone 5: http://t.co/ReuK1aJs", "retweets": 111.0 },
+{ "user": "gizmodo", "text": "Tim Cook apologizes for Apple Maps and suggests you download... Bing: http://t.co/imQFFOyW", "retweets": 280.0 },
+{ "user": "gizmodo", "text": "Neil Young unveils his new Pono music player: http://t.co/EEMznUio", "retweets": 27.0 },
+{ "user": "gizmodo", "text": "This $50 stick turns any HDTV into an Android-powered smart TV: http://t.co/8FpZUnIE", "retweets": 101.0 },
+{ "user": "gizmodo", "text": "NASA has found proof of a river on Mars: http://t.co/QPPUSQXp", "retweets": 242.0 },
+{ "user": "gizmodo", "text": "14 people who should be ashamed of their @foursquare mayorship http://t.co/7AEHQJLT", "retweets": 50.0 },
+{ "user": "gizmodo", "text": "This team of quadrocopter drones can throw and catch better than you http://t.co/GYdhzZMq", "retweets": 32.0 },
+{ "user": "gizmodo", "text": "Steve Jobs' reality distortion field lives on http://t.co/CDnZi4z8", "retweets": 56.0 },
+{ "user": "gizmodo", "text": "What's the worst children's toy you've ever seen? Let's bask in the awful http://t.co/fk41tJj0", "retweets": 18.0 },
+{ "user": "gizmodo", "text": "Spies like us: why we're all Big Brother now http://t.co/MB3gXB0b", "retweets": 8.0 },
+{ "user": "gizmodo", "text": "18 places carbon fiber just doesn't belong: http://t.co/vJo1Yhlj", "retweets": 21.0 },
+{ "user": "gizmodo", "text": "A woman had a new ear grown on her arm and attached to her head and it is PRETTY GROSS: http://t.co/NyuikEmP", "retweets": 88.0 },
+{ "user": "gizmodo", "text": "That iron man found by Nazis? It's of extraterrestrial origin. Yes, really. http://t.co/hRR7oufa", "retweets": 92.0 },
+{ "user": "gizmodo", "text": "The new best desk toy since... maybe ever? http://t.co/IwuHYYgI", "retweets": 64.0 },
+{ "user": "gizmodo", "text": "Poop snow on ancient burial ground. This can't end well. http://t.co/DAmSgIiu", "retweets": 30.0 },
+{ "user": "gizmodo", "text": "Giant electronic circuits make wonderfully geeky art: http://t.co/um99UW4Y", "retweets": 20.0 },
+{ "user": "gizmodo", "text": "This little robot will help make all that wine you drink: http://t.co/a6W5wY0p", "retweets": 16.0 },
+{ "user": "gizmodo", "text": "Facebook's finally deleting thousands of face accounts: http://t.co/3quTouWv", "retweets": 51.0 },
+{ "user": "gizmodo", "text": "These people actually really, really love Apple Maps: http://t.co/VvWsa5V7", "retweets": 29.0 },
+{ "user": "gizmodo", "text": "Is your iPhone 5 camera seeing purple? Like, where it shouldn't be? http://t.co/EBnaMfFR", "retweets": 37.0 },
+{ "user": "gizmodo", "text": "Ooh, a galaxy-shooting camera you might actually afford http://t.co/VLXkarGV", "retweets": 22.0 },
+{ "user": "gizmodo", "text": "Is this the most stunning shuttle flyover ever? http://t.co/IYKbjmnx", "retweets": 59.0 },
+{ "user": "gizmodo", "text": "Why Apple really ditched Google Maps http://t.co/evVBDYCu", "retweets": 104.0 },
+{ "user": "gizmodo", "text": "Why is this part of Facebook broken? http://t.co/Y9Ur2U5T", "retweets": 12.0 },
+{ "user": "gizmodo", "text": "Don't buy cheap iPhone 5 cables because they don't actually exist yet http://t.co/3LHLeCdO", "retweets": 56.0 },
+{ "user": "gizmodo", "text": "How vicious spyware contaminated hundreds of thousands of rental PCs: http://t.co/MlrUDWZe", "retweets": 12.0 },
+{ "user": "gizmodo", "text": "How Jack Daniels hand crafts every barrel for aging http://t.co/QeYKZDNv", "retweets": 54.0 },
+{ "user": "gizmodo", "text": "Is putting pictures of missing children on 404 pages effective? http://t.co/WFh9tiZU", "retweets": 26.0 },
+{ "user": "gizmodo", "text": "The gigantic crane that builds even more gigantic aircraft carriers http://t.co/KraKD1sE", "retweets": 17.0 },
+{ "user": "gizmodo", "text": "The 20 most obvious PINs are painfully obvious http://t.co/Du9BYjeR", "retweets": 38.0 },
+{ "user": "gizmodo", "text": "This is the closest, clearest view of Mars yet http://t.co/VrbZRVgR", "retweets": 45.0 },
+{ "user": "gizmodo", "text": "Meet the new Apple, where things don't just work http://t.co/XS36kHWi", "retweets": 146.0 },
+{ "user": "gizmodo", "text": "Cheap goodies on Google Play for the next five days: http://t.co/dT3bRBzZ", "retweets": 52.0 },
+{ "user": "gizmodo", "text": "Do phones need to come with chargers these days? http://t.co/AZMsPUeD", "retweets": 21.0 },
+{ "user": "gizmodo", "text": "The font on your car's dash might increase your risk of crashing: http://t.co/R6e6QiWp", "retweets": 27.0 },
+{ "user": "gizmodo", "text": "Holy crap, self-driving cars are now legal in California! http://t.co/nSOwBSZ1", "retweets": 277.0 },
+{ "user": "gizmodo", "text": "Judging by this photo, joining the USAF makes you a superhero http://t.co/XlCb7nK8", "retweets": 16.0 },
+{ "user": "gizmodo", "text": "Warning: Security bug can wipe out your Android phone just by visiting a web page-not only limited to Samsung http://t.co/0y6vnOKw", "retweets": 290.0 },
+{ "user": "gizmodo", "text": "Nikon D600 review: Images this spectacular have never been so cheap http://t.co/PK4LCOwX", "retweets": 33.0 },
+{ "user": "gizmodo", "text": "Oh good, Iran's homemade war drone is in the air http://t.co/ySXchFfv", "retweets": 40.0 },
+{ "user": "gizmodo", "text": "BTW, this is @brentrose tweeting for Gizmodo this week. Follow for witticisms and euphemisms.", "retweets": 0.0 },
+{ "user": "gizmodo", "text": "Major Samsung security bug can wipe your Galaxy phone (updating) http://t.co/n5yDZ3dh", "retweets": 120.0 },
+{ "user": "gizmodo", "text": "Aboard the ligher, smarter, deadlier aircraft carrier of tomorrow http://t.co/xVjQAr6J", "retweets": 22.0 },
+{ "user": "gizmodo", "text": "NASA publishes the most detailed photo of the universe ever captured, and it's stunning http://t.co/68pDKByp", "retweets": 192.0 },
+{ "user": "gizmodo", "text": "How NASA prevents a space plague outbreak http://t.co/jIeI7hPX", "retweets": 20.0 },
+{ "user": "gizmodo", "text": "iPhone 5's brain dissected. Guess what, it's made by Samsung. http://t.co/wSyjvpDc", "retweets": 321.0 },
+{ "user": "gizmodo", "text": "BlackBerry 10 adds so many features that so few people want http://t.co/eaQM0vlS", "retweets": 58.0 },
+{ "user": "gizmodo", "text": "The supercomputer that houses the entire universe http://t.co/KQciouL2", "retweets": 47.0 },
+{ "user": "gizmodo", "text": "Gambling website refunds everyone's Packers bets because of horrible refs http://t.co/bwmgYBDq", "retweets": 121.0 },
+{ "user": "gizmodo", "text": "12 deadly inventions that killed their creators (not for the faint of heart) http://t.co/BCwRAzhe", "retweets": 82.0 },
+{ "user": "gizmodo", "text": "You won't believe this amazing this wildlife photography contest winner http://t.co/nHIRgFVG", "retweets": 20.0 },
+{ "user": "gizmodo", "text": "How to build an iPhone 5 dock for $1.27 http://t.co/kqsQ1GIV", "retweets": 65.0 },
+{ "user": "gizmodo", "text": "Would you eat this disgusting stuff? (Spoiler: yes, you would) http://t.co/mRAmDRpk", "retweets": 25.0 },
+{ "user": "gizmodo", "text": "The iPhone 5 'shortage' is apparently a result of its new ultra-thin display: http://t.co/RqUSuYif", "retweets": 48.0 },
+{ "user": "gizmodo", "text": "How much do you care about how Twitter measures your influence? http://t.co/0fitjfCD", "retweets": 19.0 },
+{ "user": "gizmodo", "text": "Eric Schmidt confirms a Google Maps app on iOS 6 is still some way off: http://t.co/bobRuY06", "retweets": 76.0 },
+{ "user": "gizmodo", "text": "Breville YouBrew coffee maker review: excellent, expensive, safe for idiots (like @harrysawyers) http://t.co/67wKNuLM", "retweets": 11.0 },
+{ "user": "gizmodo", "text": "Can this flashy redesign make Myspace cool again? http://t.co/OfHVPHz6", "retweets": 94.0 },
+{ "user": "gizmodo", "text": "No, your private Facebook messages have not gone public. Filthy secrets, still safe. http://t.co/RAJsAyK0", "retweets": 67.0 },
+{ "user": "gizmodo", "text": "iOS 5.1 for Apple TV brings new AirPlay goodness http://t.co/1Yj55T52", "retweets": 48.0 },
+{ "user": "gizmodo", "text": "The Facebook for poor people you didn't know existed http://t.co/9zmMTZci", "retweets": 29.0 },
+{ "user": "gizmodo", "text": "(In case you're wondering who the awesome speller is, that's @brentrose. He has an MFA.)", "retweets": 2.0 },
+{ "user": "gizmodo", "text": "iPhone 5 vs Galaxy S III: Who's screen is prettier? http://t.co/n6CbaspY", "retweets": 108.0 },
+{ "user": "gizmodo", "text": "Yeah, we live in the future, but there's all this awesome 50-year-old Jetsons tech we still don't have http://t.co/lXsqu49Z", "retweets": 46.0 },
+{ "user": "gizmodo", "text": "This tiny telescope implant gives eyesight to the blind http://t.co/W50RHP4L", "retweets": 40.0 },
+{ "user": "gizmodo", "text": "Iran shuts down Google, will completely cut citizens fff the internet http://t.co/60ZucBic", "retweets": 146.0 },
+{ "user": "gizmodo", "text": "10 stupid, crazy, wonderful gadget fails http://t.co/p3Al28G5", "retweets": 32.0 },
+{ "user": "gizmodo", "text": "Watch an NBA player dunk the iPhone 5. Boomshakalaka! http://t.co/UQAX0awf", "retweets": 47.0 },
+{ "user": "gizmodo", "text": "Is your iPhone 5... rattling? http://t.co/mn0r2dhb", "retweets": 67.0 },
+{ "user": "gizmodo", "text": "The definitive comparison of iOS 5 Google Maps vs iOS 6 Apple Maps in one single image: http://t.co/fTwTfVMy", "retweets": 191.0 },
+{ "user": "gizmodo", "text": "Study splits breast cancer into four treatable types: http://t.co/myTn3LHu", "retweets": 23.0 },
+{ "user": "gizmodo", "text": "IBM's supercomputer genius Watson is headed for the cloud: http://t.co/Z1psttXB", "retweets": 59.0 },
+{ "user": "gizmodo", "text": "Nuance is planning to develop voice recognition which works while your phone sleeps. Good idea? http://t.co/WEXD9bQa", "retweets": 25.0 },
+{ "user": "gizmodo", "text": "Is your new iPhone picking up more scratches than you'd like? http://t.co/DGEiawOi", "retweets": 35.0 },
+{ "user": "gizmodo", "text": "Foxconn has shut down a factory after 2,000 of its employees started a massive brawl. http://t.co/d7TXo3K9", "retweets": 90.0 },
+{ "user": "gizmodo", "text": "If you thought the NES Zapper was cool before, watch it set things on fire with a laser. http://t.co/t3f0oE8O", "retweets": 37.0 },
+{ "user": "gizmodo", "text": "Want to hack NFC to get free train rides? There's an app for that. http://t.co/eAp6yTbE", "retweets": 82.0 },
+{ "user": "gizmodo", "text": "The weirdest thing people hate about the iPhone 5: http://t.co/GMwuRp8D", "retweets": 202.0 },
+{ "user": "gizmodo", "text": "Data centers waste a completely absurd amount of energy. http://t.co/NcO9pXqb", "retweets": 58.0 }
+]"""
+ val TechCrunch = """[
+{ "user": "TechCrunch", "text": "Resignation Media Hires CEO John Ellis To Run Tapiture, Its Fast-Growing Pinterest For Men http://t.co/ctn7oWJc by @anthonyha", "retweets": 18.0 },
+{ "user": "TechCrunch", "text": "FreedomPop Opens Its Freemium Internet Service To The Masses With New Public Beta http://t.co/35mA9Adp by @chrisvelazco", "retweets": 27.0 },
+{ "user": "TechCrunch", "text": "Dish And The Dream Of Internet TV http://t.co/y8KcSl8G by @ryanlawler", "retweets": 25.0 },
+{ "user": "TechCrunch", "text": "Adobe's Acrobat XI Boasts New PDF Editor And Touch-Friendly Interface ? Upgrades Start At $139 http://t.co/1YDWvlVI by @anthonyha", "retweets": 26.0 },
+{ "user": "TechCrunch", "text": "Testing Out Bodymetrics, The Startup That Wants To Be A Denim Shopper's Best Friend [TCTV] http://t.co/sPe6wA02 by @loyalelectron", "retweets": 22.0 },
+{ "user": "TechCrunch", "text": "Up Close With The Next Big Home Commodity: LED Lighting http://t.co/nGPSMnMH", "retweets": 17.0 },
+{ "user": "TechCrunch", "text": "Cloning Instagram For Video Will Not Revolutionize Mobile Video http://t.co/B5DMkSnQ by @sandeepcasi", "retweets": 43.0 },
+{ "user": "TechCrunch", "text": "(R)evolution http://t.co/dzQFqjBh by @sarahintampa", "retweets": 20.0 },
+{ "user": "TechCrunch", "text": "An Analysis Of Market Demand For Web Programming Languages http://t.co/gdYrXz7i by @marcgayle", "retweets": 91.0 },
+{ "user": "TechCrunch", "text": "The Kindle Paperwhite Is A Reader's Dream http://t.co/jGslGazO by @johnbiggs", "retweets": 61.0 },
+{ "user": "TechCrunch", "text": "Soon-To-Be-Acquired BlueSprig's AirCover Family Locator Is An iOS/Android App That Lets You Track ... http://t.co/qSQquuLS by @ingridlunden", "retweets": 24.0 },
+{ "user": "TechCrunch", "text": "Data Markets: The Emerging Data Economy http://t.co/lnTekycH by @gilelbaz", "retweets": 63.0 },
+{ "user": "TechCrunch", "text": "CoCoon, The Newest Home For Startups In Hong Kong http://t.co/IN164KNl by @mulligan", "retweets": 35.0 },
+{ "user": "TechCrunch", "text": "Imagine No Ads On Facebook. It's Easy If You Try http://t.co/kWSYY2v8 by @joshconstine", "retweets": 70.0 },
+{ "user": "TechCrunch", "text": "Microsoft Needs Windows Phone 7 - Not WP8 - To Win Significant Mobile Market Share http://t.co/X71YJ0MD by @riptari", "retweets": 36.0 },
+{ "user": "TechCrunch", "text": "Open Source Fear Mongering Is Ridiculous With The Advent Of Open APIs http://t.co/evVLkzCu by @alexwilliams", "retweets": 41.0 },
+{ "user": "TechCrunch", "text": "Logitech UE Boombox And Mobile Boombox Review: Bluetooth Speakers With A Rich Sound http://t.co/ZpewcBEX by @romaindillet", "retweets": 17.0 },
+{ "user": "TechCrunch", "text": "You Don't Need A Prototype To Raise A Seed Round http://t.co/vkPaK3sM by @VCMike", "retweets": 59.0 },
+{ "user": "TechCrunch", "text": "Jason Calacanis' Next Act, And Another Pivot For http://t.co/XBhz5HpF, As A 'Knowledge Community' http://t.co/7ZLTckm6 by @ingridlunden", "retweets": 12.0 },
+{ "user": "TechCrunch", "text": "Mass Persuasion, One User at a Time http://t.co/0Cv9fd4V by @nireyal", "retweets": 24.0 },
+{ "user": "TechCrunch", "text": "The Zooka Wireless Speaker Bar Turns Bad Audio Into Loud Noises! http://t.co/VNljouTp by @jordanrcrook", "retweets": 34.0 },
+{ "user": "TechCrunch", "text": "Iterations: We Know About B2B And B2C, But Don't Overlook B2D http://t.co/i0FIyDbX by @semil", "retweets": 55.0 },
+{ "user": "TechCrunch", "text": "5 Big Map App Issues Apple Must Solve http://t.co/XGV7PuXk", "retweets": 82.0 },
+{ "user": "TechCrunch", "text": "B&amp;N Drops Price Of Its Nook GlowLight In Advance Of Amazon's New Reader http://t.co/TkhEJpoz by @johnbiggs", "retweets": 26.0 },
+{ "user": "TechCrunch", "text": "Why Angel Investors Don?t Make Money ? And Advice For People Who Are Going To Become Angels Anyway http://t.co/u2ApCrw7", "retweets": 138.0 },
+{ "user": "TechCrunch", "text": "Barnes &amp; Noble Cuts GlowLight Nook Price To $129 As Amazon Prepares To Ship Its Own Backlit Kindles http://t.co/kiDTncdF by @chrisvelazco", "retweets": 40.0 },
+{ "user": "TechCrunch", "text": "Games Are A Difficult Investment Proposition, But Crowdfunding Could Change That http://t.co/EvpdbrsS", "retweets": 43.0 },
+{ "user": "TechCrunch", "text": "Simon Cowell And http://t.co/7lk1PBvp Are Creating An X Factor To Find The Next Mark Zuckerberg http://t.co/ZesTDWMs by @leenarao", "retweets": 192.0 },
+{ "user": "TechCrunch", "text": "How Technology Is Empowering Teachers, Minting Millionaires, And Improving Education http://t.co/EgFaGI6w by @ripemp", "retweets": 154.0 },
+{ "user": "TechCrunch", "text": "Italians Take Up The Torch To Ignite Their Own Tech Startup Scene http://t.co/zAuoFO4t by @mikebutcher", "retweets": 89.0 },
+{ "user": "TechCrunch", "text": "A Tech Way Around \"Creative Block\" http://t.co/H6eOyiay by @alexcornell", "retweets": 50.0 },
+{ "user": "TechCrunch", "text": "Klout Would Like Potential Employers To Consider Your Score Before Hiring You. And That's Stupid. http://t.co/YmnMj271 by @thatdrew", "retweets": 204.0 },
+{ "user": "TechCrunch", "text": "A Venture Capitalist's E-Commerce Shopping List http://t.co/tA7VbXBT by @VCSerge", "retweets": 55.0 },
+{ "user": "TechCrunch", "text": "Five Big Changes In The iOS 6 App Store (And What Developers Should Do) http://t.co/pkMSN96G by @sarahintampa", "retweets": 76.0 },
+{ "user": "TechCrunch", "text": "Canadian Internet Provider Rogers Experiencing Major, Prolonged Outage http://t.co/xIKbMsou by @drizzled", "retweets": 134.0 },
+{ "user": "TechCrunch", "text": "Hate http://t.co/y6Udo2nZ", "retweets": 38.0 },
+{ "user": "TechCrunch", "text": "The Search For Minority Entrepreneurs Is Over -- Now They Need To Be Ready For Investors http://t.co/8PE4Jjp3 by @waynesutton", "retweets": 59.0 },
+{ "user": "TechCrunch", "text": "Kickstarter: Helios, An iPhone Telepresence Rig On A Budget http://t.co/EgEKtsvt by @johnbiggs", "retweets": 33.0 },
+{ "user": "TechCrunch", "text": "Facebook Updates Messenger For iOS With New Chat UI, iOS 6 And iPhone 5 Support http://t.co/Ffc2a6Ib by @fredericl", "retweets": 66.0 },
+{ "user": "TechCrunch", "text": "The Next Big E-Commerce Wave: Vertically Integrated Commerce http://t.co/MEND0OQk by @bwertz", "retweets": 132.0 },
+{ "user": "TechCrunch", "text": "Bootstrap's Maintainers Leave Twitter For GitHub And Obvious Corp., Will Move It Into Its Own Organiz... http://t.co/jznaaxc1 by @fredericl", "retweets": 76.0 },
+{ "user": "TechCrunch", "text": "Should You Trust Your Gut? The Answer Is Yes. http://t.co/8TlMTip1", "retweets": 74.0 },
+{ "user": "TechCrunch", "text": "PSA For Win8 Devs: The Only Way To Distribute Your Metro Apps Is Through The Windows Store http://t.co/v26GTcsO by @fredericl", "retweets": 22.0 },
+{ "user": "TechCrunch", "text": "Gillmor Gang: Platformicide http://t.co/Sk9zwRw4 by @stevegillmor", "retweets": 17.0 },
+{ "user": "TechCrunch", "text": "Apple's Maps Is A Black Eye, Nothing More http://t.co/XK9H5B5h by @jdalrymple", "retweets": 71.0 },
+{ "user": "TechCrunch", "text": "Lessons From The Dramatic Slow-Motion Death Of Wikitravel http://t.co/u7BUYnwc by @rezendi", "retweets": 43.0 },
+{ "user": "TechCrunch", "text": "Startup Success Requires The Drive For Data http://t.co/Yl2JpFCI by @jeffma", "retweets": 78.0 },
+{ "user": "TechCrunch", "text": "Here Are The Singers Competing In Next Week's 'American Idol For The Geek Set' http://t.co/OcBvSOLl by @anthonyha", "retweets": 20.0 },
+{ "user": "TechCrunch", "text": "Ford CTO Paul Mascarenas On Bridging The Worlds Of Silicon Valley And Motor City [TCTV] http://t.co/oeD4AD5n by @loyalelectron", "retweets": 21.0 },
+{ "user": "TechCrunch", "text": "Apple Adds A Clarifying Description To Its \"Apps For Passbook\" Page In The App Store http://t.co/fkj4P0EP by @drizzled", "retweets": 42.0 },
+{ "user": "TechCrunch", "text": "Post-SingTel Acquisition, Photo Aggregation App Pixable Gets An Image-Centric Redesign http://t.co/pNS4tFke by @anthonyha", "retweets": 12.0 },
+{ "user": "TechCrunch", "text": "Rest In Peace, Charles Alfred Eldon: A Pioneer Of Silicon Valley, A Role Model For This New Generation http://t.co/3p1mt486 by @eldon", "retweets": 89.0 },
+{ "user": "TechCrunch", "text": "TechCrunch Giveaway: Free Tickets To Box's 2012 #BoxWorks Event http://t.co/t4l8IIrK by @elinblesener", "retweets": 35.0 },
+{ "user": "TechCrunch", "text": "Cloud Security Firm Qualys' IPO Opens At $12/Share, Raising $71.8M http://t.co/n2WNQkuH by @ingridlunden", "retweets": 18.0 },
+{ "user": "TechCrunch", "text": "The FeedBurner Deathwatch Continues: Google Kills AdSense For Feeds http://t.co/CztoKUFP by @fredericl", "retweets": 31.0 },
+{ "user": "TechCrunch", "text": "Google's \"Spring\" Cleaning In Fall: AdSense For Feeds, Classic Plus, Spreadsheet Gadgets Get The Axe http://t.co/pSeE3fCS by @thatdrew", "retweets": 26.0 },
+{ "user": "TechCrunch", "text": "Gillmor Gang Live 09.28.12 (TCTV) http://t.co/eUf0Df0q by @stevegillmor", "retweets": 6.0 },
+{ "user": "TechCrunch", "text": "Facebook Shares Jump More Than 6% After Gifts Launch. (Hooray For New Revenue Streams.) http://t.co/8fEXj2kG by @kimmaicutler", "retweets": 99.0 },
+{ "user": "TechCrunch", "text": "Evernote Listens To Unhappy Skitch 2.0 Users: Brings Back Some Old Features And Version 1.0 http://t.co/y2YRxxyJ by @fredericl", "retweets": 29.0 },
+{ "user": "TechCrunch", "text": "Groupon's Leadership Shuffle Continues: New North American Sales Head Named http://t.co/V4USZ7XU by @ingridlunden", "retweets": 19.0 },
+{ "user": "TechCrunch", "text": "Ptch Partners With Paramount, Letting Users Remix Their Own Paranormal Activity Found Footage http://t.co/28vTlzvu by @ryanlawler", "retweets": 17.0 },
+{ "user": "TechCrunch", "text": "Foursquare Partners With OpenTable To Make Reservations Simple http://t.co/EUz36s6i by @thatdrew", "retweets": 82.0 },
+{ "user": "TechCrunch", "text": "Treasure Data Launches Cloud-Based Data Warehouse With Investment From Ruby Creator Yukihiro ?Ma... http://t.co/Ds9mYFAX by @alexwilliams", "retweets": 15.0 },
+{ "user": "TechCrunch", "text": "Meet Apple's \"New CEO,\" Tom Brooks http://t.co/fdyX3Oox by @drizzled", "retweets": 54.0 },
+{ "user": "TechCrunch", "text": "Music-Sharing Startup MyStream Preps Android Launch, Looks Beyond Music http://t.co/CusbX5mh by @anthonyha", "retweets": 18.0 },
+{ "user": "TechCrunch", "text": "HP's Open webOS Goes 1.0, Can Now Be Ported To New Devices http://t.co/xljTC0L7 by @drizzled", "retweets": 60.0 },
+{ "user": "TechCrunch", "text": "Another Day, Another Eyeful Of HTC's Mildly-Updated One X+ http://t.co/LLU27dkp by @chrisvelazco", "retweets": 16.0 },
+{ "user": "TechCrunch", "text": "Color CEO Bill Nguyen Checks Out Of Day-To-Day Operations, While A New Leadership... http://t.co/vePSKt9x by @loyalelectron and @ryanlawler", "retweets": 19.0 },
+{ "user": "TechCrunch", "text": "Professor Facebook: Social Network Co-Designs New Course At General Assembly http://t.co/3UvQ3Mts by @ferenstein", "retweets": 19.0 },
+{ "user": "TechCrunch", "text": "Windows Phone Dev Center Now Provides User Review Translations For App Developers http://t.co/shAUIS64 by @drizzled", "retweets": 15.0 },
+{ "user": "TechCrunch", "text": "Sprint And Motorola Party Like It's 2010: Motorola XPRT Finally Gets A Taste Of Gingerbread http://t.co/vOJRwng8 by @romaindillet", "retweets": 7.0 },
+{ "user": "TechCrunch", "text": "U.S. Appeals Court: Samsung Free To Seek Lifting Of Galaxy Tab 10.1 Sales Injunction http://t.co/HTvIDlQU by @drizzled", "retweets": 24.0 },
+{ "user": "TechCrunch", "text": "Google Launches A New Tablet-Optimized User Interface For Flight Search http://t.co/xQOdAA60 by @fredericl", "retweets": 36.0 },
+{ "user": "TechCrunch", "text": "Twitter Announces New Improvements For Discover Tab, Adds Continuous Tweet Stream http://t.co/DpMRuDvO by @thatdrew", "retweets": 69.0 },
+{ "user": "TechCrunch", "text": "Apple Is Heavily Promoting Alternative Map Apps On The App Store http://t.co/LNf6lKQH by @thatdrew", "retweets": 77.0 },
+{ "user": "TechCrunch", "text": "PadPivot NST Review: The Best Available iPad Stand Just Got Better http://t.co/YJWkwkDd by @drizzled", "retweets": 11.0 },
+{ "user": "TechCrunch", "text": "AngelList Wings Is A Handy App For Searching AngelList On Your iPhone http://t.co/tJwqOMdi by @sarahintampa", "retweets": 7.0 },
+{ "user": "TechCrunch", "text": "Google Updates Gmail For iOS With Support For The iPhone 5's Larger Screen http://t.co/4fQ7YqYn by @fredericl", "retweets": 41.0 },
+{ "user": "TechCrunch", "text": "Well Played On Maps, Apple. Your Move, Google. http://t.co/kJvuAfw9 by @thatdrew", "retweets": 70.0 },
+{ "user": "TechCrunch", "text": "Flat-Fee, Dongle-free Card Payments Startup Emu Opens For Business In U.K., Registrations In Europe http://t.co/cILxZAVn by @riptari", "retweets": 27.0 },
+{ "user": "TechCrunch", "text": "Report: Apple's Pandora-Killer Hits Licensing Trouble With Sony/ATV http://t.co/7sV1BMb8 by @drizzled", "retweets": 55.0 },
+{ "user": "TechCrunch", "text": "LinkedIn Shuts Off API Access To Fellow Professional Social Network Viadeo For TOS Violations http://t.co/nAFHWLQ4 by @leenarao", "retweets": 31.0 },
+{ "user": "TechCrunch", "text": "iOS 6 Adoption At Just Over One Week: 60% For iPhone And 41% For iPad http://t.co/Q0HAgCz8 by @drizzled", "retweets": 125.0 },
+{ "user": "TechCrunch", "text": "Business Dashboards Get Instant Widgets, And Geckoboard Gets $1.5M From A Group Of Top Investors http://t.co/lDMDJUMQ by @ingridlunden", "retweets": 29.0 },
+{ "user": "TechCrunch", "text": "BlackBerry 10 Touch, Qwerty Devices Leak In Video; RIM Wants Lady Gaga To Help Sell BB10 http://t.co/7rFGECgA by @riptari", "retweets": 36.0 },
+{ "user": "TechCrunch", "text": "Neil Young Begins His Long Quest Towards True Audio Fidelity With Pono, A New Music Service And Device http://t.co/t1Ez2d26 by @johnbiggs", "retweets": 34.0 },
+{ "user": "TechCrunch", "text": "Socialize's New Ad Platform Turns Passbook Passes Into Mobile Ads http://t.co/I2aku6yx by @sarahintampa", "retweets": 21.0 },
+{ "user": "TechCrunch", "text": "Pay-By-Keyword Startup Seconds Refocuses As A Way To Make Anyone A Merchant http://t.co/id0jy9i4 by @drizzled", "retweets": 19.0 },
+{ "user": "TechCrunch", "text": "Google Maps, Now With More High-Res Satellite And 45? Aerial Imagery http://t.co/FRIe230w by @mjburnsy", "retweets": 83.0 },
+{ "user": "TechCrunch", "text": "Tim Cook Apologizes For Apple Maps, Points To Competitive Alternatives http://t.co/aOONqQLt by @jordanrcrook", "retweets": 265.0 },
+{ "user": "TechCrunch", "text": "Apple's iPhone 5 Availability Expands: What It Means To Regional Carriers http://t.co/eNU1Mzfq by @drizzled", "retweets": 31.0 },
+{ "user": "TechCrunch", "text": "Survey: Younger Shoppers Increasingly Using Mobiles To Buy And Compare http://t.co/TzI9dyeM by @riptari", "retweets": 87.0 },
+{ "user": "TechCrunch", "text": "MyVR Raises $1.4M From SV Angel, Chris Dixon To Attack The $85B Vacation Rental Industry http://t.co/h65xm9mV by @ripemp", "retweets": 29.0 },
+{ "user": "TechCrunch", "text": "New Pricing For Amazon RDS Running Oracle Database http://t.co/nwXG81fu by @alexwilliams", "retweets": 33.0 },
+{ "user": "TechCrunch", "text": "Which Founders Use FoundersCard? Craig Newmark, Kevin Rose, Leah Busque &amp; 8K Others http://t.co/krGynNsV by @ripemp", "retweets": 21.0 },
+{ "user": "TechCrunch", "text": "If You Had Remote Access To Your Neighbor's Printer, What Would You Print? http://t.co/2iXhjjNY by @alexia", "retweets": 94.0 },
+{ "user": "TechCrunch", "text": "Pluralis Takes A Crowdsourced Approach To Improving Your Landing Pages http://t.co/YcqHJuC7 by @anthonyha", "retweets": 27.0 },
+{ "user": "TechCrunch", "text": "Best photobomb of the day. http://t.co/Xa2EYTyd", "retweets": 326.0 },
+{ "user": "TechCrunch", "text": "An Afternoon With Bad Piggies, Rovio's Next Hit http://t.co/KpYlmFSQ by @jordanrcrook", "retweets": 18.0 },
+{ "user": "TechCrunch", "text": "Most Docks Should Work With The Lightning Adapter And iPhone 5 http://t.co/oGlTupcK by @johnbiggs", "retweets": 13.0 }
+]"""
+ val engadget = """[
+{ "user": "engadget", "text": "Sony reveals Japan prices for Windows 8 VAIO machines - http://t.co/FRCu2XVb", "retweets": 18.0 },
+{ "user": "engadget", "text": "FreedomPop's pay-as-you-go data service launches in beta, offering 500MB of free WiMAX per month - http://t.co/Ny48yXUl", "retweets": 10.0 },
+{ "user": "engadget", "text": "Lenovo intros ThinkCentre M78 with AMD A-Series APU and a starting price of $449 - http://t.co/OEDe1EwW", "retweets": 22.0 },
+{ "user": "engadget", "text": "HP announces the ElitePad 900, a business-friendly Windows 8 tablet arriving in January - http://t.co/RjSj2cms", "retweets": 33.0 },
+{ "user": "engadget", "text": "How would you change Nokia's Lumia 900? - http://t.co/kW4MqiCv", "retweets": 8.0 },
+{ "user": "engadget", "text": "Amazon Kindle Paperwhite review - http://t.co/A1ytikHz", "retweets": 34.0 },
+{ "user": "engadget", "text": "Refresh Roundup: week of September 24th, 2012 - http://t.co/7BZkgDcN", "retweets": 5.0 },
+{ "user": "engadget", "text": "Slackware 14.0 now available, freshens and simplifies a Linux vanguard - http://t.co/Lq2s5RL6", "retweets": 33.0 },
+{ "user": "engadget", "text": "Slingbox 350 and 500 show up unannounced in Best Buy, flaunt 1080p and built-in WiFi - http://t.co/BCBmTq8i", "retweets": 32.0 },
+{ "user": "engadget", "text": "Switched On: iOS 6 gets back from the app - http://t.co/8j4YL4Yn", "retweets": 16.0 },
+{ "user": "engadget", "text": "IRL: Dyson DC44, NUU ClickMate PowerPlus and the Galaxy S III - http://t.co/5Duf2aa5", "retweets": 8.0 },
+{ "user": "engadget", "text": "B&amp;N makes the Nook Simple Touch with GlowLight's $20 price drop official - http://t.co/qDOwJHFT", "retweets": 9.0 },
+{ "user": "engadget", "text": "New process for nanotube semiconductors could be graphene's ticket to primetime (video) - http://t.co/6hiwBora", "retweets": 30.0 },
+{ "user": "engadget", "text": "Inhabitat's Week in Green: ECOLAR house, transparent solar panel and Star Wars terrariums - http://t.co/oeINUocC", "retweets": 8.0 },
+{ "user": "engadget", "text": "Sony shuts down PSP Comic Store after October 30th, leaves most of us in the lurch for now - http://t.co/sYpIQLtO", "retweets": 30.0 },
+{ "user": "engadget", "text": "Google retires more services, consolidates others in continued efficiency bid - http://t.co/UUdCFYPD", "retweets": 53.0 },
+{ "user": "engadget", "text": "LightSquared pitches new plans to FCC in attempt to end GPS interference hex - http://t.co/MubfRh7q", "retweets": 10.0 },
+{ "user": "engadget", "text": "Razer Blade review (late 2012) - http://t.co/QJpAz1nu", "retweets": 31.0 },
+{ "user": "engadget", "text": "Maingear announces Nomad 17 gaming laptop, comes with custom paint job - http://t.co/mj9dT843", "retweets": 9.0 },
+{ "user": "engadget", "text": "Ask Engadget: should companies include a cable with a new product? - http://t.co/NpXV1CdY", "retweets": 6.0 },
+{ "user": "engadget", "text": "RT @engadgethd: Looking for more pictures of the new Logitech Harmony Touch universal remote? We've got 'em http://t.co/GL7D8DCO", "retweets": 34.0 },
+{ "user": "engadget", "text": "Target, Walmart list price drop for B&amp;N's Nook Simple Touch with GlowLight to $119 - http://t.co/zLtZGjrK", "retweets": 13.0 },
+{ "user": "engadget", "text": "Mobile Miscellany: week of September 24th, 2012 - http://t.co/e4LWzDgb", "retweets": 9.0 },
+{ "user": "engadget", "text": "Logitech Harmony Touch remote pops up unannounced at Best Buy - http://t.co/PHOgiyAj", "retweets": 16.0 },
+{ "user": "engadget", "text": "Alt-week 9.29.12: 3D pictures of the moon, 4D clocks and laser-controlled worms - http://t.co/IuEKU297", "retweets": 10.0 },
+{ "user": "engadget", "text": "Hard drive shipments recover from floods in Thailand, expected to reach record high - http://t.co/JnKct0aV", "retweets": 65.0 },
+{ "user": "engadget", "text": "Cubify lets you skin, 3D print your own personal Android - http://t.co/S6nimh5R", "retweets": 23.0 },
+{ "user": "engadget", "text": "Facebook Messenger 2.0 for iPhone brings new design to conversations, swipe left for friends list - http://t.co/fDQO7eJN", "retweets": 55.0 },
+{ "user": "engadget", "text": "US Cellular expands its Windows Phone catalog with the ZTE Render for $80 - http://t.co/uPZzQyy8", "retweets": 13.0 },
+{ "user": "engadget", "text": "Modbook Pro to launch with SSD storage, up for pre-order October 3rd - http://t.co/w87nj1W4", "retweets": 61.0 },
+{ "user": "engadget", "text": "BlackBerry 10 L-series tutorial videos surface online, give a literal peek at the future (video) - http://t.co/fLLDSRvA", "retweets": 47.0 },
+{ "user": "engadget", "text": "Logitech promises continued support for Squeezebox, says it won't force a switch to UE Smart Radio - http://t.co/xGFz7aLE", "retweets": 17.0 },
+{ "user": "engadget", "text": "Scape, Brian Eno's new ambient music creation app is now available on the iPad (video) - http://t.co/pRgrXoHA", "retweets": 33.0 },
+{ "user": "engadget", "text": "Touch Bionics releases new prosthetic fingers, flip the old ones the bird - http://t.co/xJNFmahn", "retweets": 48.0 },
+{ "user": "engadget", "text": "Sony starts delivering Ice Cream Sandwich update to Xperia go, Xperia U and Xperia sola - http://t.co/ao0Y6Iur", "retweets": 38.0 },
+{ "user": "engadget", "text": "Xi3 goes the crowdfunding route for future X3A, X7A modular PCs (video) - http://t.co/aPvAO10A", "retweets": 14.0 },
+{ "user": "engadget", "text": "FCC votes in favor of rethinking spectrum holding rules, goading broadcasters into wireless selloffs - http://t.co/x50qKCNB", "retweets": 14.0 },
+{ "user": "engadget", "text": "Prometheus Blu-ray specs unveiled, arrives with seven hours of extras October 9th (video) - http://t.co/xGnog3Kr", "retweets": 60.0 },
+{ "user": "engadget", "text": "Samsung Galaxy Note II variants for AT&amp;T, T-Mobile, Verizon possibly caught at the FCC - http://t.co/YLvixd9D", "retweets": 21.0 },
+{ "user": "engadget", "text": "Nokia Parking helps you find and pay for a spot, we go hands-on (video) - http://t.co/NnAvzIlG", "retweets": 43.0 },
+{ "user": "engadget", "text": "PSA: $25 Google Play credit for Nexus 7 ends this weekend - http://t.co/3iyUo8iK", "retweets": 32.0 },
+{ "user": "engadget", "text": "US Appeals court rules Motorola can't enforce injunction against Microsoft in Germany... again - http://t.co/K2efo4cU", "retweets": 9.0 },
+{ "user": "engadget", "text": "This is the carbon fiber core of Ferrari's first hybrid - http://t.co/y2HP1rjX", "retweets": 43.0 },
+{ "user": "engadget", "text": "For Nokia, helping the competition find its way is good business - http://t.co/1sSiTbn7", "retweets": 22.0 },
+{ "user": "engadget", "text": "Google optimizes Flight Search for tablets, makes booking trips easier - http://t.co/HufiiXw2", "retweets": 29.0 },
+{ "user": "engadget", "text": "Microsoft details its own Windows 8 rollout, lessons learned from 'dogfooding' - http://t.co/bg5swwsm", "retweets": 33.0 },
+{ "user": "engadget", "text": "PBS draws link between digital music ethics and magic spells, somehow makes it look simple (video) - http://t.co/Fx7sPphG", "retweets": 7.0 },
+{ "user": "engadget", "text": "PSA: iPhone 5 available in 22 more countries, on Cricket and US regional carriers galore - http://t.co/lE4dLNQC", "retweets": 18.0 },
+{ "user": "engadget", "text": "\"It's now a surprise to hear of a company listening to its users, issuing an apology and vowing to make it better.\" (http://t.co/R5bu5Wp1)", "retweets": 49.0 },
+{ "user": "engadget", "text": "Wikipad CEO James Bower defends his gaming tablet's $500 pricing, why one device beats two - http://t.co/eUiFdD8g", "retweets": 11.0 },
+{ "user": "engadget", "text": "Parallella project: designed to bring mouth-watering power to a board similar in size to the Raspberry Pi for $99. http://t.co/lOz8cmX8", "retweets": 12.0 },
+{ "user": "engadget", "text": "Kodak dropping out of the consumer inkjet printer business in 2013 - http://t.co/bpE6f6Za", "retweets": 20.0 },
+{ "user": "engadget", "text": ".@EngadgetDistro's Issue 59 is ready for your eyes and fingers. We've got bicycles, new Sony products and much more. (http://t.co/Xk2F1et0)", "retweets": 2.0 },
+{ "user": "engadget", "text": "Court of Appeals for the Federal Circuit tells Judge Koh to revisit Galaxy Tab 10.1 injunction - http://t.co/iIOCcwDW", "retweets": 13.0 },
+{ "user": "engadget", "text": "Indian government tells carriers to end 3G roaming pacts, doesn't stop to explain why - http://t.co/Wwkk4qXY", "retweets": 12.0 },
+{ "user": "engadget", "text": "HP takes Open webOS 1.0 live, shows it supersized on a TouchSmart (video) - http://t.co/VWd1tmd7", "retweets": 17.0 },
+{ "user": "engadget", "text": "How to set up your Raspberry Pi to play Atari 2600 games - http://t.co/U7qMnHvb", "retweets": 29.0 },
+{ "user": "engadget", "text": "Amazon breaks down its Kindle Paperwhite light technology (video) - http://t.co/ThAUTZNt", "retweets": 18.0 },
+{ "user": "engadget", "text": "NPD: Android users chew an average 870MB of cellular data per month, youngest gobble the most - http://t.co/tUHgRYn8", "retweets": 25.0 },
+{ "user": "engadget", "text": "T-Mobile leases 7,200 mobile towers to Crown Castle in a 28-year, $2.4 billion deal http://t.co/jrlV0qrE", "retweets": 7.0 },
+{ "user": "engadget", "text": "Insert Coin: The Parallella project dreams of $99 supercomputers - http://t.co/nb0vKSKM", "retweets": 14.0 },
+{ "user": "engadget", "text": "Don't mind the zero-emissions Mercedes fuel-cell car behind the invisible curtain (eyes-on video) - http://t.co/3EYNK9d8", "retweets": 14.0 },
+{ "user": "engadget", "text": "Editorial: Apple apologies actually aren't that infrequent, and that's okay - http://t.co/oQn1MnEz", "retweets": 39.0 },
+{ "user": "engadget", "text": "AT&amp;T 4G LTE adds Galaxy Note 2, Galay Tab 2 10.1, Galaxy Express and Galaxy Rugby Pro to lineup - http://t.co/uvBFFMQO", "retweets": 12.0 },
+{ "user": "engadget", "text": "Engadget Podcast 311 - 09.28.2012 - http://t.co/rirxdjgi", "retweets": 1.0 },
+{ "user": "engadget", "text": "GreenGT H2 eyes-on: the first fuel cell-powered racer to tackle Le Mans - http://t.co/zYBo1SnD", "retweets": 9.0 },
+{ "user": "engadget", "text": "Google offers up more high-res places in Maps / Earth, intros additional 45-degree imagery - http://t.co/MV8SSHxb", "retweets": 29.0 },
+{ "user": "engadget", "text": "Distro Issue 59: Boston's DBC City Bike is putting a new spin on an old design - http://t.co/1KOqTPVy", "retweets": 2.0 },
+{ "user": "engadget", "text": "Tim Cook apologizes for Maps mess - http://t.co/8QaU630q", "retweets": 210.0 },
+{ "user": "engadget", "text": "Smart Forstars concept EV brings drive-in theater to a wall near you (video) - http://t.co/u9Hxmlf3", "retweets": 20.0 },
+{ "user": "engadget", "text": "John Rogers returns with a silicon-silk circuit that dissolves inside your body - http://t.co/5qUQnKnP", "retweets": 9.0 },
+{ "user": "engadget", "text": "Neil Young's Pono music service wants to democratize high-quality audio (video) - http://t.co/gncQNRiS", "retweets": 20.0 },
+{ "user": "engadget", "text": "Build-a-bear's new store concept wants you to choose, love, stuff and fluff with high-tech (video) - http://t.co/3YwQ6tPS", "retweets": 11.0 },
+{ "user": "engadget", "text": "Sony Xperia Tablet S gets chunky update: better multi-tasking and IR, guest mode, new media apps - http://t.co/6H2fl92W", "retweets": 18.0 },
+{ "user": "engadget", "text": "Sony makes Olympus rescue pact official with $645 million investment - http://t.co/kTZdhyLK", "retweets": 17.0 },
+{ "user": "engadget", "text": "Ibis hotels to have robots paint art while they track your sleep: no, that's not creepy at all (video) - http://t.co/jzRbMwms", "retweets": 19.0 },
+{ "user": "engadget", "text": "FAVI's $50 Streaming Stick adds apps, streaming services to any HDTV with Android 4.1 Jelly Bean - http://t.co/dL9geyBU", "retweets": 20.0 },
+{ "user": "engadget", "text": "Google gives users an easy out, adds YouTube to Takeout data transfer tool - http://t.co/nPpUgW4Q", "retweets": 14.0 },
+{ "user": "engadget", "text": "NASA's Curiosity rover finds ancient streambed on Mars, evidence of 'vigorous' water flow - http://t.co/NEFjCaVj", "retweets": 67.0 },
+{ "user": "engadget", "text": "Google adds CardDAV support to contacts for easier syncing with iOS and other third-party devices - http://t.co/Sx5oXOvQ", "retweets": 34.0 },
+{ "user": "engadget", "text": "Sharp takes out $4.6 billion loan while it continues restructuring - http://t.co/O9t9mROq", "retweets": 15.0 },
+{ "user": "engadget", "text": "The Dark Knight Rises Blu-ray officially set for December 4th, limited edition Bat cowl revealed - http://t.co/7S0LD6Gx", "retweets": 210.0 },
+{ "user": "engadget", "text": "iPhone 5 vs. Lumia 920 Image Stabilization. Check out the test - http://t.co/TAMu9eYV", "retweets": 131.0 },
+{ "user": "engadget", "text": "ADATA's got an 8.9mm thick portable USB 3.0 drive, limbos under the competition by a millimeter - http://t.co/uNHhcDWN", "retweets": 10.0 },
+{ "user": "engadget", "text": "ZTE Engage cleared to land at Cricket on October 2nd for $250 - http://t.co/G72tUGEc", "retweets": 7.0 },
+{ "user": "engadget", "text": "NFL Network's Thursday Night Football Xtra app comes to Android - http://t.co/cPvJKgdR", "retweets": 12.0 },
+{ "user": "engadget", "text": "Exagon Motors Furtive-eGT electric supercar eyes-on - http://t.co/lkh6ANAW", "retweets": 9.0 },
+{ "user": "engadget", "text": "ZTE shows off the V98 Windows 8 slate, we go hands-on - http://t.co/FYrccrr8", "retweets": 13.0 },
+{ "user": "engadget", "text": "Kingston Digital SDXC cards arrive with lower price, larger capacities - http://t.co/yYfPHQOX", "retweets": 13.0 },
+{ "user": "engadget", "text": "http://t.co/WT4zylJj working on NFC-equipped business cards, kicks off limited beta program today - http://t.co/MtAea0jh", "retweets": 35.0 },
+{ "user": "engadget", "text": "Simple.TV clarifies shipping dates, tells backers units will start arriving October 10th - http://t.co/eod6khQJ", "retweets": 3.0 },
+{ "user": "engadget", "text": "Samsung Galaxy Camera swings past the FCC with AT&amp;T-capable 3G - http://t.co/4lRRnTr3", "retweets": 9.0 },
+{ "user": "engadget", "text": "Mozilla Persona sign-in launches in beta, skips the social networking ball and chain (video) - http://t.co/GpVDM7Vz", "retweets": 8.0 },
+{ "user": "engadget", "text": "Cellcom to offer iPhone 5 for $149 and up starting Friday - http://t.co/4zCHVb0f", "retweets": 19.0 },
+{ "user": "engadget", "text": "Facebook Messenger for BlackBerry reaches App World, sates compulsive chatters - http://t.co/ZmuNQLjs", "retweets": 20.0 },
+{ "user": "engadget", "text": "RIM: first BlackBerry 10 devices to focus on mid-range and up, entry-level may be ready in 2013 - http://t.co/OMQMLJvs", "retweets": 34.0 },
+{ "user": "engadget", "text": "RT @EngadgetDistro: Since iOS 6's launch, how happy are you with Apple's Maps app?", "retweets": 25.0 },
+{ "user": "engadget", "text": "Harman shows off its upcomming JBL docks and speakers, we go eyes-on - http://t.co/5bz8UCBo", "retweets": 7.0 },
+{ "user": "engadget", "text": "From the lab: Lumia 920 image stabilization and 808 drop test at Nokia R&amp;D (video) - http://t.co/s9zFp7hU", "retweets": 13.0 },
+{ "user": "engadget", "text": "Sony's Xperia T is now available on O2 and ThreeUK, Walther PPK/S not included - http://t.co/LlZsSzqQ", "retweets": 7.0 }
+]"""
+ val amazondeals = """[
+{ "user": "amazondeals", "text": "Deal of the Day: 50% Off Select BEARPAW Boots &amp; Slippers http://t.co/ZBvpSN33", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $38.69 - Honeywell Portable Wireless Door Chime and Push Button http://t.co/yYBV4ebr", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $124.99 - Ritmo Mundo Unisex White Sport Quartz Watch http://t.co/RfungjJC", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $129.99 - Giulio Romano Piemonte Black Silicone Watch http://t.co/7v1ovQCQ", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $59.99 - Stuhrling Original Men's Roulette Swiss Quartz Watch http://t.co/vreVCFvJ", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Marware Microshell for Fire (not for HD) http://t.co/Mtb1Uoea", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Belkin Quilted Case for Fire HD 7\" http://t.co/gxpUszgw", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $17.99 - iHome Rechargeable Speaker for Kindle Fire http://t.co/JCIBkwS1", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $119.95 - Shun Premier 7-Inch Santoku Knife http://t.co/Lpsyh0gY", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - Crock-Pot Cook Travel Serve 6-Quart Programmable Slow Cooker http://t.co/8HlHPMh3", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $37.49 - Tron: Legacy/Tron Original Classic (Five-Disc Combo: Blu-ray 3D / Blu-ray / DVD / Digital Co http://t.co/RqqRAwxO", "retweets": 8.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Dove Body Wash, 24 ounce (Pack of 4) http://t.co/PFmNhvsQ", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $249.99 - DwellStudio Crib Set, Owls http://t.co/f2C4ab6j", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $89.99 - Canon imageCLASS Laser Multifunction Printer http://t.co/RAiMddij", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Marware Jurni for Kindle + Kindle Paperwhite http://t.co/TzliFyFm", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $19.99 - Amazon Kindle DX Leather Cover http://t.co/BRDBNBsz", "retweets": 4.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Verso Duct Tape Case for Fire HD 7\" http://t.co/UID1EZfo", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Lightwedge Reading Light for Kindle E-readers http://t.co/8WvOPwib", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $1.99 Kindle Mysteries &amp; Thrillers http://t.co/ApOcFD5g", "retweets": 7.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Lewis N. Clark Sport Pack http://t.co/ax9n0p4I", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Sherpani Latitude LE Wheeled Suitcase http://t.co/oor6wzfr", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $79.99 - Hunter 3.5-Gallon CareFree Plus Humidifier with PermaWick Filter http://t.co/TXnzAtGi", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $89.95 - Chicago Cutlery 12-Piece Knife Set with Block http://t.co/MFtohB5H", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $189.99 - 3M Mobile Projector http://t.co/pjwDE8s3", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $9.99 - Scotch Adhesive Dot Roller Value 4-Pack http://t.co/m7FdEzXr", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $89.99 - Swingline Stack-and-Shred Red 60-Sheet Shredder, Cross-Cut, 60 Sheets http://t.co/kIB7eczO", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $249.00 - Sony Cyber-shot 18.2 MP Digital Camera http://t.co/uy6V6XfB", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $59.99 - Jamie Oliver by T-fal Nonstick Hard Anodized 10-Piece Cookware Set http://t.co/C3Kqkhwi", "retweets": 4.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $199.95 - Cutlery Saber F-11 Chef Knives with Knife Bag http://t.co/EjdYR668", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Tommy Hilfiger Pebble Leather Easy Tote http://t.co/AAKERLrs", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.00 - AudioSource Indoor/Outdoor Two-Way Speakers http://t.co/4AMrXZYG", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $159.00 - AudioSource S3D60 Soundbar with Sonic Emotion 3D Sound http://t.co/2MpkXteo", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $99.99 - Jamo S-602 Bookshelf Speaker Pair http://t.co/qsK9vPee", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $41.35 - Keeper Waterproof Roof Top Cargo Bag http://t.co/gPJ6HEqs", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $81.35 - Masterbuilt Hitch-Haul Cargo Carrier http://t.co/CmBPGfnb", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $59.99 - T3 Mane Tamer Ionic Ceramic Tourmaline Flat Iron http://t.co/Nd7myReH", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $160.99 - Sarah Peyton 10-Inch Twin-Size Memory Foam Mattress http://t.co/3PHWkPcG", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $24.99 - PG Tips tea http://t.co/zwwzDoaV", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $17.99 - Mr. Beams Motion-Sensing Wireless LED Ceiling Light http://t.co/H67DlkoE", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $49.72 - Maytag Pur Refrigerator Cyst Water Filter 2-Pack http://t.co/Y3QuaVmb", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Jabra CLIPPER Bluetooth Stereo Headset http://t.co/XkvVrWJ0", "retweets": 5.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $219.99 - Dallas: The Complete Collection (Seasons 1-14 + Movies) http://t.co/bEIlKv37", "retweets": 5.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $89.99 - Ear Force DPX21 Headset http://t.co/lmrl7A47", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! LEGO Batman 2: DC Super Heroes http://t.co/Khqr9vao", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $19.99 - Plantronics Gamecom P90 http://t.co/Dd1SUbTy", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $34.99 - PS3 Black DualShock 3 Wireless Controller http://t.co/fG0zuhW4", "retweets": 6.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Sleeping Dogs http://t.co/4Hp1acZA", "retweets": 4.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Ghost Recon Future Soldier http://t.co/EP6t5M7F", "retweets": 10.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $9.99 - Black Eyed Peas Experience http://t.co/7uIfrnlp", "retweets": 5.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $39.99 - Madden NFL 13 http://t.co/YVUYk52D", "retweets": 18.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $14.99 - Death &amp; Taxes (1 Page Book) http://t.co/QFepRypH", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $17.99 - Hello Kitty Sterling Silver Red Enamel Stud Earrings http://t.co/XG8ACVni", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Sterling Silver Created Gemstone and Diamond Jewelry Set http://t.co/swzagwJB", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $199.99 - Sterling Silver 1/2 cttw Black Diamond Bamboo Hoop Earrings http://t.co/ZF1r622t", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $24.99 - Sterling Silver Diamond Accent Dragonfly Pendant Necklace http://t.co/v8lzl73r", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Alex Stevens Men's Hoodie http://t.co/U00M1xj4", "retweets": 5.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Dickies Men's Short Sleeve Ring-Spun Work Shirt http://t.co/iTwRTTBL", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Get Ready for Fall: Save on Your Choice of Comforters http://t.co/lFFGKbuA", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - Baggallini Luggage Run Away Satchel Bag http://t.co/543etCwD", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $129.99 - Sennheiser OMX 980 In-Ear Headphones http://t.co/JgRjVQAU", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $17.99 - JLab TV Wall Mount http://t.co/ISYTXU5d", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $25.99 - Monster GreenPower Digital PowerCenter MDP 900 http://t.co/pKTK3bB7", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $12.99 - Atlantic Movie Bin http://t.co/0VMfGZUF", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $94.99 - Up to 66% off Schlage Camelot Keypad Lever Door Locks http://t.co/yBGB1JgW", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $23.55 - Christian Audigier Ed Hardy Deluxe Collection 4 Set Perfume http://t.co/hh6glIGv", "retweets": 4.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $59.99 - Panasonic DECT 6.0 Corded/Cordless Phone http://t.co/P1w5SlDz", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $99.99 - Polar SD Heart Rate Monitor Watch with S1 Foot Pod http://t.co/ckSeWPP8", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $84.99 - Cuisinart Petit Gourmet Portable Tabletop Gas Grill http://t.co/lRj6ZdPb", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $41.20 - Terrazzo Patio Table &amp; Chair Set Cover http://t.co/J6qBBl9O", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $23.50 - Classic Accessories Veranda Patio Chair Cover http://t.co/zH75UlBK", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: Up to 54% off Select Exercise Bikes from Schwinn http://t.co/nx0MyVxf", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - Business Plan Pro Complete v 12 http://t.co/HpasTV4k", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $54.99 - Home Designer Suite 2012 by Chief Architect http://t.co/SWEoEFPL", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $24.99 - Uniden PRO505XL 40-Channel CB Radio http://t.co/K1skXu8V", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $19.99 - Garmin Carry All Case for Garmin nuvi Models http://t.co/qa8Sr9gL", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $154.99 - Garmin Forerunner 405CX GPS Watch with Heart Rate Monitor http://t.co/wV3vwncz", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $12.99 - Pyrex Storage 10-Piece Set http://t.co/NfJms2Dz", "retweets": 3.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $99.99 - 10-Piece KitchenAid Stainless Steel Cookware Set http://t.co/Dtl5zK5N", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $21.95 - Da Vinci Catapult Kit, Wood http://t.co/LUvEnJgo", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $117.02 - Norton Waterstone Starter Kit: 220/1000 grit stone, 4000/8000 grit stone, SiC flattening sto http://t.co/gMhOyN5z", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $294.99 - Panasonic 3D Blu-Ray Disc 5.1 Surround Sound Home Theater System http://t.co/vkAUPbOW", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Skechers Men's Rival Running Shoe http://t.co/leAWLm5F", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Bear Grylls Men's Mountain Jacket by Craghoppers http://t.co/TLQ1hg45", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - iHome Alarm Clock Dock for Kindle Fire (Not for HD) http://t.co/oDP4ZtSl", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $34.99 - SINGER Perfect Finish 1700 Watt Steam Iron http://t.co/wiri14KB", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - Armitron Men's Black/Rosegold Multi-Function Watch http://t.co/b1TXRG94", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $54.99 - Marc Ecko Men's The Encore OZ Classic Analog Watch http://t.co/ACe4nUE8", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $109.99 - Stuhrling Men's Raptor Mechanical Skeleton Watch http://t.co/S7YTLGYW", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - Stuhrling Original Men's Regatta Swiss Quartz Black Watch http://t.co/R050xFJK", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: $64.99 - Irwin Vise-Grip GrooveLock 8-Piece Plier Set http://t.co/noygxOJi", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $31.99 - Spynet Ultravision http://t.co/ThDnpqlC", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $19.99 - Lucky Brand Gold-Tone Turquoise-Color Howlite Link Bracelet http://t.co/w9jbDbZq", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $14.99 - Sterling Silver Plated Heart Cable Bangle Bracelet http://t.co/oRPIiEae", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $79.99 - Duragold 14k Yellow Gold Diamond-Cut Hoop Earrings http://t.co/yKlvrs2O", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! Sterling Silver Diamond Stud Earrings http://t.co/8dzWkf2r", "retweets": 1.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $9.99 - Brother PT90 Personal Labeler http://t.co/pJuiSxZn", "retweets": 0.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $39.99 - TRENDnet 200 Mbps Powerline Ethernet AV Adapter Kit http://t.co/AhwHHIUD", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $299.99 - Neato XV-11 All-Floor Robotic Vacuum System http://t.co/jQTTi3Im", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Lightning Deal! $101.15 - BISSELL Lift-Off Steam Mop http://t.co/kE5YhYZZ", "retweets": 2.0 },
+{ "user": "amazondeals", "text": "Deal of the Day: Starting from $3.99 Each: Essential MP3 Albums for Your Collection http://t.co/wotNjjir", "retweets": 5.0 }
+]"""
+ val CNET = """[
+{ "user": "CNET", "text": "HP aims for business users with ElitePad 900 Windows 8 tablet http://t.co/XyOipAdL", "retweets": 10.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Apple rolls out fix for iPhone 5 Wi-Fi network data bug http://t.co/Gz13JfDD", "retweets": 93.0 },
+{ "user": "CNET", "text": "Ever wonder how your iPhone screen got its color? (Video) http://t.co/Fq9FFbnc", "retweets": 29.0 },
+{ "user": "CNET", "text": "The Kindle Paperwhite rises to the top of the e-reader pack http://t.co/QVXp6P0H", "retweets": 19.0 },
+{ "user": "CNET", "text": "How to switch from iPhone to Android http://t.co/M8I9lwua", "retweets": 131.0 },
+{ "user": "CNET", "text": "Let's crack open the iPhone 5! (video) http://t.co/4tiuroNg", "retweets": 30.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Oracle taps Nokia for location-based services, The Wall Street journal reports. http://t.co/ecxuXHD3", "retweets": 54.0 },
+{ "user": "CNET", "text": "How to simplify your messy photo collection http://t.co/nFwJ8jSi", "retweets": 24.0 },
+{ "user": "CNET", "text": "Everything you need to know about using iOS 6 http://t.co/10jWoN7I #ICYMI #Ios6", "retweets": 47.0 },
+{ "user": "CNET", "text": "Where iPhones go to die (video) http://t.co/86fBpfvh", "retweets": 33.0 },
+{ "user": "CNET", "text": "How RIM could get you to buy into BlackBerry 10 http://t.co/AUo08CkG", "retweets": 28.0 },
+{ "user": "CNET", "text": "Camera contest: Apple iPhone 5 vs. Samsung Galaxy S3 vs. HTC One X http://t.co/PmbhNgrd", "retweets": 49.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Apple backtracks on 'most powerful' map app claim http://t.co/mB39ilOg", "retweets": 50.0 },
+{ "user": "CNET", "text": "Digging for rare earths: The mines where iPhones are born http://t.co/TKHx8hYK", "retweets": 29.0 },
+{ "user": "CNET", "text": "The new Twitter for iOS adds headline photos and revamps iPad interface. Check out our review: http://t.co/234thJkl", "retweets": 17.0 },
+{ "user": "CNET", "text": "Yes, a search engine just for porn http://t.co/AhFCUf8I", "retweets": 60.0 },
+{ "user": "CNET", "text": "How to reduce annoying microphonics from your in-ear headphones http://t.co/dmwHA4LY", "retweets": 22.0 },
+{ "user": "CNET", "text": "Is this what an iPad Mini might look like? http://t.co/MmfrLItm", "retweets": 50.0 },
+{ "user": "CNET", "text": "The mad world of Foxconn, your iPhone's birthplace: @iamjaygreene reports from China http://t.co/WsUssqj7 #ICYMI", "retweets": 45.0 },
+{ "user": "CNET", "text": "Sony's new portable external battery is gorgeous http://t.co/upKqXII7", "retweets": 42.0 },
+{ "user": "CNET", "text": "FIFA Soccer 13 is not only the best soccer game, but the best sports game on an iOS device. Check out our review: http://t.co/pZv7DKF1", "retweets": 63.0 },
+{ "user": "CNET", "text": "The wireless router is arguably the most important piece of computer equipment in your home. You deserve a good one http://t.co/LQCO1VBz", "retweets": 88.0 },
+{ "user": "CNET", "text": "The periodic table of iPhones (infographic) http://t.co/ShUvE27y", "retweets": 54.0 },
+{ "user": "CNET", "text": "The environmental pitfalls at the end of an iPhone's life http://t.co/jLfiPCVA", "retweets": 43.0 },
+{ "user": "CNET", "text": "Transit directions field test: Apple Maps vs. Google Maps http://t.co/M9kDqLGX", "retweets": 36.0 },
+{ "user": "CNET", "text": "Will the Nook HD+ put out the Kindle Fire? Check out our hands on with Barnes &amp; Noble's latest tablet http://t.co/t6A2EV1D", "retweets": 10.0 },
+{ "user": "CNET", "text": "How to wake up to any song in your iTunes library in iOS 6 http://t.co/hTnUiyjt", "retweets": 68.0 },
+{ "user": "CNET", "text": "How to lock down and find Android and Windows phones http://t.co/mRw8P80z", "retweets": 25.0 },
+{ "user": "CNET", "text": "Four most-useful new settings in iOS 6 http://t.co/LHFOCLnA", "retweets": 63.0 },
+{ "user": "CNET", "text": "Looper review: Bruce Willis does the time warp, kicks ass http://t.co/ttIk26r8", "retweets": 19.0 },
+{ "user": "CNET", "text": "In need of a new app? Here are our favourites from this week http://t.co/9D3kEhWf", "retweets": 17.0 },
+{ "user": "CNET", "text": "How rocks power your iPhone (video) http://t.co/hufxUMok", "retweets": 11.0 },
+{ "user": "CNET", "text": "View Einstein's brain under a microscope, piece by piece http://t.co/b7O8YDDN", "retweets": 31.0 },
+{ "user": "CNET", "text": "If you're really, really, really paranoid about Facebook privacy issues, then maybe just leave Facebook http://t.co/fDPLjXHM", "retweets": 72.0 },
+{ "user": "CNET", "text": "How to prevent phone and tablet theft http://t.co/LbhmJT7t", "retweets": 62.0 },
+{ "user": "CNET", "text": "Whether you're a casual shopper or an avid buyer &amp; seller, eBay for Android is worth a download. Check out our review http://t.co/dNWIjcxx", "retweets": 10.0 },
+{ "user": "CNET", "text": "Twitter's revamped Android app is wonderful, but is it enough to satisfy power users? Check out our full review: http://t.co/234thJkl", "retweets": 25.0 },
+{ "user": "CNET", "text": "Google cofounder Sergey Brin: Robotic cars will be available to the general public within 5 years http://t.co/al2PyAP5", "retweets": 134.0 },
+{ "user": "CNET", "text": "The original Kindle Fire goes through the battery of Torture Tests. Will it survive? http://t.co/O0gEUklc", "retweets": 10.0 },
+{ "user": "CNET", "text": "iPhone 5 to iPhone 4S: I vibrate so much harder than you http://t.co/1XMZVvVE", "retweets": 57.0 },
+{ "user": "CNET", "text": "Take a look at how Lenovo stress-tests laptops http://t.co/MQCMeVBz", "retweets": 32.0 },
+{ "user": "CNET", "text": "FIFA Soccer 13 is not only the best soccer game, but the best sports game on an iOS device. Check out our review: http://t.co/pZv7DKF1", "retweets": 59.0 },
+{ "user": "CNET", "text": "The Sonos Connect brings effortless wireless media streaming to existing hi-fi owners. Check out our review: http://t.co/xyHxFLXi", "retweets": 9.0 },
+{ "user": "CNET", "text": "The cheap, smart Vizio 32-inch TV is a great buy. Check out our review: http://t.co/AjH2yFgQ", "retweets": 11.0 },
+{ "user": "CNET", "text": "The Super Slim PlayStation 3 shrinks a powerful gaming machine into an even tinier package. Check out our review: http://t.co/1LUyyuEj", "retweets": 35.0 },
+{ "user": "CNET", "text": "RT @benjaminphotos: @CNETNews Yes we are... http://t.co/NxGMjVWq", "retweets": 74.0 },
+{ "user": "CNET", "text": "Inside Scoop: @Josh discusses the ins and outs of Apple's recent map flub (video) http://t.co/4wLrkvFG #mapology", "retweets": 2.0 },
+{ "user": "CNET", "text": "Is the computer mouse dead? @DanAckerman on the ascendance of the touch pad: http://t.co/vjwps44j", "retweets": 43.0 },
+{ "user": "CNET", "text": "If you watch television regularly, second-screen app Zeebox for Android &amp; iOS makes an excellent companion http://t.co/buVYA8E7", "retweets": 15.0 },
+{ "user": "CNET", "text": "MacFixIt Q&amp;A: Is your iTunes not reading burned disks? http://t.co/GnPo0lLv", "retweets": 6.0 },
+{ "user": "CNET", "text": "Business cards go wireless as Moo folds in NFC technology. Bump goes the contact info http://t.co/N6zGZUdF", "retweets": 27.0 },
+{ "user": "CNET", "text": "The awesome size of Foxconn (infographic) http://t.co/drvvmsVb", "retweets": 19.0 },
+{ "user": "CNET", "text": "The day the Earth stood still... while Tim Cook apologized to it. Get the full recap on @CNETUpdate http://t.co/rqJjKXph", "retweets": 23.0 },
+{ "user": "CNET", "text": "How to download your YouTube videos in their original format (video) http://t.co/J7QOQiul", "retweets": 42.0 },
+{ "user": "CNET", "text": "Are you ready for Facebook updates to invade your caller ID? http://t.co/uG8lG9J6", "retweets": 28.0 },
+{ "user": "CNET", "text": "The cost of charging your iPhone 5 for one year: $0.41 http://t.co/DTOV8D3M", "retweets": 93.0 },
+{ "user": "CNET", "text": "The iPhone map of China (infographic) http://t.co/ioazYk6y", "retweets": 13.0 },
+{ "user": "CNET", "text": "Finally, you can play as the pigs from Angry Birds! But is 'Bad Piggies' as addictive as the original game? (review) http://t.co/7W6vflLf", "retweets": 15.0 },
+{ "user": "CNET", "text": "When an iPhone is recycled (infographic) http://t.co/YixSaMOi", "retweets": 18.0 },
+{ "user": "CNET", "text": "A mathematician accurately predicted when Android's app store would hit 25 billion downloads http://t.co/VFLBJ0z3", "retweets": 36.0 },
+{ "user": "CNET", "text": "Tech tip: Manage your iPhone address book with CopyTrans Contacts http://t.co/C3Je5FJo", "retweets": 14.0 },
+{ "user": "CNET", "text": "Man jailed for mistakenly sexting young girls, freed because he also inadvertently sexted his own family http://t.co/Mp11ELmH", "retweets": 33.0 },
+{ "user": "CNET", "text": "A TSA security guard was caught red-handed stealing an iPad in a TV sting operation http://t.co/HipBLbgs", "retweets": 43.0 },
+{ "user": "CNET", "text": "The periodic table of iPhones (infographic) http://t.co/ShUvE27y", "retweets": 21.0 },
+{ "user": "CNET", "text": "How to opt out of Facebook's online and offline tracking http://t.co/VCJnZdM7", "retweets": 70.0 },
+{ "user": "CNET", "text": "How to make your Android phone look like an iPhone 5 http://t.co/tZZYb8Ti", "retweets": 39.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Judge Lucy Koh now has the authority to lift the sales ban on Samsung's Galaxy Tab 10.1 tablet. Will she? http://t.co/2SwBiOAE", "retweets": 19.0 },
+{ "user": "CNET", "text": "Getting started with Passbook on iOS 6 http://t.co/3ATZW25o", "retweets": 33.0 },
+{ "user": "CNET", "text": "Researchers are building a real life RoboCop! No word yet on if they'll follow it up with two inferior sequels http://t.co/a3lunSCQ", "retweets": 23.0 },
+{ "user": "CNET", "text": "Scenes from the 2012 Paris Motor Show http://t.co/z1MhqngC", "retweets": 15.0 },
+{ "user": "CNET", "text": "Minecraft creator says he won't certify the game for Windows 8 http://t.co/xqo84j2B", "retweets": 25.0 },
+{ "user": "CNET", "text": "How to change your Twitter header photo http://t.co/FCs8hpTs", "retweets": 27.0 },
+{ "user": "CNET", "text": "The tweets in the Discovery tab just got a lot more relevant http://t.co/v6IWlTdQ", "retweets": 5.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Tim Cook's \"Mapology\": What's missing is any explanation why Apple would issue a beta app it knew would frustrate you http ...", "retweets": 32.0 },
+{ "user": "CNET", "text": "How to set up an Android tablet as a second display for your PC or Mac http://t.co/YynJll9N", "retweets": 48.0 },
+{ "user": "CNET", "text": "A Texas company is joining the patent wars by suing Apple over spreadsheet technology http://t.co/sntXqimH", "retweets": 16.0 },
+{ "user": "CNET", "text": "Eying the iPad's turf, Intel and the Windows 8 gang is set to make a play for business users http://t.co/7OEr4hqc", "retweets": 7.0 },
+{ "user": "CNET", "text": "Friday Poll: Would you use a self-driving car? http://t.co/qRAvKCpc", "retweets": 7.0 },
+{ "user": "CNET", "text": "The mad world of Foxconn, your iPhone's birthplace: @iamjaygreene reports from China http://t.co/WsUssqj7 #ICYMI", "retweets": 19.0 },
+{ "user": "CNET", "text": "RT @CNETNews: As Apple says sorry, Google Maps gets a little better, depending on where you are http://t.co/6k2WPgc3", "retweets": 42.0 },
+{ "user": "CNET", "text": "Sure, tablets and smartphones are risky, but HP doesn't really have any choice but to plunge into the post-PC era http://t.co/vnTBjSMo", "retweets": 6.0 },
+{ "user": "CNET", "text": "RT @jetscott: The good news: New York's getting the world's tallest Ferris wheel. The bad news: it's on Staten Island. http://t.co/5VciVVue", "retweets": 19.0 },
+{ "user": "CNET", "text": "11 essential tips for mastering iOS 6 http://t.co/10jWoN7I", "retweets": 37.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Here's what AT&amp;T's got coming up, a la Samsung: Galaxy Note 2, Rugby Pro, Express, Tab 10.1 2 http://t.co/iOwZxoRR", "retweets": 12.0 },
+{ "user": "CNET", "text": "Observers of a humans vs. bots death match decided that the bots were more human than the actual humans http://t.co/BXNwyH8a", "retweets": 11.0 },
+{ "user": "CNET", "text": "RT @jeskillings: Apple's iOS 6 maps apology today: gracious http://t.co/F25NOxr6 Apple's iPhone 4 antenna apology in 2010: grudging http ...", "retweets": 37.0 },
+{ "user": "CNET", "text": "Getting started with the YouTube app in iOS 6 http://t.co/HhACyxfp", "retweets": 12.0 },
+{ "user": "CNET", "text": "Distressed camera maker Olympus is getting a $645 million boost care of Sony http://t.co/VzKufK5z", "retweets": 8.0 },
+{ "user": "CNET", "text": "Deal alert: Get a free year of SiriusXM Internet radio! http://t.co/LXUrqZ1R", "retweets": 42.0 },
+{ "user": "CNET", "text": "Kindle Fire 2012: Even hotter than last year. Check out our full review of Amazon's updated media tablet http://t.co/lUxnqomZ", "retweets": 15.0 },
+{ "user": "CNET", "text": "Music publisher Sony/ATV killed Apple's iPhone 5 music service, report says http://t.co/whx2JbAO", "retweets": 22.0 },
+{ "user": "CNET", "text": "5 great, cheap plasma TVs http://t.co/4QYARxB8", "retweets": 10.0 },
+{ "user": "CNET", "text": "How RIM could get you to buy into BlackBerry 10 http://t.co/AUo08CkG", "retweets": 15.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Apple \"fell short\" with iOS 6 maps, and we are \"extremely sorry,\" CEO Tim Cook says in open letter http://t.co/t1U4497r", "retweets": 139.0 },
+{ "user": "CNET", "text": "iPhone 5 to iPhone 4S: I vibrate so much harder than you http://t.co/1XMZVvVE", "retweets": 43.0 },
+{ "user": "CNET", "text": "Leak: Photos of RIM's BlackBerry 10 phones hit the web http://t.co/X0OvqMwL", "retweets": 46.0 },
+{ "user": "CNET", "text": "RT @CNETNews: Apple's Tim Cook: We are \"extremely sorry\" about those problems with iOS 6 Maps http://t.co/rlcCUgQO", "retweets": 114.0 },
+{ "user": "CNET", "text": "Samsung Galaxy Music leak shows budget music phone http://t.co/aTAjDKk2", "retweets": 14.0 },
+{ "user": "CNET", "text": "Bad Piggies review: Angry Birds follow-up bolts on complexity http://t.co/lmfK0Yjd", "retweets": 16.0 },
+{ "user": "CNET", "text": "HTC One X+ turns up in O2 brochure, Jelly Bean in tow http://t.co/MGvjw24E", "retweets": 26.0 }
+]"""
+ val gadgetlab = """[
+{ "user": "gadgetlab", "text": "A Clean, Well-Lighted Face: The Kindle Paperwhite http://t.co/WlUPzZbS by @strngwys", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "Maps? Purple halos in photos? Screen glitches? What? iPhone 5 'problems' explained http://t.co/xVGBVhGE by @redgirlsays", "retweets": 22.0 },
+{ "user": "gadgetlab", "text": "Tim Cook apologizes for Mapocalypse debacle, offers interim fixes http://t.co/ahPbLcU5 by @redgirlsays", "retweets": 18.0 },
+{ "user": "gadgetlab", "text": "Pretty Cities: Google Maps Improves Aerial Images http://t.co/nEopgNpY by @alexandra_chang", "retweets": 7.0 },
+{ "user": "gadgetlab", "text": "Nokia Lumia Premium Pricing Won?t Help Windows Phone Adoption: http://t.co/mbc7fu1a by @alexandra_chang", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "BlueStacks and AMD Bring 500,000 Android Apps to Windows 8: http://t.co/GskuXhRo by @alexandra_chang", "retweets": 22.0 },
+{ "user": "gadgetlab", "text": "BlackBerry 10 Could Be Too Little, Too Late http://t.co/ViQWBSYY By @Strngwys", "retweets": 28.0 },
+{ "user": "gadgetlab", "text": "And, in case you haven't seen it yet, @GadgetLab's review of iOS 6: http://t.co/PUf9Yisb by @redgirlsays", "retweets": 10.0 },
+{ "user": "gadgetlab", "text": "Our review of the iPhone 5: http://t.co/gkrfr1Bt by @redgirlsays", "retweets": 27.0 },
+{ "user": "gadgetlab", "text": "Ballmer Pitches Window 8 to Developers, But Keeps Tight Reign On Surface: http://t.co/TdkBZbXR by @alexandra_chang", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "Gadget Lab Show: Apple?s iPhone 5 Wins, Maps App Fails http://t.co/9gLGvagS with @redgirlsays + @strngwys", "retweets": 13.0 },
+{ "user": "gadgetlab", "text": "BlackBerry 10 features all-in-one approach to the inbox http://t.co/UzTPt3Dh by @strngwys", "retweets": 14.0 },
+{ "user": "gadgetlab", "text": "Update on @redgirlsays' #walletless adventures -- \"Living Walletless, Week One: My Kingdom for a Bagel\" http://t.co/3zEIPqdl", "retweets": 8.0 },
+{ "user": "gadgetlab", "text": "Foxconn rioting leaves factory closed, dozens injured http://t.co/VSgFcak5 by @redgirlsays", "retweets": 29.0 },
+{ "user": "gadgetlab", "text": "Who's Waiting in Line for the #iPhone5? These People Are: http://t.co/qeYJaGu2 by @redgirlsays", "retweets": 21.0 },
+{ "user": "gadgetlab", "text": "Operation iPhone Drop: From Cargo Plane to Door Stoop http://t.co/TEuxL13p By @strngwys", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "Know anyone around age 60 (parents, grandparents, yourself) looking to buy their first smartphone? Give @redgirlsays a ping!", "retweets": 4.0 },
+{ "user": "gadgetlab", "text": "iPhone 5 Exposed: iFixit Tears Down Apple's Latest http://t.co/4HbErQRw by @redgirlsays", "retweets": 35.0 },
+{ "user": "gadgetlab", "text": "Facebook?s New Plug-in Gives You Better Protection From Embarrassing Overshares: http://t.co/AbiHJWwJ by @alexandra_chang", "retweets": 10.0 },
+{ "user": "gadgetlab", "text": "Apple Mapocalypse Sends iOS 6 Users Into a Tizzy, Riverbank http://t.co/amGwTFUu by @redgirlsays", "retweets": 19.0 },
+{ "user": "gadgetlab", "text": "Don?t Miss the Bus, Gus: 7 Public Transit Apps (And One Workaround) for iOS 6 http://t.co/1mgyGbFC by @alexandra_chang", "retweets": 6.0 },
+{ "user": "gadgetlab", "text": "In case you missed it... Samsung continues Apple potshots with latest ad: http://t.co/IiNzQW7p by @redgirlsays", "retweets": 24.0 },
+{ "user": "gadgetlab", "text": "Multiple Wi-Fi Issues Plague iOS 6 Upgrade http://t.co/fI3LGTeY by @strngwys", "retweets": 42.0 },
+{ "user": "gadgetlab", "text": "Any Blackberry users (in the SF area) that are planning to switch to another smartphone platform? @alexandra_chang wants to talk.", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "Downloaded iOS 6 yet? Here are 13 tips, tricks and hidden features: http://t.co/IXWOHkXB by @strngwys and @redgirlsays", "retweets": 95.0 },
+{ "user": "gadgetlab", "text": "The details and specs on HTC's Windows Phone 8X and 8S: http://t.co/k8b0kQCZ by @alexandra_chang", "retweets": 14.0 },
+{ "user": "gadgetlab", "text": "HTC's 8X and 8S are \"Windows Phone's Best Shot\": http://t.co/2egTC11C by @alexandra_chang", "retweets": 10.0 },
+{ "user": "gadgetlab", "text": "Motorola?s Razr i: A Razr M With Intel Inside http://t.co/eLtTIxan by @nateog", "retweets": 19.0 },
+{ "user": "gadgetlab", "text": "Hi-Call Phone Gloves Bring Inspector Gadget Functionality to Life http://t.co/IbsRzeew by @nateog", "retweets": 33.0 },
+{ "user": "gadgetlab", "text": "Amazon Kindle Fire HD Teardown http://t.co/IR9rx6h3 by @nateog", "retweets": 35.0 },
+{ "user": "gadgetlab", "text": "Apple?s Jony Ive Designing One-Off Leica Camera http://t.co/s3PeVcUM by @karissabe", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "iFixit EarPod Teardown: Better Buds, But Don't Try Repairing Them http://t.co/IDtX4MBz by @redgirlsays", "retweets": 12.0 },
+{ "user": "gadgetlab", "text": "Galaxy Tab 10.1 Injunction Still Stands in Apple v. Samsung http://t.co/JwOCDnw1 by @redgirlsays", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "Hands-on with the redesigned Twitter for iPad http://t.co/JKLIVV4g by @redgirlsays", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "First iPhone 5 Benchmarks: Screaming Fast, Yes, But Just Shy of Galaxy S III http://t.co/QIAhda3L by @redgirlsays", "retweets": 79.0 },
+{ "user": "gadgetlab", "text": "Fitbit adds two new Bluetooth-compatible fitness trackers: The Fitbit Zip and Fitbit One http://t.co/38m1fUSR by @redgirlsays", "retweets": 4.0 },
+{ "user": "gadgetlab", "text": "This week on the Gadget Lab Show, iPhone 5 chat and hands on with Apple's EarPods: http://t.co/0DPubUFz", "retweets": 13.0 },
+{ "user": "gadgetlab", "text": "Microsoft News: Windows 8 Event and Hints on Surface Pricing: http://t.co/kswqw4gj by @alexandra_chang", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "Yowza: iPhone 5 tops two million in pre-orders in first 24 hours on sale. http://t.co/Nx2d9CBN by @redgirlsays", "retweets": 47.0 },
+{ "user": "gadgetlab", "text": "From earlier today: iPhone 5 pre-orders come flooding in despite numerous website hiccups http://t.co/tbyirA59 by @redgirlsays", "retweets": 18.0 },
+{ "user": "gadgetlab", "text": "The iPhone 5 Lightning adapter could be bad news for high-end docks. @strngwys reports: http://t.co/90eAf68y", "retweets": 16.0 },
+{ "user": "gadgetlab", "text": "Apple iPhone 5 Specs vs. the Competition?s: Which Will You Buy?: http://t.co/r6xALNpe by @alexandra_chang", "retweets": 33.0 },
+{ "user": "gadgetlab", "text": "Think the new iPhone is great, but kinda boring? Here are 5 things that would make us fall in love all over again: http://t.co/sKm9P78B", "retweets": 32.0 },
+{ "user": "gadgetlab", "text": "So long, Ping: Apple's shuttering its failed social network Sept. 30 http://t.co/wO75kCfd by @redgirlsays", "retweets": 21.0 },
+{ "user": "gadgetlab", "text": "Why Apple Made Three iPhone 5 Models and What That Means For You: http://t.co/ll39koPx by @alexandra_chang", "retweets": 25.0 },
+{ "user": "gadgetlab", "text": "Pricing and Availability: Why It's a Tough Spec for Everyone But Apple http://t.co/bYd27fYD by @redgirlsays", "retweets": 6.0 },
+{ "user": "gadgetlab", "text": "Apple Confirms iPhone 5 Pre-Orders Start at 12:01AM September 14 http://t.co/Cwenf7Zu By @strngwys", "retweets": 45.0 },
+{ "user": "gadgetlab", "text": "Hands on with the faster, lighter, longer iPhone 5: http://t.co/D5z6yu45 by @redgirlsays", "retweets": 31.0 },
+{ "user": "gadgetlab", "text": "RT @strngwys: Why the iPod classic will never go away. http://t.co/zhCQRfu3", "retweets": 8.0 },
+{ "user": "gadgetlab", "text": "What You Need to Know About Apple?s New iPhone 5 http://t.co/ArTQrO6b by @alexandra_chang", "retweets": 41.0 },
+{ "user": "gadgetlab", "text": "RT @redgirlsays: So, what do you think was the most exciting part of today's event? iPhone 5? iPod touch? nano? iTunes? Foo Fighters?", "retweets": 9.0 },
+{ "user": "gadgetlab", "text": "Apple's #iPhone5 event is now over. New iPods, new iTunes, new EarPods. All sorts of newness. See our LIVEBLOG here... http://t.co/5AtpHCfY", "retweets": 32.0 },
+{ "user": "gadgetlab", "text": "http://t.co/z7Dsi93S has been updated with all the new product details... http://t.co/8sH8jdas #iPhone5", "retweets": 42.0 },
+{ "user": "gadgetlab", "text": "RT @wired: Apple in-ear headphones now EarPods - Look like little aliens speaking in your ears. On sale today. http://t.co/PewWjvwX", "retweets": 88.0 },
+{ "user": "gadgetlab", "text": "RT @redgirlsays: Yes, that's the foo fighters. http://t.co/qgUpreGb", "retweets": 29.0 },
+{ "user": "gadgetlab", "text": "RT @redgirlsays: Wait, we're getting a Foo Fighters concert? I guess that's almost as good as Hot Chip that I missed last night?", "retweets": 6.0 },
+{ "user": "gadgetlab", "text": "Say goodbye to the iconic Apple Earbuds. A new headphone set is introduced...the Earpods http://t.co/op5vlGq4 #Apple #iPhone5 LIVEBLOG", "retweets": 31.0 },
+{ "user": "gadgetlab", "text": "New iPod Touch will be $299 for 32 GB &amp; $399 for 64 GB, shipping sometime October http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 52.0 },
+{ "user": "gadgetlab", "text": "New iPod Touch is available in 5 colors: white, black, cyan, yellow &amp; red http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 21.0 },
+{ "user": "gadgetlab", "text": "Siri will be on the new iPod Touch. Cool! http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 16.0 },
+{ "user": "gadgetlab", "text": "New iPod Touch has same 4-inch Retina display as #iPhone5 + A5 CPU making it the 1st dual-core Touch http://t.co/DwtKQkSu #Apple LIVEBLOG", "retweets": 25.0 },
+{ "user": "gadgetlab", "text": "New iPod Touch announced. It's just 6.1-mm thin. http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "New iPod Nano has 2.5-in multitouch display. Available in white, black, pink, green, blue, yellow &amp; red http://t.co/DwtKQkSu #Apple #iPhone5", "retweets": 26.0 },
+{ "user": "gadgetlab", "text": "7th-gen iPod nano is rectangular, 5 mm thin, about 40% than iPod nano it replaces http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 12.0 },
+{ "user": "gadgetlab", "text": "New iTunes for desktops will launch late October http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 10.0 },
+{ "user": "gadgetlab", "text": "#iOS6 will feature new iTunes stores and a new desktop iTunes (w iCloud built in) will launch too http://t.co/DwtKQkSu #Apple #iPhone5", "retweets": 12.0 },
+{ "user": "gadgetlab", "text": "#iOS6 hits the iPhone 4S, 4, 3GS, 3rd-gen iPad, iPad 2 &amp; 4th-gen iPod Touch on Sept. 19 http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 42.0 },
+{ "user": "gadgetlab", "text": "iPhone 4 (8 GB) is now free &amp; 4S (16GB) is now $99 on 2-year carrier contracts http://t.co/DwtKQkSu #Apple #iPhone5 LIVEBLOG", "retweets": 32.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 pre-orders start Sept. 14. Hits stores Sept. 21. http://t.co/DwtKQkSu #Apple LIVEBLOG", "retweets": 79.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 costs the same as the iPhone 4S: $199 for 16 Gb, $200 for 32 GB, $399 for 64 GB http://t.co/DwtKQkSu #Apple LIVEBLOG", "retweets": 66.0 },
+{ "user": "gadgetlab", "text": "?iPhone 5 is the best phone we?ve ever made,? @PSchiller says http://t.co/DwtKQkSu #Apple LIVEBLOG", "retweets": 27.0 },
+{ "user": "gadgetlab", "text": "#Apple says #iPhone5 has a 4-inch screen you can still comfortably use with one hand http://t.co/DwtKQkSu LIVEBLOG", "retweets": 23.0 },
+{ "user": "gadgetlab", "text": "#Apple #iPhone5 comes in either Slate (aka black) or White http://t.co/DwtKQkSu LIVEBLOG", "retweets": 17.0 },
+{ "user": "gadgetlab", "text": "#Apple: Shared Photo Streams in #iOS6 is \"easiest way to share photos with your friends? http://t.co/DwtKQkSu #iPhone5 LIVEBLOG #iPhone5", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "#Apple now talking iOS 6 updates for the #iPhone5 http://t.co/DwtKQkSu LIVEBLOG", "retweets": 25.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 Lightning connector is 80% smaller than old 30-pin connectors http://t.co/DwtKQkSu LIVEBLOG #Apple", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 has smaller, new connecter called \"Lightning\" in a nod to Thunderbolt ports on Macs http://t.co/DwtKQkSu LIVEBLOG #Apple", "retweets": 32.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 front camera shoots 720p, has Face detection &amp; #Apple announces FaceTime over cellular networks http://t.co/DwtKQkSu LIVEBLOG", "retweets": 94.0 },
+{ "user": "gadgetlab", "text": "#Apple #iPhone5 has 40% faster photo capture thanks iPhone 4S http://t.co/DwtKQkSu LIVEBLOG", "retweets": 17.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 has camera has new image sensor for better low light photos &amp; is 25% smaller http://t.co/DwtKQkSu LIVEBLOG #Apple", "retweets": 21.0 },
+{ "user": "gadgetlab", "text": "#Apple #iPhone5 camera is 8megapixels...same as iPhone 4S. But it's not the same camera http://t.co/DwtKQkSu #LIVEBLOG", "retweets": 30.0 },
+{ "user": "gadgetlab", "text": "#Apple #iPhone5 battery: 8hrs of 3G talk time, 8hrs of LTE or 3G browsing, 10hrs of WiFi browsing, 225hrs of standby http://t.co/DwtKQkSu", "retweets": 121.0 },
+{ "user": "gadgetlab", "text": "\"We've updated every aspect of iPhone 5,? @PSchiller says. #Apple #iPhone5 LIVEBLOG http://t.co/DwtKQkSu", "retweets": 16.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 display has 44% more color saturation. #Apple says its \"most accurate\" in industry http://t.co/DwtKQkSu LIVEBLOG by @redgirlsays", "retweets": 21.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 new A6 chip has 2x faster graphics and processing power http://t.co/DwtKQkSu LIVEBLOG by @redgirlsays", "retweets": 31.0 },
+{ "user": "gadgetlab", "text": "Yes, the #iPhone5 does have LTE. #Apple liveblog http://t.co/DwtKQkSu", "retweets": 19.0 },
+{ "user": "gadgetlab", "text": "#Apple #iPhone5 photos up on our liveblog ---&gt; http://t.co/DwtKQkSu", "retweets": 35.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 has has a 326 PPI Retina display. The new screen is 4 inches, 1136x640 pixels w/ 16x9 aspect ratio http://t.co/DwtKQkSu #Apple", "retweets": 76.0 },
+{ "user": "gadgetlab", "text": "#iPhone5 is the thinnest &amp; lightest phone Apple ever http://t.co/DwtKQkSu #Apple LIVEBLOG by @redgirlsays", "retweets": 46.0 },
+{ "user": "gadgetlab", "text": "Last month, Apple sold its 400 millionth iOS device. #iPhone5 LIVEBLOG http://t.co/DwtKQkSu by @redgirlsays", "retweets": 15.0 },
+{ "user": "gadgetlab", "text": "#Apple: More than 700,000 apps in App Store &amp; 250,000 specifically for iPad http://t.co/DwtKQkSu #iPhone5 LIVEBLOG by @redgirlsays", "retweets": 6.0 },
+{ "user": "gadgetlab", "text": "#Apple CEO Tim Cook: 7million copies of Mountain Lion downloaded so far. Fastest selling version of OS X ever http://t.co/DwtKQkSu LIVEBLOG", "retweets": 10.0 },
+{ "user": "gadgetlab", "text": "Apple has 380 stores in 12 countries. On Friday it will open a store in its 13th, Sweden. http://t.co/5AtpHCfY #Apple #iPhone5 LIVEBLOG", "retweets": 11.0 },
+{ "user": "gadgetlab", "text": "#Apple CEO Tim Cook takes the stage at #iPhone5 event, has \"really cool stuff to show you.? http://t.co/DwtKQkSu liveblog by @redgirlsays", "retweets": 25.0 },
+{ "user": "gadgetlab", "text": "RT @wired: FYI, @wired's liveblog photos are Creative Commons - repost, if you like, with link and license http://t.co/PewWjvwX", "retweets": 26.0 },
+{ "user": "gadgetlab", "text": "Soon, photos from the inside of Apple's iPhone 5 event. @redgirlsays + @johnwbradley are on the scene... http://t.co/2mqmIhVB", "retweets": 14.0 },
+{ "user": "gadgetlab", "text": "Apple iPhone 5 event LIVEBLOG: the event starts in 30 mins but we've got some photos of reporters standing in line! http://t.co/2mqmIhVB", "retweets": 26.0 },
+{ "user": "gadgetlab", "text": "http://t.co/z7Dsi93S Search Confirms iPhone 5 with LTE Support, Plus New iPods http://t.co/a7mZnloy by @alexandra_chang", "retweets": 30.0 },
+{ "user": "gadgetlab", "text": "@redgirlsays on the scene at Apple's iPhone 5 unveiling &amp; she has photos...of the outside of the event. Inside soon! http://t.co/5AtpHCfY", "retweets": 0.0 },
+{ "user": "gadgetlab", "text": "Demoing at an Apple Event: Secretive, Stressful, and (Probably) Worth It http://t.co/7NOIDSJw by @strngwys", "retweets": 11.0 }
+]"""
+ val mashable = """[
+{ "user": "mashable", "text": "Which is sorriest, #Apple Maps or #Siri? @Charlie_White has the comic - http://t.co/YRXXJXGY", "retweets": 55.0 },
+{ "user": "mashable", "text": "Watch These Quadrotor #Robots Learn to Play Catch [VIDEO] http://t.co/CHZ5BPfL", "retweets": 21.0 },
+{ "user": "mashable", "text": "10 Funny @YouTube Clips of Sleeping Dogs http://t.co/ukPgWZjH", "retweets": 34.0 },
+{ "user": "mashable", "text": "65 Digital Media Resources You May Have Missed http://t.co/u1LJWc22", "retweets": 64.0 },
+{ "user": "mashable", "text": ".@HP Shows What @Windows 8 #Tablets for Business Will Look Like [PICS] http://t.co/Md6ua8Xc", "retweets": 32.0 },
+{ "user": "mashable", "text": "Twice as Many #Mobile News Readers Prefer Browsers to #Apps [STUDY] http://t.co/lEXhVLLJ", "retweets": 40.0 },
+{ "user": "mashable", "text": "7 Free #Android Apps for Killing Time in Lines http://t.co/eKu5hhsh", "retweets": 42.0 },
+{ "user": "mashable", "text": "Here's what you can expect at the Mashable #MediaSummit - http://t.co/S5Hxw3Iz Get your tickets today!", "retweets": 27.0 },
+{ "user": "mashable", "text": "'Dexter' Season 7 Premiere Available in Full on @YouTube [VIDEO] http://t.co/Cc5HeiZZ", "retweets": 345.0 },
+{ "user": "mashable", "text": ".@Amazon #Kindle Paperwhite is the Best E-Ink Screen You Can Buy [REVIEW] http://t.co/JDenefqi", "retweets": 55.0 },
+{ "user": "mashable", "text": "Watch These Epic #iPhone 5 Vs. #Android Music Videos http://t.co/Sf0De6de", "retweets": 49.0 },
+{ "user": "mashable", "text": "One Proposal to Cool a Warming Planet: An Umbrella Made of Asteroids http://t.co/ote1Dw3Z via @theatlantic", "retweets": 33.0 },
+{ "user": "mashable", "text": "Viral Video Recap: Bad Actors, #Apple Trolling and Slow-Motion Fireballs http://t.co/62XWSdWx", "retweets": 28.0 },
+{ "user": "mashable", "text": "#DidYouKnow 81% of mobile phone subscribers use text messaging? http://t.co/ABJMZpNZ", "retweets": 55.0 },
+{ "user": "mashable", "text": "Smartphone Camera Shootout: iPhone 5 vs. Galaxy SIII vs. iPhone 4S http://t.co/Qp0PM0fh", "retweets": 85.0 },
+{ "user": "mashable", "text": "Simon Cowell and http://t.co/sLfE3y62 Planning Show to Find the Next Steve Jobs http://t.co/zvIk0dwc", "retweets": 145.0 },
+{ "user": "mashable", "text": "Xi3 Packs a Fully Functional Desktop PC Into a Tiny Cube [VIDEO] http://t.co/OcOqZkU6", "retweets": 21.0 },
+{ "user": "mashable", "text": "How ?Mad Men? Could Turn You Into The Worst Employee Ever [INFOGRAPHIC] http://t.co/NngQ3qDT via @TheJaneDough", "retweets": 95.0 },
+{ "user": "mashable", "text": "Tumblr Will Be Down Next Weekend. Here's Why: http://t.co/xN8yFINP", "retweets": 129.0 },
+{ "user": "mashable", "text": "Top 10 Tech This Week http://t.co/N4emMtmD", "retweets": 50.0 },
+{ "user": "mashable", "text": "Which is Sorriest, Apple Maps or Siri? [SUNDAY COMICS] http://t.co/eka8gts0", "retweets": 89.0 },
+{ "user": "mashable", "text": "Book About Kickstarter Fails to Raise Enough Money on Kickstarter http://t.co/SpKYdnOx", "retweets": 59.0 },
+{ "user": "mashable", "text": "Top 10 #GIFs of the Week http://t.co/YVYDncNc", "retweets": 32.0 },
+{ "user": "mashable", "text": "Spookiest Smartphone Malware Yet? http://t.co/5Kk1DyPg via @TheAtlantic", "retweets": 57.0 },
+{ "user": "mashable", "text": "5 Mobile Photographers Capturing the World With #Android http://t.co/786NneBt", "retweets": 78.0 },
+{ "user": "mashable", "text": "Does This Video Show the iPad Mini? http://t.co/0Sbdpiu1", "retweets": 27.0 },
+{ "user": "mashable", "text": "Subscribe to Mashable on Kindle for the latest tech and social news on the go - http://t.co/HWWj3Xii", "retweets": 10.0 },
+{ "user": "mashable", "text": "Facebook to FTC: Let Us Advertise to Children http://t.co/mipwVjbp", "retweets": 28.0 },
+{ "user": "mashable", "text": "4 Tips to Avoid Work-From-Home Email Scams http://t.co/j4qxnmrj", "retweets": 43.0 },
+{ "user": "mashable", "text": "10 Amazing Google Earth and Maps Discoveries http://t.co/UyDMs135", "retweets": 144.0 },
+{ "user": "mashable", "text": "Top 5 Apps Your Kids Will Love This Week http://t.co/7YeW58Af", "retweets": 39.0 },
+{ "user": "mashable", "text": "#DidYouKnow T9 is short for \"Text on 9 keys\"? http://t.co/9rAFobkz", "retweets": 145.0 },
+{ "user": "mashable", "text": "Is your university better at social networking than you are? See how it ranks here: http://t.co/6aM1zpFa", "retweets": 57.0 },
+{ "user": "mashable", "text": "Facebook's upgraded its Messenger app for iPhone, and @ToddWasserman took it for a spin: http://t.co/MMgCEsJ2", "retweets": 47.0 },
+{ "user": "mashable", "text": "Imagine how it would feel to get fired from @Facebook. One man describes his experience. @joannellepan has the story -...", "retweets": 21.0 },
+{ "user": "mashable", "text": "Looking for a stand for your #iPad that doubles as a speaker? @Charlie_White found a great one: http://t.co/ImpCsgkd", "retweets": 49.0 },
+{ "user": "mashable", "text": "What would Psy's 'Gangnam Style' video be like without music? Even quirkier. @neeeda has the video - http://t.co/z6xrmclV", "retweets": 80.0 },
+{ "user": "mashable", "text": "Who knew there were such great games available for less than $15? @ChelseaBot, that's who: http://t.co/j5yqMbYd", "retweets": 24.0 },
+{ "user": "mashable", "text": "The 'Ostrich Pillow' claims to give you the ideal power-napping experience. @neeeda has more - http://t.co/YCzvanHH", "retweets": 77.0 },
+{ "user": "mashable", "text": "#DidYouKnow the Dvorak is a more efficient keyboard layout than QWERTY? http://t.co/IlHnfSsz", "retweets": 162.0 },
+{ "user": "mashable", "text": "Find out which fashion brands dominate social media - @ToddWasserman brings you this handy chart: http://t.co/hArNJOkE", "retweets": 49.0 },
+{ "user": "mashable", "text": "This iPad keyboard reveals what's wrong with Kickstarter. @PetePachal reviews it: http://t.co/2yjhGnY9", "retweets": 18.0 },
+{ "user": "mashable", "text": "Apple's given up on calling its Maps app the \"most powerful ever.\" @ToddWasserman explains why: http://t.co/3YatPmWf", "retweets": 98.0 },
+{ "user": "mashable", "text": "Week's worth of TV boiled down to the best 12 GIFs, thanks to the keen eye of @ChristErickson http://t.co/BAUiteqd", "retweets": 22.0 },
+{ "user": "mashable", "text": "Power Nap With This Head-Consuming Ostrich Pillow http://t.co/d34dzlPT", "retweets": 107.0 },
+{ "user": "mashable", "text": "#Bots Drive 16% of U.S. Web Traffic [INFOGRAPHIC] http://t.co/lfFAjUy6", "retweets": 84.0 },
+{ "user": "mashable", "text": "#Apple Cites Improved Working Conditions in Foreign Factories [REPORT] http://t.co/PKmkIYYK", "retweets": 22.0 },
+{ "user": "mashable", "text": "Bizarre @YouTube Video Removes Music From 'Gangnam Style' http://t.co/AzSLo2cq", "retweets": 47.0 },
+{ "user": "mashable", "text": "These #Robots Can Help Disabled Officers, Veterans Get Back to Work [VIDEO] http://t.co/wg29VIx7", "retweets": 28.0 },
+{ "user": "mashable", "text": "Like viral videos and gadget demos? Subscribe to the Mashable YouTube channel - http://t.co/Sp0NWvgo", "retweets": 23.0 },
+{ "user": "mashable", "text": "Biodegradable Medical Implants Dissolve in Water [VIDEO] http://t.co/XVseXUaa", "retweets": 36.0 },
+{ "user": "mashable", "text": "5 Easy Steps To Make Your Job Descriptions Go Viral http://t.co/a5ArxrL5", "retweets": 56.0 },
+{ "user": "mashable", "text": "Why Social Media Makes Customer Service Better http://t.co/JkdkzqnV", "retweets": 167.0 },
+{ "user": "mashable", "text": "10 Essential Resources for Bootstrapping #Businesses http://t.co/1e3hqvZA", "retweets": 39.0 },
+{ "user": "mashable", "text": "The Digerati: 7 Profiles of Successful #Entrepreneurs http://t.co/M5l5CmNF", "retweets": 25.0 },
+{ "user": "mashable", "text": "10 Amazing @Google Earth and Maps Discoveries http://t.co/gDrlBXJ8", "retweets": 115.0 },
+{ "user": "mashable", "text": "Former Employee Explains Why He Was Fired From @Facebook http://t.co/3n1wb5fJ", "retweets": 56.0 },
+{ "user": "mashable", "text": "6 #Apps You Don't Want to Miss http://t.co/cQxIq9VU", "retweets": 78.0 },
+{ "user": "mashable", "text": "#DidYouKnow the QWERTY keyboard layout was invented in 1868 -- and it's proven to be inefficient? http://t.co/5q3kKrQy", "retweets": 152.0 },
+{ "user": "mashable", "text": "Which Are the Most Social Colleges? [INFOGRAPHIC] http://t.co/8eztys5v", "retweets": 60.0 },
+{ "user": "mashable", "text": "Facebook Embraces Bubbles in Messenger iPhone Upgrade http://t.co/NaFHao22", "retweets": 61.0 },
+{ "user": "mashable", "text": "Zooka Speaker Bar Makes Any Bluetooth Device Sound Better [REVIEW] http://t.co/LN6jWldu", "retweets": 23.0 },
+{ "user": "mashable", "text": "Top Comments From Mashable Readers This Week http://t.co/E0ANQg3q", "retweets": 18.0 },
+{ "user": "mashable", "text": "FinderCodes Uses QR Codes to Find Lost Phones http://t.co/kn3euvFy", "retweets": 28.0 },
+{ "user": "mashable", "text": "5 Games You Should Play This Weekend http://t.co/t0kxXONY", "retweets": 42.0 },
+{ "user": "mashable", "text": "How Much Bandwidth Will the Vice Presidential Debate Need? http://t.co/ysANGsVP", "retweets": 33.0 },
+{ "user": "mashable", "text": "#DidYouKnow the JPG is a compression technique, not a file size? http://t.co/bVJjxUFJ", "retweets": 117.0 },
+{ "user": "mashable", "text": "Something Is Missing, Batman [COMIC] http://t.co/TBlNdfBg", "retweets": 20.0 },
+{ "user": "mashable", "text": "Burberry Tops Fashion Brands In Social Media This Week [CHART] http://t.co/8GZl4U5t", "retweets": 64.0 },
+{ "user": "mashable", "text": "Video Callers Know No Bounds ? Not Even Bathrooms http://t.co/JmNNbAkZ via @BNDarticles", "retweets": 14.0 },
+{ "user": "mashable", "text": "CruxSkunk iPad Keyboard Exposes the Mirage of Kickstarter [REVIEW] http://t.co/R7jjva6V", "retweets": 13.0 },
+{ "user": "mashable", "text": "Apple No Longer Saying Maps Are 'Most Powerful Ever' http://t.co/MhofMrC7", "retweets": 100.0 },
+{ "user": "mashable", "text": "Doing some weekend reading? Check out the new and improved Mashable iPad app for our latest news - http://t.co/G1S46WJe", "retweets": 11.0 },
+{ "user": "mashable", "text": "Parking Panda App Finds and Guarantees Parking Spots http://t.co/D9kl2Hsc", "retweets": 41.0 },
+{ "user": "mashable", "text": "This Week's Top TV Moments in GIF Form http://t.co/V2asPRBp", "retweets": 26.0 },
+{ "user": "mashable", "text": "Small Business Advertisers Like Facebook's Immediacy, But Not Its Metrics http://t.co/KPYD1UlC", "retweets": 50.0 },
+{ "user": "mashable", "text": "iOS 6 Users Complain About Wi-Fi, Connectivity Issues - http://t.co/iogRstNn", "retweets": 180.0 },
+{ "user": "mashable", "text": "#DidYouKnow the 1962 'Sensorama Stimulator' was a precursor to augmented reality?http://t.co/0t4Eqjuq", "retweets": 59.0 },
+{ "user": "mashable", "text": "Our iPhones Are Depleting the Earth's Resources [INFOGRAPHIC] http://t.co/XnTLqe0p", "retweets": 205.0 },
+{ "user": "mashable", "text": "iPad 4 Has Carbon Fiber Body, Flexible Display [REPORT] http://t.co/Dft5VoXD via @tabtimes", "retweets": 198.0 },
+{ "user": "mashable", "text": "Disagreement over Voice Guided Turn-by-Turn Convinced Apple to Develop own Map App http://t.co/tMwL9IVN", "retweets": 49.0 },
+{ "user": "mashable", "text": "Derrick Rose's Brilliant Digital-Only Marketing Campaign #TheReturn http://t.co/S2lfS9Sw", "retweets": 73.0 },
+{ "user": "mashable", "text": "New Rolling Stone Cover Features YouTube-Grown duo Garmin http://t.co/gxug1n8F", "retweets": 18.0 },
+{ "user": "mashable", "text": "Facebook and the Gates Foundation Organize Hackathon to Develop Apps for Future College Students http://t.co/7tvGDMCe", "retweets": 80.0 },
+{ "user": "mashable", "text": "#DidYouKnow Sennheiser made a $40,000 pair of headphones? http://t.co/JbOWGElL", "retweets": 71.0 },
+{ "user": "mashable", "text": "Only 10% of Tweets About Apple Maps Are Positive [REPORT] http://t.co/wmGh8pGx", "retweets": 116.0 },
+{ "user": "mashable", "text": "Justin Bieber's Fan Tribute Gets 200,000 Retweets http://t.co/gWtxdPHa", "retweets": 14.0 },
+{ "user": "mashable", "text": "These Are the New Tech Job Hot Spots [INFOGRAPHIC] http://t.co/w1e2AHMp", "retweets": 79.0 },
+{ "user": "mashable", "text": "82 tech gadgets and products that are worth the splurge - http://t.co/HaNdfLRW #TheHotList", "retweets": 98.0 },
+{ "user": "mashable", "text": "Startup Marketplace Connects Fashion Brands With Boutiques http://t.co/IQ0rA7HI", "retweets": 27.0 },
+{ "user": "mashable", "text": "Time Inc. CEO Promises Less Free Content http://t.co/Ftb2w4me", "retweets": 20.0 },
+{ "user": "mashable", "text": "You can now read the latest from Mashable on @pulsepad - http://t.co/h3d6lULz", "retweets": 15.0 },
+{ "user": "mashable", "text": "Many Windows 8 Tablets Will Sport a Keyboard http://t.co/5OpFx5VR via: @techreview", "retweets": 33.0 },
+{ "user": "mashable", "text": "The Bluth Family Gets Political in New Tumblr Page http://t.co/t4OzctO0", "retweets": 16.0 },
+{ "user": "mashable", "text": "China's Internet Is Getting Faster [INFOGRAPHIC] http://t.co/4dqj8Ln0", "retweets": 63.0 },
+{ "user": "mashable", "text": "Camera+ Arrives on the iPad http://t.co/EG4nDhsQ", "retweets": 25.0 },
+{ "user": "mashable", "text": "Cute Images Make Us 44% Sharper [STUDY] http://t.co/I4IiIbHX", "retweets": 42.0 },
+{ "user": "mashable", "text": "Take 100 NASA Photos, Stir, Make Van Gogh's 'Starry Night' http://t.co/dQv6mvwI", "retweets": 73.0 },
+{ "user": "mashable", "text": "Why Being Unfriended on Facebook Hurts [STUDY] http://t.co/0eHHUrTo", "retweets": 51.0 },
+{ "user": "mashable", "text": "Think Apple Maps Is Bad? Remember the Tale of MobileMe http://t.co/Koeew9eU", "retweets": 41.0 }
+]"""
+}
diff --git a/Scala/objsets/src/main/scala/objsets/TweetReader.scala b/Scala/objsets/src/main/scala/objsets/TweetReader.scala
new file mode 100644
index 0000000..289e6ed
--- /dev/null
+++ b/Scala/objsets/src/main/scala/objsets/TweetReader.scala
@@ -0,0 +1,78 @@
+package objsets
+
+object TweetReader {
+
+ object ParseTweets {
+ import scala.util.parsing.json._
+
+ def getList[T](s: String): List[T] =
+ JSON.parseFull(s).get.asInstanceOf[List[T]]
+
+ def getMap(s: String): Map[String, Any] =
+ JSON.parseFull(s).get.asInstanceOf[Map[String, Any]]
+
+ def getTweets(user: String, json: String): List[Tweet] =
+ for (map <- getList[Map[String, Any]](json)) yield {
+ val text = map("text")
+ val retweets = map("retweet_count")
+ new Tweet(user, text.toString, retweets.toString.toDouble.toInt)
+ }
+
+ def getTweetData(user: String, json: String): List[Tweet] = {
+ // is list
+ val l = getList[Map[String, Any]](json)
+ for (map <- l) yield {
+ val text = map("text")
+ val retweets = map("retweets")
+ new Tweet(user, text.toString, retweets.toString.toDouble.toInt)
+ }
+ }
+ }
+
+ def toTweetSet(l: List[Tweet]): TweetSet = {
+ l.foldLeft(new Empty: TweetSet)(_.incl(_))
+ }
+
+ def unparseToData(tws: List[Tweet]): String = {
+ val buf = new StringBuffer
+ for (tw <- tws) {
+ val json = "{ \"user\": \"" + tw.user + "\", \"text\": \"" +
+ tw.text.replaceAll(""""""", "\\\\\\\"") + "\", \"retweets\": " +
+ tw.retweets + ".0 }"
+ buf.append(json + ",\n")
+ }
+ buf.toString
+ }
+
+ val sites = List("gizmodo", "TechCrunch", "engadget", "amazondeals", "CNET", "gadgetlab", "mashable")
+
+ private val gizmodoTweets = TweetReader.ParseTweets.getTweetData("gizmodo", TweetData.gizmodo)
+ private val techCrunchTweets = TweetReader.ParseTweets.getTweetData("TechCrunch", TweetData.TechCrunch)
+ private val engadgetTweets = TweetReader.ParseTweets.getTweetData("engadget", TweetData.engadget)
+ private val amazondealsTweets = TweetReader.ParseTweets.getTweetData("amazondeals", TweetData.amazondeals)
+ private val cnetTweets = TweetReader.ParseTweets.getTweetData("CNET", TweetData.CNET)
+ private val gadgetlabTweets = TweetReader.ParseTweets.getTweetData("gadgetlab", TweetData.gadgetlab)
+ private val mashableTweets = TweetReader.ParseTweets.getTweetData("mashable", TweetData.mashable)
+
+ private val sources = List(gizmodoTweets, techCrunchTweets, engadgetTweets, amazondealsTweets, cnetTweets, gadgetlabTweets, mashableTweets)
+
+ val tweetMap: Map[String, List[Tweet]] =
+ Map() ++ Seq((sites(0) -> gizmodoTweets),
+ (sites(1) -> techCrunchTweets),
+ (sites(2) -> engadgetTweets),
+ (sites(3) -> amazondealsTweets),
+ (sites(4) -> cnetTweets),
+ (sites(5) -> gadgetlabTweets),
+ (sites(6) -> mashableTweets))
+
+ val tweetSets: List[TweetSet] = sources.map(tweets => toTweetSet(tweets))
+
+ private val siteTweetSetMap: Map[String, TweetSet] =
+ Map() ++ (sites zip tweetSets)
+
+ private def unionOfAllTweetSets(curSets: List[TweetSet], acc: TweetSet): TweetSet =
+ if (curSets.isEmpty) acc
+ else unionOfAllTweetSets(curSets.tail, acc.union(curSets.head))
+
+ val allTweets: TweetSet = unionOfAllTweetSets(tweetSets, new Empty)
+}
diff --git a/Scala/objsets/src/main/scala/objsets/TweetSet.scala b/Scala/objsets/src/main/scala/objsets/TweetSet.scala
new file mode 100644
index 0000000..9200c50
--- /dev/null
+++ b/Scala/objsets/src/main/scala/objsets/TweetSet.scala
@@ -0,0 +1,200 @@
+package objsets
+
+import common._
+import TweetReader._
+
+/**
+ * A class to represent tweets.
+ */
+class Tweet(val user: String, val text: String, val retweets: Int) {
+ override def toString: String =
+ "User: " + user + "\n" +
+ "Text: " + text + " [" + retweets + "]"
+}
+
+/**
+ * This represents a set of objects of type `Tweet` in the form of a binary search
+ * tree. Every branch in the tree has two children (two `TweetSet`s). There is an
+ * invariant which always holds: for every branch `b`, all elements in the left
+ * subtree are smaller than the tweet at `b`. The eleemnts in the right subtree are
+ * larger.
+ *
+ * Note that the above structure requires us to be able to compare two tweets (we
+ * need to be able to say which of two tweets is larger, or if they are equal). In
+ * this implementation, the equality / order of tweets is based on the tweet's text
+ * (see `def incl`). Hence, a `TweetSet` could not contain two tweets with the same
+ * text from different users.
+ *
+ *
+ * The advantage of representing sets as binary search trees is that the elements
+ * of the set can be found quickly. If you want to learn more you can take a look
+ * at the Wikipedia page [1], but this is not necessary in order to solve this
+ * assignment.
+ *
+ * [1] http://en.wikipedia.org/wiki/Binary_search_tree
+ */
+abstract class TweetSet {
+
+ /**
+ * This method takes a predicate and returns a subset of all the elements
+ * in the original set for which the predicate is true.
+ *
+ * Question: Can we implment this method here, or should it remain abstract
+ * and be implemented in the subclasses?
+ */
+ def filter(p: Tweet => Boolean): TweetSet = ???
+
+ /**
+ * This is a helper method for `filter` that propagetes the accumulated tweets.
+ */
+ def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet
+
+ /**
+ * Returns a new `TweetSet` that is the union of `TweetSet`s `this` and `that`.
+ *
+ * Question: Should we implment this method here, or should it remain abstract
+ * and be implemented in the subclasses?
+ */
+ def union(that: TweetSet): TweetSet = ???
+
+ /**
+ * Returns the tweet from this set which has the greatest retweet count.
+ *
+ * Calling `mostRetweeted` on an empty set should throw an exception of
+ * type `java.util.NoSuchElementException`.
+ *
+ * Question: Should we implment this method here, or should it remain abstract
+ * and be implemented in the subclasses?
+ */
+ def mostRetweeted: Tweet = ???
+
+ /**
+ * Returns a list containing all tweets of this set, sorted by retweet count
+ * in descending order. In other words, the head of the resulting list should
+ * have the highest retweet count.
+ *
+ * Hint: the method `remove` on TweetSet will be very useful.
+ * Question: Should we implment this method here, or should it remain abstract
+ * and be implemented in the subclasses?
+ */
+ def descendingByRetweet: TweetList = ???
+
+
+ /**
+ * The following methods are already implemented
+ */
+
+ /**
+ * Returns a new `TweetSet` which contains all elements of this set, and the
+ * the new element `tweet` in case it does not already exist in this set.
+ *
+ * If `this.contains(tweet)`, the current set is returned.
+ */
+ def incl(tweet: Tweet): TweetSet
+
+ /**
+ * Returns a new `TweetSet` which excludes `tweet`.
+ */
+ def remove(tweet: Tweet): TweetSet
+
+ /**
+ * Tests if `tweet` exists in this `TweetSet`.
+ */
+ def contains(tweet: Tweet): Boolean
+
+ /**
+ * This method takes a function and applies it to every element in the set.
+ */
+ def foreach(f: Tweet => Unit): Unit
+}
+
+class Empty extends TweetSet {
+
+ def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet = ???
+
+
+ /**
+ * The following methods are already implemented
+ */
+
+ def contains(tweet: Tweet): Boolean = false
+
+ def incl(tweet: Tweet): TweetSet = new NonEmpty(tweet, new Empty, new Empty)
+
+ def remove(tweet: Tweet): TweetSet = this
+
+ def foreach(f: Tweet => Unit): Unit = ()
+}
+
+class NonEmpty(elem: Tweet, left: TweetSet, right: TweetSet) extends TweetSet {
+
+ def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet = ???
+
+
+ /**
+ * The following methods are already implemented
+ */
+
+ def contains(x: Tweet): Boolean =
+ if (x.text < elem.text) left.contains(x)
+ else if (elem.text < x.text) right.contains(x)
+ else true
+
+ def incl(x: Tweet): TweetSet = {
+ if (x.text < elem.text) new NonEmpty(elem, left.incl(x), right)
+ else if (elem.text < x.text) new NonEmpty(elem, left, right.incl(x))
+ else this
+ }
+
+ def remove(tw: Tweet): TweetSet =
+ if (tw.text < elem.text) new NonEmpty(elem, left.remove(tw), right)
+ else if (elem.text < tw.text) new NonEmpty(elem, left, right.remove(tw))
+ else left.union(right)
+
+ def foreach(f: Tweet => Unit): Unit = {
+ f(elem)
+ left.foreach(f)
+ right.foreach(f)
+ }
+}
+
+trait TweetList {
+ def head: Tweet
+ def tail: TweetList
+ def isEmpty: Boolean
+ def foreach(f: Tweet => Unit): Unit =
+ if (!isEmpty) {
+ f(head)
+ tail.foreach(f)
+ }
+}
+
+object Nil extends TweetList {
+ def head = throw new java.util.NoSuchElementException("head of EmptyList")
+ def tail = throw new java.util.NoSuchElementException("tail of EmptyList")
+ def isEmpty = true
+}
+
+class Cons(val head: Tweet, val tail: TweetList) extends TweetList {
+ def isEmpty = false
+}
+
+
+object GoogleVsApple {
+ val google = List("android", "Android", "galaxy", "Galaxy", "nexus", "Nexus")
+ val apple = List("ios", "iOS", "iphone", "iPhone", "ipad", "iPad")
+
+ lazy val googleTweets: TweetSet = ???
+ lazy val appleTweets: TweetSet = ???
+
+ /**
+ * A list of all tweets mentioning a keyword from either apple or google,
+ * sorted by the number of retweets.
+ */
+ lazy val trending: TweetList = ???
+}
+
+object Main extends App {
+ // Print the trending tweets
+ GoogleVsApple.trending foreach println
+}
diff --git a/Scala/objsets/src/test/scala/objsets/TweetSetSuite.scala b/Scala/objsets/src/test/scala/objsets/TweetSetSuite.scala
new file mode 100644
index 0000000..be82938
--- /dev/null
+++ b/Scala/objsets/src/test/scala/objsets/TweetSetSuite.scala
@@ -0,0 +1,72 @@
+package objsets
+
+import org.scalatest.FunSuite
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class TweetSetSuite extends FunSuite {
+ trait TestSets {
+ val set1 = new Empty
+ val set2 = set1.incl(new Tweet("a", "a body", 20))
+ val set3 = set2.incl(new Tweet("b", "b body", 20))
+ val c = new Tweet("c", "c body", 7)
+ val d = new Tweet("d", "d body", 9)
+ val set4c = set3.incl(c)
+ val set4d = set3.incl(d)
+ val set5 = set4c.incl(d)
+ }
+
+ def asSet(tweets: TweetSet): Set[Tweet] = {
+ var res = Set[Tweet]()
+ tweets.foreach(res += _)
+ res
+ }
+
+ def size(set: TweetSet): Int = asSet(set).size
+
+ test("filter: on empty set") {
+ new TestSets {
+ assert(size(set1.filter(tw => tw.user == "a")) === 0)
+ }
+ }
+
+ test("filter: a on set5") {
+ new TestSets {
+ assert(size(set5.filter(tw => tw.user == "a")) === 1)
+ }
+ }
+
+ test("filter: 20 on set5") {
+ new TestSets {
+ assert(size(set5.filter(tw => tw.retweets == 20)) === 2)
+ }
+ }
+
+ test("union: set4c and set4d") {
+ new TestSets {
+ assert(size(set4c.union(set4d)) === 4)
+ }
+ }
+
+ test("union: with empty set (1)") {
+ new TestSets {
+ assert(size(set5.union(set1)) === 4)
+ }
+ }
+
+ test("union: with empty set (2)") {
+ new TestSets {
+ assert(size(set1.union(set5)) === 4)
+ }
+ }
+
+ test("descending: set5") {
+ new TestSets {
+ val trends = set5.descendingByRetweet
+ assert(!trends.isEmpty)
+ assert(trends.head.user == "a" || trends.head.user == "b")
+ }
+ }
+}