Module: Vishnu::StandardTime

Defined in:
lib/vishnu/standard_time.rb,
lib/vishnu/standard_time/cli.rb,
lib/vishnu/standard_time/railtie.rb,
lib/vishnu/standard_time/rails_helpers.rb

Defined Under Namespace

Modules: RailsHelpers Classes: CLI, Duration, FocusSession, Parts, Plan, Railtie

Constant Summary collapse

VERSION =
"0.2.0"
VST_SPEC_VERSION =
VERSION
STANDARD_NAME =
"Vishnu Standard Time"
WEBSITE_URL =
"https://time.vishnu.store"
VST_EPOCH_UNIX_MS =
1_767_225_600_000
MS_PER_DAY =
86_400_000
FLOWS_PER_SECOND =
3
FLOWS_PER_MINUTE =
180
FLOWS_PER_HOUR =
10_800
FLOWS_PER_DAY =
259_200
FLOW_LENGTH_MS_TEXT =
"333⅓"
VICKS_PER_DAY =

Legacy/compat display units kept for interop with early VST builds.

100_000
MS_PER_VICK =
864

Class Method Summary collapse

Class Method Details

.after(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil, from: local_now) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/vishnu/standard_time.rb', line 390

def after(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil, from: local_now)
  ms = unix_ms_for(from) + duration_ms_for(
    flows: flows,
    seconds: seconds,
    minutes: minutes,
    hours: hours,
    days: days,
    milliseconds: milliseconds
  )

  from.respond_to?(:source) && from.source == "local" ? local_now(Time.at(ms / 1000.0)) : from_unix_ms(ms)
end

.comma6(value) ⇒ Object



270
271
272
# File 'lib/vishnu/standard_time.rb', line 270

def comma6(value)
  format_number(value, pad: true)
end

