読者です 読者をやめる 読者になる 読者になる

リストの順序を保持したグルーピング

リストをグルーピング、しかしあくまでも順序付けされた並びの中でのグルーピングで同じキーのグループが複数あっても
まとめない。
という要求があった時、少し悩んだ。

サンプルとなるお題、、、要素のキーが以下のようにならんでいる場合、

A , A , A , B , B , A , A , D , C , C , E , E

求めたい結果は、順序どおりのグルーピングで、リストの INDEX とカウントで以下のような結果が欲しいのが
今回のお題。

index = 0   count = 3  → A , A , A の3個をカウント
index = 3   count = 2  → B , B     の2個をカウント
index = 5   count = 2  → A , A     の2個をカウント
index = 7   count = 1  → D         の1個をカウント
index = 2   count = 2  → C , C     の2個をカウント
index = 2   count = 2  → E , E     の2個をカウント

このように、A のキーをグルーピングといっても隣接だけのグルーピングでカウントする、つまりリストの並びに沿う。

何の目的で、このような結果を求めたいかというと、Excel や HTMLで表現するテーブル表にこのカウント方法に従った結果が
欲しいことがあるからだ。

Javaストリームの java.util.stream.Collectors#groupingBy を使ってしまうと、この上のサンプルで、A は 5個の
グルーピングになってしまう。
さらに、リストの 開始INDEXを求めたい。
このような課題が出現する箇所でループ文をいつもコーディングするのは、毎回コーディングに気をつけなくてはならない。
のでバグを書きやすい。

リストの要素からキーを求める java.util.function.Function を引数にした static メソッドを用意した。

/**
 * リスト→グループ開始index:カウントのマップ生成.
 * @param list リスト
 * @param getKey 要素からグルーピングするキー文字列を取得する Function
 * @return key=グループ開始index , value=グルーピングしたカウント
 */
public static <E> Map<Integer, Integer> groupingCountIndexMap(List<E> list, Function<E, String> getKey){
   Map<Integer, Integer> map = new HashMap<>();
   if (list.size()==0) return map;
   int index = 0;
   int count = 1;
   int row = 0;
   ListIterator<E> it = list.listIterator();
   while(it.hasNext()){
      String s = getKey.apply(it.next());
      if (it.hasNext()){
         if (s.equals(getKey.apply(it.next()))){
            count++;
         }else{
            map.put(row, count);
            count = 1;
            row = index + 1;
         }
         it.previous();
      }
      index++;
   }
   map.put(row, count);
   return map;
}

テスト例、、、
文字列リストでも試せるが、それではあまりにも芸がないので、Item というクラスが、public属性文字列 name を
持っているとする。

public class Item{
   public String name;
}

A , A , A , B , B , A , A , D , C , C , E , E の並びで、List を用意する。

List<Item> list;

/* 省略   */

Map<Integer, Integer> map = groupingCountIndexMap(list, e->e.name);

System.out.println(map);

標準出力として次の結果が得られる。

{0=3, 3=2, 5=2, 7=1, 8=2, 10=2}


以前、groupingBy は、

groupingBy - Oboe吹きプログラマの黙示録


Java8 StreamでWicket ListViewセル結合 - Oboe吹きプログラマの黙示録


とメモを書いたことがあったけど、改めて考えると面白い。

POI で セル書込み時に Optional を使う

Apache POI メモリ消費量が増えやすいし、今となっては使いにくい。
Excel の場合、セルのスタイル設定は今のメソッドは、まあ許すとして
セルの値セットや、スタイル適用のメソッドが実行後のセルを返してくれれば、数珠つなぎにメソッド呼べるのに。。

そこで思いついたのは、Java8 からの java.util.Optional で括って ifPresent の Consumer を使う方法
邪道かもしれないけど。

こんな風に。。。

