Предоставляет ли Linq способ легко обнаруживать пропуски в последовательности?

Я управляю каталогом файлов. Каждый файл будет назван аналогично Image_000000.pngс увеличением числовой части для каждого сохраняемого файла.

Файлы также могут быть удалены, оставляя пробелы в числовой последовательности. Причина, по которой я спрашиваю, заключается в том, что я осознаю, что в какой-то момент в будущем пользователь может использовать последовательность номеров, если я не предприму шаги для повторного использования номеров, когда они станут доступны. Я понимаю, что это миллион, и это много, но у нас более 20 лет пользователей, так что "когда-нибудь" не может быть и речи.

Итак, я специально спрашиваю, существует ли способ легко определить промежутки в последовательности без простого зацикливания. Я понимаю, что, поскольку это фиксированный диапазон, я мог бы просто зациклить ожидаемый диапазон.

И я буду, если не будет лучшей / более чистой / более легкой / более быстрой альтернативы. Если так, я хотел бы знать об этом.

Этот метод вызывается для получения следующего доступного имени файла:

public static String GetNextImageFileName()
{
    String retFile = null;
    DirectoryInfo di = new DirectoryInfo(userVars.ImageDirectory);
    FileInfo[] fia = di.GetFiles("*.*", SearchOption.TopDirectoryOnly);
    String lastFile = fia.Where(i => i.Name.StartsWith("Image_") && i.Name.Substring(6, 6).ContainsOnlyDigits()).OrderBy(i => i.Name).Last().Name;
    if (!String.IsNullOrEmpty(lastFile))
    {
        Int32 num;
        String strNum = lastFile.Substring(6, 6);
        String strExt = lastFile.Substring(13);
        if (!String.IsNullOrEmpty(strNum) && 
            !String.IsNullOrEmpty(strExt) && 
            strNum.ContainsOnlyDigits() &&
            Int32.TryParse(strNum, out num))
        {
            num++;
            retFile = String.Format("Image_{0:D6}.{1}", num, strExt);
            while (num <= 999999 && File.Exists(retFile))
            {
                num++;
                retFile = String.Format("Image_{0:D6}.{1}", num, strExt);
            }
        }
    }

    return retFile;
}

РЕДАКТИРОВАТЬ: на случай, если это кому-нибудь поможет, вот последний метод, включающий ответ Даниэля Хилгарта:

public static String GetNextImageFileName()
{
    DirectoryInfo di = new DirectoryInfo(userVars.ImageDirectory);
    FileInfo[] fia = di.GetFiles("Image_*.*", SearchOption.TopDirectoryOnly);
    List<Int32> fileNums = new List<Int32>();
    foreach (FileInfo fi in fia)
    {
        Int32 i;
        if (Int32.TryParse(fi.Name.Substring(6, 6), out i))
            fileNums.Add(i);
    }
    var result = fileNums.Select((x, i) => new { Index = i, Value = x })
                .Where(x => x.Index != x.Value)
                .Select(x => (Int32?)x.Index)
                .FirstOrDefault();

    Int32 index;
    if (result == null)
        index = fileNums.Count - 1;
    else
        index = result.Value - 1;

    var nextNumber = fileNums[index] + 1;

    if (nextNumber >= 0 && nextNumber <= 999999)
        return String.Format("Image_{0:D6}", result.Value);

    return null;
}

4 ответа

Решение

Очень простой подход для нахождения первого числа первого разрыва будет следующим:

int[] existingNumbers = /* extract all numbers from all filenames and order them */
var allNumbers = Enumerable.Range(0, 1000000);
var result = allNumbers.Where(x => !existingNumbers.Contains(x)).First();

Это вернет 1 000 000, если все числа были использованы и пробелов не существует.

Недостатком этого подхода является то, что он работает довольно плохо, поскольку он повторяется existingNumbers многократно.

Несколько лучше было бы использовать Zip:

allNumbers.Zip(existingNumbers, (a, e) => new { Number = a, ExistingNumber = e })
          .Where(x => x.Number != x.ExistingNumber)
          .Select(x => x.Number)
          .First();

Улучшенная версия ответа DuckMaestro, которая фактически возвращает первое значение первого пропуска, а не первое значение после первого пропуска, будет выглядеть следующим образом:

var tmp = existingNumbers.Select((x, i) => new { Index = i, Value = x })
                         .Where(x => x.Index != x.Value)
                         .Select(x => (int?)x.Index)
                         .FirstOrDefault();

int index;
if(tmp == null)
    index = existingNumbers.Length - 1;
else
    index = tmp.Value - 1;

var nextNumber = existingNumbers[index] + 1;

Улучшение по сравнению с другим ответом, используйте альтернативную версию Where,

int[] existingNumbers = ...
var result = existingNumbers.Where( (x,i) => x != i ).FirstOrDefault();

Значение i счетчик, начинающийся с 0,

Эта версия where поддерживается в.NET 3.5 ( http://msdn.microsoft.com/en-us/library/bb549418(v=vs.90).aspx).

Это старый вопрос, но было предложено (в комментариях), что вы могли бы использовать .Except() вместо. Мне больше нравится это решение, так как оно даст вам первое пропущенное число (пробел) или следующее наименьшее число в последовательности. Вот пример:

var allNumbers = Enumerable.Range(0, 999999); //999999 is arbitrary. You could use int.MaxValue, but it would degrade performance
var existingNumbers = new int[] { 0, 1, 2, 4, 5, 6 };

int result;
var missingNumbers = allNumbers.Except(existingNumbers);
if (missingNumbers.Any())
  result = missingNumbers.First();
else //no missing numbers -- you've reached the max
  result = -1;

Запуск вышеуказанного кода установит result чтобы:

3

Кроме того, если вы изменили существующие номера на:

var existingNumbers = new int[] { 0, 1, 3, 2, 4, 5, 6 };

Таким образом, нет разрыва, вы получите 7 обратно.

В любом случае, именно поэтому я предпочитаю решение, кроме Zip, - только мои два цента. Спасибо!

var firstnonexistingfile = Enumerable.Range(0,999999).Select(x => String.Format("Image_{0:D6}.{1}", x, strExt)).FirstOrDefault(x => !File.Exists(x));

Это будет повторяться с 0 в 999999, затем выведите результат String.Format() как IEnumerable<string> а затем найти первую строку из этой последовательности, которая возвращает ложь для File.Exists(),

Другие вопросы по тегам