Version increment using gradle task - gradle

I want to increase the version number of my project from 1.0.0. to 1.0.1 automatically whenever a new build is made through bash command. I only need to increase path number and others i will be increasing manually during manual build.
i want to change
this :
version=1.0.0
to
This:
version=1.0.1
using gradle task.
any help that how can i do this .
Is there any way to update this using regex or using substring function.

Here is an example task:
version='1.0.0' //version we need to change
task increment<<{
def v=buildFile.getText().find(version) //get this build file's text and extract the version value
String minor=v.substring(v.lastIndexOf('.')+1) //get last digit
int m=minor.toInteger()+1 //increment
String major=v.substring(0,v.length()-1) //get the beginning
//println m
String s=buildFile.getText().replaceFirst("version='$version'","version='"+major+m+"'")
//println s
buildFile.setText(s) //replace the build file's text
}
Run this task several times and you should see the version change.
A variant:
version='1.0.0'
task incrementVersion<<{
String minor=version.substring(version.lastIndexOf('.')+1)
int m=minor.toInteger()+1
String major=version.substring(0,version.length()-1)
String s=buildFile.getText().replaceFirst("version='$version'","version='"+major+m+"'")
buildFile.setText(s)
}

Here's a custom task for version bumps in Gradle (Android project):
class Version {
private int major
private int minor
private int patch
private int code
Version(int code, String version) {
this.code = code
def (major, minor, patch) = version.tokenize('.')
this.major = major.toInteger()
this.minor = minor.toInteger()
this.patch = patch.toInteger()
}
#SuppressWarnings("unused")
void bumpMajor() {
major += 1
minor = 0
patch = 0
code += 1
}
#SuppressWarnings("unused")
void bumpMinor() {
minor += 1
patch = 0
code += 1
}
#SuppressWarnings("unused")
void bumpPatch() {
patch += 1
code += 1
}
String getName() { "$major.$minor.$patch" }
int getCode() { code }
}
tasks.addRule("Pattern: bump<TYPE>Version") { String taskName ->
if (taskName.matches("bump(Major|Minor|Patch)Version")) {
task(taskName) {
doLast {
String type = (taskName - 'bump' - 'Version')
println "Bumping ${type.toLowerCase()} version…"
int oldVersionCode = android.defaultConfig.versionCode
String oldVersionName = android.defaultConfig.versionName
version = new Version(oldVersionCode, oldVersionName)
version."bump$type"()
String newVersionName = version.getName()
String newVersionCode = version.getCode()
println "$oldVersionName ($oldVersionCode) → $newVersionName ($newVersionCode)"
def updated = buildFile.getText()
updated = updated.replaceFirst("versionName '$oldVersionName'", "versionName '$newVersionName'")
updated = updated.replaceFirst("versionCode $oldVersionCode", "versionCode $newVersionCode")
buildFile.setText(updated)
}
}
}
}
See this Kanji learning Android app for completeness.
Prerequisites
Following format required (note the single quotes):
android {
defaultConfig {
versionCode 3
versionName '0.3.13'
}
}
Usage
$ ./gradlew bumpPatchVersion
> Task :app:bumpPatchVersion
Bumping patch version…
0.3.13 (3) → 0.3.14 (4)
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
$ ./gradlew bumpMinorVersion
> Task :app:bumpMinorVersion
Bumping minor version…
0.3.14 (4) → 0.4.0 (5)
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
$ ./gradlew bumpMajorVersion
> Task :app:bumpMajorVersion
Bumping major version…
0.4.0 (5) → 1.0.0 (6)
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

You could also use split with a increment-matrix, that could be changed depending on the amount of changes:
def version = '1.0.0'
def incstep = '0.0.1'.split(/\./).collect{it.toInteger()}
def indexedVersionList = version.split(/\./).toList().withIndex()
def updatedVersionList = indexedVersionList.collect{num, idx -> num.toInteger()+incstep[idx]}
def updatedVersion = updatedVersionList.join(".")