try(InputStream is = new FileInputStream("sample.xlsx");
FileOutputStream out = new FileOutputStream(new File("test.xlsx"));){
   XSSFWorkbook book = new XSSFWorkbook(is);
   XSSFSheet sheet = book.getSheetAt(0);
   XSSFCellStyle style = book.createCellStyle();
   style.setBorderTop(BorderStyle.THIN);
   style.setTopBorderColor(IndexedColors.BLACK.index);
   style.setBorderBottom(BorderStyle.THIN);
   style.setBottomBorderColor(IndexedColors.BLACK.index);
   style.setBorderRight(BorderStyle.THIN);
   style.setRightBorderColor(IndexedColors.BLACK.index);
   style.setBorderLeft(BorderStyle.THIN);
   style.setLeftBorderColor(IndexedColors.BLACK.index);
   style.setAlignment(CellStyle.ALIGN_CENTER);
   style.setVerticalAlignment(CellStyle.VERTICAL_CENTER );

   IntStream.rangeClosed(0, 8).boxed().forEach(rownum->{
      XSSFRow row = sheet.createRow(rownum);
      IntStream.rangeClosed(0, 4).boxed().forEach(columnIndex->{

         Optional.of(row.createCell(columnIndex)).ifPresent(cell->{
            cell.setCellValue( rownum + "-" + columnIndex );
            cell.setCellStyle(style);
         });

      });
   });
   book.write(out);
}catch(Exception e){
   e.printStackTrace();
}

css だけでドロップメニュー(横)

水平方向に並べたメニューのドロップを書いたので、
oboe2uran.hatenablog.com

では、縦にならべたメニューの横に表示するメニュー

f:id:posturan:20170329213707j:plain

サンプル

<ul class="dropmenu">
    <li><a href="#">menu</a>
        <ul>
           <li><a href="#">sub1</a></li>
           <li><a href="#">sub2</a></li>
           <li><a href="#">sub3</a></li>
           <li><a href="#">sub4</a></li>
        </ul>
    </li>
    <li><span>menu</span>
        <ul>
            <li><a href="#">sub11</a></li>
            <li><a href="#">sub12</a></li>
            <li><a href="#">sub13</a></li>
        </ul>
    </li>
    <li><span class="drop">menu</span>
        <ul>
            <li><a href="#">sub21</a></li>
            <li><a href="#">sub22</a></li>
            <li><a href="#">sub23</a></li>
        </ul>
    </li>
    <li>&nbsp;</li>
</ul>

前と違って CSS は、
.dropmenu ul に zindex 属性を付けない。
.dropmenu li ul li は、top position を li 負の高さを指定
float属性は li ul li で指定

.dropmenu{
	padding: 0;
	white-space: nowrap;
	font-size: 14px;
}
.dropmenu:after{ content: ""; display: block; clear: both; }
ul.dropmenu{  margin: 0; padding: 0; }
.dropmenu ul{
	position: absolute;
	margin: 0;
	padding: 0;
 }
