◆ プログラムのクイズにありがちなやつ

ちょっとやる必要があったことで どう実装するかがけっこう難しかったもの

日付の from と to とポイントがスペース区切りで 1 行に書かれているデータが並んでます
こういうの

2001/12/12 2010/10/10 3
2003/11/22 2004/03/10 9
2009/10/13 2013/11/20 5
2012/03/03 2012/08/01 1
2016/01/01 2018/02/13 20
2018/02/14 2018/05/31 30
2018/12/20 2018/12/20 4

期間は重なることがあって 各期間のポイントを合計して期間が重複しないようにしたものを同じフォーマットで出力します
プログラムのクイズとかにありそうですね

2001/12/12 から 2010/10/10 までの期間がポイント 3 で
2003/11/22 から 2004/03/10 までの期間がポイント 10 なので
2001/12/12 から 2003/11/21 までがポイント 3
2003/11/22 から 2004/03/10 までは 3 + 9 の 12 になります
2004/03/11 からはまた 3 になります

やり方 1 例

いくつかの方法でやってみたのですが わかりづらいのが多くてもっといい方法ないかなと何度かやり直してみて一番良さそうだった方法です


先に区間に区切ります
それぞれの区間の開始は from の日と to の次の日になるはずです
開始日付の重複を除いて昇順に並べます
区間終わりは次の区間の前日です

最後の区間は一番最後の to の次の日からなのでポイントが 0 でいらない区間になります
区間内はポイントが同じになるように区切っているので 各区間の最初の日に対して from と to の間にあるもののポイントを合計します

この方法だとポイントが 0 のところや 連続して同じポイントの区間が出て来る可能性があります
最後にそれを消したりくっつけたりして完成です

コード的には一番短く見やすくてできました

const data = `
2001/12/12 2010/10/10 3
2003/11/22 2004/03/10 9
2009/10/13 2013/11/20 5
2012/03/03 2012/08/01 1
2016/01/01 2018/02/13 20
2018/02/14 2018/05/31 30
2018/12/20 2018/12/20 4
`

// parse data
const rows = data.trim().split("\n").map(e => {
const [from, to, point] = e.split(" ")
return {from: new Date(from), to: new Date(to), point: +point}
})

// make sections
const sections = (function(){
const oneday = 1000 * 60 * 60 * 24
const dates = []
for(const row of rows){
dates.push(+row.from)
dates.push(+row.to + oneday)
}
const sorted = [...new Set(dates)].sort((a, b) => a - b)
const sections = [{from: new Date(sorted.shift())}]
for(const date of sorted){
sections[0].to = new Date(date - oneday)
sections.unshift({from: new Date(date)})
}
sections.reverse()
sections.pop()
return sections
})()

// calculate point of each section
for(const section of sections){
section.point = rows.filter(row => row.from <= section.from && section.from <= row.to)
.reduce((a, row) => a + row.point, 0)
}

// optimize: remove 0 and merge continuous duplicated entry
const results = []
for(const section of sections){
if(!section.point) continue
if(results[0] && results[0].point === section.point){
results[0].to = section.to
}else{
results.unshift(section)
}
}
results.reverse()

// display results
const zeropad = (n, l) => String(n).padStart(l, "0")
const datestr = d => `${d.getFullYear()}/${zeropad(d.getMonth() + 1, 2)}/${zeropad(d.getDate(), 2)}`
for(const result of results){
console.log(`${datestr(result.from)} ${datestr(result.to)} ${result.point}`)
}
2001/12/12 2003/11/21 3
2003/11/22 2004/03/10 12
2004/03/11 2009/10/12 3
2009/10/13 2010/10/10 8
2010/10/11 2012/03/02 5
2012/03/03 2012/08/01 6
2012/08/02 2013/11/20 5
2016/01/01 2018/02/13 20
2018/02/14 2018/05/31 30
2018/12/20 2018/12/20 4