Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lvgl as lv

from mpos import Activity, Intent, AppearanceManager, AppManager, SettingActivity, SettingsActivity, TimeZone
from mpos import Activity, Intent, AppearanceManager, AppManager, NumberFormat, SettingActivity, SettingsActivity, TimeZone

from bootloader import ResetIntoBootloader
from calibrate_imu import CalibrateIMUActivity
Expand Down Expand Up @@ -82,6 +82,7 @@ def getIntent(self):
{"title": "Light/Dark Theme", "key": "theme_light_dark", "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")], "changed_callback": self.theme_changed},
{"title": "Theme Color", "key": "theme_primary_color", "placeholder": "HTML hex color, like: EC048C", "ui": "dropdown", "ui_options": theme_colors, "changed_callback": self.theme_changed, "default_value": AppearanceManager.DEFAULT_PRIMARY_COLOR},
{"title": "Timezone", "key": "timezone", "ui": "dropdown", "ui_options": [(tz, tz) for tz in TimeZone.get_timezones()], "changed_callback": lambda *args: TimeZone.refresh_timezone_preference()},
{"title": "Number Format", "key": "number_format", "ui": "dropdown", "ui_options": NumberFormat.get_format_options(), "changed_callback": lambda *args: NumberFormat.refresh_preference(), "default_value": "comma_dot"},
# Advanced settings, alphabetically:
{"title": "Auto Start App", "key": "auto_start_app", "ui": "radiobuttons", "ui_options": [(app.name, app.fullname) for app in AppManager.get_app_list()]},
{"title": "Check IMU Calibration", "key": "check_imu_calibration", "ui": "activity", "activity_class": CheckIMUCalibrationActivity},
Expand Down
6 changes: 5 additions & 1 deletion internal_filesystem/lib/mpos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
from .camera_manager import CameraManager
from .sensor_manager import SensorManager
from .time_zone import TimeZone
from .number_format import NumberFormat
from .device_info import DeviceInfo
from .build_info import BuildInfo

# Battery manager (imported early for UI dependencies)
from .battery_manager import BatteryManager
from .webserver.webserver import WebServer
try:
from .webserver.webserver import WebServer
except ImportError:
WebServer = None # _webrepl not available on desktop/Unix builds

# Common activities
from .app.activities.chooser import ChooserActivity
Expand Down
111 changes: 111 additions & 0 deletions internal_filesystem/lib/mpos/number_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from . import config


NUMBER_FORMAT_MAP = {
"comma_dot": (".", ","), # 1,234.56 US/UK
"dot_comma": (",", "."), # 1.234,56 Europe
"space_comma": (",", " "), # 1 234,56 French
"apos_dot": (".", "'"), # 1'234.56 Swiss
"under_dot": (".", "_"), # 1_234.56 Tech
"none_dot": (".", ""), # 1234.56 No thousands
"none_comma": (",", ""), # 1234,56 No thousands
}

DEFAULT_FORMAT = "comma_dot"


class NumberFormat:
"""Number formatting utility using the system number format preference."""

number_format_preference = None

@staticmethod
def refresh_preference():
"""Refresh the number format preference from SharedPreferences."""
NumberFormat.number_format_preference = config.SharedPreferences(
"com.micropythonos.settings"
).get_string("number_format")
if not NumberFormat.number_format_preference:
NumberFormat.number_format_preference = DEFAULT_FORMAT

@staticmethod
def get_separators():
"""Return (decimal_sep, thousands_sep) for the current preference."""
if NumberFormat.number_format_preference is None:
NumberFormat.refresh_preference()
return NUMBER_FORMAT_MAP.get(
NumberFormat.number_format_preference,
NUMBER_FORMAT_MAP[DEFAULT_FORMAT],
)

@staticmethod
def format_number(value, decimals=None):
"""Format a number using the current number format preference.

Args:
value: int or float to format.
decimals: number of decimal places (None = auto for ints, strip trailing zeros for floats).

Returns:
Formatted string.
"""
dec_sep, thou_sep = NumberFormat.get_separators()

if isinstance(value, int) and decimals is None:
negative = value < 0
s = str(abs(value))
s = _insert_thousands(s, thou_sep)
return ("-" + s) if negative else s

# Float formatting
if decimals is None:
decimals = 2
s = "{:.{}f}".format(float(value), decimals)

negative = s.startswith("-")
if negative:
s = s[1:]