.dropmenu li{
	list-style-type: none;
	position: relative;
	width: 100px;
	height: 27px;
	line-height: 27px;
	background: #0068b7;
	text-align: center;
}
.dropmenu li a{
	display: block;
	height: 27px;
	line-height: 27px;
	vertical-align: middle;
	color: #ffffff;
	text-decoration: none;
}
.dropmenu li span{
	display: table-cell;
	width: 100px;
	height: 27px;
	line-height: 27px;
	vertical-align: middle;
	background: #0068b7;
	color: #ffffff;
	text-decoration: none;
	cursor: pointer;
}
.dropmenu li ul li{
	float: left;
	position: relative;
	left: 100px;
	top: -27px;
}
.dropmenu li ul li a{
	height: 27px;
	line-height: 27px;
	vertical-align: middle;
	border-top: 1px solid #0068b7;
	background: #003f8e;
	text-align: center;
}
.dropmenu li:hover > a{ background: #003f8e; }
.dropmenu li a:hover, .dropmenu li span:hover{ background: #192f60;  }
.dropmenu li ul li{
	overflow: hidden;
	height: 0;
	transition: .2s;
}
.dropmenu li:hover ul li{
	overflow: visible;
	height: 27px;
}

css だけでドロップメニュー

html サンプル

<ul class="dropmenu">
    <li><a href="#">menu</a>
        <ul>
           <li><a href="#">sub1</a></li>
           <li><a href="#">sub2</a></li>
           <li><a href="#">sub3</a></li>
           <li><a href="#">sub4</a></li>
        </ul>
    </li>
    <li><span>menu</span>
        <ul>
            <li><a href="#">sub1</a></li>
            <li><a href="#">sub2</a></li>
            <li><a href="#">sub3</a></li>
        </ul>
    </li>
    <li><span class="drop">menu</span>
        <ul>
            <li><a href="#">sub1</a></li>
            <li><a href="#">sub2</a></li>
            <li><a href="#">sub3</a></li>
        </ul>
    </li>
    <li>&nbsp;</li>
</ul>

CSS

.dropmenu{
   padding: 0;
   white-space: nowrap;
   font-size: 14px;
}
.dropmenu:after{ content: ""; display: block; clear: both; }
ul.dropmenu{  margin: 0; padding: 0; }
.dropmenu ul{
   position: absolute;
   z-index: 9999;
   margin: 0;
   padding: 0;
 }
.dropmenu li{
   list-style-type: none;
   position: relative;
   width: 100px;
   float: left;
   height: 27px;
   background: #0068b7;
   text-align: center;
}
.dropmenu li a{
   display: block;
   height: 27px;
   line-height: 27px;
   vertical-align: middle;
   color: #ffffff;
   text-decoration: none;
}
.dropmenu li span{
   display: table-cell;
   width: 100px;
   height: 27px;
   line-height: 27px;
   vertical-align: middle;
   background: #0068b7;
   color: #ffffff;
   text-decoration: none;
   cursor: pointer;
}
.dropmenu li ul li a{
   height: 27px;
   line-height: 27px;
   vertical-align: middle;
   border-top: 1px solid #0068b7;
   background: #003f8e;
   text-align: center;
}
.dropmenu li:hover > a{ background: #003f8e; }
.dropmenu li a:hover, .dropmenu li span:hover{ background: #192f60;  }
.dropmenu li ul li{
   overflow: hidden;
   height: 0;
   transition: .7s;
}
.dropmenu li:hover ul li{
   overflow: visible;
   height: 27px;
}

CSS のポイントは、
:after疑似要素で float解除
li a と li span 各々で、height と line-height 同じサイズで、vertical-align: middle;
これは、li の高さだけを指定すると、vertical-align: middle が効かないからだ。
li:hover > a で選択候補色
li ul li を
overflow: hidden;
height: 0;
transition: .7s; overflow: hidden;
で時間をかけてドロップメニューの開閉
.dropmenu li a:hover, .dropmenu li span:hover{ background: #192f60; } で選択色

f:id:posturan:20170328222744j:plain

css float属性の解除は、after疑似要素で。

今更。。。
css float属性の解除は、after疑似要素を使うのがいい。

<ul class="foo">
    <li>a</li>
    <li>b</li>
    <li>c</li>
</ul>
<ul>
    <li>d</li>
    <li>e</li>
    <li>f</li>
</ul>

に対して、

.foo:after{ content: ""; display: block; clear: both; }

.foo li{
   float: left;
   list-style: none;
   
   width: 50px;
   border: 1px solid #808080;
}

Wicket 8 FeedbackPanel サンプル

Wicket8 の AjaxButton onSubmit を使用して気がついたのだが、前の Wicket バージョンでは、onSubmit の中で任意に
error(Serializable) を呼ぶと、onError が走るという認識だったけど、Wicket 8 でそう動かない。Validator を
仕掛けておいて、バリデーションエラーの時に、onError が走る。
Validator の仕掛け以外で、onSubmit でエラーを判定してエラー処理をしたいことは多々あるわけで、
error(Serializable) で onError が走らないのは不便だ。

final TextField<String> nameField = new TextField<>("name", new Model<>());
queue(nameField);

// フィードバック
final FeedbackPanel feedback = new FeedbackPanel("feedback");
feedback.setOutputMarkupId(true);
final AtomicBoolean feedbackError = new AtomicBoolean(false);
feedback.setFilter(e->feedbackError.get());
queue(feedback);

queue(AjaxButton.onSubmit("send", (b, t)->{
   feedbackError.set(false);
   try{
      String name = Optional.ofNullable(nameField.getModelObject())
                  .orElseThrow(()->new RuntimeException("必須エラー"));

      // フィードバッククリア
      t.add(feedback);
      // 正常処理

   }catch(Exception e){
      error(e.getMessage());
      feedbackError.set(true);
      t.add(feedback);
   }
}));

こんな意見、指摘、
 「 Validator エラー判定を全て用意するのが正しい、そうでなければそれは設計ミス!」
ということを言う人がいるかもしれない。

でも、それには私は反対だ。

HTTPClient の ResponseHandler

HTTPClient といえば、
https://hc.apache.org/ の HttpClient
この中の ResponseHandler は、残念なのか仕方ないのか、@FunctionalInterface を持っているわけではない。
あの面倒くさいHTTPで受け取る処理をもう少しスマートに書きたくて、とりあえず以下の static メソッドを書いてみる。

static void executeHttp(HttpClient client, HttpUriRequest request, Consumer<HttpResponse> handler)
throws ClientProtocolException, IOException{
   client.execute(request, response ->{
      handler.accept(response);
      return null;
   });
}

そして、HttpGet してみる。

try(CloseableHttpClient httpclient = HttpClients.createDefault()){
   HttpGet httpget = new HttpGet("http://google.com");
   System.out.println("Executing request " + httpget.getRequestLine());
   executeHttp(httpclient, httpget, response->{
      int status = response.getStatusLine().getStatusCode();
      if (status >= 200 && status < 300){
         Optional.ofNullable(response.getEntity()).<Runnable>map(entity->()->{
            try{
               System.out.println("EntityUtils.toString(entity) = " +  EntityUtils.toString(entity) );
            }catch(ParseException | IOException e){
                e.printStackTrace();
            }
         }).orElse(()->{
             System.out.println("NULL receive!!");
         }).run();
      }else{
         System.out.println("Error! HTTP status = " + status );
      }
   });
}catch(Exception e){
   e.printStackTrace();
}finally{
}

なんだか、あまりスマートじゃない。

思い切って、HTTP 200 以外は全てエラーということで、、、
でも、HttpEntity を正常処理でわたさないとならないか悩んで、、

static void executeHttp(HttpClient client, HttpUriRequest request, Consumer<HttpEntity> receive, Consumer<HttpResponse> error) {
   try{
      client.execute(request, new ResponseHandler<String>(){
         @Override
         public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException{
            int status = response.getStatusLine().getStatusCode();
            if (status==200){
               Optional.ofNullable(response.getEntity()).<Runnable>map(entity->()->{
                  receive.accept(entity);
               }).orElse(()->{
                  error.accept(response);
               }).run();
            }else{
               error.accept(response);
            }
            return null;
         }
      });
   }catch(IOException e){
      error.accept(null);
   }
}
try(CloseableHttpClient httpclient = HttpClients.createDefault()){
   HttpGet httpget = new HttpGet("http://google.com");
   System.out.println("Executing request " + httpget.getRequestLine());
   executeHttp(httpclient, httpget
   , entity->{
      try{
         System.out.println("EntityUtils.toString(entity) = " +  EntityUtils.toString(entity) );
      }catch(IOException e1){
         e1.printStackTrace();
      }
   }
   , res->{
      // エラー処理
   });
}catch(Exception e){
   e.printStackTrace();
}finally{
}

それでもやはりスマートとは言えない。EntityUtils.toString か toByteArray かは、Consumer 実行の中で記述するから
こうなってしまうのか。。。