Here’s what I hope is my final cut at a int2sexagesimal filter. It occurred to me that, besides “fixing” MAC addresses mangled by YAML 1.1, base 60 grouping is only useful for <hours>:<minutes>:<seconds>, or maybe <degrees>:<arc-minutes>:<arc-seconds>. For other conversions you need to be able to specify a list of bases. This version allows that. The EXAMPLES section includes converting seconds to ['years', 'days', 'hours', 'minutes', 'seconds'] and teaspoons to ['gallons', 'quarts', 'pints', 'cups', 'tablespoons', 'teaspoons']. I’m sure the lack of that capability has been slowing down the uptake of Ansible, so there’s that excuse gone.
Provide whatever list of bases fits your data.
Not all solutions have a pressing problem. But if you happen to bump into a problem for which this is a solution, you are welcome to it! Happy Filtering!
#!/usr/bin/python
# filter_plugins/int2sexagesimal.py
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = '''
name: int2sexagesimal
author: Todd Lewis (https://forum.ansible.com/u/utoddl/summary)
type: filter
short_description: convert integer to sexagesimal string
version_added: "2.18"
description:
- |
This filter plugin converts an integer to the equivalent sexagesimal
string. Sexagesimals consist of decimal integers joined by colons. All but
the first such integer must be less than the corresponding base (60 by
default) and left-padded with zeros to a length equal to the length of
string representing O(base - 1). The first joined integer may be shorter,
longer, or that same length, possibly left-padded with zeros if both
O(minparts) and O(maxparts) are specified and equal, but it may be longer
and larger than the O(base) if necessary to satisfy the O(maxparts) constraint.
- |
Commonly used for converting C(seconds) to C(<hours>:<minutes>:<seconds>),
C(int2sexagesimal) also can be useful to recover a MAC address which was
inadvertently interpreted as a sexagesimal integer constant by YAML 1.1.
For example, V(mac: 11:33:22:55:44:11) is equivalent to V(mac: 8986232651) in YAML 1.1.
- |
If the input integer is negative, a C(-) will be prefixed to the resulting
string.
- |
A non-integer input value is returned unchanged. This simplifies filtering
MAC addresses which may have been mangled by YAML 1.1 as described
above: V({{ macs | map('int2sexagesimal', minparts=6, maxparts=6\\) }})
Setting minparts equal to maxparts ensures the first part is zero-padded.
options:
maxparts:
description:
- The maximum number of colon-delimited parts in the resulting string
representation. For example, the result C(143:14:30) has three parts.
type: integer
required: false
minparts:
description:
- The minimum number of colon-delimited parts in the resulting
string representation.
type: integer
required: false
base:
description:
- The value at which a corresponding part's value carries over to the
next leftward part.
- Does not apply to the left-most part if O(maxparts) is specified and
reached. For example, the result C(60:59:59) represents 60 hours, 59
minutes, 59 seconds with the default O(base) of 60 and O(maxparts=3).
Without setting O(maxparts) the result would be C(01:00:59:59).
- O(base) can also be a list of integers. For example O(base=[100, 365, 24, 60, 60])
with O(maxparts=5) would convert seconds to
C(<years>:<days>:<hours>:<minutes>:<seconds>) and the left-most
base value would not be used. But with O(maxparts=6)
and a large enough input value the result would include a leading
C(<millenium>:) part.
- If more parts are generated than specified in the O(base) list, the
left-most (or only) O(base) value is used repeatedly.
type: integer or list of integers
required: false
default: 60
_value:
description:
- The integer to convert to a sexagesimal string, or any other non-integer.
A non-integer O(_value) is returned unchanged.
type: raw
require: true
'''
EXAMPLES = '''
- name: Restore mangled MAC address YAML 1.1 constant
ansible.builtin.debug:
msg: '{{ 8986232651 | int2sexagesimal() }}'
# TASK [Restore mangled MAC address YAML 1.1 constant] ******************************
# ok: [localhost] =>
# msg: '11:33:22:55:44:11'
- name: Display converted integer 3753 with various parameters
ansible.builtin.debug:
msg:
- "# 1 hour, 2 minutes, 33 seconds"
- "3753 | int2sexagesimal() => {{ '%11s' | format(3753 | int2sexagesimal()) }}"
- "3753 | int2sexagesimal(minparts=2) => {{ '%11s' | format(3753 | int2sexagesimal(minparts=2)) }}"
- "3753 | int2sexagesimal(maxparts=2) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=2)) }}"
- "3753 | int2sexagesimal(maxparts=2, minparts=2) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=2, minparts=2)) }}"
- ""
- "3753 | int2sexagesimal(minparts=3) => {{ '%11s' | format(3753 | int2sexagesimal(minparts=3)) }}"
- "3753 | int2sexagesimal(maxparts=3) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=3)) }}"
- "3753 | int2sexagesimal(maxparts=3, minparts=3) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=3, minparts=3)) }}"
- ""
- "3753 | int2sexagesimal(minparts=4) => {{ '%11s' | format(3753 | int2sexagesimal(minparts=4)) }}"
- "3753 | int2sexagesimal(maxparts=4) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=4)) }}"
- "3753 | int2sexagesimal(maxparts=4, minparts=4) => {{ '%11s' | format(3753 | int2sexagesimal(maxparts=4, minparts=4)) }}"
# TASK [Display converted integer 3753 with various parameters] *********************
# ok: [localhost] =>
# msg:
# - '# 1 hour, 2 minutes, 33 seconds'
# - 3753 | int2sexagesimal() => 01:02:33
# - 3753 | int2sexagesimal(minparts=2) => 01:02:33
# - 3753 | int2sexagesimal(maxparts=2) => 62:33
# - 3753 | int2sexagesimal(maxparts=2, minparts=2) => 62:33
# - ''
# - 3753 | int2sexagesimal(minparts=3) => 01:02:33
# - 3753 | int2sexagesimal(maxparts=3) => 1:02:33
# - 3753 | int2sexagesimal(maxparts=3, minparts=3) => 01:02:33
# - ''
# - 3753 | int2sexagesimal(minparts=4) => 00:01:02:33
# - 3753 | int2sexagesimal(maxparts=4) => 01:02:33
# - 3753 | int2sexagesimal(maxparts=4, minparts=4) => 00:01:02:33
- name: Convert large number of seconds with different bases
ansible.builtin.debug:
msg:
- "# 10749 years, 66 days, 18 hour, 24 minutes, 11 seconds"
- "{{ 338986232651 | int2sexagesimal(maxparts=5, base=bases) }}"
vars:
bases:
- 100 # years / century
- 365 # days / year
- 24 # hours / day
- 60 # minutes / hour
- 60 # seconds / minute
# TASK [Convert large number of seconds with different bases] ***********************
# ok: [localhost] =>
# msg:
# - '# 10749 years, 66 days, 18 hour, 24 minutes, 11 seconds'
# - 10749:066:18:24:11
- name: Same conversion as above, but create a dict to label the parts
ansible.builtin.debug:
msg:
- "{{ dict(['years', 'days', 'hours', 'minutes', 'seconds']
| zip(338986232651
| int2sexagesimal(maxparts=5, minparts=5, base=[100, 365, 24, 60, 60])
| split(':')
| map('regex_replace', '^0+([0-9]+)', '\\1')
)
) }}"
# TASK [Same conversion as above, but create a dict to label the parts] *************
# ok: [localhost] =>
# msg:
# - days: '66'
# hours: '18'
# minutes: '24'
# seconds: '11'
# years: '10749'
- name: Convert 3041 teaspoons to gallons, quarts, pints, cups, tablespoons, teaspoons
ansible.builtin.debug:
msg:
- - 3041 teaspoons as a colon-delimited string
- "{{ 3041 | int2sexagesimal(maxparts=6, minparts=6, base=[4, 2, 2, 16, 3]) }}"
- - 3041 teaspoons as a dict
- "{{ dict(['gallons', 'quarts', 'pints', 'cups', 'tablespoons', 'teaspoons']
| zip(3041
| int2sexagesimal(maxparts=6, minparts=6, base=[4, 2, 2, 16, 3])
| split(':')
| map('regex_replace', '^0+([0-9]+)', '\\1')
)
) }}"
#TASK [Convert 3041 teaspoons to gallons, quarts, pints, cups, tablespoons, teaspoons] ****
#ok: [localhost] =>
# msg:
# - - 3041 teaspoons as a colon-delimited string
# - '3:3:1:1:05:2'
# - - 3041 teaspoons as a dict
# - cups: '1'
# gallons: '3'
# pints: '1'
# quarts: '3'
# tablespoons: '5'
# teaspoons: '2'
'''
from ansible.module_utils.common.text.converters import to_text
from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleFilterTypeError
def _flatten(terms):
ret = []
for term in terms:
if isinstance(term, (list, tuple)):
ret.extend(term)
else:
ret.append(term)
return ret
def int2sexagesimal(value, **kwargs):
if not isinstance(value, int):
return value
minparts = kwargs.get('minparts', None)
maxparts = kwargs.get('maxparts', None)
base = kwargs.get('base', 60)
if minparts and (not isinstance(minparts, int) or minparts < 2):
raise AnsibleFilterError(f'if specified, minparts must be an int > 1')
if maxparts and (not isinstance(maxparts, int) or maxparts < 2):
raise AnsibleFilterError(f'if specified, maxparts must be an int > 1')
if minparts and maxparts and (minparts > maxparts):
raise AnsibleFilterError(f'if both are specified, minparts must be less than maxparts')
if not base:
base = 60
bases = _flatten([base])
for b in bases:
if not isinstance(b, int) or b < 2:
raise AnsibleFilterError(f'base must consist of integers > 1')
if value < 0:
sign = "-"
value = abs(value)
else:
sign = ""
acc = []
while not maxparts or len(acc) < maxparts:
base = bases[-1]
if len(bases) > 1:
bases.pop()
partfmt = "0" + f"0{len(f"{base-1}")}d"
if maxparts and len(acc) == maxparts - 1:
if minparts and minparts == maxparts:
# zero-pad if minparts == maxparts
acc.insert(0, f"{value:{partfmt}}")
else:
acc.insert(0, f"{value}")
break
acc.insert(0, f"{value % base:{partfmt}}")
value //= base
if value == 0:
break
if minparts:
while len(acc) < minparts:
base = bases[-1]
if len(bases) > 1:
bases.pop()
partfmt = "0" + f"0{len(f"{base-1}")}d"
acc.insert(0, f'{0:{partfmt}}')
result_string = to_text(f"{sign}{':'.join(acc)}")
return result_string
class FilterModule(object):
''' Ansible core jinja2 filters '''
def filters(self):
return {'int2sexagesimal': int2sexagesimal}
if __name__ == "__main__":
for val in [ 86399, 3600, -500, 36125, 32000000, 8986232651, 338986232651, 216000, 'a string']:
for ma in [None, 3, 5, 6]:
for mi in [None, 4, 5, 6]:
for ba in [None, [100, 365, 24, 60, 60]]:
if ma and mi and mi > ma:
break
print(f"{str(val):8s}: min:{str(mi):5s}, max:{str(ma):5s}, base:{str(ba):24s}) => {int2sexagesimal(val, minparts=mi, maxparts=ma, base=ba):17s}")
# print(f" 86399: {int2sexagesimal( 86399 )}")
# print(f" 86399: {int2sexagesimal( 86399, minparts=2)}")
# print(f" 86399: {int2sexagesimal( 86399, maxparts=3)}")
# print(f" 3600: {int2sexagesimal( 3600 )}")
# print(f" -500: {int2sexagesimal( -500 )}")
# print(f" 36125: {int2sexagesimal( 36125, minparts=3)}")
# print(f"8986232651: {int2sexagesimal(8986232651, minparts=3)}")
# print(f" 216000: {int2sexagesimal( 216000, minparts=3)}")
# print(f"'a string': {int2sexagesimal('a string', minparts=8)}")