Maßeinheiten umrechnen

10

Suchen Sie nach der am besten geeigneten Maßeinheit für eine Liste von Stoffen, bei denen die Stoffe in unterschiedlichen (aber kompatiblen) Volumeneinheiten angegeben sind.

Einheitenumrechnungstabelle

In der Einheitenumrechnungstabelle werden verschiedene Einheiten gespeichert und wie sich diese Einheiten verhalten:

id  unit          coefficient                 parent_id
36  "microlitre"  0.0000000010000000000000000 37
37  "millilitre"  0.0000010000000000000000000 5
 5  "centilitre"  0.0000100000000000000000000 18
18  "decilitre"   0.0001000000000000000000000 34
34  "litre"       0.0010000000000000000000000 19
19  "dekalitre"   0.0100000000000000000000000 29
29  "hectolitre"  0.1000000000000000000000000 33
33  "kilolitre"   1.0000000000000000000000000 35
35  "megalitre"   1000.0000000000000000000000 0

Das Sortieren nach dem Koeffizienten zeigt, dass parent_ideine untergeordnete Einheit mit ihrem numerischen Vorgesetzten verknüpft ist.

Diese Tabelle kann in PostgreSQL erstellt werden mit:

CREATE TABLE unit_conversion (
  id serial NOT NULL, -- Primary key.
  unit text NOT NULL, -- Unit of measurement name.
  coefficient numeric(30,25) NOT NULL DEFAULT 0, -- Conversion value.
  parent_id integer NOT NULL DEFAULT 0, -- Relates units in order of increasing measurement volume.
  CONSTRAINT pk_unit_conversion PRIMARY KEY (id)
)

Es sollte einen Fremdschlüssel von parent_idbis geben id.

Substanztabelle

In der Stofftabelle sind bestimmte Stoffmengen aufgeführt. Beispielsweise:

 id  unit          label     quantity
 1   "microlitre"  mercury   5
 2   "millilitre"  water     500
 3   "centilitre"  water     2
 4   "microlitre"  mercury   10
 5   "millilitre"  water     600

Die Tabelle könnte ähneln:

CREATE TABLE substance (
  id bigserial NOT NULL, -- Uniquely identifies this row.
  unit text NOT NULL, -- Foreign key to unit conversion.
  label text NOT NULL, -- Name of the substance.
  quantity numeric( 10, 4 ) NOT NULL, -- Amount of the substance.
  CONSTRAINT pk_substance PRIMARY KEY (id)
)

Problem

Wie würden Sie eine Abfrage erstellen, die eine Messung findet, um die Summe der Substanzen mit den wenigsten Ziffern darzustellen, die eine ganze Zahl (und optional eine reale Komponente) haben?

Wie würden Sie zum Beispiel zurückkehren:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Aber nicht:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Weil 112 weniger reelle Ziffern als 1,12 hat und 112 kleiner als 1120 ist. In bestimmten Situationen ist die Verwendung reeller Ziffern jedoch kürzer - beispielsweise 1,1 Liter gegenüber 110 Zentilitern.

Meistens habe ich Probleme, die richtige Einheit basierend auf der rekursiven Beziehung auszuwählen.

Quellcode

Bisher habe ich (offensichtlich nicht arbeitend):

-- Normalize the quantities
select
  sum( coefficient * quantity ) AS kilolitres
from
  unit_conversion uc,
  substance s
where
  uc.unit = s.unit
group by
  s.label

Ideen

Erfordert dies die Verwendung von Protokoll 10 , um die Anzahl der Ziffern zu bestimmen?

Einschränkungen

Die Einheiten sind nicht alle in Zehnerpotenzen. Zum Beispiel: http://unitsofmeasure.org/ucum-essence.xml

Dave Jarvis
quelle
3
@mustaccio Ich hatte genau das gleiche Problem an meinem vorherigen Platz, auf einem sehr Produktionssystem. Dort mussten wir die Mengen berechnen, die in einer Lebensmittelküche verwendet wurden.
Dekso
2
Ich erinnere mich an einen rekursiven CTE mit mindestens zwei Ebenen. Ich glaube, ich habe zuerst die Summen mit der kleinsten Einheit berechnet, die in der Liste für die gegebene Substanz aufgetaucht ist, und sie dann in die größte Einheit umgerechnet, die noch einen ganzzahligen Teil ungleich Null hat.
Dekso
1
Sind alle Einheiten mit Potenzen von 10 konvertierbar? Ist Ihre Liste der Einheiten vollständig?
Erwin Brandstetter

Antworten:

2

