Convertir unidades de medida

10

Buscando calcular la unidad de medida más adecuada para una lista de sustancias donde las sustancias se administran en volúmenes de unidades diferentes (pero compatibles).

Tabla de conversión de unidades

La tabla de conversión de unidades almacena varias unidades y cómo se relacionan esas unidades:

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

Ordenar por el coeficiente muestra que parent_idvincula una unidad secundaria a su superior numérico.

Esta tabla se puede crear en 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)
)

Debe haber una clave foránea de parent_ida id.

Tabla de sustancias

La tabla de sustancias enumera cantidades específicas de sustancias. Por ejemplo:

 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 tabla podría parecerse 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

¿Cómo crearía una consulta que encuentre una medida para representar la suma de las sustancias utilizando la menor cantidad de dígitos que tenga un número entero (y opcionalmente un componente real)?

Por ejemplo, ¿cómo regresarías?

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Pero no:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Debido a que 112 tiene menos dígitos reales que 1.12 y 112 es menor que 1120. Sin embargo, en ciertas situaciones, el uso de dígitos reales es más corto, como 1.1 litros frente a 110 centilitros.

Principalmente, tengo problemas para elegir la unidad correcta en función de la relación recursiva.

Código fuente

Hasta ahora tengo (obviamente no funciona):

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

Ideas

¿Requiere esto usar el registro 10 para determinar la cantidad de dígitos?

Restricciones

Las unidades no están todas en potencias de diez. Por ejemplo: http://unitsofmeasure.org/ucum-essence.xml

Dave Jarvis
fuente
3
@mustaccio Tuve exactamente el mismo problema en mi lugar anterior, en un sistema muy productivo. Allí tuvimos que calcular las cantidades utilizadas en una cocina de entrega de alimentos.
dezso
2
Recuerdo un CTE recursivo de al menos dos niveles. Creo que primero calculé las sumas con la unidad más pequeña que apareció en la lista para la sustancia dada y luego la convertí en la unidad más grande que todavía tiene una parte entera distinta de cero.
dezso
1
¿Todas las unidades son convertibles con potencias de 10? ¿Está completa tu lista de unidades?
Erwin Brandstetter

Respuestas:

2

Esto se ve feo:

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

pero parece hacer el truco:

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

Realmente no necesita la relación padre-hijo en la unit_conversiontabla, porque las unidades de la misma familia están naturalmente relacionadas entre sí por orden de coefficient, siempre y cuando haya identificado a la familia.

mustaccio
fuente
2

Creo que esto se puede simplificar en gran medida.

1. Modificar unit_conversiontabla

O, si no puede modificar la tabla, simplemente agregue la columna exp10para "exponente base 10", que coincide con el número de dígitos que se desplazarán en el sistema decimal:

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. Función de escritura

para calcular el número de posiciones para desplazarse hacia la izquierda o hacia la derecha:

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

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:

  • ÚNASE a las tablas de sustancias y unidades.
  • Calcule el número ideal de posiciones para cambiar con la función f_shift_comma()desde arriba.
  • IZQUIERDA ÚNETE a la tabla de unidades por segunda vez para encontrar unidades cercanas al óptimo.
  • Elija la unidad más cercana con DISTINCT ON ()y ORDER BY.
  • Si no se encuentra una unidad mejor, recurra a lo que teníamos COALESCE().
  • Esto debería cubrir todos los casos de esquina y ser bastante rápido .

-> Demostración SQLfiddle .

Erwin Brandstetter
fuente
1
@DaveJarvis: Y allí pensé que había cubierto todo ... este detalle habría sido de gran ayuda en la pregunta cuidadosamente elaborada.
Erwin Brandstetter