網頁

2015年7月25日 星期六

Javascript 部分時間、數字 Excel function 實作概念

Excel的 function 作為被廣泛接受的功能,也有不少在前端使用的需求
有些函式可以直接藉由JavaScript的內建物件如 Math Object 使用,但有些則需要自己動手做

function AVERAGE() {
    var sum = 0,
          total = 0;
    for (var i = 0, max = arguments.length; i < max; i++) {
        var num = parseFloat(arguments[i]);
        if (!isNaN(num)) {
            sum += num;
            total++;
        }
    }
    return sum / total;
}

先以 AVERAGE來說,要寫成像Excel那樣能接不定長度參數,就得用Function的arguments object

GCD(最大公因數)跟LCM(最小公倍數)兩個函式寫成不定長度也是要用相同概念
網路上找到的不少解答都是2個參數的版本,那樣寫起來簡單很多,但用法就跟Excel的不大一樣
我寫的版本如下:
function GCD() {
    var al = arguments.length;
    try {
        if (al === 0) {
            return undefined;
        } else if (al == 1) {
            return arguments[0];
        } else if (al == 2) {
            return (arguments[1] === 0) ? arguments[0] : (GCD(arguments[1], arguments[0] % arguments[1]));
        } else if (al > 2) {
            var result = arguments[0];
            for (var i = 1; i < al; i++) {
                result = GCD(result, arguments[i]);
            }
            return result;
        }
    } catch (e) {
        return NaN;
    }
}

function LCM() {
    var al = arguments.length;
    try {
        if (al === 0) {
            return undefined;
        } else if (al == 1) {
            return arguments[0];
        } else {
            var result = arguments[0];
            for (var i = 1; i < al; i++) {
                var gcd = GCD(result, arguments[i]);
                result = result / gcd * arguments[i];
            }
            return result;
        }
    } catch (e) {
        return NaN;
    }
}

寫的時候我有在猜 arguments 參數應該沒辦法做Array的shift()、unshift()
雖然他的用法很像Array
然後也證明這個猜測是正確的,他就是一個較獨特的物件
不然原本想用 recursion 的方式調整參數來寫
不過現在的寫法回頭看也不算太麻煩就是
我懶得處理 0 或是參數太多、數字太大等可能會造成例外的條件,所以直接 try catch
有需要處理這些條件的還需要再調整函式


浮點數相關的函式則需要特別注意浮點數的偏移
Excel 的使用者會很習慣的打上 CEILING(2.3+2.4) 這樣的用法
因為 IEEE 754 binary floating point standard 的規格因素,2.3+2.4 算出來會是 4.699999999999999
雖然是運算上的正確結果,但是這不符合人類的想法
在無條件進位或捨去的函式更是可能產出完全不同的結果
要解決運算結果偏移,我用的是 toFixed 捨棄掉不必要的精確小數點位數
最後算出來的結果再用 parseFloat 包過,去掉那些多餘的 0 就OK了

function getPrecisedFloat(num){
  return parseFloat(parseFloat(num).toFixed(6));
}
function CEILING(num){
  return Math.ceil(getPrecisedFloat(num));
}
function FLOOR(num){
  return Math.floor(getPrecisedFloat(num));
}

ROUND相關的 Excel function 發現跟 JavaScript 用的 Math.round 有差異
處理過的版本如下:

function ROUND(number, digits) {
  digits = digits || 0;
  if (isNaN(number) || isNaN(digits)) return NaN;
  return Math.round(getPrecisedFloat(number * Math.pow(10, digits)) / Math.pow(10, digits));
}
function ROUNDDOWN(number, digits) {
  if (isNaN(number) || isNaN(digits)) return NaN;
  var sign = (number > 0) ? 1 : -1;
  return sign * (Math.floor(getPrecisedFloat(Math.abs(number) * Math.pow(10, digits))) / Math.pow(10, digits));
}

function ROUNDUP(number, digits) {
  if (isNaN(number) || isNaN(digits)) return NaN;
  var sign = (number > 0) ? 1 : -1;
  return sign * (Math.ceil(getPrecisedFloat(Math.abs(number) * Math.pow(10, digits))) / Math.pow(10, digits));
}
function MROUND(number, multiple) {
  if (isNaN(number) || isNaN(multiple)) return NaN;
  if (number * multiple < 0) {
    return NaN;
  }
  return Math.round(getPrecisedFloat(number / multiple)) * multiple;
}

時間函式相較之下簡單很多
知道怎麼從 JavaScript Date Object 取得自己需要的內容就可以了

function YEAR(arg) {
    var date = new Date(arg);
    return date.getFullYear();
}

function MONTH(arg) {
    var date = new Date(arg);
    return date.getMonth();
}

function DAY(arg) {
    var date = new Date(arg);
    return date.getDate();
}

function EDATE(start_date, months) {
  var d = new Date(start_date),
        countM = d.getMonth() + months,
        expectM = countM > 12 ? countM - 12 :
                          countM < 0 ? countM + 12 : countM;
  d.setMonth(d.getMonth() + months);
  if (d.getMonth() > expectM) {
    return new Date(d.getFullYear(), expectM + 1, 0).getTime();
  }
  return d.getTime();
}

function EOMONTH(start_date, months) {
  var d = new Date(start_date),
        countM = d.getMonth() + months,
        expectM = countM > 12 ? countM - 12 :
                          countM < 0 ? countM + 12 : countM;
  d.setMonth(d.getMonth() + months);
  var lastDay = new Date(d.getFullYear(), expectM + 1, 0);
  return lastDay.getTime();
}

沒有留言:

張貼留言