【问题标题】:Does Linq provide a way to easily spot gaps in a sequence?Linq 是否提供了一种方法来轻松发现序列中的空白?
【发布时间】:2013-06-30 03:33:52
【问题描述】:

我正在管理一个文件目录。每个文件的命名类似于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;
}

编辑:如果它对任何人有帮助,这是最后的方法,结合 Daniel Hilgarth 的回答:

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;
}

【问题讨论】:

  • 您真的需要增加数字,还是只需要唯一的文件名?获取唯一文件名的方法比您使用的方法更简单。
  • @Servy 你的意思是像Path.GetTempFileName() 这样的东西吗?我考虑过这个选项,但我需要确保用户可以在必要时在我们的应用程序之外找到文件,所以我选择了“平均乔”更易读的东西。
  • 只需在前面粘贴yymmdd 格式的日期即可。如果您还将 6 位数字附加到日期,则可以在到达 999999 时将数字换行。除非您在一天内创建超过 999999 个文件,否则不会给您带来任何冲突。 (它会在 2099 年给你带来麻烦,但你可能不会太担心...... ;)
  • 这实际上正是我的想法,但即使你确实想要一些更具可读性的东西,你也有更好的选择。例如,将创建的时间戳附加到问题的末尾(只要确保有足够的精度以确保仍然没有重复并处理由于夏令时而出现的重复情况(可能通过使用没有 DST 的位置))。除了这些之外,还有其他选项仍然比你拥有的更容易处理,我的问题仍然是,它们是一个选项,还是你必须使用这个系统?
  • 10,000,000 个文件仍然没有问题吗?还是一亿?换句话说:只需增加您使用的位数,直到您可以确定不会出现回绕。

标签: c# linq .net-3.5 sequences


【解决方案1】:

找到第一个间隙的第一个数字的一​​个非常简单的方法如下:

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;

【讨论】:

  • 我看到Zip 是后来的介绍(我认为是.NET 4.0?)。不幸的是,这个应用程序在短期内停留在 .NET 3.5 中。
  • @DonBoitnott:确实,它已在 .NET 4 中引入。您可以通过枚举器自己创建它。但我认为你应该使用DuckMaestro提出的解决方案。
  • 我不确定最后的编辑,但总的来说,我认为你们两个为我提供了一个非常好的解决方案。请参阅我最新的 OP 编辑​​,了解我 认为 将是最终的代码修改。顺便说一句...tmp 正在返回是间隙的值,而不是之前或之后。
  • @DonBoitnott:在我的版本中,它返回间隙的第一个值,同时返回第一个值 afterindex第一个差距。后面带有if 的附加代码说明了没有间隙的情况。您的最终编辑没有考虑这种情况,因此甚至无法编译。
  • 不编译?当然可以。然而,我确实明白你关于最后一点的必要性的观点。当没有间隙时,返回null。因此,您需要进行额外检查才能获得下一个数字。然后我验证 nextNumber&lt;= 999999 以保持我正在寻找的范围。
【解决方案2】:

改进另一个答案,使用Where 的替代版本。

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

i 是一个从0 开始的计数器。

.NET 3.5 (http://msdn.microsoft.com/en-us/library/bb549418(v=vs.90).aspx) 支持此版本的 where

【讨论】:

  • 您能解释一下为什么这是一项改进吗?我不是 Linq 专家。并建议我在i 上围绕这个包裹一个for 循环?
  • @DonBoitnott:如您所见,i 是您传递给Where 的 lambda 参数之一。它将是当前项目的索引,并由 LINQ 传递给您的 lambda。这里没有for 循环。如果仍然不清楚,请阅读 LINQ :) 这是对我的答案的改进,因为它仅在需要时迭代 one 集合。它不会比这更好 - 我会在我的代码中使用这种方法而不是我建议的方法。
  • @DanielHilgarth 那么这里的最终价值是什么?你的有一个固定的范围(0、1000000),但这似乎只是在existingNumbers 中循环。所以我会得到第一个差距值,或者下一个可用值,或者......?什么?
  • @DonBoitnott:好问题。它会给你差距之后的第一个值,我意识到这并没有真正的帮助。要真正获得差距的第一个数字,您需要做更多这样的事情:var index = existingNumbers.Select((x, i) =&gt; new { Index = i, Value = x }).Where(x =&gt; x.Index != x.Value).Select(x =&gt; x.Index).FirstOrDefault() - 1; if(index == -1) index = existingNumbers.Length - 1; var nextNumber = existingNumbers[index] + 1;
  • @DonBoitnott:我更新了我的答案以包含与此类似的代码。
【解决方案3】:
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&lt;string&gt;,然后从该序列中找到第一个为File.Exists() 返回false 的字符串。

【讨论】:

  • 我试过了,它返回了确实存在的第一个文件。我目录中的文件在Image_000000.pngImage_000038.png 之间。唯一的差距是Image_000007,但我回来了Image_000000.png
【解决方案4】:

这是一个老问题,但有人建议(在 cmets 中)您可以改用 .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 解决方案之外的原因 - 只是我的两美分。 谢谢!

【讨论】:

  • 这有一个缺点,它枚举missingNumbers 两次(至少到第一个间隙),因此执行Except 两次。没有此问题的解决方案如下所示:var missingNumbers = allNumbers.Except(existingNumbers).GetEnumerator(); if (missingNumbers.MoveNext()) result = missingNumbers.Current; else //no missing numbers -- you've reached the max result = -1;
  • @DanielHilgarth 所以性能问题(枚举两次)是由于调用 .Any() 方法引起的?
  • 这是由于在 missingNumbers 上调用了两个方法,这两个方法都迭代了查询。记住:LINQ 是使用延迟执行实现的
猜你喜欢
  • 2016-09-24
  • 1970-01-01
  • 2020-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-30
  • 2010-12-08
  • 1970-01-01
相关资源
最近更新 更多