Updating map value for a specific key in Kotlin - dictionary

I am new at Kotlin and trying to catch up the language.
I have a function that returns DayofWeek as a key and an Int as a value.
The issue that I am facing is that I need to take a list of object that has another list of object inside that has an Int value which I need to save and increment for every time I see the same value.
Here is my function -
class OrdersAnalyzer {
data class Order(val orderId: Int, val creationDate: LocalDateTime, val orderLines: List<OrderLine>)
data class OrderLine(val productId: Int, val name: String, val quantity: Int, val unitPrice: BigDecimal)
fun totalDailySales(orders: List<Order>) : Map<DayOfWeek, Int> {
val map: MutableMap<DayOfWeek, Int>? = mutableMapOf(
Pair(DayOfWeek.SUNDAY, 0),
Pair(DayOfWeek.MONDAY, 0),
Pair(DayOfWeek.TUESDAY, 0),
Pair(DayOfWeek.WEDNESDAY, 0),
Pair(DayOfWeek.THURSDAY, 0),
Pair(DayOfWeek.FRIDAY, 0),
Pair(DayOfWeek.SATURDAY, 0)
)
for (order in orders) {
val dayOfWeek = order.creationDate.dayOfWeek
var quantity = 0
map?.put(dayOfWeek, quantity)
}
return map!!
}
}
So the issues I am facing right now are 2 -
1) How can I increment the value of each pair when it is the corrent DayOfWeek? I don't want to replace, I want to add it to the last value.
2) When returning the Map, I do not want to return the DayOfWeeks that have the value of 0. How can I do that?

