Chuyển đổi đơn vị đo lường


10

Tìm cách tính đơn vị đo lường phù hợp nhất cho danh sách các chất trong đó các chất được cho theo khối lượng đơn vị khác nhau (nhưng tương thích).

Bảng chuyển đổi đơn vị

Bảng chuyển đổi đơn vị lưu trữ các đơn vị khác nhau và cách các đơn vị đó liên quan:

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

Sắp xếp theo hệ số cho thấy rằng parent_idliên kết một đơn vị con với cấp trên số của nó.

Bảng này có thể được tạo trong PostgreSQL bằng cách sử dụng:

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

Cần có một khóa ngoại từ parent_idđến id.

Bảng chất

Bảng Chất liệt kê số lượng cụ thể của các chất. Ví dụ:

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

Bảng có thể giống:

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

Vấn đề

Làm thế nào bạn có thể tạo một truy vấn tìm thấy một phép đo để biểu thị tổng của các chất bằng cách sử dụng ít chữ số nhất có toàn bộ số (và thành phần thực tùy chọn)?

Ví dụ: bạn sẽ trở về như thế nào:

  quantity  unit        label
        15  microlitre  mercury 
       112  centilitre  water

Nhưng không:

  quantity  unit        label
        15  microlitre  mercury 
      1.12  litre       water

Bởi vì 112 có ít chữ số thực hơn 1,12 và 112 nhỏ hơn 1120. Tuy nhiên, trong một số trường hợp, sử dụng chữ số thực ngắn hơn - chẳng hạn như 1,1 lít so với 110 centilit.

Hầu hết, tôi gặp khó khăn khi chọn đơn vị chính xác dựa trên mối quan hệ đệ quy.

Mã nguồn

Cho đến nay tôi có (rõ ràng là không làm việc):

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

Ý tưởng

Điều này có yêu cầu sử dụng log 10 để xác định số chữ số không?

Những ràng buộc

Các đơn vị không phải là tất cả trong quyền hạn của mười. Ví dụ: http://unitsofmeasure.org/ucum-essence.xml


3
@mustaccio Tôi đã gặp vấn đề chính xác ở nơi trước đây của tôi, trên một hệ thống sản xuất. Ở đó chúng tôi phải tính toán số lượng sử dụng trong một nhà bếp giao thức ăn.
dezso

2
Tôi nhớ một CTE đệ quy ít nhất hai cấp. Tôi nghĩ rằng trước tiên tôi đã tính các khoản tiền với đơn vị nhỏ nhất xuất hiện trong danh sách cho chất đã cho sau đó chuyển đổi thành đơn vị lớn nhất vẫn có phần nguyên khác không.
dezso

1
Có phải tất cả các đơn vị chuyển đổi với sức mạnh của 10? Danh sách các đơn vị của bạn đã hoàn thành chưa?
Erwin Brandstetter

Câu trả lời:


2

Điều này có vẻ xấu:

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

nhưng dường như để thực hiện các mẹo:

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

Bạn không thực sự cần mối quan hệ cha-con trong unit_conversionbảng, bởi vì các đơn vị trong cùng một gia đình có liên quan tự nhiên với nhau theo thứ tự coefficient, miễn là bạn đã xác định được gia đình.


2

Tôi nghĩ rằng, điều này có thể được đơn giản hóa phần lớn.

1. Sửa đổi unit_conversionbảng

Hoặc, nếu bạn không thể sửa đổi bảng, chỉ cần thêm cột exp10cho "cơ sở số mũ 10", trùng với số chữ số để thay đổi trong hệ thập phân:

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. Viết hàm

để tính số lượng vị trí để dịch chuyển sang trái hoặc phải:

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. Truy vấn

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.

Giải thích:

  • THAM GIA chất và bảng đơn vị.
  • Tính số lượng vị trí lý tưởng để thay đổi với chức năng f_shift_comma()từ trên.
  • TRÁI THAM GIA vào bảng đơn vị lần thứ hai để tìm các đơn vị gần với mức tối ưu.
  • Chọn đơn vị gần nhất với DISTINCT ON ()ORDER BY.
  • Nếu không tìm thấy đơn vị nào tốt hơn, hãy quay lại với những gì chúng ta đã có COALESCE().
  • Điều này sẽ bao gồm tất cả các trường hợp góc và được khá nhanh .

-> Bản demo SQLfiddle .


1
@DaveJarvis: Và ở đó tôi nghĩ rằng tôi đã bao quát mọi thứ ... chi tiết này sẽ thực sự hữu ích trong câu hỏi được làm cẩn thận.
Erwin Brandstetter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.