Convertir les unités de mesure

10

Vous cherchez à calculer l'unité de mesure la plus appropriée pour une liste de substances où les substances sont données dans des volumes unitaires différents (mais compatibles).

Tableau de conversion des unités

La table de conversion des unités stocke différentes unités et comment ces unités sont liées:

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

Le tri par coefficient montre que le parent_idlien entre une unité enfant et son supérieur numérique.

Cette table peut être créée dans PostgreSQL en utilisant:

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

Il doit y avoir une clé étrangère de parent_idà id.

Tableau des substances

Le tableau des substances répertorie des quantités spécifiques de substances. Par exemple:

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

Le tableau pourrait ressembler à:

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

Problème

Comment pourriez-vous créer une requête qui trouve une mesure pour représenter la somme des substances en utilisant le moins de chiffres qui a un nombre entier (et éventuellement une composante réelle)?

Par exemple, comment retourneriez-vous:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Mais non:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Parce que 112 a moins de chiffres réels que 1,12 et 112 est plus petit que 1120. Pourtant, dans certaines situations, l'utilisation de chiffres réels est plus courte - comme 1,1 litre contre 110 centilitres.

Surtout, j'ai du mal à choisir l'unité correcte en fonction de la relation récursive.

Code source

Jusqu'à présent, j'ai (évidemment ne fonctionne pas):

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

Des idées

Cela nécessite-t-il d'utiliser le journal 10 pour déterminer le nombre de chiffres?

Contraintes

Les unités ne sont pas toutes en puissance de dix. Par exemple: http://unitsofmeasure.org/ucum-essence.xml

Dave Jarvis
la source
3
@mustaccio J'ai eu exactement le même problème à ma place précédente, sur un système très productif. Là, nous avons dû calculer les quantités utilisées dans une cuisine de livraison de nourriture.
dezso
2
Je me souviens d'un CTE récursif d'au moins deux niveaux. Je pense que j'ai d'abord calculé les sommes avec la plus petite unité qui est apparue dans la liste pour la substance donnée, puis je l'ai convertie en la plus grande unité ayant toujours une partie entière non nulle.
dezso
1
Toutes les unités sont-elles convertibles avec des puissances de 10? Votre liste d'unités est-elle complète?
Erwin Brandstetter

Réponses:

2

Cela a l'air moche:

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

mais semble faire l'affaire:

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

Vous n'avez pas vraiment besoin de la relation parent-enfant dans le unit_conversiontableau, car les unités d'une même famille sont naturellement liées les unes aux autres par ordre de coefficient, tant que la famille est identifiée.

mustaccio
la source
2

Je pense que cela peut être largement simplifié.

1. Modifier le unit_conversiontableau

Ou, si vous ne pouvez pas modifier le tableau, ajoutez simplement la colonne exp10"exposant base 10", qui coïncide avec le nombre de chiffres à décaler dans le système décimal:

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. Fonction d'écriture

pour calculer le nombre de positions à décaler vers la gauche ou la droite:

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. Requête

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.

Explique:

  • JOINDRE les tables de substances et d'unités.
  • Calculez le nombre idéal de positions à décaler avec la fonction f_shift_comma()d'en haut.
  • JOINDRE À GAUCHE à la table des unités une deuxième fois pour trouver des unités proches de l'optimum.
  • Choisissez l'unité la plus proche avec DISTINCT ON ()et ORDER BY.
  • Si aucune meilleure unité n'est trouvée, revenez à ce que nous avions avec COALESCE().
  • Cela devrait couvrir tous les cas d'angle et être assez rapide .

-> Démo SQLfiddle .

Erwin Brandstetter
la source
1
@DaveJarvis: Et là, je pensais avoir tout couvert ... ce détail aurait été très utile dans la question autrement soigneusement conçue.
Erwin Brandstetter