CodeIQ:ファイル名をわかりやすくソート

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「ファイル名をわかりやすくソート」(CodeIQ)を参照してください。

問題の概要

ファイル名に番号が振られている場合、番号を数値として扱ってソートするという問題です。
詳細な仕様は問題を引用します。

  • ファイルを識別する名前は「ファイル名」と「拡張子」で構成される。例)file0123.txt
  • ファイル名部分の右端に数字がある場合は、まず右端の数字以外の部分でソートする。
  • その後、数字部分について数値としてソートする。数値として同じ場合は、さらに文字列とみてソートする。
  • 最後に、拡張子部分についてソートする。
※ファイル名部分の途中に数字が入っていても、アルファベットと同じように扱うものとします。(右端の数字以外は数値とはみなさない)。
 ファイル名に使われる文字はアルファベット、数字、アンダースコアのみとし、大文字と小文字は区別するものとします。

私のプログラム

C言語で解答しています。

import java.io.*;
import java.util.*;
import java.util.Comparator;
import java.util.regex.*;

public class Main {
	//=========================================================================
	//	ファイル名情報保持クラス
	//=========================================================================
	public class FileName {
		private final String  Name;
		private final String  Num;
		private final String  Ext;

		public FileName(String filename){
			//  拡張子とファイル名に分ける
			//  <file><num>.extの場合
			Pattern p1 = Pattern.compile("^(.+[\\p{Alpha}_])(\\d*)(\\.\\w+)$");
			Matcher m1 = p1.matcher(filename);
			if (m1.find()) {
				Name = m1.group(1);
				Num = m1.group(2);
				Ext = m1.group(3);
			}
			else {
				//  <num>.extの場合
				Pattern p2 = Pattern.compile("^(\\d+)(\\.\\w+)$");
				Matcher m2 = p2.matcher(filename);
				if(m2.find()){
					Name = "";
					Num = m2.group(1);
					Ext = m2.group(2);
				}
				else{
					//  <file>.extの場合
					Pattern p3 = Pattern.compile("^(.+)(\\.\\w+)$");
					Matcher m3 = p3.matcher(filename);
					if(m3.find()){
						Name = m3.group(1);
						Num = "";
						Ext = m3.group(2);
					}
					//<file>の場合
					else{
						Name = filename;
						Num = "";
						Ext = "";
					}
				}
			}
		}

		public int toInt(){
			try{
				return Integer.parseInt(Num);
			}
			catch(Exception e){
				return 0;
			}
		}

		public String toOriginalName(){
			return String.format("%s%s%s", Name, Num, Ext);
		}
	}

	//=========================================================================
	//	ソート条件
	//=========================================================================
	private class FileComparator implements Comparator<FileName>{

		@Override
		public int compare(FileName o1, FileName o2) {
			int ret;

			// 数値部分を16進数表記(0埋め8桁)で比較
			ret = o1.Name.compareTo(o2.Name);
			if(ret != 0){
				return ret;
			}

			// 数値部分を比較
			ret = o1.toInt()-o2.toInt();
			if(ret != 0){
				return ret;
			}

			// 数値部分が同じなら長い方を優先
			ret = o2.Num.length() - o1.Num.length();
			if(ret != 0){
				return ret;
			}

			ret = o1.Ext.compareTo(o2.Ext);
			return ret;
		}
	}

	//=========================================================================
	//	ファイル名ソートクラス
	//=========================================================================
	ArrayList<FileName> Input = new ArrayList<>();	// 入力値保持リスト

	// 入力値を保持する
	public void setInput(String s){
		FileName fn = new FileName(s);
		Input.add(fn);
	}

	// 結果出力
	public void print(){
		for(FileName fn: Input){
			System.out.println(fn.toOriginalName());
		}
	}

	// ソート
	public void sort(){
		Collections.sort(Input, new FileComparator());
	}

	// テスト用メイン
	public static void main(String[] args) throws IOException{
		try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(System.in))) {
			String line;
			Main sorter = new Main();

			while ((line = stdReader.readLine()) != null) {
				try{
					line = line.trim();

					// 空行がきたら終わりとする
					if(line.isEmpty()){
						break;
					}

					sorter.setInput(line);
				}
				catch(Exception e){
					return;
				}
			}

			sorter.sort();
			sorter.print();
		}

		return;
	}
}

解説

この問題は実際の業務で作成するプログラムでもありそうな感じです。
ポイントは次の2点でしょうか?

  1. 表示用と内部保持用で形式を変える
  2. ソート条件

考え方

ファイル名をファイル名、番号、拡張子に分解して保持し、それぞれの項目を条件に従ってソートした上で、再度連結した文字列を出力すれば良いだけです。

ちなみに、上記のプログラムは2回目に提出したものです。
私の最初のアイデアはソートを簡単にするため、一度分解した文字列の番号部分を16進数表記で8桁(64bit)の0埋めされた文字列に変換し、それを内部ファイル名として扱うというものでした。
例えば、「hoge1.txt」は内部的に「hoge00000001.txt」になるわけです。番号部分の桁が揃うのでソート条件が1回になるというアイデアでした。ただ、この問題の仕様では「123.txt」の様な名前の場合、123を番号として扱う必要があり(問題からは読み取れませんでしたが)、この方法はやめました。76行目のコメントはその時の処理からの修正漏れで、コメントが間違っています。

ファイル名の管理

ファイル名はFileNameクラスで管理しています。
このクラスはファイル名、番号、拡張子をメンバに持っており、コンストラクタにファイル名(入力値)を受け取ったらそれを分解して保持します。
コンストラクタでは正規表現を使ってファイル名を分割しています。この時ファイル名が「name001.ext」か「name.ext」か「001.ext」か「name」かを気にする必要があります。私はあまり考えず、長い方から処理して処理できなかったらより短いパターンというif文を書いていますが考えればもう少し見通しよく処理できる気がします。

ソート

ソートは難しくありません。単純に仕様通りに処理すれば良いだけです。ポイントは必ず処理できるようにFileNameのインスタンスの値を設定しておくことでしょうか。例えばファイルの名前部分がない場合にnullが入っていたりするとnullチェックが必要になります。それを避けるため、比較時に必ず小さいと判断される文字列(空文字列)をセットしておくというのは有用なテクニックです。

雑感

この問題はいかにも実際の業務でありそうな感じで、データを管理するクラスを用意したり、ソート条件を作ったりというよくあるプログラムが書けるかをテストするもので、実際の業務ができそうかを測るのには適した問題と思います。
ただし、問題の条件では「123.ext」のようなファイル名が「ファイル名.拡張子」なのか「番号.拡張子」なのかが明確ではないというのはいただけません。