1 /* -*- mode: c++ -*- */
2 
3 #ifndef R_PROGRESS_H
4 #define R_PROGRESS_H
5 
6 #include <unistd.h>
7 #include <sys/time.h>
8 
9 #ifdef Win32
10 #  include <io.h>
11 #endif
12 
13 #include <string>
14 #include <sstream>
15 #include <iomanip>
16 #include <cmath>
17 #include <cstring>
18 #include <cstdlib>
19 #include <cerrno>
20 
21 #include <R.h>
22 #include <Rinternals.h>
23 #include <R_ext/Print.h>
24 
25 // For gettimeofday implementation on windows
26 #ifdef Win32
27 
28 // MSVC defines this in winsock2.h!?
29 typedef struct timeval {
30     long tv_sec;
31     long tv_usec;
32 } timeval;
33 int gettimeofday(struct timeval * tp, struct timezone * tzp);
34 
35 #endif
36 
37 namespace RProgress {
38 
39 class RProgress {
40 
41  public:
42 
RProgress(std::string format,double total,int width,std::string cursor_char,std::string complete_char,std::string incomplete_char,bool clear,double show_after)43    RProgress(std::string format,
44        double total,
45        int width,
46        std::string cursor_char,
47        std::string complete_char,
48        std::string incomplete_char,
49        bool clear,
50        double show_after) :
51 
52     first(true), format(format), total(total), current(0), count(0),
53     width(width), cursor_char(cursor_char), complete_char(complete_char),
54     incomplete_char(incomplete_char), clear(clear), show_after(show_after),
55     last_draw(""), start(0), toupdate(false), complete(false), reverse(false) {
56 
57     supported = is_supported();
58     use_stderr = default_stderr();
59   }
60 
61    RProgress(std::string format = "[:bar] :percent",
62        double total = 100,
63        int width = Rf_GetOptionWidth() - 2,
64        char complete_char = '=',
65        char incomplete_char = '-',
66        bool clear = true,
67        double show_after = 0.2) :
68 
first(true)69     first(true), format(format), total(total), current(0), count(0),
70     width(width), cursor_char(1, complete_char), complete_char(1, complete_char),
71     incomplete_char(1, incomplete_char), clear(clear), show_after(show_after),
72     last_draw(""), start(0), toupdate(false), complete(false), reverse(false) {
73 
74     supported = is_supported();
75     use_stderr = default_stderr();
76   }
77 
78 
~RProgress()79   ~RProgress() { }
80 
set_format(std::string format)81   void set_format(std::string format)    { this->format = format;         }
set_total(double total)82   void set_total(double total)           { this->total = total;           }
set_width(int width)83   void set_width(int width)              { this->width = width;           }
set_cursor_char(const char * cursor_char)84   void set_cursor_char(const char* cursor_char) {
85     this->cursor_char = cursor_char;
86   }
set_complete_char(const char * complete_char)87   void set_complete_char(const char* complete_char) {
88     this->complete_char = complete_char;
89   }
set_incomplete_char(const char * incomplete_char)90   void set_incomplete_char(const char* incomplete_char) {
91     this->incomplete_char = incomplete_char;
92   }
set_clear(bool clear)93   void set_clear(bool clear)             { this->clear = clear;           }
set_show_after(double show_after)94   void set_show_after(double show_after) { this->show_after = show_after; }
95 
set_reverse(bool reverse)96   void set_reverse(bool reverse)         { this->reverse = reverse;       }
97 
98   void tick(double len = 1) {
99     // Start the timer
100     if (first) { start = time_now(); }
101 
102     current += len;
103     count++;
104 
105     // We only update after show_after secs
106     toupdate = toupdate || time_now() - start > show_after;
107 
108     if (current >= total) complete = true;
109 
110     // Need to render at the beginning and at the end, always
111     if (first || toupdate || complete) this->render();
112 
113     if (complete) this->terminate();
114 
115     first = false;
116   }
117 
update(double ratio)118   void update(double ratio) {
119     double goal = ratio * total;
120     this->tick(goal - current);
121   }
122 
123  private:
124 
125   bool first;			// Is the next one the first tick?
126   bool supported;		// \r supported at all?
127   std::string format;		// Format template
128   double total;			// Total number of ticks
129   double current;		// Current number of ticks
130   int count;                    // Total number of calls
131   int width;			// Width of progress bar
132   bool use_stderr;		// Whether to print to stderr
133   std::string cursor_char;		// Character for cursor tick
134   std::string complete_char;		// Character for completed ticks
135   std::string incomplete_char;		// Character for incomplete ticks
136   bool clear;			// Should we clear the line at the end?
137   double show_after;		// Delay to show/increase the progress bar
138   std::string last_draw;	// Last progress bar drawn
139 
140   double start;			// Start time
141   bool toupdate;		// Are we updating? (After show_after.)
142   bool complete;		// Are we complete?
143   bool reverse;  // go from right to left rather than left to right
144 
render()145   void render() {
146     if (!supported) return;
147 
148     std::string str = format;
149 
150     std::stringstream buffer;
151 
152     double ratio_now = ratio();
153 
154     // percent
155     buffer << std::setw(3) << ratio_now * 100 << "%";
156     replace_all(str, ":percent", buffer.str());
157     buffer.str(""); buffer.clear();
158 
159     // elapsed
160     double elapsed_secs = time_now() - start;
161     std::string elapsed = vague_dt(elapsed_secs);
162     replace_all(str, ":elapsed", elapsed);
163 
164     // eta
165     double percent = round(ratio_now * 100);
166     double eta_secs = percent == 100 ? 0 :
167       elapsed_secs * (total / current - 1.0);
168     std::string eta = std::isinf(eta_secs) ? "?s" : vague_dt(eta_secs);
169     replace_all(str, ":eta", eta);
170 
171     // rate
172     if (elapsed_secs == 0) {
173       buffer << "?";
174     } else {
175       double rate_num = elapsed_secs == 0 ? 0 : current / elapsed_secs;
176       buffer << pretty_bytes(rate_num) << "/s";
177     }
178     replace_all(str, ":rate", buffer.str());
179     buffer.str(""); buffer.clear();
180 
181     // current
182     buffer << round(current);
183     replace_all(str, ":current", buffer.str());
184     buffer.str(""); buffer.clear();
185 
186     // total
187     buffer << round(total);
188     replace_all(str, ":total", buffer.str());
189     buffer.str(""); buffer.clear();
190 
191     // bytes
192     replace_all(str, ":bytes", pretty_bytes(current));
193 
194     // spin
195     replace_all(str, ":spin", spin_symbol());
196 
197     // bar
198     std::string str_no_bar = str;
199     replace_all(str_no_bar, ":bar", "");
200     long int bar_width = width - str_no_bar.length();
201     if (bar_width < 0) bar_width = 0;
202 
203     double complete_len = round(bar_width * ratio_now);
204     std::string bar;
205 
206     if (reverse) {
207       for (long int i = (long int) complete_len; i < bar_width; i++) {
208         bar += incomplete_char;
209       }
210       if (complete_len > 0) {
211         bar += cursor_char;
212       }
213       for (int i = 0; i < (complete_len - 1); i++) { bar += complete_char; }
214     } else {
215       for (int i = 0; i < (complete_len - 1); i++) { bar += complete_char; }
216       if (complete_len > 0) {
217         bar += cursor_char;
218       }
219       for (long int i = (long int) complete_len; i < bar_width; i++) {
220         bar += incomplete_char;
221       }
222     }
223     replace_all(str, ":bar", bar);
224 
225     if (last_draw != str) {
226       if (last_draw.length() > str.length()) { clear_line(use_stderr, width); }
227       cursor_to_start(use_stderr);
228       if (use_stderr) {
229 	REprintf(str.c_str());
230       } else {
231 	Rprintf(str.c_str());
232       }
233       last_draw = str;
234     }
235   }
236 
terminate()237   void terminate() {
238     if (! supported) return;
239     if (clear) {
240       clear_line(use_stderr, width);
241       cursor_to_start(use_stderr);
242     } else {
243       if (use_stderr) {
244 	REprintf("\n");
245       } else {
246 	Rprintf("\n");
247       }
248     }
249   }
250 
ratio()251   double ratio() {
252     double ratio = current / total;
253     if (ratio < 0) ratio = 0;
254     if (ratio > 1) ratio = 1;
255     return ratio;
256   }
257 
spin_symbol()258   std::string spin_symbol() {
259     const char symbols[4] = {'-', '\\', '|', '/'};
260     return std::string(1, symbols[(count - 1) % 4]);
261   }
262 
clear_line(bool use_stderr,int width)263   void clear_line(bool use_stderr, int width) {
264 
265     char *spaces = (char*) calloc(width + 2, sizeof(char));
266     if (!spaces) Rf_error("Progress bar: out of memory");
267     for (int i = 1; i <= width; i++) spaces[i] = ' ';
268     spaces[0] = '\r';
269     spaces[width + 1] = '\0';
270 
271     if (use_stderr) {
272       REprintf(spaces);
273     } else {
274       Rprintf(spaces);
275     }
276     free(spaces);
277   }
278 
cursor_to_start(bool use_stderr)279   void cursor_to_start(bool use_stderr) {
280 
281     if (use_stderr) {
282       REprintf("\r");
283     } else {
284       Rprintf("\r");
285     }
286   }
287 
is_r_studio()288   bool is_r_studio() {
289 
290     char *v = std::getenv("RSTUDIO");
291 
292     return v != 0 && v[0] == '1' && v[1] == '\0';
293   }
294 
is_r_app()295   bool is_r_app() {
296 
297     char *v = std::getenv("R_GUI_APP_VERSION");
298 
299     return v != 0;
300   }
301 
302   // In R Studio we should print to stdout, because priting a \r
303   // to stderr is buggy (reported)
304 
default_stderr()305   bool default_stderr() {
306 
307     return !is_r_studio();
308   }
309 
310   // If stdout is a terminal, or R Studio or macOS R.app
311   // On windows, stdout is a terminal, apparently
312 
is_supported()313   bool is_supported() {
314 
315     return is_option_enabled() &&
316       (isatty(1) || is_r_studio() || is_r_app());
317   }
318 
319   // gettimeofday for windows, from
320   // https://stackoverflow.com/questions/10905892
321 
322 #ifdef Win32
323 
324 #define WIN32_LEAN_AND_MEAN
325 #include <Windows.h>
326 #include <stdint.h> // portable: uint64_t   MSVC: __int64
327 
gettimeofday(struct timeval * tp,struct timezone * tzp)328   int gettimeofday(struct timeval * tp, struct timezone * tzp) {
329     // Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
330     static const uint64_t EPOCH = ((uint64_t) 116444736000000000ULL);
331 
332     SYSTEMTIME  system_time;
333     FILETIME    file_time;
334     uint64_t    time;
335 
336     GetSystemTime( &system_time );
337     SystemTimeToFileTime( &system_time, &file_time );
338     time =  ((uint64_t)file_time.dwLowDateTime )      ;
339     time += ((uint64_t)file_time.dwHighDateTime) << 32;
340 
341     tp->tv_sec  = (long) ((time - EPOCH) / 10000000L);
342     tp->tv_usec = (long) (system_time.wMilliseconds * 1000);
343     return 0;
344   }
345 
346 #endif
347 
time_now()348   static double time_now() {
349     struct timeval now;
350     gettimeofday(&now, /* tzp = */ 0);
351     return now.tv_sec + now.tv_usec / 1000000.0;
352   }
353 
replace_all(std::string & str,const std::string & from,const std::string & to)354   static void replace_all(std::string& str, const std::string& from,
355 		   const std::string& to) {
356     if (from.empty()) return;
357 
358     size_t start_pos = 0;
359 
360     while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
361       str.replace(start_pos, from.length(), to);
362       start_pos += to.length();
363     }
364   }
365 
366 public:
367 
vague_dt(double seconds)368   static std::string vague_dt(double seconds) {
369     double minutes = seconds / 60;
370     double hours = minutes / 60;
371     double days = hours / 24;
372     double years = days / 365.25;
373 
374     std::stringstream buffer;
375 
376     buffer << std::setw(2);
377 
378     if (seconds < 50) {
379       buffer << round(seconds) << "s";
380     } else if (minutes < 50) {
381       buffer << round(minutes) << "m";
382     } else if (hours < 18) {
383       buffer << round(hours) << "h";
384     } else if (days < 30) {
385       buffer << round(days) << "d";
386     } else if (days < 335) {
387       buffer << round(days/30) << "M";
388     } else {
389       buffer << round(years) << "y";
390     }
391 
392     return buffer.str();
393   }
394 
pretty_bytes(double rate)395   static std::string pretty_bytes(double rate) {
396 
397     errno = 0;
398     long bytes = lround(rate);
399     if (errno == ERANGE) {
400       bytes = LONG_MAX;
401     }
402 
403     if (bytes == 0) { return "0B"; }
404 
405     std::string units[] = { "B", "kB", "MB", "GB", "TB", "PB", "EB",
406 			    "ZB", "YB" };
407     long int num_units = (long int)(sizeof(units) / sizeof(units[0]));
408     double idx = std::floor(std::log(bytes) / std::log(1000.0));
409     if (idx >= num_units) { idx = num_units - 1; }
410 
411     double res = round(bytes / std::pow(1000.0, idx) * 100.0) / 100.0;
412     std::stringstream buffer;
413 
414     buffer.precision(2);
415     buffer << std::fixed << res << units[(long) idx];
416     return buffer.str();
417   }
418 
is_option_enabled()419   static bool is_option_enabled() {
420     SEXP opt = PROTECT(Rf_GetOption1(Rf_install("progress_enabled")));
421     if (Rf_isNull(opt)) {
422       UNPROTECT(1);
423       return true;
424     }
425     Rboolean t = R_compute_identical(opt, Rf_ScalarLogical(1), 16);
426     UNPROTECT(1);
427     return t;
428   }
429 
430 }; // class RProgress
431 
432 }  // namespace RProgress
433 
434 #endif
435