nodejs通過lodash合併去重由unixtime和Date組成的兩個陣列

NO IMAGE

1. 問題起源


最近在實現一個API,其中有一部分功能是需要從Mongodb中取出一個由Date物件組成的陣列,然後將客戶端傳過來的unixtime合併到該陣列中,並且去重複。

比如,假設從mongodb中取回來的資料中有一個叫做gaming的項,專門用來記錄使用者進入遊戲的開始時間和退出時間。 那麼mongoose的schema的定義將大概是這樣的:

const DeviceLogSchema = new Schema({
...
gaming: [{ enter: Date, exit: Date, _id: false }],          
... 
});

而從mongodb取回來的資料大概就是這個樣子的:

{
"gaming": [
{
"enter": 2017-04-25T14:32:12.081Z,
"exit": 2017-04-25T14:48:52.082Z,
},
{
"enter": 2017-04-26T14:32:12.081Z,
"exit": 2017-04-26T14:48:52.082Z,
}
],
}

也就是說通過mongoose的model取回來的記錄中,enter和exit都是Date(對mongodb來說)型別的,而對於js來說,就是一個Object(js將所有簡單型別以外的資料型別都處理成Object)。

let deviceLog = await DeviceLog.findOne({});
console.log(typeof deviceLog.enter) // ->Object

而客戶端每隔一段時間就會呼叫api來將新的使用者遊戲時間回傳給伺服器,但用的格式是unixtime。

{
"gaming": [{
"enter": 1493130733081,
"exit": 1493131734082,
},{
"enter": 1493130735084,
"exit": 1493131736087,
}],
}

這裡的enter和exit的unixtime時間格式,對於js來說,就是number型別的。

我們通過mongoose來儲存的時候,不需要將unixtime進行任何轉換,直接儲存, mongoose會將其自動轉成Date格式進行儲存。也就是說,如果儲存前的gaming內容是如下這個樣子的:

"gaming": [
{
"enter": 2017-04-25T14:32:12.081Z,
"exit": 2017-04-25T14:48:52.082Z,
},
{
"enter": 2017-04-26T14:32:12.081Z,
"exit": 2017-04-26T14:48:52.082Z,
},
{
"enter": 1493130733081,
"exit": 1493131734082,
},
{
"enter": 1493130735084,
"exit": 1493131736087,
}
],

那麼通過mongoose的model儲存之後,最終會自動成為類似以下這樣的格式:

"gaming": [
{
"enter": 2017-04-25T14:32:12.081Z,
"exit": 2017-04-25T14:48:52.082Z,
},
{
"enter": 2017-04-26T14:32:12.081Z,
"exit": 2017-04-26T14:48:52.082Z,
},
{
"enter": 2017-04-27T14:22:12.021Z,
"exit": 2017-04-27T15:32:12.031Z,
},
{
"enter": 2017-04-26T16:22:12.082Z,
"exit": 2017-04-26T16:52:12.082Z,
}
],

那麼這裡要解決的問題就是:

  • 如何將客戶端傳過來的新陣列和從mongodb取回來的陣列進行合併
  • 合併時如何根據遊戲進入的時間enter來去重複

當然,我們可以用原始的方法,做兩層遍歷,分別便利兩個不同的陣列,然後將其中一個比對資料的型別轉換成另外一個資料對應的型別,然後進行比較其是否相等,相等的話就去掉,不想等的話就將資料追加到陣列中。

但,這樣效率太低了,應該有更好的更優雅的方法來幫助我們處理這種問題。

2. 實驗資料

那麼我們就根據上面碰到的問題,來建立兩個實驗所用的資料。一個是代表從mongodb取回來的資料:

const orgGaming = [
{
"enter": new Date("2017-04-25T14:32:12.081Z"),
"exit": new Date("2017-04-25T14:48:52.082Z"),
},
{
"enter": new Date("2017-04-26T14:32:12.081Z"),
"exit": new Date("2017-04-26T14:48:52.082Z"),
}
]

一個是客戶端傳進來的資料:

const newGaming = [
{
"enter": 1493130732081, // 這和orgGamine第一條資料重複
"exit": 1493131732082, // 這和orgGamine第一條資料重複
},
{
"enter": 1493130735084,
"exit": 1493131736087,
}
]

新陣列中的第一條資料和enter和資料庫中的第一條資料的enter,事實上是相同的,所以我們希望合併之後這個重複資料是去掉的。

3. ES6陣列合並新特性


其實,如果不是因為要考慮去重複的問題的話,我們完全可以通過ES6的新特性來完成的。

array1.push(...array2)

這裡的’…’操作符叫做擴充套件運算子,是ES6引入的新特性。目的是將一個陣列打散成用逗號分隔的引數序列。

const array = [1, 2];
console.log(...array); // 相當於 console.log(1,2)

所以上面的示例程式碼的意思就是將array2打散後,將每個元素作為引數push到array1中生成新的陣列。所以,如果應用到我們的場景中的話

