Konversi satuan pengukuran

10

Mencari untuk menghitung unit pengukuran yang paling cocok untuk daftar zat di mana zat diberikan dalam volume satuan yang berbeda (tetapi kompatibel).

Tabel Konversi Satuan

Tabel konversi unit menyimpan berbagai unit dan bagaimana unit tersebut berhubungan:

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

Penyortiran berdasarkan koefisien menunjukkan bahwa parent_idtautan unit anak ke atasan numeriknya.

Tabel ini dapat dibuat di PostgreSQL menggunakan:

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

Harus ada kunci asing dari parent_idke id.

Tabel zat

Tabel Substansi mencantumkan jumlah zat tertentu. Sebagai contoh:

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

Tabelnya mungkin menyerupai:

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

Masalah

Bagaimana Anda membuat kueri yang menemukan pengukuran untuk mewakili jumlah zat menggunakan digit paling sedikit yang memiliki bilangan bulat (dan komponen nyata opsional)?

Misalnya, bagaimana Anda akan kembali:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Tapi tidak:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Karena 112 memiliki digit nyata lebih sedikit dari 1,12 dan 112 lebih kecil dari 1120. Namun dalam situasi tertentu menggunakan digit nyata lebih pendek - seperti 1,1 liter vs 110 centilitre.

Sebagian besar, saya mengalami masalah dalam memilih unit yang benar berdasarkan pada hubungan rekursif.

Kode sumber

Sejauh ini saya punya (jelas tidak bekerja):

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

Ide ide

Apakah ini mengharuskan menggunakan log 10 untuk menentukan jumlah digit?

Kendala

Unit tidak semuanya memiliki kekuatan sepuluh. Misalnya: http://unitsofmeasure.org/ucum-essence.xml

Dave Jarvis
sumber
3
@ustaccio Saya punya masalah yang sama persis di tempat saya sebelumnya, pada sistem yang sangat produksi. Di sana kami harus menghitung jumlah yang digunakan di dapur pengiriman makanan.
dezso
2
Saya ingat setidaknya dua level CTE rekursif. Saya pikir saya pertama menghitung jumlah dengan unit terkecil yang muncul dalam daftar untuk substansi yang diberikan kemudian mengubahnya menjadi unit terbesar yang masih memiliki bagian bilangan bulat nol.
dezso
1
Apakah semua unit dapat dikonversi dengan kekuatan 10? Apakah daftar unit Anda lengkap?
Erwin Brandstetter

Jawaban:

2

Ini terlihat jelek:

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

tetapi tampaknya melakukan trik:

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

Anda tidak benar-benar membutuhkan hubungan orang tua-anak dalam unit_conversiontabel, karena unit-unit dalam keluarga yang sama secara alami terkait satu sama lain berdasarkan urutan coefficient, selama Anda memiliki keluarga yang diidentifikasi.

mustaccio
sumber
2

Saya pikir, ini bisa disederhanakan.

1. Ubah unit_conversiontabel

Atau, jika Anda tidak dapat mengubah tabel, cukup tambahkan kolom exp10untuk "basis eksponen 10", yang bertepatan dengan jumlah digit untuk bergeser dalam sistem desimal:

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. Fungsi menulis

untuk menghitung jumlah posisi untuk bergeser ke kiri atau kanan:

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

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.

Menjelaskan:

  • GABUNG tabel substansi dan satuan.
  • Hitung jumlah posisi ideal untuk digeser dengan fungsi f_shift_comma()dari atas.
  • KIRI BERGABUNG ke tabel unit kedua kalinya untuk menemukan unit dekat dengan optimal.
  • Pilih unit terdekat dengan DISTINCT ON ()dan ORDER BY.
  • Jika tidak ada unit yang lebih baik ditemukan, kembalilah ke apa yang kita miliki COALESCE().
  • Ini harus mencakup semua kasus sudut dan cukup cepat .

-> Demo SQLfiddle .

Erwin Brandstetter
sumber
1
@ DaveJarvis: Dan di sana saya pikir saya telah membahas semuanya ... detail ini akan sangat membantu dalam pertanyaan yang dibuat dengan hati-hati.
Erwin Brandstetter