2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ファイル名を人間にとって自然な形でソート

Last updated at Posted at 2020-12-01

背景

プログラムで処理された結果をみている時に「1.txt」「11.txt」「2.txt」と並んでいると悲しい気分になりますよね。昨日ちょうど悲しくなったので、ファイル名を直感的に並べるロジックを考えてみました。

実行結果

実行結果を見てもらえば説明は不要かと思います。

[
  "aaa.txt",
  "aaa.xlsx",
  "aaa(1).txt",
  "aaa(1).xlsx",
  "aaa(2).txt",
  "aaa(2).xlsx",
  "aaa(10).txt",
  "aaa(10).xlsx",
  "aaa1.txt",
  "aaa1.xlsx",
  "aaa2.txt",
  "aaa2.xlsx",
  "v1.0.0",
  "v1.0.0-SNAPSHOT",
  "v1.0.1",
  "v1.0.10",
  "v1.2.1",
  "v1.10.1"
]

もっと良いコード

コメントで教えてもらいました。a.localeCompareのnumericオプションでいけちゃいます。

console.log(JSON.stringify([
    "aaa(10).xlsx",
    "aaa1.xlsx",
    "aaa.xlsx",
    "aaa(10).txt",
    "v1.0.0-SNAPSHOT",
    "v1.2.1", "aaa.txt",
    "aaa1.txt",
    "aaa(2).xlsx",
    "aaa2.xlsx",
    "aaa(1).xlsx",
    "aaa(1).txt",
    "v1.10.1",
    "v1.0.10",
    "aaa2.txt",
    "aaa(2).txt",
    "v1.0.1",
    "v1.0.0"

].sort((a, b) => a.localeCompare(b, [], {numeric: true})), null, 2))

車輪の再開発なコード

とはいえ、他の言語で実装する人にはちょっと参考になる、、、かもしれない

import path from "path";

export function numberPadding(n: number | string, l: number) {
    const nStr = `${n}`
    // 想定している桁数を超えた場合にはpaddingせずにそのまま返す
    return "0".repeat(Math.max(0, l - nStr.length)) + nStr
}

function naturalSortKey(str: string, paddingLength: number): string {
    // 数字のソート順を直感的にするために、0パディングをかける
    // A1.txt -> A0001.txt
    // A2.txt -> A0002.txt
    // A11.txt -> A0011.txt
    return str.replace(/[0-9]+/g, (substring: string) => {
        return numberPadding(substring, paddingLength)
    })
}

export function byNaturalFilename(aStr: string, bStr: string): number {
    /*
    連番の場合、最初の一つには番号がついていないケースがある。
    A.txt
    A(1).txt
    このような場合にも対処するため、拡張子以外が一致している場合には拡張子でソート、それ以外は拡張子抜きでソートする
     */
    const aExt = path.extname(aStr)
    const aBase = path.basename(aStr, aExt)
    const bExt = path.extname(bStr)
    const bBase = path.basename(bStr, bExt)
    // 文字列の長さ以上にパディングが必要になることはない
    const paddingLength = Math.max(aStr.length, bStr.length)

    if (aBase === bBase) {
        return naturalSortKey(aExt, paddingLength).localeCompare(naturalSortKey(bExt, paddingLength))
    }
    return naturalSortKey(aBase, paddingLength).localeCompare(naturalSortKey(bBase, paddingLength))
}

export function byNaturalFilenameReverse(aStr: string, bStr: string): number {
    return byNaturalFilename(bStr, aStr)
}

console.log(JSON.stringify([
    "aaa(10).xlsx",
    "aaa1.xlsx",
    "aaa.xlsx",
    "aaa(10).txt",
    "v1.0.0-SNAPSHOT",
    "v1.2.1", "aaa.txt",
    "aaa1.txt",
    "aaa(2).xlsx",
    "aaa2.xlsx",
    "aaa(1).xlsx",
    "aaa(1).txt",
    "v1.10.1",
    "v1.0.10",
    "aaa2.txt",
    "aaa(2).txt",
    "v1.0.1",
    "v1.0.0"
].sort(byNaturalFilename), null, 2))
2
1
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?