diff --git a/platform/platform-tests/testSrc/com/intellij/internal/statistics/StatisticsUtilTest.kt b/platform/platform-tests/testSrc/com/intellij/internal/statistics/StatisticsUtilTest.kt index 54cd1b2d6171..c1ab11f3f310 100644 --- a/platform/platform-tests/testSrc/com/intellij/internal/statistics/StatisticsUtilTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/internal/statistics/StatisticsUtilTest.kt @@ -7,6 +7,7 @@ import com.intellij.internal.statistic.eventLog.EventLogConfiguration import com.intellij.internal.statistic.utils.StatisticsUtil import com.intellij.internal.statistic.utils.StatisticsUtil.getCurrentHourInUTC import com.intellij.internal.statistic.utils.StatisticsUtil.getNextPowerOfTwo +import com.intellij.internal.statistic.utils.StatisticsUtil.roundToHighestDigit import com.intellij.internal.statistic.utils.StatisticsUtil.roundToPowerOfTwo import com.intellij.internal.statistic.utils.StatisticsUtil.roundToUpperBound import com.intellij.testFramework.LightPlatformTestCase @@ -120,6 +121,94 @@ class StatisticsUtilTest : LightPlatformTestCase() { assertEquals(expected, roundToPowerOfTwo(value), "Incorrect key for value `$value`") } + @Test + fun `test round to highest digit int`() { + // Test from -10..10 + for (i in -10..-5) { + testRoundToHighestDigit(i, -10) + } + for (i in -4..-1) { + testRoundToHighestDigit(i, -1) + } + testRoundToHighestDigit(0, 0) + for (i in 1..4) { + testRoundToHighestDigit(i, 1) + } + for (i in 5..10) { + testRoundToHighestDigit(i, 10) + } + + testRoundToHighestDigit(11, 10) + testRoundToHighestDigit(16, 20) + testRoundToHighestDigit(64, 60) + testRoundToHighestDigit(65, 70) + + testRoundToHighestDigit(94, 90) + testRoundToHighestDigit(95, 100) + testRoundToHighestDigit(99, 100) + + for (i in 1..9) { + val expected = i * 1000 + testRoundToHighestDigit(expected, expected) + } + + testRoundToHighestDigit(1024, 1000) + testRoundToHighestDigit(1500, 2000) + testRoundToHighestDigit(1999, 2000) + + testRoundToHighestDigit(Int.MAX_VALUE, 2000000000) + testRoundToHighestDigit(Int.MIN_VALUE + 1, -2000000000) + testRoundToHighestDigit(Int.MIN_VALUE, -2000000000) + } + + private fun testRoundToHighestDigit(value: Int, expected: Int) { + assertEquals(expected, roundToHighestDigit(value), "Incorrect key for value `$value`") + } + + @Test + fun `test round to highest digit long`() { + // Test from -10..10 + for (i in -10L..-5L) { + testRoundToHighestDigit(i, -10L) + } + for (i in -4L..-1L) { + testRoundToHighestDigit(i, -1L) + } + testRoundToHighestDigit(0L, 0L) + for (i in 1L..4L) { + testRoundToHighestDigit(i, 1L) + } + for (i in 5L..10L) { + testRoundToHighestDigit(i, 10L) + } + + testRoundToHighestDigit(11L, 10L) + testRoundToHighestDigit(16L, 20L) + testRoundToHighestDigit(64L, 60L) + testRoundToHighestDigit(65L, 70L) + + testRoundToHighestDigit(94L, 90L) + testRoundToHighestDigit(95L, 100L) + testRoundToHighestDigit(99L, 100L) + + for (i in 1L..9L) { + val expected = i * 1000L + testRoundToHighestDigit(expected, expected) + } + + testRoundToHighestDigit(1024L, 1000L) + testRoundToHighestDigit(1500L, 2000L) + testRoundToHighestDigit(1999L, 2000L) + + testRoundToHighestDigit(Long.MAX_VALUE, 9000000000000000000L) + testRoundToHighestDigit(Long.MIN_VALUE + 1L, -9000000000000000000L) + testRoundToHighestDigit(Long.MIN_VALUE, -9000000000000000000L) + } + + private fun testRoundToHighestDigit(value: Long, expected: Long) { + assertEquals(expected, roundToHighestDigit(value), "Incorrect key for value `$value`") + } + @Test fun `test round to upper bound`() { // Test empty bounds return next power of two @@ -150,7 +239,7 @@ class StatisticsUtilTest : LightPlatformTestCase() { private fun testRoundToUpperBound(value: Int, bounds: IntArray, expected: Int) { assertEquals(expected, roundToUpperBound(value, bounds), "Incorrect key for value `$value`") } - + @Test fun `test hash sensitive data`() { diff --git a/platform/statistics/src/com/intellij/internal/statistic/utils/StatisticsUtil.kt b/platform/statistics/src/com/intellij/internal/statistic/utils/StatisticsUtil.kt index 7aa4b5b85901..ad0c274ccc90 100644 --- a/platform/statistics/src/com/intellij/internal/statistic/utils/StatisticsUtil.kt +++ b/platform/statistics/src/com/intellij/internal/statistic/utils/StatisticsUtil.kt @@ -5,6 +5,8 @@ import java.text.SimpleDateFormat import java.time.ZoneOffset import java.util.* import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.math.roundToLong object StatisticsUtil { private const val kilo = 1000 @@ -71,6 +73,62 @@ object StatisticsUtil { return java.lang.Long.signum(value) * nextPowerOfTwo } + /** + * Please, use only in specific cases where precision of `roundToPowerOfTwo` is not enough + * + * Anonymizes sensitive project properties by rounding it to the power of ten multiplied with either: + * - leading digit if second digit is smaller than 4 or equal to 4 + * - leading digit plus one if second digit is greater than 5 or equal to 5 + * + * Special cases: + * - returns the same value if all trailing digits equal zero; + * - returns `-roundToHighestDigit(abs(value))` if the value is negative; + * - returns 0 in case of 0; + * - returns +/-1 for abs(value) in 1..4 + * - returns +/-10 for abs(value) in 5..10 + * - returns `roundToHighestDigit(Int.MAX_VALUE)` for Int.MIN_VALUE + */ + @JvmStatic + fun roundToHighestDigit(value: Int): Int { + if (value == 0) return 0 + val abs = if (value != Int.MIN_VALUE) abs(value) else Int.MAX_VALUE // See abs(Int) documentation + if (abs <= 4) return Integer.signum(value) * 1 + else if (abs <= 10) return Integer.signum(value) * 10 + + var firstDigit = abs.toFloat() + var tenPowX = 1 + while (firstDigit > 10) { + firstDigit /= 10 + tenPowX *= 10 + } + + // can't get overflow because second digit in max int (2147483647) is '1' + return Integer.signum(value) * (firstDigit.roundToInt() * tenPowX) + } + + /** + * Please, use only in specific cases where precision of `roundToPowerOfTwo` is not enough + * + * @see com.intellij.internal.statistic.utils.StatisticsUtil.roundToHighestDigit(int) + */ + @JvmStatic + fun roundToHighestDigit(value: Long): Long { + if (value == 0L) return 0L + val abs = if (value != Long.MIN_VALUE) abs(value) else Long.MAX_VALUE // See abs(Long) documentation + if (abs <= 4L) return java.lang.Long.signum(value) * 1L + else if (abs <= 10L) return java.lang.Long.signum(value) * 10L + + var firstDigit = abs.toDouble() + var tenPowX = 1L + while (firstDigit > 10L) { + firstDigit /= 10L + tenPowX *= 10L + } + + // can't get overflow because second digit in max long (9223372036854775807L) is '2' + return java.lang.Long.signum(value) * (firstDigit.roundToLong() * tenPowX) + } + /** * Anonymizes value by finding upper bound in provided bounds. * Allows more fine tuning then `com.intellij.internal.statistic.utils.StatisticsUtil#roundToPowerOfTwo`