client: Add ConcatenatedList, LeftOuterJoinedMap, tests and utilities, fix some bugs

This commit is contained in:
Andras Slemmer 2016-09-27 17:24:08 +01:00
parent 99e758e021
commit 9a212a8714
12 changed files with 796 additions and 17 deletions

View File

@ -140,8 +140,9 @@ class AggregatedList<A, E : Any, K : Any>(
return insertIndex
} else {
val elements = aggregationList[aggregationIndex].elements
val elementHashCode = addedItem.hashCode()
val elementIndex = elements.binarySearch(
comparison = { element -> compareValues(keyHashCode, element.hashCode()) }
comparison = { element -> compareValues(elementHashCode, element.hashCode()) }
)
val addIndex = if (elementIndex < 0) {
-elementIndex - 1

View File

@ -0,0 +1,243 @@
package com.r3corda.client.fxutils
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import java.util.*
import kotlin.comparisons.compareValues
class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : TransformationList<A, ObservableList<A>>(sourceList) {
class WrappedObservableList<A>(
val observableList: ObservableList<A>
)
private val indexMap = HashMap<WrappedObservableList<out A>, Pair<Int, ListChangeListener<A>>>()
// Maps each list index to the offset of the next nested element
// Example: { {"a", "b"}, {"c"} } -> { 2, 3 }
private val nestedIndexOffsets = ArrayList<Int>(sourceList.size)
init {
var offset = 0
sourceList.forEachIndexed { index, observableList ->
val wrapped = WrappedObservableList(observableList)
indexMap[wrapped] = Pair(index, createListener(wrapped))
offset += observableList.size
nestedIndexOffsets.add(offset)
}
}
private fun createListener(wrapped: WrappedObservableList<A>): ListChangeListener<A> {
val listener = ListChangeListener<A> { change ->
beginChange()
while (change.next()) {
if (change.wasPermutated()) {
val listIndex = indexMap[wrapped]!!.first
val permutation = IntArray(change.to)
if (listIndex >= firstInvalidatedPosition) {
recalculateOffsets()
}
val startingOffset = startingOffsetOf(listIndex)
val firstTouched = startingOffset + change.from
for (i in 0..firstTouched - 1) {
permutation[i] = i
}
for (i in startingOffset + change.from..startingOffset + change.to - 1) {
permutation[startingOffset + i] = change.getPermutation(i)
}
nextPermutation(firstTouched, startingOffset + change.to, permutation)
} else if (change.wasUpdated()) {
val listIndex = indexMap[wrapped]!!.first
val startingOffset = startingOffsetOf(listIndex)
for (i in change.from..change.to - 1) {
nextUpdate(startingOffset + i)
}
} else {
if (change.wasRemoved()) {
val listIndex = indexMap[wrapped]!!.first
invalidateOffsets(listIndex)
val startingOffset = startingOffsetOf(listIndex)
nextRemove(startingOffset + change.from, change.removed)
}
if (change.wasAdded()) {
val listIndex = indexMap[wrapped]!!.first
invalidateOffsets(listIndex)
val startingOffset = startingOffsetOf(listIndex)
nextAdd(startingOffset + change.from, startingOffset + change.to)
}
}
recalculateOffsets()
}
endChange()
}
wrapped.observableList.addListener(listener)
return listener
}
// Tracks the first position where the *nested* offset is invalid
private var firstInvalidatedPosition = sourceList.size
override fun sourceChanged(change: ListChangeListener.Change<out ObservableList<A>>) {
beginChange()
while (change.next()) {
if (change.wasPermutated()) {
// Update indexMap
val iterator = indexMap.iterator()
for (entry in iterator) {
val (wrapped, pair) = entry
val (index, listener) = pair
if (index >= change.from && index < change.to) {
entry.setValue(Pair(change.getPermutation(index), listener))
}
}
// Calculate the permuted sublist of nestedIndexOffsets
val newSubNestedIndexOffsets = IntArray(change.to - change.from)
val firstTouched = if (change.from == 0) 0 else nestedIndexOffsets[change.from - 1]
var currentOffset = firstTouched
for (i in 0 .. change.to - change.from - 1) {
currentOffset += source[change.from + i].size
newSubNestedIndexOffsets[i] = currentOffset
}
val concatenatedPermutation = IntArray(newSubNestedIndexOffsets.last())
// Set the non-permuted part
var offset = 0
for (i in 0 .. change.from - 1) {
val nestedList = source[i]
for (j in offset .. offset + nestedList.size - 1) {
concatenatedPermutation[j] = j
}
offset += nestedList.size
}
// Now the permuted part
for (i in 0 .. newSubNestedIndexOffsets.size - 1) {
val startingOffset = startingOffsetOf(change.from + i)
val permutedListIndex = change.getPermutation(change.from + i)
val permutedOffset = (if (permutedListIndex == 0) 0 else newSubNestedIndexOffsets[permutedListIndex - 1])
for (j in 0 .. source[permutedListIndex].size - 1) {
concatenatedPermutation[startingOffset + j] = permutedOffset + j
}
}
// Record permuted offsets
for (i in 0 .. newSubNestedIndexOffsets.size - 1) {
nestedIndexOffsets[change.from + i] = newSubNestedIndexOffsets[i]
}
nextPermutation(firstTouched, newSubNestedIndexOffsets.last(), concatenatedPermutation)
} else if (change.wasUpdated()) {
// This would be translated to remove + add, but that requires a backing list for removed elements
throw UnsupportedOperationException("Updates not supported")
} else {
if (change.wasRemoved()) {
// Update indexMap
val iterator = indexMap.iterator()
for (entry in iterator) {
val (wrapped, pair) = entry
val (index, listener) = pair
val removeEnd = change.from + change.removedSize
if (index >= change.from) {
if (index < removeEnd) {
wrapped.observableList.removeListener(listener)
iterator.remove()
} else {
entry.setValue(Pair(index - change.removedSize, listener))
}
}
}
// Propagate changes
invalidateOffsets(change.from)
val removeStart = startingOffsetOf(change.from)
val removed = change.removed.flatMap { it }
nextRemove(removeStart, removed)
}
if (change.wasAdded()) {
// Update indexMap
if (change.from != indexMap.size) {
val iterator = indexMap.iterator()
for (entry in iterator) {
val (index, listener) = entry.value
if (index >= change.from) {
// Shift indices
entry.setValue(Pair(index + change.addedSize, listener))
}
}
}
change.addedSubList.forEachIndexed { sublistIndex, observableList ->
val wrapped = WrappedObservableList(observableList)
indexMap[wrapped] = Pair(change.from + sublistIndex, createListener(wrapped))
}
invalidateOffsets(change.from)
recalculateOffsets()
nextAdd(startingOffsetOf(change.from), nestedIndexOffsets[change.to - 1])
for (i in change.from .. change.to - 1) {
source[i].addListener { change: ListChangeListener.Change<out A> ->
}
}
}
}
recalculateOffsets()
}
endChange()
}
private fun invalidateOffsets(index: Int) {
firstInvalidatedPosition = Math.min(firstInvalidatedPosition, index)
}
private fun startingOffsetOf(listIndex: Int): Int {
if (listIndex == 0) {
return 0
} else {
return nestedIndexOffsets[listIndex - 1]
}
}
private fun recalculateOffsets() {
if (firstInvalidatedPosition < source.size) {
val firstInvalid = firstInvalidatedPosition
var offset = if (firstInvalid == 0) 0 else nestedIndexOffsets[firstInvalid - 1]
for (i in firstInvalid .. source.size - 1) {
offset += source[i].size
if (i < nestedIndexOffsets.size) {
nestedIndexOffsets[i] = offset
} else {
nestedIndexOffsets.add(offset)
}
}
while (nestedIndexOffsets.size > source.size) {
nestedIndexOffsets.removeAt(source.size)
}
firstInvalidatedPosition = nestedIndexOffsets.size
}
}
override val size: Int get() {
recalculateOffsets()
if (nestedIndexOffsets.size > 0) {
return nestedIndexOffsets.last()
} else {
return 0
}
}
override fun getSourceIndex(index: Int): Int {
throw UnsupportedOperationException("Source index not supported in concatenation")
}
override fun get(index: Int): A {
recalculateOffsets()
val listIndex = nestedIndexOffsets.binarySearch(
comparison = { offset -> compareValues(offset, index) }
)
if (listIndex >= 0) {
var nonEmptyListIndex = listIndex + 1
while (source[nonEmptyListIndex].isEmpty()) {
nonEmptyListIndex++
}
return source[nonEmptyListIndex][0]
} else {
// The element is in the range of this list
val rangeListIndex = -listIndex - 1
val subListOffset = index - startingOffsetOf(rangeListIndex)
return source[rangeListIndex][subListOffset]
}
}
}

View File

@ -0,0 +1,50 @@
package com.r3corda.client.fxutils
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.*
/**
* [LeftOuterJoinedMap] implements a special case of a left outer join where we're matching on primary keys of both
* tables.
*/
class LeftOuterJoinedMap<K : Any, A, B, C>(
val leftTable: ObservableMap<K, out A>,
val rightTable: ObservableMap<K, out B>,
assemble: (K, A, ObservableValue<B?>) -> C
) : ReadOnlyBackedObservableMapBase<K, C, SimpleObjectProperty<B?>>() {
init {
leftTable.forEach { entry ->
val rightValueProperty = SimpleObjectProperty(rightTable.get(entry.key))
backingMap.set(entry.key, Pair(assemble(entry.key, entry.value, rightValueProperty), rightValueProperty))
}
leftTable.addListener { change: MapChangeListener.Change<out K, out A> ->
var addedValue: C? = null
var removedValue: C? = null
if (change.wasRemoved()) {
removedValue = backingMap.remove(change.key)?.first
}
if (change.wasAdded()) {
val rightValue = rightTable.get(change.key)
val rightValueProperty = SimpleObjectProperty(rightValue)
val newValue = assemble(change.key, change.valueAdded, rightValueProperty)
backingMap.set(change.key, Pair(newValue, rightValueProperty))
addedValue = newValue
}
fireChange(createMapChange(change.key, removedValue, addedValue))
}
rightTable.addListener { change: MapChangeListener.Change<out K, out B> ->
if (change.wasRemoved() && !change.wasAdded()) {
backingMap.get(change.key)?.second?.set(null)
}
if (change.wasAdded()) {
backingMap.get(change.key)?.second?.set(change.valueAdded)
}
}
}
}

View File

@ -10,16 +10,16 @@ import kotlin.comparisons.compareValues
* [MapValuesList] takes an [ObservableMap] and returns its values as an [ObservableList].
* The order of returned elements is deterministic but unspecified.
*/
class MapValuesList<K, A> private constructor(
sourceMap: ObservableMap<K, A>,
class MapValuesList<K, A, C> private constructor(
val sourceMap: ObservableMap<K, A>,
private val backingList: ObservableList<Map.Entry<K, A>>, // sorted by K.hashCode()
private val exposedList: ObservableList<A>
) : ObservableList<A> by exposedList {
private val exposedList: ObservableList<C>
) : ObservableList<C> by exposedList {
companion object {
fun <K, A> create(sourceMap: ObservableMap<K, A>): MapValuesList<K, A> {
fun <K, A, C> create(sourceMap: ObservableMap<K, A>, assemble: (Map.Entry<K, A>) -> C): MapValuesList<K, A, C> {
val backingList = FXCollections.observableArrayList<Map.Entry<K, A>>(sourceMap.entries.sortedBy { it.key!!.hashCode() })
return MapValuesList(sourceMap, backingList, backingList.map { it.value })
return MapValuesList(sourceMap, backingList, backingList.map { assemble(it) })
}
}

View File

@ -0,0 +1,66 @@
package com.r3corda.client.fxutils
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import java.util.*
/**
* This is a variant of [EasyBind.map] where the mapped list is backed, therefore the mapping function will only be run
* when an element is inserted or updated.
*/
class MappedList<A, B>(list: ObservableList<A>, val function: (A) -> B) : TransformationList<B, A>(list) {
private val backingList = ArrayList<B>(list.size)
init {
list.forEach {
backingList.add(function(it))
}
}
override fun sourceChanged(change: ListChangeListener.Change<out A>) {
beginChange()
while (change.next()) {
if (change.wasPermutated()) {
val from = change.from
val to = change.to
val permutation = IntArray(to, { change.getPermutation(it) })
val permutedSubList = ArrayList<B?>(to - from)
permutedSubList.addAll(Collections.nCopies(to - from, null))
for (i in 0 .. (to - from - 1)) {
permutedSubList[permutation[from + i]] = backingList[from + i]
}
permutedSubList.forEachIndexed { i, element ->
backingList[from + i] = element!!
}
nextPermutation(from, to, permutation)
} else if (change.wasUpdated()) {
backingList[change.from] = function(source[change.from])
nextUpdate(change.from)
} else {
if (change.wasRemoved()) {
val removePosition = change.from
val removed = ArrayList<B>(change.removedSize)
for (i in 0 .. change.removedSize - 1) {
removed.add(backingList.removeAt(removePosition))
}
nextRemove(change.from, removed)
}
if (change.wasAdded()) {
val addStart = change.from
val addEnd = change.to
for (i in addStart .. addEnd - 1) {
backingList.add(i, function(change.list[i]))
}
nextAdd(addStart, addEnd)
}
}
}
endChange()
}
override fun get(index: Int) = backingList[index]
override val size: Int get() = backingList.size
override fun getSourceIndex(index: Int) = index
}

View File

@ -5,6 +5,7 @@ import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import rx.Observable
/**
@ -45,3 +46,39 @@ fun <A> Observable<A>.foldToObservableList(): ObservableList<A> {
list.add(newElement)
}
}
fun <A, B, K, C> Observable<A>.foldToObservableMap(
initialAccumulator: C, folderFun: (A, C, ObservableMap<K, B>) -> C
): ObservableMap<K, out B> {
val result = FXCollections.observableHashMap<K, B>()
/**
* This capture is fine, as [Platform.runLater] runs closures in order
*/
var currentAccumulator = initialAccumulator
subscribe {
Platform.runLater {
currentAccumulator = folderFun(it, currentAccumulator, result)
}
}
return result
}
/**
* This variant simply associates each event with its key.
* @param toKey Function retrieving the key to associate with.
* @param merge The function to be called if there is an existing element at the key.
*/
fun <A, K> Observable<A>.foldToObservableMap(
toKey: (A) -> K,
merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }
): ObservableMap<K, out A> {
return foldToObservableMap(Unit) { newElement, _unit, map ->
val key = toKey(newElement)
val oldValue = map.get(key)
if (oldValue != null) {
map.set(key, merge(key, oldValue, newElement))
} else {
map.set(key, newElement)
}
}
}

View File

@ -10,7 +10,9 @@ import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import javafx.collections.transformation.FilteredList
import org.fxmisc.easybind.EasyBind
import org.slf4j.LoggerFactory
import java.util.function.Predicate
import kotlin.concurrent.thread
/**
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
@ -27,7 +29,8 @@ fun <A, B> ObservableValue<out A>.map(function: (A) -> B): ObservableValue<B> =
* val dogs: ObservableList<Dog> = (..)
* val dogOwners: ObservableList<Person> = dogs.map { it.owner }
*/
fun <A, B> ObservableList<out A>.map(function: (A) -> B): ObservableList<B> = EasyBind.map(this, function)
fun <A, B> ObservableList<out A>.map(function: (A) -> B): ObservableList<B> = MappedList(this, function)
fun <A, B> ObservableList<out A>.mapNonBacked(function: (A) -> B): ObservableList<B> = EasyBind.map(this, function)
/**
* val aliceHeight: ObservableValue<Long> = (..)
@ -90,12 +93,22 @@ fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>)
}
}
/**
* data class Dog(val owner: Person?)
* val dogs: ObservableList<Dog> = (..)
* val owners: ObservableList<Person> = dogs.map(Dog::owner).filterNotNull()
*/
fun <A> ObservableList<out A?>.filterNotNull(): ObservableList<A> {
@Suppress("UNCHECKED_CAST")
return filtered { it != null } as ObservableList<A>
}
/**
* val people: ObservableList<Person> = (..)
* val concatenatedNames = people.fold("", { names, person -> names + person.name })
* val concatenatedNames = people.foldObservable("", { names, person -> names + person.name })
* val concatenatedNames2 = people.map(Person::name).fold("", String::plus)
*/
fun <A, B> ObservableList<out A>.fold(initial: B, folderFunction: (B, A) -> B): ObservableValue<B> {
fun <A, B> ObservableList<out A>.foldObservable(initial: B, folderFunction: (B, A) -> B): ObservableValue<B> {
return Bindings.createObjectBinding({
var current = initial
forEach {
@ -171,3 +184,73 @@ fun <K, V> ObservableMap<K, V>.getObservableValue(key: K): ObservableValue<V?> {
return property
}
/**
* val nameToPerson: ObservableMap<String, Person> = (..)
* val people: ObservableList<Person> = nameToPerson.getObservableValues()
*/
fun <K, V> ObservableMap<K, V>.getObservableValues(): ObservableList<V> {
return MapValuesList.create(this) { it.value }
}
/**
* val nameToPerson: ObservableMap<String, Person> = (..)
* val people: ObservableList<Person> = nameToPerson.getObservableValues()
*/
fun <K, V> ObservableMap<K, V>.getObservableEntries(): ObservableList<Map.Entry<K, V>> {
return MapValuesList.create(this) { it }
}
/**
* val groups: ObservableList<ObservableList<Person>> = (..)
* val allPeople: ObservableList<Person> = groups.concatenate()
*/
fun <A> ObservableList<ObservableList<A>>.concatenate(): ObservableList<A> {
return ConcatenatedList(this)
}
/**
* val people: ObservableList<Person> = (..)
* val managerEmployeeMapping: ObservableList<Pair<Person, ObservableList<Person>>> =
* people.leftOuterJoin(people, Person::name, Person::managerName) { manager, employees -> Pair(manager, employees) }
*/
fun <A : Any, B : Any, C, K : Any> ObservableList<A>.leftOuterJoin(
rightTable: ObservableList<B>,
leftToJoinKey: (A) -> K,
rightToJoinKey: (B) -> K,
assemble: (A, ObservableList<B>) -> C
): ObservableList<C> {
val joinedMap = leftOuterJoin(rightTable, leftToJoinKey, rightToJoinKey)
return joinedMap.getObservableValues().map { pair ->
pair.first.map { assemble(it, pair.second) }
}.concatenate()
}
fun <A : Any, B : Any, K : Any> ObservableList<A>.leftOuterJoin(
rightTable: ObservableList<B>,
leftToJoinKey: (A) -> K,
rightToJoinKey: (B) -> K
): ObservableMap<K, Pair<ObservableList<A>, ObservableList<B>>> {
val leftTableMap = associateByAggregation(leftToJoinKey)
val rightTableMap = rightTable.associateByAggregation(rightToJoinKey)
val joinedMap: ObservableMap<K, Pair<ObservableList<A>, ObservableList<B>>> =
LeftOuterJoinedMap(leftTableMap, rightTableMap) { _key, left, rightValue ->
Pair(left, ChosenList(rightValue.map { it ?: FXCollections.emptyObservableList() }))
}
return joinedMap
}
fun <A> ObservableList<A>.getValueAt(index: Int): ObservableValue<A?> {
return Bindings.valueAt(this, index)
}
fun <A> ObservableList<A>.first(): ObservableValue<A?> {
return getValueAt(0)
}
fun <A> ObservableList<A>.last(): ObservableValue<A?> {
return Bindings.createObjectBinding({
if (size > 0) {
this[this.size - 1]
} else {
null
}
}, arrayOf(this))
}

View File

@ -6,7 +6,8 @@ import javafx.collections.transformation.TransformationList
import java.util.*
/**
* This list type just replays changes propagated from the underlying source list. Used for testing changes.
* This list type just replays changes propagated from the underlying source list. Used for testing changes and backing a
* non-backed observable
*/
class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(sourceList) {
@ -22,12 +23,13 @@ class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(
val from = c.from
val to = c.to
val permutation = IntArray(to, { c.getPermutation(it) })
val permutedSubList = ArrayList<A>(to - from)
val permutedSubList = ArrayList<A?>(to - from)
permutedSubList.addAll(Collections.nCopies(to - from, null))
for (i in 0 .. (to - from - 1)) {
permutedSubList.add(replayedList[permutation[from + i]])
permutedSubList[permutation[from + i]] = replayedList[from + i]
}
permutedSubList.forEachIndexed { i, element ->
replayedList[from + i] = element
replayedList[from + i] = element!!
}
nextPermutation(from, to, permutation)
} else if (c.wasUpdated()) {
@ -37,7 +39,6 @@ class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(
}
} else {
if (c.wasRemoved()) {
// TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position
val removePosition = c.from
for (i in 0 .. c.removedSize - 1) {
replayedList.removeAt(removePosition)

View File

@ -0,0 +1,117 @@
package com.r3corda.client.fxutils
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import org.junit.Before
import org.junit.Test
import java.util.*
class ConcatenatedListTest {
var sourceList = FXCollections.observableArrayList<ObservableList<String>>(FXCollections.observableArrayList("hello"))
var concatenatedList = ConcatenatedList(sourceList)
var replayedList = ReplayedList(concatenatedList)
@Before
fun setup() {
sourceList = FXCollections.observableArrayList<ObservableList<String>>(FXCollections.observableArrayList("hello"))
concatenatedList = ConcatenatedList(sourceList)
replayedList = ReplayedList(concatenatedList)
}
@Test
fun addWorks() {
require(replayedList.size == 1)
require(replayedList[0] == "hello")
sourceList.add(FXCollections.observableArrayList("a", "b"))
require(replayedList.size == 3)
require(replayedList[0] == "hello")
require(replayedList[1] == "a")
require(replayedList[2] == "b")
sourceList.add(1, FXCollections.observableArrayList("c"))
require(replayedList.size == 4)
require(replayedList[0] == "hello")
require(replayedList[1] == "c")
require(replayedList[2] == "a")
require(replayedList[3] == "b")
sourceList[0].addAll("d", "e")
require(replayedList.size == 6)
require(replayedList[0] == "hello")
require(replayedList[1] == "d")
require(replayedList[2] == "e")
require(replayedList[3] == "c")
require(replayedList[4] == "a")
require(replayedList[5] == "b")
sourceList[1].add(0, "f")
require(replayedList.size == 7)
require(replayedList[0] == "hello")
require(replayedList[1] == "d")
require(replayedList[2] == "e")
require(replayedList[3] == "f")
require(replayedList[4] == "c")
require(replayedList[5] == "a")
require(replayedList[6] == "b")
}
@Test
fun removeWorks() {
sourceList.add(FXCollections.observableArrayList("a", "b"))
sourceList.add(1, FXCollections.observableArrayList("c"))
sourceList[0].addAll("d", "e")
sourceList[1].add(0, "f")
sourceList.removeAt(1)
require(replayedList.size == 5)
require(replayedList[0] == "hello")
require(replayedList[1] == "d")
require(replayedList[2] == "e")
require(replayedList[3] == "a")
require(replayedList[4] == "b")
sourceList[0].clear()
require(replayedList.size == 2)
require(replayedList[0] == "a")
require(replayedList[1] == "b")
}
@Test
fun permutationWorks() {
sourceList.addAll(FXCollections.observableArrayList("a", "b"), FXCollections.observableArrayList("c"))
require(replayedList.size == 4)
require(replayedList[0] == "hello")
require(replayedList[1] == "a")
require(replayedList[2] == "b")
require(replayedList[3] == "c")
sourceList.sortWith(object : Comparator<ObservableList<String>> {
override fun compare(p0: ObservableList<String>, p1: ObservableList<String>): Int {
return p0.size - p1.size
}
})
require(replayedList.size == 4)
require(replayedList[0] == "hello")
require(replayedList[1] == "c")
require(replayedList[2] == "a")
require(replayedList[3] == "b")
sourceList.add(0, FXCollections.observableArrayList("d", "e", "f"))
sourceList.sortWith(object : Comparator<ObservableList<String>> {
override fun compare(p0: ObservableList<String>, p1: ObservableList<String>): Int {
return p0.size - p1.size
}
})
require(replayedList.size == 7)
require(replayedList[0] == "hello")
require(replayedList[1] == "c")
require(replayedList[2] == "a")
require(replayedList[3] == "b")
require(replayedList[4] == "d")
require(replayedList[5] == "e")
require(replayedList[6] == "f")
}
}

View File

@ -0,0 +1,115 @@
package com.r3corda.client.fxutils
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import org.junit.Before
import org.junit.Test
import java.util.*
class LeftOuterJoinedMapTest {
data class Person(val name: String, val age: Int)
data class Dog(val name: String, val owner: String)
var people = FXCollections.observableArrayList<Person>(Person("Alice", 12))
var dogs = FXCollections.observableArrayList<Dog>(Dog("Scruffy", owner = "Bob"))
var joinedList = people.leftOuterJoin(dogs, Person::name, Dog::owner)
// We replay the nested observable as well
var replayedList = ReplayedList(joinedList.map { Pair(it.first, ReplayedList(it.second)) })
@Before
fun setup() {
people = FXCollections.observableArrayList<Person>(Person("Alice", 12))
dogs = FXCollections.observableArrayList<Dog>(Dog("Scruffy", owner = "Bob"))
joinedList = people.leftOuterJoin(dogs, Person::name, Dog::owner)
replayedList = ReplayedList(joinedList.map { Pair(it.first, ReplayedList(it.second)) })
}
// TODO perhaps these are too brittle because they test indices that are not stable. Use Expect dsl?
@Test
fun addWorks() {
require(replayedList.size == 1)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 0)
dogs.add(Dog("Scooby", owner = "Alice"))
require(replayedList.size == 1)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Scooby")
people.add(Person("Bob", 34))
require(replayedList.size == 2)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Scooby")
require(replayedList[1].first.name == "Bob")
require(replayedList[1].second.size == 1)
require(replayedList[1].second[0].name == "Scruffy")
dogs.add(Dog("Bella", owner = "Bob"))
require(replayedList.size == 2)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Scooby")
require(replayedList[1].first.name == "Bob")
require(replayedList[1].second.size == 2)
require(replayedList[1].second[0].name == "Bella")
require(replayedList[1].second[1].name == "Scruffy")
// We have another Alice wat
people.add(Person("Alice", 91))
require(replayedList.size == 3)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Scooby")
require(replayedList[1].first.name == "Alice")
require(replayedList[1].second.size == 1)
require(replayedList[1].second[0].name == "Scooby")
require(replayedList[2].first.name == "Bob")
require(replayedList[2].second.size == 2)
require(replayedList[2].second[0].name == "Bella")
require(replayedList[2].second[1].name == "Scruffy")
}
@Test
fun removeWorks() {
dogs.add(Dog("Scooby", owner = "Alice"))
people.add(Person("Bob", 34))
dogs.add(Dog("Bella", owner = "Bob"))
require(people.removeAt(0).name == "Alice")
require(replayedList.size == 1)
require(replayedList[0].first.name == "Bob")
require(replayedList[0].second.size == 2)
require(replayedList[0].second[0].name == "Bella")
require(replayedList[0].second[1].name == "Scruffy")
require(dogs.removeAt(0).name == "Scruffy")
require(replayedList.size == 1)
require(replayedList[0].first.name == "Bob")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Bella")
people.add(Person("Alice", 213))
require(replayedList.size == 2)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 1)
require(replayedList[0].second[0].name == "Scooby")
require(replayedList[1].first.name == "Bob")
require(replayedList[1].second.size == 1)
require(replayedList[1].second[0].name == "Bella")
dogs.clear()
require(replayedList.size == 2)
require(replayedList[0].first.name == "Alice")
require(replayedList[0].second.size == 0)
require(replayedList[1].first.name == "Bob")
require(replayedList[1].second.size == 0)
people.clear()
require(replayedList.size == 0)
}
}

View File

@ -0,0 +1,66 @@
package com.r3corda.client.fxutils
import javafx.collections.FXCollections
import org.junit.Before
import org.junit.Test
class MappedListTest {
var sourceList = FXCollections.observableArrayList("Alice")
var mappedList = MappedList(sourceList) { it.length }
var replayedList = ReplayedList(mappedList)
@Before
fun setup() {
sourceList = FXCollections.observableArrayList("Alice")
mappedList = MappedList(sourceList) { it.length }
replayedList = ReplayedList(mappedList)
}
@Test
fun addWorks() {
require(replayedList.size == 1)
require(replayedList[0] == 5)
sourceList.add("Bob")
require(replayedList.size == 2)
require(replayedList[0] == 5)
require(replayedList[1] == 3)
sourceList.add(0, "Charlie")
require(replayedList.size == 3)
require(replayedList[0] == 7)
require(replayedList[1] == 5)
require(replayedList[2] == 3)
}
@Test
fun removeWorks() {
sourceList.add("Bob")
sourceList.add(0, "Charlie")
require(replayedList.size == 3)
sourceList.removeAt(1)
require(replayedList.size == 2)
require(replayedList[0] == 7)
require(replayedList[1] == 3)
}
@Test
fun permuteWorks() {
sourceList.add("Bob")
sourceList.add(0, "Charlie")
sourceList.sortBy { it.length }
require(sourceList[0] == "Bob")
require(sourceList[1] == "Alice")
require(sourceList[2] == "Charlie")
require(replayedList.size == 3)
require(replayedList[0] == 3)
require(replayedList[1] == 5)
require(replayedList[2] == 7)
}
}

View File

@ -148,7 +148,7 @@ class CashViewer : View() {
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
*/
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
val sumAmount = amounts.fold(Amount(0, currency), Amount<Currency>::plus)
val sumAmount = amounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
/**
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
@ -168,7 +168,7 @@ class CashViewer : View() {
*/
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
val equivSumAmount = reportingCurrency.bind { currency ->
equivAmounts.fold(Amount(0, currency), Amount<Currency>::plus)
equivAmounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
}
/**