OffsetDateTime , ZonedDateTime

この2つを積極的に使うプロジェクトって、意外と少ない。

LocalDateTime localdatetime = LocalDateTime.now();

// LocalDateTime → ZoneDateTime
ZonedDateTime zoneddatetime = localdatetime.atZone(ZoneId.systemDefault());

// LocalDateTime → OffsetDateTime
OffsetDateTime offsetdatetime = localdatetime.atOffset(ZoneOffset.ofHours(9));

それぞれ、toString() として標準出力すると

2024-04-21T16:49:23.526946600
2024-04-21T16:49:23.526946600+09:00[Asia/Tokyo]
2024-04-21T16:49:23.526946600+09:00

以下は、OffsetDateTime 覚えての以下のインスタンスメソッドは、覚えておくといいかも。
withOffsetSameInstant(ZoneOffset offset)
結果が暗黙の日の同じインスタントを持つようにしながら、指定されたオフセットを使ってこのOffsetTimeのコピーを返します。

withOffsetSameLocal(ZoneOffset offset)
結果が同じローカル時間を持つようにしながら、指定されたオフセットを使ってこのOffsetTimeのコピーを返します。

開始ー終了、あるいは、from-to などのリストの連続性を確認する

任意のfrom-toを表現するオブジェクトとして、

import lombok.Data;

@Data
public class Route{
    private Integer from;
    private Integer to;
}

こんなクラスがあったとする。これのリストのパターンとして、
想定している Route のリスト、連続性が成立している

From To
0 9
10 19
20 29
30 null

重なりがある(=20)

From To
0 9
10 20
20 29
30 null

抜けがある(=19)

From To
0 9
10 18
20 29
30 null

これら Route のリストの連続性、重なりの存在、抜けの存在をチェックする方法で
考えた方法が、Stream の reduce を使う方法、AtomicBoolean を使わなければスッキリするのだが。
もっと綺麗な方法が思いつかない。

List<Route> list; // ←ここに何かの処理で格納される。
AtomicBoolean result = new AtomicBoolean(true);
list.stream().reduce(new Route(), (a, b)->{
    result.set(Boolean.logicalAnd(res.get(), a.getTo()==null
            ? true
            : (b.getFrom()-a.getTo())==1));
    return b;
});

// result.get()==true なら、連結に重なりも抜けはない。連続性が想定どおり
// result.get()==false なら、連結に重なりか抜けがある。

DBeaver のSQL書式設定

DBeaverを使っているのですが、
select などの文字を大文字にするか、小文字にするか、
これら、SQLの句を圧倒的に小文字に書く人が多いのですが、
個人的には、大文字派です。
DBeaverで自動で補完入力する時、小文字が厭で大文字にするなら、
「設定」画面、SQL書式設定で、次のように、Upper を選択します。

Spring DATA JPA Specification

org.springframework.data.jpa.repository.JpaSpecificationExecutor

Specification を使って以下のメソッドで動的クエリを生成できる。

long count(Specification<T> spec)

boolean exists(Specification<T> spec)

List<T> findAll(Specification<T> spec)

Page<T> findAll(Specification<T> spec, Pageable pageable)

List<T> findAll(Specification<T> spec, Sort sort)

T findOne(Specification<T> spec)

でも、@Query で、native Query の使用と、Specification を同時には使えない。

CSV形式の1行分の文字列からList<String>への変換をinterface method にする。

先日書いた、CSV形式の1行分の文字列からList<String>への変換 - Oboe吹きプログラマの黙示録
を単純に、interface のメソッドにする。

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

public interface Csvtolist extends Serializable{

    public static Function<String, List<String>> of(){
        return Csvtolist::create;
    }
    