Das sieht hässlich aus:

  with uu(unit, coefficient, u_ord) as (
    select
     unit, 
     coefficient,
     case 
      when log(u.coefficient) < 0 
      then floor (log(u.coefficient)) 
      else ceil(log(u.coefficient)) 
     end u_ord
    from
     unit_conversion u 
  ),
  norm (label, norm_qty) as (
   select
    s.label,
    sum( uc.coefficient * s.quantity ) AS norm_qty
  from
    unit_conversion uc,
    substance s
  where
    uc.unit = s.unit
  group by
    s.label
  ),
  norm_ord (label, norm_qty, log, ord) as (
   select 
    label,
    norm_qty, 
    log(t.norm_qty) as log,
    case 
     when log(t.norm_qty) < 0 
     then floor(log(t.norm_qty)) 
     else ceil(log(t.norm_qty)) 
    end ord
   from norm t
  )
  select
   norm_ord.label,
   norm_ord.norm_qty,
   norm_ord.norm_qty / uu.coefficient val,
   uu.unit
  from 
   norm_ord,
   uu where uu.u_ord = 
     (select max(uu.u_ord) 
      from uu 
      where mod(norm_ord.norm_qty , uu.coefficient) = 0);

aber scheint den Trick zu tun:

|   LABEL | NORM_QTY | VAL |       UNIT |
-----------------------------------------
| mercury |   1.5e-8 |  15 | microlitre |
|   water |  0.00112 | 112 | centilitre |

Sie brauchen die Eltern-Kind-Beziehung in der unit_conversionTabelle nicht wirklich , da die Einheiten in derselben Familie natürlich in der Reihenfolge von miteinander verwandt sind coefficient, solange Sie die Familie identifiziert haben.

mustaccio
quelle
2

Ich denke, das kann weitgehend vereinfacht werden.

1. Ändern Sie die unit_conversionTabelle

Wenn Sie die Tabelle nicht ändern können, fügen Sie einfach die Spalte exp10für "Exponentenbasis 10" hinzu, die mit der Anzahl der im Dezimalsystem zu verschiebenden Stellen übereinstimmt:

CREATE TABLE unit_conversion(
   unit text PRIMARY KEY
  ,exp10 int
);

INSERT INTO unit_conversion VALUES
     ('microlitre', 0)
    ,('millilitre', 3)
    ,('centilitre', 4)
    ,('litre',      6)
    ,('hectolitre', 8)
    ,('kilolitre',  9)
    ,('megalitre',  12)
    ,('decilitre',  5);

2. Schreibfunktion

um die Anzahl der Positionen zu berechnen, die nach links oder rechts verschoben werden sollen:

CREATE OR REPLACE FUNCTION f_shift_comma(n numeric)
  RETURNS int LANGUAGE SQL IMMUTABLE AS
$$
SELECT CASE WHEN ($1 % 1) = 0 THEN                    -- no fractional digits
          CASE WHEN ($1 % 10) = 0 THEN 0              -- no trailing 0, don't shift
          ELSE length(rtrim(trunc($1, 0)::text, '0')) -- trunc() because numeric can be 1.0
                   - length(trunc($1, 0)::text)       -- trailing 0, shift right .. negative
          END
       ELSE                                           -- fractional digits
          length(rtrim(($1 % 1)::text, '0')) - 2      -- shift left .. positive
       END
$$;

3. Abfrage

SELECT DISTINCT ON (substance_id)
       s.substance_id, s.label, s.quantity, s.unit
      ,COALESCE(s.quantity * 10^(u1.exp10 - u2.exp10)::numeric
              , s.quantity)::float8 AS norm_quantity
      ,COALESCE(u2.unit, s.unit) AS norm_unit
FROM   substance s 
JOIN   unit_conversion u1 USING (unit)
LEFT   JOIN unit_conversion u2 ON f_shift_comma(s.quantity) <> 0
                              AND @(u2.exp10 - (u1.exp10 - f_shift_comma(s.quantity))) < 2
                              -- since maximum gap between exp10 in unit table = 3
                              -- adapt to ceil(to max_gap / 2) if you have bigger gaps
ORDER  BY s.substance_id
     , @(u2.exp10 - (u1.exp10 - f_shift_comma(s.quantity))) -- closest unit first
     , u2.exp10    -- smaller unit first to avoid point for ties.

Erklären:

  • Verbinden Sie Stoff- und Einheitentabellen.
  • Berechnen Sie die ideale Anzahl von Positionen, die mit der Funktion f_shift_comma()von oben verschoben werden sollen .
  • LINKS ein zweites Mal mit dem Einheitentisch verbinden, um Einheiten zu finden, die nahe am Optimum liegen.
  • Wählen Sie die nächste Einheit mit DISTINCT ON ()und ORDER BY.
  • Wenn keine bessere Einheit gefunden wird, greifen Sie auf das zurück, was wir hatten COALESCE().
  • Dies sollte alle Eckfälle abdecken und ziemlich schnell sein .

-> SQLfiddle- Demo.

Erwin Brandstetter
quelle
1
@ DaveJarvis: Und da dachte ich, ich hätte alles abgedeckt ... dieses Detail wäre bei der ansonsten sorgfältig ausgearbeiteten Frage wirklich hilfreich gewesen.
Erwin Brandstetter