需求:一個文件夾被復(fù)制,要求新文件夾的名字合適且不重復(fù)。
在 nodejs后端吻商,我在實現(xiàn)文件夾復(fù)制的功能時利职,發(fā)現(xiàn)簡單的給原名子添加 “的副本” 作為新名字趣效,復(fù)制多次的話得到的“...的副本的副本...”特別長,且沒有什么意義猪贪,于是決定模仿 mac 的文件復(fù)制的命名的表現(xiàn)跷敬,寫了個命名算法。 這個算法的代碼實現(xiàn)很簡單热押,難的是理解需求和詳細規(guī)則(即為什么要這么做)西傀。
關(guān)于mac文件重命名的規(guī)則
- 每次復(fù)制文件斤寇,如果不是“的副本”結(jié)尾的文件,復(fù)制的新文件會加上“的副本( n)”拥褂,這里的n是自然數(shù)字娘锁,不含前導(dǎo)零。括號是指可能有饺鹃,也可能沒有空格和數(shù)字
- 如果原文件名是“的副本”結(jié)尾莫秆,復(fù)制出的新文件的名字會加上“ n”。
- 如果原文件名是“的副本 n”尤慰,復(fù)制出的新文件名結(jié)尾的數(shù)字會比原名的索引大且向上最接近原名且不和其他文件重名馏锡。
- 復(fù)制時,如果原文件夾是“的副本”加上有前導(dǎo)0時伟端,會去掉前導(dǎo)0杯道,并應(yīng)用上一條規(guī)則。
- 文件夾視為文件责蝠,即創(chuàng)建的新文件名不能和當前文件夾下的文件重名
只要明確了重命名的詳細規(guī)則党巾,我們就很容易明確如何算法的實現(xiàn)細節(jié)。
2019.3.25更新
優(yōu)化 重寫了代碼霜医,包裝成一個方法齿拂,可以自定義后綴名。
/**
* 文件復(fù)制的命名算法
* @param {string} oName 被復(fù)制的文件的名稱
* @param {Array} filenames 目錄下的所有文件名數(shù)組
* @param {string} suffix 后綴(默認為'的副本')
*/
const getCopyedName = (oName, filenames, suffix = '的副本') => {
let index;
let root; // 詞根
let match = new RegExp(`${suffix}( \\d+)?$`).exec(oName);
console.log(match);
// 1. 求出 oName 的 索引值 和 詞根(不含后綴和索引的源文件名 )
if (match) {
if (match[1]) index = parseInt(match[1]);
else index = 0;
root = oName.slice(0, match.index);
} else {
index = 0;
root = oName;
}
console.log('被復(fù)制文件的詞根:', root);
console.log('被復(fù)制文件的索引:', index);
// 2. 根據(jù)“詞根”肴敛,獲得當前目錄下的相同詞根 索引列表署海。純詞根(沒有后綴)不在范圍內(nèi),因為復(fù)制的新名字必然有后綴医男。
const reg = new RegExp(`^${root}${suffix}( [1-9][0-9]*)?$`); // 注意這里要求為非0開始的數(shù)字
let indexs = [];
filenames.forEach((item) => {
match = reg.exec(item);
if (match) {
const i = match[1] ? parseInt(match[1]) : 1;
indexs.push(i);
}
})
// indexs 理論上不會有重復(fù)的值砸狞。如果你的目錄可能會有重名的文件,請做“去重”處理镀梭。
// 3. 尋找可用的 index
// 從 indexs 找出與 index + 1 相等的值刀森,如果不存在,新的文件名即為 root + suffix + (index+1)
// 如果存在报账,繼續(xù)找出 index + 2 的值研底,直到發(fā)現(xiàn)一個 index + n 在 數(shù)組中不存在。
indexs.sort((a,b)=> a - b);
console.log('目錄下的同詞根的文件index(排序):', indexs);
index++;
for (let i = 0, len = indexs.length; i < len; i++) {
if (indexs[i] == index) index++;
else if (indexs[i] > index) break;
}
console.log('復(fù)制后的文件名:', root + suffix + ' ' + index);
return root + suffix + ' ' + index;
}
// 測試
const filenames = [
'aa的副本 23', 'aa的副本 003', 'bb', 'aa', 'aa的副本', '的副本', 'aa的副本 12',
'aa的副本 13', 'aa的副本 14'
];
getCopyedName('aa的副本 12', filenames);
下面的內(nèi)容是很久以前寫的透罢,看思路就行了榜晦,舊版的代碼(不優(yōu)雅)不要太過在意。
實現(xiàn)
下面是具體的實現(xiàn)琐凭,不過是針對文件夾的芽隆,其實換成文件也完全沒問題。
確定原文件夾的索引
首先獲取原文件夾名字统屈,然后定義一個 index胚吁,用于確定原文件“xx的副本 n”的 n 的取值。
let regExpName = oFolder.name;
let index;
接著我們確定原文件夾名稱的 n 到底是哪個愁憔,n 的獲取方式如下表腕扶。
原文件夾名 | n |
---|---|
xx | 0 |
xx的副本 | 1 |
xx的副本 2 | 2 |
xx的副本 002 | 2 |
... | ... |
注:不會復(fù)制出名為“xx的副本 1”的文件夾。
為了讀取名字結(jié)尾的數(shù)字吨掌,使用了正則表達式半抱。
if (/的副本( \d+)?$/g.test(oFolder.name)) {
let r = / \d+$/g.exec(oFolder.name);
if (r == null) {
// 文件夾名格式: xx的副本
index = 1;
} else {
// 格式:xx的副本 n (n可以包含前導(dǎo)0)
index = parseInt(r[0]);
regExpName = oFolder.name.slice(0, r.index);
}
} else {
// 格式:xx
regExpName = regExpName;
index = 0;
}
通過正則表達式,已經(jīng)知道了原文件夾的名字符合的是哪一種情況膜宋,并給 index 賦予了正確的值窿侈。這里還對 regExpName 進行操作,使其為“的副本”結(jié)尾秋茫,以方便接下來的查詢其他文件夾的操作作為正則表達式的一部分史简。
得到符合“xx的副本 n”的所有文件夾名字
接下來是找出原文件夾所在文件夾系的所有文件夾中,符合 /^${regExpName}的副本( [1-9][0-9]*)?$/
的所有名字肛著,正則表達式的意思是要求符合“xx的副本”或者“xx的副本 n”(注意這個 n 是不含前導(dǎo)0的數(shù)字)圆兵。
let checkedNameFolders = await models.Folder.findAll({
where: {
parentId: oFolder.parentId,
name: {
$regexp: `^${regExpName}的副本( [1-9][0-9]*)?$`
}
}
});
let checkedName = checkedNameFolders.map(item => {
let name = item.name;
let e = /\d+$/g.exec( name );
if(e == null) return 1;
return parseInt( e[0] );
});
// 去重并排序
checkedName = [...new Set(checkedName)].sort((a, b) => a - b);
這里我是用了 sequelize 獲取了 數(shù)據(jù)庫中所有名字符合該正則表達式的文件夾對象,取得了文件夾對象的 name枢贿,并判斷 name 的結(jié)尾是否有數(shù)字殉农,有數(shù)字的話,就提取這個成數(shù)字局荚,放到 checkedName 數(shù)組內(nèi)超凳,沒有的話就返回1。最后我們對這個數(shù)組進行去重和升序耀态,理論上去重操作是不需要的轮傍,但誰知道數(shù)據(jù)庫里面會發(fā)生什么事情呢。不管怎樣茫陆,穩(wěn)妥起見做個去重金麸。
循環(huán)找出可以使用的索引
let newIndex = 1;
for (let i = index + 1; ;i++) {
if (!checkedName.includes(i)) {
newIndex = i;
break;
}
}
我們會從原文件夾的索引+1后,進行遞增并判斷該數(shù)組里時候含有這個值簿盅,一旦發(fā)現(xiàn)沒有挥下,就確定了我們的復(fù)制文件夾索引,停止循環(huán)桨醋。(理論上這里的算法是可以優(yōu)化的棚瘟,因為我們之前已經(jīng)給數(shù)組排序了,而inclues方法每次都要遍歷數(shù)組效率并不高喜最,在文件數(shù)量很多的情況下可能不好使了偎蘸。)
根據(jù)確定后的可用索引映射回文件名
根據(jù)我之前給出的表格,從索引得出最終的名字。
let newName;
switch (newIndex) {
case 1:
newName = `${regExpName}的副本`;
break;
default:
newName = `${regExpName}的副本 ${newIndex}`;
break;
}
到了這里迷雪,我們就獲得了想要的新文件夾的名字了限书。