Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
</body>
</html>
 
/**
  Rules -
  https://zh.wikipedia.org/wiki/%E8%81%94%E7%AB%8B%E5%88%B6
  https://en.wikipedia.org/wiki/Mixed-member_proportional_representation
  
  1)各政黨總當選名額,以全國總應選名額依各政黨在第二張投政黨名單的得票比率分配。(假設使用與目前不分區算法相同的最大餘數法)
  2)扣除該黨在選舉區已得議席
  3)其差額再由政黨比例代表名額中補足。
  
  @param {Number} totalSeat - 總席次數量,正整數。
  @param {Number} threshold - 小黨門檻(5 代表 5%)。程式本身沒有檢查說加起來要 = 100%,未滿 100% 的部分,視同未過門檻的小黨們的和。
  @param {Number[]} partyVotePercentages - 各黨政黨票(不分區)得票率百分比(34 表示 34%)
  @param {Number[]} localSeats - 各黨區域立委席次
  @return {Object()} list of seats - 各黨總席次
*/
function calculateSeatsMMP(totalSeat, threshold, partyVotePercentages, localSeats) {
  let expectedSeats = calculateSeats(totalSeat, partyVotePercentages, threshold).map(party => party.seat)
  return localSeats.map((localSeat, idx) => expectedSeats[idx] < localSeat ? localSeat : expectedSeats[idx])
}
// Test cases.
//
console.table([
  {
    // The one in wikipedia ( https://zh.wikipedia.org/wiki/%E8%81%94%E7%AB%8B%E5%88%B6 )
    //
    actual:calculateSeatsMMP(100, 5, [25, 5, 40, 30], [15, 6, 19, 20]),
    expected: [25, 6, 40, 30]
  },
  {
    // 增額增到爆炸:小黨都沒超過門檻(6%),單一大黨過門檻;
    // 小黨的區域當選,全都變成增額名額,大黨把席次全吃下來。
    //
    actual:calculateSeatsMMP(10, 6, [5,5,90], [1,2,5]),
    expected: [1,2,10]
  },
  {
    // 測試 threshold。沒有增額。
    //
    actual:calculateSeatsMMP(41, 5, [2,3,50,45], [0,0,10,10]),
    expected: [0,0,22,19]
  }
])
// Libraries
//
// From: https://github.com/g0v/partyvote2016/blob/gh-pages/main.js
//
function calculateSeats(totalSeat, stage1votes, threshold) {
  // Apply rule 5 & rule 1
  //
  var stage1sum = stage1votes.reduce(function(s, p){
    return s + (p >= threshold ? p : 0)
  }, 0);
  var stage2votes = stage1votes.map(function(p){
    return p >= threshold ? +(p * 100 / stage1sum).toFixed(2) : 0
  });
  // Apply rule 2
  //
  var stage2totalSeat = 0
  var partiesData = stage2votes.map(function(p, idx){
    var seat = totalSeat * p / 100, flooredSeat = Math.floor(seat);
    stage2totalSeat += flooredSeat;
    return {
      id: idx, // partiesData will be sorted later, thus requires idx
      seat: flooredSeat,
      remain: seat - flooredSeat
    }
  })
  var result = stage2votes.map(function(p, idx){
    return {
      stage1votes: stage1votes[idx],
      value: p,
      seat: partiesData[idx].seat
    }
  });
  // Apply rule 3
  //
  shuffle(partiesData).sort(function(a, b){return b.remain-a.remain})
  while(stage2totalSeat < totalSeat) {
    var partyData = partiesData.shift();
    result[partyData.id].seat += 1;
    stage2totalSeat += 1;
  }
  return result;
}
function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;
  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
  return array;
}
Output

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
anonymouspro
0viewers