mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
client: Add ConcatenatedList, LeftOuterJoinedMap, tests and utilities, fix some bugs
This commit is contained in:
parent
99e758e021
commit
9a212a8714
@ -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
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user