y_yo43
@y_yo43

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

24時間フルの時間空白チェック&重複チェックのロジックについて

解決したいこと

入力される時間範囲全ての組み合わせで時間重複がないかをチェックするプログラムを作成したいです。(組み合わせ条件はnC2)

時間範囲が24時間ではない重複チェックプログラムは沢山あるのですが、時間範囲が24時間になった時にどうも上手く行きませんでした。

入力条件

  • n組の範囲時間が入力される。
  • 空白時間が存在できない
  • 重複時間も存在できない
  • 時間範囲は0-24時間
  • 分単位で時間指定可
  • 日跨ぎ有り

(OK例 : 09:00-18:00 , 18:00-09:00)
(OK例 : 08:30-15:30 , 15:30-22:00 , 2200:-08:30)

(NG例 : 09:00-18:00 , 18:00-03:00[27時]) ※空白時間あり
(NG例 : 08:30-15:30 , 19:00-22:00 , 22:00-10:00[34時]) ※空白時間と重複時間あり

自分で試したこと

下記の時間組み合わせを想定して式を組みました。(s:StartTime , e:EndTime)

[A] s1|------------------|e1
                     s2|---------------------|e2

[B]                s1|---------------------|e1
       s2|------------------|e2

[C] s1|------------------|e1
              s2|--------|e2

[D]        s1|--------|e1
      s2|------------------|e2

  • 組んだ式

e1 =< s2 && e2 =< s1

エラーの組み合わせ

正しいのにエラーになる例 : (1) 08:30-15:30 , (2) 15:30-22:00 , (3) 2200:-08:30
[NG] (1)と(2):15:30 =< 15:30 && 22:00 =< 08:30
[OK] (1)と(3):22:00 =< 22:00 && 08:00 =< 15:30
[OK] (2)と(3):15:30 =< 22:30 && 08:30 =< 08:30

追記

皆さん色々なご指摘ありがとうございます。以下ご指摘された条件になります。
※個人開発なので細かい仕様とか要件を出してます。

利用言語:Java17
フレームワーク:SpringBoot 3.2.0

現在、JSONで情報をPOSTできるよう下記のようなFormクラスをListで持たせて、Contllerクラスで@RequestBodyを利用してフォーマットを指定しています。

import lombok.Getter;

@Getter
public class TimeSectionRegisterForm {
    
    String startTime;
    String endTime;
    
    public TimeSectionRegisterForm(String startTime, String endTime) {
        this.startTime = startTime;
        this.endTime = endTime;
    }
    
}
import java.util.Collections;
import java.util.List;

import lombok.Getter;

@Getter
public class TimeSectionsRegisterForm {
    
    List<TimeSectionRegisterForm> list;

    public List<TimeSectionRegisterForm> asList(){
        return Collections.unmodifiableList(list);
    }
    
}
@RestController
public class TimesectionController {

    @PostMapping("/")
    public ResponseEntity<URI> register(@RequestBody TimeSectionsRegisterForm timeSections){
        //処理
    }
}

入力条件

  • 入力条件に「n組の範囲時間が入力される。」とあるが、n=0やn=1はありえるのか。
    システム的に時間の範囲を登録しないといけないためn=0はないです(0 < n)。
    一応ユーザから入力してもらう想定なので、今の所n=1やn=5などもありえます。
    (入力制限を付けるかどうかは実装次第になってます。)

  • 範囲時間の開始と終了が同じだった場合、その間は0時間扱いなのか24時間扱いなのか
    今回、日付の概念は持たせないのでその辺りの処理をどうしようか迷っています。
    例えば、(00:00-00:00)だった場合は(00:00-24:00)と同義かなと個人的には考えています。
    ([1/1の] 09:00 - [1/2の]09:00)の場合でも内部的には+24した方がいいのかなと考えていました。
    それか、n=1の場合は終了時刻から1分引く処理を追加するとかでしょうか。

  • 範囲時間は必ず順番通りに渡されるのか。例えば、08:30-15:30, 2200:-08:30, 15:30-22:00のような順番でもOKとみなすのか。
    想定していませんでした。
    今個人的に考えている要件としては順番があべこべでも重複&空白が無ければ問題無いです。なので、そのままデータをDBに登録します。

0

6Answer

