Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xbrl unit #4

Merged
merged 5 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions unit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package xbrl

import "strings"

// Unit specifies the unit in which a numeric fact has been measured.
// A Unit can be either a simple measure, product of measures, or a ratio of products of measures with a numerator and a denominator.
//
// A simple unit that represents shares looks like:
// <unit>
// <measure>shares</measure>
// </unit>
//
// Numeric Facts reference units by ID via the Fact's `unitRef` attribute.
// https://www.xbrl.org/Specification/XBRL-2.1/REC-2003-12-31/XBRL-2.1-REC-2003-12-31+corrected-errata-2013-02-20.html#_4.8
type Unit struct {
ID string `xml:"id,attr"`
Measures Measures `xml:"measure"`
Divide *Divide `xml:"divide"`
}

// Divide represents a ratios of Units that has a numerator and a denominator.
// For example, XBRL can represent a complex unit like earnings per share (EPS) as dollars per share (USD / share):
// <unit>
// <divide>
// <unitNumerator>
// <measure>iso4127:USD</measure>
// </unitNumerator>
// <unitDenominator>
// <measure>shares</measure>
// </unitDenominator>
// </divide>
// </unit>
//
// https://www.xbrl.org/Specification/XBRL-2.1/REC-2003-12-31/XBRL-2.1-REC-2003-12-31+corrected-errata-2013-02-20.html#_4.8.2
type Divide struct {
Numerator Measures `xml:"unitNumerator>measure"`
Denominator Measures `xml:"unitDenominator>measure"`
}

// Measure represents a unit of measure. The element value can be xml namespaced (xsd:Qname) or as plain text.
// XML namespaced: <measure>iso4217:USD</measure>
// plain text: <measure>shares</measure>
//
// Note that if the value is XML namespaced, the namespace should be declared in the XML, but this parser does not validate that.
// https://www.xbrl.org/Specification/XBRL-2.1/REC-2003-12-31/XBRL-2.1-REC-2003-12-31+corrected-errata-2013-02-20.html#_4.8.2
type Measure struct {
Value string `xml:",chardata"`
}

type Measures []Measure

// String returns a human readable representation of the Unit.
func (u Unit) String() string {
// If the Divide element is not nil, there can be no top-level Meaures.
if u.Divide != nil {
return u.Divide.Numerator.String() + " / " + u.Divide.Denominator.String()
}

// If the divider element is nil, there must be 1+ top-level Measures.
return u.Measures.String()
}

// String returns the local name of the measure if the value is formatted as 'xsd:Qname', otherwise the value itself is returned.
// Ex: `<measure>iso4127:USD</measure>` -> "USD"
// `<measure>shares</measure>` -> "shares"
func (m Measure) String() string {
if index := strings.IndexRune(m.Value, ':'); index != -1 && index < len(m.Value) {
return m.Value[index+1 : len(m.Value)]
}

return m.Value
}

// String returns a human readable representation of the product of all the `Measure`s in this slice.
func (m Measures) String() string {
// More than one Measure implies multiplication.
var builder strings.Builder
for index, measure := range m {
if index > 0 {
builder.WriteString(" * ")
}

builder.WriteString(measure.String())
}

return builder.String()
}
108 changes: 108 additions & 0 deletions unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package xbrl

import (
"encoding/xml"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUnmarshalUnit(t *testing.T) {
t.Run("simple unit", func(t *testing.T) {
// language=xml
unitXML := `<unit>
<measure>shares</measure>
</unit>`

var unit Unit
require.NoError(t, xml.Unmarshal([]byte(unitXML), &unit))

require.Len(t, unit.Measures, 1)
assert.Equal(t, "shares", unit.Measures[0].Value)
assert.Equal(t, "shares", unit.Measures[0].String())
assert.Nil(t, unit.Divide)

assert.Equal(t, unit.String(), "shares")
})

t.Run("product of measures", func(t *testing.T) {
// language=xml
unitXML := `<unit>
<measure>myns:feet</measure>
<measure>myns:feet</measure>
</unit>`

var unit Unit
require.NoError(t, xml.Unmarshal([]byte(unitXML), &unit))

require.Len(t, unit.Measures, 2)
assert.Equal(t, "myns:feet", unit.Measures[0].Value)
assert.Equal(t, "feet", unit.Measures[0].String())

assert.Equal(t, "myns:feet", unit.Measures[1].Value)
assert.Equal(t, "feet", unit.Measures[1].String())

assert.Nil(t, unit.Divide)

assert.Equal(t, "feet * feet", unit.String())
})

t.Run("ratio of simple measures", func(t *testing.T) {
// language=xml
unitXML := `<unit>
<divide>
<unitNumerator>
<measure>iso4127:USD</measure>
</unitNumerator>
<unitDenominator>
<measure>shares</measure>
</unitDenominator>
</divide>
</unit>`

var unit Unit
require.NoError(t, xml.Unmarshal([]byte(unitXML), &unit))

require.Len(t, unit.Measures, 0)

assert.NotNil(t, unit.Divide)
assert.Len(t, unit.Divide.Numerator, 1)
assert.Equal(t, "iso4127:USD", unit.Divide.Numerator[0].Value)

assert.Len(t, unit.Divide.Denominator, 1)
assert.Equal(t, "shares", unit.Divide.Denominator[0].Value)

assert.Equal(t, "USD / shares", unit.String())
})

t.Run("ratio of products of measures", func(t *testing.T) {
// language=xml
unitXML := `<unit>
<divide>
<unitNumerator>
<measure>iso4127:USD</measure>
</unitNumerator>
<unitDenominator>
<measure>myns:feet</measure>
<measure>myns:feet</measure>
</unitDenominator>
</divide>
</unit>`

var unit Unit
require.NoError(t, xml.Unmarshal([]byte(unitXML), &unit))

require.Len(t, unit.Measures, 0)

assert.NotNil(t, unit.Divide)
assert.Len(t, unit.Divide.Numerator, 1)
assert.Equal(t, "iso4127:USD", unit.Divide.Numerator[0].Value)

assert.Len(t, unit.Divide.Denominator, 2)
assert.Equal(t, "myns:feet", unit.Divide.Denominator[0].Value)
assert.Equal(t, "myns:feet", unit.Divide.Denominator[1].Value)

assert.Equal(t, "USD / feet * feet", unit.String())
})
}
1 change: 1 addition & 0 deletions xbrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package xbrl

type XBRL struct {
Contexts []Context `xml:"context"`
Units []Unit `xml:"unit"`
}