Converter unidades de medida

10

Procurando calcular a unidade de medida mais adequada para uma lista de substâncias nas quais as substâncias são fornecidas em volumes unitários diferentes (mas compatíveis).

Tabela de conversão de unidades

A tabela de conversão de unidades armazena várias unidades e como essas unidades se relacionam:

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

A classificação pelo coeficiente mostra que parent_idvincula uma unidade filho ao seu numérico superior.

Esta tabela pode ser criada no 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)
)

Deve haver uma chave estrangeira de parent_idpara id.

Tabela de substâncias

A tabela de substâncias lista quantidades específicas de substâncias. Por exemplo:

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

A tabela pode se parecer com:

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

Como você criaria uma consulta que encontre uma medida para representar a soma das substâncias usando o menor número de dígitos que possua um número inteiro (e opcionalmente componente real)?

Por exemplo, como você retornaria:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Mas não:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Como 112 tem menos dígitos reais que 1.12 e 112 é menor que 1120. No entanto, em certas situações, o uso de dígitos reais é menor - como 1,1 litro vs 110 centilitros.

Principalmente, estou tendo problemas para escolher a unidade correta com base na relação recursiva.

Código fonte

Até agora eu tenho (obviamente não trabalhando):

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

Ideias

Isso requer o uso do log 10 para determinar o número de dígitos?

Restrições

As unidades não são todas com potências de dez. Por exemplo: http://unitsofmeasure.org/ucum-essence.xml

Dave Jarvis
fonte
3
@mustaccio Eu tive exatamente o mesmo problema no meu local anterior, em um sistema muito de produção. Lá tivemos que calcular as quantidades usadas em uma cozinha de entrega de alimentos.
Dez
2
Lembro-me de um CTE recursivo de pelo menos dois níveis. Penso que primeiro calculei as somas com a menor unidade que apareceu na lista para a substância especificada e depois a converti na maior unidade ainda com parte inteira diferente de zero.
Dez
11
Todas as unidades são conversíveis com potências de 10? Sua lista de unidades está completa?
Erwin Brandstetter

Respostas:

2

Isso parece feio:

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

mas parece fazer o truque:

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

Você realmente não precisa da relação pai-filho na unit_conversiontabela, porque as unidades da mesma família estão naturalmente relacionadas entre si pela ordem de coefficient, desde que a família seja identificada.

mustaccio
fonte
2

Eu acho que isso pode ser bastante simplificado.

1. unit_conversionTabela de modificação

Ou, se você não pode modificar a tabela, basta adicionar a coluna exp10da "base do expoente 10", que coincide com o número de dígitos a serem deslocados no 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. Função de gravação

para calcular o número de posições para deslocar para a esquerda ou direita:

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.

Explicar:

  • JUNTE tabelas de substâncias e unidades.
  • Calcule o número ideal de posições para mudar com a função f_shift_comma()de cima.
  • ESQUERDA JUNTE-SE à tabela de unidades uma segunda vez para encontrar unidades próximas da ideal.
  • Escolha a unidade mais próxima com DISTINCT ON ()e ORDER BY.
  • Se nenhuma unidade melhor for encontrada, volte ao que tínhamos COALESCE().
  • Isso deve cobrir todos os casos de canto e ser bem rápido .

-> Demonstração do SQLfiddle .

Erwin Brandstetter
fonte
11
@DaveJarvis: E lá eu pensei que tinha coberto tudo ... esse detalhe teria sido realmente útil na pergunta cuidadosamente elaborada.
Erwin Brandstetter