どの言語で書いているのか分からないので的外れになるかもしれませんが
とりあえず 時間:分 だと扱いづらいので、全部 分 に変換してからそれぞれの時間の境界をチェックしていけば空白チェックと重複チェックはできますよね
あとは 最後の時間 - 最初の時間 が 24*60=1440 であれば24時間フルに埋まってることになります

0Like

Comments

  1. @y_yo43

    Questioner

    回答ありがとうございます。色々と追記しました。
    全部分に変換する方法は考えたことがありませんでした。
    確かに、重複した時間をreturnはしないので全てを分に変換して加減算で値を色々したらいい感じにできそうな気がしますね。

    例えば、(09:00-18:00 , 18:00-09:00)ならそれぞれ(540分, 900分)に変換して合計が1440であれば重複はないし空白もないということですよね。

    (08:30-15:30 , 15:30-22:00 , 2200:-08:30)ならそれぞれ(420分 , 390分 , 630)の合計が1440なので問題なしと。

言語とフレームワークは何でしょう? フレームワークによっては日時を表す型があって、それによって話が変わってきます。

それともそんな話は関係なくて、単純に文字列として e1 =< s2 && e2 =< s1 というような式を使って比較したいということですか?

0Like

Comments

  1. @y_yo43

    Questioner

    回答ありがとうございます。色々と追記しました。

    単純に文字列として e1 =< s2 && e2 =< s1 というような式を使って比較したいということですか?

    いいえ、もっと良い処理方法があればそちらを使いたいと思っています。
    個人的にここまで考えましたが...という意味で記載しました。

入力条件が不足しているように思います。以下の場合はどうなるのでしょうか。

  • 入力条件に「n組の範囲時間が入力される。」とあるが、n=0やn=1はありえるのか。
  • 範囲時間の開始と終了が同じだった場合、その間は0時間扱いなのか24時間扱いなのか。
  • 範囲時間は必ず順番通りに渡されるのか。例えば、08:30-15:30, 2200:-08:30, 15:30-22:00のような順番でもOKとみなすのか。
0Like

Comments

  1. @y_yo43

    Questioner

    回答ありがとうございます。
    入力条件について追記しました。

もし、.NET6以降が可能でしたら、TimeOnly 構造体が便利かもしれません。

open System

let t1 = TimeOnly(23, 0)
let t2 = TimeOnly(1, 0)
let span = TimeSpan(2, 0, 0)

t2 - t1 = span // true

ひとつの期間を(開始時刻, 終了時刻)のように、TimeOnlyのタプルで表現した場合、次のようなコードが考えられます。F#ですが、C#でもLINQを活用すれば似たような雰囲気になるかと思います。

F#による例
open System

let isContinuous (periods: (TimeOnly * TimeOnly) list) =
    match periods with
    | []
    | [ _ ] -> true
    | p ->
        p
        |> List.sortBy (fun (s, _) -> s)
        |> List.pairwise
        |> List.forall (fun ((_, e), (s, _)) -> e = s)

let totalSpan (periods: (TimeOnly * TimeOnly) list) =
    periods |> List.map (fun (s, e) -> e - s) |> List.reduce (+)

let isContinuousTotal expected periods =
    totalSpan periods = expected && isContinuous periods
0Like