const orgGaming = [
{
"enter": new Date("2017-04-25T14:32:12.081Z"),
"exit": new Date("2017-04-25T14:48:52.082Z"),
},
{
"enter": new Date("2017-04-26T14:32:12.081Z"),
"exit": new Date("2017-04-26T14:48:52.082Z"),
}
]
const newGaming = [
{
"enter": 1493130732081,
"exit": 1493131732082,
},
{
"enter": 1493130735084,
"exit": 1493131736087,
}
]
orgGaming.push(...newGaming);
console.log(orgGaming);

最終將會輸出沒有去重複的結果:

[ 
{ enter: 2017-04-25T14:32:12.081Z,
exit: 2017-04-25T14:48:52.082Z },
{ enter: 2017-04-26T14:32:12.081Z,
exit: 2017-04-26T14:48:52.082Z },
{ enter: 1493130732081, 
exit: 1493131732082 },
{ enter: 1493130735084, 
exit: 1493131736087 } 
]

當然,ES6的這個陣列合並方式還可以這樣寫:

[...array1,...array2]

同時,ES6還提供了對簡單資料型別去重複方式:

[...new Set([...array1 ,...array2])];

但是,這個只能針對簡單資料型別進行去重複,比如數字型別和字串型別等。

const array1 = ['techgogogo', 'sina', 'baidu'];
const array2 = ['techgogogo', 'google'];
console.log([... new Set([...array1, ...array2])]);

最後輸出:

[ 'techgogogo', 'sina', 'baidu', 'google' ]

但是對於我們這裡的物件型別組成的陣列,它是做不到的。

最重要的是,它沒有提供一個comparator的回撥方法來放我們處理應該如何判斷,兩個資料是否是重複的。

這裡,lodash的陣列操作,也許是個正確的解決方案(之一)。

4. lodash合併物件型別陣列並去重複

lodash的unionWith方式可以合併兩個陣列,並且可以讓我們提供一個comparator方法來控制該如何比較兩個陣列中的元素是否是一致的,以此來判斷這個資料是否是重複的。

官方文件對unionWith方法的描述請看這裡:https://lodash.com/docs/4.17.4#unionWith

_.unionWith([arrays], [comparator])

理解起來也比較簡單,請看程式碼如下:

const _ = require('lodash');
const orgGaming = [
{
"enter": new Date("2017-04-25T14:32:12.081Z"),
"exit": new Date("2017-04-25T14:48:52.082Z"),
},
{
"enter": new Date("2017-04-26T14:32:12.081Z"),
"exit": new Date("2017-04-26T14:48:52.082Z"),
}
]
const newGaming = [
{
"enter": 1493130732081,
"exit": 1493131732082,
},
{
"enter": 1493130735084,
"exit": 1493131736087,
}
]
gaming = _.unionWith(orgGaming, newGaming, (value1, value2) => {
if (typeof value1.enter === 'number' && typeof value2.enter === 'number') {
return (value1.enter === value2.enter);
} else if (typeof value1.enter === 'number' && typeof value2.enter === 'object') {
return (value1.enter === value2.enter.getTime());
} else if (typeof value1.enter === 'object' && typeof value2.enter === 'number') {
return (value1.enter.getTime() === value2.enter);
} else if (typeof value1.enter === 'object' && typeof value2.enter === 'object') {
return (value1.enter.getTime() === value2.enter.getTime());
}
});
console.log(gaming);

這裡關鍵的地方就是uionWith,有幾個地方需要注意:

  • 引數的順序,特別是前兩個陣列引數。如果第一個陣列中某個成員判定和第二個陣列中的某個成員是重複的,那麼第一個陣列中的該元素會保留,第二個陣列中的對應元素會移除。
  • 第三個引數就是一個回撥方法,接受兩個引數,其實就是兩個需要比對的陣列的成員,這裡我們通過比對兩個成員的enter是否相等來判斷該成員是否重複。
  • 判斷是否重複的時候,我們需要將日記先轉換成unixtime的格式,然後再進行比較。

最終我們可以看到去重複後的輸出:

[ { enter: 2017-04-25T14:32:12.081Z,
exit: 2017-04-25T14:48:52.082Z },
{ enter: 2017-04-26T14:32:12.081Z,
exit: 2017-04-26T14:48:52.082Z },
{ enter: 1493130735084, 
exit: 1493131736087 } ]

可以看到,最後輸出的列表中只有三個物件,其中一個重複的已經被摒棄掉了。

最後我們通過mongoose將這份資料儲存到mongodb時,如前面所述,會自動將unixtime轉換成Date進行儲存,這樣資料就統一起來了。這裡mongoose的操作就不贅述了,有興趣的朋友可以自己實踐下。

以上就是本人對兩個由物件型別組成的陣列進行合併的一些嘗試和實踐,如果大家有更好更優雅的方式的話,歡迎在評論中給出來。先拜謝了!

本文由天地會珠海分舵編寫,轉載需授權,喜歡點個贊,吐槽請評論,進一步交流請關注公眾號techgogogo或者直接聯絡本人微信zhubaitian1