require 'puppet/concurrent/thread_local_singleton'
module Puppet::Pops
module Time
NSECS_PER_USEC = 1000
NSECS_PER_MSEC = NSECS_PER_USEC * 1000
NSECS_PER_SEC = NSECS_PER_MSEC * 1000
NSECS_PER_MIN = NSECS_PER_SEC * 60
NSECS_PER_HOUR = NSECS_PER_MIN * 60
NSECS_PER_DAY = NSECS_PER_HOUR * 24
KEY_STRING = 'string'.freeze
KEY_FORMAT = 'format'.freeze
KEY_NEGATIVE = 'negative'.freeze
KEY_DAYS = 'days'.freeze
KEY_HOURS = 'hours'.freeze
KEY_MINUTES = 'minutes'.freeze
KEY_SECONDS = 'seconds'.freeze
KEY_MILLISECONDS = 'milliseconds'.freeze
KEY_MICROSECONDS = 'microseconds'.freeze
KEY_NANOSECONDS = 'nanoseconds'.freeze
# TimeData is a Numeric that stores its value internally as nano-seconds but will be considered to be seconds and fractions of
# seconds when used in arithmetic or comparison with other Numeric types.
#
class TimeData < Numeric
include LabelProvider
attr_reader :nsecs
def initialize(nanoseconds)
@nsecs = nanoseconds
end
def <=>(o)
case o
when self.class
@nsecs <=> o.nsecs
when Integer
to_int <=> o
when Float
to_f <=> o
else
nil
end
end
def label(o)
Utils.name_to_segments(o.class.name).last
end
# @return [Float] the number of seconds
def to_f
@nsecs.fdiv(NSECS_PER_SEC)
end
# @return [Integer] the number of seconds with fraction part truncated
def to_int
@nsecs / NSECS_PER_SEC
end
def to_i
to_int
end
# @return [Complex] short for `#to_f.to_c`
def to_c
to_f.to_c
end
# @return [Rational] initial numerator is nano-seconds and denominator is nano-seconds per second
def to_r
Rational(@nsecs, NSECS_PER_SEC)
end
undef_method :phase, :polar, :rect, :rectangular
end
class Timespan < TimeData
def self.from_fields(negative, days, hours, minutes, seconds, milliseconds = 0, microseconds = 0, nanoseconds = 0)
ns = (((((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000 + milliseconds) * 1000 + microseconds) * 1000 + nanoseconds
new(negative ? -ns : ns)
end
def self.from_hash(hash)
hash.include?('string') ? from_string_hash(hash) : from_fields_hash(hash)
end
def self.from_string_hash(hash)
parse(hash[KEY_STRING], hash[KEY_FORMAT] || Format::DEFAULTS)
end
def self.from_fields_hash(hash)
from_fields(
hash[KEY_NEGATIVE] || false,
hash[KEY_DAYS] || 0,
hash[KEY_HOURS] || 0,
hash[KEY_MINUTES] || 0,
hash[KEY_SECONDS] || 0,
hash[KEY_MILLISECONDS] || 0,
hash[KEY_MICROSECONDS] || 0,
hash[KEY_NANOSECONDS] || 0)
end
def self.parse(str, format = Format::DEFAULTS)
if format.is_a?(::Array)
format.each do |fmt|
fmt = FormatParser.singleton.parse_format(fmt) unless fmt.is_a?(Format)
begin
return fmt.parse(str)
rescue ArgumentError
end
end
raise ArgumentError, _("Unable to parse '%{str}' using any of the formats %{formats}") % { str: str, formats: format.join(', ') }
end
format = FormatParser.singleton.parse_format(format) unless format.is_a?(Format)
format.parse(str)
end
# @return [true] if the stored value is negative
def negative?
@nsecs < 0
end
def +(o)
case o
when Timestamp
Timestamp.new(@nsecs + o.nsecs)
when Timespan
Timespan.new(@nsecs + o.nsecs)
when Integer, Float
# Add seconds
Timespan.new(@nsecs + (o * NSECS_PER_SEC).to_i)
else
raise ArgumentError, _("%{klass} cannot be added to a Timespan") % { klass: a_an_uc(o) } unless o.is_a?(Timespan)
end
end
def -(o)
case o
when Timespan
Timespan.new(@nsecs - o.nsecs)
when Integer, Float
# Subtract seconds
Timespan.new(@nsecs - (o * NSECS_PER_SEC).to_i)
else
raise ArgumentError, _("%{klass} cannot be subtracted from a Timespan") % { klass: a_an_uc(o) }
end
end
def -@
Timespan.new(-@nsecs)
end
def *(o)
case o
when Integer, Float
Timespan.new((@nsecs * o).to_i)
else
raise ArgumentError, _("A Timestamp cannot be multiplied by %{klass}") % { klass: a_an(o) }
end
end
def divmod(o)
case o
when Integer
to_i.divmod(o)
when Float
to_f.divmod(o)
else
raise ArgumentError, _("Can not do modulus on a Timespan using a %{klass}") % { klass: a_an(o) }
end
end
def modulo(o)
divmod(o)[1]
end
def %(o)
modulo(o)
end
def div(o)
case o
when Timespan
# Timespan/Timespan yields a Float
@nsecs.fdiv(o.nsecs)
when Integer, Float
Timespan.new(@nsecs.div(o))
else
raise ArgumentError, _("A Timespan cannot be divided by %{klass}") % { klass: a_an(o) }
end
end
def /(o)
div(o)
end
# @return [Integer] a positive integer denoting the number of days
def days
total_days
end
# @return [Integer] a positive integer, 0 - 23 denoting hours of day
def hours
total_hours % 24
end
# @return [Integer] a positive integer, 0 - 59 denoting minutes of hour
def minutes
total_minutes % 60
end
# @return [Integer] a positive integer, 0 - 59 denoting seconds of minute
def seconds
total_seconds % 60
end
# @return [Integer] a positive integer, 0 - 999 denoting milliseconds of second
def milliseconds
total_milliseconds % 1000
end
# @return [Integer] a positive integer, 0 - 999.999.999 denoting nanoseconds of second
def nanoseconds
total_nanoseconds % NSECS_PER_SEC
end
# Formats this timestamp into a string according to the given `format`
#
# @param [String,Format] format The format to use when producing the string
# @return [String] the string representing the formatted timestamp
# @raise [ArgumentError] if the format is a string with illegal format characters
# @api public
def format(format)
format = FormatParser.singleton.parse_format(format) unless format.is_a?(Format)
format.format(self)
end
# Formats this timestamp into a string according to {Format::DEFAULTS[0]}
#
# @return [String] the string representing the formatted timestamp
# @api public
def to_s
format(Format::DEFAULTS[0])
end
def to_hash(compact = false)
result = {}
n = nanoseconds
if compact
s = total_seconds
result[KEY_SECONDS] = negative? ? -s : s
result[KEY_NANOSECONDS] = negative? ? -n : n unless n == 0
else
add_unless_zero(result, KEY_DAYS, days)
add_unless_zero(result, KEY_HOURS, hours)
add_unless_zero(result, KEY_MINUTES, minutes)
add_unless_zero(result, KEY_SECONDS, seconds)
unless n == 0
add_unless_zero(result, KEY_NANOSECONDS, n % 1000)
n /= 1000
add_unless_zero(result, KEY_MICROSECONDS, n % 1000)
add_unless_zero(result, KEY_MILLISECONDS, n / 1000)
end
result[KEY_NEGATIVE] = true if negative?
end
result
end
def add_unless_zero(result, key, value)
result[key] = value unless value == 0
end
private :add_unless_zero
# @api private
def total_days
total_nanoseconds / NSECS_PER_DAY
end
# @api private
def total_hours
total_nanoseconds / NSECS_PER_HOUR
end
# @api private
def total_minutes
total_nanoseconds / NSECS_PER_MIN
end
# @api private
def total_seconds
total_nanoseconds / NSECS_PER_SEC
end
# @api private
def total_milliseconds
total_nanoseconds / NSECS_PER_MSEC
end
# @api private
def total_microseconds
total_nanoseconds / NSECS_PER_USEC
end
# @api private
def total_nanoseconds
@nsecs.abs
end
# Represents a compiled Timestamp format. The format is used both when parsing a timestamp
# in string format and when producing a string from a timestamp instance.
#
class Format
# A segment is either a string that will be represented literally in the formatted timestamp
# or a value that corresponds to one of the possible format characters.
class Segment
def append_to(bld, ts)
raise NotImplementedError, "'#{self.class.name}' should implement #append_to"
end
def append_regexp(bld, ts)
raise NotImplementedError, "'#{self.class.name}' should implement #append_regexp"
end
def multiplier
raise NotImplementedError, "'#{self.class.name}' should implement #multiplier"
end
end
class LiteralSegment < Segment
def append_regexp(bld)
bld << "(#{Regexp.escape(@literal)})"
end
def initialize(literal)
@literal = literal
end
def append_to(bld, ts)
bld << @literal
end
def concat(codepoint)
@literal.concat(codepoint)
end
def nanoseconds
0
end
end
class ValueSegment < Segment
def initialize(padchar, width, default_width)
@use_total = false
@padchar = padchar
@width = width
@default_width = default_width
@format = create_format
end
def create_format
case @padchar
when nil
'%d'
when ' '
"%#{@width || @default_width}d"
else
"%#{@padchar}#{@width || @default_width}d"
end
end
def append_regexp(bld)
if @width.nil?
case @padchar
when nil
bld << (use_total? ? '([0-9]+)' : "([0-9]{1,#{@default_width}})")
when '0'
bld << (use_total? ? '([0-9]+)' : "([0-9]{1,#{@default_width}})")
else
bld << (use_total? ? '\s*([0-9]+)' : "([0-9\\s]{1,#{@default_width}})")
end
else
case @padchar
when nil
bld << "([0-9]{1,#{@width}})"
when '0'
bld << "([0-9]{#{@width}})"
else
bld << "([0-9\\s]{#{@width}})"
end
end
end
def nanoseconds(group)
group.to_i * multiplier
end
def multiplier
0
end
def set_use_total
@use_total = true
end
def use_total?
@use_total
end
def append_value(bld, n)
bld << sprintf(@format, n)
end
end
class DaySegment < ValueSegment
def initialize(padchar, width)
super(padchar, width, 1)
end
def multiplier
NSECS_PER_DAY
end
def append_to(bld, ts)
append_value(bld, ts.days)
end
end
class HourSegment < ValueSegment
def initialize(padchar, width)
super(padchar, width, 2)
end
def multiplier
NSECS_PER_HOUR
end
def append_to(bld, ts)
append_value(bld, use_total? ? ts.total_hours : ts.hours)
end
end
class MinuteSegment < ValueSegment
def initialize(padchar, width)
super(padchar, width, 2)
end
def multiplier
NSECS_PER_MIN
end
def append_to(bld, ts)
append_value(bld, use_total? ? ts.total_minutes : ts.minutes)
end
end
class SecondSegment < ValueSegment
def initialize(padchar, width)
super(padchar, width, 2)
end
def multiplier
NSECS_PER_SEC
end
def append_to(bld, ts)
append_value(bld, use_total? ? ts.total_seconds : ts.seconds)
end
end
# Class that assumes that leading zeroes are significant and that trailing zeroes are not and left justifies when formatting.
# Applicable after a decimal point, and hence to the %L and %N formats.
class FragmentSegment < ValueSegment
def nanoseconds(group)
# Using %L or %N to parse a string only makes sense when they are considered to be fractions. Using them
# as a total quantity would introduce ambiguities.
raise ArgumentError, _('Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude') if use_total?
n = group.to_i
p = 9 - group.length
p <= 0 ? n : n * 10 ** p
end
def create_format
if @padchar.nil?
'%d'
else
"%-#{@width || @default_width}d"
end
end
def append_value(bld, n)
# Strip trailing zeroes when default format is used
n = n.to_s.sub(/\A([0-9]+?)0*\z/, '\1').to_i unless use_total? || @padchar == '0'
super(bld, n)
end
end
class MilliSecondSegment < FragmentSegment
def initialize(padchar, width)
super(padchar, width, 3)
end
def multiplier
NSECS_PER_MSEC
end
def append_to(bld, ts)
append_value(bld, use_total? ? ts.total_milliseconds : ts.milliseconds)
end
end
class NanoSecondSegment < FragmentSegment
def initialize(padchar, width)
super(padchar, width, 9)
end
def multiplier
width = @width || @default_width
if width < 9
10 ** (9 - width)
else
1
end
end
def append_to(bld, ts)
ns = ts.total_nanoseconds
width = @width || @default_width
if width < 9
# Truncate digits to the right, i.e. let %6N reflect microseconds
ns /= 10 ** (9 - width)
ns %= 10 ** width unless use_total?
else
ns %= NSECS_PER_SEC unless use_total?
end
append_value(bld, ns)
end
end
def initialize(format, segments)
@format = format.freeze
@segments = segments.freeze
end
def format(timespan)
bld = timespan.negative? ? '-' : ''
@segments.each { |segment| segment.append_to(bld, timespan) }
bld
end
def parse(timespan)
md = regexp.match(timespan)
raise ArgumentError, _("Unable to parse '%{timespan}' using format '%{format}'") % { timespan: timespan, format: @format } if md.nil?
nanoseconds = 0
md.captures.each_with_index do |group, index|
segment = @segments[index]
next if segment.is_a?(LiteralSegment)
group.lstrip!
raise ArgumentError, _("Unable to parse '%{timespan}' using format '%{format}'") % { timespan: timespan, format: @format } unless group =~ /\A[0-9]+\z/
nanoseconds += segment.nanoseconds(group)
end
Timespan.new(timespan.start_with?('-') ? -nanoseconds : nanoseconds)
end
def to_s
@format
end
private
def regexp
@regexp ||= build_regexp
end
def build_regexp
bld = '\A-?'
@segments.each { |segment| segment.append_regexp(bld) }
bld << '\z'
Regexp.new(bld)
end
end
# Parses a string into a Timestamp::Format instance
class FormatParser
extend Puppet::Concurrent::ThreadLocalSingleton
def initialize
@formats = Hash.new { |hash, str| hash[str] = internal_parse(str) }
end
def parse_format(format)
@formats[format]
end
private
NSEC_MAX = 0
MSEC_MAX = 1
SEC_MAX = 2
MIN_MAX = 3
HOUR_MAX = 4
DAY_MAX = 5
SEGMENT_CLASS_BY_ORDINAL = [
Format::NanoSecondSegment, Format::MilliSecondSegment, Format::SecondSegment, Format::MinuteSegment, Format::HourSegment, Format::DaySegment
]
def bad_format_specifier(format, start, position)
_("Bad format specifier '%{expression}' in '%{format}', at position %{position}") % { expression: format[start,position-start], format: format, position: position }
end
def append_literal(bld, codepoint)
if bld.empty? || !bld.last.is_a?(Format::LiteralSegment)
bld << Format::LiteralSegment.new(''.concat(codepoint))
else
bld.last.concat(codepoint)
end
end
# States used by the #internal_parser function
STATE_LITERAL = 0 # expects literal or '%'
STATE_PAD = 1 # expects pad, width, or format character
STATE_WIDTH = 2 # expects width, or format character
def internal_parse(str)
bld = []
raise ArgumentError, _('Format must be a String') unless str.is_a?(String)
highest = -1
state = STATE_LITERAL
padchar = '0'
width = nil
position = -1
fstart = 0
str.codepoints do |codepoint|
position += 1
if state == STATE_LITERAL
if codepoint == 0x25 # '%'
state = STATE_PAD
fstart = position
padchar = '0'
width = nil
else
append_literal(bld, codepoint)
end
next
end
case codepoint
when 0x25 # '%'
append_literal(bld, codepoint)
state = STATE_LITERAL
when 0x2D # '-'
raise ArgumentError, bad_format_specifier(str, fstart, position) unless state == STATE_PAD
padchar = nil
state = STATE_WIDTH
when 0x5F # '_'
raise ArgumentError, bad_format_specifier(str, fstart, position) unless state == STATE_PAD
padchar = ' '
state = STATE_WIDTH
when 0x44 # 'D'
highest = DAY_MAX
bld << Format::DaySegment.new(padchar, width)
state = STATE_LITERAL
when 0x48 # 'H'
highest = HOUR_MAX unless highest > HOUR_MAX
bld << Format::HourSegment.new(padchar, width)
state = STATE_LITERAL
when 0x4D # 'M'
highest = MIN_MAX unless highest > MIN_MAX
bld << Format::MinuteSegment.new(padchar, width)
state = STATE_LITERAL
when 0x53 # 'S'
highest = SEC_MAX unless highest > SEC_MAX
bld << Format::SecondSegment.new(padchar, width)
state = STATE_LITERAL
when 0x4C # 'L'
highest = MSEC_MAX unless highest > MSEC_MAX
bld << Format::MilliSecondSegment.new(padchar, width)
state = STATE_LITERAL
when 0x4E # 'N'
highest = NSEC_MAX unless highest > NSEC_MAX
bld << Format::NanoSecondSegment.new(padchar, width)
state = STATE_LITERAL
else # only digits allowed at this point
raise ArgumentError, bad_format_specifier(str, fstart, position) unless codepoint >= 0x30 && codepoint <= 0x39
if state == STATE_PAD && codepoint == 0x30
padchar = '0'
else
n = codepoint - 0x30
if width.nil?
width = n
else
width = width * 10 + n
end
end
state = STATE_WIDTH
end
end
raise ArgumentError, bad_format_specifier(str, fstart, position) unless state == STATE_LITERAL
unless highest == -1
hc = SEGMENT_CLASS_BY_ORDINAL[highest]
bld.find { |s| s.instance_of?(hc) }.set_use_total
end
Format.new(str, bld)
end
end
class Format
DEFAULTS = ['%D-%H:%M:%S.%-N', '%H:%M:%S.%-N', '%M:%S.%-N', '%S.%-N', '%D-%H:%M:%S', '%H:%M:%S', '%D-%H:%M', '%S'].map { |str| FormatParser.singleton.parse_format(str) }
end
end
end
end
Copyright 2K16 - 2K18 Indonesian Hacker Rulez