Comments

  1. あ、0時から24時(翌日の0時)とか、8時から翌日の8時...みたいな、単体で24時間になるような期間は、TimeOnlyの引き算で表現できないので、そのようなケースがある場合は、別途考える必要があります。

  2. @y_yo43

    Questioner

    回答ありがとうございます。回答を元に色々と追記しました。
    F#やC#を使ったことがなくTimeOnly構造体のタプルというものを知りませんでした...
    Javaでいうところ、Timeクラスの内部で複数のデータを保持できて順序を管理できるものという理解で大丈夫でしょうか...?(こちら参照しました。)

    今の所最低2組の時間を受け付ける想定でしたが、1組だけ来るケースもありえます。
    これは最低2組の時間を送るよう要件を変えてしまっても問題ないです。

  3. .NETのTimeOnlyに相当するものは、Javaだと、java.time.LocalTimeのようです。
    タプルの認識はコメントいただいたもので合っているのですが、Java標準に相当するものがあるのかは、私にはわかりませんでした。

    ただ、今回のように、「開始時刻と終了時刻が同じ場合、24時間とみなしたい」など、特殊な要件がある場合は、ただのタプルで表現してしまうと、その「24時間とみなすロジック」が色々なところに散ってしまうので、どのみちクラスなどで表現したほうが良いのかもしれません。

    問題を小さく分割すると

    1. ひとつの期間を表現する方法
    2. 複数の期間が重複なく連続しているかを判定する方法
    3. 複数の期間の合計を求める方法

    があり、このコメントの前半は、1.にあたります。

    2.に関しては、複数の期間を開始時刻昇順でソートしたうえで、すべてが「ひとつ前の終了時刻 = 開始時刻」を満たすかどうかを検査することが、方法のひとつとして考えられます。
    ※要素数が1であれば、連続とみなす。

  4. TimeRange.java
    import java.time.LocalTime;
    import java.time.Duration;
    
    public record TimeRange(LocalTime start, LocalTime end) {
        public Duration duration() {
            var between = Duration.between(start, end);
            var day = Duration.ofDays(1);
            if (between.isZero()) return day;
            if (between.isNegative()) return day.plus(between);
            return between;
        }
    }
    
    TimeRangeService.java
    import java.time.Duration;
    import java.util.Comparator;
    import java.util.List;
    import java.util.stream.Gatherers;
    
    public class TimeRangeService {
        public static boolean isContinuous(List<TimeRange> ranges) {
            return
                ranges.size() == 1 ||
                ranges
                .stream()
                .sorted(Comparator.comparing(TimeRange::start))
                .gather(Gatherers.windowSliding(2))
                .allMatch(w -> Duration.between(w.getFirst().end(), w.getLast().start()).isZero());
        }
    
        public static Duration totalDuration(List<TimeRange> ranges) {
            return
                ranges
                .stream()
                .map(r -> r.duration())
                .reduce(Duration.ZERO, (state, current) -> state.plus(current));
        }
    
        public static boolean isContinuousTotal(Duration expected, List<TimeRange> ranges) {
            return totalDuration(ranges).equals(expected) && isContinuous(ranges);
        }
    }
    

    細かいテストはしていないので、雰囲気だけでも参考になれば。

  5. @y_yo43

    Questioner

    返信遅くなりすみません。
    例示していただいたコードが分からない部分が多かったため調べていました。
    (イメージコードありがとうございます。理解すると非常に分かりやすくてイメージがしやすかったですm__m)

    ただのタプルで表現してしまうと、その「24時間とみなすロジック」が色々なところに散ってしまうので、どのみちクラスなどで表現したほうが良いのかもしれません。

    私もモデルに時間を管理するRecordクラスを作成してルールを書くのが良いと思いました。
    StreamAPIも(そもそも関数型プログラミングを)使ったことが無かったので、これを機に少し触ってみようと思います!

  6. あとは、やはり、日付を跨ぐ場合があるのに、入力が時刻のみというのは不安なので、もし、リクエストを日時の組にできるなら、それに越したことはないと思います。
    将来、曜日や祝日によって分岐したいとかなっても対応できる可能性が残せますし。

  7. @y_yo43

    Questioner

    今回は、時間帯によって条件を変えるだけという要件なので日付を持たせることができないのですよね...。

    それとも入力情報は時間だけだけど日付を跨いでいた場合、内部的には日時として扱うとかの方がいいのですかね(まだまだ力不足ゆえ判断ができませんでした)

    おそらくですが判断基準としては、[平日->祝日]の日跨ぎとかが発生するかどうかですよね。そうなると日時の情報で処理したほうが拡張性があると。
    そうなると今回はそういったケースが無いので日付の情報は不要と思いました。
    (個人開発なんだからその辺は好きにやって〜って感じだとは思いますが)

    一応時間を扱うモデルとしてこのような感じで実装してみました。
    (ほぼほぼイメージで書いて頂いたコードそのままですが...)
    Java17にgatheres#windowSlidingが無かったのでGPTで同等の機能を持つメソッドを考えてもらいました。

    import java.time.Duration;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    
    public class TimeRanges {
        
        List<TimeRange> list;
    
        public TimeRanges(List<TimeRange> list) {
            if(!isContinuousTotal(Duration.ofHours(24), list)){
                throw new IllegalArgumentException("時間範囲に誤りがあります。");
            }
            this.list = list;
        }
    
        public List<TimeRange> asList(){
            return Collections.unmodifiableList(this.list);
        }
    
        public static TimeRanges from(List<TimeRange> list){
            return new TimeRanges(list);
        }
    
    
        private static boolean isContinuous(List<TimeRange> ranges){
            ranges.stream().sorted(Comparator.comparing(TimeRange::start));
            
            Stream<List<TimeRange>> slidingRanges = windowSliding(ranges, 2, 1);
    
            return slidingRanges.allMatch(v -> Duration.between(v.get(0).end(), v.get(1).start()).isZero());
        }
    
    
        private static Duration totalDuration(List<TimeRange> ranges){
            return ranges
                    .stream()
                    .map(v -> v.duration())
                    .reduce(Duration.ZERO, (state, current) -> state.plus(current));
        }
    
    
        private boolean isContinuousTotal(Duration expected, List<TimeRange> ranges){
            return totalDuration(ranges).equals(expected) && isContinuous(ranges);
        }
    
    
        public static <T> Stream<List<T>> windowSliding(List<T> source, int windowSize, int step) {
            if (windowSize <= 0 || step <= 0) {
                throw new IllegalArgumentException("Window size and step must be greater than 0");
            }
            return IntStream.range(0, (source.size() - windowSize) / step + 1)
                            .mapToObj(i -> source.subList(i * step, Math.min(i * step + windowSize, source.size())))
                            .filter(subList -> subList.size() == windowSize);
        }
    }
    