.duration(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/vishnu/standard_time.rb', line 351

def duration(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
  ms = duration_ms_for(
    flows: flows,
    seconds: seconds,
    minutes: minutes,
    hours: hours,
    days: days,
    milliseconds: milliseconds
  )
  Duration.new(
    flows: flows || flows_for(milliseconds: ms),
    milliseconds: ms,
    seconds: ms / 1000.0
  )
end

.duration_ms_for(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil) ⇒ Object



340
341
342
343
344
345
346
347
348
349
# File 'lib/vishnu/standard_time.rb', line 340

def duration_ms_for(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
  ms = 0.0
  ms += flows.to_f * 1000.0 / FLOWS_PER_SECOND if flows
  ms += seconds.to_f * 1000 if seconds
  ms += minutes.to_f * 60_000 if minutes
  ms += hours.to_f * 3_600_000 if hours
  ms += days.to_f * MS_PER_DAY if days
  ms += milliseconds.to_f if milliseconds
  ms.positive? ? ms.ceil : ms.floor
end

.flows_elapsed_today(parts = local_now) ⇒ Object



306
307
308
# File 'lib/vishnu/standard_time.rb', line 306

def flows_elapsed_today(parts = local_now)
  Integer(parts.flow)
end

.flows_for(seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil) ⇒ Object



318
319
320
321
322
323
324
325
326
# File 'lib/vishnu/standard_time.rb', line 318

def flows_for(seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
  total_seconds = 0.0
  total_seconds += seconds.to_f if seconds
  total_seconds += minutes.to_f * 60 if minutes
  total_seconds += hours.to_f * 3600 if hours
  total_seconds += days.to_f * 86_400 if days
  total_seconds += milliseconds.to_f / 1000 if milliseconds
  (total_seconds * FLOWS_PER_SECOND).round
end

.flows_left_today(parts = local_now) ⇒ Object



310
311
312
# File 'lib/vishnu/standard_time.rb', line 310

def flows_left_today(parts = local_now)
  [FLOWS_PER_DAY - flows_elapsed_today(parts), 0].max
end

.focus(flows: nil, seconds: nil, minutes: nil, hours: nil, label: nil, started_at: local_now) ⇒ Object



418
419
420
421
# File 'lib/vishnu/standard_time.rb', line 418

def focus(flows: nil, seconds: nil, minutes: nil, hours: nil, label: nil, started_at: local_now)
  total = flows || flows_for(seconds: seconds, minutes: minutes, hours: hours)
  FocusSession.new(total_flows: total, label: label, started_at: started_at)
end

.format_flow_count(parts) ⇒ Object



274
275
276
# File 'lib/vishnu/standard_time.rb', line 274

def format_flow_count(parts)
  "#{comma6(parts.flow)} / #{format_number(FLOWS_PER_DAY)} flows"
end

.format_flow_of_minute(parts) ⇒ Object



278
279
280
# File 'lib/vishnu/standard_time.rb', line 278

def format_flow_of_minute(parts)
  "FLOW #{parts.flow_of_minute.to_s.rjust(3, "0")} / 179"
end

.format_flows_left(parts = local_now) ⇒ Object



290
291
292
# File 'lib/vishnu/standard_time.rb', line 290

def format_flows_left(parts = local_now)
  "#{format_number(flows_left_today(parts))} flows left today"
end

.format_number(value, pad: false) ⇒ Object



263
264
265
266
267
268
# File 'lib/vishnu/standard_time.rb', line 263

def format_number(value, pad: false)
  s = Integer(value).abs.to_s
  s = s.rjust(6, "0") if pad
  grouped = s.reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
  Integer(value).negative? ? "-#{grouped}" : grouped
end

.format_percent_of_day(parts, digits = 2) ⇒ Object



282
283
284
# File 'lib/vishnu/standard_time.rb', line 282

def format_percent_of_day(parts, digits = 2)
  "%0#{digits + 3}.#{digits}f%% OF DAY" % parts.percent_of_day
end

.format_percent_remaining(parts, digits = 2) ⇒ Object



286
287
288
# File 'lib/vishnu/standard_time.rb', line 286

def format_percent_remaining(parts, digits = 2)
  "%0#{digits + 3}.#{digits}f%% DAY REMAINING" % percent_remaining(parts)
end

.format_prompt(parts = local_now) ⇒ Object



302
303
304
# File 'lib/vishnu/standard_time.rb', line 302

def format_prompt(parts = local_now)
  "VST #{comma6(parts.flow)}/#{format_number(FLOWS_PER_DAY)} #{format('%.2f%%', parts.percent_of_day)}"
end

.format_vst(parts) ⇒ Object



294
295
296
# File 'lib/vishnu/standard_time.rb', line 294

def format_vst(parts)
  "VST #{format_vst_day(parts)} · #{format_flow_count(parts)} · #{format_percent_of_day(parts)}"
end

.format_vst_day(parts) ⇒ Object



258
259
260
261
# File 'lib/vishnu/standard_time.rb', line 258

def format_vst_day(parts)
  sign = parts.day.negative? ? "-" : "+"
  "D#{sign}#{parts.day.abs.to_s.rjust(6, "0")}" 
end

.format_vst_precise(parts) ⇒ Object



298
299
300
# File 'lib/vishnu/standard_time.rb', line 298

def format_vst_precise(parts)
  "VST #{format_vst_day(parts)} · #{format_flow_of_minute(parts)} · #{format_flow_count(parts)} · #{format_percent_of_day(parts)}"
end

.from_day_ms(day, day_ms) ⇒ Object



212
213
214
# File 'lib/vishnu/standard_time.rb', line 212

def from_day_ms(day, day_ms)
  parts_from_day_ms(Integer(day), Integer(day_ms))
end

.from_unix_ms(unix_ms) ⇒ Object



206
207
208
209
210
# File 'lib/vishnu/standard_time.rb', line 206

def from_unix_ms(unix_ms)
  ms = Integer(unix_ms)
  day, day_ms = (ms - VST_EPOCH_UNIX_MS).divmod(MS_PER_DAY)
  parts_from_day_ms(day, day_ms, unix_ms: ms, source: "standard")
end

.hours_for(flows:) ⇒ Object



336
337
338
# File 'lib/vishnu/standard_time.rb', line 336

def hours_for(flows:)
  seconds_for(flows: flows) / 3600.0
end

.local_now(time = Time.now) ⇒ Object



239
240
241
242
243
244
245
# File 'lib/vishnu/standard_time.rb', line 239

def local_now(time = Time.now)
  day_ms = ((time.hour * 60 + time.min) * 60 + time.sec) * 1000 + (time.usec / 1000)
  local_midnight_as_utc = Time.utc(time.year, time.month, time.day)
  epoch = Time.at(VST_EPOCH_UNIX_MS / 1000, in: "+00:00")
  day = ((local_midnight_as_utc - epoch) / 86_400).floor
  parts_from_day_ms(day, day_ms, unix_ms: (time.to_f * 1000).floor, source: "local")
end

.minutes_for(flows:) ⇒ Object



332
333
334
# File 'lib/vishnu/standard_time.rb', line 332

def minutes_for(flows:)
  seconds_for(flows: flows) / 60.0
end

.nowObject



235
236
237
# File 'lib/vishnu/standard_time.rb', line 235

def now
  from_unix_ms((Time.now.to_f * 1000).floor)
end

.parse_duration(input) ⇒ Object

Raises:

  • (ArgumentError)


367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/vishnu/standard_time.rb', line 367

def parse_duration(input)
  text = input.to_s.strip.downcase
  raise ArgumentError, "duration is required" if text.empty?

  case text
  when /\A([+-]?\d+(?:\.\d+)?)\s*(f|flow|flows)?\z/
    flows = Regexp.last_match(1).to_f
    duration(flows: flows)
  when /\A([+-]?\d+(?:\.\d+)?)\s*(ms|millisecond|milliseconds)\z/
    duration(milliseconds: Regexp.last_match(1).to_f)
  when /\A([+-]?\d+(?:\.\d+)?)\s*(s|sec|secs|second|seconds)\z/
    duration(seconds: Regexp.last_match(1).to_f)
  when /\A([+-]?\d+(?:\.\d+)?)\s*(m|min|mins|minute|minutes)\z/
    duration(minutes: Regexp.last_match(1).to_f)
  when /\A([+-]?\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)\z/
    duration(hours: Regexp.last_match(1).to_f)
  when /\A([+-]?\d+(?:\.\d+)?)\s*(d|day|days)\z/
    duration(days: Regexp.last_match(1).to_f)
  else
    raise ArgumentError, "unknown duration '#{input}'. Try 4500, 4500f, 25m, 90s, 1h."
  end
end

.parts_from_day_ms(day, day_ms, unix_ms: nil, source: "standard") ⇒ Object

Raises:

  • (ArgumentError)


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/vishnu/standard_time.rb', line 216

def parts_from_day_ms(day, day_ms, unix_ms: nil, source: "standard")
  day_ms = Integer(day_ms)
  raise ArgumentError, "day_ms must be from 0 through 86399999" if day_ms.negative? || day_ms >= MS_PER_DAY

  flow = (day_ms * FLOWS_PER_DAY) / MS_PER_DAY
  Parts.new(
    day: Integer(day),
    day_ms: day_ms,
    flow: flow,
    flow_of_minute: flow % FLOWS_PER_MINUTE,
    second_of_day: day_ms / 1000,
    percent_of_day: (day_ms.to_f / MS_PER_DAY) * 100,
    vick: day_ms / MS_PER_VICK,
    pulse: day_ms % MS_PER_VICK,
    unix_ms: unix_ms,
    source: source
  )
end

.percent_remaining(parts = local_now) ⇒ Object



314
315
316
# File 'lib/vishnu/standard_time.rb', line 314

def percent_remaining(parts = local_now)
  [100.0 - parts.percent_of_day.to_f, 0.0].max
end

.plan(&block) ⇒ Object



423
424
425
426
427
# File 'lib/vishnu/standard_time.rb', line 423

def plan(&block)
  p = Plan.new
  p.instance_eval(&block) if block
  p
end

.seconds_for(flows:) ⇒ Object



328
329
330
# File 'lib/vishnu/standard_time.rb', line 328

def seconds_for(flows:)
  flows.to_f / FLOWS_PER_SECOND
end

.stamp(message = nil, parts = local_now) ⇒ Object



429
430
431
432
# File 'lib/vishnu/standard_time.rb', line 429

def stamp(message = nil, parts = local_now)
  suffix = message.to_s.strip
  suffix.empty? ? "[#{format_vst(parts)}]" : "[#{format_vst(parts)}] #{suffix}"
end


202
203
204
# File 'lib/vishnu/standard_time.rb', line 202

def standard_link
  "#{STANDARD_NAME}#{WEBSITE_URL}"
end

.svg_badge(parts = local_now, width: 560, height: 32) ⇒ Object



434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/vishnu/standard_time.rb', line 434

def svg_badge(parts = local_now, width: 560, height: 32)
  text = CGI.escapeHTML(format_vst(parts))
  url = CGI.escapeHTML(WEBSITE_URL)
  <<~SVG.strip
    <svg xmlns="http://www.w3.org/2000/svg" width="#{Integer(width)}" height="#{Integer(height)}" role="img" aria-label="#{text}">
      <title>#{text}</title>
      <a href="#{url}" target="_blank">
        <rect width="100%" height="100%" rx="6"/>
        <text x="12" y="21" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="white">#{text}</text>
      </a>
    </svg>
  SVG
end

.to_h(parts) ⇒ Object



448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/vishnu/standard_time.rb', line 448

def to_h(parts)
  {
    scale: "VST",
    version: VST_SPEC_VERSION,
    day: parts.day.to_s,
    flow: parts.flow,
    flow_of_minute: parts.flow_of_minute,
    flows_per_day: FLOWS_PER_DAY,
    flows_left_today: flows_left_today(parts),
    percent_of_day: parts.percent_of_day.round(6),
    percent_remaining: percent_remaining(parts).round(6),
    text: format_vst(parts),
    precise_text: format_vst_precise(parts),
    day_ms: parts.day_ms.to_s,
    vick: parts.vick,
    pulse: parts.pulse,
    website_url: WEBSITE_URL,
    standard_link: standard_link
  }
end

.to_unix_ms(parts) ⇒ Object



254
255
256
# File 'lib/vishnu/standard_time.rb', line 254

def to_unix_ms(parts)
  VST_EPOCH_UNIX_MS + Integer(parts.day) * MS_PER_DAY + Integer(parts.day_ms)
end

.unix_ms_for(parts) ⇒ Object



247
248
249
250
251
252
# File 'lib/vishnu/standard_time.rb', line 247

def unix_ms_for(parts)
  return Integer(parts) if parts.is_a?(Integer)
  return Integer(parts.unix_ms) if parts.respond_to?(:unix_ms) && parts.unix_ms

  to_unix_ms(parts)
end

.until_flow(target_flow, from: local_now) ⇒ Object

Raises:

  • (ArgumentError)


403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/vishnu/standard_time.rb', line 403

def until_flow(target_flow, from: local_now)
  target = Integer(target_flow)
  raise ArgumentError, "target flow must be from 0 through #{FLOWS_PER_DAY - 1}" if target.negative? || target >= FLOWS_PER_DAY

  current = Integer(from.flow)
  flow_delta = target >= current ? target - current : FLOWS_PER_DAY - current + target
  ms = duration_ms_for(flows: flow_delta)
  Duration.new(
    flows: flow_delta,
    milliseconds: ms,
    seconds: ms / 1000.0,
    target: after(flows: flow_delta, from: from)
  )
end

.website_urlObject



198
199
200
# File 'lib/vishnu/standard_time.rb', line 198

def website_url
  WEBSITE_URL
end