3db70bf8f508853bfb28923ee3cd7ba899bb4a5b
[idea/community.git] / plugins / stats-collector / src / com / intellij / stats / personalization / impl / UserFactorStorageBase.kt
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.stats.personalization.impl
18
19 import com.intellij.openapi.components.PersistentStateComponent
20 import com.intellij.stats.personalization.*
21 import com.intellij.util.attribute
22 import org.jdom.Element
23 import java.text.DecimalFormat
24 import java.util.*
25
26 abstract class UserFactorStorageBase
27     : UserFactorStorage, PersistentStateComponent<Element> {
28
29     private companion object {
30         val DOUBLE_VALUE_FORMATTER = DecimalFormat().apply {
31             maximumFractionDigits = 6
32             minimumFractionDigits = 1
33         }
34     }
35
36     private val state = CollectorState()
37
38     override fun <U : FactorUpdater> getFactorUpdater(description: UserFactorDescription<U, *>): U =
39             description.updaterFactory.invoke(getAggregateFactor(description.factorId))
40
41     override fun <R : FactorReader> getFactorReader(description: UserFactorDescription<*, R>): R =
42             description.readerFactory.invoke(getAggregateFactor(description.factorId))
43
44     override fun getState(): Element {
45         val element = Element("component")
46         state.writeState(element)
47         return element
48     }
49
50     override fun loadState(newState: Element) {
51         state.applyState(newState)
52     }
53
54     private fun getAggregateFactor(factorId: String): MutableDoubleFactor =
55             state.aggregateFactors.computeIfAbsent(factorId, { DailyAggregateFactor() })
56
57     private class CollectorState {
58         val aggregateFactors: MutableMap<String, DailyAggregateFactor> = HashMap()
59
60         fun applyState(element: Element) {
61             aggregateFactors.clear()
62             for (child in element.children) {
63                 val factorId = child.getAttributeValue("id")
64                 if (child.name == "factor" && factorId != null) {
65                     val factor = DailyAggregateFactor.restore(child)
66                     if (factor != null) aggregateFactors[factorId] = factor
67                 }
68             }
69         }
70
71         fun writeState(element: Element) {
72             for ((id, factor) in aggregateFactors.asSequence().sortedBy { it.key }) {
73                 val factorElement = Element("factor")
74                 factorElement.attribute("id", id)
75                 factor.writeState(factorElement)
76                 element.addContent(factorElement)
77             }
78         }
79     }
80
81     class DailyAggregateFactor private constructor(private val aggregates: SortedMap<Day, DailyData> = sortedMapOf())
82         : MutableDoubleFactor {
83         constructor() : this(sortedMapOf())
84
85         init {
86             ensureLimit()
87         }
88
89         companion object {
90             val DAYS_LIMIT = 10
91
92             fun restore(element: Element): DailyAggregateFactor? {
93                 val data = sortedMapOf<Day, DailyData>()
94                 for (child in element.children) {
95                     val date = child.getAttributeValue("date")
96                     val day = DayImpl.fromString(date)
97                     if (child.name == "dailyData" && day != null) {
98                         val dailyData = DailyData.restore(child)
99                         if (dailyData != null) data.put(day, dailyData)
100                     }
101                 }
102
103                 if (data.isEmpty()) return null
104                 return DailyAggregateFactor(data)
105             }
106         }
107
108         fun writeState(element: Element) {
109             for ((day, data) in aggregates) {
110                 val dailyDataElement = Element("dailyData")
111                 dailyDataElement.attribute("date", day.toString())
112                 data.writeState(dailyDataElement)
113                 element.addContent(dailyDataElement)
114             }
115         }
116
117         override fun availableDays(): List<Day> = aggregates.keys.toList()
118
119         override fun incrementOnToday(key: String): Boolean {
120             return updateOnDate(DateUtil.today()) {
121                 compute(key, { _, oldValue -> if (oldValue == null) 1.0 else oldValue + 1.0 })
122             }
123         }
124
125         override fun onDate(date: Day): Map<String, Double>? = aggregates[date]?.data
126
127         override fun updateOnDate(date: Day, updater: MutableMap<String, Double>.() -> Unit): Boolean {
128             val old = aggregates[date]
129             if (old != null) {
130                 updater.invoke(old.data)
131                 return true
132             }
133
134             if (aggregates.size < DAYS_LIMIT || aggregates.firstKey() < date) {
135                 val data = DailyData()
136                 updater.invoke(data.data)
137                 aggregates.put(date, data)
138                 ensureLimit()
139                 return true
140             }
141
142             return false
143         }
144
145         private fun ensureLimit() {
146             while (aggregates.size > DAYS_LIMIT) {
147                 aggregates.remove(aggregates.firstKey())
148             }
149         }
150     }
151
152     private class DailyData(val data: MutableMap<String, Double> = HashMap()) {
153         companion object {
154             fun restore(element: Element): DailyData? {
155                 val data = mutableMapOf<String, Double>()
156                 for (child in element.children) {
157                     if (child.name == "observation") {
158                         val dataKey = child.getAttributeValue("name")
159                         val dataValue = child.getAttributeValue("value")
160
161                         // skip all if any observation is inconsistent
162                         val value = dataValue.toDoubleOrNull() ?: return null
163                         data[dataKey] = value
164                     }
165                 }
166
167                 if (data.isEmpty()) return null
168                 return DailyData(data)
169             }
170         }
171
172         fun writeState(element: Element) {
173             for ((key, value) in data.asSequence().sortedBy { it.key }) {
174                 val observation = Element("observation")
175                 observation.attribute("name", key)
176                 observation.attribute("value", DOUBLE_VALUE_FORMATTER.format(value))
177                 element.addContent(observation)
178             }
179         }
180     }
181 }