# Split on the Python decimal point
if "." in s:
int_part, frac_part = s.split(".")
# Strip trailing zeros from fractional part
frac_part = frac_part.rstrip("0")
else:
int_part = s
frac_part = ""

int_part = _insert_thousands(int_part, thou_sep)

if frac_part:
result = int_part + dec_sep + frac_part
else:
result = int_part

return ("-" + result) if negative else result

@staticmethod
def get_format_options():
"""Return a list of (label, key) tuples for the settings dropdown."""
return [
("1,234.56 (US/UK)", "comma_dot"),
("1.234,56 (Europe)", "dot_comma"),
("1 234,56 (French)", "space_comma"),
("1'234.56 (Swiss)", "apos_dot"),
("1_234.56 (Tech)", "under_dot"),
("1234.56 (No separator)", "none_dot"),
("1234,56 (No separator)", "none_comma"),
]


def _insert_thousands(int_str, separator):
"""Insert thousands separator into an integer string."""
if not separator or len(int_str) <= 3:
return int_str
parts = []
while len(int_str) > 3:
parts.append(int_str[-3:])
int_str = int_str[:-3]
parts.append(int_str)
parts.reverse()
return separator.join(parts)
82 changes: 82 additions & 0 deletions tests/test_number_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import unittest
from mpos.number_format import NumberFormat, NUMBER_FORMAT_MAP, DEFAULT_FORMAT


class TestNumberFormat(unittest.TestCase):

def setUp(self):
NumberFormat.number_format_preference = DEFAULT_FORMAT

def test_default_is_us_format(self):
self.assertEqual(NumberFormat.get_separators(), (".", ","))

def test_format_int_default(self):
self.assertEqual(NumberFormat.format_number(1234), "1,234")
self.assertEqual(NumberFormat.format_number(0), "0")
self.assertEqual(NumberFormat.format_number(999), "999")
self.assertEqual(NumberFormat.format_number(1000), "1,000")
self.assertEqual(NumberFormat.format_number(1234567), "1,234,567")

def test_format_negative_int(self):
self.assertEqual(NumberFormat.format_number(-1234), "-1,234")
self.assertEqual(NumberFormat.format_number(-5), "-5")

def test_format_float_default(self):
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1,234.56")
self.assertEqual(NumberFormat.format_number(1234.50, 2), "1,234.5")
self.assertEqual(NumberFormat.format_number(1234.00, 2), "1,234")
self.assertEqual(NumberFormat.format_number(0.5, 2), "0.5")

def test_format_europe(self):
NumberFormat.number_format_preference = "dot_comma"
self.assertEqual(NumberFormat.format_number(1234), "1.234")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1.234,56")

def test_format_french(self):
NumberFormat.number_format_preference = "space_comma"
self.assertEqual(NumberFormat.format_number(1234), "1 234")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1 234,56")

def test_format_swiss(self):
NumberFormat.number_format_preference = "apos_dot"
self.assertEqual(NumberFormat.format_number(1234), "1'234")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1'234.56")

def test_format_tech(self):
NumberFormat.number_format_preference = "under_dot"
self.assertEqual(NumberFormat.format_number(1234), "1_234")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1_234.56")

def test_format_no_thousands_dot(self):
NumberFormat.number_format_preference = "none_dot"
self.assertEqual(NumberFormat.format_number(1234567), "1234567")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1234.56")

def test_format_no_thousands_comma(self):
NumberFormat.number_format_preference = "none_comma"
self.assertEqual(NumberFormat.format_number(1234567), "1234567")
self.assertEqual(NumberFormat.format_number(1234.56, 2), "1234,56")

def test_strip_trailing_zeros(self):
NumberFormat.number_format_preference = DEFAULT_FORMAT
self.assertEqual(NumberFormat.format_number(1.10, 2), "1.1")
self.assertEqual(NumberFormat.format_number(1.00, 2), "1")
self.assertEqual(NumberFormat.format_number(1.00, 8), "1")

def test_large_number(self):
self.assertEqual(NumberFormat.format_number(100000000), "100,000,000")

def test_get_format_options_returns_list(self):
options = NumberFormat.get_format_options()
self.assertIsInstance(options, list)
self.assertTrue(len(options) == len(NUMBER_FORMAT_MAP))
for label, key in options:
self.assertIn(key, NUMBER_FORMAT_MAP)

def test_unknown_preference_falls_back_to_default(self):
NumberFormat.number_format_preference = "nonexistent"
self.assertEqual(NumberFormat.get_separators(), (".", ","))


if __name__ == "__main__":
unittest.main()
Loading