涉及数组的Javascript循环计算(例如:摊销计划)

时间:2018-02-08 20:47:25

标签: javascript arrays spreadsheet lazy-evaluation circular-reference

我很早就建立了一个可以在很多场景中用作电子表格替代品的应用程序。它为用户提供了创建和组织分析的非表格方法。用户的指令被转换为Javascript(仅使用纯函数)。 (界面介于类似Google Blockly和文本编辑器中的直接编码之间。)代替通常在电子表格中使用的单元格范围,此产品使用Javascript数组。我在这种方法中遇到的一个问题是(准)循环计算,例如在简单的摊销计划中找到的。

到目前为止我的(不成功)尝试解决问题涉及:

  • 懒惰列表评估(使用https://github.com/dankogai/js-list-lazy
  • 将所有已编译的JS包装在函数中以延迟eval,这样用户的内容就不需要进行拓扑排序(当前就是这样)。

下面我将提供足够的背景来说明问题,这可以被广泛推断。以抵押贷款的摊还时间表为例:

基本上,BOP(“期初”)余额取决于前一期间的EOP(“期末”)余额。 EOP余额取决于同期的BOP余额。在使用连续单元格范围的电子表格中,这不是循环的,因为每个BOP余额和EOP余额都是一个离散单元格。但是,如果BOP Balance和EOP Balance(以及所有其他基于时间序列的计算)都是数组,那么在尝试检索元素时会有一个循环引用。该帖子的电子表格截图作为补充提供在帖子的末尾。

尝试在我的应用程序中构建此分析会生成以下JS(为了清晰起见,我已对其进行了编辑和重新组织)。此代码,如果转换为电子表格,可以正常工作(请参阅帖子末尾的补充屏幕截图):

// Credit: Apache OpenOffice
function payment (rate, periods, present_value, future_value, type) {
  var fv = future_value || 0
  var t = type || 0
  if (rate === 0) {
    return -1 * ((present_value + fv) / periods)
  } else {
    var term = (1 + rate) ** periods // Transpiling with Babel; otherwise, use Math.pow(1 + rate, periods)
    if (t === 1) {
      return -1 * ((fv * rate / (term - 1) + present_value * rate / (1 - 1 / term)) / (1 + rate))
    } else {
      return -1 * (fv * rate / (term - 1) + present_value * rate / (1 - 1 / term))
    }
  }
}

var loan_principal = 1000000
var annual_interest_rate = 0.06
var interest_rate_monthly = annual_interest_rate / 12
var amortization_period_years = 25
var amortization_period_months = amortization_period_years * 12

var months_range = _.range(1, amortization_period_months + 1, 1) // See: http://underscorejs.org/#range [1, 2, 3, ... 298, 299, 300]

var bop_balance = months_range.map(function (current_element, current_list_position) {
  if (current_list_position === 0) {
    return loan_principal
  } else {
    // Along with eop_balance, this causes a graph cycle
    return eop_balance[current_list_position - 1]
  }
})

var monthly_payment = months_range.map(function (current_element, current_list_position) {
  return payment(interest_rate_monthly, amortization_period_months, loan_principal)
})

var principal_payment = months_range.map(function (current_element, current_list_position) {
  var current_mthly_pmt = monthly_payment[current_list_position]
  var current_int_pmt = interest_payment[current_list_position]
  return current_mthly_pmt - current_int_pmt
})

var interest_payment = months_range.map(function (current_element, current_list_position) {
  if (current_list_position === 0) {
    return loan_principal * interest_rate_monthly * -1
  } else {
    var previous_bal = eop_balance[current_list_position - 1]
    return previous_bal * interest_rate_monthly * -1
  }
})

var eop_balance = months_range.map(function (current_element, current_list_position) {
  // This causes a graph cycle
  var cur_bal = bop_balance[current_list_position]
  var cur_prin_pmt = principal_payment[current_list_position]
  return cur_bal + cur_prin_pmt
})

由于bop_balance和eop_balance之间的循环,此代码不会在拓扑上排序。由于循环参考,它不会完全评估。

有关如何解决此一般情况的任何建议?谢谢。

补充资料:

以下是表示分析的同一电子表格的两个视图: Amortization schedule showing values Amortization schedule showing formulas

对应用程序中纯函数的依赖是尽量减少来自电子表格的用户的混淆。

如果看到实际应用有助于提供背景信息,请随时访问https://www.getpinevale.com。我把它放在最后,所以它不会分散注意力。

1 个答案:

答案 0 :(得分:0)

你不应该做双链接结构。我建议使用plain for循环并构建两个数组。

for(int i=0; i<range.length;i++){
  if(i==0){
    bop_balance[i]= loan_principal;
    eop_balance[i]= somecalculatedamount(loan_principal);
  }else{
    bop_balance[i] = eop_balance[i-1];
    eop_balance[i] = somecalculatedamount(bop_balance[i])
  }
}

我不知道我的功能是否正确但是我想要做的重点:

  • 不要链接数据结构
  • 并使用两个数组之外的控制结构

根据以下评论,我建议使用reduce。

let range = [1,2,3,4];
let output = range.reduce(function(accumulator, currentValue, currentIndex) {
  let balance = {};
  if (currentIndex == 0) {
    balance.bop = 0;
    balance.eop = currentValue;
  }else{
    balance.bop = accumulator[currentIndex-1].eop;
    balance.eop = balance.bop+5;
  }

  accumulator.push(balance);
  return accumulator;
}, []);

console.log(output);