Linq 문제

개선 버전

IEnumerable<int> Extract(IEnumerable<int> numbers)
{
    int? prev = null;
    var toggle = false;
    foreach (var n in numbers)
    {
        prev ??= n;

        if (!toggle && n - prev is 1)
        {
            toggle = true;
            yield return prev.Value;
        }
        else if (toggle && n - prev is not 1)
            toggle = false;

        prev = n;
    }
}

@al6uiz 님 답이 좀 더 코드가 깔끔한 것 같습니다.

Linq로 풀어보려고 했는데 Console 입력 시 생각처럼 동작하지 않네요 -_-a

4개의 좋아요

제가 평소 코딩할 때 예상하는 범위 내에서는 안되는 케이스가 도저히 생각나지 않네요 ㅎ
케이스를 알려주시면 내공 향상에 도움이 될 것 같습니다 ㅎㅎ

4개의 좋아요

콘솔에서 1, 0, 1, 2 를 입력해보세요.

3개의 좋아요

아! FirstOrDefault()이 즉시 실행 계열 함수라 콘솔 입력 같은 특수 시나리오에서 입력을 왜곡시키는군요!

IEnumerable 확장 메서드 내부에서는 즉시 실행 계열 함수를 쓸 때 주의했어야 했는데 짧게 만드는데 집착해서 그만ㅎ

  static IEnumerable<int> Extract(IEnumerable<int> numbers)
  {
      var prev = numbers.FirstOrDefault();
      var prePrev = prev;
      foreach (var current in numbers)
      {
          if (prePrev + 1 != prev && prev + 1 == current)
              yield return prev;
          (prePrev, prev) = (prev, current);
      }
  }

FirstOrDefault()를 사용하지 않는 것이 가장 좋겠지만 간단한 수정으로 foreach 입력에서 Skip(1)을 빼면 일반 시나리오서는 첫 번째 요소를 두 번 보게 되지만 결과는 변하지 않고, 콘솔 시나리오에서는 FirstOfDefault()로 소비된 첫 번째 값을 보상해줄 수 있으므로 위의 코드면 될 것 같네요.

이렇게 또 경험을 얻고 갑니다! 감사합니다ㅎ

5개의 좋아요

저도 많이 고생한 부분입니다. ㅠㅠ

올 때는 맘대로지만, 갈 때는… ㅋㅋ
Linq 로 만들어 주시고 가셔야죠. ^^

4개의 좋아요

:tired_face: 제가 아는 내에서의 결론은 기본 LINQ 만으로 Console 케이스를 만족하는 코드는 불가능하다 입니다 ㅋㅋㅋ

Console 아닌 케이스에서 잘 동작하는 LINQ 코드

static IEnumerable<int> Extract(IEnumerable<int> numbers) => numbers
    .Zip(numbers.Cast<int?>().Prepend(null)
        .Zip(new int?[2].Concat(numbers.Cast<int?>()), (prev, prePrev) => (prev, prePrev)))
    .Where(x => x.Second.prePrev + 1 != x.Second.prev && x.Second.prev + 1 == x.First)
    .Select(x => x.Second.prev.Value);

아마도 입력으로 들어오는 IEnumerable<int> numbers를 두 군데 이상에서 사용하고 있기 때문일 것 같은데요.

@루나시아 님 답변을 참고한 Aggregate() 사용

public static IEnumerable<int> Extract(IEnumerable<int> numbers) => numbers
      .Aggregate<int, (int? prePrev, int? prev, IEnumerable<int> result)>((null, null, []), (x, current) =>
          (x.prev, current, x.prePrev + 1 != x.prev && x.prev + 1 == current ? x.result.Append(x.prev.Value) : x.result)).result;

이것 역시 Aggregate() 메서드의 실행 결과 값인 result 필드를 반환하여 사용하므로 Console서 동작 불가

아마 Aggregate()Select()의 속성을 합친 메서드가 제공된다 가능할 것입니다.

