Search code examples
oracle-databasefunctionmathstored-procedureslcm

Lowest fraction Value In Oracle


I am trying to create a function to return lowest fraction value. the sample code is here :

create or replace function fraction_sh(x number) return varchar2
is
    fra1 number;
    pwr number;
    intprt number;
    v4 number;
    numer number;
    denom number;
    gcdval number;
    frac varchar2(50);
    begin
       if x <> 0 then  
           fra1 := mod(x,1);
           pwr := length(mod(x,1))-1;
           intprt := trunc(x);
           numer :=mod(x,1)*power(10,length(mod(x,1))-1);
           denom :=power(10,length(mod(x,1))-1);
           gcdval := gcdnew(power(10,length(mod(x,1))-1),mod(x,1)*power(10,length(mod(x,1))-1));
               if intprt = 0 then 
                       frac := to_char(trunc(numer/gcdval))||'/'||to_char(trunc(denom/gcdval));
                DBMS_OUTPUT.put_line(1||' '||denom||' '||gcdval||' '||numer);
              else
                    frac := (intprt*to_char(trunc(denom/gcdval)))+to_char(trunc(numer/gcdval))||'/'||to_char(trunc(denom/gcdval));
                   DBMS_OUTPUT.put_line(2||' '||denom||' '||gcdval||' '||numer);
              end if;
        end if;
     return frac;
end;


create or replace function gcdnew (a number, b number, p_precision number default null, orig_larger_num number default null) return number is
v_orig_larger_num number := greatest(nvl(orig_larger_num,-1),a,b);
v_precision_level number := p_precision;
begin
  if a is null or b is null or (a = 0 and b = 0) then return 1; end if;

  if p_precision is null or p_precision <= 0 then
      v_precision_level := 4; 
  end if;

  if b is null or b = 0 or (b/v_orig_larger_num <= power(10,-1*v_precision_level) and greatest(a,b) <> v_orig_larger_num) then
      return a;
  else
      return (gcdnew(b,mod(a,b),v_precision_level,v_orig_larger_num));
  end if;

end;

Inmost cases it works, but when i try to pass 2/11 it returns 2/10.

Any help appreciated.


Solution

  • The problem with what you're currently doing is precision. With 2/11 the resulting number is 0.1818181... recurring, and the length of that - and therefore the pwr value - end up as 40, which destroys the later calculations.

    With modifications to limit the precision (and tidied up a bit, largely to remove repeated calculations when you have handy variables already):

    create or replace function fraction_sh(p_float number) return varchar2
    is
      l_precision pls_integer := 10;
      l_int_part pls_integer;
      l_frac_part number;
      l_power pls_integer;
      l_numer number;
      l_denom number;
      l_gcdval number;
      l_result varchar2(99);
    begin
      if p_float is null or p_float = 0 then
        return null;
      end if;
    
      l_int_part := trunc(p_float);
      l_frac_part := round(mod(p_float, 1), l_precision);
      l_power := length(l_frac_part);
      l_denom := power(10, l_power);
      l_numer := l_frac_part * l_denom;
    
      l_gcdval := gcdnew(l_denom, l_numer, ceil(l_precision/2));
    
      if l_int_part = 0 then 
        l_result := trunc(l_numer/l_gcdval) ||'/'|| trunc(l_denom/l_gcdval);
      else
        l_result := l_int_part * (trunc(l_denom/l_gcdval) + trunc(l_numer/l_gcdval))
          ||'/'|| trunc(l_denom/l_gcdval);
      end if;
    
      return l_result;
    end;
    /
    

    Which gets:

    with t(n) as (
                select 9/12 from dual
      union all select 2/11 from dual
      union all select 1/2 from dual
      union all select 1/3 from dual
      union all select 1/4 from dual
      union all select 1/5 from dual
      union all select 1/6 from dual
      union all select 1/7 from dual
      union all select 1/8 from dual
      union all select 1/9 from dual
      union all select 1/10 from dual
      union all select 4/3 from dual
      union all select 0 from dual
      union all select 1 from dual
    )
    select n, fraction_sh(n) as fraction
    from t;
    
            N FRACTION                      
    ---------- ------------------------------
           .75 3/4                           
    .181818182 2/11                          
            .5 1/2                           
    .333333333 1/3                           
           .25 1/4                           
            .2 1/5                           
    .166666667 1/6                           
    .142857143 1/7                           
          .125 1/8                           
    .111111111 1/9                           
            .1 1/10                          
    1.33333333 4/3                           
             0                               
             1 1/1                           
    

    So you might want to add some handling for either passing in 1, or the approximation after rounding ending up as 1/1 - presumably just to return a plain '1' in either case.

    I've set l_precision to 10 rather arbitrarily, you can make that larger, but will hit problems at some point so test carefully with whatever value you pick.

    (And I haven't looked at gdcnew at all; that can probably be simplified a bit too.)