From 687e6fcdffefd23dfaa59e8f5597bed665e16724 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 19 Mar 2026 19:15:47 +0100 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 | 8 +++-- 4 files changed, 46 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 ce658e3..e8c3eaf 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 d769a23..4b930cc 100644 --- a/app/views/readouts/_form.html.erb +++ b/app/views/readouts/_form.html.erb @@ -11,9 +11,7 @@ <%= readout.quantity.relative_pathname(@superquantity) %> - <%= 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.hidden_field :quantity_id %> diff --git a/db/migrate/20250121230456_create_readouts.rb b/db/migrate/20250121230456_create_readouts.rb index 6a9b460..2e49aff 100644 --- a/db/migrate/20250121230456_create_readouts.rb +++ b/db/migrate/20250121230456_create_readouts.rb @@ -1,10 +1,14 @@ class CreateReadouts < ActiveRecord::Migration[7.2] def change create_table :readouts do |t| - t.references :user, null: false, foreign_key: true + # Reference :user through :quantity (:measurement may be NULL). + t.references :measurement, 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 + # Move to Measurement? #t.references :collector, foreign_key: true #t.references :device, foreign_key: true