This is how I did it with Kotlin DSL (build.gradle.kts):
tasks.create("incrementVersion") {
group = "my tasks"
description = "Increments the version in this build file everywhere it is used."
fun generateVersion(): String {
val updateMode = properties["mode"] ?: "minor" // By default, update the minor
val (oldMajor, oldMinor, oldPatch) = version.split(".").map(String::toInt)
var (newMajor, newMinor, newPatch) = arrayOf(oldMajor, oldMinor, 0)
when (updateMode) {
"major" -> newMajor = (oldMajor + 1).also { newMinor = 0 }
"minor" -> newMinor = oldMinor + 1
else -> newPatch = oldPatch + 1
}
return "$newMajor.$newMinor.$newPatch"
}
doLast {
val newVersion = properties["overrideVersion"] as String? ?: generateVersion()
val oldContent = buildFile.readText()
val newContent = oldContent.replace("""= "$version"""", """= "$newVersion"""")
buildFile.writeText(newContent)
}
}
Usage:
./gradlew incrementVersion [-P[mode=major|minor|patch]|[overrideVersion=x]]
Examples:
./gradlew incrementVersion -Pmode=minor
./gradlew incrementVersion -PoverrideVersion=2.5.11
That is given that you have something like this in your build script:
version = "1.2.3"
... and the patch part of the version is just a number (not containing letters like alpha, rc, etc.).

Below solution will not create an issue evern last number exceed from 9-10 and so on
version='1.0.11.1001'
task incrementrevsion{
def v = version
println v
String minor=v.substring(v.lastIndexOf('.')+1) //get last digit
int m=minor.toInteger()+1 //increment
println m
String major=v.substring(0,v.lastIndexOf(".")); //get the beginning
println major
String s=buildFile.getText().replaceFirst("version='$version'","version='"+major+ "." +m+"'")
//println s
buildFile.setText(s) //replace the build file's text
}

def patch = version.substring(version.lastIndexOf('.') + 1)
def p = patch.toInteger() + 1
def major = version.substring(0, version.length() - p.toString().length())
def s = buildFile.getText().replaceFirst("version = '$version'", "version = '" + major + p + "'")
buildFile.setText(s)
The only difference with Alexiy's answer that line 3 contains m.toString().length() as if minor version > 10, i.e 1.0.12 and you will use that approach it will change it to 1.0.113. We need to calculate the length of minor version instead of chopping off only 1 symbol.
And one more thing, usually the last number is called patch, minor is a middle one :)

My Solution where the version will be set by a Parameter.
version = '4.0.0' // I have whitespaces between the equals-sign
task setVersion << {
group = 'Versioning'
description = "Sets the version to the new number specified with -PnewVersion=\'x.x.x\'"
println version
if(project.hasProperty('newVersion')) {
println 'Set Project to new Version '+newVersion
String s=buildFile.getText().replaceFirst("version = '$version'","version = '"+newVersion+"'")
buildFile.setText(s)
}
}
Call Gradle Task with:
gradle setVersion -PnewVersion='5.1.1'

As for me work this solution.
You need add this code into build.gradle file:
version='1.0.1'
tasks.register("incrementVersion") {
doLast {
def ver = version
println ver
String lastNumber = ver.substring(ver.lastIndexOf('.') + 1)
int increment = lastNumber.toInteger() + 1
println increment
String firstNumber = ver.substring(0, ver.lastIndexOf("."))
println firstNumber
String result = buildFile.getText().replaceFirst("version='$version'","version='" + firstNumber + "." + increment + "'")
buildFile.setText(result)
}
}

I know I am posting this quite late, but the answers mentioned above work well upto '1.0.99'. After which it starts misbehaving.
If any one is still interested, I found a different approach.
task increment {
def v = buildFile.getText().find(version)
def (major, minor, patch) = v.tokenize('.')
int newPatch = patch.toInteger() + 1
String newVersion = major + "." + minor + "." + newPatch
String updatedVersion = buildFile.getText().replaceFirst("version='"+v+"'","version='"+newVersion+"'")
buildFile.setText(updatedVersion)
}

This is an example of same think but with KTS(Kotlin Script).
val newVersion: String? by project
tasks.register("bumpVersion") {
this.doFirst {
println("Old version $version")
val newVersion = takeIf { newVersion.isNullOrBlank() }?.let {
val versionArray = version.toString().split(".")
"${versionArray[0]}.${versionArray[1]}.${versionArray.last().toInt().plus(1)}"
} ?: newVersion
buildFile.readText().apply {
println("Bump to $newVersion")
val content = this.replaceFirst("version = \"$version\"", "version = \"$newVersion\"")
buildFile.writeText(content)
}
}
}
Increment automatic the patch or it is possible to send new version as property.
./gradlew bumpVersion -PnewVersion=0.2.0

Kotlin dsl:
tasks.create("incrementPatch") {
group = "version"
description = "Автоматически поднять патч версию в файле VERSION"
doFirst {
incrementVersion("patch")
}
}
tasks.create("incrementMinor") {
group = "version"
description = "Автоматически поднять минор версию в файле VERSION"
doFirst {
incrementVersion("minor")
}
}
tasks.create("incrementMajor") {
group = "version"
description = "Автоматически поднять мажор версию в файле VERSION"
doFirst {
incrementVersion("major")
}
}
tasks.named("compileKotlin") {
dependsOn(":incrementPatch")
}
fun incrementVersion(updateMode: String){
val versions = file("version").readText().trim()
println("read version = $versions")
val (oldMajor, oldMinor, oldPatch) = versions.substringBefore("-").split(".").map(String::toInt)
var (newMajor, newMinor, newPatch) = arrayOf(oldMajor, oldMinor, 0)
when (updateMode) {
"major" -> newMajor = (oldMajor + 1).also { newMinor = 0 }
"minor" -> newMinor = oldMinor + 1
else -> newPatch = oldPatch + 1
}
val newVersion ="$newMajor.$newMinor.$newPatch-SNAPSHOT"
println("new version = $newVersion")
file("version").writeText(newVersion)
}

Related

Sort array by filename in groovy

I'm trying to sort a list of jars by their filenames:
def jars = ['app-5.0.0.jar', 'app-5.1.1.jar', 'app-5.2.0-9.jar', 'app-5.2.0-10.jar', 'app-5.2.0.jar', 'app-5.1.0.jar']
jars = jars.sort().reverse()
println jars
The result is:
[app-5.2.0.jar, app-5.2.0-9.jar, app-5.2.0-10.jar, app-5.1.1.jar, app-5.1.0.jar, app-5.0.0.jar]
However, I'm more interested in the natural (and probably more intuitive) sorting to receive this sorted list:
[app-5.2.0-10.jar, app-5.2.0-9.jar, app-5.2.0.jar, app-5.1.1.jar, app-5.1.0.jar, app-5.0.0.jar]
Is there a way to achieve this?
this is my current algorithm for sorting but it's too verbose in my opinion. However, it really does what I'm looking for. Each part of the version (major, minor, maintenance, build) is evaluated independently:
jars = jars.sort { a, b ->
File fileA = new File(a)
File fileB = new File(b)
def partsA = fileA.name.findAll(/\d+/)
def partsB = fileB.name.findAll(/\d+/)
if (partsA[0] == null) partsA[0] = "0"
if (partsB[0] == null) partsB[0] = "0"
if (partsA[0].toInteger() < partsB[0].toInteger()) {
println "${partsA[0]} < ${partsB[0]}"
return -1
} else if (partsA[0].toInteger() > partsB[0].toInteger()) {
println "${partsA[0]} > ${partsB[0]}"
return 1
} else {
if (partsA[1] == null) partsA[1] = "0"
if (partsB[1] == null) partsB[1] = "0"
if (partsA[1].toInteger() < partsB[1].toInteger()) {
println "${partsA[1]} < ${partsB[1]}"
return -1
} else if (partsA[1].toInteger() > partsB[1].toInteger()) {
println "${partsA[1]} > ${partsB[1]}"
return 1
} else {
if (partsA[2] == null) partsA[2] = "0"
if (partsB[2] == null) partsB[2] = "0"
if (partsA[2].toInteger() < partsB[2].toInteger()) {
println "${partsA[2]} < ${partsB[2]}"
return -1
} else if (partsA[2].toInteger() > partsB[2].toInteger()) {
println "${partsA[2]} > ${partsB[2]}"
return 1
} else {
if (partsA[3] == null) partsA[3] = "0"
if (partsB[3] == null) partsB[3] = "0"
if (partsA[3].toInteger() < partsB[3].toInteger()) {
println "${partsA[3]} < ${partsB[3]}"
return -1
} else if (partsA[3].toInteger() > partsB[3].toInteger()) {
println "${partsA[3]} > ${partsB[3]}"
return 1
} else {
println "${partsA[3]} = ${partsB[3]}"
return 0
}
}
}
}
}
Had to try this:
def jars = ['app-5.0.0.jar', 'app-5.1.1.jar', 'app-5.2.0-9.jar', 'app-5.2.0-10.jar', 'app-5.2.0.jar', 'app-5.1.0.jar', 'app-1.0.jar', 'app-0.10.jar']
jars = jars.sort{ -it.findAll( /\d+/ ).join().toInteger() }
println jars
Gets:
[app-5.2.0-10.jar, app-5.2.0-9.jar, app-5.2.0.jar, app-5.1.1.jar, app-5.1.0.jar, app-5.0.0.jar, app-1.0.jar, app-0.10.jar]
Or more thorough version that handles large patch versions:
def jars = ['app-5.0.0.jar', 'app-5.1.1.jar', 'app-5.2.0-9.jar', 'app-5.2.0-10.jar', 'app-5.2.0.jar', 'app-5.1.0.jar', 'app-5.1.1-172.jar']
jars.sort{ a, b ->
def aList = a.findAll(/\d+/)
def bList = b.findAll(/\d+/)
for ( int i = 0 ; i < aList.size() ; i++ ) {
def aVal = aList[i] ? aList[i].toInteger() : 0
def bVal = bList[i] ? bList[i].toInteger() : 0
if ( aVal <=> bVal ) { // only return if non-zero i.e. not equal
return aVal <=> bVal
}
}
bList.size() > aList.size() ? -1 : 0 // all facets match up to now, if b has additional parts it must be later version
}
println jars.reverse()
Gets:
[app-5.2.0-10.jar, app-5.2.0-9.jar, app-5.2.0.jar, app-5.1.1-172.jar, app-5.1.1.jar, app-5.1.0.jar, app-5.0.0.jar]
How about something like this:
def jars = ['app-5.0.0.jar', 'app-5.1.1.jar', 'app-5.2.0-9.jar', 'app-5.2.0-10.jar', 'app-5.2.0.jar', 'app-5.1.0.jar', 'app-5.1.1-172.jar']
// it is probably sufficient to just choose a "high enough" number
// (e.g. 10) instead of resolving max digits.
def maxDigits = jars*.findAll(/\d+/).flatten()*.size().max()
// sort the strings consisting of left-padded version numbers
// e.g. sorting string for 'app-5.1.1-172.jar' is ' 5 1 1172'
jars.sort{ it.findAll(/\d+/)*.padLeft(maxDigits).join() }
println 'max digits: ' + maxDigits
println jars.reverse()
Output:
max digits: 3
[app-5.2.0-10.jar, app-5.2.0-9.jar, app-5.2.0.jar, app-5.1.1-172.jar, app-5.1.1.jar, app-5.1.0.jar, app-5.0.0.jar]

Spock to provide temporary files for the Gradle task method within expect: and where: blocks

I would like to use Spock's Data Driven Testing.
My task has a method with a File argument to process. Result is an Array with processed lines. If there is nothing valid to process mentioned array will have 0 size. I used a JUnit rule for creating a temporary folder and I suspect it's the problem.
How can I solve this ?
class H2JSpec extends Specification {
#Rule
TemporaryFolder temporaryFolder
#Shared
private ArrayList<File> tempFiles = []
def "let's build the mappings for template"() {
setup:
Project project = ProjectBuilder.builder().build()
H2JTask task = project.task('h2j', type: H2JTask)
def inputs = ["""#dsfs
""",
"""#this file defines the mapping of h files declarations into java errorcodes: autoenum=off prefix=ec_ class=test.framework.base.MsgErrorCodes
"""]
tempFiles.add(temporaryFolder.newFile('1.txt'))
tempFiles.add(temporaryFolder.newFile('2.txt'))
tempFiles[0].withWriter { it << inputs[0] }
tempFiles[1].withWriter { it << inputs[1] }
expect:
task.prepareCommandList(a).size() == b
where:
a || b
tempFiles[0] || 0
tempFiles[1] || 1
}
}
And the result is the java.io.FileNotFoundException.
To be honest I'd eliminate use of Rule at all and rewrite the test in the following way:
#Grab(group='org.spockframework', module='spock-core', version='1.0-groovy-2.4')
import spock.lang.*
class H2JSpec extends Specification {
def "let's build the mappings for template"() {
setup:
def task = new A()
when:
def f = File.createTempFile('aaa', 'bbb')
f.write(a)
then:
task.prepareCommandList(f).size() == b
where:
a || b
"""#dsfs
""" || 3
"""#this file defines the mapping of h files declarations into java errorcodes: autoenum=off prefix=ec_ class=test.framework.base.MsgErrorCodes
""" || 2
}
}
class A {
List<String> prepareCommandList(File f) {
f.readLines()
}
}

Gradle CustomTask not considering InputFiles for dependencies

I'm trying to write a custom Gradle task that takes a specific set of input files, which may be in different directories, an output file and call a javaexec with the inputs.
I currently have it where it will skip the #TaskAction if it sees that the #OutputFile exists but it pays no consideration to the InputFiles list and if they are newer than the output file.
Here are the relevant parts of the buildfile:
class JavaCPPTask extends DefaultTask {
def className = []
String outputDir = ""
String localClasspath = "bin/classes"
String runIfExists = ""
String javacpp = "libs/javacpp.jar"
String outputName = ""
//#InputDirectory
//File inputFile = new File("bin/classes/com/package/ve")
#InputFiles
List<File> getInputFiles() {
ArrayList<File> inputFiles = new ArrayList<File>()
for ( int i = 0; i < className.size(); i++ ) {
def inputFileStr = localClasspath + '/' + className.get(i).replace('.','/') + '.class'
println("InputFileStr: " + inputFileStr )
File inputFile = new File(inputFileStr)
println("Input file exists? " + inputFile.exists() )
inputFiles.add(inputFile)
}
return inputFiles
}
#OutputFile
File getTargetFile() {
if ( outputName.length() == 0 ) {
def nameWithPackage = className.get(0)
def name = nameWithPackage.substring(nameWithPackage.lastIndexOf(".")+1)
outputName = 'jni' + name;
}
File outputFile = new File( outputDir + outputName + '.cpp');
println("Output Name is: " + outputFile )
println("Output file exists? " + outputFile.exists() )
return outputFile
}
JavaCPPTask() {
}
#TaskAction
def javacpp() {
def haveFile = new File(runIfExists)
if ( runIfExists.length() > 0 && ! haveFile.exists() ) {
println("Skipping class " + className + " makefile does not exist: " + runIfExists );
}
else {
def useOutputName = []
if ( outputName.length() > 0 ) {
useOutputName = [ '-o', outputName ]
}
def optionArray = [ '-nocompile', '-classpath', localClasspath, '-d', outputDir ]
def allOptions = [javacpp] + useOutputName + optionArray + className.findAll()
project.javaexec { main="-jar"; args allOptions }
}
}
}
def codecOpus = ['com.package.ve.CodecOpus']
task javaCPP_CodecOpus(type:JavaCPPTask) {
className = codecOpus
outputDir = 'jni/VECodecOpus/'
runIfExists = outputDir + 'Android.mk'
}
And the output, which may or may not help. The output file has already been created by a previous run, but the .class file has been updated so I'd expect that the target would run again.
> c:\java\gradle-2.2.1\bin\gradle -b build
JavaCPP.gradle javaCPP_CodecOpus
Incremental java compilation is an incubating feature.
InputFileStr: bin/classes/com/package/ve/CodecOpus.class
Input file exists? true
:javaCPP_CodecOpus
InputFileStr: bin/classes/com/package/ve/CodecOpus.class
Input file exists? true
Output Name is: jni\VECodecOpus\jniCodecOpus.cpp
Output file exists? true
Output Name is: jni\VECodecOpus\jniCodecOpus.cpp
Output file exists? true
Output Name is: jni\VECodecOpus\jniCodecOpus.cpp
Output file exists? true
Output Name is: jni\VECodecOpus\jniCodecOpus.cpp
Output file exists? true
InputFileStr: bin/classes/com/package/ve/CodecOpus.class
Input file exists? true
:javaCPP_CodecOpus UP-TO-DATE
BUILD SUCCESSFUL
What am I doing wrong?
Turns out after debugging through the Gradle source, the script is working. I had been forcing an update and a recompile on the .java file which created a new .class file, but Gradle was smart enough to know that the contents of the class file had not changed. Once I changed the source in the .java file and recompiled, Gradle correctly identified the source as changed and rebuilt the target.
Guess I've been using Make for far too long.
There's still and issue with the script since i was forced to run a clean build before I started down this rabbit hole, but the out of date input file wasn't it.

Simple ranking algorithm in Groovy

I have a short groovy algorithm for assigning rankings to food based on their rating. This can be run in the groovy console. The code works perfectly, but I'm wondering if there is a more Groovy or functional way of writing the code. Thinking it would be nice to get rid of the previousItem and rank local variables if possible.
def food = [
[name:'Chocolate Brownie',rating:5.5, rank:null],
[name:'Fudge', rating:2.1, rank:null],
[name:'Pizza',rating:3.4, rank:null],
[name:'Icecream', rating:2.1, rank:null],
[name:'Cabbage', rating:1.4, rank:null]]
food.sort { -it.rating }
def previousItem = food[0]
def rank = 1
previousItem.rank = rank
food.each { item ->
if (item.rating == previousItem.rating) {
item.rank = previousItem.rank
} else {
item.rank = rank
}
previousItem = item
rank++
}
assert food[0].rank == 1
assert food[1].rank == 2
assert food[2].rank == 3
assert food[3].rank == 3 // Note same rating = same rank
assert food[4].rank == 5 // Note, 4 skipped as we have two at rank 3
Suggestions?
This is my solution:
def rank = 1
def groupedByRating = food.groupBy { -it.rating }
groupedByRating.sort().each { rating, items ->
items.each { it.rank = rank }
rank += items.size()
}
I didn't try it, but maybe this could work.
food.eachWithIndex { item, i ->
if( i>0 && food[i-1].rating == item.rating )
item.rank = food[i-1].rank
else
item.rank = i + 1
}
Here's another alternative that doesn't use "local defs" with the groovy inject method:
food.sort { -it.rating }.inject([index: 0]) { map, current ->
current.rank = (current.rating == map.lastRating ? map.lastRank : map.index + 1)
[ lastRating: current.rating, lastRank: current.rank, index: map.index + 1 ]
}

how to read immutable data structures from file in scala

I have a data structure made of Jobs each containing a set of Tasks. Both Job and Task data are defined in files like these:
jobs.txt:
JA
JB
JC
tasks.txt:
JB T2
JA T1
JC T1
JA T3
JA T2
JB T1
The process of creating objects is the following:
- read each job, create it and store it by id
- read task, retrieve job by id, create task, store task in the job
Once the files are read this data structure is never modified. So I would like that tasks within jobs would be stored in an immutable set. But I don't know how to do it in an efficient way. (Note: the immutable map storing jobs may be left immutable)
Here is a simplified version of the code:
class Task(val id: String)
class Job(val id: String) {
val tasks = collection.mutable.Set[Task]() // This sholud be immutable
}
val jobs = collection.mutable.Map[String, Job]() // This is ok to be mutable
// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) {
val job = new Job(line.trim)
jobs += (job.id -> job)
}
// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
val tokens = line.split("\t")
val job = jobs(tokens(0).trim)
val task = new Task(job.id + "." + tokens(1).trim)
job.tasks += task
}
Thanks in advance for every suggestion!
The most efficient way to do this would be to read everything into mutable structures and then convert to immutable ones at the end, but this might require a lot of redundant coding for classes with a lot of fields. So instead, consider using the same pattern that the underlying collection uses: a job with a new task is a new job.
Here's an example that doesn't even bother reading the jobs list--it infers it from the task list. (This is an example that works under 2.7.x; recent versions of 2.8 use "Source.fromPath" instead of "Source.fromFile".)
object Example {
class Task(val id: String) {
override def toString = id
}
class Job(val id: String, val tasks: Set[Task]) {
def this(id0: String, old: Option[Job], taskID: String) = {
this(id0 , old.getOrElse(EmptyJob).tasks + new Task(taskID))
}
override def toString = id+" does "+tasks.toString
}
object EmptyJob extends Job("",Set.empty[Task]) { }
def read(fname: String):Map[String,Job] = {
val map = new scala.collection.mutable.HashMap[String,Job]()
scala.io.Source.fromFile(fname).getLines.foreach(line => {
line.split("\t") match {
case Array(j,t) => {
val jobID = j.trim
val taskID = t.trim
map += (jobID -> new Job(jobID,map.get(jobID),taskID))
}
case _ => /* Handle error? */
}
})
new scala.collection.immutable.HashMap() ++ map
}
}
scala> Example.read("tasks.txt")
res0: Map[String,Example.Job] = Map(JA -> JA does Set(T1, T3, T2), JB -> JB does Set(T2, T1), JC -> JC does Set(T1))
An alternate approach would read the job list (creating jobs as new Job(jobID,Set.empty[Task])), and then handle the error condition of when the task list contained an entry that wasn't in the job list. (You would still need to update the job list map every time you read in a new task.)
I did a feel changes for it to run on Scala 2.8 (mostly, fromPath instead of fromFile, and () after getLines). It may be using a few Scala 2.8 features, most notably groupBy. Probably toSet as well, but that one is easy to adapt on 2.7.
I don't have the files to test it, but I changed this stuff from val to def, and the type signatures, at least, match.
class Task(val id: String)
class Job(val id: String, val tasks: Set[Task])
// read tasks
val tasks = (
for {
line <- io.Source.fromPath("tasks.txt").getLines().toStream
tokens = line.split("\t")
jobId = tokens(0).trim
task = new Task(jobId + "." + tokens(1).trim)
} yield jobId -> task
).groupBy(_._1).map { case (key, value) => key -> value.map(_._2).toSet }
// read jobs
val jobs = Map() ++ (
for {
line <- io.Source.fromPath("jobs.txt").getLines()
job = new Job(line.trim, tasks(line.trim))
} yield job.id -> job
)
You could always delay the object creation until you have all the data read in from the file, like:
case class Task(id: String)
case class Job(id: String, tasks: Set[Task])
import scala.collection.mutable.{Map,ListBuffer}
val jobIds = Map[String, ListBuffer[String]]()
// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) {
val job = line.trim
jobIds += (job.id -> new ListBuffer[String]())
}
// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
val tokens = line.split("\t")
val job = tokens(0).trim
val task = job.id + "." + tokens(1).trim
jobIds(job) += task
}
// create objects
val jobs = jobIds.map { j =>
Job(j._1, Set() ++ j._2.map { Task(_) })
}
To deal with more fields, you could (with some effort) make a mutable version of your immutable classes, used for building. Then, convert as needed:
case class Task(id: String)
case class Job(val id: String, val tasks: Set[Task])
object Job {
class MutableJob {
var id: String = ""
var tasks = collection.mutable.Set[Task]()
def immutable = Job(id, Set() ++ tasks)
}
def mutable(id: String) = {
val ret = new MutableJob
ret.id = id
ret
}
}
val mutableJobs = collection.mutable.Map[String, Job.MutableJob]()
// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) {
val job = Job.mutable(line.trim)
jobs += (job.id -> job)
}
// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
val tokens = line.split("\t")
val job = jobs(tokens(0).trim)
val task = Task(job.id + "." + tokens(1).trim)
job.tasks += task
}
val jobs = for ((k,v) <- mutableJobs) yield (k, v.immutable)
One option here is to have some mutable but transient configurer class along the lines of the MutableMap above but then pass this through in some immutable form to your actual class:
val jobs: immutable.Map[String, Job] = {
val mJobs = readMutableJobs
immutable.Map(mJobs.toSeq: _*)
}
Then of course you can implement readMutableJobs along the lines you have already coded

Resources