    public static List<String> create(String str){
        char DELIMITER = ',';
        char CARRIAGE_RETURN = '\r';
        char NEWLINE = '\n';
        char DOUBLE_QUOTE = '"';
        if (str==null || str.length()==0){
            return Collections.emptyList();
        }
        List<String> tokens = new ArrayList<String>();
        StringBuilder tokenBuf = new StringBuilder();
        boolean insideDoubleQuote = false;
        boolean isDoubleQuoteEscapeActive = false;
        StringBuilder wspBuf = new StringBuilder();
        for(int ii=0; ii < str.length(); ii++){
            final char ch = str.charAt(ii);
            if (ch==CARRIAGE_RETURN || ch==NEWLINE){
                if (insideDoubleQuote){
                    tokenBuf.append(ch);
                }else{
                    throw new RuntimeException("unquoted "
                     + (ch=='\n' ? "newline" : "carriage return")
                     + " found at position #" + (ii+1));
                }
            }else if(ch==DOUBLE_QUOTE){
                if (insideDoubleQuote){
                    if (isDoubleQuoteEscapeActive){
                        tokenBuf.append(ch);
                        isDoubleQuoteEscapeActive = false;
                    }else if(((ii+1) < str.length()) && str.charAt(ii+1)==DOUBLE_QUOTE){
                        isDoubleQuoteEscapeActive = true;
                    }else{
                        insideDoubleQuote = false;
                    }
                }else{
                    insideDoubleQuote = true;
                    if (wspBuf.length() != 0){
                        if (tokenBuf.length() != 0){
                            tokenBuf.append(wspBuf);
                        }
                        wspBuf.delete(0, wspBuf.length());
                    }
                }
            }else{
                if (insideDoubleQuote){
                    tokenBuf.append(ch);
                }else{
                    if (ch==DELIMITER){
                        tokens.add(tokenBuf.toString());
                        tokenBuf.delete(0, tokenBuf.length());
                        wspBuf.delete(0, wspBuf.length());
                    }else if(Character.isWhitespace(ch)){
                         wspBuf.append(ch);
                    }else{
                        if (wspBuf.length() != 0){
                            if (tokenBuf.length() != 0){
                                tokenBuf.append(wspBuf);
                            }
                            wspBuf.delete(0, wspBuf.length());
                        }
                        tokenBuf.append(ch);
                    }
                }
            }
        }
        if (insideDoubleQuote){
            throw new RuntimeException("terminating double quote not found");
        }
        tokens.add(tokenBuf.toString());
        return tokens;
    }
}

of() で、Function<String, List<String>> を返すようにしているので、
Optional の map で使用できる。

List<String> list = Optional.ofNullable(str).map(Csvtolist.of()).orElse(List.of());

結局、以下のように書いても同じではあるけど。

List<String> list = Optional.ofNullable(str).map(Csvtolist::create).orElse(List.of());

GROUP_CONCAT からCSVにする。

MySQL GROUP_CONCAT あるいは、PostgreSQLSTRING_AGG から、CSV1行を作る。
作ってSELECTした文字列は、先日書いた原始的なメソッド
CSV形式の1行分の文字列からList<String>への変換 - Oboe吹きプログラマの黙示録
を使って、List<String> を取得する。

任意の列で、GROUP BY して、GROUP_CONCAT / STRING_AGG で join する時に
RFC4180 準拠、カンマ区切り、ダブルクォーテーション括りにする。

MySQL の場合、、、

対象列が、NOT NULL制約がある場合、

CONCAT('"',
       GROUP_CONCAT(REGEXP_REPLACE(task_name, '"', '""')
                    ORDER BY create_date DESC 
                    SEPARATOR '","'),
       '"') AS task_name_csv

対象列が、NOT NULL制約がない場合、

CONCAT('"',
       GROUP_CONCAT(REGEXP_REPLACE(COALESCE(task_name, ''), '"', '""')
                    ORDER BY create_date DESC
                    SEPARATOR '","'),
       '"') AS task_name_csv

PostgreSQLの場合、、、

対象列が、NOT NULL制約がある場合、

'"' || STRING_AGG(REGEXP_REPLACE(task_name, '"', '""', 'g'), '","' ORDER BY create_date DESC)
    || '"' AS task_name_csv

対象列が、NOT NULL制約がない場合、