단일 입력 참조를 위한 AggregateSelect() 메서드 사용

Aggregate.cs (dot.net)

Aggregate() 메서드의 구현을 살짝 바꿔 마지막 결과를 반환하지 않고 중간 과정을 yield return 해주는 AggregateSelect() 메서드를 만듭니다.

public static IEnumerable<TAccumulate> AggregateSelect<TSource, TAccumulate>(
    this IEnumerable<TSource> source, 
    TAccumulate seed, Func<TAccumulate, 
    TSource, TAccumulate> func)
{
    TAccumulate result = seed;
    foreach (TSource element in source)
    {
        var current = func(result, element);
        yield return current;
        result = current;
    }
}

이놈을 이용하면

static IEnumerable<int> Extract(IEnumerable<int> numbers) => numbers
    .AggregateSelect(
        (prePrev: default(int?), prev: default(int?), current: default(int?)), 
        (x, current) => (x.prev, x.current, current))
    .Where(x => x.prePrev + 1 != x.prev && x.prev + 1 == x.current)
    .Select(x => x.prev.Value);

Console 입력에도 동작하는 깔끔~한 코드가 나오지만 요렇게 하면 반칙!! :grin:

9개의 좋아요

best of best!

5개의 좋아요
        public static IEnumerable<int> Extract(IEnumerable<int> numbers)
            => numbers.Aggregate<int, (bool append, int val, IEnumerable<int> e)>((false, 0, []), (acc, val) => (acc.val == val, val + 1, acc.val == val && !acc.append ? acc.e.Append(acc.val - 1) : acc.e)).e;

코드를 작성하고 보니 이미 비슷한 답이 있네요

4개의 좋아요

이 문제를 Linq 로 만드는 것은 어려운 일인 것 같습니다.
도움을 주신 회원님들께 감사드립니다.

사실, 제가 풀었던 방식은 아래와 같습니다.

    IEnumerable<int> Extract(IEnumerable<int> numbers)
    {
        bool wasSequencing = false;
        int? previous = default;
        foreach (var current in numbers)
        {
            bool isSequencing = previous + 1 == current;
            if (wasSequencing is false && isSequencing)
                yield return previous!;
            wasSequencing = isSequencing;
            previous = current;
        }
    }

@al6uiz 님의 해결책과 변수의 추상화 정도만 다르고 로직은 같습니다.

다행히 입력 값이 int 이기에, nullable value type 의 lifted operator 의 특성에 기댈 수 있어 좀 더 간단해 보이는 것 뿐입니다.

이 코드를 Linq 로 바꾸는 게 제 능력 상 불가능하더군요. 그래서, 이 문제를 내게 되었습니다.

뿐만 아니라, 이 문제를 풀면서 저 또한 IEnumerable 을 ICollection 으로 취급하거나, 즉시 실행 개열 메서드를 아무 생각 없이 호출하는 실수를 자주 범한다는 점을 발견했습니다.

그러한 경험을 공유하면 좋겠다는 취지도 이 문제를 내게 된 또 다른 이유입니다.

결론적으로, @al6uiz 님의 답변처럼, 아래와 같이 일반화해도 괜찮을 것 같다는 생각을 했봅니다.

 public static IEnumerable<T> SubSequenceHeaders<T> (
   this IEnumerable<T> source, Func<T?, T, bool> sequencingFilter) where T : struct
 {
     bool wasSequencing = false;
     T? previous = default;
     foreach (var current in source)
     {
         bool isSequencing = sequencingFilter(previous, current);
         if (wasSequencing is false && isSequencing)
             yield return previous!;
         wasSequencing = isSequencing;
         previous = current;
     }
 }
var headers = Numbers().SubSequenceHeaders((prev, curr) =>
   prev is null ? false
   : prev + 1 == curr;

foreach( var n in headers)
{
   Console.WriteLine($">>{n});
}
6개의 좋아요