From 3ac3a6f13c8689a945348835e35fd58c31c3afb8 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 25 Apr 2026 16:39:54 +0200 Subject: [PATCH] Drop Readout.value decimal type in favor of float --- DESIGN.md | 34 ++++++++++++++++++++ app/helpers/application_helper.rb | 6 +++- app/views/readouts/_form.html.erb | 4 +-- db/migrate/20250121230456_create_readouts.rb | 4 ++- db/schema.rb | 3 +- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..0f6c1ba --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,34 @@ +DESIGN +====== + +Below is a list of design decisions. The justification is to be consulted +whenever a change is considered, to avoid regressions. + +### Data type for DB storage of numeric values (`decimal` vs `float`) + +* among database engines supported (by Rails), SQLite offers storage of + `decimal` data type with the lowest precision, equal to the precision of + `REAL` type (double precision float value, IEEE 754), but in a floating point + format, + * decimal types in other database engines offer greater precision, but store + data in a fixed point format, +* biology-related values differ by several orders of magnitude; storing them in + fixed point format would only make sense if required precision would be + greater than that offered by floating point format, + * even then, fixed point would mean either bigger memory requirements or + worse precision for numbers close to scale limit, + * for a fixed point format to use the same 8 bytes of storage as IEEE + 754, precision would need to be limited to 18 digits (4 bytes/9 digits) + and scale approximately half of that - 9, + * double precision floating point guarantees 15 digits of precision, which + is more than enough for all expected use cases, + * single precision floating point only guarntees 6 digits of precision, + which is estimated to be too low for some use cases (e.g. storing + latitude/longitude with a resolution grater than 100m) +* double precision floating point (IEEE 754) is a standard that ensures + compatibility with all database engines, + * the same data format is used internally by Ruby as a `Float`; it + guarantees no conversions between storage and computation, + * as a standard with hardware implementations ensures both: computing + efficiency and hardware/3rd party library compatibility as opposed to Ruby + custom `BigDecimal` type diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 39a7430..c78608e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -102,13 +102,17 @@ module ApplicationHelper def number_field(method, options = {}) attr_type = object.type_for_attribute(method) - if attr_type.type == :decimal + case attr_type.type + when :decimal options[:value] = object.public_send(method)&.to_scientific options[:step] ||= BigDecimal(10).power(-attr_type.scale) options[:max] ||= BigDecimal(10).power(attr_type.precision - attr_type.scale) - options[:step] options[:min] = options[:min] == :step ? options[:step] : options[:min] options[:min] ||= -options[:max] + options[:size] ||= attr_type.precision / 2 + when :float + options[:size] ||= 6 end super end diff --git a/app/views/readouts/_form.html.erb b/app/views/readouts/_form.html.erb index c8be1b8..79d1293 100644 --- a/app/views/readouts/_form.html.erb +++ b/app/views/readouts/_form.html.erb @@ -7,9 +7,7 @@ <%= form.hidden_field :quantity_id %> - <%= form.number_field :value, required: true, - size: readout.type_for_attribute(:value).precision / 2, - autofocus: readout_counter == 0 %> + <%= form.number_field :value, required: true, autofocus: readout_counter == 0 %> <%= form.collection_select :unit_id, @user_units, :id, diff --git a/db/migrate/20250121230456_create_readouts.rb b/db/migrate/20250121230456_create_readouts.rb index 6a9b460..e9428d9 100644 --- a/db/migrate/20250121230456_create_readouts.rb +++ b/db/migrate/20250121230456_create_readouts.rb @@ -3,8 +3,10 @@ class CreateReadouts < ActiveRecord::Migration[7.2] create_table :readouts do |t| t.references :user, null: false, foreign_key: true t.references :quantity, null: false, foreign_key: true + # :category + :value + :unit as a separate table? (NumericValue, TextValue) + t.integer :category, null: false, default: 0 + t.float :value, null: false, limit: Float::MANT_DIG t.references :unit, foreign_key: true - t.decimal :value, null: false, precision: 30, scale: 15 #t.references :collector, foreign_key: true #t.references :device, foreign_key: true diff --git a/db/schema.rb b/db/schema.rb index 3975ec5..3205384 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -28,8 +28,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_01_21_230456) do create_table "readouts", charset: "utf8mb4", collation: "utf8mb4_0900_as_ci", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "quantity_id", null: false + t.integer "category", default: 0, null: false + t.float "value", limit: 53, null: false t.bigint "unit_id" - t.decimal "value", precision: 30, scale: 15, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["quantity_id", "created_at"], name: "index_readouts_on_quantity_id_and_created_at", unique: true