> 順番があべこべでも重複&空白が無ければ問題無い

  • [08:00-04:00, 04:00-08:00] //08:00~32:00
  • [04:00-08:00, 08:00-04:00] //04:00~28:00
    などは区別しなくてもよいのでしょうか?

重複,空白,24時間ちょうど等のバリデーションという観点に限ればどちらも一緒なのでしょうが
そもそもの要求次第ではこれが問題となる場合もあるのではないかな、と。

そもそもの要求&入力仕様

  1. 「n組の時間範囲が必要」&「たまたまそれらが空白なし&重複なし&全体範囲が24時間であるか確認したい」
  2. 「空白なし&重複なし&全体範囲が24時間であるような、n組の時間範囲が必要」

これが 1. であれば現在の仕様でよいと思うのですが
入力条件からすると 2. なのでは?という気がします。

つまり ['08:30', '15:30', '22:00'] のような開始時間の組としての入力があれば
['08:30-15:30', '15:30-22:00', '22:00-32:30'] という時間範囲の組が一意に定まります。

こちらの方が入力が手間でない&そもそも正しい範囲の形でしか入力できないというメリットがあります。
仮に 2. である&入力仕様自体を変更できてしまうのなら、検討されてみてはいかがでしょうか

0Like

Comments

  1. @y_yo43

    Questioner

    ご意見ありがとうございます。

    [08:00-04:00, 04:00-08:00] //08:00~32:00
    [04:00-08:00, 08:00-04:00] //04:00~28:00
    などは区別しなくてもよいのでしょうか?

    これは

    • [08:00-04:00] [04:00-08:00] //08:00-28:00と28:00-32:00の24時間
      もしくは
    • [04:00-08:00] [08:00-04:00] //04:00-08:00と08:00-28:00の24時間
      の2つの組になり、それぞれの時間の範囲を表現しているだけなので問題ないと思います。今回の要件的にも欲しいのは時間の範囲のみなので、重複,空白,24時間ちょうど等のバリデーションという観点だけなので今のところは問題ないと思いました。
      もちろん要件次第だと前後関係も気にしないとダメなパターンもあると思います。

    ['08:30', '15:30', '22:00'] のような開始時間の組としての入力があれば
    ['08:30-15:30', '15:30-22:00', '22:00-32:30'] という時間範囲の組が一意に定まります。

    この案とてもいいなと思いました。
    しかし今回ユーザに入力してもらうのが時間の範囲なので、開始時刻のみ入力するとなると少し違和感がありました。
    フロント側で終了時刻を入力したら次の組の開始時刻が自動で入力されるような機能で実装してみようと思います。

以下はいかがでしょうか。

Javaプログラム内で一時的なDBを作るようなイメージです。
要はコマを配列にして、きれいに上から順番に並ぶかどうか確認する。という考え方です。
DBの形式は適宜変更ください。

