はじめに
SENSYN Robotics(センシンロボティクス)の中山です。
Webアプリやそのインフラ周りと、Web側とドローンの接続を行うデバイスドライバ的な部分を担当しています。
ここ1〜2年探していた、ループを使わずにリスト上の連続する2つの値を要素とするリストを作る方法をやっと思いついたので、それを記録するためのエントリです。
なにをしたいか
直近でやりたかったのは、ドローンの飛行記録から実際に飛行した距離を算出することでした。ドローンは飛行している場所(緯度・経度)を定期的に通知しています。CSVにすると、以下のようなイメージのデータです。
時刻,緯度,経度
2020-12-08T02:26:33Z.231,35.166264279608,138.67945381096
2020-12-08T02:26:33.834Z,35.166264279610,138.67945381097
2020-12-08T02:26:34.102Z,35.166264279611,138.67945381098
...
二点の緯度・経度が分かれば、その二点間の距離は計算可能です。そして、2点間の距離が計算できれば、あとは各2点間の距離を足し合わせて飛行した距離が計算できます。
ループを使って実装してみる
2点間の距離の計算にはnpm install geolib
してgeolibを使います。
ループを使うとこんな感じでしょうか。
const geolib = require('geolib');
const route = [
{latitude: 35.166264279608, longitude: 138.67945381096},
{latitude: 35.167274290660, longitude: 138.67945381157},
{latitude: 35.168284321721, longitude: 138.67945381218},
];
let from = null;
let dist = 0.0;
for (const to of route) {
if (from != null) {
dist += geolib.getDistance(from, to);
}
from = to;
}
console.log(`distance: ${dist}m`);
連続する2つの値を扱うために、前回のループの値をfromに入れて、今回の値をtoとして扱っています。
これはこれで悪くないのですが、fromやdistを書き換えているのが気持ち悪いです。if文があるのも美しさに欠けます。
reduce()を使って実装してみる
変数の書き換えを排除するためにreduce()
を使ってみると、こんな風になりました。
const geolib = require('geolib');
const route = [
{latitude: 35.166264279608, longitude: 138.67945381096},
{latitude: 35.167274290660, longitude: 138.67945381157},
{latitude: 35.168284321721, longitude: 138.67945381218},
];
const dist = route
.reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}])
.filter(v => v.from != v.to)
.map(v => geolib.getDistance(v.from, v.to))
.reduce((sum, d) => sum + d, 0);
console.log(`distance: ${dist}m`);
最初のreduce()
.reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}])
ここで連続する2つの値を要素とするリストを作っています。route
の各要素に対して、適用結果の配列の最後の要素とのペアを作っていきます。
-
acc
の初期値[{from: 要素0, to: 要素0}]
- 1回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}]
- 2回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}]
- 3回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}, {from: 要素1, to: 要素2}]
スプレッド演算子...
を使わずに、(acc, v) => {acc.push({from: acc[acc.length - 1].to, to: v}); return acc}
とかでもいいです。
filter()
ここで目的の「連続する2つの値を要素とするリスト」を含むリストができたのですが、最初の2つの要素はfrom
とto
がどちらも要素0を指しています。距離を計算するのであればこのままでもいいのですが、汎用性を考えるとちょっと問題があります。そこでfrom
とto
が同じものを排除して、「連続する2つの値を要素とするリスト」だけを取り出すのがここのfilter()
です
.filter(v => v.from != v.to)
map()
2点から2点間の距離を計算しています。
.map(v => geolib.getDistance(v.from, v.to))
2個目のreduce()
距離を合計して、ルート全体の飛行距離を計算しています。
.reduce((sum, d) => sum + d, 0);
まとめ
ループを使わずに配列上の連続する2つの値を要素とするリストを作るには、以下のようなコードを書けば良い。
list
.reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: list[0], to: list[0]}])
.filter(v => v.from != v.to)
JavaScript以外でも、reduce()
とfilter()
が使えれば同じ手法が使えると思います。