File Coverage

blib/lib/Health/BladderDiary/GenTable.pm
Criterion Covered Total %
statement 60 125 48.0
branch 28 80 35.0
condition 7 35 20.0
subroutine 5 5 100.0
pod 1 1 100.0
total 101 246 41.0


line stmt bran cond sub pod time code
1             package Health::BladderDiary::GenTable;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-11-13'; # DATE
5             our $DIST = 'Health-BladderDiary-GenTable'; # DIST
6             our $VERSION = '0.004'; # VERSION
7              
8 1     1   117483 use 5.010001;
  1         14  
9 1     1   6 use strict;
  1         2  
  1         20  
10 1     1   5 use warnings;
  1         2  
  1         27  
11              
12 1     1   5 use Exporter qw(import);
  1         2  
  1         1492  
13             our @EXPORT_OK = qw(gen_bladder_diary_table_from_entries);
14              
15             our %SPEC;
16              
17             $SPEC{gen_bladder_diary_table_from_entries} = {
18             v => 1.1,
19             summary => 'Create bladder diary table from bladder diary entries',
20             args => {
21             entries => {
22             schema => 'str*',
23             req => 1,
24             pos => 0,
25             cmdline_src => 'stdin_or_file',
26             },
27             },
28             };
29             sub gen_bladder_diary_table_from_entries {
30 2     2 1 29417 my %args = @_;
31              
32 2         6 my @unparsed_entries;
33             SPLIT_ENTRIES: {
34 2 100       4 if ($args{entries} =~ /\S\R\R+\S/) {
  2         26  
35             # there is a blank line between non-blanks, assume entries are
36             # written in paragraphs
37 1         11 @unparsed_entries = split /\R\R+/, $args{entries};
38 1         3 for (@unparsed_entries) {
39 3         9 s/\R+/ /g;
40 3         14 s/\s+\z//;
41             }
42             } else {
43             # there are no blank lines, assume entries are written as individual
44             # lines
45 1         5 @unparsed_entries = split /^/, $args{entries};
46             }
47 2         7 for (@unparsed_entries) {
48 6         16 s/\R+/ /g;
49 6         24 s/\s+\z//;
50             }
51             } # SPLIT_ENTRIES
52              
53 2         7 my @urinations;
54             my @intakes;
55             PARSE_ENTRIES: {
56 2         3 my $i = 0;
  2         4  
57 2         5 for my $uentry (@unparsed_entries) {
58 6         11 my $uentry0 = $uentry;
59 6         10 $i++;
60 6         9 my $time;
61 6 50       35 $uentry =~ s/\A(\d\d)[:.]?(\d\d)(?:-(\d\d)[:.]?(\d\d))?\s*//
62             or return [400, "Entry #$i: invalid time, please start with hhmm or hh:mm: $uentry0"];
63 6         30 my ($h, $m, $h2, $m2) = ($1, $2, $3, $4);
64 6 50       46 $uentry =~ s/(\w+):?\s*//
65             or return [400, "Entry #$i: event (e.g. drink, urinate) expected: $uentry"];
66 6         16 my $event = $1;
67 6 100 66     35 if ($event eq 'u' || $event eq 'urin') { $event = 'urinate' }
  2 100       3  
    50          
68 2         5 elsif ($event eq 'd') { $event = 'drink' }
69 0         0 elsif ($event eq 'c') { $event = 'comment' }
70 6 50       28 $event =~ /\A(drink|eat|poop|urinate|comment)\z/
71             or return [400, "Entry #$i: unknown event '$event', please choose eat|drink|poop|urinate|comment"];
72              
73 6         53 my $parsed_entry = {
74             # XXX check that time is monotonically increasing
75             time => sprintf("%02d.%02d", $h, $m),
76             _h => $h,
77             _m => $m,
78             _time => $h*60 + $m,
79             _raw => $uentry0,
80             };
81              
82             # scrape key-value pairs from unparsed entry
83 6         12 my %kv;
84 6         31 while ($uentry =~ /(\w+)=(.+?)(?=[,.]?\s+\w+=|[.]?\s*\z)/g) {
85 6         40 $kv{$1} = $2;
86             }
87             #use DD; dd \%kv;
88              
89 6         14 for my $k (qw/vol type comment urgency color/) {
90 30 100       60 if (defined $kv{$k}) {
91 6         16 $parsed_entry->{$k} = $kv{$k};
92             }
93             }
94              
95 6 100 33     45 $uentry =~ /\b(\d+)ml\b/ and $parsed_entry->{vol} //= $1;
96 6 50 0     18 $uentry =~ /\bv(\d+)\b/ and $parsed_entry->{vol} //= $1;
97 6 100 33     28 $uentry =~ /\bu([0-9]|10)\b/ and $parsed_entry->{urgency} //= $1;
98 6 100 33     26 $uentry =~ /\bc([0-6])\b/ and $parsed_entry->{color} //= do {
99 2 50       7 if ($1 == 0) { 'clear' } # very good
  2 0       8  
    0          
    0          
    0          
    0          
    0          
100 0         0 elsif ($1 == 1) { 'light yellow' } # good
101 0         0 elsif ($1 == 2) { 'yellow' } # fair
102 0         0 elsif ($1 == 3) { 'dark yellow' } # light dehydrated
103 0         0 elsif ($1 == 4) { 'amber' } # dehydrated
104 0         0 elsif ($1 == 5) { 'brown' } # very dehydrated
105 0         0 elsif ($1 == 6) { 'red' } # severe dehydrated
106             };
107              
108 6 100       20 if ($event eq 'drink') {
    50          
    50          
109             return [400, "Entry #$i: please specify volume for $event"]
110 4 50       11 unless defined $parsed_entry->{vol};
111 4   100     17 $parsed_entry->{type} //= "water";
112 4         15 push @intakes, $parsed_entry;
113             } elsif ($event eq 'eat') {
114 0         0 $parsed_entry->{type} = "food";
115 0         0 push @intakes, $parsed_entry;
116             } elsif ($event eq 'urinate') {
117             return [400, "Entry #$i: please specify volume for $event"]
118 2 50       7 unless defined $parsed_entry->{vol};
119 2 50       10 $parsed_entry->{"ucomment"} = "poop" . ($parsed_entry->{comment} ? ": $parsed_entry->{comment}" : "");
120 2         6 push @urinations, $parsed_entry;
121             }
122             }
123             } # PARSE_ENTRIES
124              
125 2 50       6 if ($args{_raw}) {
126 2         29 return [200, "OK", {
127             intakes => \@intakes,
128             urinations => \@urinations,
129             }];
130             }
131              
132 0           my @rows;
133 0           my $ivol_cum = 0;
134 0           my $uvol_cum = 0;
135 0           my $prev_utime;
136 0           my $num_drink = 0;
137 0           my $num_urinate = 0;
138             GROUP_INTO_HOURS: {
139 0           my $h = do {
  0            
140 0 0         my $hi = @intakes ? $intakes[0]{_h} : undef;
141 0 0         my $hu = @urinations ? $urinations[0]{_h} : undef;
142 0   0       my $h = $hi // $hu;
143 0 0 0       $h = $hi if defined $hi && $hi < $h;
144 0 0 0       $h = $hu if defined $hu && $hu < $h;
145 0           $h;
146             };
147 0           while (1) {
148 0 0 0       last unless @intakes || @urinations;
149              
150 0           my @hour_rows;
151 0 0         push @hour_rows, {time => sprintf("%02d.00-%02d.00", $h, $h+1 <= 23 ? $h+1 : 0)};
152              
153 0           my $j = 0;
154 0   0       while (@intakes && $intakes[0]{_h} == $h) {
155 0           my $entry = shift @intakes;
156 0           $hour_rows[$j]{"intake type"} = $entry->{type};
157 0           $hour_rows[$j]{itime} = $entry->{time};
158 0           $hour_rows[$j]{"icomment"} = $entry->{comment};
159 0 0         if (defined $entry->{vol}) {
160 0           $num_drink++;
161 0           $hour_rows[$j]{"ivol (ml)"} = $entry->{vol};
162 0           $ivol_cum += $entry->{vol};
163 0           $hour_rows[$j]{"ivol cum"} = $ivol_cum;
164             }
165 0           $j++;
166             }
167              
168 0           $j = 0;
169 0   0       while (@urinations && $urinations[0]{_h} == $h) {
170 0           my $entry = shift @urinations;
171 0           $hour_rows[$j]{"urin/defec time"} = $entry->{time};
172 0           $hour_rows[$j]{"color"} = $entry->{color};
173 0           $hour_rows[$j]{"ucomment"} = $entry->{comment};
174 0           $hour_rows[$j]{"urgency (0-10)"} = $entry->{urgency};
175 0 0         if (defined $entry->{vol}) {
176 0           $num_urinate++;
177 0           $hour_rows[$j]{"uvol (ml)"} = $entry->{vol};
178 0           $uvol_cum += $entry->{vol};
179 0           $hour_rows[$j]{"uvol cum"} = $uvol_cum;
180 0           my $mins_diff;
181 0 0         if (defined $prev_utime) {
182 0 0         $mins_diff = $prev_utime > $entry->{_time} ? (24*60+$entry->{_time} - $prev_utime) : ($entry->{_time} - $prev_utime);
183             }
184             #$hour_rows[$j]{"utimediff"} = $mins_diff;
185             $hour_rows[$j]{"urate (ml/h)"} = defined($prev_utime) ?
186 0 0         sprintf("%.0f", $entry->{vol} / $mins_diff * 60) : undef;
187             }
188 0           $j++;
189              
190 0           $prev_utime = $entry->{_time};
191             }
192 0           push @rows, @hour_rows;
193 0           $h++;
194 0 0         $h = 0 if $h >= 24;
195             }
196             } # GROUP_INTO_HOURS
197              
198             ADD_SUMMARY_ROWS: {
199 0           push @rows, {};
  0            
200              
201 0           push @rows, {
202             time => 'freq drink/urin',
203             'itime' => $num_drink,
204             'urin/defec time' => $num_urinate,
205             };
206 0 0         push @rows, {
    0          
207             time => 'avg (ml)',
208             'ivol (ml)' => sprintf("%.0f", $num_drink ? $ivol_cum / $num_drink : 0),
209             'uvol (ml)' => sprintf("%.0f", $num_urinate ? $uvol_cum / $num_urinate : 0),
210             };
211             }
212              
213             # return result
214              
215 0           [200, "OK", \@rows, {
216             'table.fields' => [
217             'time',
218             'intake type',
219             'itime',
220             'ivol (ml)',
221             'ivol cum',
222             'icomment',
223             'urin/defec time',
224             'uvol (ml)',
225             'uvol cum',
226             'urate (ml/h)',
227             'color',
228             'urgency (0-10)',
229             'ucomment',
230             ],
231             'table.field_aligns' => [
232             'left', #'time',
233             'left', #'intake type',
234             'left', #'itime',
235             'right', #'ivol (ml)',
236             'right', #'ivol cum',
237             'left', #'icomment',
238             'left', #'urin/defec time',
239             'right', #'uvol (ml)',
240             'right', #'uvol cum',
241             'right', #'urate (ml/h)',
242             'left', #'color',
243             'left', #'urgency (0-10)',
244             'left', #'ucomment',
245             ],
246             }];
247             }
248              
249             1;
250             # ABSTRACT: Create bladder diary table from entries
251              
252             __END__
253              
254             =pod
255              
256             =encoding UTF-8
257              
258             =head1 NAME
259              
260             Health::BladderDiary::GenTable - Create bladder diary table from entries
261              
262             =head1 VERSION
263              
264             This document describes version 0.004 of Health::BladderDiary::GenTable (from Perl distribution Health-BladderDiary-GenTable), released on 2020-11-13.
265              
266             =head1 SYNOPSIS
267              
268             Your bladder entries e.g. in `bd-entry1.txt` (I usually write in Org document):
269              
270             0730 drink: 300ml type=water
271              
272             0718 urinate: 250ml
273              
274             0758 urinate: 100ml
275              
276             0915 drink 300ml
277              
278             1230 drink: 600ml, note=thirsty
279              
280             1245 urinate: 200ml
281              
282             From the command-line (I usually run the script from inside Emacs):
283              
284             % gen-bladder-diary-table-from-entries < bd-entry1.txt
285             | time | intake type | itime | ivol (ml) | ivol cum | icomment | urination time | uvol (ml) | uvol cum | urgency (0-3) | ucolor (0-3) | ucomment |
286             |----------+-------------+-------+-----------+----------+----------+----------------+-----------+----------+---------------+--------------+----------+
287             | 07-08.00 | water | 07.30 | 300 | 300 | | 07.18 | 250 | 250 | | | |
288             | | | | | | | 07.58 | 100 | 350 | | | |
289             | 08-09.00 | | | | | | | | | | | |
290             | 09-10.00 | water | 09.15 | 300 | 600 | | | | | | | |
291             | 10-11.00 | | | | | | | | | | | |
292             | 12-13.00 | water | 12.30 | 600 | 1200 | thirsty | 12.45 | 200 | | | | |
293             | | | | | | | | | | | | |
294             | total | | | 1200 | | | | 550 | | | | |
295             | freq | | | 3 | | | | 3 | | | | |
296             | avg | | | 400 | | | | 183 | | | | |
297              
298             Produce CSV instead:
299              
300             % gen-bladder-diary-table-from-entries --format csv < bd-entry1.txt > bd-entry1.csv
301              
302             =head1 DESCRIPTION
303              
304             This module can be used to visualize bladder diary entries (which is more
305             comfortable to type in) into table form (which is more comfortable to look at).
306              
307             =head2 Diary entries
308              
309             The input to the module is bladder diary entries in the form of text. The
310             entries should be written in paragraphs, chronologically, each separated by a
311             blank line. If there is no blank line, then entries are assumed to be written in
312             single lines.
313              
314             The format of an entry is:
315              
316             <TIME> ("-" <TIME2>)? WS EVENT (":")? WS EXTRA
317              
318             It is designed to be easy to write. Time can be written as C<hh:mm> or just
319             C<hhmm> in 24h format.
320              
321             Event can be one of C<drink> (or C<d> for short), C<eat>, C<urinate> (or C<u> or
322             C<urin> for short), C<poop>, or C<comment> (or C<c> for short).
323              
324             Extra is a free-form text, but you can use C<word>=C<text> syntax to write
325             key-value pairs. Some recognized keys are: C<vol>, C<comment>, C<type>,
326             C<urgency>, C<color>.
327              
328             Some other information are scraped for writing convenience:
329              
330             /\b(\d+)ml\b/ for volume
331             /\bv(\d+)\b/ for volume
332             /\bu([0-9]|10)\b/ for urgency (1-10)
333             /\bc([0-6])\b/ for clear to dark orange color (0=clear, 1=light yellow, 2=yellow, 3=dark yellow, 4=amber, 5=brown, 6=red)
334              
335             Example C<drink> entry (all are equivalent):
336              
337             07:30 drink: vol=300ml
338             0730 drink 300ml
339             0730 d 300ml
340              
341             Example C<urinate> entry (all are equivalent):
342              
343             07:45 urinate: vol=200ml urgency=4 color=light yellow comment=at home
344             0745 urin 200ml urgency=4 color=light yellow comment=at home
345             0745 u 200ml u4 c1 comment=at home
346              
347             =head3 Urination entries
348              
349             A urination entry is an entry with event C<urination> (can be written as just
350             C<u> or C<urin>). At least volume is required, can be written in ml unit e.g.
351             C<300ml>, or using C<vNUMBER> e.g. C<v300>, or using C<vol> key, e.g.
352             C<vol=300>. Example:
353              
354             1230 u 200ml
355              
356             You can also enter color, using C<color=NAME> or C<c0>..C<c6> for short. These
357             colors from 7-color-in-test-tube urine color chart is recommended:
358             L<https://www.dreamstime.com/urine-color-chart-test-tubes-medical-vector-illustration-image163017644>
359             or
360             L<https://stock.adobe.com/images/urine-color-chart-urine-in-test-tubes-medical-vector/299230365>:
361              
362             0 - clear
363             1 - light yellow
364             2 - yellow
365             3 - dark yellow
366             4 - amber
367             5 - brown
368             6 - red
369              
370             Example:
371              
372             1230 u 200ml c2
373              
374             You can also enter urgency information using C<urgency=NUMBER> or C<u0>..C<u10>,
375             which is a number from 0 (not urgent at all) to 10 (most urgent). Example:
376              
377             1230 u 200ml c2 u4
378              
379             =head2 Drink entries
380              
381             A drink (fluid intake) entry is an entry with event C<drink> (can be written as
382             just C<d>). At least volume is required, can be written in ml unit e.g.
383             C<300ml>, or using C<vNUMBER> e.g. C<v300>, or using C<vol> key, e.g.
384             C<vol=300>. Example:
385              
386             1300 d 300ml
387              
388             You can also input the kind of drink using C<type=NAME>. If type is not
389             specified, C<water> is assumed. Example:
390              
391             1300 d 300ml type=coffee
392              
393             =head1 KEYWORDS
394              
395             voiding diary, bladder diary
396              
397             =head1 FUNCTIONS
398              
399              
400             =head2 gen_bladder_diary_table_from_entries
401              
402             Usage:
403              
404             gen_bladder_diary_table_from_entries(%args) -> [status, msg, payload, meta]
405              
406             Create bladder diary table from bladder diary entries.
407              
408             This function is not exported by default, but exportable.
409              
410             Arguments ('*' denotes required arguments):
411              
412             =over 4
413              
414             =item * B<entries>* => I<str>
415              
416              
417             =back
418              
419             Returns an enveloped result (an array).
420              
421             First element (status) is an integer containing HTTP status code
422             (200 means OK, 4xx caller error, 5xx function error). Second element
423             (msg) is a string containing error message, or 'OK' if status is
424             200. Third element (payload) is optional, the actual result. Fourth
425             element (meta) is called result metadata and is optional, a hash
426             that contains extra information.
427              
428             Return value: (any)
429              
430             =head1 HOMEPAGE
431              
432             Please visit the project's homepage at L<https://metacpan.org/release/Health-BladderDiary-GenTable>.
433              
434             =head1 SOURCE
435              
436             Source repository is at L<https://github.com/perlancar/perl-Health-BladderDiary-GenTable>.
437              
438             =head1 BUGS
439              
440             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Health-BladderDiary-GenTable>
441              
442             When submitting a bug or request, please include a test-file or a
443             patch to an existing test-file that illustrates the bug or desired
444             feature.
445              
446             =head1 SEE ALSO
447              
448             =head1 AUTHOR
449              
450             perlancar <perlancar@cpan.org>
451              
452             =head1 COPYRIGHT AND LICENSE
453              
454             This software is copyright (c) 2020 by perlancar@cpan.org.
455              
456             This is free software; you can redistribute it and/or modify it under
457             the same terms as the Perl 5 programming language system itself.
458              
459             =cut