入力値
 09:00-18:00 , 18:00-09:00

設定
 1コマの単位
  例)1時間1コマ
    30分1コマ
    15分1コマ

プログラムの処理
 正常値のパターン
 ①入力値をDB化
  09:00-18:00,18:00-09:00
   ↓
   連番,コマ,入力値,入力コマ
   1, 09:00,09:00-18:00,1
2, 10:00,09:00-18:00,1
3, 11:00,09:00-18:00,1
4, 12:00,09:00-18:00,1
5, 13:00,09:00-18:00,1
6, 14:00,09:00-18:00,1
7, 15:00,09:00-18:00,1
8, 16:00,09:00-18:00,1
9, 17:00,09:00-18:00,1
10,18:00,09:00-18:00,1
11,19:00,18:00-09:00,2
12,20:00,18:00-09:00,2
13,21:00,18:00-09:00,2
14,22:00,18:00-09:00,2
15,23:00,18:00-09:00,2
16,00:00,18:00-09:00,2
17,01:00,18:00-09:00,2
18,02:00,18:00-09:00,2
19,03:00,18:00-09:00,2
20,04:00,18:00-09:00,2
21,05:00,18:00-09:00,2
22,06:00,18:00-09:00,2
23,07:00,18:00-09:00,2
24,08:00,18:00-09:00,2
 ②空白チェック
  DBの数が24の場合かチェック
  24なら、問題なし

 ③重複チェック
  上記DBの2項目(コマ)の重複を確認
  問題なし

 異常値のパターン
  18:00-19:00が抜けている場合
 ①入力値をDB
  09:00-18:00,19:00-09:00
   ↓
   連番,コマ,入力値,入力コマ
   1, 09:00,09:00-18:00,1
2, 10:00,09:00-18:00,1
3, 11:00,09:00-18:00,1
4, 12:00,09:00-18:00,1
5, 13:00,09:00-18:00,1
6, 14:00,09:00-18:00,1
7, 15:00,09:00-18:00,1
8, 16:00,09:00-18:00,1
9, 17:00,09:00-18:00,1
10,19:00,19:00-09:00,2
11,20:00,19:00-09:00,2
12,21:00,19:00-09:00,2
13,22:00,19:00-09:00,2
14,23:00,19:00-09:00,2
15,00:00,19:00-09:00,2
16,01:00,19:00-09:00,2
17,02:00,19:00-09:00,2
18,03:00,19:00-09:00,2
19,04:00,19:00-09:00,2
20,05:00,19:00-09:00,2
21,06:00,19:00-09:00,2
22,07:00,19:00-09:00,2
23,08:00,19:00-09:00,2
24,null ,null ,null

 ②空白チェック
  配列の数が24に満たないのでNG。

 ③重複チェック
  問題なし

  17:00-18:00が重複
 ①入力値をDB
  09:00-18:00,17:00-09:00
   ↓
   連番,コマ,入力値,入力コマ
   1, 09:00,09:00-18:00,1
2, 10:00,09:00-18:00,1
3, 11:00,09:00-18:00,1
4, 12:00,09:00-18:00,1
5, 13:00,09:00-18:00,1
6, 14:00,09:00-18:00,1
7, 15:00,09:00-18:00,1
8, 16:00,09:00-18:00,1
9, 17:00,09:00-18:00,1
10,17:00,17:00-09:00,2
11,18:00,19:00-09:00,2
12,19:00,19:00-09:00,2
13,20:00,19:00-09:00,2
14,21:00,19:00-09:00,2
15,22:00,19:00-09:00,2
16,23:00,19:00-09:00,2
17,00:00,19:00-09:00,2
18,01:00,19:00-09:00,2
19,02:00,19:00-09:00,2
20,03:00,19:00-09:00,2
21,04:00,19:00-09:00,2
22,05:00,19:00-09:00,2
23,06:00,19:00-09:00,2
24,07:00,19:00-09:00,2
 ②空白チェック
  問題なし

 ③重複チェック
  17のコマが二つあるので問題あり。

要はコマを配列にして、きれいに上から順番に並ぶかどうか確認する。という考え方です。

0Like

Comments

  1. amateさんと考え方は近いと思います。

Your answer might help someone💌