'"' || STRING_AGG(REGEXP_REPLACE(COALESCE(task_name, ''), '"', '""', 'g'), '","' ORDER BY create_date  DESC)
    || '"' AS task_name_csv

しかし、PostgreSQL には、列の型に、ARRAY型が存在するわけで、
PostgreSQL は、わざわざ、このようなことはしないで、ARRAY型にして SELECT すれば、
こんなCSVを生成した結果をSELECTで取り込んで、
先日書いたJavaのメソッドでリストにするなんてことをする必要性はない。

ARRAY_AGG(task_name ORDER BY create_date DESC)

あるいは、

ARRAY_AGG(COALESCE(task_name, '') ORDER BY create_date DESC)

GROUP_CONCAT結果をCSVにするなんて、MySQLだけしか有効性はないであろう。
=============================================================
ARRAY型で、思い起こせばこんなことを書いていた。。。
int[] → ArrayTypeHandler - Oboe吹きプログラマの黙示録
配列型に対するmybatis の TypeHandler よく使いそうなものを作った - Oboe吹きプログラマの黙示録
arraytypehandler · yipuran/yipuran-mybatis Wiki · GitHub

CSV形式の1行分の文字列からList<String>への変換

RFC4180準拠、カンマ区切り、括り文字はダブルクォーテーションの1行分の文字列
から、List<String> に変換する原始的なロジックで書いたメソッドを
ある目的の為に書きました。
Java8以上のコードではなく、見にくくても敢えて原始的なロジックで書いています。
",,"や、”, ,” のように、カンマ区切り並んだ場合は、””空文字列をListに詰め込みます。

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public static List<String> csvlineToList(String line) {
    char DELIMITER = ',';
    char CARRIAGE_RETURN = '\r';
    char NEWLINE = '\n';
    char DOUBLE_QUOTE = '"';
    if (line==null || line.length()==0){
        return Collections.emptyList();
    }
    List<String> tokens = new ArrayList<String>();
    StringBuilder tokenBuf = new StringBuilder();
    boolean insideDoubleQuote = false;
    boolean isDoubleQuoteEscapeActive = false;
    StringBuilder wspBuf = new StringBuilder();
    for(int ii=0; ii < line.length(); ii++){
        final char ch = line.charAt(ii);
        if (ch==CARRIAGE_RETURN || ch==NEWLINE){
            if (insideDoubleQuote){
                tokenBuf.append(ch);
            }else{
                throw new RuntimeException("unquoted " + (ch=='\n' ? "newline" : "carriage return") +
                        " found at position #" + (ii+1));
            }
        }else if(ch==DOUBLE_QUOTE){
            if (insideDoubleQuote){
                if (isDoubleQuoteEscapeActive){
                    tokenBuf.append(ch);
                    isDoubleQuoteEscapeActive = false;
                }else if(((ii+1) < line.length()) && line.charAt(ii+1)==DOUBLE_QUOTE){
                    isDoubleQuoteEscapeActive = true;
                }else{
                    insideDoubleQuote = false;
                }
            }else{
                insideDoubleQuote = true;
                if (wspBuf.length() != 0){
                    if (tokenBuf.length() != 0){
                        tokenBuf.append(wspBuf);
                    }
                    wspBuf.delete(0, wspBuf.length());
                }
            }
        }else{
            if (insideDoubleQuote){
                tokenBuf.append(ch);
            }else{
                if (ch==DELIMITER){
                    tokens.add(tokenBuf.toString());
                    tokenBuf.delete(0, tokenBuf.length());
                    wspBuf.delete(0, wspBuf.length());
                }else if(Character.isWhitespace(ch)){
                     wspBuf.append(ch);
                }else{
                    if (wspBuf.length() != 0){
                        if (tokenBuf.length() != 0){
                            tokenBuf.append(wspBuf);
                        }
                        wspBuf.delete(0, wspBuf.length());
                    }
                    tokenBuf.append(ch);
                }
            }
        }
    }
    if (insideDoubleQuote){
        throw new RuntimeException("terminating double quote not found");
    }
    tokens.add(tokenBuf.toString());
    return tokens;
}