Here is a modified version of the elegant answer provided by Arjan, with some test code.
Updated the answer after the comment from Alon Shlider -- now counting all order item quantities grouped by day of the week:
fun totalDailySales(orders: List<Order>): Map<DayOfWeek, Int> =
orders.groupBy { it.creationDate.dayOfWeek }
.mapValues { sumItemQuantities(it.value) }
fun sumItemQuantities(orders: List<Order>) =
orders.flatMap { it.orderLines.map { line -> line.quantity } }.sum()
fun main() {
val orders = listOf(
Order(
1,
LocalDateTime.now().minusDays(2),
listOf(
OrderLine(6, "laptop", 28, 1200.toBigDecimal())
)
),
Order(
2,
LocalDateTime.now().minusDays(1),
listOf(
OrderLine(496, "VR headset", 6, 400.toBigDecimal())
)
)
)
println(totalDailySales(orders))
}
Output:
{FRIDAY=28, SATURDAY=6}
With this approach, Kotlin functions do the grouping and counting for you. The groupBy function creates a map from DayOfWeek to a list of orders (grouping all orders with the same day of week in a list). The mapValues function transforms that map by replacing the lists with the result of the sumItemQuantities function (for each list).
In the for loop in your code, you can retrieve the current quantity for a specific day (or use zero if it isn't set yet), increase it by the right amount and then store it. To return only the map entries with non zero values, you could filter (return totalsPerDay.filter { it.value > 0 }) or start with an empty map. This is your function with some changes:
fun totalDailySales(orders: List<Order>): Map<DayOfWeek, Int> {
val totalsPerDay = mutableMapOf<DayOfWeek, Int>()
for (order in orders) {
val dayOfWeek = order.creationDate.dayOfWeek
val currentQuantity = totalsPerDay[dayOfWeek] ?: 0
// This is not the best way to increment by the sum of the order
// item quantities...
val orderItemQuantities = sumItemQuantities(listOf(order))
totalsPerDay[dayOfWeek] = currentQuantity + orderItemQuantities
}
return totalsPerDay
}
Output after calling it:
println(OrdersAnalyzer().totalDailySales(orders))
{FRIDAY=28, SATURDAY=6}

Updated answer (also thanks to Freek de Bruijn), with some test code.
I think it would be something like this:
fun totalDailySales(orders: List<Order>) : Map<DayOfWeek, Int> =
orders.groupBy { it.creationDate.dayOfWeek }
.mapValues { it.value.flatMap { it.orderLines } }
.mapValues { it.value.map { it.quantity } }
.mapValues { it.value.sum() }
groupBy creates a Map where the values are of type List<Order>, so you need to call a few steps to convert these values to Int. First we use flatMap to convert List<Order> to List<OrderLine> (map would convert to List<List<OrderLine>>). Then we use map to get the quantities out of List<OrderLine>, and finally sum() to add up all those quantities.
val orders = listOf(
Order(
2,
LocalDateTime.now().minusDays(2),
listOf(
OrderLine(5, "monitor", 10, 200.toBigDecimal()),
OrderLine(4, "keyboard", 5, 50.toBigDecimal())
)
)
)
println(totalDailySales(orders))
This results in the output:
{FRIDAY=15}

Related

Check for duplicated values in sortedMap

I have a map initialized as
val cache: SortedMap<String, String> = sortedMapOf()
The map is used as a cache and can contain duplicated values with their own unique key. I want to check and count how many duplicated values there are in the cache. Note that the cache can contain millions of entries.
As of now, I check for duplicates in this way
val uniqueValueSet = hashSetOf<String>()
val numDuplicates = cache.filter {!uniqueValueSet.add(it.value)}.count()
However, I feel like this check is memory inefficient, where adding all the distinct values to a set creates an obsolete set with possibly millions of elements.
Is there a better, and more optimized, way of checking duplicates between the values in a map?
If you're interested only in amount of duplicates you can do just:
val numOfDuplicates = cache.size - cache.values.toHashSet().size
It will still create a set with all distinct values, but it will be the only overhead.
Another option is to trade space complexity (O(N) -> O(M), where N - size of cache, M - amount of unique duplicates; makes sense if M << N) to time complexity (O(N*logN) -> O(N^2)):
var numOfDuplicates = 0
val duplicates = hashSetOf<String>()
for (value in cache.values) {
if (value in duplicates) {
numOfDuplicates++
} else if (cache.values.atLeastTwo { it == value }) {
duplicates.add(value)
}
}
public inline fun <T> Iterable<T>.atLeastTwo(predicate: (T) -> Boolean): Boolean {
var atLeastOne = false
for (it in this) {
if (predicate(it)) {
if (!atLeastOne) {
atLeastOne = true
} else {
return true
}
}
}
return false
}
This should be the fastest (inspired by Михаил Нафталь's solution):
val numDuplicates = cache.size - cache.values.distinct().size
Edit 1: I did some tests and up to 50.000 entries (key and value each Random strings of 10 alphanumeric characters) this is indeed faster. Above approximately 50.000 entries Михаил Нафталь's solution becomes drastically faster:
val numOfDuplicates = cache.size - cache.values.toHashSet().size
Edit 2: Added some simple tests:
val count_of_entries = 1_000_000
val lengthOfRandomKey = 10
val lengthOfRandomValue = 10
fun randomString(length: Int): String {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return (1..length).map { _ -> kotlin.random.Random.nextInt(0, charPool.size) }.map(charPool::get)
.joinToString("")
}
val cache: java.util.SortedMap<String, String> = sortedMapOf()
repeat (count_of_entries) { cache[randomString(lengthOfRandomKey)] = randomString(lengthOfRandomValue) }
val uniqueValueSet = hashSetOf<String>()
val start1 = System.nanoTime()
val numDuplicates1 = cache.filter { !uniqueValueSet.add(it.value) }.count()
val end1 = System.nanoTime()
println("$numDuplicates1 duplicates, time ${(end1 - start1).toDouble() / 1_000_000_000} s")
val start2 = System.nanoTime()
val numDuplicates2 = cache.size - cache.values.toHashSet().size
val end2 = System.nanoTime()
println("$numDuplicates2 duplicates, time ${(end2 - start2).toDouble() / 1_000_000_000} s")
val start3 = System.nanoTime()
val numDuplicates3 = cache.size - cache.values.distinct().size
val end3 = System.nanoTime()
println("$numDuplicates3 duplicates, time ${(end3 - start3).toDouble() / 1_000_000_000} s")

Kotlin - Filter/Map from two Collections

Is it possible to filter and map two Collections based on desired criteria match like the following:
fun main(args: Array<String>) {
val selectedDates = listOf("2018-08-12", "2018-08-13", "2018-08-14")
val expenses = listOf(Expense("Food", "2018-08-12"),
Expense("Transportation", "2018-08-15"),
Expense("Misc.", "2018-08-13"),
Expense("Uber", "2018-08-12"),
Expense("Clothing", "2018-08-16"))
val listOfExpensesInSelectedDate = mutableListOf<Expense>()
for (date in selectedDates){
listOfExpensesInSelectedDate.addAll(expenses.filter { it.date==date })
}
println(listOfExpensesInSelectedDate)
}
data class Expense(
val expense:String,
val date: String
)
Provided the given code above, I am trying to return a list of Expenses that match with the dates from another list of Strings. In the example above, I have used both for Loop and filter function to get my desired result. But is it possible to avoid the for loop and filter and map both the Collections in single line of code?
You could just use in to filter:
val listOfExpensesInSelectedDate = expenses.filter { it.date in selectedDates }
Edit: since hotkey posted the comment about optimal solution, I have tried this in my pc and anyone interested can try it too:
(1st) my answer as was posted:
val start = Date().time
for (i in 1..10000) {
val listOfExpensesInSelectedDate = expenses.filter { it.date in selectedDates }
}
val end= Date().time
println(end - start)
Average time result: 26ms (23ms-35ms)
(2nd) my answer with the hotkey's suggestion to use a set:
val start = Date().time
for (i in 1..10000) {
val expSet = selectedDates.toSet()
val listOfExpensesInSelectedDate = expenses.filter { it.date in expSet }
}
val end= Date().time
println(end - start)
Average time result: 70ms (50ms-86ms)
(3d) hotkey's answer:
val start = Date().time
for (i in 1..10000) {
val groups = expenses.groupBy { it.date }
val listOfExpensesInSelectedDate = selectedDates.flatMap { groups[it].orEmpty() }
}
val end= Date().time
println(end - start)
Average time result: 100ms (74ms-150ms)
You can simplify and optimize your code by first grouping expenses by the date and then selecting and merging the groups that have their key in the selectedDates, like this:
val selectedDates = listOf("2018-08-12", "2018-08-13", "2018-08-14")
val expenses: List<Expense> = TODO("content omitted")
val groups = expenses.groupBy { it.date }
val listOfExpensesInSelectedDate = selectedDates.flatMap { groups[it].orEmpty() }
See: groupBy, flatMap

How to convert List to Map in Kotlin?

For example I have a list of strings like:
val list = listOf("a", "b", "c", "d")
and I want to convert it to a map, where the strings are the keys.
I know I should use the .toMap() function, but I don't know how, and I haven't seen any examples of it.
You have two choices:
The first and most performant is to use associateBy function that takes two lambdas for generating the key and value, and inlines the creation of the map:
val map = friends.associateBy({it.facebookId}, {it.points})
The second, less performant, is to use the standard map function to create a list of Pair which can be used by toMap to generate the final map:
val map = friends.map { it.facebookId to it.points }.toMap()
From List to Map with associate function
With Kotlin 1.3, List has a function called associate. associate has the following declaration:
fun <T, K, V> Iterable<T>.associate(transform: (T) -> Pair<K, V>): Map<K, V>
Returns a Map containing key-value pairs provided by transform function applied to elements of the given collection.
Usage:
class Person(val name: String, val id: Int)
fun main() {
val friends = listOf(Person("Sue Helen", 1), Person("JR", 2), Person("Pamela", 3))
val map = friends.associate({ Pair(it.id, it.name) })
//val map = friends.associate({ it.id to it.name }) // also works
println(map) // prints: {1=Sue Helen, 2=JR, 3=Pamela}
}
From List to Map with associateBy function
With Kotlin, List has a function called associateBy. associateBy has the following declaration:
fun <T, K, V> Iterable<T>.associateBy(keySelector: (T) -> K, valueTransform: (T) -> V): Map<K, V>
Returns a Map containing the values provided by valueTransform and indexed by keySelector functions applied to elements of the given collection.
Usage:
class Person(val name: String, val id: Int)
fun main() {
val friends = listOf(Person("Sue Helen", 1), Person("JR", 2), Person("Pamela", 3))
val map = friends.associateBy(keySelector = { person -> person.id }, valueTransform = { person -> person.name })
//val map = friends.associateBy({ it.id }, { it.name }) // also works
println(map) // prints: {1=Sue Helen, 2=JR, 3=Pamela}
}
If you have duplicates in your list that you don't want to lose, you can do this using groupBy.
Otherwise, like everyone else said, use associate/By/With (which in the case of duplicates, I believe, will only return the last value with that key).
An example grouping a list of people by age:
class Person(val name: String, val age: Int)
fun main() {
val people = listOf(Person("Sue Helen", 31), Person("JR", 25), Person("Pamela", 31))
val duplicatesKept = people.groupBy { it.age }
val duplicatesLost = people.associateBy({ it.age }, { it })
println(duplicatesKept)
println(duplicatesLost)
}
Results:
{31=[Person#41629346, Person#4eec7777], 25=[Person#3b07d329]}
{31=Person#4eec7777, 25=Person#3b07d329}
Convert a Iteratable Sequence Elements to a Map in kotlin ,
associate vs associateBy vs associateWith:
*Reference:Kotlin Documentation
1- associate (to set both Keys & Values): Build a map that can set key & value elements :
IterableSequenceElements.associate { newKey to newValue } //Output => Map {newKey : newValue ,...}
If any of two pairs would have the same key the last one gets added to the map.
The returned map preserves the entry iteration order of the original array.
2- associateBy (just set Keys by calculation): Build a map that we can set new Keys, analogous elements will be set for values
IterableSequenceElements.associateBy { newKey } //Result: => Map {newKey : 'Values will be set from analogous IterableSequenceElements' ,...}
3- associateWith (just set Values by calculation): Build a map that we can set new Values, analogous elements will be set for Keys
IterableSequenceElements.associateWith { newValue } //Result => Map { 'Keys will be set from analogous IterableSequenceElements' : newValue , ...}
Example from Kotlin tips :
You can use associate for this task:
val list = listOf("a", "b", "c", "d")
val m: Map<String, Int> = list.associate { it to it.length }
In this example, the strings from list become the keys and their corresponding lengths (as an example) become the values inside the map.
That have changed on the RC version.
I am using val map = list.groupByTo(destinationMap, {it.facebookId}, { it -> it.point })

How can I create new map with new values but same keys from an existing map?

I have an existing map in Groovy.
I want to create a new map that has the same keys but different values in it.
Eg.:
def scores = ["vanilla":10, "chocolate":9, "papaya": 0]
//transformed into
def preference = ["vanilla":"love", "chocolate":"love", "papaya": "hate"]
Any way of doing it through some sort of closure like:
def preference = scores.collect {//something}
You can use collectEntries
scores.collectEntries { k, v ->
[ k, 'new value' ]
}
An alternative to using a map for the ranges would be to use a switch
def grade = { score ->
switch( score ) {
case 10..9: return 'love'
case 8..6: return 'like'
case 5..2: return 'meh'
case 1..0: return 'hate'
default : return 'ERR'
}
}
scores.collectEntries { k, v -> [ k, grade( v ) ] }
Nice, functional style solution(including your ranges, and easy to modify):
def scores = [vanilla:10, chocolate:9, papaya: 0]
// Store somewhere
def map = [(10..9):"love", (8..6):"like", (5..2):"meh", (1..0):"hate"]
def preference = scores.collectEntries { key, score -> [key, map.find { score in it.key }.value] }
// Output: [vanilla:love, chocolate:love, papaya:hate]
def scores = ["vanilla":10, "chocolate":9, "papaya": 0]
def preference = scores.collectEntries {key, value -> ["$key":(value > 5 ? "like" : "hate")]}
Then the result would be
[vanilla:like, chocolate:like, papaya:hate]
EDIT: If you want a map, then you should use collectEntries like tim_yates said.

Field type returning numbers [Axapta]

I want to get the field types. My code is as follows:
tID = dict.tableName2Id(tableName);
counter = 0;
dt = new DictTable(tID);
if (dt)
{
counter = dt.fieldNext(counter);
while (counter)
{
df = dt.fieldObject(counter);
if (df)
{
fields = conIns(fields,1,df.baseType());
}
counter = dt.fieldNext(counter);
}
}
On return to .NET Business connector, the types are shown as numbers instead of strings.
Kindly help.
EDIT : DataField.baseType() returns "Types" can this be converted to string and then added to the container?
EDIT 2: Ok now, im getting a Types Enumeration. Is there any way to map this enumeration in AX and add to container as string?
Got it!! Here's the code :
tID = dict.tableName2Id(tableName);
counter = 0;
dt = new DictTable(tID);
if (dt)
{
counter = dt.fieldNext(counter);
while (counter)
{
df = dt.fieldObject(counter);
if (df)
{
t = df.baseType();
fields = conIns(fields,1,enum2str(t));
}
counter = dt.fieldNext(counter);
}
}

Resources