Converti unità di misura


10

Cercando di calcolare l'unità di misura più adatta per un elenco di sostanze in cui le sostanze sono fornite in volumi di unità diversi (ma compatibili).

Tabella di conversione unità

La tabella di conversione delle unità memorizza varie unità e il modo in cui tali unità sono correlate:

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

L'ordinamento per coefficiente mostra che parent_idcollega un'unità figlio al suo superiore numerico.

Questa tabella può essere creata in PostgreSQL usando:

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)
)

Dovrebbe esserci una chiave esterna da parent_ida id.

Tabella delle sostanze

La tabella delle sostanze elenca quantità specifiche di sostanze. Per esempio:

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

La tabella potrebbe assomigliare a:

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)
)

Problema

Come creeresti una query che trova una misura per rappresentare la somma delle sostanze usando il minor numero di cifre che ha un numero intero (e facoltativamente un componente reale)?

Ad esempio, come restituiresti:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Ma no:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Perché 112 ha meno cifre reali di 1,12 e 112 è minore di 1120. Tuttavia, in alcune situazioni l'uso di cifre reali è più breve, come 1,1 litri contro 110 centilitri.

Principalmente, ho problemi a scegliere l'unità corretta in base alla relazione ricorsiva.

Codice sorgente

Finora ho (ovviamente non funzionante):

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

idee

Ciò richiede l'utilizzo del registro 10 per determinare il numero di cifre?

vincoli

Le unità non hanno tutti un potere di dieci. Ad esempio: http://unitsofmeasure.org/ucum-essence.xml


3
@mustaccio Ho avuto lo stesso identico problema nel mio posto precedente, su un sistema molto produttivo. Lì dovevamo calcolare gli importi utilizzati in una cucina per la consegna di alimenti.
dezso,

2
Ricordo un CTE ricorsivo di almeno due livelli. Penso di aver prima calcolato le somme con l'unità più piccola che è stata visualizzata nell'elenco per la sostanza data, quindi l'ho convertita nell'unità più grande che ha ancora una parte intera diversa da zero.
dezso,

1
Tutte le unità sono convertibili con potenze di 10? Il tuo elenco di unità è completo?
Erwin Brandstetter,

Risposte:


2

Sembra brutto:

  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);

ma sembra fare il trucco:

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

Non hai davvero bisogno della relazione genitore-figlio nella unit_conversiontabella, perché le unità della stessa famiglia sono naturalmente correlate l'una all'altra dall'ordine di coefficient, purché tu abbia identificato la famiglia.


2

Penso che questo possa essere ampiamente semplificato.

1. Modifica unit_conversiontabella

Oppure, se non è possibile modificare la tabella, è sufficiente aggiungere la colonna exp10per "base esponente 10", che coincide con il numero di cifre da spostare nel sistema decimale:

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. Funzione di scrittura

per calcolare il numero di posizioni da spostare a sinistra o a destra:

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. Query

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.

Spiegare:

  • UNISCITI alle tabelle delle sostanze e delle unità.
  • Calcola il numero ideale di posizioni da spostare con la funzione f_shift_comma()dall'alto.
  • SINISTRA UNISCITI nuovamente alla tabella delle unità per trovare le unità vicine all'ottimale.
  • Scegli l'unità più vicina con DISTINCT ON ()e ORDER BY.
  • Se non viene trovata un'unità migliore, torna a quello che avevamo COALESCE().
  • Questo dovrebbe coprire tutti i casi angolari ed essere piuttosto veloce .

-> Demo SQLfiddle .


1
@DaveJarvis: E lì pensavo di aver coperto tutto ... questo dettaglio sarebbe stato davvero utile nella domanda altrimenti attentamente elaborata.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.