Ölçü birimlerini dönüştürme


10

Maddelerin farklı (ancak uyumlu) birim hacimlerde verildiği bir madde listesi için en uygun ölçü birimini hesaplamak isteyen.

Birim Dönüşüm Tablosu

Birim dönüşüm tablosu çeşitli birimleri ve bu birimlerin nasıl bir ilişki içerdiğini depolar:

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

Katsayıya göre sıralama, parent_idbir alt birimi sayısal üst noktasına bağladığını gösterir .

Bu tablo PostgreSQL'de şunlar kullanılarak oluşturulabilir:

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

Bir yabancı anahtar olmalıdır parent_idiçin id.

Madde Tablosu

Madde Tablosu belirli miktarlarda maddeyi listeler. Örneğin:

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

Tablo aşağıdakine benzeyebilir:

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

Sorun

Tam sayıya (ve isteğe bağlı olarak gerçek bileşene) sahip en az haneyi kullanan maddelerin toplamını temsil eden bir ölçüm bulan bir sorguyu nasıl oluşturabilirsiniz?

Örneğin, nasıl geri dönersiniz?

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Ama değil:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Çünkü 112, 1.12'den daha az gerçek basamağa sahiptir ve 112, 1120'den daha küçüktür. Ancak, bazı durumlarda gerçek rakamların kullanılması daha kısadır - 1.1 litre ve 110 centilitre gibi.

Çoğunlukla, özyinelemeli ilişkiye dayalı olarak doğru birimi seçmede sorunlar yaşıyorum.

Kaynak kodu

Şimdiye kadar (açıkçası çalışmayan) var:

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

fikirler

Bu basamak sayısını belirlemek için log 10 kullanılmasını gerektirir mi?

Kısıtlamalar

Birimlerin hepsi on kişilik değil. Örneğin: http://unitsofmeasure.org/ucum-essence.xml


3
@mustaccio Aynı problemi önceki yerimde, çok üretim sistemindeydim. Orada bir yemek dağıtım mutfağında kullanılan miktarları hesaplamak zorunda kaldık.
dezso

2
En az iki seviyeli özyinelemeli bir CTE hatırlıyorum. Sanırım ilk olarak verilen madde için listede ortaya çıkan en küçük birim ile toplamları hesapladım ve daha sonra hala sıfır olmayan tamsayı kısmına sahip en büyük birime dönüştürdüm.
dezso

1
Tüm birimler 10'luk güçlerle dönüştürülebilir mi? Birim listeniz tamamlandı mı?
Erwin Brandstetter

Yanıtlar:


2

Bu çirkin görünüyor:

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

ama hile yapıyor gibi görünüyor:

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

unit_conversionTabloda ebeveyn-çocuk ilişkisine gerçekten ihtiyacınız yoktur , çünkü aynı ailedeki birimler coefficient, ailenizi tanımladığınız sürece doğal olarak birbirleriyle ilişkilidir .


2

Bence bu büyük ölçüde basitleştirilebilir.

1. unit_conversionTabloyu değiştirin

Veya tabloyu değiştiremiyorsanız exp10, ondalık sistemde kaydırılacak basamak sayısı ile çakışan "üs tabanı 10" sütununu eklemeniz yeterlidir:

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. Yazma işlevi

sola veya sağa kaydırılacak konum sayısını hesaplamak için:

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

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.

Açıklamak:

  • Madde ve birim tablolarına KATILIN.
  • f_shift_comma()Yukarıdan işlev ile kaydırmak için ideal konum sayısını hesaplayın .
  • LEFT, optimum seviyeye yakın üniteleri bulmak için ünite tablosuna ikinci kez KATILIN.
  • En yakın birimini seçin DISTINCT ON ()ve ORDER BY.
  • Daha iyi bir birim bulunamazsa, sahip olduğumuz şeye geri dönün COALESCE().
  • Bu, tüm köşe vakalarını kapsamalı ve oldukça hızlı olmalıdır .

-> SQLfiddle demosu.


1
@DaveJarvis: Ve orada her şeyi kapsadığımı sanıyordum ... bu detay aksi takdirde özenle hazırlanmış bir soruda gerçekten yararlı olurdu.
Erwin Brandstetter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.