diff --git a/frontend/src/lib/utils.test.js b/frontend/src/lib/utils.test.js index 0292fe80bc49a..c63008e86e072 100644 --- a/frontend/src/lib/utils.test.js +++ b/frontend/src/lib/utils.test.js @@ -109,10 +109,10 @@ describe('compactNumber()', () => { expect(compactNumber(10)).toEqual('10') expect(compactNumber(293)).toEqual('293') expect(compactNumber(5001)).toEqual('5K') - expect(compactNumber(5312)).toEqual('5.3K') - expect(compactNumber(5392)).toEqual('5.4K') - expect(compactNumber(2833102, 2)).toEqual('2.83M') - expect(compactNumber(8283310234)).toEqual('8.3B') + expect(compactNumber(5312)).toEqual('5.31K') + expect(compactNumber(5392)).toEqual('5.39K') + expect(compactNumber(2833102)).toEqual('2.83M') + expect(compactNumber(8283310234)).toEqual('8.28B') }) }) describe('pluralize()', () => { diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 91e55f5064404..90773ea127812 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -776,28 +776,17 @@ export function pluralize(count: number, singular: string, plural?: string, incl return includeNumber ? `${count} ${form}` : form } -function suffixFormatted(value: number, base: number, suffix: string, maxDecimals: number): string { - /* Helper function for compactNumber */ - const multiplier = 10 ** maxDecimals - return `${Math.round((value * multiplier) / base) / multiplier}${suffix}` -} - -export function compactNumber(value: number, maxDecimals: number = 1): string { - /* - Returns a number in a compact format with a thousands or millions suffix if applicable. - Server-side equivalent posthog_filters.py#compact_number - Example: - compactNumber(5500000) - => "5.5M" - */ - if (value < 1000) { - return Math.floor(value).toString() - } else if (value < 1000000) { - return suffixFormatted(value, 1000, 'K', maxDecimals) - } else if (value < 1000000000) { - return suffixFormatted(value, 1000000, 'M', maxDecimals) - } - return suffixFormatted(value, 1000000000, 'B', maxDecimals) +/** Return a number in a compact format, with a SI suffix if applicable. + * Server-side equivalent: utils.py#compact_number. + */ +export function compactNumber(value: number): string { + value = parseFloat(value.toPrecision(3)) + let magnitude = 0 + while (Math.abs(value) >= 1000) { + magnitude++ + value /= 1000 + } + return value.toString() + ['', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y'][magnitude] } export function sortedKeys(object: Record): Record { diff --git a/posthog/templatetags/posthog_filters.py b/posthog/templatetags/posthog_filters.py index b387d0ca0cb90..c8f669d6d15a3 100644 --- a/posthog/templatetags/posthog_filters.py +++ b/posthog/templatetags/posthog_filters.py @@ -3,35 +3,13 @@ from django import template +from posthog.utils import compact_number + register = template.Library() Number = Union[int, float] - -@register.filter -def compact_number(value: Number, max_decimals: int = 1) -> str: - """ - Returns a number in a compact format with a thousands or millions suffix if applicable. - Client-side equivalent utils.tsx#compactNumber - Example: - {% compact_number 5500000 %} - => "5.5M" - """ - - def suffix_formatted(value: Number, base: float, suffix: str) -> str: - multiplier: int = 10 ** max_decimals - return f"{str(math.floor(value * multiplier / base) / multiplier).rstrip('0').rstrip('.')}{suffix}" - - if value < 1000: - return str(math.floor(value)) - - if value < 1_000_000: - return suffix_formatted(value, 1_000.0, "K") - - if value < 1_000_000_000: - return suffix_formatted(value, 1_000_000.0, "M") - - return suffix_formatted(value, 1_000_000_000.0, "B") +register.filter(compact_number) @register.filter diff --git a/posthog/test/test_templatetags.py b/posthog/test/test_templatetags.py index bfbef02c57efe..cc779b94f3709 100644 --- a/posthog/test/test_templatetags.py +++ b/posthog/test/test_templatetags.py @@ -18,10 +18,10 @@ def test_utmify_email_url(self): def test_compact_number(self): self.assertEqual(compact_number(5001), "5K") - self.assertEqual(compact_number(5312), "5.3K") - self.assertEqual(compact_number(5392), "5.3K") # rounds down - self.assertEqual(compact_number(2833102, 2), "2.83M") - self.assertEqual(compact_number(8283310234), "8.2B") + self.assertEqual(compact_number(5312), "5.31K") + self.assertEqual(compact_number(5392), "5.39K") + self.assertEqual(compact_number(2833102), "2.83M") + self.assertEqual(compact_number(8283310234), "8.28B") def test_percentage(self): self.assertEqual(percentage(0.1829348, 2), "18.29%") diff --git a/posthog/utils.py b/posthog/utils.py index bd7ca8e009b3b..eb88c4f7b3954 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -420,7 +420,7 @@ def get_machine_id() -> str: return hashlib.md5(uuid.getnode().to_bytes(6, "little")).hexdigest() -def get_table_size(table_name): +def get_table_size(table_name) -> str: from django.db import connection query = ( @@ -430,16 +430,28 @@ def get_table_size(table_name): ) cursor = connection.cursor() cursor.execute(query) - return dict_from_cursor_fetchall(cursor) + return dict_from_cursor_fetchall(cursor)[0]["size"] -def get_table_approx_count(table_name): +def get_table_approx_count(table_name) -> str: from django.db import connection query = f"SELECT reltuples::BIGINT as \"approx_count\" FROM pg_class WHERE relname = '{table_name}'" cursor = connection.cursor() cursor.execute(query) - return dict_from_cursor_fetchall(cursor) + return compact_number(dict_from_cursor_fetchall(cursor)[0]["approx_count"]) + + +def compact_number(value: Union[int, float]) -> str: + """Return a number in a compact format, with a SI suffix if applicable. + Client-side equivalent: utils.tsx#compactNumber. + """ + value = float("{:.3g}".format(value)) + magnitude = 0 + while abs(value) >= 1000: + magnitude += 1 + value /= 1000.0 + return "{:f}".format(value).rstrip("0").rstrip(".") + ["", "K", "M", "B", "T", "P", "E", "Z", "Y"][magnitude] def is_postgres_alive() -> bool: diff --git a/posthog/views.py b/posthog/views.py index 69a4554f17176..a911accd13b99 100644 --- a/posthog/views.py +++ b/posthog/views.py @@ -118,27 +118,25 @@ def system_status(request): "value": f"{postgres_version // 10000}.{(postgres_version // 100) % 100}.{postgres_version % 100}", } ) - event_table_count = get_table_approx_count(Event._meta.db_table)[0]["approx_count"] - event_table_size = get_table_size(Event._meta.db_table)[0]["size"] + event_table_count = get_table_approx_count(Event._meta.db_table) + event_table_size = get_table_size(Event._meta.db_table) - element_table_count = get_table_approx_count(Element._meta.db_table)[0]["approx_count"] - element_table_size = get_table_size(Element._meta.db_table)[0]["size"] + element_table_count = get_table_approx_count(Element._meta.db_table) + element_table_size = get_table_size(Element._meta.db_table) - session_recording_event_table_count = get_table_approx_count(SessionRecordingEvent._meta.db_table)[0][ - "approx_count" - ] - session_recording_event_table_size = get_table_size(SessionRecordingEvent._meta.db_table)[0]["size"] + session_recording_event_table_count = get_table_approx_count(SessionRecordingEvent._meta.db_table) + session_recording_event_table_size = get_table_size(SessionRecordingEvent._meta.db_table) metrics.append( - {"metric": "Postgres elements table size", "value": f"~{element_table_count} rows (~{element_table_size})"} + {"metric": "Postgres elements table size", "value": f"{element_table_count} rows (~{element_table_size})"} ) metrics.append( - {"metric": "Postgres events table size", "value": f"~{event_table_count} rows (~{event_table_size})"} + {"metric": "Postgres events table size", "value": f"{event_table_count} rows (~{event_table_size})"} ) metrics.append( { "metric": "Postgres session recording table size", - "value": f"~{session_recording_event_table_count} rows (~{session_recording_event_table_size})", + "value": f"{session_recording_event_table_count} rows (~{session_recording_event_table_size})", } )